feat(perception): backend - mock event store, perception service, /perception API routes

This commit is contained in:
2026-05-22 00:33:43 +08:00
parent 09f9cf2bf0
commit f9ee644f25
7 changed files with 647 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ from .documents import router as documents_router
from .knowledge import router as knowledge_router
from .agent import router as agent_router
from .status import router as status_router
from .perception import router as perception_router
# Keep package boundaries explicit so backend imports stay predictable.
@@ -18,6 +19,7 @@ api_router.include_router(knowledge_router)
api_router.include_router(agent_router)
api_router.include_router(compliance_router)
api_router.include_router(status_router)
api_router.include_router(perception_router)
__all__ = [
"api_router",
@@ -26,4 +28,5 @@ __all__ = [
"agent_router",
"compliance_router",
"status_router",
"perception_router",
]

View File

@@ -0,0 +1,67 @@
"""Define API routes for perception (regulatory intelligence)."""
from __future__ import annotations
import json
from fastapi import APIRouter, Query
from fastapi.responses import StreamingResponse
from app.shared.bootstrap import get_perception_service
from app.shared.async_utils import iter_in_thread
router = APIRouter(prefix="/perception", tags=["智能感知"])
@router.get("/stats")
async def get_perception_stats():
"""Return KPI statistics for the perception dashboard."""
return get_perception_service().get_stats()
@router.get("/events")
async def list_events(
source: str | None = Query(default=None, description="来源筛选 (MIIT/UN-ECE/ISO/国标委/EUR-Lex/IATF)"),
impact_level: str | None = Query(default=None, description="影响等级 (high/medium/low)"),
limit: int = Query(default=50, ge=1, le=100),
):
"""Return regulatory events with optional filters."""
events = get_perception_service().list_events(
source=source,
impact_level=impact_level,
limit=limit,
)
return {"events": events, "total": len(events)}
@router.get("/events/{event_id}")
async def get_event(event_id: str):
"""Return a single regulatory event by ID."""
event = get_perception_service().get_event(event_id)
if event is None:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail=f"Event {event_id} not found")
return event
@router.post("/events/{event_id}/analyze")
async def analyze_event(event_id: str):
"""Stream SSE impact analysis for a regulatory event."""
service = get_perception_service()
async def event_stream():
async for item in iter_in_thread(service.analyze_event(event_id)):
event_name = item.get("event", "message")
data = item.get("data", "")
if isinstance(data, (dict, list)):
data = json.dumps(data, ensure_ascii=False)
yield f"event: {event_name}\ndata: {data}\n\n"
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)

View File

@@ -0,0 +1 @@
"""Perception application package."""

View File

