mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 08:01:58 +00:00
135 lines
7.1 KiB
Python
135 lines
7.1 KiB
Python
"""助手搭图时就发现「查的区间里表里没有数据」(copilot_gen.GraphChecker.coverage、data/coverage.sql_gaps)。
|
|
|
|
部署上撞过:周报的取数节点用一张只存最新一天的表算上周、上上周,跑完才发现全是 0。现在:
|
|
|
|
- 自查:助手搭的图里工具节点写死的 SQL,按今天跑一遍覆盖检查(不跑原 SQL,只跑算界和现查范围的两条小查询,按表去重、
|
|
限时);完全不重叠的作为一条问题交回模型改一轮(换有这段数据的表,或者在说明里照实告诉用户);修正后仍在的不挡运行,
|
|
作为提醒留在检查结果里;
|
|
- look / db_schema 的剖析信息:日期范围最早和最晚是同一天时加一句「可能每天整表重灌:算历史周期请换有历史的表」;
|
|
- 助手提示:写按周期取数的 SQL 前先看表的日期范围。
|
|
|
|
库是现造的 SQLite 文件,表名、列名、数字都是虚构的。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
import uuid
|
|
|
|
import pytest
|
|
from langchain_core.messages import AIMessageChunk
|
|
|
|
from app.data import introspect
|
|
from app.data.engine import engines
|
|
from app.data.schema_view import PROFILE_HINT_PROMPT, profile_lines
|
|
from app.db.base import SessionLocal
|
|
from app.db.models import DataSource
|
|
from tests.test_copilot_answer_ops import _sse, _use, client # noqa: F401
|
|
|
|
NO_DATA_SQL = "SELECT COUNT(*) AS n FROM gate_events WHERE event_date >= '20261012' AND event_date < '20261019'"
|
|
HAS_DATA_SQL = "SELECT COUNT(*) AS n FROM gate_events WHERE event_date >= '20200101'"
|
|
|
|
|
|
@pytest.fixture
|
|
async def plant(tmp_path):
|
|
path = tmp_path / f"{uuid.uuid4().hex[:8]}.db"
|
|
with sqlite3.connect(path) as db:
|
|
db.executescript("CREATE TABLE gate_events (event_date TEXT, member_id INTEGER);"
|
|
"INSERT INTO gate_events VALUES ('20200101', 1), ('20200101', 2);")
|
|
name = f"plant_{uuid.uuid4().hex[:6]}"
|
|
async with SessionLocal() as session:
|
|
row = DataSource(name=name, kind="sqlite", database=str(path), readonly=True, enabled=True)
|
|
session.add(row)
|
|
await session.flush()
|
|
row.schema_cache = await introspect.introspect(row)
|
|
await session.commit()
|
|
source_id = row.id
|
|
async with SessionLocal() as session:
|
|
source = await session.get(DataSource, source_id)
|
|
yield name, source
|
|
await engines.invalidate(source_id)
|
|
|
|
|
|
def _node(nid: str, ntype: str, config: dict) -> dict:
|
|
return {"id": nid, "type": ntype, "position": {"x": 0, "y": 0}, "data": {"label": nid, "config": config}}
|
|
|
|
|
|
def _graph(name: str, sql: str) -> tuple[dict, list]:
|
|
nodes = {"start": _node("start", "input", {"fields": [{"name": "question"}]}),
|
|
"fetch": _node("fetch", "tool", {"tool": f"db_query__{name}", "args": {"sql": sql}}),
|
|
"out": _node("out", "output", {"fields": [{"name": "n", "value": "{{ nodes.fetch }}"}]})}
|
|
return nodes, [{"source": "start", "target": "fetch"}, {"source": "fetch", "target": "out"}]
|
|
|
|
|
|
async def test_the_checker_returns_it_once_then_keeps_it_as_a_warning(plant):
|
|
from app.api.copilot_gen import GraphApplier, GraphChecker
|
|
|
|
name, source = plant
|
|
nodes, edges = _graph(name, NO_DATA_SQL)
|
|
checker = GraphChecker(GraphApplier(nodes, edges), sources=[source])
|
|
[issue] = [i for i in await checker.issues() if i["code"] == "sql_no_data"]
|
|
assert issue["level"] == "error" and issue["node_id"] == "fetch" and issue["field"] == "args.sql"
|
|
assert "查的区间是 2026-10-12 到 2026-10-18,但 gate_events 的 event_date 只有 2020-01-01 这一天的数据" \
|
|
in issue["message"]
|
|
assert "换有这段数据的表" in issue["for_model"] and "explanation" in issue["for_model"]
|
|
# 交回模型改过一轮:之后不再挡;收尾时作为提醒
|
|
assert not [i for i in await checker.issues() if i["code"] == "sql_no_data"]
|
|
[warning] = await checker.coverage(level="warning")
|
|
assert warning["level"] == "warning" and warning["code"] == "sql_no_data"
|
|
|
|
|
|
async def test_a_window_with_data_and_templated_sql_are_left_alone(plant):
|
|
from app.api.copilot_gen import GraphApplier, GraphChecker
|
|
|
|
name, source = plant
|
|
for sql in (HAS_DATA_SQL, "SELECT COUNT(*) AS n FROM gate_events WHERE event_date >= '{{ input.day }}'"):
|
|
nodes, edges = _graph(name, sql)
|
|
checker = GraphChecker(GraphApplier(nodes, edges), sources=[source])
|
|
assert not [i for i in await checker.issues() if i["code"] == "sql_no_data"], sql
|
|
|
|
|
|
async def test_the_stream_asks_for_one_repair_then_runs_with_a_warning(client, monkeypatch, plant): # noqa: F811
|
|
name, _ = plant
|
|
nodes, edges = _graph(name, NO_DATA_SQL)
|
|
build = [{"op": "plan", "summary": "数一数"},
|
|
*({"op": "add_node", "node": {"id": n["id"], "type": n["type"], "label": n["id"],
|
|
"config": n["data"]["config"]}} for n in nodes.values()),
|
|
*({"op": "add_edge", "edge": e} for e in edges), {"op": "done", "explanation": "数一数", "run": True}]
|
|
keep = [{"op": "done", "explanation": "gate_events 只有 2020-01-01 的数据,这段时间没有数据"}]
|
|
seen: list[str] = []
|
|
|
|
class Scripted:
|
|
async def astream(self, messages):
|
|
human = next(text for role, text in messages if role == "human")
|
|
seen.append(human)
|
|
for op in (keep if "上线前自查" in human else build):
|
|
yield AIMessageChunk(content=json.dumps(op, ensure_ascii=False) + "\n")
|
|
|
|
_use(monkeypatch, Scripted())
|
|
got = await _sse(client, {"instruction": "上周到访多少次", "intent": "answer"})
|
|
[repairing] = [op for op in got if op["op"] == "check" and op["status"] == "repairing"]
|
|
assert [i["code"] for i in repairing["issues"]] == ["sql_no_data"]
|
|
assert any("上线前自查" in h and "换有这段数据的表" in h for h in seen), "交回模型改一轮"
|
|
[passed] = [op for op in got if op["op"] == "check" and op["status"] == "passed"]
|
|
assert [w["code"] for w in passed["warnings"]] == ["sql_no_data"]
|
|
[final] = [op for op in got if op["op"] == "final"]
|
|
assert final["next"] == "run", "修正后仍在的不挡运行"
|
|
assert any(i["code"] == "sql_no_data" and i["level"] == "warning" for i in final["issues"])
|
|
|
|
|
|
def test_a_one_day_profile_says_so():
|
|
facts = {"at": "2026-10-18T02:00:00+00:00", "date_ranges": [{"column": "event_date", "min": "20261017",
|
|
"max": "20261017"}]}
|
|
lines = profile_lines(facts, set(), None)
|
|
assert any("剖析时这张表的 event_date 只有一天(2026-10-17)的数据,可能每天整表重灌:算历史周期请换有历史的表" in ln
|
|
for ln in lines), lines
|
|
assert lines[-1].strip() == PROFILE_HINT_PROMPT
|
|
wide = {**facts, "date_ranges": [{"column": "event_date", "min": "20260101", "max": "20261017"}]}
|
|
assert not any("整表重灌" in ln for ln in profile_lines(wide, set(), None))
|
|
|
|
|
|
def test_the_copilot_is_told_to_look_at_the_dates_first():
|
|
from app.api.copilot import NODE_REFERENCE
|
|
|
|
assert "先看表的日期范围" in NODE_REFERENCE and "只有一天的数据" in NODE_REFERENCE
|