Files
ZNJJ-api-server/test/agent/test_workflow_prompts.py
T
Eric Wang e72d9c9bc0 feat(agent): align with workflow@20261022 - turn_context messages and prompt tails
- merge main (b0ff2e0..c7ca781): workflow/20261022 moves dynamic context
  out of fixed prompts - collection/extraction prompts carry no inline vars;
  reference time and current form travel in the user message <turn_context>
  block; photo/confirm data kept at prompt tails
- prompts: all 8 files regenerated from 20261022 (byte-identity tests
  point at the new export)
- collect: user message = <turn_context>(本轮参考时间) + <user_input>;
  extraction message adds 当前表单; single trusted clock per turn threads
  through reply/extraction/repair
- photo: raw input only (20261022 photo nodes use system_text, no wrapper)
- workflow fake llm: unwraps <user_input> before event parsing
- fields: accident_time normalized to extraction format (YYYY年M月D日H点m分,
  strip zero padding) - fixes form assertion mismatch with padded output
- evals/run.py: trust_env=False - bypass Windows system proxy (registry
  proxy at 192.168.0.4:7897 was intercepting localhost and returning 503)
- verified: 172 unit tests; ALL golden suites pass on real model
  (46/46): collection_skip 12/12, photo_skip 4/4, no_false 2/2,
  button_guidance 12/12, non_motor_vehicle 14/14, time_confirmation 2/2
2026-10-22 12:28:24 +08:00

119 lines
5.0 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.
"""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/20261022/事故信息采集2025118.json"
NODES = {
"collection": "事故信息收集",
"single_photo": "单车拍照",
"dual_photo": "双车拍照",
"single_photo_addon": "单车拍照#6",
"dual_photo_addon": "双车拍照#7",
"single_confirm": "单车信息确认",
"dual_confirm": "双车信息确认",
"extraction": "文本内容提取事故信息",
}
_INPUT_KEYS = {"extraction": "description"}
@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)
input_key = _INPUT_KEYS.get(prompt, "systemPrompt")
original = next(i["value"] for i in node["inputs"] if i["key"] == input_key)
archived = load_prompt(prompt).split("--- 原文开始 ---", 1)[1].split("--- 原文结束 ---", 1)[0]
assert archived.strip() == original.strip()
def test_render_replaces_time_and_person_variables():
# 20261022:collection/extraction 提示词本体不含变量(动态上下文走
# user 消息的 <turn_context>);拍照/确认提示词尾部仍渲染人员变量。
rendered = render_prompt(COLLECTION, {})
assert "{{$" not 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
def test_user_message_carries_turn_context():
from src.agent.collect import _build_user_message
from datetime import datetime, timezone
now = datetime(2026, 10, 22, 8, 0, tzinfo=timezone.utc)
message = _build_user_message({"input_text": "两辆"}, now=now)
assert "<turn_context>" in message
assert "本轮参考时间:2026-10-22 08:00:00 +0000" in message
assert "<user_input>" in message
assert "两辆" in message
@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) == 2 # 对话 + 提取
@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
# 20261022 格式:<turn_context> + <user_input>
assert "<turn_context>" in llm.calls[0]["user_message"]
assert "<user_input>\n两辆\n</user_input>" in 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": "本轮输入"},
]