mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 15:31:59 +00:00
a7b8854dc4
业务日期的类型门槛:只有时刻的 TIME / TIMETZ 不再算(一天里的几点几分不是日期,MySQL 还常拿它存时长;和 catalog.is_date_column 同一个口径,记成业务日期后 SQL 检查会对按真实日期列统计的查询一直报错误级),原因说「只有时刻,不含日期」。类型照表结构里记的说「类型是 X」(introspect 存的是 SQLAlchemy 反射出来的类型,SQLite 里写成 TIMESTAMPTZ 的反射成 NUMERIC),没写类型的列反射成「NULL」,按没有声明类型说。被采纳的一边有问题说「被采纳的盲算按时间筛的列……」,未被采纳的一边另说清为什么也要看它(两边都是日期列时,审核人选的才是业务日期按哪一列)。数据源还没有可用的表结构(未探查、探查失败)时说表结构不可用,不说这一列不在表结构里。上传表格建的数据源(origin=upload,节点认出后经候选的 uploaded 交给 derive)日期一律存成文字,原因说「来自上传的表格,表格里的日期一律按文字保存」,不说成用户声明了 TEXT;预告和真写逐字一致。 遮罩:收窄补等值条件、交给盲算的分组取值,都按 SQL 认出取值来自遮罩列的名字(复用审批质询的 inquiry._derived_hidden:起别名、套函数、经子查询或 WITH 转一道、写在列名表里的都认,解析不了又写着遮罩列的整份算)。以前外层按 area 分组、area 在子查询里由 region 改名而来时照常补上「area = '华东'」,差异说明还原成「region = '华东'」进了审批卡、对照工件和目录日志,「华东」也进了盲算的任务;和遮罩列无关的 WITH 照常收窄。按位置取自遮罩列的输入直接交回「输入 vars.res.rows[0][0] 取自设置了遮罩的列,不能用于复算」(同 cell() 的说法),「核对不了」改成「无法核对」。伸进查询结果的行、却认不出取自哪一列的(下标不是字面量、改过形状的结果没带快照 id 和数据源名)计算式里写成「其他输入」,不把卡上的值交给盲算。按位置取的格落在的快照只为认遮罩算遮罩,不再进重算的快照清单:以前同一个指标里装着它整份行的容器输入会对上它、被它的遮罩列挡下。
114 lines
5.7 KiB
Python
114 lines
5.7 KiB
Python
"""按位置取自遮罩列的一格(vars.res.rows[1][2]、nodes.fetch.rows[0][0])在推演之外的两处:期 4 I2a 把它接进了
|
||
recompute 的还原(_positional),用 recompute 的地方都跟着变,这里把变化钉住。
|
||
|
||
- 双盲复算的预检(dual_check._plan_metric 什么都不改先重算一遍):以前这种指标照常盲算,遮着的那一格还会作为字面量
|
||
写进给盲算模型的计算式;现在预检判它用到了遮罩列,按无法复算记,原因不说「推演」(这里不是推演)
|
||
- 运行对比(engine/compare.py):这种输入两边的值都不给,标 masked,和 cell() 取的遮罩格一样
|
||
|
||
库和数字都是编的:号码写成带横线的文本,区域名是「东区」这类。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
|
||
from app.engine import compare
|
||
from app.engine.runner import run_manager
|
||
from tests.fixtures.compare_runs import BASE_ROWS, NEXT_ROWS, metric, scalar_in, week
|
||
from tests.test_evidence_metric_masks import ( # noqa: F401 - 夹具按名字注入(engine_up 自动生效)
|
||
METRICS,
|
||
PHONES,
|
||
SQL,
|
||
client,
|
||
engine_up,
|
||
node,
|
||
set_source,
|
||
shop,
|
||
)
|
||
|
||
|
||
def dual_graph():
|
||
"""取数 → 数据整形(vars.res)→ 口径卡 → 双盲复算(只复算 mix)→ 成果。"""
|
||
nodes = [
|
||
node("start", "input"),
|
||
node("fetch", "tool", tool="db_query__shop", args={"sql": SQL}),
|
||
node("parse", "transform", mode="json", template="{{ nodes.fetch }}", assign_to="res"),
|
||
node("card", "metrics", caliber="遮罩口径", caliber_version="v1", metrics=METRICS),
|
||
node("dual", "dual_check", metrics=["mix"]),
|
||
node("out", "output", fields=[{"name": "结果", "value": "{{ nodes.card.text }}"}]),
|
||
]
|
||
return {"nodes": nodes, "edges": [{"source": a["id"], "target": b["id"]} for a, b in zip(nodes, nodes[1:])]}
|
||
|
||
|
||
async def dual_output(run_id: str) -> dict:
|
||
from sqlalchemy import select
|
||
|
||
from app.core import artifact_store
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import RunEvent
|
||
|
||
async with SessionLocal() as session:
|
||
[finished] = (await session.execute(select(RunEvent).where(
|
||
RunEvent.run_id == run_id, RunEvent.type == "node.finished", RunEvent.node_id == "dual"))).scalars()
|
||
return artifact_store.load(finished.data["artifact"])
|
||
|
||
|
||
async def test_the_dual_check_does_not_blind_compute_a_metric_reading_a_masked_cell_by_position(shop):
|
||
"""mix = round(cell(nodes.fetch, 1, 'amount') + len(vars.res.rows[1][2]), 1):cell() 那一格不在遮罩列上,按位置取的
|
||
号码在。认出它取自遮罩列,按无法复算记缺口,原因点名是哪个输入、不说「推演」;没遮罩时预检照常通过、指标进入盲算。"""
|
||
import asyncio
|
||
|
||
from app.db.base import SessionLocal
|
||
from app.db.models import Run
|
||
|
||
async def run(options):
|
||
await set_source(shop, options)
|
||
run_id = (await run_manager.start(graph=dual_graph(), input_payload={})).id
|
||
for _ in range(400):
|
||
await asyncio.sleep(0.05)
|
||
async with SessionLocal() as session:
|
||
row = await session.get(Run, run_id)
|
||
if row.status in ("succeeded", "failed") and row.manifest_seq is not None:
|
||
await run_manager.wait_idle(run_id)
|
||
assert row.status == "succeeded", row.error
|
||
return await dual_output(run_id)
|
||
raise AssertionError(f"运行 {run_id} 没跑完:{row.status}")
|
||
|
||
masked = await run({"mask_columns": ["phone"]})
|
||
[mix] = masked["metrics"]
|
||
assert mix["metric_id"] == "mix" and mix["status"] == "unchecked"
|
||
assert mix["reason"] == "输入 vars.res.rows[1][2] 取自设置了遮罩的列,不能用于复算"
|
||
assert "推演" not in json.dumps(masked, ensure_ascii=False)
|
||
assert not [p for p in PHONES if p in json.dumps(masked, ensure_ascii=False)]
|
||
assert any("遮罩" in gap for gap in masked["issuance_gaps"])
|
||
|
||
# 没遮罩:预检通过,原因不再是遮罩(盲算模型没配,按盲算没交回数记,但已经走到了盲算这一步)
|
||
plain = await run({})
|
||
[mix] = plain["metrics"]
|
||
assert "遮罩" not in (mix.get("reason") or "") and "无法按原表达式重算" not in (mix.get("reason") or "")
|
||
|
||
|
||
def region_len(fetch, bot, rows, bot_rows):
|
||
"""东区名称的字数:按位置取自取数结果的第 0 行第 0 列(region)。"""
|
||
return [metric("rlen", "len(nodes.fetch.rows[0][0])", len(rows[0][0]),
|
||
[scalar_in("nodes.fetch.rows[0][0]", rows[0][0], "fetch", "tool")], name="首个区域名字数",
|
||
unit="字", decimals=0)]
|
||
|
||
|
||
def test_the_compare_page_withholds_a_masked_cell_read_by_position():
|
||
"""运行对比:按位置取自遮罩列(region)的一格两边都不给值、标 masked,和 cell() 取的遮罩格一样;没遮罩时照给。"""
|
||
def masked(run):
|
||
run.masked = {a: frozenset({"region"}) for a in run.queries}
|
||
return run
|
||
|
||
result = compare.compare(masked(week("r1", metrics=region_len)).side(),
|
||
masked(week("r2", NEXT_ROWS, metrics=region_len)).side())
|
||
[rlen] = [m for m in result["metrics"] if m["metric_id"] == "rlen"]
|
||
[inp] = rlen["inputs"]
|
||
assert inp["masked"] is True and inp["base"] is None and inp["target"] is None
|
||
assert "东区" not in json.dumps(result, ensure_ascii=False)
|
||
|
||
shown = compare.compare(week("r1", metrics=region_len).side(), week("r2", NEXT_ROWS, metrics=region_len).side())
|
||
[rlen] = [m for m in shown["metrics"] if m["metric_id"] == "rlen"]
|
||
[inp] = rlen["inputs"]
|
||
assert not inp.get("masked") and inp["base"] == BASE_ROWS[0][0] and inp["target"] == NEXT_ROWS[0][0]
|