feat(agent): domain events and transition whitelist validation (assignment 3)

Architecture (mirrors workflow/20261016 chatNode structure):
- events.py: client markers parsed to typed ClientEvent (sources
  verified against workflow JSON); plain text like '拍好了' is not an event
- transitions.py: LEGAL_TRANSITIONS whitelist drafted from chatNode
  prompts and Condition edges (16 entries, each with source_ref);
  is_legal_transition rejects unlisted/terminal-violating moves
- graph.py: multi-node graph (collect/single_photo/dual_photo/...
  matching workflow chatNodes), route_by_stage conditional edges,
  _fake_llm_propose simulates LLM stage-code proposals (assignment 4
  replaces with real LLM); code only keeps context bookkeeping
  (no_reply_count) and validation
- state.py: expanded domain fields (pending_question, accident/
  person fields, clarify/no-reply counts, turn_patch, state_version)

Key finding: 单车拍照#6/双车拍照#7 are browser_addon branches ending
at 0000, so 2005/2015 allow both 300x (direct) and 0000.

Verified: pytest 87 passed (24 transition/event tests newly green).
Known limit: stage proposals come from a fake LLM; per-stage scripts
and real LLM integration are assignments 4-5.
This commit is contained in:
Eric Wang
2026-10-19 16:25:04 +08:00
parent 5d68b731d4
commit 8323d53a70
7 changed files with 582 additions and 68 deletions
+41
View File
@@ -0,0 +1,41 @@
"""作业 3 事件解析测试。
来源:workflow/20261016/事故信息采集2025118.json 中的客户端标记。
关键规则:普通自然语言(如“拍好了”“继续”)不是事件。
"""
import pytest
from src.agent.events import ClientEvent, parse_client_event
@pytest.mark.parametrize(
("text", "expected"),
[
("【开始】", ClientEvent.SESSION_START),
("【继续办理】", ClientEvent.CONTINUE),
("【拍摄完成】", ClientEvent.PHOTO_DONE),
("【用户无回复】", ClientEvent.NO_REPLY),
("【客户端连续3次拍摄识别失败:图片过于模糊】", ClientEvent.PHOTO_FAILED_3X),
("【客户端连续3次拍摄识别失败:摄像头启动失败】", ClientEvent.PHOTO_FAILED_3X),
("【客户端连续3次拍摄识别失败:未识别到完整车牌】", ClientEvent.PHOTO_FAILED_3X),
(" 【拍摄完成】 ", ClientEvent.PHOTO_DONE), # 容忍首尾空白
],
)
def test_markers_parse_to_events(text, expected):
assert parse_client_event(text) is expected
@pytest.mark.parametrize(
"text",
[
"拍好了", # 普通“拍好了”不等于拍摄完成事件
"我已经拍完了",
"继续",
"你好",
"",
" ",
],
)
def test_plain_text_is_not_an_event(text):
assert parse_client_event(text) is None