"""事故处理应用服务(作业 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"]