mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:51:59 +00:00
102 lines
5.6 KiB
Python
102 lines
5.6 KiB
Python
"""报告末尾「分析范围」里的编号点得开(GET /runs/{id}/evidence/analyses/{alias}):那份分析结论的出处链和点开正文里它的判定语
|
|
是同一条(api/evidence._scope_chain),正文没引用的结论(算不了的、可比性检查)也点得开;证据目录里没有的编号 404。
|
|
|
|
真运行、真算子:照 tests/test_analysis_e2e_claims.py 的虚构零售库 shop(数据目录标了地区、渠道是属性),Agent 查两次库、
|
|
做一次变化拆解、一次波动基线(两周历史不够,算不了)、一次可比性检查;写作模型只引用拆解的判定语。模型都换成脚本。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from httpx import ASGITransport, AsyncClient
|
|
from langchain_core.messages import AIMessage, ToolMessage
|
|
|
|
from app.analysis import contract
|
|
from app.core import artifact_store
|
|
from app.engine.evidence import iter_segments
|
|
from app.main import app
|
|
from tests.test_analysis_e2e_claims import ( # noqa: F401 - 夹具按名字引入
|
|
ARGS, BY_REGION, DECOMPOSE, TOOL, TOTALS, _finish, _graph, _report, engine_up, shop,
|
|
)
|
|
|
|
BASELINE = contract.TOOL_NAMES["baseline"]
|
|
COMPARABLE = contract.TOOL_NAMES["comparable"]
|
|
TEXT = "## 本周销售\n\n[[a:A1]]。[[see:A1]]"
|
|
CHAIN = ["analysis", "analysis_method", "analysis_calc", "analysis_input", "analysis_recompute", "analysis_seal"]
|
|
|
|
|
|
def _script(monkeypatch):
|
|
from app.providers import mock_model
|
|
|
|
calls = [(TOOL, {"sql": TOTALS}), (TOOL, {"sql": BY_REGION}), (DECOMPOSE, ARGS),
|
|
(BASELINE, {"query": "Q1", "period": "week", "measure": "gmv", "label": "销售额"}),
|
|
(COMPARABLE, {"query": "Q2", "period": "week", "base": "2026-W41", "target": "2026-W42", "measure": "gmv"})]
|
|
|
|
def decide(model, messages):
|
|
if not model.tools:
|
|
return AIMessage(content=TEXT)
|
|
done = sum(isinstance(m, ToolMessage) for m in messages)
|
|
if done < len(calls):
|
|
name, args = calls[done]
|
|
return AIMessage(content="", tool_calls=[{"name": name, "args": args, "id": f"call_{done}"}])
|
|
return AIMessage(content="销售额下降,主要来自华东。")
|
|
|
|
monkeypatch.setattr(mock_model.MockChatModel, "_decide", decide)
|
|
|
|
|
|
def _graph_with_tools():
|
|
graph = _graph()
|
|
bot = next(n for n in graph["nodes"] if n["id"] == "bot")
|
|
bot["data"]["config"]["tools"] = [TOOL, DECOMPOSE, BASELINE, COMPARABLE]
|
|
return graph
|
|
|
|
|
|
def _steps(chain):
|
|
"""同一步连着几条(几份输入、输入后面接的查询步骤)算一步。"""
|
|
steps = [s["step"] for s in chain]
|
|
return [s for i, s in enumerate(steps) if s.startswith("analysis") and steps[i - 1:i] != [s]]
|
|
|
|
|
|
async def test_scope_aliases_open_the_same_chain_as_the_phrase_in_the_text(monkeypatch):
|
|
_script(monkeypatch)
|
|
row = await _finish(_graph_with_tools())
|
|
assert row.status == "succeeded", row.error
|
|
_, doc, _ = await _report(row)
|
|
scope = doc["analysis_scope"]
|
|
assert [i["alias"] for i in scope["decomposed"]] == ["A1"]
|
|
assert [i["alias"] for i in scope["refused"]] == ["A2"] and [i["alias"] for i in scope["phenomena"]] == ["A3"]
|
|
phrase = next(s for s in iter_segments(doc) if s.get("ref") == "a:A1")
|
|
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
|
cited = (await client.get(f"/api/runs/{row.id}/evidence/segments/{phrase['id']}")).json()
|
|
body = (await client.get(f"/api/runs/{row.id}/evidence/analyses/A1")).json()
|
|
# 和点开正文里的判定语同一条链(同一份结论、同样复算一致),只是没有这一处引用的那几样
|
|
assert _steps(body["chain"]) == CHAIN
|
|
assert [s["step"] for s in body["chain"]] == [s["step"] for s in cited["chain"]]
|
|
assert body["rendered"] == phrase["text"] == body["chain"][0]["phrase"]
|
|
assert body["seal"]["covered"] is True and body["report"]["node_id"] == "write"
|
|
|
|
# 正文没引用的也点得开:算不了的波动基线照实给原因,可比性检查照样复算
|
|
refused = (await client.get(f"/api/runs/{row.id}/evidence/analyses/A2", params={"report": "write"})).json()
|
|
judged = refused["chain"][0]
|
|
assert (judged["op"], judged["status"], judged["reason_code"]) == ("baseline", "refused", "history_short")
|
|
assert judged["phrase"] == scope["refused"][0]["phrase"]
|
|
assert next(s for s in refused["chain"] if s["step"] == "analysis_recompute")["status"] == "same"
|
|
comparable = (await client.get(f"/api/runs/{row.id}/evidence/analyses/A3")).json()
|
|
assert comparable["chain"][0]["method_label"] == contract.method_label("comparable")
|
|
|
|
# 目录里没有的编号、不是分析结论的编号(Q1):404,说清是哪种
|
|
for alias in ("A9", "Q1"):
|
|
missing = await client.get(f"/api/runs/{row.id}/evidence/analyses/{alias}")
|
|
assert missing.status_code == 404 and missing.json()["code"] == "evidence_analysis_not_found"
|
|
|
|
|
|
async def test_a_tampered_report_does_not_open_its_scope(monkeypatch):
|
|
_script(monkeypatch)
|
|
row = await _finish(_graph_with_tools())
|
|
checked, _, _ = await _report(row)
|
|
# 报告文档被改过(哈希对不上):和片段接口同一个错误码,不给出处
|
|
path = artifact_store._path_of(checked["doc_artifact"])
|
|
path.write_bytes(path.read_bytes().replace("本周销售".encode(), "本周营业".encode(), 1))
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
|
res = await client.get(f"/api/runs/{row.id}/evidence/analyses/A1")
|
|
assert res.status_code == 409 and res.json()["code"] == "evidence_doc_tampered"
|