refactor(agent): inject deps via context_schema, LLM-driven photo/confirm nodes
- graph.py: LangGraph-native DI - StateGraph(context_schema=GraphDeps), nodes take (state, runtime: Runtime[GraphDeps]); build_graph() decoupled from LLM; deleted scaffold _event_reply/_fake_llm_propose/stub nodes - photo.py: model proposes stage+body via per-mode prompts (direct/addon); validator enforces clientMode exits; deterministic event forcing removed - confirm.py: single runtime for single/dual confirm, extraction call isolated per person_index - llm.py: workflow_fake_llm() (offline workflow-faithful responder, history-aware) for smoke/tests - backend: LangGraphBackend(graph, llm) passes context per call - tests: invoke helpers inject GraphDeps; illegal-proposal test asserts error + state untouched via second well-behaved fake Verified: 143 passed; smoke turns 1/2/1 with original welcome text
This commit is contained in:
@@ -26,7 +26,8 @@ from src.agent.prompts import COLLECTION, load_prompt
|
||||
def collect_state(**overrides) -> dict:
|
||||
state = {
|
||||
"session_id": "s1",
|
||||
"input_text": "你好",
|
||||
# 使用完整句而非短促词,避免触发分流守卫(“你好”<=2 字)
|
||||
"input_text": "发生了交通事故,两辆车追尾",
|
||||
"stage_code": "1002",
|
||||
"turn_count": 1,
|
||||
"no_reply_count": 0,
|
||||
@@ -138,13 +139,16 @@ async def test_collect_illegal_stage_proposal_not_committed():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_graph_collect_node_with_llm():
|
||||
# 图级:build_graph(llm=...) 时首轮走 welcome(不调模型),
|
||||
# 第二轮起 collect 节点走 run_collect
|
||||
# 图级:首轮走 welcome(确定性,不调模型);第二轮起 collect
|
||||
# 节点经 Runtime 注入的 llm 走 run_collect
|
||||
from src.agent.graph import GraphDeps
|
||||
|
||||
llm = FakeLLMService(lambda s, u: "<state>1002</state>收到。")
|
||||
graph = build_graph(llm=llm)
|
||||
graph = build_graph()
|
||||
first = await graph.ainvoke(
|
||||
{"session_id": "s1", "input_text": "追尾了"},
|
||||
config={"configurable": {"thread_id": "s1"}},
|
||||
context=GraphDeps(llm=llm),
|
||||
)
|
||||
assert len(llm.calls) == 0 # welcome 是确定性节点
|
||||
assert first["stage_code"] == "1001"
|
||||
@@ -152,6 +156,7 @@ async def test_graph_collect_node_with_llm():
|
||||
result = await graph.ainvoke(
|
||||
{"session_id": "s1", "input_text": "继续办理"},
|
||||
config={"configurable": {"thread_id": "s1"}},
|
||||
context=GraphDeps(llm=llm),
|
||||
)
|
||||
assert len(llm.calls) == 1
|
||||
assert result["response_text"].startswith("<state>1002</state>")
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
"""作业 5 验收测试:信息核实节点(LLM 决策版)。
|
||||
|
||||
测试用脚本化 FakeLLM 模拟“行为良好的模型”:按用户最新输入
|
||||
返回对应的带前缀回复;字段提取调用按提示词返回 JSON。
|
||||
断言仍是行为级的:stage 推进、person_fields 落库、person_index
|
||||
切换、模糊不计入失败。
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agent.graph import GraphDeps, build_graph, workflow_fake_llm
|
||||
from src.agent.llm import FakeLLMService
|
||||
|
||||
|
||||
def latest_input(message: str) -> str:
|
||||
return message.split("【用户最新输入】")[-1].strip()
|
||||
|
||||
|
||||
def make_llm(dialogue_script, extraction_script=None):
|
||||
"""dialogue_script(latest_input) -> 带前缀回复;
|
||||
extraction_script(extraction_system_prompt) -> JSON 字符串。"""
|
||||
|
||||
def responder(system, user):
|
||||
if system.startswith("【字段提取】"):
|
||||
if extraction_script:
|
||||
return extraction_script(system)
|
||||
return "{}"
|
||||
return dialogue_script(latest_input(user))
|
||||
|
||||
return FakeLLMService(responder)
|
||||
|
||||
|
||||
async def invoke(graph, thread_id, text, llm=None, **initial):
|
||||
payload = {"session_id": thread_id, "input_text": text}
|
||||
payload.update(initial)
|
||||
return await graph.ainvoke(
|
||||
payload,
|
||||
config={"configurable": {"thread_id": thread_id}},
|
||||
context=GraphDeps(llm=llm or workflow_fake_llm()),
|
||||
)
|
||||
|
||||
|
||||
# ---------- 单车核实 ----------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_entry_consumes_pending_identity_question():
|
||||
# 用户确认车主身份 → 模型直接问姓名(身份问题的提问发生在
|
||||
# 拍照节点进入 3001 的同一回复里,这里不得重复)
|
||||
llm = make_llm(
|
||||
lambda q: "<state>3001</state>好的,请问您的姓名是什么?"
|
||||
)
|
||||
graph = build_graph()
|
||||
result = await invoke(
|
||||
graph, "t-entry", "是的,我是车主", llm=llm,
|
||||
stage_code="3001", client_mode="direct",
|
||||
)
|
||||
|
||||
assert result["stage_code"] == "3001"
|
||||
assert "姓名" in result["response_text"]
|
||||
assert "车主" not in result["response_text"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_confirm_full_chain_to_0000():
|
||||
def dialogue(q):
|
||||
if q == "是的":
|
||||
return "<state>3001</state>好的,请问您的姓名是什么?"
|
||||
if q == "张三":
|
||||
return "<state>3001</state>张三您好,请问您的身份证号码后四位是否正确?"
|
||||
if q == "是对的":
|
||||
return "<state>3001</state>请问您的手机号码后四位是否正确?"
|
||||
return "<state>0000</state>好的,信息已全部确认无误。感谢您的配合。"
|
||||
|
||||
def extraction(system):
|
||||
if "【用户输入】张三" in system:
|
||||
return json.dumps({"xm1": "张三"}, ensure_ascii=False)
|
||||
return "{}"
|
||||
|
||||
llm = make_llm(dialogue, extraction)
|
||||
graph = build_graph()
|
||||
|
||||
r1 = await invoke(graph, "t-chain", "是的", llm=llm, stage_code="3001")
|
||||
assert "姓名" in r1["response_text"]
|
||||
r2 = await invoke(graph, "t-chain", "张三", llm=llm)
|
||||
assert "身份证" in r2["response_text"]
|
||||
assert r2["person_fields"]["xm1"] == "张三"
|
||||
r3 = await invoke(graph, "t-chain", "是对的", llm=llm)
|
||||
assert "手机" in r3["response_text"]
|
||||
r4 = await invoke(graph, "t-chain", "没错", llm=llm)
|
||||
assert r4["stage_code"] == "0000"
|
||||
assert r4["response_text"].startswith("<state>0000</state>")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deny_collects_correct_value():
|
||||
llm = make_llm(
|
||||
lambda q: "<state>3001</state>抱歉,请告诉我您正确的身份证号码后四位。",
|
||||
lambda system: "{}",
|
||||
)
|
||||
graph = build_graph()
|
||||
result = await invoke(
|
||||
graph, "t-deny", "不对", llm=llm,
|
||||
stage_code="3001", person_fields={"xm1": "张三"}, client_mode="direct",
|
||||
)
|
||||
|
||||
assert result["stage_code"] == "3001"
|
||||
assert "身份证" in result["response_text"]
|
||||
assert result["person_fields"].get("sfzmwh1") in (None, "")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vague_answer_stays_without_counting_failure():
|
||||
llm = make_llm(
|
||||
lambda q: "<state>3001</state>请问您是否是这辆车的车主?请回答“是”或“不是”。",
|
||||
lambda system: "{}",
|
||||
)
|
||||
graph = build_graph()
|
||||
result = await invoke(
|
||||
graph, "t-vague", "嗯", llm=llm,
|
||||
stage_code="3001", clarify_count=0, client_mode="direct",
|
||||
)
|
||||
|
||||
assert result["stage_code"] == "3001"
|
||||
assert "车主" in result["response_text"]
|
||||
assert (result.get("clarify_count") or 0) == 0
|
||||
|
||||
|
||||
# ---------- 双车核实 ----------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dual_person_index_switches_after_handoff():
|
||||
llm = make_llm(
|
||||
lambda q: "<state>3002</state>您好。请问这辆车是由您驾驶的吗?",
|
||||
lambda system: json.dumps({"person_index": 2}),
|
||||
)
|
||||
graph = build_graph()
|
||||
result = await invoke(
|
||||
graph, "t-dual", "移交完毕", llm=llm,
|
||||
stage_code="3002", person_index=1,
|
||||
person_fields={"xm1": "张三"}, client_mode="direct",
|
||||
)
|
||||
|
||||
assert result["stage_code"] == "3002"
|
||||
assert result["person_index"] == 2
|
||||
assert "驾驶" in result["response_text"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dual_second_person_fields_isolated():
|
||||
def dialogue(q):
|
||||
if q == "是的":
|
||||
return "<state>3002</state>好的,驾驶员您好。请问您的姓名是什么?"
|
||||
return "<state>3002</state>请问您的身份证号码后四位是否正确?"
|
||||
|
||||
def extraction(system):
|
||||
if "【用户输入】李四" in system:
|
||||
return json.dumps({"xm2": "李四"}, ensure_ascii=False)
|
||||
return "{}"
|
||||
|
||||
llm = make_llm(dialogue, extraction)
|
||||
graph = build_graph()
|
||||
|
||||
await invoke(
|
||||
graph, "t-dual2", "是的", llm=llm,
|
||||
stage_code="3002", person_index=2,
|
||||
person_fields={"xm1": "张三", "sjhm1": "13800001111"},
|
||||
client_mode="direct",
|
||||
)
|
||||
result = await invoke(graph, "t-dual2", "李四", llm=llm)
|
||||
|
||||
assert result["person_fields"]["xm2"] == "李四"
|
||||
assert result["person_fields"]["xm1"] == "张三"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dual_completion_goes_to_0000():
|
||||
llm = make_llm(
|
||||
lambda q: "<state>0000</state>好的,两位驾驶员的信息均已核实完毕。感谢配合。",
|
||||
lambda system: "{}",
|
||||
)
|
||||
graph = build_graph()
|
||||
result = await invoke(
|
||||
graph, "t-dual-done", "没错", llm=llm,
|
||||
stage_code="3002", person_index=2,
|
||||
person_fields={"xm1": "张三", "xm2": "李四"},
|
||||
client_mode="direct",
|
||||
)
|
||||
|
||||
assert result["stage_code"] == "0000"
|
||||
@@ -0,0 +1,116 @@
|
||||
"""拍照节点回归:LLM 提议阶段与正文,运行时校验并保存上下文。
|
||||
|
||||
fake 输出仅验证接线与校验,不代替真实模型对提示词服从性的评测。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agent.collect import CollectReplyError
|
||||
from src.agent.graph import GraphDeps, build_graph, workflow_fake_llm
|
||||
from src.agent.llm import FakeLLMService
|
||||
|
||||
|
||||
async def invoke(graph, thread_id, text, llm=None, **initial):
|
||||
return await graph.ainvoke(
|
||||
{"session_id": thread_id, "input_text": text, **initial},
|
||||
config={"configurable": {"thread_id": thread_id}},
|
||||
context=GraphDeps(llm=llm or workflow_fake_llm()),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("source", "target"),
|
||||
[("2000", "2001"), ("2001", "2002"), ("2002", "2003"), ("2003", "2005"),
|
||||
("2010", "2011"), ("2011", "2012"), ("2012", "2013"),
|
||||
("2013", "2014"), ("2014", "2015")],
|
||||
)
|
||||
async def test_photo_applies_model_proposal(source, target):
|
||||
raw = f"<state>{target}</state>模型生成的本轮拍照引导。"
|
||||
llm = FakeLLMService(lambda s, u: raw)
|
||||
result = await invoke(build_graph(), "photo", "【拍摄完成】", llm=llm,
|
||||
stage_code=source, client_mode="direct")
|
||||
assert result["stage_code"] == target
|
||||
assert result["response_text"] == raw
|
||||
assert len(llm.calls) == 1
|
||||
assert "【拍摄完成】" in llm.calls[0]["user_message"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("text", ["【拍摄完成】", "【客户端连续3次拍摄识别失败:图片过于模糊】", "【用户无回复】"])
|
||||
async def test_event_alone_does_not_force_stage(text):
|
||||
# 模型保持阶段时,运行时不会仅凭事件强制跳转。
|
||||
llm = FakeLLMService(lambda s, u: "<state>2012</state>请确认当前操作。")
|
||||
result = await invoke(build_graph(), "hold", text, llm=llm,
|
||||
stage_code="2012", client_mode="direct")
|
||||
assert result["stage_code"] == "2012"
|
||||
assert len(llm.calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_failure_stage_also_comes_from_model():
|
||||
llm = FakeLLMService(lambda s, u: "<state>0005</state>正在转接人工。")
|
||||
result = await invoke(build_graph(), "fail", "【客户端连续3次拍摄识别失败:图片过于模糊】", llm=llm,
|
||||
stage_code="2012", client_mode="direct")
|
||||
assert result["stage_code"] == "0005"
|
||||
assert len(llm.calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("source", "mode", "target", "node"),
|
||||
[("2005", "direct", "3001", "单车拍照"),
|
||||
("2005", "browser_addon", "0000", "单车拍照#6"),
|
||||
("2015", "direct", "3002", "双车拍照"),
|
||||
("2015", "browser_addon", "0000", "双车拍照#7")],
|
||||
)
|
||||
async def test_mode_selects_workflow_prompt(source, mode, target, node):
|
||||
from src.agent.prompts import load_prompt
|
||||
from src.agent.photo import PHOTO_PROMPTS
|
||||
|
||||
kind = "single" if source == "2005" else "dual"
|
||||
expected_prompt = load_prompt(PHOTO_PROMPTS[(kind, mode)])
|
||||
expected_body = expected_prompt.split("--- 原文开始 ---", 1)[1].split("--- 原文结束 ---", 1)[0].strip()
|
||||
raw = f"<state>{target}</state>对应模式的结束话术。"
|
||||
llm = FakeLLMService(lambda s, u: raw)
|
||||
text = "车子左后方" if source == "2005" else "【拍摄完成】"
|
||||
result = await invoke(build_graph(), "end", text, llm=llm,
|
||||
stage_code=source, client_mode=mode)
|
||||
assert result["stage_code"] == target
|
||||
assert len(llm.calls) == 1
|
||||
assert f"节点「{node}」" in expected_prompt
|
||||
# 模型收到原文,预填变量已经渲染,来源头部不进入模型上下文。
|
||||
assert "{{$" not in llm.calls[0]["system_prompt"]
|
||||
assert expected_body.splitlines()[0] in llm.calls[0]["system_prompt"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_photo_preserves_guidance_and_handoff_history():
|
||||
replies = iter([
|
||||
"<state>2015</state>您可以点击手机界面中间的拍照按钮。",
|
||||
"<state>3002</state>请问第一辆车是由您驾驶的吗?",
|
||||
])
|
||||
llm = FakeLLMService(lambda s, u: next(replies))
|
||||
graph = build_graph()
|
||||
first = await invoke(graph, "history", "拍照按钮在哪里?", llm=llm,
|
||||
stage_code="2015", client_mode="direct")
|
||||
second = await invoke(graph, "history", "【拍摄完成】", llm=llm)
|
||||
assert "手机界面中间" in first["response_text"]
|
||||
assert llm.calls[1]["history"][0] == {"role": "user", "content": "拍照按钮在哪里?"}
|
||||
assert llm.calls[1]["user_message"] == "【拍摄完成】"
|
||||
assert second["dialogue"][-1]["content"] == second["response_text"]
|
||||
assert "驾驶" in second["dialogue"][-1]["content"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(("source", "mode", "target"), [
|
||||
("2000", "direct", "2003"),
|
||||
("2015", "browser_addon", "3002"),
|
||||
("2005", "direct", "0000"),
|
||||
])
|
||||
async def test_illegal_proposal_fails_after_one_repair(source, mode, target):
|
||||
llm = FakeLLMService(lambda s, u: f"<state>{target}</state>非法提议")
|
||||
with pytest.raises(CollectReplyError):
|
||||
await invoke(build_graph(), "illegal", "继续", llm=llm,
|
||||
stage_code=source, client_mode=mode)
|
||||
assert len(llm.calls) == 2
|
||||
@@ -11,15 +11,16 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agent.graph import build_graph
|
||||
from src.agent.graph import GraphDeps, build_graph, workflow_fake_llm
|
||||
|
||||
|
||||
async def ask(graph, thread_id: str, text: str) -> dict:
|
||||
async def ask(graph, thread_id: str, text: str, llm=None) -> dict:
|
||||
# 只传本轮变化的字段;对已有 checkpoint 的 thread 传完整初始状态,
|
||||
# 其中的 turn_count=0 会覆盖已累计的值(LangGraph 输入按通道覆盖)。
|
||||
return await graph.ainvoke(
|
||||
{"session_id": thread_id, "input_text": text},
|
||||
config={"configurable": {"thread_id": thread_id}},
|
||||
context=GraphDeps(llm=llm or workflow_fake_llm()),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
|
||||
import pytest
|
||||
|
||||
import src.agent.graph as graph_mod
|
||||
from src.agent.graph import build_graph
|
||||
from src.agent.llm import FakeLLMService
|
||||
|
||||
from src.agent.graph import GraphDeps, build_graph, workflow_fake_llm
|
||||
from src.agent.transitions import (
|
||||
TERMINAL_STAGES,
|
||||
TransitionSpec,
|
||||
@@ -21,12 +22,13 @@ from src.agent.transitions import (
|
||||
)
|
||||
|
||||
|
||||
def invoke(graph, thread_id, text, **initial):
|
||||
def invoke(graph, thread_id, text, llm=None, **initial):
|
||||
payload = {"session_id": thread_id, "input_text": text}
|
||||
payload.update(initial)
|
||||
return graph.ainvoke(
|
||||
payload,
|
||||
config={"configurable": {"thread_id": thread_id}},
|
||||
context=GraphDeps(llm=llm or workflow_fake_llm()),
|
||||
)
|
||||
|
||||
|
||||
@@ -82,12 +84,21 @@ async def test_valid_reply_resets_no_reply_count():
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_illegal_proposal_keeps_stage(monkeypatch):
|
||||
# LLM 提议非法跳步(2001 → 2003):校验拒绝,状态不被污染
|
||||
async def test_illegal_proposal_raises_and_state_untouched():
|
||||
# LLM 坚持非法跳步(2001 → 2003):白名单拒绝 + 一次修复后
|
||||
# 仍非法 → CollectReplyError,非法阶段不写状态
|
||||
from src.agent.collect import CollectReplyError
|
||||
|
||||
llm = FakeLLMService(lambda s, u: "<state>2003</state>跳步")
|
||||
graph = build_graph()
|
||||
monkeypatch.setattr(graph_mod, "_fake_llm_propose", lambda s, e: "2003")
|
||||
result = await invoke(graph, "t-illegal", "【拍摄完成】", stage_code="2001")
|
||||
assert result["stage_code"] == "2001"
|
||||
with pytest.raises(CollectReplyError):
|
||||
await invoke(graph, "t-illegal", "【拍摄完成】", llm=llm, stage_code="2001")
|
||||
assert len(llm.calls) == 2
|
||||
|
||||
# 会话未被污染:下一轮用守规矩的 fake 继续,阶段仍是 2001
|
||||
good = FakeLLMService(lambda s, u: "<state>2002</state>已收到。")
|
||||
result = await invoke(graph, "t-illegal", "【拍摄完成】", llm=good, stage_code="2001")
|
||||
assert result["stage_code"] == "2002"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""20261020 工作流的提示词移植与模型决策回归。"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agent.collect import CollectReplyError, run_collect
|
||||
from src.agent.llm import FakeLLMService
|
||||
from src.agent.prompts import COLLECTION, load_prompt, render_prompt
|
||||
|
||||
WORKFLOW = Path(__file__).resolve().parents[2] / "workflow/20261020/事故信息采集2025118.json"
|
||||
NODES = {
|
||||
"collection": "事故信息收集",
|
||||
"single_photo": "单车拍照",
|
||||
"dual_photo": "双车拍照",
|
||||
"single_photo_addon": "单车拍照#6",
|
||||
"dual_photo_addon": "双车拍照#7",
|
||||
"single_confirm": "单车信息确认",
|
||||
"dual_confirm": "双车信息确认",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("prompt", "node_name"), NODES.items())
|
||||
def test_archived_prompt_matches_workflow(prompt, node_name):
|
||||
workflow = json.loads(WORKFLOW.read_text(encoding="utf-8-sig"))
|
||||
node = next(n for n in workflow["nodes"] if n["name"] == node_name)
|
||||
original = next(i["value"] for i in node["inputs"] if i["key"] == "systemPrompt")
|
||||
archived = load_prompt(prompt).split("--- 原文开始 ---", 1)[1].split("--- 原文结束 ---", 1)[0]
|
||||
assert archived.strip() == original.strip()
|
||||
|
||||
|
||||
def test_render_replaces_time_and_person_variables():
|
||||
now = datetime(2026, 10, 20, 12, 0, tzinfo=timezone.utc)
|
||||
rendered = render_prompt(COLLECTION, {}, now=now)
|
||||
assert "2026-10-20 12:00:00 +0000" in rendered
|
||||
rendered = render_prompt("dual_confirm", {
|
||||
"person_fields": {"hphm1": "测试A", "hphm2": "测试B", "sfzmwh1": "1234"}
|
||||
})
|
||||
assert "测试A" in rendered and "测试B" in rendered
|
||||
assert "1234" in rendered
|
||||
assert "{{$" not in rendered
|
||||
assert "未提供" in rendered
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_short_answer_does_not_override_model_routing():
|
||||
# “在”可能是明确回答;运行时不得按字数一律拒绝分流。
|
||||
llm = FakeLLMService(lambda s, u: "<state>2000</state>信息已齐,请拍摄车辆前方。")
|
||||
result = await run_collect({
|
||||
"stage_code": "1002", "input_text": "在", "clarify_count": 0,
|
||||
"dialogue": [{"role": "assistant", "content": "您还在事故现场吗?"}],
|
||||
}, llm)
|
||||
assert result["stage_code"] == "2000"
|
||||
assert result["clarify_count"] == 0
|
||||
assert len(llm.calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_format_and_stage_errors_share_one_repair_budget():
|
||||
replies = iter(["缺少前缀", "<state>9999</state>非法阶段"])
|
||||
llm = FakeLLMService(lambda s, u: next(replies))
|
||||
with pytest.raises(CollectReplyError):
|
||||
await run_collect({"stage_code": "1002", "input_text": "追尾了"}, llm)
|
||||
assert len(llm.calls) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_collect_sends_history_as_messages():
|
||||
history = [
|
||||
{"role": "user", "content": "没有人受伤"},
|
||||
{"role": "assistant", "content": "<state>1002</state>请问有几辆车?"},
|
||||
]
|
||||
llm = FakeLLMService(lambda s, u: "<state>1002</state>请问您还在现场吗?")
|
||||
await run_collect({
|
||||
"stage_code": "1002", "input_text": "两辆", "dialogue": history,
|
||||
}, llm)
|
||||
assert llm.calls[0]["history"] == history
|
||||
assert llm.calls[0]["user_message"] == "两辆"
|
||||
assert "没有人受伤" not in llm.calls[0]["user_message"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_openai_adapter_preserves_message_roles():
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
from src.agent.llm import OpenAILLMService
|
||||
|
||||
create = AsyncMock(return_value=SimpleNamespace(choices=[
|
||||
SimpleNamespace(message=SimpleNamespace(content="<state>1002</state>收到。"))
|
||||
]))
|
||||
llm = OpenAILLMService(model="test", api_key="test", max_retries=0)
|
||||
llm._client = SimpleNamespace(chat=SimpleNamespace(completions=SimpleNamespace(create=create)))
|
||||
history = [{"role": "assistant", "content": "之前的问题"}]
|
||||
await llm.complete("系统提示词", "本轮输入", history=history)
|
||||
assert create.call_args.kwargs["messages"] == [
|
||||
{"role": "system", "content": "系统提示词"},
|
||||
{"role": "assistant", "content": "之前的问题"},
|
||||
{"role": "user", "content": "本轮输入"},
|
||||
]
|
||||
Reference in New Issue
Block a user