mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:11:59 +00:00
111 lines
6.6 KiB
Python
111 lines
6.6 KiB
Python
"""上游查库全失败时,报告照实说没查到(engine/nodes/report.failed_queries)。
|
||
|
||
2026-10 部署上撞过:数据源连不上,Agent 的六次查询全部「查询失败」,证据目录里一次查询都没有;写作模型照着上游文字里的
|
||
SQL 和列名写出 [[v:Q1.r0.…]],重写一次还是那样,答案里全是没填上的 ⟦?v:Q1…⟧。现在写作提示里多一节「没有查到的数据」,
|
||
一次成功的都没有时不许写单元格引用、照实写没查到和原因;重写请求里同一句。上游节点的调用记录照 Agent 产出的形状写
|
||
(tool_calls:tool、args、ok、result),数据源和 SQL 都是虚构的。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from tests.test_report_node import engine_up, events, finish, node, script # noqa: F401
|
||
|
||
FAILED_CALLS = ("[{'tool': 'db_query__shop', 'args': {'sql': 'SELECT COUNT(*) AS n FROM orders'}, 'ok': True, "
|
||
"'result': '查询失败:connect() got an unexpected keyword argument'}, "
|
||
"{'tool': 'db_query__shop', 'args': {'sql': 'SELECT COUNT(*) AS n FROM orders'}, 'repeat': True}, "
|
||
"{'tool': 'db_schema__shop', 'args': {}, 'ok': True, 'result': '表 orders …'}]")
|
||
|
||
|
||
def graph(calls: str = FAILED_CALLS) -> dict:
|
||
nodes = [
|
||
node("start", "input", fields=[{"name": "q", "default": "订单有多少"}]),
|
||
node("agent1", "transform", mode="expression",
|
||
expression="{'text': '三条查询都失败了:连不上数据源', 'tool_calls': " + calls + "}"),
|
||
node("write", "report", instructions="回答:{{ input.q }}"),
|
||
node("out", "output", fields=[{"name": "答案", "value": "{{ nodes.write.text }}"}]),
|
||
]
|
||
chain = ["start", "agent1", "write", "out"]
|
||
return {"nodes": nodes, "edges": [{"source": a, "target": b} for a, b in zip(chain, chain[1:])]}
|
||
|
||
|
||
def _prompt(messages) -> str:
|
||
return "\n".join(str(m.content) for m in messages)
|
||
|
||
|
||
async def test_the_writer_is_told_the_queries_failed_and_not_to_cite(monkeypatch):
|
||
seen = script(monkeypatch, "订单有 [[v:Q1.r0.n]] 单。", "没有查到订单数:数据源连接失败,请检查数据源连接。")
|
||
run = await finish(graph())
|
||
first = _prompt(seen[0])
|
||
assert "## 没有查到的数据" in first and "上游有 1 次数据查询失败了" in first, first
|
||
assert "db_query__shop:SELECT COUNT(*) AS n FROM orders(查询失败:connect() got an unexpected keyword argument)" in first
|
||
assert "这次一次成功的查询都没有:不要写 [[v:…]] 单元格引用" in first
|
||
assert "db_schema__shop" not in first.split("## 没有查到的数据")[1].split("\n\n")[0], "看表结构不算查询失败"
|
||
# 第一稿照旧引用了不存在的 Q1:重写请求里同一句要求
|
||
assert len(seen) == 2 and "这次一次成功的查询都没有" in str(seen[1][-1].content)
|
||
assert run.output["答案"] == "没有查到订单数:数据源连接失败,请检查数据源连接。"
|
||
[warned] = [e for e in await events(run.id, "log") if (e.data or {}).get("code") == "report_queries_failed"]
|
||
assert warned.data["failed"] == 1 and warned.data["any_query"] is False
|
||
|
||
|
||
async def test_without_failed_queries_the_prompt_is_unchanged(monkeypatch):
|
||
seen = script(monkeypatch, "没有数据。")
|
||
calls = "[{'tool': 'db_schema__shop', 'args': {}, 'ok': True, 'result': '表 orders …'}]"
|
||
run = await finish(graph(calls))
|
||
assert "没有查到的数据" not in _prompt(seen[0])
|
||
assert not [e for e in await events(run.id, "log") if (e.data or {}).get("code") == "report_queries_failed"]
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 失败之后又改对了的那一次不算「没有查到」
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
def _failed(calls: list[dict]) -> list[dict]:
|
||
from app.engine.nodes.report import failed_queries
|
||
from app.engine.schema import GraphSpec
|
||
|
||
g = graph("[]")
|
||
spec = GraphSpec.model_validate(g)
|
||
state = {"nodes": {"agent1": {"text": "x", "tool_calls": calls}}}
|
||
return failed_queries(state, spec, spec.node_map()["write"])
|
||
|
||
|
||
BAD = {"tool": "db_query__shop", "args": {"sql": "SELECT line AS 产线, SUM(qty) AS 产量 FROM output GROUP BY SUM(qty)"},
|
||
"ok": True, "result": "查询失败:aggregate functions are not allowed in GROUP BY"}
|
||
|
||
|
||
def _good(sql: str, tool: str = "db_query__shop") -> dict:
|
||
return {"tool": tool, "args": {"sql": sql}, "ok": True, "result": '{"columns": ["产线", "产量"], "rows": [["甲线", 10]]}'}
|
||
|
||
|
||
def test_a_failure_fixed_by_the_next_query_is_superseded():
|
||
"""取数 Agent 写错一条(GROUP BY 里放了聚合),下一条马上改对、读的表覆盖了失败那条:这次失败已被替代,不进写作提示。"""
|
||
fixed = _good("SELECT line AS 产线, SUM(qty) AS 产量 FROM output GROUP BY line")
|
||
assert _failed([BAD, fixed]) == []
|
||
# 解析不了的失败 SQL:同一个数据源之后有成功的查询,也算被替代
|
||
garbled = {**BAD, "args": {"sql": "SELEC line FROM"}}
|
||
assert _failed([garbled, fixed]) == []
|
||
|
||
|
||
def test_a_failure_not_covered_by_a_later_query_stays():
|
||
"""之后成功的查询读的是别的表(没覆盖失败那条读的表)、或者成功在失败之前:照旧列进「没有查到的数据」。"""
|
||
other = _good("SELECT COUNT(*) AS n FROM stations")
|
||
[left] = _failed([BAD, other])
|
||
assert "aggregate functions" in left["reason"]
|
||
before = _good("SELECT line AS 产线, SUM(qty) AS 产量 FROM output GROUP BY line")
|
||
assert len(_failed([before, BAD])) == 1, "成功在失败之前,不算替代了它"
|
||
elsewhere = _good("SELECT line, SUM(qty) FROM output GROUP BY line", tool="db_query__crm")
|
||
assert len(_failed([BAD, elsewhere])) == 1, "别的数据源上的成功不算"
|
||
|
||
|
||
async def test_when_every_query_failed_the_writer_still_says_none_succeeded(monkeypatch):
|
||
calls = ("[{'tool': 'db_query__shop', 'args': {'sql': 'SELECT line, SUM(qty) FROM output GROUP BY SUM(qty)'}, "
|
||
"'ok': True, 'result': '查询失败:aggregate functions are not allowed'}, "
|
||
"{'tool': 'db_query__shop', 'args': {'sql': 'SELECT line, SUM(qty) FROM output GROUP BY 1'}, "
|
||
"'ok': True, 'result': '查询失败:connection reset'}]")
|
||
seen = script(monkeypatch, "没有查到产量:查询失败。")
|
||
run = await finish(graph(calls))
|
||
first = _prompt(seen[0])
|
||
assert "上游有 2 次数据查询失败了" in first and "这次一次成功的查询都没有" in first
|
||
[warned] = [e for e in await events(run.id, "log") if (e.data or {}).get("code") == "report_queries_failed"]
|
||
assert warned.data["failed"] == 2
|