Files
AgentLab/backend/tests/test_ledger_derive.py
ThearchyHelios 2b9dcc55e3 评测题集与期望答案(第 2 波 W2-c)第一步:模型、派生图、期望的三种来源、目录快照与影子起草
- db/models_eval.py:EvalSuite、EvalCase、EvalRun、EvalResult 一次定全(第 3 波的执行、评分直接用),另加
  EvalCatalogSnapshot 登记目录快照(谁、何时、来源、影子起草的进度和用量)。EvalResult.job_id 是 eval_run_id 的
  别名:后台任务运行时的收尾按 task_model.job_id 认逐项。预算缺省按评测-D9(同时 2 题、每题 900 秒、单次 2 美元、
  每日 5 美元),这一波只定字段。
- app/ledger/derive.py:全站唯一的「从钉住版本派生只读图」的生成器(总方案 X3)。这一波实现口径探针:拷出口径卡读
  的取数节点(id、配置、图级 defaults 原样)+ caliber_from 固定到那一版的口径卡;只认可复用的源头卡,取数节点只许
  引用入口字段,别的照实拒收。对账(这次的取数 + 口径 + 对账卡)、召回(新旧两版带前缀、caliber_from.nodes 改名
  映射)的扩展点写在模块说明里,assemble / caliber_block 直接可用。
- app/eval/cases.py、suites.py:题目和题集的形状与校验。数值期望不许人填(评测-D4,写了 value 一类的键一律拒收),
  文字要点单列、不计入准确率;只做单轮(评测-D12,history 必须为空);变量只认定时任务那份 {{ schedule.* }};
  题目指纹去掉确认人、出处运行、升版处置这些记录项。
- app/eval/gold.py:SQL 题、口径题、从运行认定三种期望。每次都以来源 eval、只读 + 遮罩的探索运行重算,期望值取封存的
  查询快照(或口径卡的指标集);只读守卫拒写语句、用到遮罩列的拒收(认法同证据面板的派生遮罩)、取不到值 / 失败 /
  超时 / 停在人工确认上各写各的原因。口径升版用和正式运行同一个 unresolved_caliber_upgrades 判、退役按
  subgraph_pin_problems 判,待处置就跳过;recompute / dual / incomparable 三选一、没有缺省。作答前后各算一次
  (bracket / settle)给第 3 波的执行调。
- app/eval/snapshots.py:目录快照存成 kind=eval_catalog_snapshot 的内容寻址工件(读法即 data/catalog_view.py 定的
  那一种);影子起草是不占数据源队的后台任务,起草写进内存里的目录副本,不调 write_entry、不碰线上目录、不查业务库,
  只起草题集用得到的表,开始前估算花费;停止、出错的不出快照。
- data/catalog_draft.py 只加入口 run_shadow_draft(同一个起草引擎,唯一不同是不去业务库数有效行过滤的排除比例);
  data/catalog.py 把写库前的并入抽成纯函数 merge_draft、建起草模型抽成 build_draft_model(行为不变,影子起草和写库
  用同一套并入规矩、同一个模型建法)。
2026-10-16 13:49:53 +08:00

160 lines
9.1 KiB
Python

