mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 10:11:58 +00:00
2b9dcc55e3
- 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(行为不变,影子起草和写库
用同一套并入规矩、同一个模型建法)。
160 lines
9.1 KiB
Python
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 }}"}})
|