feat(services): assignment 6 full - form/business key mapping and stream write-back

- 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
This commit is contained in:
Eric Wang
2026-10-22 21:07:01 +08:00
parent 8717d9066c
commit 7f52f1c4d0
5 changed files with 144 additions and 17 deletions
+24 -11
View File
@@ -13,11 +13,13 @@
endpoint 只负责校验、协议(SSE / 响应模型)与依赖注入。
"""
from ..backends.chat import ChatBackend, ChatInput, ChatResult
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,
)
@@ -37,8 +39,12 @@ class AccidentAppService:
# ---- chat:图每轮读取最新业务状态,产出差量写回 ----
def _injected_fields(self, session_id: str) -> dict:
"""repository 业务字段 -> 图输入的分组注入(acd 进 accident_fields,
两组人员进 person_fields,与 AgentState 的键结构对齐)。"""
"""repository 业务字段 -> 图输入的分组注入。
acd 业务键经 BUSINESS_TO_FORM_KEY 映射回图表单键
(无表单来源的 bjrjs/ywmtc 不注入);人员业务键与图的
person_fields 同构,原样注入。
"""
business = self._repository.load(session_id)
if not business:
return {}
@@ -48,11 +54,19 @@ class AccidentAppService:
if spec is None:
continue
if spec.group is FieldGroup.ACD:
injected["accident_fields"][key] = value
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)
@@ -65,15 +79,14 @@ class AccidentAppService:
injected_fields=injected or None,
)
)
# 图产出的字段差量(turn_patch / 完整字段)写回 repository,
# 图产出的字段差量写回 repository,
# 使 set/get_info 与后续轮次看到同一事实来源
patch = result.form_update if isinstance(result.form_update, dict) else {}
if patch:
self._repository.apply_update(chat_input.session_id, patch)
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(),事件流原样透传。"""
"""流式代理:字段注入同 chat();FormUpdate 事件捕获写回后透传。"""
injected = self._injected_fields(chat_input.session_id)
result_input = ChatInput(
session_id=chat_input.session_id,
@@ -82,9 +95,9 @@ class AccidentAppService:
client_mode=chat_input.client_mode,
injected_fields=injected or None,
)
# 骨架阶段原样透传事件流;字段差量写回在作业 6 完整版随
# FormUpdate 事件捕获补齐(对照计划作业 8 的真实增量)。
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、零辅助对话 ----