- fields_registry: FORM_TO_BUSINESS_KEY bidirectional mapping between the graph's six form keys (has_injury...) and the acd business keys (ywrysw...); bjrjs/ywmtc have no form source (set_info only); business_patch_from_form maps graph diffs to whitelisted business keys and filters internal/unknown keys - app_service: chat injection now maps repository business keys back to form keys; both complete() and stream() write back field diffs (FormUpdate events captured in stream, closing the skeleton gap); form keys never leak into the business repository - docs/assignment6-compat.md: mapping table documented, stream write-back backlog item resolved - tests: mapping coverage, write-back mapping end-to-end, reverse mapping injection (bjrjs/ywmtc excluded), stream FormUpdate write-back Verified: 205 unit tests
166 lines
6.9 KiB
Python
166 lines
6.9 KiB
Python
"""事故处理应用服务(作业 6-1):四业务接口共享同一份业务状态。
|
||
|
||
职责边界(对应计划作业 6):
|
||
- set_info / get_info / delete_session **不调用 LLM、不创建辅助对话**
|
||
(对照旧 FastGPT 实现的“辅助对话 + 删记录”hack);
|
||
- chat 每轮从 repository 读最新业务字段注入图(set_info 写入后
|
||
下一轮 chat 必须可见),图产出字段差量后写回 repository;
|
||
- 未知 key 兼容策略:get 对未知 key 返回 JSON 空串 '""'(现状兼容);
|
||
set 对未知 key 拒绝写入(白名单保护,差异见兼容清单);
|
||
- includeInputInfo 继续被 endpoint 接受,但本层不再使用
|
||
——新后端无需触发辅助对话。
|
||
|
||
endpoint 只负责校验、协议(SSE / 响应模型)与依赖注入。
|
||
"""
|
||
|
||
from ..backends.chat import ChatBackend, ChatInput, ChatResult, FormUpdate
|
||
from .fields_registry import (
|
||
FIELD_REGISTRY,
|
||
GROUP_KEYS,
|
||
BUSINESS_TO_FORM_KEY,
|
||
FieldGroup,
|
||
business_patch_from_form,
|
||
parse_field_value,
|
||
to_external_string,
|
||
)
|
||
from .repository import SessionRepository
|
||
|
||
# set_info 对未知/受保护 key 的拒绝码(与现有“入参不规范”文案对齐)
|
||
SET_INFO_INVALID_KEY_MSG = "入参不规范:未知或受保护的字段 key"
|
||
|
||
|
||
class AccidentAppService:
|
||
"""四个业务接口的应用服务(langgraph 后端)。"""
|
||
|
||
def __init__(self, backend: ChatBackend, repository: SessionRepository):
|
||
self._backend = backend
|
||
self._repository = repository
|
||
|
||
# ---- chat:图每轮读取最新业务状态,产出差量写回 ----
|
||
|
||
def _injected_fields(self, session_id: str) -> dict:
|
||
"""repository 业务字段 -> 图输入的分组注入。
|
||
|
||
acd 业务键经 BUSINESS_TO_FORM_KEY 映射回图表单键
|
||
(无表单来源的 bjrjs/ywmtc 不注入);人员业务键与图的
|
||
person_fields 同构,原样注入。
|
||
"""
|
||
business = self._repository.load(session_id)
|
||
if not business:
|
||
return {}
|
||
injected: dict = {"accident_fields": {}, "person_fields": {}}
|
||
for key, value in business.items():
|
||
spec = FIELD_REGISTRY.get(key)
|
||
if spec is None:
|
||
continue
|
||
if spec.group is FieldGroup.ACD:
|
||
form_key = BUSINESS_TO_FORM_KEY.get(key)
|
||
if form_key is not None:
|
||
injected["accident_fields"][form_key] = value
|
||
else:
|
||
injected["person_fields"][key] = value
|
||
return injected
|
||
|
||
def _writeback_patch(self, session_id: str, patch: dict) -> None:
|
||
"""图产出的差量(表单键/人员键)经映射落 repository 业务键。"""
|
||
business_patch = business_patch_from_form(patch)
|
||
if business_patch:
|
||
self._repository.apply_update(session_id, business_patch)
|
||
|
||
async def chat(self, chat_input: ChatInput) -> ChatResult:
|
||
"""非流式一轮对话:注入最新业务字段 -> 执行图 -> 写回字段差量。"""
|
||
injected = self._injected_fields(chat_input.session_id)
|
||
result = await self._backend.complete(
|
||
ChatInput(
|
||
session_id=chat_input.session_id,
|
||
text=chat_input.text,
|
||
need_form_update=chat_input.need_form_update,
|
||
client_mode=chat_input.client_mode,
|
||
injected_fields=injected or None,
|
||
)
|
||
)
|
||
# 图产出的字段差量写回 repository,
|
||
# 使 set/get_info 与后续轮次看到同一事实来源
|
||
if isinstance(result.form_update, dict):
|
||
self._writeback_patch(chat_input.session_id, result.form_update)
|
||
return result
|
||
|
||
async def stream(self, chat_input: ChatInput):
|
||
"""流式代理:字段注入同 chat();FormUpdate 事件捕获写回后透传。"""
|
||
injected = self._injected_fields(chat_input.session_id)
|
||
result_input = ChatInput(
|
||
session_id=chat_input.session_id,
|
||
text=chat_input.text,
|
||
need_form_update=chat_input.need_form_update,
|
||
client_mode=chat_input.client_mode,
|
||
injected_fields=injected or None,
|
||
)
|
||
async for event in self._backend.stream(result_input):
|
||
if isinstance(event, FormUpdate) and isinstance(event.data, dict):
|
||
self._writeback_patch(chat_input.session_id, event.data)
|
||
yield event
|
||
|
||
# ---- set_info / get_info:零 LLM、零辅助对话 ----
|
||
|
||
def set_info(self, session_id: str, key: str, value: str) -> tuple[str, str]:
|
||
"""白名单校验后写入 repository。返回 (code, msg)。"""
|
||
spec = FIELD_REGISTRY.get(key)
|
||
if spec is None or not spec.writable:
|
||
return "500", SET_INFO_INVALID_KEY_MSG
|
||
try:
|
||
parsed = parse_field_value(spec, value)
|
||
except ValueError:
|
||
return "500", "入参不规范:字段值无法解析"
|
||
self._repository.apply_update(session_id, {key: parsed})
|
||
return "200", ""
|
||
|
||
def get_info(self, session_id: str, key: str) -> str:
|
||
"""按分组或单键读出,返回 JSON 字符串(兼容现有 /get_info 契约)。"""
|
||
import json
|
||
|
||
business = self._repository.load(session_id)
|
||
if key == "all":
|
||
return json.dumps(
|
||
{
|
||
group.value: {
|
||
k: to_external_string(business.get(k))
|
||
for k in GROUP_KEYS[group]
|
||
}
|
||
for group in FieldGroup
|
||
},
|
||
ensure_ascii=False,
|
||
)
|
||
try:
|
||
group = FieldGroup(key)
|
||
except ValueError:
|
||
group = None
|
||
if group is not None:
|
||
return json.dumps(
|
||
{k: to_external_string(business.get(k)) for k in GROUP_KEYS[group]},
|
||
ensure_ascii=False,
|
||
)
|
||
# 单键:未知 key 返回 JSON 空串(兼容现状);已知 key 返回其外部表示
|
||
if key not in FIELD_REGISTRY:
|
||
return json.dumps("", ensure_ascii=False)
|
||
return json.dumps(to_external_string(business.get(key)), ensure_ascii=False)
|
||
|
||
# ---- delete_session:业务状态 + 图 checkpoint 一起清理 ----
|
||
|
||
async def delete_session(self, session_id: str, drop_checkpoint) -> None:
|
||
"""删除会话全部数据。
|
||
|
||
drop_checkpoint:由 core 层注入的 checkpoint 清理函数
|
||
(图生命周期归 lifespan 管理,本层不持有图引用)。
|
||
repository 删除幂等;checkpoint 缺失时静默(幂等语义一致)。
|
||
"""
|
||
self._repository.delete(session_id)
|
||
if drop_checkpoint is not None:
|
||
try:
|
||
await drop_checkpoint(session_id)
|
||
except Exception:
|
||
# checkpoint 已不存在视为删除成功的一部分(幂等)
|
||
pass
|
||
|
||
|
||
__all__ = ["AccidentAppService", "SET_INFO_INVALID_KEY_MSG"]
|