@@ -0,0 +1,143 @@
"""Perception application service — event listing and streaming impact analysis."""
from __future__ import annotations
import json
from typing import Generator
from app.application.knowledge.services import KnowledgeRetrievalService
from app.infrastructure.perception.mock_event_store import MockEventStore
from app.services.llm.llm_factory import get_llm_client
from app.config.settings import settings
_ANALYSIS_SYSTEM_PROMPT = (
"你是汽车行业法规合规专家专注于中国国家标准GB、国际法规UN-ECE、ISO"
"及欧盟法规EUR-Lex的解读与合规建议。回答需专业、简洁、结构清晰。"
)
class PerceptionService:
"""Orchestrate regulatory event queries and streaming impact analysis."""
def __init__(
self,
event_store: MockEventStore,
retrieval_service: KnowledgeRetrievalService,
) -> None:
self._store = event_store
self._retrieval = retrieval_service
# ------------------------------------------------------------------
# Queries
# ------------------------------------------------------------------
def list_events(
self,
*,
source: str | None = None,
impact_level: str | None = None,
limit: int = 50,
) -> list[dict]:
return self._store.filter(source=source, impact_level=impact_level, limit=limit)
def get_event(self, event_id: str) -> dict | None:
return self._store.get(event_id)
def get_stats(self) -> dict:
return self._store.stats()
# ------------------------------------------------------------------
# Streaming analysis
# ------------------------------------------------------------------
def analyze_event(self, event_id: str) -> Generator[dict, None, None]:
"""Yield SSE-ready dicts: sources → content chunks → done."""
event = self._store.get(event_id)
if not event:
yield {"event": "error", "data": f"事件 {event_id} 不存在"}
return
# --- 1. RAG retrieval: find related library documents ---
query = event["title"] + " " + " ".join(event["tags"])
chunks: list = []
affected_docs: list[dict] = []
try:
chunks = self._retrieval.retrieve(query=query, top_k=5)
seen: set[str] = set()
for chunk in chunks:
if chunk.doc_id not in seen:
seen.add(chunk.doc_id)
affected_docs.append(
{
"doc_id": chunk.doc_id,
"doc_name": chunk.doc_name,
"score": round(float(chunk.score), 4),
"snippet": (chunk.content or "")[:180],
"clause": getattr(chunk, "section_title", "") or "",
}
)
except Exception: # noqa: BLE001
pass
yield {"event": "sources", "data": json.dumps(affected_docs, ensure_ascii=False)}
# --- 2. Build context from retrieved chunks ---
context_parts = [
f"[文档{i}: {c.doc_name}]\n{(c.content or '')[:400]}"
for i, c in enumerate(chunks[:5], 1)
]
context = "\n\n".join(context_parts) if context_parts else "(知识库中暂无相关文档)"
# --- 3. Build prompt ---
effective = event.get("effective_at") or "待定"
user_content = f"""请对以下法规动态进行专业影响分析。
【法规动态】
标准编号:{event['standard_code']}
标题:{event['title']}
来源:{event['source_label']}
摘要:{event['summary']}
生效日期:{effective}
分类:{event['category']}
关键词:{', '.join(event['tags'])}
【知识库关联文档】
{context}
请用 Markdown 格式,从以下四个维度进行分析:
## 核心变化
列出本次法规更新最关键的 3-5 项变化(用 - 列表)
## 业务影响
分析对现有产品、认证流程、技术文档的具体影响
## 整改建议
给出优先级排序的行动清单(标注 🔴高 🟡中 🟢低 优先级)
## 时间节点
关键合规时间表与里程碑提醒"""
messages = [
{"role": "system", "content": _ANALYSIS_SYSTEM_PROMPT},
{"role": "user", "content": user_content},
]
# --- 4. Stream LLM response ---
try:
client = get_llm_client(
provider=settings.llm_provider,
model=settings.llm_model,
)
if hasattr(client, "stream_chat"):
for chunk in client.stream_chat(messages):
yield {"event": "content", "data": chunk}
else:
response = client.chat(messages)
yield {"event": "content", "data": response.content or ""}
except Exception as exc: # noqa: BLE001
yield {"event": "error", "data": str(exc)}
return
yield {"event": "done", "data": "{}"}

View File

@@ -0,0 +1 @@
"""Perception infrastructure package."""

View File

