mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 06:11:58 +00:00
587bf7e27c
起因是一次探索运行:Copilot 改节点时把工具绑定整份抹掉,agent 没有工具只能写「假设调用」, 校验节点的自动修复编出 total_count=0 让 schema 通过,协作成员把工具调用标记当文字输出了四轮, 轮数用完后节点把原话当成功结果交出去——运行标成已完成,数据库一次都没查。 平台可信度(先写失败测试) - Copilot 修改节点改为合并:没写的字段保留,null 才删除;协作成员按名字合并。前后端同一套规则, 工具集合变小时自查报警、改图回执列出变化 - 新校验规则:提示词点名了某个工具而节点没有绑定它,判错误,Copilot 自查会交回去修 - 校验修复只许改格式:冒出原文没有的值就判修复无效;修复这次模型调用计入用量 - 识别模型把工具调用写成文字的情况(DSML、<tool_call> 等标记):纠正一次,仍然如此就判失败并说明原因 - 协作团队用完轮数不再按成功交付:默认失败,可选降档,点出一次都没派到的成员 - 审批恢复后,同一节点里已经执行过的工具调用复用结果,不再重复执行 后端 - 查询时限交给数据库执行(Oracle call_timeout、PG statement_timeout、MySQL max_execution_time、 SQLite progress handler),可按数据源配置;超时报错与实际耗时一致 - 等审批的运行可以真正取消;会话列表带最后一轮状态;测连接结果持久化;探查结构可只看不存; 自定义工具可草稿试跑、写坏的参数定义保存时拒绝;MCP 服务改动后作废旧工具缓存; 检索命中带贡献分解;重建索引在后台跑并报进度;报错统一收进 core/errors 前端 - 公共层:管理页共用件、失败说明(lib/explain)、健康状态进公共库;目录按表各自加载并有超时, 一张表卡住不再拖垮整站;上传显示真实字节进度;离线横幅只播报不变的句子 - 画布:卡片只说游标所在的那一次执行;等审批时可放弃运行;团队收尾判定与未完成说清楚; 回放用量取那一刻的值 - 编排页:Copilot 合并语义与工具变化回执;「用完轮数时」选项;正式运行期间锁住所有改图入口 - 回答流 / 问数据:新失败类型说人话并给下一步;画布与右栏互相定位;数据源范围、回收站、重新核对 - 记录页:「航迹」页签复用画布时间轴,可拖动回放、列出关键时刻;列表行带执行 / 等人 / 失败缩略条 - 外壳:统一的离开前确认(data router),导航、⌘K、快捷键、后退都会先问 验证(沙箱):tsc 干净,pytest 838 passed(两遍),14 个检查脚本中 13 个全绿; check-ui 余 1 项旧断言(按旧日期格式找运行行),3c 更新。
137 lines
6.2 KiB
Python
137 lines
6.2 KiB
Python
"""换个 schema 看看,不擅自改缓存;表结构给结构化的列;卡片知道缓存是按哪个 schema 探的。
|
|
|
|
「换个 schema 看看」以前一探就把缓存换成那个 schema 的结构——哪怕用户随后选了
|
|
「只看看,不改」,助手此刻看到的已经是另一套表了。前端只好再探一次配置里的
|
|
schema 把缓存换回来,而那一次要是失败,缓存和配置就对不上,只剩一条会消失的 toast。
|
|
|
|
表结构以前只有一段给模型看的文本,前端照着文本格式去解析列,格式一改就全乱。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from app.main import app
|
|
|
|
|
|
@pytest.fixture
|
|
async def client():
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
|
yield c
|
|
|
|
|
|
@pytest.fixture
|
|
async def source(client, tmp_path):
|
|
path = tmp_path / "preview.db"
|
|
db = sqlite3.connect(path)
|
|
db.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, store TEXT NOT NULL, amount REAL)")
|
|
db.execute("CREATE TABLE sales_daily (day TEXT, total REAL)")
|
|
db.execute("CREATE VIEW v_big_orders AS SELECT * FROM orders WHERE amount > 100")
|
|
db.commit()
|
|
db.close()
|
|
r = await client.post("/api/datasources", json={"name": "preview_src", "kind": "sqlite",
|
|
"database": str(path)})
|
|
assert r.status_code == 201, r.text
|
|
yield r.json()
|
|
await client.delete(f"/api/datasources/{r.json()['id']}")
|
|
|
|
|
|
async def _cache(source_id: str) -> dict:
|
|
from app.db.base import SessionLocal
|
|
from app.db.models import DataSource
|
|
|
|
async with SessionLocal() as session:
|
|
row = await session.get(DataSource, source_id)
|
|
return {"cache": dict(row.schema_cache or {}), "synced": row.schema_synced_at}
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# REQ-7:缓存是按哪个 schema 探的
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def test_cached_schema_says_which_schema_the_cache_came_from(client, source):
|
|
assert source["cached_schema"] is None, "没探过就是没有缓存"
|
|
r = await client.post(f"/api/datasources/{source['id']}/introspect")
|
|
assert r.json()["cached_schema"] == "", "按默认 schema 探的写空串,和「没探过」分开"
|
|
r = await client.post(f"/api/datasources/{source['id']}/introspect", params={"schema": "main"})
|
|
assert r.json()["cached_schema"] == "main"
|
|
listed = next(s for s in (await client.get("/api/datasources")).json() if s["id"] == source["id"])
|
|
assert listed["cached_schema"] == "main"
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# REQ-6 ①:只看看,不改缓存
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def test_a_dry_run_shows_what_is_there_without_touching_the_cache(client, source):
|
|
await client.post(f"/api/datasources/{source['id']}/introspect")
|
|
before = await _cache(source["id"])
|
|
|
|
r = await client.post(f"/api/datasources/{source['id']}/introspect",
|
|
params={"schema": "main", "dry_run": "true"})
|
|
assert r.status_code == 200, r.text
|
|
preview = r.json()
|
|
assert preview["dry_run"] is True
|
|
assert preview["schema"] == "main"
|
|
assert preview["table_count"] == 3
|
|
assert set(preview["tables"]) == {"main.orders", "main.sales_daily", "main.v_big_orders"}
|
|
assert preview["schema_error"] == ""
|
|
|
|
after = await _cache(source["id"])
|
|
assert after == before, "只看看也把缓存换掉了——助手此刻看到的就不是配置里那个 schema 了"
|
|
|
|
|
|
async def test_a_dry_run_that_finds_nothing_says_why_and_still_leaves_the_cache(client, source):
|
|
await client.post(f"/api/datasources/{source['id']}/introspect")
|
|
before = await _cache(source["id"])
|
|
|
|
r = await client.post(f"/api/datasources/{source['id']}/introspect",
|
|
params={"schema": "no_such_schema", "dry_run": "true"})
|
|
preview = r.json()
|
|
assert preview["dry_run"] is True and preview["table_count"] == 0
|
|
assert preview["schema_error"], "探失败了要说出来,不能看起来像「这个 schema 是空的」"
|
|
assert "main" in preview["available_schemas"]
|
|
assert await _cache(source["id"]) == before
|
|
|
|
|
|
async def test_without_dry_run_the_cache_is_still_replaced(client, source):
|
|
r = await client.post(f"/api/datasources/{source['id']}/introspect", params={"schema": "main"})
|
|
assert r.json()["table_count"] == 3
|
|
assert (await _cache(source["id"]))["cache"]["schema"] == "main"
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# REQ-6 ②:表结构的结构化列
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def test_table_schema_comes_with_structured_columns(client, source):
|
|
await client.post(f"/api/datasources/{source['id']}/introspect")
|
|
body = (await client.get(f"/api/datasources/{source['id']}/schema",
|
|
params={"table": "orders"})).json()
|
|
assert body["found"] is True
|
|
assert body["kind"] == "table" and body["qualified"] == "orders"
|
|
assert body["detail"].startswith("表 orders"), "给模型看的那段文本照旧"
|
|
assert body["columns"] == [
|
|
{"name": "id", "type": "INTEGER", "pk": True, "not_null": False, "comment": None},
|
|
{"name": "store", "type": "TEXT", "pk": False, "not_null": True, "comment": None},
|
|
{"name": "amount", "type": "REAL", "pk": False, "not_null": False, "comment": None},
|
|
]
|
|
|
|
view = (await client.get(f"/api/datasources/{source['id']}/schema",
|
|
params={"table": "V_BIG_ORDERS"})).json()
|
|
assert view["found"] is True and view["kind"] == "view", "和给模型的那份一样,大小写不敏感"
|
|
|
|
|
|
async def test_an_unknown_table_has_no_columns_but_keeps_the_explanation(client, source):
|
|
await client.post(f"/api/datasources/{source['id']}/introspect")
|
|
body = (await client.get(f"/api/datasources/{source['id']}/schema",
|
|
params={"table": "nope"})).json()
|
|
assert body["found"] is False and body["columns"] == []
|
|
assert "没有 nope" in body["detail"]
|