mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:51:57 +00:00
a7e91b16a1
- api/evals.py(骨架已登记、只给搭建者):题集、题目的增删改查(改要带读到的 version,对不上 409;有评测记录的题集不能删,
只能归档)。保存题目时试算一次(细案 01 做法 8):没过只读守卫、用到遮罩列、取不到值、运行失败、超时、口径升版待处置的,
照实回 422(带机器码和 field)、不保存;只改标签、启用的不重新试算。另有只试算不保存、试算已有的题。
- 从运行认定:GET /runs/{id}/queries 列出那次运行的查询(只给 SQL、列名、行数,不给格),POST …/cases/from-run 拷出一条
查询的 SQL 建题,记确认人、时间、出自哪次运行;客户端不能自己写 from_run,要改 SQL 就改成 SQL 题。
- 口径:GET /calibers 和蓝图助手同一份口径清单(能不能复用、为什么);GET/POST …/upgrade 看、处置口径升版,
recompute / dual / incomparable 三选一、没有缺省;不可比的旧题归档、按新版本另建新题。
- 目录快照:当前目录(当场写好)、影子起草的计划和估算、后台影子起草(202)、查看、停止。
- app/eval/transfer.py:题集导出成 JSON(数据源、工作流按名字;不含期望值、答案、运行,从运行认定只带确认人和时间),导入
按名字对上、逐题过同一套校验和同一道试算关,对不上、没过的跳过并写原因,一道都导不进来就不建题集。
- tests/fixtures/eval/retail_suite.json:零售样例题集,配 tests/fixtures/acceptance/retail.py(数据源名 retail_demo,
retail.SOURCE_NAME):支付日归属、退款、取消、华南高于华东,一道「华中区」期望没有数据,一道问法含糊期望反问。测试在
按今天生成的零售库上导入,六道题都试算通过、期望和直接查库一样、陷阱按口径算。
131 lines
7.7 KiB
Python
131 lines
7.7 KiB
Python
"""题目的形状与校验(app/eval/cases.py、suites.py):数值期望不许人填(评测-D4)、文字要点不计入准确率、只做单轮
|
|
(评测-D12)、变量只认定时任务那份、期望和期望来源配得上、题目指纹只看内容。纯函数,不碰库。"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from zoneinfo import ZoneInfo
|
|
|
|
import pytest
|
|
|
|
from app.eval import cases, suites
|
|
from app.eval.cases import CaseInvalid, normalize_case
|
|
|
|
SQL = {"kind": "sql", "datasource_id": "ds1", "sql": "SELECT SUM(amount) AS gmv FROM orders"}
|
|
|
|
|
|
def invalid(body: dict, **kw) -> CaseInvalid:
|
|
with pytest.raises(CaseInvalid) as caught:
|
|
normalize_case(body, **kw)
|
|
return caught.value
|
|
|
|
|
|
@pytest.mark.parametrize("key", cases.HUMAN_VALUE_KEYS)
|
|
def test_numbers_cannot_be_filled_in_by_hand(key):
|
|
e = invalid({"question": "q", "gold": SQL, "expectations": [{"kind": "scalar", "column": "gmv", key: 330}]})
|
|
assert "数值期望不许人填" in e.message and e.field == f"expectations.0.{key}"
|
|
|
|
|
|
def test_text_points_never_count_toward_accuracy():
|
|
fields = normalize_case({"question": "为什么涨了?", "expectations": [
|
|
{"kind": "text", "points": [" 华南门店多 ", "客单价高"], "required": True}]})
|
|
[exp] = fields["expectations"]
|
|
assert exp == {"id": "e1", "kind": "text", "required": False, "points": ["华南门店多", "客单价高"]}
|
|
assert fields["gold"] is None
|
|
|
|
|
|
def test_only_single_turn_cases_this_round():
|
|
assert invalid({"question": "q", "history": [{"q": "上一轮"}], "expectations": [{"kind": "ask"}]}).field == "history"
|
|
assert normalize_case({"question": "q", "history": [], "expectations": [{"kind": "ask"}]})["history"] is None
|
|
|
|
|
|
@pytest.mark.parametrize("body, field", [
|
|
({"question": "q", "expectations": [{"kind": "scalar", "column": "gmv"}]}, "gold"),
|
|
({"question": "q", "gold": SQL, "expectations": [{"kind": "ask"}]}, "gold"),
|
|
({"question": "q", "gold": SQL, "expectations": [{"kind": "ask"}, {"kind": "none"}]}, "expectations"),
|
|
({"question": "q", "gold": SQL, "expectations": [{"kind": "scalar", "metric": "gmv"}]}, "expectations.0.metric"),
|
|
({"question": "q", "gold": SQL, "expectations": [{"kind": "scalar"}]}, "expectations.0.column"),
|
|
({"question": "q", "gold": SQL, "expectations": [{"kind": "scalar", "column": "g", "row": 1, "where": {"a": 1}}]},
|
|
"expectations.0.row"),
|
|
({"question": "q", "gold": SQL, "expectations": [{"kind": "table"}]}, "expectations.0.keys"),
|
|
({"question": "q", "gold": SQL, "expectations": [{"kind": "scalar", "column": "g", "colum": "x"}]},
|
|
"expectations.0.colum"),
|
|
({"question": "q", "gold": SQL, "expectations": [{"kind": "scalar", "column": "g", "tolerance": 2}]},
|
|
"expectations.0.tolerance"),
|
|
({"question": "q", "gold": SQL, "expectations": [{"id": "a", "kind": "none"}, {"id": "a", "kind": "none"}]},
|
|
"expectations.1.id"),
|
|
({"question": "", "expectations": [{"kind": "ask"}]}, "question"),
|
|
({"question": "q", "expectations": []}, "expectations"),
|
|
({"question": "q", "workflow_id": "w1", "expectations": [{"kind": "ask"}]}, "workflow_id"),
|
|
({"kind": "workflow", "workflow_id": "w1", "workflow_version": 1, "expectations": [{"kind": "ask"}]},
|
|
"expectations"),
|
|
({"question": "上周 {{ schedule.lastweek }}", "expectations": [{"kind": "ask"}]}, "question"),
|
|
({"question": "q", "gold": {**SQL, "sql": "SELECT {{ schedule.today | shout }}"},
|
|
"expectations": [{"kind": "none"}]}, "gold.sql"),
|
|
({"question": "q", "gold": {**SQL, "sql": "SELECT {{ schedule.today "}, "expectations": [{"kind": "none"}]},
|
|
"gold.sql"),
|
|
({"question": "q", "gold": {"kind": "caliber", "workflow_id": "w", "version": 1, "node_id": "c",
|
|
"inputs": {"start": "{{ vars.x }}"}}, "expectations": [{"kind": "scalar",
|
|
"metric": "m"}]},
|
|
"gold.inputs"),
|
|
({"question": "q", "gold": {"kind": "caliber", "workflow_id": "w", "version": 1, "node_id": "c"},
|
|
"expectations": [{"kind": "table", "keys": ["a"]}]}, "expectations.0.kind"),
|
|
({"question": "q", "gold": {"kind": "magic"}, "expectations": [{"kind": "none"}]}, "gold.kind"),
|
|
])
|
|
def test_expectations_and_gold_must_fit_together(body, field):
|
|
assert invalid(body).field == field
|
|
|
|
|
|
def test_from_run_gold_is_only_built_by_the_server():
|
|
gold = {"kind": "from_run", "datasource_id": "ds1", "sql": "SELECT 1 AS n", "run_id": "r1", "query": "a1",
|
|
"confirmed_by": "甲", "confirmed_at": "2026-10-01T00:00:00+00:00"}
|
|
assert invalid({"question": "q", "gold": gold, "expectations": [{"kind": "scalar", "column": "n"}]}).field \
|
|
== "gold.kind"
|
|
fields = normalize_case({"question": "q", "gold": gold, "expectations": [{"kind": "scalar", "column": "n"}]},
|
|
trusted_gold=True)
|
|
# 原样带回来的可以过(改标签时整份 gold 跟着回来)
|
|
again = normalize_case({"gold": gold, "tags": ["x"]}, current=fields)
|
|
assert again["gold"] == fields["gold"] and again["tags"] == ["x"]
|
|
|
|
|
|
def test_the_case_hash_follows_content_not_records():
|
|
base = normalize_case({"question": "q", "gold": SQL, "expectations": [{"kind": "scalar", "column": "gmv"}],
|
|
"tags": ["a"]})
|
|
assert cases.case_hash({**base, "tags": ["b"], "enabled": False}) == cases.case_hash(base)
|
|
assert cases.case_hash({**base, "question": "q2"}) != cases.case_hash(base)
|
|
caliber = {"kind": "caliber", "workflow_id": "w", "version": 1, "node_id": "c", "inputs": {}}
|
|
pinned = {**base, "gold": caliber}
|
|
# 升版处置的记录不进指纹(dual 时旧的那一份照旧可比),改钉版本才进
|
|
assert cases.case_hash({**pinned, "gold": {**caliber, "upgrade": {"policy": "dual", "to_version": 2}}}) == \
|
|
cases.case_hash(pinned)
|
|
assert cases.case_hash({**pinned, "gold": {**caliber, "version": 2}}) != cases.case_hash(pinned)
|
|
|
|
|
|
def test_the_disposal_record_is_kept_only_by_the_server():
|
|
caliber = {"kind": "caliber", "workflow_id": "w", "version": 1, "node_id": "c", "inputs": {}}
|
|
stored = {**caliber, "upgrade": {"policy": "dual", "from_version": 1, "to_version": 2}}
|
|
body = {"question": "q", "expectations": [{"kind": "scalar", "metric": "m"}]}
|
|
kept = normalize_case({**body, "gold": {**caliber, "upgrade": {"policy": "recompute", "to_version": 9}}},
|
|
current={"gold": stored})
|
|
assert kept["gold"]["upgrade"] == stored["upgrade"], "客户端带来的不算,照库里的留着"
|
|
moved = normalize_case({**body, "gold": {**caliber, "node_id": "other"}}, current={"gold": stored})
|
|
assert "upgrade" not in moved["gold"], "换了卡就没有处置"
|
|
fresh = normalize_case({**body, "gold": {**caliber, "upgrade": {"policy": "dual"}}})
|
|
assert "upgrade" not in fresh["gold"]
|
|
|
|
|
|
def test_variables_render_at_the_as_of_moment():
|
|
as_of = datetime(2027, 1, 1, 8, 0, tzinfo=ZoneInfo("Asia/Shanghai"))
|
|
values = cases.variable_values(as_of, "Asia/Shanghai")
|
|
assert values["last_week"] == "2026-W52" and values["yesterday"] == "2026-12-31"
|
|
assert cases.render("'{{ schedule.last_week_start }}'..'{{ schedule.last_week_end }}'", values) == \
|
|
"'2026-12-21'..'2026-12-27'"
|
|
|
|
|
|
def test_suite_budget_defaults_and_ranges():
|
|
assert suites.effective_budget({}) == {"max_parallel": 2, "per_case_timeout_s": 900, "per_run_max_usd": 2.0,
|
|
"daily_max_usd": 5.0}
|
|
assert suites.normalize_budget({"max_parallel": 2, "per_run_max_usd": 1}) == {"per_run_max_usd": 1.0}
|
|
with pytest.raises(CaseInvalid) as caught:
|
|
suites.normalize_budget({"max_parallel": 1.5})
|
|
assert caught.value.field == "budget.max_parallel" and caught.value.code == suites.SUITE_INVALID
|