@@ -0,0 +1,421 @@
"""Mock regulatory event store with 20 high-quality pre-seeded events."""
from __future__ import annotations
from typing import Any
MOCK_EVENTS: list[dict[str, Any]] = [
# ------------------------------------------------------------------ HIGH
{
"id": "evt-001",
"source": "MIIT",
"source_label": "工业和信息化部",
"standard_code": "GB 18384-2025",
"title": "《电动汽车安全要求》国家标准第三版正式发布",
"summary": (
"新增 IP67 级别高压系统密封防护要求;热失控预警响应时间压缩至 5 分钟;"
"调整碰撞安全测试工况,新增侧柱碰工况。本标准于 2026 年 7 月 1 日强制实施。"
),
"impact_level": "high",
"published_at": "2025-11-15",
"effective_at": "2026-07-01",
"category": "电动汽车安全",
"tags": ["电池安全", "高压防护", "碰撞安全", "热失控"],
"source_url": "https://openstd.samr.gov.cn",
"status": "enacted",
},
{
"id": "evt-002",
"source": "UN-ECE",
"source_label": "联合国欧洲经委会",
"standard_code": "UN R155 Amendment 3",
"title": "UN-ECE R155 网络安全法规第三次修订正式生效",
"summary": (
"新增对 OTA空中升级全生命周期的安全审计要求强化车辆 TARA"
"(威胁分析与风险评估)文档化义务;扩展 CSMS 监控范围至售后服务商。"
),
"impact_level": "high",
"published_at": "2026-01-20",
"effective_at": "2026-07-01",
"category": "网络安全",
"tags": ["OTA", "网络安全", "CSMS", "TARA", "R155"],
"source_url": "https://unece.org/transport/vehicle-regulations",
"status": "enacted",
},
{
"id": "evt-003",
"source": "MIIT",
"source_label": "工业和信息化部",
"standard_code": "GB/T 40429-2026征求意见稿",
"title": "《汽车整车信息安全技术要求》修订征求意见",
"summary": (
"增加基于人工智能的异常行为检测要求;新增车云通信双向认证机制规范;"
"提出数据最小化原则在车辆 OBD 数据收集中的应用细则。"
),
"impact_level": "high",
"published_at": "2026-03-05",
"effective_at": None,
"category": "信息安全",
"tags": ["信息安全", "数据安全", "AI检测", "OBD"],
"source_url": "https://www.miit.gov.cn/",
"status": "draft",
},
{
"id": "evt-004",
"source": "MIIT",
"source_label": "工业和信息化部",
"standard_code": "NEV 双积分 2026",
"title": "2026 年度新能源汽车双积分管理办法年度调整",
"summary": (
"纯电动乘用车标准车型积分CAFC基准值上调 8%"
"提高 A 级及以上续航里程门槛;新增氢燃料电池商用车积分计算细则。"
),
"impact_level": "high",
"published_at": "2026-02-28",
"effective_at": "2026-04-01",
"category": "新能源政策",
"tags": ["双积分", "纯电动", "燃料电池", "碳配额"],
"source_url": "https://www.miit.gov.cn/",
"status": "enacted",
},
{
"id": "evt-017",
"source": "MIIT",
"source_label": "工业和信息化部",
"standard_code": "智能网联汽车准入管理办法实施细则",
"title": "智能网联汽车准入管理实施细则正式落地",
"summary": (
"明确 L3 及以上自动驾驶功能的准入申报路径;"
"要求 OEM 建立数据安全管理体系并完成等保 2.0 三级认证;"
"道路测试数据留存期延长至 3 年。"
),
"impact_level": "high",
"published_at": "2026-03-01",
"effective_at": "2026-09-01",
"category": "智能网联",
"tags": ["智能网联", "L3自动驾驶", "准入管理", "数据留存"],
"source_url": "https://www.miit.gov.cn/",
"status": "enacted",
},
{
"id": "evt-018",
"source": "EUR-Lex",
"source_label": "欧盟官方公报",
"standard_code": "EU Cyber Resilience Act (CRA)",
"title": "《欧盟网络韧性法案》核心条款对车联网设备生效",
"summary": (
"联网汽车 ECU 须满足 CRA「重要类 II」安全要求"
"强制 SBOM软件物料清单公开披露"
"OEM 须提供至少 10 年的漏洞修复支持承诺。"
),
"impact_level": "high",
"published_at": "2026-02-15",
"effective_at": "2027-01-01",
"category": "网络安全",
"tags": ["CRA", "SBOM", "漏洞管理", "网络韧性"],
"source_url": "https://eur-lex.europa.eu",
"status": "enacted",
},
# --------------------------------------------------------------- MEDIUM
{
"id": "evt-005",
"source": "UN-ECE",
"source_label": "联合国欧洲经委会",
"standard_code": "UN R156 Amendment 2",
"title": "UN-ECE R156 软件升级与 SUMS 法规补充修订",
"summary": (
"明确 SUMS软件更新管理系统对 ECU 版本追溯的最低保留年限为 15 年;"
"新增售后 OTA 推送的用户知情同意要求规范。"
),
"impact_level": "medium",
"published_at": "2026-01-10",
"effective_at": "2026-07-01",
"category": "软件升级",
"tags": ["OTA", "SUMS", "软件版本", "R156"],
"source_url": "https://unece.org/transport/vehicle-regulations",
"status": "enacted",
},
{
"id": "evt-006",
"source": "国标委",
"source_label": "国家标准化管理委员会",
"standard_code": "GB/T 35273-2026",
"title": "《信息安全技术 个人信息安全规范》更新版发布",
"summary": (
"将车内人脸识别、声纹采集列为敏感个人信息;"
"补充自动驾驶场景下乘员行为数据的去标识化技术规范;"
"强化数据出境安全评估触发阈值。"
),
"impact_level": "medium",
"published_at": "2025-12-01",
"effective_at": "2026-06-01",
"category": "数据安全",
"tags": ["个人信息", "PIPL", "数据安全", "生物识别"],
"source_url": "https://openstd.samr.gov.cn",
"status": "enacted",
},
{
"id": "evt-007",
"source": "EUR-Lex",
"source_label": "欧盟官方公报",
"standard_code": "EU AI Act — Art.13 & Art.14",
"title": "《欧盟人工智能法案》第13-14条透明度与人工监督条款正式生效",
"summary": (
"要求在汽车 ADAS 系统中植入 AI 使用记录日志;"
"驾驶员监控 AI 系统须披露决策逻辑;"
"高风险 AI 系统需提供人工干预接口。"
),
"impact_level": "medium",
"published_at": "2026-02-01",
"effective_at": "2026-08-01",
"category": "AI 法规",
"tags": ["AI法案", "透明度", "ADAS", "高风险AI"],
"source_url": "https://eur-lex.europa.eu",
"status": "enacted",
},
{
"id": "evt-008",
"source": "ISO",
"source_label": "国际标准化组织",
"standard_code": "ISO 45001:2025 Amd.1",
"title": "ISO 45001 职业健康安全管理体系第一次修正",
"summary": (
"新增心理健康风险纳入 OHS 危害辨识范围;"
"明确远程办公人员安全管理职责;"
"更新绩效评价指标体系,新增事故未遂事件统计要求。"
),
"impact_level": "medium",
"published_at": "2025-10-20",
"effective_at": "2026-01-01",
"category": "EHS 管理",
"tags": ["ISO 45001", "EHS", "职业健康", "安全管理"],
"source_url": "https://www.iso.org",
"status": "enacted",
},
{
"id": "evt-009",
"source": "MIIT",
"source_label": "工业和信息化部",
"standard_code": "GB/T 28001-2026征求意见",
"title": "《汽车产品安全召回管理规程》修订征求意见",
"summary": (
"扩展召回触发条件,将 OTA 推送导致的功能异常纳入强制报告范围;"
"缩短重大安全隐患召回启动时限至 15 个工作日。"
),
"impact_level": "medium",
"published_at": "2026-03-15",
"effective_at": None,
"category": "召回管理",
"tags": ["召回", "OTA", "安全隐患", "产品安全"],
"source_url": "https://www.miit.gov.cn/",
"status": "draft",
},
{
"id": "evt-010",
"source": "国标委",
"source_label": "国家标准化管理委员会",
"standard_code": "GB 38031-2025",
"title": "《电动汽车用动力蓄电池安全要求》修订版发布",
"summary": (
"新增电池系统针刺、浸水、挤压等极端工况测试程序;"
"热扩散防护等级要求升级;"
"强化 BMS电池管理系统状态监测数据记录要求。"
),
"impact_level": "medium",
"published_at": "2025-09-15",
"effective_at": "2026-03-01",
"category": "电池安全",
"tags": ["动力电池", "BMS", "热扩散", "安全测试"],
"source_url": "https://openstd.samr.gov.cn",
"status": "enacted",
},
{
"id": "evt-016",
"source": "UN-ECE",
"source_label": "联合国欧洲经委会",
"standard_code": "UN R100 Rev.4(草案)",
"title": "UN R100 电动汽车安全认证法规第四次修订草案发布",
"summary": (
"拟对 400V 以上高压系统的绝缘电阻监测提出实时 CAN 总线传输要求;"
"新增极低温工况(-40°C的电池性能验证程序。"
),
"impact_level": "medium",
"published_at": "2026-04-08",
"effective_at": None,
"category": "电动汽车安全",
"tags": ["R100", "高压安全", "绝缘监测", "低温性能"],
"source_url": "https://unece.org/transport/vehicle-regulations",
"status": "draft",
},
{
"id": "evt-019",
"source": "ISO",
"source_label": "国际标准化组织",
"standard_code": "ISO/SAE 21434:2026 Amd.1",
"title": "ISO/SAE 21434 汽车网络安全工程第一次修正",
"summary": (
"将 AI 推理组件纳入汽车网络安全工程范围;"
"补充端到端加密通信在 V2X 场景中的 TARA 建模要求;"
"新增第三方 ECU 供应商 CSMS 审计方法。"
),
"impact_level": "medium",
"published_at": "2026-04-10",
"effective_at": "2026-10-01",
"category": "网络安全",
"tags": ["ISO 21434", "网络安全", "V2X", "AI安全"],
"source_url": "https://www.iso.org",
"status": "enacted",
},
# ------------------------------------------------------------------ LOW
{
"id": "evt-011",
"source": "ISO",
"source_label": "国际标准化组织",
"standard_code": "ISO 26262:2026 Ed.3(征求意见)",
"title": "ISO 26262 功能安全第三版征求意见启动",
"summary": (
"拟新增对 AI/ML 组件功能安全验证方法的指导附录;"
"讨论 SOTIF预期功能安全与 ISO 26262 的协调融合路径。"
),
"impact_level": "low",
"published_at": "2026-04-01",
"effective_at": None,
"category": "功能安全",
"tags": ["功能安全", "ASIL", "AI安全", "SOTIF"],
"source_url": "https://www.iso.org",
"status": "consultation",
},
{
"id": "evt-012",
"source": "EUR-Lex",
"source_label": "欧盟官方公报",
"standard_code": "REACH Regulation Update 2026",
"title": "欧盟 REACH 法规限制物质清单更新(第 22 批)",
"summary": (
"新增 3 种 SVHCs高度关注物质包括特定阻燃剂和密封材料成分"
"汽车零部件豁免条款调整,影响部分内饰材料供应商。"
),
"impact_level": "low",
"published_at": "2026-01-30",
"effective_at": "2026-09-01",
"category": "环保法规",
"tags": ["REACH", "SVHCs", "环保", "化学品管理"],
"source_url": "https://eur-lex.europa.eu",
"status": "enacted",
},
{
"id": "evt-013",
"source": "MIIT",
"source_label": "工业和信息化部",
"standard_code": "CCER 汽车碳配额 2026",
"title": "自愿减排CCER汽车行业核算方法学更新",
"summary": (
"更新纯电动汽车全生命周期碳排放核算边界;"
"新增动力电池回收环节碳减排量认定方法;"
"与全国碳市场对接的企业碳账户数据接口规范发布。"
),
"impact_level": "low",
"published_at": "2026-02-10",
"effective_at": "2026-06-01",
"category": "碳排放",
"tags": ["CCER", "碳排放", "碳中和", "碳核算"],
"source_url": "https://www.miit.gov.cn/",
"status": "enacted",
},
{
"id": "evt-014",
"source": "IATF",
"source_label": "国际汽车工作组",
"standard_code": "IATF 16949:2025 CSR 通告",
"title": "IATF 16949 质量管理体系客户特殊要求更新通告",
"summary": (
"多家主机厂OEM同步更新 CSR涵盖软件定义汽车SDV"
"场景下的质量过程管控;电子电气 BOM 变更管理流程补充规范。"
),
"impact_level": "low",
"published_at": "2026-03-20",
"effective_at": "2026-07-01",
"category": "质量管理",
"tags": ["IATF 16949", "质量管理", "SDV", "CSR"],
"source_url": "https://www.iatfglobaloversight.org",
"status": "enacted",
},
{
"id": "evt-015",
"source": "国标委",
"source_label": "国家标准化管理委员会",
"standard_code": "GB 7258-2025 勘误",
"title": "《机动车运行安全技术条件》年度勘误发布",
"summary": (
"更正第 12 章灯光系统技术要求中的参数引用错误;"
"澄清前雾灯安装位置尺寸定义;此次为勘误性修订,不影响已认证车型。"
),
"impact_level": "low",
"published_at": "2026-01-05",
"effective_at": "2026-01-05",
"category": "运行安全",
"tags": ["GB 7258", "灯光", "运行安全", "勘误"],
"source_url": "https://openstd.samr.gov.cn",
"status": "enacted",
},
{
"id": "evt-020",
"source": "国标委",
"source_label": "国家标准化管理委员会",
"standard_code": "GB/T 27930-2026",
"title": "《电动汽车非车载传导式充电通信协议》更新版发布",
"summary": (
"兼容 CHAdeMO 4.0 与 CCS2 双协议栈;"
"新增大功率充电(>350kW通信握手流程"
"强化充电过程 BMS 实时诊断数据上报规范。"
),
"impact_level": "low",
"published_at": "2026-03-25",
"effective_at": "2026-12-01",
"category": "充电标准",
"tags": ["充电协议", "BMS", "大功率充电", "CHAdeMO"],
"source_url": "https://openstd.samr.gov.cn",
"status": "enacted",
},
]
# Index for fast lookup
_EVENT_INDEX: dict[str, dict] = {e["id"]: e for e in MOCK_EVENTS}
class MockEventStore:
"""In-memory mock store for regulatory events."""
def all(self) -> list[dict]:
return list(MOCK_EVENTS)
def get(self, event_id: str) -> dict | None:
return _EVENT_INDEX.get(event_id)
def filter(
self,
*,
source: str | None = None,
impact_level: str | None = None,
limit: int = 50,
) -> list[dict]:
events = list(MOCK_EVENTS)
if source:
events = [e for e in events if e["source"] == source]
if impact_level:
events = [e for e in events if e["impact_level"] == impact_level]
events.sort(key=lambda e: e["published_at"], reverse=True)
return events[:limit]
def stats(self) -> dict:
from datetime import date, timedelta
events = MOCK_EVENTS
cutoff = (date.today() - timedelta(days=90)).isoformat()
return {
"total": len(events),
"high_impact": sum(1 for e in events if e["impact_level"] == "high"),
"medium_impact": sum(1 for e in events if e["impact_level"] == "medium"),
"low_impact": sum(1 for e in events if e["impact_level"] == "low"),
"recent_90d": sum(1 for e in events if e["published_at"] >= cutoff),
}

View File

@@ -23,6 +23,8 @@ from app.infrastructure.vectorstore.bm25_retriever import BM25Retriever
from app.infrastructure.vectorstore.dense_retriever import DenseRetriever
from app.infrastructure.vectorstore.milvus_vector_index import MilvusVectorIndex
from app.infrastructure.vectorstore.cross_encoder_reranker import OpenAICompatibleReranker
from app.infrastructure.perception.mock_event_store import MockEventStore
from app.application.perception.services import PerceptionService
# Keep shared wiring centralized so dependency construction remains consistent.
@@ -151,3 +153,12 @@ def get_agent_conversation_service() -> AgentConversationService:
answer_generator=OpenAICompatibleAnswerGenerator(),
conversation_store=get_conversation_store(),
)
@lru_cache
def get_perception_service() -> PerceptionService:
"""Return perception service for regulatory intelligence."""
return PerceptionService(
event_store=MockEventStore(),
retrieval_service=get_retrieval_service(),
)