"""从钉住版本派生只读图(app/ledger/derive.py;总方案 X3:评测的口径题、账本的对账与召回共用这一个生成器)。
- load_pinned:只认可复用的源头口径卡;工作流、版本、节点不在、不是口径卡、转抄别处、输入不是查库的格、取数节点引用了
入口字段以外的值,照实拒收(NotDerivable,带机器码)。
- caliber_probe:入口字段照抄、取数节点 id 和配置原样、口径卡 caliber_from 固定到那一版、「缺输入时」照抄;图能通过
校验;同样的输入永远得到同一张图。
- 扩展点:带前缀的段改名并写出 caliber_from.nodes 映射;两份口径不带前缀放进一张图会撞 id、直接报错。
工作流、库、表都是虚构的。
"""
from __future__ import annotations
import pytest
from app.db.base import SessionLocal
from app.db.models import Workflow, WorkflowVersion
from app.engine.schema import GraphSpec, validate_graph
from app.ledger import derive
def node(nid, ntype, label=None, **config):
return {"id": nid, "type": ntype, "position": {"x": 7, "y": 9}, "data": {"label": label or nid, "config": config}}
def chain(*nodes, defaults=None):
graph = {"nodes": list(nodes), "edges": [{"source": a["id"], "target": b["id"]} for a, b in zip(nodes, nodes[1:])]}
if defaults:
graph["defaults"] = defaults
return graph
FIELDS = [{"name": "start", "required": True, "type": "date"}, {"name": "region", "default": "华东"}]
FETCH_SQL = ("SELECT SUM(amount) AS gmv, COUNT(*) AS n FROM orders WHERE paid_date >= '{{ input.start }}' "
"AND region = '{{ input.region | trim }}'")
def weekly(fetch_sql: str = FETCH_SQL, **card) -> dict:
return chain(node("start", "input", fields=FIELDS),
node("fetch", "tool", "取数", tool="db_query__shop_x", args={"sql": fetch_sql}, assign_to="rows"),
node("card", "metrics", "周销售口径卡", caliber="周销售", caliber_version="v2", on_missing="null",
metrics=[{"id": "gmv", "name": "销售额", "expression": "cell(nodes.fetch, 0, 'gmv')"},
{"id": "avg", "expression": "cell(nodes.fetch, 0, 'gmv') / cell(nodes.fetch, 0, 'n')"}],
**card),
node("write", "report", instructions="写周报"),
node("out", "output"), defaults={"approval": "dangerous"})
async def publish(graph: dict, *, level: str | None = "published") -> str:
async with SessionLocal() as session:
wf = Workflow(name=f"派生_{id(graph)}", graph=graph, status="published", published_version=1, version=1)
session.add(wf)
await session.flush()
session.add(WorkflowVersion(workflow_id=wf.id, version=1, graph=graph, level=level))
await session.commit()
return wf.id
async def pinned_of(graph: dict, **kw) -> derive.Pinned:
wf_id = await publish(graph, **kw)
async with SessionLocal() as session:
return await derive.load_pinned(session, wf_id, 1, "card")
async def test_the_probe_copies_the_fetch_and_pins_the_card():
pinned = await pinned_of(weekly())
assert pinned.published and not pinned.retired and pinned.reads == (("fetch", "db_query__shop_x", ("gmv", "n")),)
assert [m["id"] for m in pinned.metrics] == ["gmv", "avg"] and pinned.caliber == "周销售 @ v2"
probe = derive.caliber_probe(pinned)
nodes = {n["id"]: n for n in probe.graph["nodes"]}
assert [n["id"] for n in probe.graph["nodes"]] == [derive.ENTRY_ID, "fetch", "card", derive.EXIT_ID]
# 入口字段照抄(必填、缺省值、类型都不变)
assert nodes[derive.ENTRY_ID]["data"]["config"]["fields"] == FIELDS
# 取数节点 id、配置原样
original = weekly()["nodes"][1]
assert nodes["fetch"]["data"] == original["data"] and nodes["fetch"]["type"] == "tool"
# 口径卡固定到那一版那张卡;「缺输入时」照抄;本地不写指标式
card = nodes["card"]["data"]["config"]
assert card == {"caliber_from": {"workflow_id": pinned.workflow_id, "workflow_version": 1, "node_id": "card"},
"on_missing": "null"}
# 图级 defaults 照抄:节点上没写的项按同样的规则取值
assert probe.graph["defaults"] == {"approval": "dangerous"}
assert nodes[derive.EXIT_ID]["data"]["config"]["fields"] == [{"name": "周销售口径卡",
"value": "{{ nodes.card.text }}"}]
assert probe.card_ids == ("card",) and probe.origin["kind"] == "caliber_probe"
assert probe.origin["from"][0]["workflow_version"] == 1
assert validate_graph(GraphSpec.model_validate(probe.graph)).ok
# 确定性:同样的版本永远得到同一张图
assert derive.caliber_probe(pinned).graph == probe.graph
# 处置写在本地口径卡上,交给和正式运行同一个升版判断
assert derive.caliber_probe(pinned, upgrade_policy="dual").graph["nodes"][2]["data"]["config"][
"upgrade_policy"] == "dual"
@pytest.mark.parametrize("graph, code, words", [
(chain(node("start", "input"), node("fetch", "transform", mode="expression", expression="{}", assign_to="k"),
node("card", "metrics", metrics=[{"id": "x", "expression": "vars.k.x"}]), node("out", "output")),
derive.NOT_REUSABLE, "输入不全是查库结果"),
(chain(node("start", "input"), node("fetch", "tool", tool="db_query__shop_x", args={"sql": "SELECT 1 AS v"}),
node("card", "metrics", metrics=[{"id": "x", "expression": "cell(nodes.fetch, 1, 'v')"}]),
node("out", "output")), derive.NOT_REUSABLE, "别的行号"),
(chain(node("start", "input"), node("card", "metrics", caliber_from={"workflow_id": "w", "workflow_version": 1,
"node_id": "c"}), node("out", "output")),
derive.NOT_REUSABLE, "转抄"),
(chain(node("start", "input"), node("card", "report"), node("out", "output")), derive.NOT_CARD, "不是口径卡"),
(weekly("SELECT SUM(amount) AS gmv, COUNT(*) AS n FROM orders WHERE region = '{{ vars.region }}'"),
derive.TEMPLATE, "vars.region"),
(weekly("SELECT SUM(amount) AS gmv, COUNT(*) AS n FROM {{ nodes.pick.table }}"), derive.TEMPLATE, "nodes.pick"),
(weekly("SELECT SUM(amount) AS gmv, COUNT(*) AS n FROM orders -- {{ file_text('x') }}"), derive.TEMPLATE,
"file_text"),
])
async def test_cards_that_cannot_be_derived_say_why(graph, code, words):
with pytest.raises(derive.NotDerivable) as caught:
await pinned_of(graph)
assert caught.value.code == code and words in caught.value.message
async def test_missing_workflows_versions_and_nodes():
wf_id = await publish(weekly())
async with SessionLocal() as session:
for args, words in (((("nope", 1, "card")), "不存在"), (((wf_id, 9, "card")), "没有 v9"),
(((wf_id, 1, "ghost")), "没有节点 ghost")):
with pytest.raises(derive.NotDerivable, match=words) as caught:
await derive.load_pinned(session, *args)
assert caught.value.code == derive.GONE
async def test_draft_versions_load_but_say_they_are_not_published():
pinned = await pinned_of(weekly(), level=None)
async with SessionLocal() as session:
wf = await session.get(Workflow, pinned.workflow_id)
wf.published_version = None
await session.commit()
again = await derive.load_pinned(session, pinned.workflow_id, 1, "card")
assert again.published is False
async def test_prefixed_blocks_rename_and_map_for_two_calibers_in_one_graph():
"""召回(第 3 波)要把新旧两版放进同一张图:带前缀的段改名,口径卡写出改名映射。"""
pinned = await pinned_of(weekly())
old = derive.caliber_block(pinned, prefix="old_")
new = derive.caliber_block(pinned, prefix="new_")
assert [n["id"] for n in old.nodes] == ["old_fetch", "old_card"] and old.rename == {"fetch": "old_fetch"}
assert old.nodes[-1]["data"]["config"]["caliber_from"]["nodes"] == {"fetch": "old_fetch"}
graph = derive.assemble("recall", [old, new], fields=pinned.entry_fields)
assert graph.card_ids == ("old_card", "new_card") and len(graph.origin["from"]) == 2
names = [f["name"] for f in graph.graph["nodes"][-1]["data"]["config"]["fields"]]
assert names == ["周销售口径卡", "周销售口径卡_2"]
# 不带前缀放两份:节点 id 撞了,直接报错,不悄悄拼出一张错图
with pytest.raises(ValueError, match="重复"):
derive.assemble("recall", [derive.caliber_block(pinned), derive.caliber_block(pinned)])
with pytest.raises(ValueError, match="不认识"):
derive.assemble("nope", [old])
def test_template_problem_allows_only_entry_fields():
assert derive.template_problem("q", {"args": {"sql": "SELECT '{{ input.start }}', '{{ input.x | trim }}'"}}) is None
assert derive.template_problem("q", {"args": {"sql": "SELECT 1"}, "limit": 5}) is None
assert "vars.a" in derive.template_problem("q", {"args": {"sql": "{{ vars.a }}"}})
assert "input" in derive.template_problem("q", {"args": {"sql": "{{ input }}"}})