assistant/tests/test_prospecting_dialogue.py

102 lines
5.4 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.

"""真实解析合同经HTTP夹具测试,不调用外部模型。"""
import json
import httpx
import pytest
from backend.prospecting import dialogue
from backend.prospecting.analysis import analyze_sources
from backend.prospecting import analysis
from backend.prospecting.contracts import ProspectingBrief, TARGET_QUESTION
CONFIG = {"provider": "deepseek", "model": "test-model", "api_key": "test-placeholder", "base_url": "https://model.invalid/v1"}
def install_http(monkeypatch, target, handler):
client_class = httpx.AsyncClient
monkeypatch.setattr(target, "get_llm_config", lambda: CONFIG)
monkeypatch.setattr(target.httpx, "AsyncClient", lambda **kwargs: client_class(transport=httpx.MockTransport(handler), **kwargs))
def plan_response(**updates):
plan = {"brief": ProspectingBrief(target_customer="杭州准备装修的人", keywords=["杭州 装修准备"]).model_dump(),
"summary": "寻找杭州准备装修的人", "stages": ["搜索", "阅读"], "missing": [], "assumptions": ["关键词由目标生成"]}
plan.update(updates)
return {"choices": [{"message": {"content": json.dumps(plan, ensure_ascii=False)}}]}
@pytest.mark.asyncio
async def test_interpretation_uses_json_contract_without_default_quotas(monkeypatch):
sent = []
def handler(request):
sent.append(json.loads(request.content))
return httpx.Response(200, json=plan_response())
install_http(monkeypatch, dialogue, handler)
plan = await dialogue.interpret_message("找杭州准备装修的人", ProspectingBrief(), [])
assert plan.brief.product == ""
assert plan.brief.max_notes is None
assert plan.missing == [TARGET_QUESTION]
assert sent[0]["response_format"] == {"type": "json_object"}
assert "地区、产品" not in plan.summary
@pytest.mark.asyncio
async def test_interpretation_rejects_model_invented_destination(monkeypatch):
brief = ProspectingBrief(intent="inspect", subject_urls=["https://www.xiaohongshu.com/explore/not-provided"])
install_http(monkeypatch, dialogue, lambda request: httpx.Response(200, json=plan_response(brief=brief.model_dump())))
with pytest.raises(ValueError, match="不在用户原话"):
await dialogue.interpret_message("看这篇笔记", ProspectingBrief(), [])
@pytest.mark.asyncio
async def test_interpretation_accepts_actual_user_share_link(monkeypatch):
url = "https://xhslink.com/a/real"
brief = ProspectingBrief(intent="inspect", subject_urls=[url])
install_http(monkeypatch, dialogue, lambda request: httpx.Response(200, json=plan_response(brief=brief.model_dump())))
plan = await dialogue.interpret_message("看这个 " + url, ProspectingBrief(), [])
assert plan.brief.subject_urls == [url]
assert plan.missing == []
@pytest.mark.asyncio
@pytest.mark.parametrize("body", [{"choices": []}, {"choices": [{"message": {"content": "bad json"}}]}, plan_response(summary="")])
async def test_invalid_interpretation_is_retryable_and_human_readable(monkeypatch, body):
install_http(monkeypatch, dialogue, lambda request: httpx.Response(200, json=body))
with pytest.raises(ValueError, match="意图识别未返回可用计划"):
await dialogue.interpret_message("找装修客户", ProspectingBrief(), [])
@pytest.mark.asyncio
async def test_follow_without_message_text_has_no_irrelevant_question(monkeypatch):
brief = ProspectingBrief(intent="follow", action_scope="已核对的现有候选")
install_http(monkeypatch, dialogue, lambda request: httpx.Response(200, json=plan_response(brief=brief.model_dump())))
plan = await dialogue.interpret_message("关注这些人", ProspectingBrief(), [])
assert plan.missing == []
@pytest.mark.asyncio
async def test_analysis_batches_cover_every_source_and_keep_reply_context(monkeypatch):
note = {"source_id": "n", "source_type": "note", "source_url": "https://www.xiaohongshu.com/explore/n", "content": "父正文"}
root = {"source_id": "c", "source_type": "comment", "source_url": note["source_url"], "content": "父评论"}
sources = [note, root, *[{"source_id": f"r{i}", "source_type": "reply", "source_url": note["source_url"],
"content": f"本人需求{i}", "parent_source_id": "c", "root_source_id": "c", "author": f"回复者{i}"} for i in range(18)]]
requests = []
def handler(request):
text = json.loads(request.content)["messages"][0]["content"]
batch = json.loads(text.split("待分析sources:", 1)[1])
context = json.loads(text.split("context:", 1)[1].split("\n待分析sources:", 1)[0])
requests.append((batch, context))
output = {"results": [{"source_id": row["source_id"], "decision": "keep", "quote": row["content"], "reason": "待核需求"} for row in batch], "summary": "已读摘录"}
return httpx.Response(200, json={"model": "test-model", "choices": [{"message": {"content": json.dumps(output, ensure_ascii=False)}}], "usage": {"total_tokens": 10}})
install_http(monkeypatch, analysis, handler)
result = await analyze_sources(ProspectingBrief(target_customer="有需求的人"), sources)
assert len(result["decisions"]) == 20
assert len(result["leads"]) == 20
assert result["analysis_batches"] == 2
assert result["analysis_usage"]["total_tokens"] == 20
assert {row["source_id"] for row in requests[1][1]} == {"n", "c"}
reply = next(row for row in result["leads"] if row["id"] == "r17")
assert reply["author"] == "回复者17"
assert reply["parent_source_id"] == "c"