2026-05-22 00:33:43 +08:00
|
|
|
|
"""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
|
2026-06-08 11:16:28 +08:00
|
|
|
|
from app.infrastructure.perception.base_event_store import BaseEventStore
|
2026-05-22 00:33:43 +08:00
|
|
|
|
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,
|
2026-06-08 11:16:28 +08:00
|
|
|
|
event_store: BaseEventStore,
|
2026-05-22 00:33:43 +08:00
|
|
|
|
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,
|
2026-05-26 20:21:31 +08:00
|
|
|
|
"doc_title": chunk.doc_title,
|
2026-05-22 00:33:43 +08:00
|
|
|
|
"score": round(float(chunk.score), 4),
|
2026-05-26 20:21:31 +08:00
|
|
|
|
"snippet": (chunk.text or "")[:180],
|
2026-05-22 00:33:43 +08:00
|
|
|
|
"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 = [
|
2026-05-26 20:21:31 +08:00
|
|
|
|
f"[文档{i}: {c.doc_title}]\n{(c.text or '')[:400]}"
|
2026-05-22 00:33:43 +08:00
|
|
|
|
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": "{}"}
|