Files
ZNJJ-api-server/test/agent/test_scaffold.py
T
Eric Wang 1c6b1a513d 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.
2026-10-20 14:53:46 +08:00

70 lines
2.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""作业 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"]