feat(agent): LLM-powered collection node with welcome stage and session-scoped delete (assignment 4)
- prompts/collection.md: versioned collection system prompt extracted from workflow node yQaBxfrWjbRr with source/version/porting notes - llm.py: LLMService protocol; FakeLLMService (scripted, records calls); OpenAILLMService (async, wait_for timeout with cancellation, retry only on timeout/429/connection, errors normalized) - collect.py: run_collect reference impl - one LLM call produces state prefix + body, controlled repair (max 1 retry) for missing prefix or illegal stage proposal, whitelist guard, CollectReplyError without raw model content; dialogue history + confirmed fields injected into prompt so the model skips known info (作业4-5) - fields.py / clock.py: three-state field candidates (unknown/ candidate/confirmed), per-type validation (count not inferred from description, future time rejected), injectable Clock - graph.py: welcome node - first message returns fixed 1001 greeting without LLM (mirrors workflow is_first_message -> Assigned Reply#3), route empty stage to welcome - endpoints/delete_session: backend switch - langgraph clears graph checkpoint, fastgpt path unchanged - config: AGENT_LLM_PROVIDER fake|openai with validation - untrack .env (secrets), move template to root .env.example Verified: - pytest 103 passed; smoke turns 1/2/1 - Real LLM e2e: POST /chat multi-turn context works - Golden eval collection_skip_known_info: 12/12 cases pass - Golden eval no_false: 8/10 cases pass; 4 confirm/photo cases fail at final step - model routes on short reply '对' (prompt-adherence gap, photo/confirm stages are assignment 5 scope); recorded in evals/results/20261020-144856 Known limits: field extraction from model reply not wired (stub); photo/confirm nodes still scaffold; clarify counting is approximate.
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
"""作业 2 验收测试:LangGraph 后端适配器。
|
||||
|
||||
通过标准(对应迁移计划作业 2):
|
||||
- ``complete()`` 返回带 ``<state>1002</state>`` 前缀的 ``ChatResult``,
|
||||
API 层据此解析阶段码;
|
||||
- ``complete()`` 返回带 ``<state>`` 前缀的 ``ChatResult``,
|
||||
API 层据此解析阶段码(首轮为 welcome 固定问候 1001);
|
||||
- ``stream()`` 先产出含完整 state 前缀的 ``TextDelta``,再产出正文,
|
||||
保证 ``stage_code`` 事件先于正文;
|
||||
- 会话通过 ``thread_id = session_id`` 绑定(图 checkpoint 语义)。
|
||||
|
||||
注意:``src/backends/langgraph.py`` 中的 TODO 完成前,
|
||||
本文件的测试会失败,属于预期状态。
|
||||
注意:新会话首条消息返回 1001(对照工作流 is_first_message
|
||||
分支);1002 是进入采集后的阶段。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
@@ -31,8 +31,8 @@ async def test_complete_returns_state_prefixed_result() -> None:
|
||||
|
||||
result = await backend.complete(make_input())
|
||||
|
||||
assert result.content.startswith("<state>1002</state>")
|
||||
assert "发生了交通事故" in result.content
|
||||
# 首轮为 welcome 固定问候(1001,对照工作流 is_first_message 分支)
|
||||
assert result.content.startswith("<state>1001</state>")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -44,8 +44,8 @@ async def test_stream_emits_prefix_before_body() -> None:
|
||||
assert events, "stream 至少应产生一个 TextDelta"
|
||||
assert all(isinstance(event, TextDelta) for event in events)
|
||||
joined = "".join(event.text for event in events)
|
||||
assert joined.startswith("<state>1002</state>")
|
||||
assert "发生了交通事故" in joined
|
||||
# 首轮为 welcome 固定问候(1001)
|
||||
assert joined.startswith("<state>1001</state>")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -59,4 +59,5 @@ async def test_sessions_are_isolated_by_thread_id() -> None:
|
||||
)
|
||||
|
||||
assert first.content != "" and second.content != ""
|
||||
assert "你好" in other.content
|
||||
# 三个会话的首轮都是 welcome 问候;checkpoint 相互隔离
|
||||
assert other.content.startswith("<state>1001</state>")
|
||||
|
||||
Reference in New Issue
Block a user