Files
ZNJJ-api-server/test/backends/test_langgraph_backend.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

64 lines
2.1 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.
"""作业 2 验收测试:LangGraph 后端适配器。
通过标准(对应迁移计划作业 2):
- ``complete()`` 返回带 ``<state>`` 前缀的 ``ChatResult``,
API 层据此解析阶段码(首轮为 welcome 固定问候 1001);
- ``stream()`` 先产出含完整 state 前缀的 ``TextDelta``,再产出正文,
保证 ``stage_code`` 事件先于正文;
- 会话通过 ``thread_id = session_id`` 绑定(图 checkpoint 语义)。
注意:新会话首条消息返回 1001(对照工作流 is_first_message
分支);1002 是进入采集后的阶段。
"""
import pytest
from src.backends.chat import ChatInput, TextDelta
from src.backends.langgraph import LangGraphBackend
def make_input() -> ChatInput:
return ChatInput(
session_id="session-001",
text="发生了交通事故",
client_mode="direct",
)
@pytest.mark.asyncio
async def test_complete_returns_state_prefixed_result() -> None:
backend = LangGraphBackend()
result = await backend.complete(make_input())
# 首轮为 welcome 固定问候(1001,对照工作流 is_first_message 分支)
assert result.content.startswith("<state>1001</state>")
@pytest.mark.asyncio
async def test_stream_emits_prefix_before_body() -> None:
backend = LangGraphBackend()
events = [event async for event in backend.stream(make_input())]
assert events, "stream 至少应产生一个 TextDelta"
assert all(isinstance(event, TextDelta) for event in events)
joined = "".join(event.text for event in events)
# 首轮为 welcome 固定问候(1001)
assert joined.startswith("<state>1001</state>")
@pytest.mark.asyncio
async def test_sessions_are_isolated_by_thread_id() -> None:
backend = LangGraphBackend()
first = await backend.complete(make_input())
second = await backend.complete(make_input())
other = await backend.complete(
ChatInput(session_id="session-002", text="你好", client_mode="direct")
)
assert first.content != "" and second.content != ""
# 三个会话的首轮都是 welcome 问候;checkpoint 相互隔离
assert other.content.startswith("<state>1001</state>")