- 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.
70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
"""作业 1 验收测试:LangGraph 最小图。
|
||
|
||
通过标准(对应迁移计划作业 1):
|
||
- 同一会话(thread)轮数累计:1 → 2;
|
||
- 不同会话相互隔离:新 thread 的轮数从 1 开始;
|
||
- 不依赖 FastGPT 与真实模型,离线可重复执行。
|
||
|
||
注意:节点实现(src/agent/graph.py 中的 TODO)完成前,
|
||
本文件的测试会失败,属于预期状态。
|
||
"""
|
||
|
||
import pytest
|
||
|
||
from src.agent.graph import build_graph
|
||
|
||
|
||
async def ask(graph, thread_id: str, text: str) -> dict:
|
||
# 只传本轮变化的字段;对已有 checkpoint 的 thread 传完整初始状态,
|
||
# 其中的 turn_count=0 会覆盖已累计的值(LangGraph 输入按通道覆盖)。
|
||
return await graph.ainvoke(
|
||
{"session_id": thread_id, "input_text": text},
|
||
config={"configurable": {"thread_id": thread_id}},
|
||
)
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_same_thread_accumulates_turns() -> None:
|
||
graph = build_graph()
|
||
|
||
first = await ask(graph, "thread-a", "你好")
|
||
second = await ask(graph, "thread-a", "发生了交通事故")
|
||
|
||
assert first["turn_count"] == 1
|
||
assert second["turn_count"] == 2
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_different_threads_are_isolated() -> None:
|
||
graph = build_graph()
|
||
|
||
await ask(graph, "thread-a", "你好")
|
||
await ask(graph, "thread-a", "发生了交通事故")
|
||
other = await ask(graph, "thread-b", "你好")
|
||
|
||
assert other["turn_count"] == 1
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_first_turn_is_welcome_stage() -> None:
|
||
# 新会话首条消息:welcome 节点固定问候,返回 1001(对照工作流
|
||
# work_start is_first_message → Assigned Reply#3),不调用模型
|
||
graph = build_graph()
|
||
result = await ask(graph, "thread-a", "你好")
|
||
|
||
assert result["stage_code"] == "1001"
|
||
assert result["response_text"].startswith("<state>1001</state>")
|
||
assert result["turn_count"] == 1
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_second_turn_keeps_stage_without_event() -> None:
|
||
# 问候后的普通输入:脚手架版无事件则保持 1001,
|
||
# 【继续办理】等事件经白名单进入 1002(见 test_transitions.py)
|
||
graph = build_graph()
|
||
await ask(graph, "thread-a", "你好")
|
||
result = await ask(graph, "thread-a", "我出车祸了")
|
||
|
||
assert result["stage_code"] == "1001"
|
||
assert "我出车祸了" in result["response_text"]
|