mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 06:31:58 +00:00
4c0e83bc80
- CatalogView{mode: live | snapshot | off, statuses: all | reviewed, snapshot}:缺省 live + all,行为和以前一样;reviewed 只留已核实、已确认的项(驳回的项原样留着交给各处,照旧经 visible_notes 去掉,冻结快照和 SQL 检查靠驳回的关系编号认出不该现推的关系——为此在 catalog.py 加 restrict_notes,没给 visible_notes 加参数)
- 视图从哪来:运行从 Run.options['catalog_view'] 读(catalog_view.of_run:RunContext.extra 里的发起快照,审批恢复、续跑沿用),ToolContext 多一个 catalog_view,节点(调用工具、llm/agent、多 agent)构造时带上;生成过程从参数读(下一个提交接 answer_ops)
- 四处消费一律经它:运行时 db_schema / db_query(tools/datasource._catalog_of,冻结进表结构快照的也是这一份)、look 表详情(TableKind.load、copilot_context.look_service 收 catalog_view)、自查的 SQL 检查器(sqlcheck.load_checkers 收 catalog_view,改用 catalog.read_catalogs 一次读出)、双盲复算(_Sources 带视图,交给盲算的目录和比较 SQL 用的中文名都按它;核查 agent、反方审稿的 db_schema 也按这次运行的视图)
- 冻结快照:不是缺省视图的在引用 meta 里记下 catalog_view(不进内容哈希,缺省视图不记,快照和以前一字不差),证据面板据此写得出「本次运行目录:关」
- snapshot 模式只定读法(kind=eval_catalog_snapshot 的工件,{sources: {源 id: {tables: {表: {version, notes}}}}}),内容由第 2 波的题集包写;快照不在了照实报错(CatalogSnapshotMissing),不悄悄退回当前目录
- 新增 tests/test_catalog_view.py:四种视图在四处给的是同一份、缺省等于当前目录、运行从 options 读且冻结 meta 记下、lint(消费模块不许直接 read_catalog / read_entry / 查 CatalogNote);去掉 _catalog_of 里的视图回跑两项失败
182 lines
9.4 KiB
Python
182 lines
9.4 KiB
Python
"""目录视图:运行和助手读数据目录统一经这一个接缝(docs/design/next-round/01-效果度量与回归评测.md 做法 5,总方案 X7)。
|
||
|
||
**为什么要接缝。** 目录在四处被读:运行时的 db_schema / db_query 工具(目录同时冻结进表结构快照)、助手查看(look 表详情)、
|
||
自查用的 SQL 检查器、双盲复算(交给盲算的说明、比较两条 SQL 用的中文名)。以前各读各的、没有统一的开关,「开目录和关
|
||
目录」「只用已核实的项」「起草模型 A 的目录和 B 的」这些对照都做不了,评测期间目录被起草或剖析改了也不知道。现在四处都
|
||
经这里取,一处定读哪一份;lint 测试(tests/test_catalog_view.py)不许这几个模块绕过它直接 read_catalog、直接查 CatalogNote。
|
||
|
||
**视图**(CatalogView):
|
||
- mode:live 当前目录(缺省,行为和以前一样);snapshot 一份冻结的目录快照(工件 id 在 snapshot,内容见 SNAPSHOT_KIND);
|
||
off 不给目录。
|
||
- statuses:all 全部(驳回的照旧不给模型看);reviewed 只要已核实(verified)和已确认(confirmed)的项,推断的
|
||
(proposed)去掉——量的是模型起草、还没审过的那些项是帮忙还是添乱。
|
||
|
||
**视图从哪来。** 运行从 Run.options['catalog_view'] 读(launch_run 收 options,发起时定下、随 run.started 封存,审批恢复、
|
||
重启续跑都沿用——用上下文变量传的话恢复之后就丢了);生成过程(问数据的 answer_ops、评测的作答)从参数读。都不给就是 live。
|
||
冻结进表结构快照时,不是缺省视图的在工件引用的 meta 里记下视图(describe),证据面板据此写得出「本次运行目录:关」。
|
||
|
||
**快照的内容**(第 2 波的评测题集包写,这里只定读法):kind=eval_catalog_snapshot 的内容寻址工件,
|
||
{"sources": {源 id: {"tables": {表名: {"version": 版本, "notes": 目录}}}}}。快照里没有这个源就是这个源没有目录;
|
||
快照工件不在了照实报错(CatalogSnapshotMissing),不悄悄退回当前目录——那样对照就白做了。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from collections.abc import Iterable, Mapping
|
||
from dataclasses import dataclass
|
||
from typing import Any
|
||
|
||
from app.data import catalog
|
||
from app.data.catalog import CatalogEntry
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
#: Run.options 里放视图的键
|
||
OPTION_KEY = "catalog_view"
|
||
MODES = ("live", "snapshot", "off")
|
||
#: 各状态集合留下哪些项的状态(all 不筛:驳回的由各处照旧经 visible_notes 去掉)
|
||
STATUS_SETS: dict[str, tuple[str, ...] | None] = {"all": None, "reviewed": ("verified", "confirmed")}
|
||
#: 目录快照工件的 kind(细案 01 做法 6)
|
||
SNAPSHOT_KIND = "eval_catalog_snapshot"
|
||
|
||
|
||
class CatalogViewInvalid(ValueError):
|
||
"""视图写得不对(服务端代码写错了才会有:客户端写不了 Run.options)。"""
|
||
|
||
|
||
class CatalogSnapshotMissing(RuntimeError):
|
||
"""视图指定的目录快照工件不在了、或者内容认不出。"""
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class CatalogView:
|
||
mode: str = "live"
|
||
statuses: str = "all"
|
||
#: mode=snapshot 时快照工件的 id
|
||
snapshot: str | None = None
|
||
|
||
def __post_init__(self) -> None:
|
||
if self.mode not in MODES:
|
||
raise CatalogViewInvalid(f"目录视图的 mode 只能是 {'、'.join(MODES)},当前为「{self.mode}」")
|
||
if self.statuses not in STATUS_SETS:
|
||
raise CatalogViewInvalid(f"目录视图的 statuses 只能是 {'、'.join(STATUS_SETS)},当前为「{self.statuses}」")
|
||
if (self.mode == "snapshot") != bool(self.snapshot):
|
||
raise CatalogViewInvalid("目录视图只有 mode=snapshot 时带快照工件 id,而且必须带")
|
||
|
||
@property
|
||
def is_default(self) -> bool:
|
||
return self.mode == "live" and self.statuses == "all"
|
||
|
||
def as_option(self) -> dict[str, Any]:
|
||
"""写进 Run.options['catalog_view'] 的形状。"""
|
||
return {"mode": self.mode, "statuses": self.statuses, **({"snapshot": self.snapshot} if self.snapshot else {})}
|
||
|
||
@classmethod
|
||
def parse(cls, raw: Any) -> CatalogView:
|
||
"""Run.options['catalog_view']、生成参数 → 视图。None 是缺省(live);写得不对抛 CatalogViewInvalid。"""
|
||
if raw is None:
|
||
return LIVE
|
||
if isinstance(raw, CatalogView):
|
||
return raw
|
||
if not isinstance(raw, Mapping):
|
||
raise CatalogViewInvalid(f"目录视图需要是一个对象,当前为「{raw}」")
|
||
unknown = set(raw) - {"mode", "statuses", "snapshot"}
|
||
if unknown:
|
||
raise CatalogViewInvalid(f"目录视图里不认识的键:{'、'.join(sorted(unknown))}")
|
||
return cls(mode=str(raw.get("mode") or "live"), statuses=str(raw.get("statuses") or "all"),
|
||
snapshot=str(raw["snapshot"]) if raw.get("snapshot") else None)
|
||
|
||
|
||
LIVE = CatalogView()
|
||
|
||
|
||
def of_options(options: Mapping[str, Any] | None) -> CatalogView:
|
||
"""一次运行的视图(Run.options 里的 catalog_view)。没有就是 live。"""
|
||
return CatalogView.parse((options or {}).get(OPTION_KEY))
|
||
|
||
|
||
def of_run(run_ctx: Any) -> CatalogView:
|
||
"""运行里的节点构造 ToolContext 时用:RunContext.extra['run_options'] 里的视图(runner 每段执行都放这份)。"""
|
||
from app.engine.runner import RUN_OPTIONS_KEY
|
||
|
||
return of_options((getattr(run_ctx, "extra", None) or {}).get(RUN_OPTIONS_KEY))
|
||
|
||
|
||
def describe(view: CatalogView | None) -> dict[str, Any] | None:
|
||
"""记进冻结快照 meta 的说法:缺省视图(live、全部)不记,别的照 as_option 记下。"""
|
||
view = view or LIVE
|
||
return None if view.is_default else view.as_option()
|
||
|
||
|
||
def restrict(view: CatalogView | None, notes: Mapping[str, Any] | None) -> dict[str, Any]:
|
||
"""一张表的目录在这个视图下留下什么(状态集合;驳回的项原样留着,见 catalog.restrict_notes)。"""
|
||
allowed = STATUS_SETS[(view or LIVE).statuses]
|
||
if allowed is None:
|
||
return dict(notes or {})
|
||
return catalog.restrict_notes(notes, allowed)
|
||
|
||
|
||
def _restricted(view: CatalogView, entries: Mapping[str, CatalogEntry]) -> dict[str, CatalogEntry]:
|
||
if STATUS_SETS[view.statuses] is None:
|
||
return dict(entries)
|
||
out: dict[str, CatalogEntry] = {}
|
||
for table, entry in entries.items():
|
||
if notes := restrict(view, entry.notes):
|
||
out[table] = CatalogEntry(table=entry.table, notes=notes, version=entry.version,
|
||
updated_by=entry.updated_by, updated_at=entry.updated_at)
|
||
return out
|
||
|
||
|
||
def _snapshot_tables(view: CatalogView, source_id: str) -> dict[str, CatalogEntry]:
|
||
from app.core.artifact_store import load
|
||
|
||
try:
|
||
content = load(view.snapshot or "")
|
||
except ValueError as e: # 内容和哈希对不上
|
||
raise CatalogSnapshotMissing(f"目录快照 {str(view.snapshot)[:12]}… 读不出:{e}") from e
|
||
if not isinstance(content, dict) or not isinstance(content.get("sources"), dict):
|
||
raise CatalogSnapshotMissing(f"目录快照 {str(view.snapshot)[:12]}… 不在了或内容认不出,这次不能按它读目录")
|
||
tables = ((content["sources"].get(source_id) or {}).get("tables")) or {}
|
||
out: dict[str, CatalogEntry] = {}
|
||
for table, body in tables.items():
|
||
if isinstance(body, Mapping) and isinstance(body.get("notes"), Mapping):
|
||
out[str(table)] = CatalogEntry(table=str(table), notes=dict(body["notes"]),
|
||
version=int(body.get("version") or 0))
|
||
return out
|
||
|
||
|
||
async def read_many(session: Any, view: CatalogView | None, source_ids: Iterable[str]) -> dict[str, dict[str, CatalogEntry]]:
|
||
"""几个数据源在这个视图下的目录:{源 id: {表名: CatalogEntry}},没有目录的源不出现。live 一次查询读出。"""
|
||
view = view or LIVE
|
||
ids = [i for i in dict.fromkeys(source_ids) if i]
|
||
if view.mode == "off" or not ids:
|
||
return {}
|
||
if view.mode == "snapshot":
|
||
raw = {sid: found for sid in ids if (found := _snapshot_tables(view, sid))}
|
||
else:
|
||
raw = await catalog.read_catalogs(session, ids)
|
||
return {sid: kept for sid, entries in raw.items() if (kept := _restricted(view, entries))}
|
||
|
||
|
||
async def read(view: CatalogView | None, source: Any) -> dict[str, CatalogEntry]:
|
||
"""一个数据源在这个视图下的目录:{表名: CatalogEntry}。用自己的短会话读(调用方的会话里已经加载的对象不会因为回滚
|
||
过期)。live 读不到(库出错)当没有目录、记日志:目录是附加说明,不能挡住查询;快照不在了照实抛 CatalogSnapshotMissing。"""
|
||
from app.db.base import SessionLocal
|
||
|
||
view = view or LIVE
|
||
source_id = getattr(source, "id", None)
|
||
if view.mode == "off" or not source_id:
|
||
return {}
|
||
if view.mode == "snapshot":
|
||
return _restricted(view, _snapshot_tables(view, source_id))
|
||
try:
|
||
async with SessionLocal() as own:
|
||
return (await read_many(own, view, [source_id])).get(source_id, {})
|
||
except Exception: # noqa: BLE001
|
||
logger.exception("读取数据源 %s 的数据目录失败,本次不附目录", getattr(source, "name", source_id))
|
||
return {}
|
||
|
||
|
||
__all__ = ["CatalogSnapshotMissing", "CatalogView", "CatalogViewInvalid", "LIVE", "MODES", "OPTION_KEY", "SNAPSHOT_KIND",
|
||
"STATUS_SETS", "describe", "of_options", "of_run", "read", "read_many", "restrict"]
|