feat(agent): form pipeline + collection prompt workflow@20261021
- merge main: workflow/20261021 (collection prompt adds step-5 mandatory time readback-confirm before routing; non-motor/0003 branch rework; accident_cause unified schema), updated eval suites (photo_skip/ button_guidance rewritten, no_false trimmed to asr, new non_motor_vehicle + time_confirmation with form/reply assertions), runner gains form/formMatches/replyMatches expectations - prompts/collection.md regenerated from 20261021; extraction prompt archived (contentExtract description, 6-key whitelist) - collect: extraction call per turn (extraction.md + current time/form injected), merge follows 更新表单 semantics (meaningful values only, number/string equality, diff -> turn_patch); accident_fields now uses external form keys - backend: form_update flows to ChatResult and SSE formUpdate event (stream emits prefix, FormUpdate, body) - fields: FORM_FIELDS whitelist with strict type validation - verified: 155 unit tests; golden evals on real model - photo_skip 4/4, no_false 2/2, photo_button_guidance 12/12, non_motor_vehicle 14/14, collection_skip 10/12, time_confirmation 1/2; remaining 3 failures are model sequence/literal-template adherence, recorded in plan appendix
This commit is contained in:
+69
-33
@@ -20,12 +20,11 @@ import json
|
||||
import re
|
||||
|
||||
from .fields import (
|
||||
FieldCandidate,
|
||||
FieldStatus,
|
||||
validate_candidate,
|
||||
FORM_FIELDS,
|
||||
validate_form_update,
|
||||
)
|
||||
from .llm import LLMService
|
||||
from .prompts import COLLECTION, render_prompt
|
||||
from .prompts import COLLECTION, EXTRACTION, render_prompt
|
||||
from .state import AgentState
|
||||
from .transitions import COLLECT_STAGE, is_legal_transition
|
||||
|
||||
@@ -131,37 +130,76 @@ def _build_user_message(state: AgentState) -> str:
|
||||
|
||||
|
||||
|
||||
def _extract_candidates(state: AgentState, body: str) -> list[FieldCandidate]:
|
||||
"""从模型正文中提取字段候选(计划作业 4-4)。
|
||||
# 采集阶段表单状态使用工作流的外部表单键(has_injury 等,见 fields.FORM_FIELDS);
|
||||
# 内部键(ywrysw 等)属于 get_info 分组,作业 6 再建立映射。
|
||||
|
||||
参考版返回空列表:字段提取依赖模型的结构化输出,留给真实
|
||||
LLM 接入后实现(同一回复内 JSON 解析优先;确需独立提取调用时,
|
||||
记录原因/调用次数/延迟)。validate_candidate 的字段级校验
|
||||
已由单元测试覆盖,此处先接通数据流。
|
||||
# 提取调用的系统提示标记(不以 <state> 开头,不会被误当对话回复解析)
|
||||
_EXTRACTION_MARKER = "【字段提取】"
|
||||
|
||||
|
||||
async def _extract_turn_fields(state: AgentState, llm: LLMService) -> dict:
|
||||
"""独立提取调用(计划作业 4-4:调用计数可审计——每轮在对话调用之外多一次)。
|
||||
|
||||
提示词为工作流「文本内容提取事故信息」原文(含当前时间与当前表单
|
||||
注入),模型只输出 JSON;校验见 fields.validate_form_update。
|
||||
提取失败不阻塞对话(返回 {})。
|
||||
"""
|
||||
return []
|
||||
system = (
|
||||
_EXTRACTION_MARKER
|
||||
+ "\n按下列系统提示词执行提取,只输出 JSON 对象,不输出解释。\n\n"
|
||||
+ render_prompt(EXTRACTION, state)
|
||||
)
|
||||
try:
|
||||
raw = await llm.complete(
|
||||
system,
|
||||
state.get("input_text", ""),
|
||||
history=conversation_history(state),
|
||||
)
|
||||
data = json.loads(raw.strip())
|
||||
except Exception:
|
||||
return {}
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
return validate_form_update(data)
|
||||
|
||||
|
||||
def _apply_candidates(
|
||||
state: AgentState, candidates: list[FieldCandidate]
|
||||
) -> tuple[dict, dict, dict]:
|
||||
"""把校验通过的候选写入正式字段。
|
||||
def _has_meaningful_value(value) -> bool:
|
||||
"""对应「更新表单」的 hasMeaningfulValue:None/空串无效(0 有效)。"""
|
||||
return value is not None and value != ""
|
||||
|
||||
返回 (accident_fields, confirm_status, turn_patch)。
|
||||
只有 CONFIRMED 的候选才写入正式字段并进入本轮差量;
|
||||
CANDIDATE/UNKNOWN 只更新 confirm_status,供后续核实节点消费
|
||||
(计划作业 4-5 的三态语义)。
|
||||
|
||||
def _is_same_value(old, new) -> bool:
|
||||
"""对应「更新表单」的 isSameValue:数字与数字字符串视为相等。"""
|
||||
if old == new:
|
||||
return True
|
||||
if old is None or new is None:
|
||||
return False
|
||||
if old == "" or new == "":
|
||||
return False
|
||||
try:
|
||||
return float(old) == float(new)
|
||||
except (TypeError, ValueError):
|
||||
return str(old).strip() == str(new).strip()
|
||||
|
||||
|
||||
def _merge_form(state: AgentState, update: dict) -> tuple[dict, dict]:
|
||||
"""按「更新表单」语义合并:白名单内、有效、有变化的字段进入差量。
|
||||
|
||||
返回 (合并后的完整表单, 本轮差量 turn_patch)。
|
||||
"""
|
||||
accident_fields = dict(state.get("accident_fields") or {})
|
||||
confirm_status = dict(state.get("confirm_status") or {})
|
||||
form = dict(state.get("accident_fields") or {})
|
||||
patch: dict = {}
|
||||
for cand in candidates:
|
||||
validated = validate_candidate(cand)
|
||||
confirm_status[validated.key] = validated.status
|
||||
if validated.status is FieldStatus.CONFIRMED:
|
||||
accident_fields[validated.key] = validated.value
|
||||
patch[validated.key] = validated.value
|
||||
return accident_fields, confirm_status, patch
|
||||
for key in FORM_FIELDS:
|
||||
if key not in update:
|
||||
continue
|
||||
value = update[key]
|
||||
if not _has_meaningful_value(value):
|
||||
continue
|
||||
if _is_same_value(form.get(key), value):
|
||||
continue
|
||||
form[key] = value
|
||||
patch[key] = value
|
||||
return form, patch
|
||||
|
||||
|
||||
def _bookkeeping(state: AgentState) -> tuple[int, int]:
|
||||
@@ -199,10 +237,9 @@ async def run_collect(state: AgentState, llm: LLMService) -> dict:
|
||||
)
|
||||
|
||||
|
||||
# 4) 字段候选提取与三态落库(提取本身见 _extract_candidates)
|
||||
accident_fields, confirm_status, patch = _apply_candidates(
|
||||
state, _extract_candidates(state, raw)
|
||||
)
|
||||
# 4) 字段提取与表单差量(提取调用在对话调用之外,见 _extract_turn_fields)
|
||||
extracted = await _extract_turn_fields(state, llm)
|
||||
accident_fields, patch = _merge_form(state, extracted)
|
||||
no_reply_count, clarify_count = _bookkeeping(state)
|
||||
|
||||
# 5) 状态增量:response_text 保留完整前缀文本,API 层负责解析;
|
||||
@@ -218,7 +255,6 @@ async def run_collect(state: AgentState, llm: LLMService) -> dict:
|
||||
"stage_code": stage,
|
||||
"raw_status_code": stage,
|
||||
"accident_fields": accident_fields,
|
||||
"confirm_status": confirm_status,
|
||||
"turn_patch": patch,
|
||||
"dialogue": dialogue,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user