mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:21:59 +00:00
审批质询遮罩收口(后端):质询工件里的 SQL 检查结果字段照证据面板,片段里的遮罩值照 SQL 换掉
做项 3 时发现的同一类缺口:agent 的查询快照带着 SQL 检查结果(数据源查询工具对照数据目录查出的问题,checks),质询工件接口 原样带出——sql_excerpt 是从这条 SQL 摘的片段(按遮罩列筛选的那个条件,例如 buyer_phone = '13900000001'),for_model 是给模型 的改法、也照抄了片段。事件和工件里的 content.sql 已经换掉遮罩值,旁边的片段没换,等于从旁边把值露出来;证据面板对报告快照 本来就只给约定的几个字段、不给 for_model。 - inquiry_artifact 里的 checks 经 _masked_checks:字段照证据面板的 _query_checks(不交 for_model),sql_excerpt 照查询清单的 SQL 换(_scrub_sql,和遮罩列做等值比较的短的数、日期这样的字也认得出) - 界面不读 checks,只是接口上不再带原值 - 补单元测试和端到端测试(把 SQL 检查换成会摘出遮罩值片段的假检查)。做过去掉实现、测试变红的验证
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
查询快照(queries[].artifact)照回答时的办法遮好:遮罩列(含起了别名、套了函数的)整列遮,别的列里整格是遮罩值的
|
||||
格子也遮,masked 是有格子整格被遮掉的列名(界面把这几列整列画成「已遮罩」);一格文字里夹着遮罩值的(备注里写着
|
||||
手机号),只把那一截换成「已遮罩」,这一列不记进 masked。content.sql 就是事件里记下的那条(遮罩值已换掉),两处
|
||||
一字不差。回答文档
|
||||
一字不差;content.checks(SQL 检查结果)的字段照证据面板,片段 sql_excerpt 里的遮罩值照 SQL 换掉。回答文档
|
||||
(answer.doc_artifact)组装时就遮好了,原样给;回答里引用的报告指标(doc.catalog 里 m: 条目的 artifact,口径卡)
|
||||
也从这里取,界面一律按 doc.catalog[alias].artifact 取。没写进运行记录的回答(recorded 为假)在本进程里留一小时,
|
||||
这段时间里它引用的工件照样能取;之后、或者服务重启过,回 404。别的工件一律 404
|
||||
|
||||
@@ -2189,18 +2189,30 @@ async def inquiry_artifact(run_id: str, artifact_id: str) -> dict[str, Any]:
|
||||
recorded = next((q["sql"] for d in referenced for q in d.get("queries") or []
|
||||
if isinstance(q, dict) and q.get("artifact") == artifact_id and isinstance(q.get("sql"), str)),
|
||||
None)
|
||||
compared, kind = plan.tainted.get(artifact_id, set()), plan.kinds.get(str(content.get("source") or ""))
|
||||
if recorded is not None:
|
||||
shown = {**shown, "sql": recorded}
|
||||
elif isinstance(shown.get("sql"), str):
|
||||
name = str(content.get("source") or "")
|
||||
shown = {**shown, "sql": _scrub_sql(shown["sql"], plan.secrets, plan.tainted.get(artifact_id, set()),
|
||||
plan.kinds.get(name))}
|
||||
shown = {**shown, "sql": _scrub_sql(shown["sql"], plan.secrets, compared, kind)}
|
||||
if "checks" in shown:
|
||||
shown = {**shown, "checks": _masked_checks(shown["checks"], plan.secrets, compared, kind)}
|
||||
out: dict[str, Any] = {"id": artifact_id, "content": shown, "masked": plan.masked[artifact_id]}
|
||||
if gone := plan.gone.get(artifact_id):
|
||||
out["mask_note"] = SOURCE_GONE.format(source="、".join(gone))
|
||||
return out
|
||||
|
||||
|
||||
def _masked_checks(checks: Any, secrets: _Secrets, compared: set[str], kind: str | None) -> list[dict[str, Any]]:
|
||||
"""快照里记下的 SQL 检查结果(数据源查询工具对照数据目录查出的问题)交给质询工件的样子:字段照证据面板
|
||||
(_query_checks),给模型的那句改法不交出去——它照抄了 SQL 片段;SQL 片段(sql_excerpt)摘自同一条 SQL,照查询
|
||||
清单的 SQL 换掉遮罩值(_scrub_sql):SQL 换了、片段没换,等于把换掉的值从旁边露出来。"""
|
||||
from app.api.evidence import _query_checks
|
||||
|
||||
return [{**check, "sql_excerpt": _scrub_sql(check["sql_excerpt"], secrets, compared, kind)}
|
||||
if isinstance(check.get("sql_excerpt"), str) else check
|
||||
for check in _query_checks({"checks": checks})]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BUSY_CONTINUE", "BUSY_DECIDE", "INQUIRY_BUSY", "INQUIRY_DISABLED", "INQUIRY_NOT_OPEN", "INQUIRY_SCHEMA",
|
||||
"INQUIRY_SYSTEM", "NOT_RECORDED", "QUESTION_MAX", "QUOTE_MAX", "Ask", "InquiryRefused", "ask", "inquiry_artifact",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
也一样;遮罩值不误伤阈值、天数、LIMIT(长的数也一样)、提问人自己写的数、报告口径卡里的指标,分档共用的边界、
|
||||
和日期、天数修饰、标签里写着的同一个数也不牵连阈值;SQL 里照原样留着的阈值只在回答正文里不遮,格子照旧按值遮,
|
||||
回答和质询工件对得上;质询工件按值遮只出现在报告快照里的遮罩值;一格文字里夹着遮罩值时回答整份不交,质询工件里
|
||||
只换掉那一截
|
||||
只换掉那一截;质询工件里 SQL 检查结果的片段照 SQL 换,给模型的那句改法不交出去
|
||||
- 接线:设置里的上限、运行固定的数据版本交到 agent 手里(上传表格重传之后查的仍是运行固定的那一版);同名重建的
|
||||
数据源、没固定过版本的上传表格不查
|
||||
- 触顶、失败照样记一条事件,说法给提问人看得懂、说下一步;轮数用完不算查询次数到顶,查询失败、引用解析不了在末尾注明
|
||||
@@ -1286,6 +1286,35 @@ async def test_a_masked_value_inside_a_text_cell_is_cut_out_of_the_artifact(monk
|
||||
await engines.invalidate(source_id)
|
||||
|
||||
|
||||
async def test_sql_check_excerpts_in_the_artifact_are_masked_like_the_sql(monkeypatch, masked_shop, client):
|
||||
"""agent 的查询带着 SQL 检查结果(对照数据目录查出的问题,片段摘自这条 SQL):质询工件里片段照查询清单的 SQL
|
||||
换掉遮罩值,给模型的那句改法照抄了片段,不交出去(同证据面板)。"""
|
||||
from app.tools import datasource
|
||||
|
||||
excerpt = "buyer_phone = '13900000001'"
|
||||
|
||||
async def flagged(checker, sql):
|
||||
return [{"code": "wrong_date_column", "level": "warning", "table": "sales", "column": "buyer_phone",
|
||||
"message": "查询按「买家手机号」筛选", "for_model": f"这条 SQL 按 {excerpt} 筛选",
|
||||
"sql_excerpt": excerpt}] if excerpt in sql else []
|
||||
|
||||
monkeypatch.setattr(datasource, "_sql_checks", flagged)
|
||||
plays = {"买得最多": Play([TOP_BUYER, BUYER_ORDERS], "这位客户共 [[v:Q2.r0.笔数]] 笔。[[see:Q2]]")}
|
||||
Models(monkeypatch, masked_shop, plays=plays)
|
||||
paused = await pause(masked_shop)
|
||||
[approval] = await pending(paused.id)
|
||||
r = await ask(client, paused.id, report="write", unit=None, question="买得最多的客户买过几笔?",
|
||||
approval_id=approval.id)
|
||||
assert r.status_code == 200, r.text
|
||||
_, orders = r.json()["inquiry"]["queries"]
|
||||
got = await client.get(f"/api/runs/{paused.id}/inquiries/artifacts/{orders['artifact']}")
|
||||
assert got.status_code == 200, got.text
|
||||
assert got.json()["content"]["checks"] == [
|
||||
{"code": "wrong_date_column", "level": "warning", "table": "sales", "column": "buyer_phone",
|
||||
"message": "查询按「买家手机号」筛选", "sql_excerpt": "buyer_phone = '已遮罩'"}]
|
||||
assert got.json()["content"]["sql"] == orders["sql"] and "1390000000" not in got.text
|
||||
|
||||
|
||||
#: 同样的四笔订单,手机号存成整数:遮罩值在 SQL 里写成数字
|
||||
PHONES = ("CREATE TABLE sales (id INTEGER PRIMARY KEY, region TEXT, buyer_phone INTEGER, amount INTEGER, is_test INTEGER);"
|
||||
"INSERT INTO sales VALUES (1, '华东', 13900000001, 1200, 0), (2, '华东', 13900000002, 300, 1),"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- 阈值只在同一个数在这条 SQL 别处确实被换掉时才不留(换不换和 SQL 里换字面量是同一套判断):日期、天数、标签里写着
|
||||
同一个短的数,没设遮罩的列上的等值,都不牵连阈值
|
||||
- 快照按列遮,另按值遮:别的列里写着遮罩值的格子也换成「已遮罩」;列名里写着遮罩值的,列名也换;质询工件里,
|
||||
没设遮罩的列里一格文字夹着的遮罩值按子串换(回答一路照旧整份不交)
|
||||
没设遮罩的列里一格文字夹着的遮罩值按子串换(回答一路照旧整份不交),SQL 检查结果的片段照 SQL 换
|
||||
- 组装回答之后还认得出遮罩值的(正文里复述的阈值除外),不交出去
|
||||
- 遮罩值上万个时换一遍也不卡:不现编上万个分支的正则;上千个条件连成一长串照样按结构认,成百个分档边界看不过来的
|
||||
按夹住算,也不卡
|
||||
@@ -669,6 +669,23 @@ def test_a_masked_value_inside_a_text_cell_is_cut_out_of_the_artifact_view():
|
||||
assert inquiry_mod._masked_view(snapshot, set(), secrets)[0] is snapshot
|
||||
|
||||
|
||||
def test_sql_check_excerpts_in_the_artifact_are_masked_like_the_sql():
|
||||
"""质询工件里快照记下的 SQL 检查结果:片段(sql_excerpt)摘自同一条 SQL,照查询清单的 SQL 换掉遮罩值(日期这样的
|
||||
字也认得出是在找哪一天);给模型的那句改法(for_model)照抄了片段,不交出去(同证据面板)。"""
|
||||
secrets = secrets_of({"columns": ["birth_date"], "rows": [["1990-05-01"]]}, {"birth_date"})
|
||||
message = "「客户」的业务日期按「注册日期」计,查询却按「生日」筛选"
|
||||
checks = [{"code": "wrong_date_column", "level": "warning", "table": "customers", "column": "birth_date",
|
||||
"message": message, "for_model": "这条 SQL 按 birth_date = '1990-05-01' 筛选",
|
||||
"sql_excerpt": "birth_date = '1990-05-01'"},
|
||||
{"code": "stock_summed", "level": "info", "table": "balances", "message": "存量跨日期求和",
|
||||
"sql_excerpt": "SUM(balance)"}]
|
||||
assert inquiry_mod._masked_checks(checks, secrets, {"birth_date"}, "sqlite") == [
|
||||
{"code": "wrong_date_column", "level": "warning", "table": "customers", "column": "birth_date",
|
||||
"message": message, "sql_excerpt": "birth_date = '已遮罩'"},
|
||||
{"code": "stock_summed", "level": "info", "table": "balances", "message": "存量跨日期求和",
|
||||
"sql_excerpt": "SUM(balance)"}]
|
||||
|
||||
|
||||
def test_report_values_that_happen_to_be_masked_values_are_shown():
|
||||
"""报告口径卡里的指标恰好和遮罩列里的某个值一样(单笔目标 20000,金额也有 20000 的单):报告里本来就看得到,
|
||||
引用它的回答照常交出去。"""
|
||||
|
||||
Reference in New Issue
Block a user