Files
AgentLab/backend/app/ledger/querycore.py
T
ThearchyHelios eddeded2a4 全栈安全加固、性能提速与架构演进:连接池与沙箱止血、双路混合检索、多版本归档及前端分包治理
- 第一批(安全加固与生命周期止血):隔离测试宿主机密钥与 .env 泄漏;修复 SQLite 取消查询挂死、:memory: 连接池报错与 EngineCache 世代竞态;补齐运行与评测删除时的 Checkpointer 线程、Artifact 磁盘文件与无主附件回收;加固三套沙箱路径越界、进程组清理与 Seatbelt 数据目录隔离,封堵 SSRF IPv4-mapped IPv6 元数据绕过与响应体 OOM;修复推理模型多轮 reasoning_content、结构化输出 Token 漏计与缓存折扣计费
- 第二批(数据库/集群/问数提速与首屏分包):补齐核心表索引与 SQLite WAL 调优,列表接口按需 defer 大列,收敛启动扫描并打通跨进程 slot 槽位唤醒;优化跨源全局选表、哈希桶数字绑定、流式 CSV 导入及多方言分析算子;剥离首屏静态引入的 @xyflow/react 与全局样式(首屏主包降至 461.59 kB),补充流式 rAF 攒批、Store 精细订阅与表单无障碍绑定
- 第三批(架构演进与工程自动化闭环):记录库超挂接阈值历史版本自动增量归档至单库多表(解除 SQLite 10 库 ATTACH 硬上限);重构知识库为词面 BM25 与全集合归一化向量矩阵真·双路独立召回并支持游标分批重建;升级多方言感知 SQL 模板词法分析(E'...' / $$...$$ / q'[...]' / # / 嵌套注释)与 Decimal / bool / NaN 参数校验,消除 ledger 层对 api 路由层的反向依赖;拆分 3101 行 CaliberDrafts 页面为 7 个领域子模块,消除检查脚本本机路径硬编码并新增 GitHub Actions CI 门禁与三批回归测试套件
2026-11-13 14:51:36 +08:00

1777 lines
110 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""进程内 QueryCore 固定执行(K09):按一份**已发布的固定口径**跑一次,交回类型化标量与证据引用。
这一层是「按固定计划执行」的服务,不是又一个入口平台。一次调用只说五件事:口径身份(workflow + card,版本可点名、
不点名就读**推荐版并当场固定**)、要算哪几个指标(K08 闭包)、这次入口参数的**类型化取值**、预算与 trace。回来的是
一份结果:类型化 metrics、这次实际用到的发布/manifest 引用(K07)、selection/closure 身份、参数解释与哈希(K01)、
数据时点与质量状态、每条查询的执行/attempt/工件/来源证据引用,以及「这次到底算成了没有」。
**这一版只按显式选择执行。** `metric_ids` 必须点名(空的选择直接拒,不静默当整卡);交回的是标量值(声明成表的指标
在发 SQL 之前就拒)。「整卡」与表形输出留给以后的版本,不在这里按缺省猜。
**固定的是发布物,不是当下这张图。** 执行的是不可变版本行里的那一版图(K07 的发布清单就是按它物化的),清单必须读得
出来、且**就是这一版这一张图**的(图指纹对得上、图上的源头口径卡一张不缺);清单里记着的编译器/绑定/参数/解析器约定
和现在这一套不兼容就**明确拒绝**,绝不按新编译器现场重解释旧发布(A09)。没有发布清单的老资产(legacy)照样拒:旧 trial
路径一个字不动,但不能拿它冒充 QueryCore 的固定发布执行。
**执行路径零模型、零编译。** 内部 LLM = 0(派生的探针图里只有入口、取数、口径卡、出口,没有报告/Agent 节点),
作者侧 SQL 编译 = 0(只按固定计划做参数绑定与选择展开:K01 的 `params.plan` 把这一版模板按这次的值渲染成字面语句,
K08 的闭包决定执行哪几条查询、要哪些参数)。不建通用 Agent、不建任务平台、不注册驱动、不做结果缓存。
**进程内、可超时。** `run_manager.start` 起一次探索运行(`READ_ONLY_OPTION` 只读边界),随后在**同一个事件循环里**
`await` 它收尾(`run_manager.wait_idle`:等任务,不阻塞循环)。到点还没收尾就照实交回 `status="running"` + run_id,
**绝不把超时说得像成功**;空指标、部分查询没结果、某个指标被遮罩也一样 `ok=False`。
**身份与用途由宿主定,客户端改不了。** 认证主体来自服务端身份(HTTP 入口从 `current_principal()` 取,宿主入口由调用方
显式传),purpose 是**宿主常量**:默认交互入口是 `compose`(探索运行,走 K07 的同一套判定但不继承正式身份,也不拿它绕过
formal);给 K10 父 Run 用的是 `execute_formal`(purpose=formal,复用同一个 `publications.judge`,发布依据不齐就拒)。
客户端没有传 purpose / role 的路子——两个入口都不收这两个字段。
**普通使用者按资产授权(K14)。** **每一个用途**(compose 的普通问数、交互入口、画布试跑、父 Run 的 compose 调用,以及 formal
的 `execute_formal` 与正式父 Run 派生的那一次执行)在**固定 workflow + version 之后、起 Run 之前**都多判一道:这一版口径有没有
授权给这次的 actor(`apps/access.caliber_role`,靠的是 AppGrant 加上应用当前发布版 `AppVersion.pins` 精确固定着这一版),
到 `use` 及以上才放行,否则当场 403(`querycore_not_authorized`)——HTTP 入口、固定请求路由、自动路由、`caliber_query` 工具
这几条路都靠它兜底,谁也不能靠「换一个入口」绕过去;应用图里引用一份没授权给它的口径同样在这次执行被拒。**正式还要多一道**:
formal 在授权之后照旧走 `publications.judge`(发布依据不齐照样拒),两道都过才执行。builder / admin / 关登录由**调用方**显式
放行(`auth/chat_policy.restricted_actor`),这几档不写进资产授权函数;actor 缺失(认不出是谁)按普通使用者算:资产授权查不到
主体就是没有授权,照拒。
**服务端替普通使用者准备的那条路(`execute_prepared`)。** 普通使用者的模型生成的图里**不要求**它自己画口径查询:服务端
在准备一轮问数据时把 `caliber_query` 自动绑进它的 Agent / 调用工具节点(chat/launch,只给有授权口径的主体绑),模型因此
能自己挑一份有授权的口径调用。要一条**deterministic** 的固定执行(一条确定请求直达、不经模型)时用 `execute_prepared`:
服务端按固定的 workflow / version / card / metric_ids / inputs 搭一张最小的**父图**(入口 → 调用工具 caliber_query → 出口),
经 `user_chat_run` 那套白名单与选项(遮罩 + 只读)起一次探索父 Run,**实际执行走的是 `caliber_query` 工具**(派生的子运行
就是 `execute` 那一次固定执行),当场 await 收尾,交回与 `execute` 同形的 Outcome:run_id 是这次调用起的父 Run
(普通使用者读得到它:显式绑定、白名单、遮罩/只读、每条 query artifact 接回的证据都在它上面),execution_run_id 是它派生
出来真正跑那几条 SQL 的子运行。用户不编辑图,执行层却有明确的绑定、白名单与父 Run 证据。
**K10:父 Run 的工具入口(`caliber_query`)。** 一次运行里的 Agent / 调用工具节点绑上这个工具,就按一份固定口径把数取回来:
工具用的是**父 Run 的上下文**——父 Run 的用途决定这次走 formal 还是 compose(正式父 Run 用 formal judge;交互、画布这类
compose 父 Run 拿不到正式身份),父 Run 固定的上传/记录源版本、运行选项(遮罩、只读、数据目录视图)与来源照原样带下去。
执行仍然是 K09 的进程内固定执行:派生出来的探针运行是**明确的子 Run**(origin 里记着 `parent_run_id`,来源也带下去,
不失去父关系),并且**当场 await 到收尾**。回来的是与交互工作台同一种形状的结果:metrics + queries 数组,每条查询都带
query_id / execution_id / attempt 与工件、来源、参数材料;父 Run 的节点据此把每条 query artifact 以合法引用接回自己的封存
证据(伪造的、别的运行落的、不是 query_snapshot 的工件一律拒绝,见 capabilities.registry.snapshot_owned)。
**值只被模板替换一次、且不无条件转 float。** 每个参数的解释与哈希来自 `params.plan` 的同一份 resolved 值(检查、渲染、
快照材料共一份);实际执行过的语句与它不一致(运行期被重新绑定)照实报 `querycore_rebound`。交回的值原样保留类型:
JSON 里的整数就是精确整数(大整数不转 float),十进制文本原样保留(记成 `decimal`),布尔/字符串/空值各按各的。
**交回的是证据引用,不是 SQL 正文。** 每条查询给工件 id、来源(workflow/version/node)、attempt(那次执行的执行号)、
调用 id、列与行数、是否截断、以及**这次计划的语句哈希/参数输入哈希与执行时那份的哈希**——两串哈希对得上就说明这次没有
运行期重绑定。口径的 SQL 正文(连同快照里记着的源模板)不外发:里面可能写着遮罩值,要读正文走证据接口(那里按遮罩处理)。
**K13:量这一层自己。** 交回的 `measurement` 记这一次的分段耗时(发布/定位、闭包/计划、参数、排队、数据库、证据、总耗时)、
计数(计划与真发出去的 SQL 条数、内部模型调用数、交回的查询条数与被截断的条数、问题/错误/超时)与身份(trace / run /
query ids)。测量只读时钟与这次运行自己留下的记录与封存事件,不改执行路径:不加缓存、不改计划、不重试。**量不到的一律
是 None**——没跑完(超时)就没有执行证据,数据库那一截与 SQL 条数不填 0 冒充;排队那一项来自运行记录的 `usage.queue_ms`
(引擎在等并发名额前后量的那一段),不是等人审批的 `wait_ms`,老运行没有这一项就是 None。
"""
from __future__ import annotations
import asyncio
import json
import logging
import re
import time
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Any
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.apps import publications
from app.tools.registry import ToolContext, execution_id_of, register
logger = logging.getLogger(__name__)
#: 默认交互入口的用途(探索/组合式问数那一档):不是正式身份,也不因为缺验证审阅被拦
DEFAULT_PURPOSE = publications.PURPOSE_COMPOSE
#: 宿主给 K10 父 Run 用的用途:和正式运行同一套发布依据判定(复用 publications.judge)
HOST_PURPOSE = publications.PURPOSE_FORMAL
#: K10 注册进工具登记表的**真实工具名**(不是 `db_query__` 前缀那类动态工具):父 Run 的 Agent / 调用工具节点绑它,
#: 就按一份固定口径取一次数(app/ledger/querycore.execute),并把每条 query artifact 接回父 Run 的证据。
CALIBER_QUERY = "caliber_query"
#: 子 Run 的 origin 里记父运行 id 的键(capabilities.registry.snapshot_owned 按它认「明确派生的子运行」)
PARENT_RUN_KEY = "parent_run_id"
#: 运行选项(Run.options / RunContext.extra)里放这次口径查询预算的键:父 Run 发起时定下就照它,没定就走缺省
BUDGET_OPTION = "caliber_query_budget"
def origin_kind() -> str:
"""这次执行记在运行上的来源(engine/origin.QUERYCORE_ORIGIN):口径的固定执行,和口径试跑同一档探索运行。
只从 engine/origin 的常量取,别处不许另写一个字符串。"""
from app.engine.origin import QUERYCORE_ORIGIN
return QUERYCORE_ORIGIN
#: 一次执行默认最多等多久(秒)——到点交回 running,不冒充成功
DEFAULT_TIMEOUT_S = 30.0
#: 默认预算:这次最多执行几条查询(闭包里的查询数超过就拒,不静默少算)
DEFAULT_MAX_QUERIES = 32
# --------------------------------------------------------------------------
# 机器码:拒绝时给程序认(值层面/执行层面的问题另见 problems 里的码)
# --------------------------------------------------------------------------
NOT_FOUND = "querycore_not_found"
CARD_MISSING = "querycore_card_missing"
VERSION_MISSING = "querycore_version_missing"
NOT_PUBLISHED = "querycore_version_unpublished"
MANIFEST_REQUIRED = "querycore_manifest_required"
MANIFEST_UNREADABLE = "querycore_manifest_unreadable"
INCOMPATIBLE = "querycore_incompatible"
SELECTION_REFUSED = "querycore_selection_refused"
INPUTS_REFUSED = "querycore_inputs_refused"
UNKNOWN_INPUT = "querycore_unknown_input"
PARAMETERS_REFUSED = "querycore_parameters_refused"
BUDGET_REFUSED = "querycore_budget_refused"
OUTPUT_UNSUPPORTED = "querycore_output_unsupported"
#: 工具入口(K10):不在任何运行里调用,就没有父运行可以把证据接回去
NO_PARENT = "querycore_no_parent"
#: 普通使用者按这一版口径执行,但资产授权里没有给它(K14):起 Run 之前就拒
NOT_AUTHORIZED = "querycore_not_authorized"
#: 父 Run 上已经固定的数据版本和这次要执行的口径对不上(另一份口径目标、源已经换了一版):明确拒,不静默换一版
DATA_VERSION_CONFLICT = "querycore_data_version_conflict"
#: 准备好的那次执行(execute_prepared)里子运行跑的版本和这一层固定/授权的那一版对不上(推荐版在中途换过):照实说,
#: 不把另一版的数当这一版交出去
VERSION_MOVED = "querycore_version_moved"
#: 上面这一批(起 Run 之前拒绝的那几个码):写给「普通使用者看到的失败说法只留码、不留口径身份」的地方按它们认
#: (api/runs 的失败说法投影),别处不要另列一份
REFUSAL_CODES = (NOT_FOUND, CARD_MISSING, VERSION_MISSING, NOT_PUBLISHED, MANIFEST_REQUIRED, MANIFEST_UNREADABLE,
INCOMPATIBLE, SELECTION_REFUSED, INPUTS_REFUSED, UNKNOWN_INPUT, PARAMETERS_REFUSED, BUDGET_REFUSED,
OUTPUT_UNSUPPORTED, NO_PARENT, NOT_AUTHORIZED, DATA_VERSION_CONFLICT, VERSION_MOVED)
#: 结果里的问题码(ok=False 的原因)
NO_RUN = "querycore_run_missing"
EXECUTION = "querycore_execution"
TIMEOUT = "querycore_timeout"
EVIDENCE = "querycore_evidence"
EMPTY = "querycore_empty"
METRIC_MISSING = "querycore_metric_missing"
METRIC_NO_VALUE = "querycore_metric_no_value"
METRIC_MASKED = "querycore_metric_masked"
QUERY_MISSING = "querycore_query_missing"
REBOUND = "querycore_rebound"
#: 运行到这几个状态就是终态了(interrupted 也算:它停在那儿不再自己往下跑);别的都是还在跑
TERMINAL_RUN_STATUSES = ("succeeded", "failed", "cancelled", "interrupted")
#: 十进制文本:带小数点/指数,或者位数多到 float 存不下(大整数不转 float,原样保留)
_DECIMAL_TEXT = re.compile(r"^[+-]?(?:\d+\.\d*|\.\d+|\d+(?:\.\d+)?[eE][+-]?\d+|\d{16,})$")
#: 取值本身就写成日期/时刻(含时区偏移):算「这次数据窗口」时按它认,认字段声明的类型之外再认一层值
_DATE_LIKE = re.compile(r"^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$")
#: 入口字段声明成时间类的:算「这次数据窗口」用
_TIME_TYPES = ("date", "datetime", "time")
class QueryCoreRefused(Exception):
"""这次固定执行不发起(服务层面就拒):message 给人看,code 给程序认,status_code 是 HTTP 入口该回的状态。
problems 是底层那一层原样的几条(参数完整性问题、闭包的未决项、发布依据的缺口……),带各自的机器码。"""
def __init__(self, message: str, code: str, *, status_code: int = 409,
problems: Sequence[Any] = ()) -> None:
super().__init__(message)
self.message = message
self.code = code
self.status_code = status_code
self.problems = tuple(problems)
def as_dict(self) -> dict[str, Any]:
return {"detail": self.message, "code": self.code,
**({"problems": [dict(p) for p in self.problems]} if self.problems else {})}
# --------------------------------------------------------------------------
# 类型化取值:只认类型,不改值
# --------------------------------------------------------------------------
def typed(value: Any) -> str:
"""这个值在 JSON 里的类型:integer / number / decimal / boolean / string / null / 别的类型名。
**不把 decimal 和大整数转成 float**(R06 的原值口径):JSON 里的整数已经是精确整数原样交回,十进制文本(驱动
Decimal 落进工件后按文本存下来的)原样保留并标成 decimal,不四舍五入、不截位。"""
if value is None:
return "null"
if isinstance(value, bool):
return "boolean"
if isinstance(value, int):
return "integer"
if isinstance(value, float):
return "number"
if isinstance(value, Decimal):
return "decimal"
if isinstance(value, str):
return "decimal" if _DECIMAL_TEXT.match(value.strip()) else "string"
if isinstance(value, (list, tuple)):
return "array"
if isinstance(value, Mapping):
return "object"
return type(value).__name__
def exact(value: Any) -> Any:
"""值原样交回,只把 Decimal 换成它的十进制文本(精确位数一个不少)。
接口这一层会把 Decimal 编码成 float(丢精度),所以十进制不能以 Decimal 的形状出这道门:换成文本、类型标成
decimal。别的值(int / float / str / bool / None)原样,不做任何转换。"""
return str(value) if isinstance(value, Decimal) else value
def _typed_metric(metric: Mapping[str, Any], hidden: str | None) -> dict[str, Any]:
"""一个指标的类型化投影:值原样保留(遮罩时不给值),带类型、单位、小数位与质量结论。"""
out: dict[str, Any] = {k: metric[k] for k in ("id", "name", "unit", "status", "rendered", "decimals", "format")
if metric.get(k) is not None}
if hidden:
out.update(masked=True, rendered=hidden, value=None, value_type="null", status="masked")
out["quality"] = {"status": "unknown", "checks": [
{"code": "masked", "status": "unknown", "detail": "指标值已遮罩,不能在此作为结果交出"}]}
return out
value = metric.get("value")
out["value"] = exact(value)
out["value_type"] = typed(value)
if metric.get("contract_findings"):
out["contract_findings"] = [
dict(f) if isinstance(f, Mapping) else str(f)
for f in metric["contract_findings"]
if f
]
out["quality"] = _metric_quality(metric)
return out
def _metric_quality(metric: Mapping[str, Any]) -> dict[str, Any]:
from app.engine.validation import metric_quality
return metric_quality(dict(metric))
# --------------------------------------------------------------------------
# 预算
# --------------------------------------------------------------------------
def _budget_of(budget: Mapping[str, Any] | None) -> dict[str, Any]:
"""这次执行的预算:最多几条查询、最多等多久。给了的照用,没给的用缺省(不新建一套额度体系)。"""
made = {"max_queries": DEFAULT_MAX_QUERIES, "timeout_s": DEFAULT_TIMEOUT_S}
for key in ("max_queries", "timeout_s"):
if budget and budget.get(key) is not None:
made[key] = budget[key]
return made
# --------------------------------------------------------------------------
# 输入:类型化
# --------------------------------------------------------------------------
def _inputs_of(pinned: Any, inputs: Mapping[str, Any]) -> tuple[dict[str, Any], list[str]]:
"""这次入口的取值:不认识的字段拒(不静默丢掉),按声明类型校验,再按 K01 的语义把缺省值补上。
收值和试跑、组合式问数同一份(`params.provided`:显式传的空串算没给),校验用同一份表单(apps/forms),
定值用同一份 `params.resolve`。
**这次选择用什么参数就是什么参数**:给了选择时,入口契约收窄成这次要用的那几个(K08 的 required_entry_fields)——
声明过、但这次不用的字段照收不误(不报「不认识」),**但不带进这次执行**,列表交回调用方记在 ignored 里,
不悄悄用它跑出另一份数(B 独有那条查询的参数,选 A 时一律为 0)。"""
from app.apps.forms import input_schema, validate_form
from app.ledger import derive, params
if not isinstance(inputs, Mapping):
raise QueryCoreRefused("入口参数要给一组字段和值", INPUTS_REFUSED, status_code=422)
narrowed = [dict(f) for f in pinned.entry_fields]
full = [dict(f) for f in derive.entry_fields(pinned.graph)] or narrowed
unknown = sorted(str(k) for k in inputs if str(k) not in params.declared(full))
if unknown:
raise QueryCoreRefused(f"不认识的入口字段:{'、'.join(unknown)}", UNKNOWN_INPUT, status_code=422)
used = set(params.declared(narrowed))
values = params.resolve(narrowed, params.provided({k: v for k, v in inputs.items() if str(k) in used}))
errors = validate_form(input_schema(narrowed), values)
if errors:
raise QueryCoreRefused(";".join(str(e.get("message")) for e in errors[:5]), INPUTS_REFUSED,
status_code=422, problems=[dict(e) for e in errors])
return values, sorted(str(k) for k in inputs if str(k) not in used)
# --------------------------------------------------------------------------
# 这次执行什么:K08 闭包 + K01 计划
# --------------------------------------------------------------------------
@dataclass(frozen=True)
class _Plan:
"""一条查询的 K01 计划:渲染好的字面语句 + 快照材料(模板哈希、输入哈希、每个参数写在哪儿)。"""
node_id: str
query_id: str
tool: str
statement: str
material: dict[str, Any] = field(default_factory=dict)
def as_dict(self) -> dict[str, Any]:
from app.ledger import params
return {"query_id": self.query_id, "node_id": self.node_id, "tool": self.tool,
"template_hash": self.material.get("template_hash"),
"statement_hash": params.rendered_hash(self.statement),
"inputs_hash": self.material.get("inputs_hash"),
"adapter": self.material.get("adapter"),
"parameters": [dict(p) for p in self.material.get("parameters") or []],
"declared": [dict(d) for d in self.material.get("declared") or []]}
def _sql_of(node: Mapping[str, Any]) -> str | None:
args = ((node.get("data") or {}).get("config") or {}).get("args") if isinstance(node, Mapping) else None
sql = args.get("sql") if isinstance(args, Mapping) else None
return sql if isinstance(sql, str) and sql.strip() else None
def _plans(pinned: Any, closure: Any, values: Mapping[str, Any]) -> tuple[list[_Plan], list[dict[str, Any]]]:
"""这次要执行的每条查询的 K01 计划(闭包已经决定执行哪几条、要哪些参数)。
检查、渲染、快照材料用的是同一份 resolved 值(params.plan);不合规(缺参数、未声明、值承载不了)在这里就把
逐条问题交回,**一条 SQL 都不发**。"""
from app.ledger import params
by_node = {str(query.node_id): query for query in closure.queries}
plans: list[_Plan] = []
problems: list[dict[str, Any]] = []
for node in pinned.fetches:
node_id = str(node.get("id"))
sql = _sql_of(node)
if sql is None:
continue # 闭包已经拒过「取数节点没有可读 SQL」,这里不重判
try:
statement = params.plan(sql, fields=pinned.entry_fields, values=values,
source={**pinned.ref(), "node_id": node_id}, node_id=node_id,
where=f"口径「{pinned.card_title}」的入口")
except params.ParameterRefused as e:
problems += [p.as_dict() for p in e.problems]
continue
query = by_node.get(node_id)
plans.append(_Plan(node_id=node_id, query_id=str(getattr(query, "query_id", "") or ""),
tool=str(getattr(query, "tool", "") or ""), statement=statement.statement,
material=dict(statement.material)))
return plans, problems
def _scalar_output(pinned: Any, metric_ids: Sequence[str]) -> None:
"""首版只交标量:声明成 `contract.shape = table` 的指标在这里明确拒(不悄悄按第一行给个值)。"""
by_id = {str(m.get("id")): m for m in pinned.metrics}
for metric_id in metric_ids:
contract = (by_id.get(str(metric_id)) or {}).get("contract")
if isinstance(contract, Mapping) and str(contract.get("shape") or "scalar") != "scalar":
raise QueryCoreRefused(
f"指标「{metric_id}」声明成表(contract.shape={contract.get('shape')}):这一版只交标量结果",
OUTPUT_UNSUPPORTED, status_code=422)
# --------------------------------------------------------------------------
# 证据:类型化指标 + 每条查询的执行引用
# --------------------------------------------------------------------------
async def _sealed_of(run_id: str, memo: dict[str, Any] | None) -> Any:
"""这一次收尾的封存证据。给了 memo 就复用:`read_sealed` 每次都要把整条事件流重读一遍、重算一遍清单哈希
(K13 量下来约 3.5ms),而一次收尾里指标、查询引用、执行量测三处读的是**同一份**已经封好的快照,没必要读三遍。
读不出来时照旧抛,由调用的那一处记一条自己的问题(语义不变)。"""
from app.engine.evidence_reader import read_sealed
if memo is None:
return await read_sealed(run_id)
if "sealed" not in memo:
memo["sealed"] = await read_sealed(run_id)
return memo["sealed"]
async def _metrics_from(sealed_run_id: str, *, card_id: str, metric_ids: Sequence[str],
memo: dict[str, Any] | None = None,
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
"""从封存证据里取这次要算的那几个指标,按闭包的顺序、**只认这张卡节点**(同一节点执行过多次取最后一次)。
遮罩按试跑同一套(api/evidence 的 _Masks / _metric_masks):被遮的指标不给值,也照实标出来——数据源设了遮罩列
的时候,这里绝不能把值交出去。交回 (类型化指标, 原始指标),原始那一份给质量报告用。
memo 是这一次收尾里几处共用的封存证据(见 _sealed_of):给了就用那一份,没给就自己读一次。"""
from app.engine.evidence_reader import metric_cards, open_masks, resolve_metric_masks
sealed = await _sealed_of(sealed_run_id, memo)
if not sealed.trusted:
return [], {"trusted": False}
masks = open_masks(sealed)
found: dict[str, dict[str, Any]] = {}
raw: dict[str, dict[str, Any]] = {}
for node_id, artifact, card in metric_cards(sealed):
if str(node_id) != str(card_id):
continue
for metric in card.get("metrics") or []:
if not isinstance(metric, dict) or not metric.get("id"):
continue
metric_id = str(metric["id"])
if metric_id not in set(metric_ids):
continue
# 同一张卡执行过多次时按**最后一次**的产出算(和「节点最后一次产出」同一个认法),
# 不拿早先那次的值
_, hidden = await resolve_metric_masks(metric, node_id, artifact, sealed, masks, card=card)
found[metric_id] = _typed_metric(metric, hidden)
raw[metric_id] = dict(metric)
ordered = [found[mid] for mid in metric_ids if mid in found]
return ordered, {"trusted": True, "metrics": {mid: raw[mid] for mid in metric_ids if mid in raw}}
def _tables_of(sql: Any) -> list[str]:
from app.engine.evidence import sql_tables
try:
return sql_tables(str(sql or ""))
except Exception: # noqa: BLE001 - SQL 读不出表名只影响这一条引用,不影响结果本身
return []
async def _queries_from(sealed_run_id: str, *, closure: Any, plans: Mapping[str, _Plan],
memo: dict[str, Any] | None = None) -> list[dict[str, Any]]:
"""闭包里每条要执行的查询的执行引用:用了哪份快照工件、来源、attempt(exec)、列与行数、执行时的参数材料与哈希。
每条查询按**逐次记下的执行**(sealed.query_records:一次执行一条,带 node_id / exec / execution_id)认它自己那一次,
不按工件 id 去重的字典认:同一份内容会得到同一个工件 id(内容寻址),两条不同的逻辑查询(两个节点写同一条 SQL、
参数一样、数据一样)会落到**同一个**工件上,`sealed.queries` 那种字典只剩最后一条,早先那条就查不到自己了。
同一个节点执行过多次时按**最后一次**算(同「节点最后一次产出」的认法)。读不出来/没有快照的照实留空,由调用方
记一条问题(不编造)。
memo 是这一次收尾里几处共用的封存证据(见 _sealed_of):给了就用那一份,没给就自己读一次。"""
from app.core.artifact_store import load
sealed = await _sealed_of(sealed_run_id, memo)
out: list[dict[str, Any]] = []
for query in closure.queries:
plan = plans.get(str(query.node_id))
item: dict[str, Any] = {"query_id": query.query_id, "node_id": query.node_id, "tool": query.tool,
"source": dict(query.source), "metric_ids": list(query.metric_ids),
"computes_unselected": list(query.computes_unselected),
"required_entry_fields": list(query.required_entry_fields),
"sql_hash": query.sql_hash, "binding_fingerprint": query.binding_fingerprint,
"artifact": None, "attempt": None, "execution_id": None, "call_id": None, "via": None,
"columns": [], "row_count": None, "truncated": None, "column_types": {},
"data_source": None, "data_version": None, "tables": [], "checks": [],
"schema_artifact": None,
"no_data": [], "parameters": None, "executed_sql": None,
"rebound": None, "found": False}
record = _query_record_of(sealed, str(query.node_id))
if record is not None:
artifact = str(record.get("artifact") or "")
try:
snap = load(artifact)
except ValueError:
snap = None
if isinstance(snap, Mapping):
material = snap.get("parameters") if isinstance(snap.get("parameters"), Mapping) else None
item.update(
artifact=artifact, attempt=record.get("exec") if record.get("exec") is not None
else record.get("attempt"),
call_id=record.get("call_id"), via=record.get("via"),
# 这次实际执行的执行身份:同一次运行里同一条 SQL 跑两次(重跑、agent 两步)也各不相同,
# 按调用 id(agent)或这个节点第几次执行(run 内序列)区分(tools/registry.execution_ref)
execution_id=record.get("execution_id") or execution_id_of(
str(query.node_id), attempt=item.get("attempt"), call_id=record.get("call_id"),
run_id=sealed_run_id),
columns=[str(c) for c in snap.get("columns") or []],
row_count=len(snap.get("rows") or []),
truncated=bool(snap.get("truncated")), column_types=dict(snap.get("column_types") or {}),
data_source=record.get("source") or snap.get("source"),
data_version=snap.get("data_version"), tables=_tables_of(snap.get("sql")),
schema_artifact=snap.get("schema_artifact"),
checks=list(snap.get("checks") or []), no_data=list(snap.get("no_data") or []),
# 快照里的参数材料只留哈希与承载方式:源模板正文不外发(同试跑查询快照那一处)
parameters=({"inputs_hash": material.get("inputs_hash"),
"template_hash": material.get("template_hash"),
"adapter": material.get("adapter"), "binding": material.get("binding")}
if material else None),
executed_sql=snap.get("sql"), found=True)
if plan is not None:
# 运行期重绑定:实际执行的语句 / 它记下的参数材料必须和这次计划的一致(值只被模板替换一次)。要比对的是
# 语句本身,所以它只活在这一层——**返回里不给 SQL 正文**(口径的 SQL 里可能写着遮罩值,同试跑查询快照
# 那一处的分寸):交回的是两边的哈希与工件引用,要读正文走证据接口(那里按遮罩处理)
item["planned_statement_hash"] = plan.as_dict()["statement_hash"]
item["planned_inputs_hash"] = plan.material.get("inputs_hash")
item["rebound"] = _rebound(item, plan)
item.pop("executed_sql", None)
out.append(item)
return out
def _query_record_of(sealed: Any, node_id: str) -> dict[str, Any] | None:
"""这个节点交回过的查询快照里**最后一次**的那一条(逐次身份,sealed.query_records)。
按 node_id 从逐次记下的执行里取,不从按工件 id 去重的 `sealed.queries` 里取:两条不同的逻辑查询(两个节点写同一条
SQL、参数一样、数据一样)会落到同一个工件上——那种字典只剩最后一条,早先那条就找不到自己那一次了。
老运行、只回了 tool.end 的执行(没有台账条目)逐次记录里没有,退回按工件去重的那一份,认法和以前一样。"""
records = [dict(r) for r in sealed.query_records_of(node_id) if r.get("artifact")]
if not records:
records = [{"artifact": str(artifact), **dict(info or {})}
for artifact, info in sealed.queries.items()
if str((info or {}).get("node_id") or "") == str(node_id)]
return records[-1] if records else None
def _rebound(item: Mapping[str, Any], plan: _Plan) -> str | None:
"""这条查询执行时的语句/参数和计划对不上就是重绑定(返回一句原因,对上返回 None)。"""
if not item.get("found"):
return None
executed = item.get("executed_sql")
if isinstance(executed, str):
item["executed_hash"] = _statement_hash(executed)
if executed != plan.statement:
return "实际执行的语句和这次计划渲染出来的不一致(运行期被重新绑定)"
material = item.get("parameters")
if isinstance(material, Mapping) and material.get("inputs_hash") \
and material.get("inputs_hash") != plan.material.get("inputs_hash"):
return "实际执行的参数输入哈希和这次计划的不一致(运行期被重新绑定)"
return None
def _statement_hash(statement: Any) -> str:
from app.ledger import params
return params.rendered_hash(statement)
# --------------------------------------------------------------------------
# 结果
# --------------------------------------------------------------------------
@dataclass(frozen=True)
class Outcome:
"""一次固定执行的结果。`ok` 是「这次真的算成了」:执行成功、闭包里每条查询都有结果、点名的每个指标都有值(没被
遮罩)——空指标、部分结果一律 ok=False。
`run_id` 是**这次执行的那次运行**;`execute_prepared` 交回的 Outcome 里它是服务端起的父 Run(显式绑定、白名单、
遮罩/只读与证据都在它上面),真正跑那几条 SQL 的子运行写在 `execution_run_id`(派生的口径固定执行)——其余字段
(status / metrics / queries / problems / quality)说的都是**子运行那一次**算成了没有。别的入口 execution_run_id 为空。"""
run_id: str
status: str
ok: bool
workflow_id: str
card_id: str
version: int
explicit_version: bool
purpose: str
publication: dict[str, Any]
selection: dict[str, Any]
parameters: dict[str, Any]
metrics: tuple[dict[str, Any], ...]
queries: tuple[dict[str, Any], ...]
data: dict[str, Any]
quality: dict[str, Any]
problems: tuple[dict[str, Any], ...]
trace: Any = None
#: K13 的测量记录:这次执行量到的分段耗时与计数(见 Measurement)。量不到的字段是 None
measurement: Measurement | None = None
#: 这次调用派生出来真正执行的那次运行(execute_prepared 的子运行;别的入口为空)
execution_run_id: str | None = None
def as_dict(self) -> dict[str, Any]:
return {"run_id": self.run_id, "status": self.status, "ok": self.ok, "workflow_id": self.workflow_id,
"card_id": self.card_id, "version": self.version, "explicit_version": self.explicit_version,
"purpose": self.purpose, "publication": dict(self.publication),
"selection": dict(self.selection), "parameters": dict(self.parameters),
"metrics": [dict(m) for m in self.metrics], "queries": [dict(q) for q in self.queries],
"data": dict(self.data), "quality": dict(self.quality),
"problems": [dict(p) for p in self.problems],
**({"trace": self.trace} if self.trace is not None else {}),
**({"execution_run_id": self.execution_run_id} if self.execution_run_id else {}),
**({"measurement": self.measurement.as_dict()} if self.measurement is not None else {})}
def _problem(code: str, message: str, **extra: Any) -> dict[str, Any]:
return {"code": code, "message": message, **{k: v for k, v in extra.items() if v not in (None, "")}}
# --------------------------------------------------------------------------
# K13:一次固定执行的测量(分段耗时与计数)
#
# 量的是「这次固定执行自己」:发布/定位、闭包/计划、参数、排队、数据库、证据各花多久,发出去几条 SQL、调了几个
# 模型、交回几条查询、有没有被截断、有没有出错或超时,以及 trace / run / query 的身份。测量只读时钟与这次运行自己
# 留下的记录与封存事件,不改执行路径:不加缓存、不改计划、不重试。
# --------------------------------------------------------------------------
#: 分段耗时的键(毫秒):locate/closure/params/run/evidence 首尾相接,加起来就是 total;queue 与 database 是 run 里面
#: 的两段(各有出处,不是另加的时间);量不到的一律 None,不填 0 冒充
PHASES = ("locate_ms", "closure_ms", "params_ms", "run_ms", "queue_ms", "database_ms", "evidence_ms", "total_ms")
class _Stage:
"""分段计时:段与段首尾相接(`mark` 结束当前段、开下一段),total 从建它那一刻算起。只读时钟,不改执行。"""
def __init__(self) -> None:
self._last = time.perf_counter()
self._started = self._last
self.ms: dict[str, int] = {}
def mark(self, name: str) -> int:
now = time.perf_counter()
self.ms[name] = max(0, int((now - self._last) * 1000))
self._last = now
return self.ms[name]
def total_ms(self) -> int:
return max(0, int((time.perf_counter() - self._started) * 1000))
@dataclass(frozen=True)
class Measurement:
"""一次固定执行的测量记录(K13)。**只记这一次真的量到的**:没量到的一律 None,不填 0 冒充。
phases(毫秒):
- `locate_ms` 发布/定位:版本解析(点名或推荐版)与固定、发布清单核验、用途判定;
- `closure_ms` 闭包/计划:K08 的显式选择展开、预算与输出形状判定;
- `params_ms` 参数:入口取值与校验(K01)、逐条查询的渲染与快照材料、探针图成形;
- `run_ms` 起运行到收尾:**含** queue 与 database 两段,也含图引擎自己的开销;
- `queue_ms` 排队:这次从要并发名额到拿到名额的时长(含发名额那一下的开销;没人在排队时就是那几毫秒),来自
运行记录的 `usage.queue_ms`(engine/runner 在要名额前后量的那一段),**不是**等人审批的 `wait_ms`;老运行、
还没收尾的运行没有这一项就是 None;
- `database_ms` 数据库:这次取数节点自己的 `node.finished.duration_ms` 之和(同一个节点执行多次取最后一次)。它
含着 SQL 往返、驱动与行映射,是这次执行里离数据库最近的一段,**不是**数据库内核时间;
- `evidence_ms` 证据:读封存证据、取指标与查询引用、拼结果与质量报告;
- `total_ms` 整次 `execute` 的墙钟。
counts:`planned_queries`(闭包里要执行的查询数)、`executed_sql`(真的发出去的 SQL 条数,按封存里逐次记下的查询
快照数,不是计划条数)、`llm_calls`(内部模型调用数,执行路径固定为 0)、`returned_queries` / `truncated_queries`
(交回证据引用的查询条数 / 其中被截断的条数)、`problems` / `errors`(结果里的问题条数,errors 不含超时)、
`timeouts`(到点没收尾 = 1)。
ids:`run_id`、`trace`、`workflow_id` / `card_id` / `version`、`query_ids`、`execution_ids`——按其中任何一个都能把
这次测量定位回那条运行、那条查询。
"""
phases: dict[str, Any]
counts: dict[str, Any]
ids: dict[str, Any]
def as_dict(self) -> dict[str, Any]:
return {"phases": dict(self.phases), "counts": dict(self.counts), "ids": dict(self.ids)}
def _queue_of(row: Any) -> int | None:
"""这次排队等了多久(毫秒):运行记录上的 `usage.queue_ms`,由 runtime 在等并发名额前后量(engine/runner._drive)。
没有这一项就是没量到(加这一项之前的运行、还没收尾的运行):返回 None,**绝不拿等人审批的 wait_ms 冒充排队**。"""
usage = getattr(row, "usage", None)
if not isinstance(usage, Mapping):
return None
queued = usage.get("queue_ms")
return int(queued) if isinstance(queued, int) and not isinstance(queued, bool) else None
def _from_evidence(sealed: Any, closure: Any) -> dict[str, Any]:
"""封存事件里量得到的几样:取数节点各自的执行时长、真发出去几条 SQL、内部模型调用数。
取数节点的执行时长按 `node.finished.duration_ms` 求和(同一个节点执行过多次取最后一次,同「节点最后一次产出」
的认法);SQL 条数按封存里**逐次**记下的查询快照数(同一份内容被两条查询执行两次就是两条),不是闭包里的计划
条数。模型调用数按 `llm.end` 事件数——执行路径 LLM=0,这一项是给「真的为 0」留个可核的数,不是估计。"""
fetch_nodes = {str(query.node_id) for query in closure.queries}
durations: dict[str, int] = {}
calls = 0
for _, kind, node_id, data in sealed.events:
if kind == "llm.end":
calls += 1
if kind != "node.finished" or not isinstance(data, Mapping) or str(node_id) not in fetch_nodes:
continue
spent = data.get("duration_ms")
if isinstance(spent, int) and not isinstance(spent, bool):
durations[str(node_id)] = max(0, spent)
return {"database_ms": sum(durations.values()), "llm_calls": calls,
"executed_sql": sum(1 for record in sealed.query_records
if str(record.get("node_id") or "") in fetch_nodes)}
def _measurement(stages: _Stage, *, run_id: str, workflow_id: str, card_id: str, version: int, closure: Any,
status: str, problems: Sequence[Mapping[str, Any]], queries: Sequence[Mapping[str, Any]],
facts: Mapping[str, Any], trace: Any) -> Measurement:
"""把这次执行量到的收成一份记录。成功之后才读得到的(SQL 条数、数据库那一截、交回几条查询)没读到时是 None。"""
stages.mark("evidence_ms")
timeouts = sum(1 for problem in problems if str(problem.get("code")) == TIMEOUT)
# 证据读到了才算量到「执行了几条 SQL / 交回几条查询」:没跑完(超时)一条都没有,也不能按计划条数顶上
measured = status == "succeeded" and facts.get("executed_sql") is not None
counts = {
"planned_queries": len(closure.queries),
"executed_sql": facts.get("executed_sql"),
"llm_calls": facts.get("llm_calls"),
"returned_queries": sum(1 for query in queries if query.get("found")) if measured else None,
"truncated_queries": (sum(1 for query in queries if query.get("found") and query.get("truncated"))
if measured else None),
"problems": len(problems),
"errors": len(problems) - timeouts,
"timeouts": timeouts,
}
phases = {name: stages.ms.get(name) for name in ("locate_ms", "closure_ms", "params_ms", "run_ms", "evidence_ms")}
# queue 与 database 是 run 里面的两段,各有各的出处:一个来自运行记录的排队那一项,一个来自封存事件里取数节点
# 自己的执行时长;两样都不在阶段计时器里
phases["queue_ms"] = facts.get("queue_ms")
phases["database_ms"] = facts.get("database_ms")
phases["total_ms"] = stages.total_ms()
assert set(phases) == set(PHASES), "分段耗时的键要和 PHASES 一致"
identity = {
"run_id": run_id, "trace": trace, "workflow_id": workflow_id, "card_id": card_id, "version": version,
"query_ids": [str(query.query_id) for query in closure.queries],
"execution_ids": [str(query["execution_id"]) for query in queries if query.get("execution_id")],
}
return Measurement(phases=phases, counts=counts, ids=identity)
# --------------------------------------------------------------------------
# 固定执行
# --------------------------------------------------------------------------
@dataclass(frozen=True)
class _Prepared:
"""一次固定执行在**起 Run 之前**已经判完、备好的东西(`_prepared` 交回)。"""
workflow: Any
resolved: Any
#: 这一版的图(清单核验、口径要读的源都按它认)
graph: dict[str, Any]
#: K08 的固定闭包与它那一版的口径卡(`derive.load_pinned`)
loaded: Any
closure: Any
values: dict[str, Any]
ignored: list[str]
plans: dict[str, _Plan]
publication_ref: dict[str, Any]
#: 派生的探针图(真正跑的那张:入口、取数、口径卡、出口)
graph_spec: dict[str, Any]
async def _prepared(session: AsyncSession, *, workflow_id: str, card_id: str, metric_ids: Sequence[str],
inputs: Mapping[str, Any] | None, version: int | None, purpose: str, actor: Any,
budget: Mapping[str, Any] | None, timeout_s: float | None,
stages: _Stage) -> tuple[_Prepared, dict[str, Any]]:
"""起运行之前判完这一次固定执行要用的东西:固定版本(并核普通使用者的资产授权)→ 核发布清单 → 解闭包 → 收参数。
`execute` 与 `execute_prepared` 共用这一份(两条路在起 Run 之前该判的一模一样,不各抄一遍):交回准备好的那一份
和这次的预算(`max_queries` / `timeout_s`,给了的照用)。"""
from app.apps.versions import is_published
from app.db.models import Workflow
from app.ledger import derive
workflow_id, card_id = str(workflow_id or ""), str(card_id or "")
wanted = [str(m) for m in (metric_ids or []) if str(m or "").strip()]
if not wanted:
raise QueryCoreRefused("metric_ids 是空的:这次一个指标也不选(不静默当整卡)", SELECTION_REFUSED,
status_code=422)
attempted = _budget_of(budget)
if timeout_s is not None:
attempted["timeout_s"] = timeout_s
# ---- 1) 版本:点名就固定到它;不点名读一次推荐版并固定(解析只发生这一次)----------------
workflow = await session.get(Workflow, workflow_id)
if workflow is None:
raise QueryCoreRefused("工作流不存在,可能已被删除", NOT_FOUND, status_code=404)
resolved = await publications.resolve(session, workflow, version=version, purpose=purpose)
if resolved is None:
detail = (f"「{workflow.name}」没有 v{version} 这个版本" if version is not None
else f"「{workflow.name}」现在没有推荐版(还没发布过)")
raise QueryCoreRefused(detail, VERSION_MISSING, status_code=404)
if not is_published(resolved.row, current=workflow.published_version) or resolved.row.retired_at is not None:
raise QueryCoreRefused(f"「{workflow.name}」v{resolved.version} 没有发布或已退役,不能按固定口径执行",
NOT_PUBLISHED, status_code=409)
# 版本已经固定:普通使用者这一档先核资产授权,没授权就当场拒(一条 SQL 都不发,也不起 Run)
await _require_caliber(session, actor, purpose=purpose, workflow=workflow, version=resolved.version)
# 卡在不在图里(图不存在时也要给出具体的一句话)由后面的闭包解析说;这里先把「清单必须就是这一版这一张图」核掉
# ---- 2) K07:清单必须读得出来、就是这一版这一张图、约定与现在兼容(不现场重解释旧发布)-----
graph = (resolved.row.graph or {}) if isinstance(resolved.row.graph, dict) else {}
content = _verified_manifest(resolved, graph=graph)
publication_ref = await _judged(session, workflow, resolved, purpose=purpose, content=content, graph=graph)
stages.mark("locate_ms")
# ---- 3) K08 闭包 + K01 参数语义(都在发起运行之前判完)--------------------------------
try:
loaded = await derive.load_pinned(session, resolved.workflow_id, resolved.version, card_id, metric_ids=wanted)
except derive.NotDerivable as e:
# 卡不在/不是口径卡:这一版里根本没有这张卡,谁能补也不该在这里补
code = CARD_MISSING if e.code in (derive.GONE, derive.NOT_CARD) else (
SELECTION_REFUSED if e.code == derive.SELECTION else e.code)
raise QueryCoreRefused(e.message, code, status_code=409,
problems=[p.as_dict() for p in getattr(e, "problems", ())]) from None
if loaded.closure is None or loaded.closure.whole_card:
raise QueryCoreRefused("这次的点名没有解析成显式选择(闭包不完整)", SELECTION_REFUSED, status_code=409)
closure = loaded.closure
if len(closure.queries) > int(attempted["max_queries"]):
raise QueryCoreRefused(
f"这次要执行 {len(closure.queries)} 条查询,超过预算 {attempted['max_queries']}:不静默少算",
BUDGET_REFUSED, status_code=429)
_scalar_output(loaded, closure.metric_ids)
stages.mark("closure_ms")
# 入口取值与校验(K01)到逐条查询的渲染和探针图成形都算「参数」这一段
values, ignored = _inputs_of(loaded, inputs or {})
plans, parameter_problems = _plans(loaded, closure, values)
if parameter_problems:
first = parameter_problems[0]
raise QueryCoreRefused(f"参数完整性不通过:{first.get('message') or first.get('code')}",
str(first.get("code") or PARAMETERS_REFUSED), status_code=422,
problems=parameter_problems)
graph_spec = derive.caliber_probe(loaded).graph
stages.mark("params_ms")
return _Prepared(workflow=workflow, resolved=resolved, graph=graph, loaded=loaded, closure=closure,
values=values, ignored=ignored, plans={plan.node_id: plan for plan in plans},
publication_ref=publication_ref, graph_spec=graph_spec), attempted
async def _require_caliber(session: AsyncSession, actor: Any, *, purpose: str, workflow: Any, version: Any) -> None:
"""普通使用者按一份口径执行之前的那一道(K14):资产授权到 use 及以上才放行,否则 403(起 Run 之前就拒)。
- **每一个用途都过这一道**:compose(问数、交互入口、画布试跑、父 Run 的 compose 调用)与 formal(`execute_formal`、
正式父 Run 派生的那一次执行)一样——正式运行也不能替使用者执行一份没授权给它的口径。formal 另**额外**走
`publications.judge`(发布依据不齐照样拒),两道都过才执行:授权这一道在前,依据那一道在后;
- 靠的是 `apps/access.caliber_role`:AppGrant(给本人、他所在的组、所有人,至少 use)加上应用**当前发布版**精确固定着
这一版(AppVersion.pins),两样都在才算「授权给了这个主体」;
- builder / admin / 关登录由**调用方**显式放行(`auth/chat_policy.restricted_actor`),这几档不写进资产授权函数;
- actor 缺失、认不出是谁:按普通使用者算——资产授权查不到主体就是没有授权,照拒(不拿「认不出」当放行)。"""
from app.apps.access import at_least, caliber_role
from app.auth.chat_policy import effective_actor, restricted_actor
who = effective_actor(actor)
if not await restricted_actor(session, who):
return
version = int(version)
role = await caliber_role(session, who, str(workflow.id), version)
if at_least(role, "use"):
return
detail = "正式运行也一样要这一份授权。" if purpose != publications.PURPOSE_COMPOSE else ""
raise QueryCoreRefused(
f"没有权限:口径「{workflow.name}」v{version} 没有授权给你使用。" + detail
+ "用一份已发布的口径要有应用授权(让搭建者把固定着这一版口径的应用授权给你,至少「使用」),"
"或者按数据源自己搭一张图",
NOT_AUTHORIZED, status_code=403,
problems=[{"code": NOT_AUTHORIZED, "workflow_id": str(workflow.id), "version": version,
"purpose": purpose, "role": role or ""}])
async def execute(
session: AsyncSession,
*,
workflow_id: str,
card_id: str,
metric_ids: Sequence[str],
inputs: Mapping[str, Any] | None = None,
version: int | None = None,
purpose: str = DEFAULT_PURPOSE,
actor: str | None = None,
timeout_s: float | None = None,
budget: Mapping[str, Any] | None = None,
trace: Any = None,
parent_run_id: str | None = None,
run_options: Mapping[str, Any] | None = None,
data_versions: Mapping[str, Any] | None = None,
emit: Any = None,
) -> Outcome:
"""按固定口径跑一次,交回类型化标量与证据引用。
- `workflow_id` + `card_id` 定口径身份;`version` 点名就固定到它,不点名就**读一次推荐版并固定**(不反复解析);
- `metric_ids` 是这次要算的指标(显式选择,走 K08 闭包;空的不当整卡);
- `inputs` 是这次入口的类型化取值(不认识的字段拒,缺参数在发 SQL 之前拒);
- `purpose` 由**宿主常量**定(默认 `compose`;`execute_formal` 传 formal),客户端传不了;
- `actor` 是认证主体(HTTP 入口从服务端身份取,宿主入口显式传),只记进运行来源,不从请求体读;普通使用者执行时
(compose 与 formal 一样)**固定版本之后、起 Run 之前**还要核这一版口径的资产授权(`apps/access.caliber_role` 到
use 及以上,否则 403 `querycore_not_authorized`,见 `_require_caliber`);
- `budget` 给 `max_queries` / `timeout_s`,`trace` 原样记进运行来源;
- `parent_run_id` / `run_options` / `data_versions` / `emit` 是 K10 父 Run 交下来的上下文:派生出来的探针运行是
**明确的子运行**(origin 里记 parent_run_id),照原样带上父 Run 的选项与固定的数据版本(遮罩、只读、上传/记录源
版本固定一条不断);这次口径那一版要读、父 Run 发起时又没算到的源,在这一层按**父 Run 的**类别与主体补固定
(`pin_sources_for_parent`),`emit` 用来把这次补固定追加进父 Run 的事件流。
拒(不发起)抛 QueryCoreRefused;发起了就交回 Outcome(含没跑完的 `status="running"`)。**被拒的这一次没有运行,也就
没有测量记录**(那时一条 SQL 都没发,没有运行可以定位)。"""
from app.auth.chat_policy import actor_name, effective_actor
# K13:这次执行的分段计时从进这门就开始
stages = _Stage()
if purpose not in publications.PURPOSES:
raise ValueError(f"不认识的用途:{purpose}")
# 认证主体:判授权用它本身(给了的用它,没给就取当前请求的身份——HTTP 入口在中间件里写好了),角色与组不丢;
# 记进运行、算行级范围用它的**名字**。两处是同一个主体,不能一处认得出、另一处认不出
subject = effective_actor(actor)
name = actor_name(actor)
prepared, attempted = await _prepared(session, workflow_id=workflow_id, card_id=card_id, metric_ids=metric_ids,
inputs=inputs, version=version, purpose=purpose, actor=subject,
budget=budget, timeout_s=timeout_s, stages=stages)
resolved, workflow = prepared.resolved, prepared.workflow
loaded, closure = prepared.loaded, prepared.closure
# ---- 4) 父 Run 上补固定这次要读的源(K10:晚解析的源固定在父 Run 上,不留给子运行)--------
# 子运行里补(tools/datasource._pin_late_records)会按 exploratory 重算行级范围(正式父 Run 的 all 被收成
# mine/none)、而且只写子运行那一行:同一次运行里的两次调用各固定各的版本。这里按父 Run 的类别与主体固定,
# 记在父 Run 上,子运行照它继承。
versions = data_versions
if parent_run_id:
versions = await pin_sources_for_parent(parent_run_id=str(parent_run_id), resolved=resolved,
graph=prepared.graph, card_id=str(loaded.node_id),
versions=data_versions, emit=emit)
# ---- 5) 起一次只读的探索运行(进程内 await,超时交回 running)--------------------------
run = await _start(prepared.graph_spec, values=prepared.values, workflow=workflow, resolved=resolved,
closure=closure, publication_ref=prepared.publication_ref, purpose=purpose, actor=name,
trace=trace, parent_run_id=parent_run_id, run_options=run_options,
data_versions=versions)
status = await _await_run(run.id, timeout_s=float(attempted["timeout_s"]))
stages.mark("run_ms")
# ---- 6) 结果:类型化指标 + 证据引用 + 质量 --------------------------------------------
return await _outcome(run.id, status=status, workflow=workflow, resolved=resolved, loaded=loaded, closure=closure,
plans=prepared.plans, values=prepared.values, ignored=prepared.ignored,
publication_ref=prepared.publication_ref, purpose=purpose, attempted=attempted,
trace=trace, stages=stages)
async def execute_formal(
session: AsyncSession,
*,
workflow_id: str,
card_id: str,
metric_ids: Sequence[str],
inputs: Mapping[str, Any] | None = None,
version: int | None = None,
actor: str | None = None,
timeout_s: float | None = None,
budget: Mapping[str, Any] | None = None,
trace: Any = None,
parent_run_id: str | None = None,
run_options: Mapping[str, Any] | None = None,
data_versions: Mapping[str, Any] | None = None,
emit: Any = None,
) -> Outcome:
"""宿主入口(K10 父 Run 用):purpose 固定成 formal,走**同一个** `publications.judge`——发布依据不齐(缺记录、
不是 matched、没有认证审阅、只确认了一部分、清单不是这一版这一张图的、老资产没有清单)一律拒,不换路、不降级。
这一层不发起正式运行(正式身份由父 Run 持有):它只按 formal 的分寸要求同一份依据,执行本身仍是进程内的固定执行。
**资产授权那一道照样要过**(`_require_caliber`,对每个用途都判):普通使用者拿不到这一版口径的 use 及以上时,正式运行也
不能替它执行——先是授权、再是发布依据,两道都过才跑。父 Run 交下来的上下文
(parent_run_id / run_options / data_versions / emit)同 `execute`,原样带进派生的子运行,晚解析的源也照 `execute` 那一套
固定在父 Run 上(行级范围取正式这一档:看全部)。"""
return await execute(session, workflow_id=workflow_id, card_id=card_id, metric_ids=metric_ids, inputs=inputs,
version=version, purpose=HOST_PURPOSE, actor=actor, timeout_s=timeout_s, budget=budget,
trace=trace, parent_run_id=parent_run_id, run_options=run_options,
data_versions=data_versions, emit=emit)
# --------------------------------------------------------------------------
# 服务端替普通使用者准备的那一条(K14):最小父图 + 真的从 caliber_query 工具走一次
# --------------------------------------------------------------------------
#: `execute_prepared` 那张最小父图的节点 id:入口、调用工具(口径查询)、出口
PREPARED_INPUT = "start"
PREPARED_NODE = "caliber"
PREPARED_OUTPUT = "out"
def prepared_graph(workflow_id: str, *, card_id: str, metric_ids: Sequence[str],
inputs: Mapping[str, Any] | None = None) -> dict[str, Any]:
"""服务端替普通使用者搭的那张**最小父图**:入口 → 调用工具(`caliber_query`)→ 出口。
**显式绑定**,不是模型挑的:口径身份(workflow + card)、要算的指标、这次入口取值都写死在节点上,用户不碰画布。这三个
节点都在 user_chat_run 那一份白名单里(input / tool / output),跑起来照样带遮罩与只读;能碰哪些口径仍由 QueryCore 按
资产授权核(`_require_caliber`,工具里那一次执行再核一遍)。"""
def node(nid: str, ntype: str, label: str, config: dict[str, Any]) -> dict[str, Any]:
return {"id": nid, "type": ntype, "position": {"x": 0, "y": 0},
"data": {"label": label, "config": config}}
args: dict[str, Any] = {"workflow": str(workflow_id), "card": str(card_id),
"metric_ids": [str(m) for m in metric_ids], "inputs": dict(inputs or {})}
return {
"nodes": [node(PREPARED_INPUT, "input", "入口", {}),
node(PREPARED_NODE, "tool", "按口径取数", {"tool": CALIBER_QUERY, "args": args}),
node(PREPARED_OUTPUT, "output", "成果",
{"fields": [{"name": "口径", "value": f"{{{{ nodes.{PREPARED_NODE}.ok }}}}"}]})],
"edges": [{"source": PREPARED_INPUT, "target": PREPARED_NODE},
{"source": PREPARED_NODE, "target": PREPARED_OUTPUT}],
}
async def execute_prepared(
session: AsyncSession,
*,
workflow_id: str,
card_id: str,
metric_ids: Sequence[str],
inputs: Mapping[str, Any] | None = None,
version: int | None = None,
actor: Any = None,
timeout_s: float | None = None,
budget: Mapping[str, Any] | None = None,
trace: Any = None,
origin_kind: str = "chat",
origin: Mapping[str, Any] | None = None,
run_options: Mapping[str, Any] | None = None,
) -> Outcome:
"""普通使用者的一条**确定请求**:服务端替它搭最小父图、经 `user_chat_run` 起一次探索父 Run,实际从 `caliber_query` 工具走。
与 `execute` 的区别只有一件:那条路是服务端自己起一次探针运行(用户看不到图),这一条是**真的发一次父 Run**——图里有
一个显式绑定 `caliber_query` 的调用工具节点,执行的是同一个工具(同一份固定执行、同一个子运行),证据接在父 Run 上。
普通使用者的「模型自己挑口径」那条路是它在自己的图里绑这个工具(chat/launch 自动绑定);这一条用于确定请求直达,不经模型。
- 起 Run 之前判的东西和 `execute` **一模一样**(`_prepared` 同一份):固定版本、普通使用者的资产授权
(`querycore_not_authorized`)、发布清单、K08 闭包、K01 参数——没授权/读不出来/点错了都在起任何 Run 之前拒;
- 父 Run 选项照普通问数:遮罩(MASK_RESULTS_OPTION)+ 只读(READ_ONLY_OPTION),记忆范围是这位使用者自己的;这次口径
查询的预算写进父 Run 的选项(`BUDGET_OPTION`),派生的子运行照它走;`origin_kind` / `origin` 由调用方给(问数据那一轮
是 chat + 对话/轮次),普通使用者读得到这次运行(origin_kind=chat、遮罩、自己是发起人);
- 交回的 Outcome 与 `execute` 同形:`run_id` 是这次起的**父 Run**(显式绑定、白名单、遮罩/只读与每条 query artifact
的证据都在它上面),`execution_run_id` 是它派生出来真正跑那几条 SQL 的子运行;metrics / queries / problems /
quality / status / ok 说的都是子运行那一次算成了没有;
- 父 Run 没跑成(工具被拒、运行失败、到点没收尾):照实交回 ok=False + 一条问题(`status="running"` 时报超时),
**绝不把「没跑成」说成成功**;这时没有子运行可定位,也就没有 metrics。
- `version` 点名时按**点名的那一版**固定并授权这一道;工具那条路固定的是**推荐版**(`caliber_query` 的参数里没有版本,
读一次推荐版并当场固定)——两者对不上(推荐版在这中间换过)时照实记一条 `querycore_version_moved` 并交回 ok=False,
不把另一版的数当这一版交出去。
拒(没发起任何运行)抛 QueryCoreRefused;发起了就交回 Outcome。"""
from app.auth.chat_policy import actor_name, effective_actor
from app.chat.launch import user_chat_run
from app.engine.launch import LaunchRefused, launch_run
stages = _Stage()
# 判授权用主体本身(角色与组不丢),记进运行、当记忆范围用名字:和 execute 同一套
subject = effective_actor(actor)
name = actor_name(actor)
prepared, attempted = await _prepared(session, workflow_id=workflow_id, card_id=card_id, metric_ids=metric_ids,
inputs=inputs, version=version, purpose=DEFAULT_PURPOSE, actor=subject,
budget=budget, timeout_s=timeout_s, stages=stages)
# 图上的口径身份用**这一层固定下来的**那一份(卡节点是核对过的源头卡,指标是闭包收窄过的那几个)
graph = prepared_graph(prepared.resolved.workflow_id, card_id=str(prepared.loaded.node_id),
metric_ids=list(prepared.closure.metric_ids), inputs=inputs or {})
# 白名单、记忆范围、遮罩与只读:和 HTTP 发起一轮问数据共用同一份(chat/launch),不在这里另抄一条
memory_scope, chat_options, graph = await user_chat_run(session, graph, name)
options = {BUDGET_OPTION: dict(attempted), **dict(run_options or {}), **chat_options}
try:
parent = await launch_run(session, graph=graph, input_payload={}, memory_scope=memory_scope,
run_class="exploratory", actor=name, options=options, origin_kind=origin_kind,
origin=dict(origin) if origin is not None else None)
except LaunchRefused as e:
# 没发起任何运行:和服务层别的拒绝同一个形状(调用方只认 QueryCoreRefused 这一种)
raise QueryCoreRefused(e.message, EXECUTION, status_code=e.status_code) from None
status = await _await_run(parent.id, timeout_s=float(attempted["timeout_s"]))
stages.mark("run_ms")
return await _prepared_outcome(parent.id, status=status, prepared=prepared, attempted=attempted, trace=trace)
async def _prepared_outcome(parent_run_id: str, *, status: str, prepared: _Prepared, attempted: Mapping[str, Any],
trace: Any) -> Outcome:
"""把父 Run 里那次工具调用的结果收成一份与 `execute` 同形的 Outcome(见 `execute_prepared`)。
结果取的是那次调用的**完整产出**(封存证据里按节点取它最后一次的产出工件),不是 tool.end 那个截到 2000 个字符的
预览:一次口径查询可能交回好几条查询,预览读不全。产出不在(父 Run 没跑成、工具没交出结果)时照实报 ok=False。"""
payload = await _tool_payload(parent_run_id)
if payload is None:
return _failed_outcome(parent_run_id, status=status, prepared=prepared, attempted=attempted, trace=trace)
problems = [dict(p) for p in payload.get("problems") or []]
# 固定的是发布物:子运行跑的必须就是这一层固定并授权过的那一版(推荐版在这中间换过就照实说,不把另一版当这一版交出)
ran_version = int(payload.get("version") or prepared.resolved.version)
if ran_version != int(prepared.resolved.version):
problems.append(_problem(
VERSION_MOVED, f"这次执行使用的是 v{ran_version},不是这一层固定并授权过的 "
f"v{int(prepared.resolved.version)}:推荐版在中途换过,请重新发起"))
return Outcome(
run_id=parent_run_id, status=str(payload.get("status") or status),
ok=bool(payload.get("ok")) and not any(str(p.get("code")) == VERSION_MOVED for p in problems),
workflow_id=str(payload.get("workflow_id") or prepared.resolved.workflow_id),
card_id=str(payload.get("card_id") or prepared.loaded.node_id), version=ran_version,
explicit_version=bool(payload.get("explicit_version")), purpose=str(payload.get("purpose") or DEFAULT_PURPOSE),
publication=dict(payload.get("publication") or {}), selection=dict(payload.get("selection") or {}),
parameters=dict(payload.get("parameters") or {}),
metrics=tuple(dict(m) for m in payload.get("metrics") or []),
queries=tuple(dict(q) for q in payload.get("queries") or []),
data=dict(payload.get("data") or {}), quality=dict(payload.get("quality") or {}),
problems=tuple(problems), trace=trace if trace is not None else payload.get("trace"),
measurement=_measurement_of(payload.get("measurement")),
execution_run_id=str(payload.get("run_id") or "") or None)
def _failed_outcome(parent_run_id: str, *, status: str, prepared: _Prepared, attempted: Mapping[str, Any],
trace: Any) -> Outcome:
"""父 Run 没交出结果(失败、还挂着、工具没交回产出):照实交回 ok=False 的那一份,不编造指标。"""
if status == "running":
problems = [_problem(TIMEOUT, f"这次执行在 {attempted['timeout_s']} 秒内没有收尾:状态仍是运行中,"
"请按 run_id 去看运行结果。超时不算成功")]
else:
problems = [_problem(EXECUTION, "这次口径查询没有交回结果(运行状态:" + status + ")")]
return Outcome(
run_id=parent_run_id, status=status, ok=False, workflow_id=str(prepared.resolved.workflow_id),
card_id=str(prepared.loaded.node_id), version=int(prepared.resolved.version), explicit_version=False,
purpose=DEFAULT_PURPOSE, publication=dict(prepared.publication_ref),
selection=_selection_of(prepared.closure), parameters={"values": dict(prepared.values)},
metrics=(), queries=(), data={"as_of": None, "sources": [], "tables": [], "data_versions": {},
"window": {}, "read": False},
quality={"status": "unknown"}, problems=tuple(problems), trace=trace)
async def _tool_payload(run_id: str) -> dict[str, Any] | None:
"""父 Run 里那个口径查询节点交回的完整产出;不是口径查询的结果(没跑、跑成别的样子)返回 None。"""
from app.engine.evidence_reader import read_sealed
try:
sealed = await read_sealed(run_id)
except Exception: # noqa: BLE001 - 证据读不出来只影响这一份结果的还原,不该让调用变成 500
logger.exception("prepare 的父运行 %s 读不出封存证据", run_id)
return None
payload = sealed.payload_of([PREPARED_NODE])
if not isinstance(payload, Mapping):
return None
if str(payload.get("kind") or payload.get("tool") or "") != CALIBER_QUERY:
return None
return dict(payload)
def _measurement_of(record: Any) -> Measurement | None:
"""子运行交回来的测量记录(dict)还原成 Measurement;形状不对就当没量到(None),不填 0 冒充。"""
if not isinstance(record, Mapping):
return None
phases, counts, ids = record.get("phases"), record.get("counts"), record.get("ids")
if not all(isinstance(part, Mapping) for part in (phases, counts, ids)):
return None
return Measurement(phases=dict(phases), counts=dict(counts), ids=dict(ids))
# --------------------------------------------------------------------------
# 内部:清单核验、判定、发起、等待、结果
# --------------------------------------------------------------------------
def _verified_manifest(resolved: Any, *, graph: Mapping[str, Any]) -> dict[str, Any]:
"""这一版的发布清单:读得出来、内容哈希对得上、**就是这一版这一张图**的,约定和现在这一套兼容。
老资产(这一版没有发布事件)走 GET/trial 那条老路,这里明确拒:没有清单就没有「固定发布执行」可核。"""
publication = resolved.publication
if publication is None:
raise QueryCoreRefused(
f"「{resolved.workflow_id}」v{resolved.version} 没有发布清单(加发布依据机制之前发布的老资产):"
"QueryCore 只按固定发布执行,老资产请走原来的试运行路径", MANIFEST_REQUIRED, status_code=409)
try:
content = publications.read(publication)
except publications.PublicationUnreadable as e:
raise QueryCoreRefused(e.message, MANIFEST_UNREADABLE, status_code=409,
problems=[{"code": e.code}]) from None
problems = publications.compiler_problems(content)
if problems:
raise QueryCoreRefused("这一版的发布清单不是按现在这套编译器/绑定/参数/解析器约定物化的,不按新规则重解释旧发布:"
+ ";".join(str(p.get("message") or p.get("code")) for p in problems[:4]),
INCOMPATIBLE, status_code=409, problems=problems)
problem = publications.manifest_problem(publication, graph=graph)
if problem is not None:
raise QueryCoreRefused(str(problem.get("message") or "发布清单和这一版对不上"),
publications.BASIS_MISMATCH, status_code=409, problems=[problem])
return content
async def _judged(session: Any, workflow: Any, resolved: Any, *, purpose: str, content: Mapping[str, Any],
graph: Mapping[str, Any]) -> dict[str, Any]:
"""同一次判定(publications.judge,固定到刚解析出来的那一版):formal 依据不齐就拒,别的用途只读身份。
判定用的版本是 `resolved.version`(已经固定),不重新解析推荐版;交回的那一份把清单引用和内容里的确定信息一起带上。"""
judged = await publications.judge(session, workflow, version=resolved.version, purpose=purpose)
if judged is None:
raise QueryCoreRefused(f"「{workflow.name}」v{resolved.version} 读不出来", VERSION_MISSING, status_code=404)
refusal = judged.refusal
if refusal is not None:
# formal:发布依据不齐(缺记录/不是 matched/没有认证审阅/只确认一部分/清单错图缺卡……)。机器码用 judge 的原码,
# 让入口和正式运行的说法一致;不换路、不降级成探索执行
raise QueryCoreRefused(refusal[1], refusal[0], status_code=409, problems=list(judged.problems))
return {**judged.as_dict(),
"workflow_id": resolved.workflow_id, "graph_hash": str(content.get("graph_hash") or ""),
"execution_hash": str(content.get("execution_hash") or ""), "format": publications.FORMAT,
"compiler": dict(content.get("compiler") or {}), "level": str(content.get("level") or ""),
"publication_id": str(getattr(resolved.publication, "id", "") or "")}
# --------------------------------------------------------------------------
# K10:晚解析的数据源固定在**父 Run** 上
# --------------------------------------------------------------------------
def target_key(workflow_id: str, version: Any, card_id: str) -> str:
"""这次执行钉住的固定目标:`<工作流>@<版本>:<卡>`。
父 Run 记在补固定项上,用来认出「同一次运行里的这份固定是哪一份口径要的」——换了另一份口径(另一个工作流、
另一版、另一张卡)之后源又换过版本,就要明确说冲突,不能悄悄读另一版。"""
return f"{workflow_id}@{int(version)}:{card_id}"
def card_sources(graph: Mapping[str, Any], card_id: str) -> tuple[set[str], bool]:
"""这一版图里那张口径卡读的取数源名,以及认不认得出来(纯函数,无 IO)。
认得出:`card_inputs` 给出的取数节点绑的 `db_query__` / `db_schema__` 源。认不出(不是口径卡、式子里认不出取数
节点)就退回**这一版图里绑的全部数据源**(宁可多固定几个,也不漏),照实标成没认出来。"""
from app.engine.launch import tool_bindings
from app.engine.schema import GraphSpec
from app.look.calibers import card_inputs
from app.tools.datasource import source_of_tool
inputs, _reason = card_inputs(dict(graph), card_id)
tools = [str(item.get("tool") or "") for item in inputs if isinstance(item, dict)]
sure = bool(tools)
if not sure:
tools = [name for _, name in tool_bindings(GraphSpec.model_validate(dict(graph)))]
return {source for tool in tools if (source := source_of_tool(tool)) is not None}, sure
async def pin_sources_for_parent(*, parent_run_id: str, resolved: Any, graph: Mapping[str, Any],
card_id: str, versions: Mapping[str, Any] | None,
emit: Any = None) -> dict[str, Any]:
"""这次口径那一版读的源里,父 Run 还没固定的,按**父 Run 的**类别与主体补固定进父 Run 的记录(K10)。
为什么不能在子运行里补(tools/datasource._pin_late_records):那条路按子运行的类别(exploratory)与主体重算
行级范围——正式父 Run 的 all 会被收成 mine/none;而且只写进子运行那一行:同一次运行里的两次调用各固定各的版本。
这里固定的是**父 Run 自己已经授权的那一档**(`run_class` / `started_by`,同它发起时给别的源定范围的那一套),
只记在父 Run 上,子运行照父 Run 那一份继承——不放大也不缩小。
- 已经固定过的源照旧复用(同一次运行里两次调用读同一版),不重新固定;
- 已经固定过、但那次固定属于**另一份口径目标**、而这个源现在的版本已经不是固定那一版:明确拒
(`DATA_VERSION_CONFLICT`),不静默换一版;
- 父 Run 的记录读不回来(直接调工具的单测、在别处调用):原样交回,不固定也不报错。
补固定本身在父 Run 上追加一条事件(`LATE_PIN_CODE` 的日志事件,带源、版本、口径目标、以及这次判定的行级范围):
父 Run 的 `run.started` 已经落库、封不进去,只能追加。"""
from sqlalchemy import select
from app.data import table_versions
from app.db.base import SessionLocal
from app.db.models import DataSource, Run
from app.tools.datasource import _late_lock
out = dict(versions or {})
names, _sure = card_sources(graph, card_id)
if not names:
return out
key = target_key(resolved.workflow_id, resolved.version, card_id)
pinned: list[dict[str, Any]] = []
async with _late_lock():
# 上传源要读当前指针到写进运行记录,持着版本存储的锁(同 tools/datasource._pin_late)
async with table_versions.pin_lock(), SessionLocal() as own:
run = await own.get(Run, str(parent_run_id))
if run is None:
return out # 父 Run 的记录不在了:不在这里猜,交给调用方
stored = dict(run.data_versions or {})
for name in sorted(names):
row = (await own.execute(select(DataSource).where(
DataSource.name == name, DataSource.enabled.is_(True)))).scalar_one_or_none()
if row is None:
continue # 名字对不上(源被改名、停用、删了):查询自己会照实报错
entry = stored.get(row.id)
if _pinned_version(entry) is not None:
_refuse_conflict(row, entry=entry, key=key, resolved=resolved, card_id=card_id,
current=await _current_version(own, row))
out[row.id] = entry
continue
entry = await _pin_now(own, row, run_class=str(run.run_class or "exploratory"),
started_by=run.started_by)
if entry is None:
continue # 登记或数据文件不在:不固定,查询时照实报错
entry.update(late=True, caliber=key)
stored[row.id] = entry
out[row.id] = entry
pinned.append({"source_id": row.id, "source": row.name, "entry": entry})
if pinned:
run.data_versions = stored
await own.commit()
for item in pinned:
await _announce_pin(emit, **item, key=key, resolved=resolved)
return out
def _pinned_version(entry: Any) -> Any:
"""这一项固定的是哪一版:上传源是快照 id,记录表是表的版本号。没固定(或形状不对)为 None。"""
if not isinstance(entry, dict):
return None
if isinstance(entry.get("snapshot"), str) and entry["snapshot"]:
return entry["snapshot"]
if isinstance(entry.get("version"), int) and isinstance(entry.get("seq"), int):
return int(entry["version"])
return None
async def _current_version(session: AsyncSession, row: Any) -> Any:
"""这个源现在这一版是哪一版(和 `_pinned_version` 同一种取值)。"""
if (getattr(row, "origin", None) or "manual") == "records":
from app.records.tables import table_of_source
table = await table_of_source(session, row.id)
return int(table.version) if table is not None else None
snapshot = getattr(row, "current_snapshot_id", None)
return str(snapshot) if snapshot else None
async def _pin_now(session: AsyncSession, row: Any, *, run_class: str, started_by: str | None) -> dict[str, Any] | None:
"""现在把这一版固定下来(和发起时固定用的是同一套):上传源取当前快照,记录表记高水位与**父 Run 的**行级范围。"""
from app.records.views import pin_entry
if (getattr(row, "origin", None) or "manual") == "records":
return await pin_entry(session, row, run_class=run_class, started_by=started_by)
snapshot = getattr(row, "current_snapshot_id", None)
if not snapshot:
return None
return {"snapshot": str(snapshot), "name": row.name}
def _refuse_conflict(row: Any, *, entry: Mapping[str, Any], key: str, resolved: Any, card_id: str,
current: Any) -> None:
"""这一项固定属于另一份口径目标、而这个源现在已经是另一版:明确拒,不静默换一版(同一次运行不能读两版数据)。
消息要短:节点把它交给 describe_exception 时会按 240 个字符截断,写太长机器码就被截掉了。精确的值放在
problems 里(程序看那一份)。"""
owned_by = str(entry.get("caliber") or "")
if not owned_by or owned_by == key or _pinned_version(entry) == current:
return
raise QueryCoreRefused(
f"数据源「{row.name}」在这次运行里固定的是 {_version_text(row, _pinned_version(entry))},"
f"这次要执行的口径「v{resolved.version}:{card_id}」引用的是 {_version_text(row, current)}:"
"同一次运行不能悄悄换一版数据。请重新发起运行,或让两份口径用同一版数据",
DATA_VERSION_CONFLICT, status_code=409,
problems=[{"code": DATA_VERSION_CONFLICT, "source": row.name, "pinned": _pinned_version(entry),
"current": current, "caliber": owned_by, "target": key}])
def _version_text(source: Any, value: Any) -> str:
"""报错里那一版的写法:上传源给快照 id 的前 12 位,记录表给版本号。"""
if value is None:
return "(读不出来)"
if (getattr(source, "origin", None) or "manual") == "records":
return f"记录表第 {value} 版"
return f"快照 {str(value)[:12]}"
async def _announce_pin(emit: Any, *, source_id: str, source: str, entry: Mapping[str, Any], key: str,
resolved: Any) -> None:
"""把这次补固定写进父 Run 的事件流(`run.started` 已经落库,补不进去,只能追加)。"""
import inspect
if emit is None:
return
from app.core.events import EventType
from app.tools.datasource import LATE_PIN_CODE
pinned_text = (str(entry.get("snapshot")) if entry.get("snapshot")
else f"记录表第 {entry.get('version')} 版、高水位 {entry.get('seq')}")
data = {"source_id": source_id, "source": source, "late": True, "caliber": key, "pinned_by": "caliber_query",
"workflow_id": resolved.workflow_id, "version": resolved.version,
**{k: entry.get(k) for k in ("snapshot", "seq", "rows", "viewer", "version") if entry.get(k) is not None}}
note = (f"数据源「{source}」发起时没有固定版本,这次口径查询在父运行上固定为当时的那一版({pinned_text}),"
f"行级范围照父运行的授权({entry.get('rows') or '不适用'});之后继续运行、审批恢复、同一次运行里别的调用"
"都沿用这一版")
try:
out = emit(EventType.LOG, level="info", code=LATE_PIN_CODE, message=note, **data)
if inspect.isawaitable(out):
await out
except Exception: # noqa: BLE001 - 事件只是留痕,固定本身已经记进运行记录
logger.warning("补固定的父 Run 事件没发出去(口径 %s@%s)", resolved.workflow_id, resolved.version,
exc_info=True)
async def _start(graph_spec: Mapping[str, Any], *, values: Mapping[str, Any], workflow: Any, resolved: Any,
closure: Any, publication_ref: Mapping[str, Any], purpose: str, actor: str | None,
trace: Any, parent_run_id: str | None = None, run_options: Mapping[str, Any] | None = None,
data_versions: Mapping[str, Any] | None = None) -> Any:
"""起一次只读的探索运行:来源按 engine/origin 的常量记(口径固定执行),
origin 数据里带上这次固定的版本、清单、闭包身份、用途与 trace。
K10 父 Run 那条路(`caliber_query`):派生的探针运行是**明确的子运行**——origin 里记 `parent_run_id`,选项、固定的数据
版本照父 Run 原样带下去(遮罩、数据目录视图、上传/记录源版本固定一条不断),只读边界照旧一律加上。"""
from app.core.events import EventType
from app.engine.origin import origin_note
from app.engine.runner import run_manager
from app.tools.datasource import READ_ONLY_OPTION
origin = {
"service": "querycore", "purpose": purpose, "workflow_id": resolved.workflow_id,
"version": resolved.version, "node_id": closure.source.get("card_id"),
"card": closure.source.get("card_id"),
"manifest": publication_ref.get("manifest"), "identity": publication_ref.get("identity"),
"selection_hash": closure.selection_hash, "closure_hash": closure.closure_hash,
"metric_ids": list(closure.metric_ids), "query_ids": list(closure.query_ids),
**({PARENT_RUN_KEY: str(parent_run_id)} if parent_run_id else {}),
**({"trace": trace} if trace is not None else {}),
}
data = {**origin, "actor": actor}
kind = origin_kind()
notes = [(EventType.RUN_ORIGIN, None, origin_note(kind, **data))]
# 父 Run 的选项照原样带下去(遮罩、数据目录视图……),只读边界一律加上:这次执行的取数一律只读
options = {**dict(run_options or {}), READ_ONLY_OPTION: True}
try:
return await run_manager.start(
graph=dict(graph_spec), input_payload=dict(values), workflow_id=resolved.workflow_id,
workflow_name=str(workflow.name or ""), run_class="exploratory", version=resolved.version,
version_hash=str(getattr(resolved.row, "graph_hash", "") or ""), started_by=actor,
options=options, data_versions=dict(data_versions) if data_versions is not None else None,
notes=notes, origin_kind=kind, origin=origin)
except ValueError as e:
raise QueryCoreRefused(f"这次固定执行的图过不了校验:{e}", EXECUTION, status_code=409) from None
async def _await_run(run_id: str, *, timeout_s: float) -> str:
"""进程内等这次运行收尾:先等执行任务(不阻塞事件循环),没完就看库里的状态,到点交回 running。
**超时不算成功**:还没到终态就照实回 running,run_id 交给调用方自己去看(同试跑接口的分寸)。"""
from app.db.base import SessionLocal
from app.db.models import Run
from app.engine.runner import run_manager
loop = asyncio.get_running_loop()
deadline = loop.time() + max(0.0, float(timeout_s))
while True:
async with SessionLocal() as poll:
row = await poll.get(Run, run_id)
status = str(row.status) if row is not None else "unknown"
if row is None or status in TERMINAL_RUN_STATUSES:
return status
remaining = deadline - loop.time()
if remaining <= 0:
return "running"
await run_manager.wait_idle(run_id, timeout=min(remaining, 0.5))
async def _outcome(run_id: str, *, status: str, workflow: Any, resolved: Any, loaded: Any, closure: Any,
plans: Mapping[str, _Plan], values: Mapping[str, Any], ignored: Sequence[str],
publication_ref: Mapping[str, Any], purpose: str, attempted: Mapping[str, Any],
trace: Any, stages: _Stage) -> Outcome:
"""收尾:按运行状态取证据、拼类型化结果、判「算成了没有」,并把这次量到的收成一份测量记录(K13)。"""
from app.db.base import SessionLocal
from app.db.models import Run
from app.engine.validation import validation_report
problems: list[dict[str, Any]] = []
metrics: list[dict[str, Any]] = []
raw_metrics: dict[str, Any] = {}
queries: list[dict[str, Any]] = []
trusted = False
#: 这一次收尾里几处共用的封存证据(见 _sealed_of):同一份快照不读三遍
memo: dict[str, Any] = {}
async with SessionLocal() as poll:
row = await poll.get(Run, run_id)
finished = row.finished_at.isoformat() if row is not None and row.finished_at else None
error = row.error if row is not None else None
# K13:排队时长读运行记录上的那一项(老运行、还没收尾的运行没有它就是 None);其余几样要读到执行证据才有
facts: dict[str, Any] = {"queue_ms": _queue_of(row), "database_ms": None, "llm_calls": None, "executed_sql": None}
if row is None:
# 运行记录读不回来(被清掉之类):交不回结果,也绝不冒充成功
problems.append(_problem(NO_RUN, "这次执行的运行记录不在了:交不回结果"))
elif status == "succeeded":
try:
metrics, raw = await _metrics_from(run_id, card_id=str(loaded.node_id), metric_ids=closure.metric_ids,
memo=memo)
trusted = bool(raw.get("trusted"))
raw_metrics = raw.get("metrics") or {}
if not trusted:
problems.append(_problem(EVIDENCE, "这次运行的封存证据核验没通过:不能把结果当已核验的值交出"))
except Exception: # noqa: BLE001 - 证据读不出来(工件被改、读不回来)也不该让请求变成 500
logger.exception("QueryCore 执行 %s 的指标证据读不出来", run_id)
problems.append(_problem(EVIDENCE, "这次运行的指标证据读不出来:不能把结果交出"))
try:
queries = await _queries_from(run_id, closure=closure, plans=plans, memo=memo)
except Exception: # noqa: BLE001 - 查询快照读不出来:交不回证据引用,照实说
logger.exception("QueryCore 执行 %s 的查询证据读不出来", run_id)
problems.append(_problem(QUERY_MISSING, "这次运行的查询快照读不出来:证据引用交不回来"))
try:
# K13:数据库那一截(取数节点自己的执行时长)与真发出去的 SQL 条数按封存事件与逐次记录量。封存都得
# 到这一步了还读不出来,就留 None——量不到是量不到,不拿计划条数或 0 顶上
facts.update(_from_evidence(await _sealed_of(run_id, memo), closure))
except Exception: # noqa: BLE001 - 量不出来不该影响结果本身
logger.exception("QueryCore 执行 %s 的执行量测不出来", run_id)
elif status == "running":
problems.append(_problem(TIMEOUT, f"这次执行在 {attempted['timeout_s']} 秒内没有收尾:状态仍是运行中,"
"请按 run_id 去看运行结果。超时不算成功"))
else:
problems.append(_problem(EXECUTION, error or "这次执行没有成功完成", status=status))
present = {str(m.get("id")) for m in metrics}
if row is not None and status == "succeeded":
# 指标/查询层面的问题只在「跑完了」时才说:没跑完谈不上缺指标,那条已经由 TIMEOUT/EXECUTION/NO_RUN 说了
for metric_id in closure.metric_ids:
if metric_id not in present:
problems.append(_problem(METRIC_MISSING, f"点名的指标「{metric_id}」没有出现在结果里",
metric_id=metric_id))
continue
if str(next(m for m in metrics if str(m.get("id")) == metric_id).get("status") or "") != "ok":
problems.append(_problem(METRIC_NO_VALUE, f"指标「{metric_id}」这次没有可用的标量值",
metric_id=metric_id))
if any(m.get("masked") for m in metrics):
problems.append(_problem(METRIC_MASKED, "有指标的值被遮罩:不把遮罩值当结果交出"))
missing_queries = [q["query_id"] for q in queries if not q.get("found")]
if missing_queries:
problems.append(_problem(QUERY_MISSING, "这次要执行的查询没有留下快照:" + "、".join(missing_queries),
query_ids=missing_queries))
for query in queries:
if query.get("rebound"):
problems.append(_problem(REBOUND, f"查询「{query['query_id']}」:{query['rebound']}",
query_id=query["query_id"]))
if not metrics:
problems.append(_problem(EMPTY, "这次执行没有任何指标结果:空结果不算成功"))
quality = validation_report(list(raw_metrics.values()), execution=status, trusted=trusted)
parameters = {
"adapter": _plans_adapter(plans),
"values": dict(values),
"ignored": list(ignored),
"contract": [dict(c) for c in _contract_of(loaded)],
"queries": [plan.as_dict() for plan in plans.values()],
}
measurement = _measurement(stages, run_id=run_id, workflow_id=str(resolved.workflow_id),
card_id=str(loaded.node_id), version=int(resolved.version), closure=closure,
status=status, problems=problems, queries=queries, facts=facts, trace=trace)
return Outcome(
run_id=run_id, status=status, ok=not problems and status == "succeeded" and bool(metrics),
workflow_id=str(resolved.workflow_id), card_id=str(loaded.node_id), version=int(resolved.version),
explicit_version=bool(resolved.explicit), purpose=purpose, publication=dict(publication_ref),
selection=_selection_of(closure), parameters=parameters, metrics=tuple(metrics), queries=tuple(queries),
data=_data_of(queries, loaded=loaded, values=values, finished=finished),
quality=quality, problems=tuple(problems), trace=trace, measurement=measurement)
def _plans_adapter(plans: Mapping[str, _Plan]) -> str:
from app.ledger import params
for plan in plans.values():
if plan.material.get("adapter"):
return str(plan.material["adapter"])
return params.ADAPTER
def _contract_of(loaded: Any) -> list[dict[str, Any]]:
from app.ledger import params
return params.contract(loaded.entry_fields)
def _selection_of(closure: Any) -> dict[str, Any]:
"""这次的 selection/closure 身份:算哪些指标、执行哪几条查询(含共享 SQL 还在算谁)、解析器版本与两份哈希。"""
return {**closure.summary(), "card_metric_ids": list(closure.card_metric_ids)}
def _data_of(queries: Sequence[Mapping[str, Any]], *, loaded: Any, values: Mapping[str, Any],
finished: str | None) -> dict[str, Any]:
"""这次数据读了什么、什么时候读的:来源、表、上传源的数据版本、这次的时间窗口。"""
from app.ledger import params
sources: set[str] = set()
tables: set[str] = set()
versions: dict[str, Any] = {}
for query in queries:
if query.get("data_source"):
sources.add(str(query["data_source"]))
tables.update(str(t) for t in query.get("tables") or [])
if query.get("data_version") and query.get("data_source"):
versions[str(query["data_source"])] = query["data_version"]
declared = params.declared(loaded.entry_fields)
window = {str(name): value for name, value in values.items()
if (params.field_type(declared.get(name) or {}) in _TIME_TYPES
or (isinstance(value, str) and _DATE_LIKE.match(value.strip())))}
return {"as_of": finished, "sources": sorted(sources), "tables": sorted(tables),
"data_versions": versions, "window": window, "read": bool(queries)}
# --------------------------------------------------------------------------
# K10:父 Run 的工具入口(caliber_query)
# --------------------------------------------------------------------------
class CaliberQueryArgs(BaseModel):
"""caliber_query 的参数:口径身份(workflow + card)、要算的指标、这次入口的类型化取值。
**参数到此为止**:用途(formal/compose)、认证主体、父 Run 身份、固定的数据版本都由父 Run 的上下文定,参数里没有
也传不了;点名版本留给别的调用方,父 Run 走的是「读一次推荐版并当场固定」。"""
workflow: str = Field(description="口径所在的工作流:工作流 id,或唯一的工作流名字")
card: str = Field(description="工作流里那张口径卡的节点 id:一次只算一张卡")
metric_ids: list[str] = Field(min_length=1, description="这次要算的指标 id,可以多个;空的不当整卡")
inputs: dict[str, Any] = Field(
default_factory=dict,
description="这次入口的取值:{字段:值},按入口声明的类型校验(日期写 YYYY-MM-DD;跨源穿透传入对侧编码或标准实体键时由平台自动对齐换算)",
)
@register(
name=CALIBER_QUERY,
category="口径",
effect="read",
description="按一份已发布的固定口径取一次数(工作流 + 口径卡 + 指定指标 + 入口取值),交回类型化的指标值和每条查询的"
"证据引用——数出自哪一版、哪份发布清单、哪条 SQL 的执行都核得到。只读,不改数据。",
args_schema=CaliberQueryArgs,
)
async def caliber_query(ctx: ToolContext, **kwargs: Any) -> dict[str, Any]:
"""父 Run 的工具入口(K10):用**父 Run 的上下文**按固定口径执行一次,当场 await 收尾,把查询证据接回父 Run。
- 用途由父 Run 定:`formal` 的父 Run 走 `execute_formal`(同一套 `publications.judge`,发布依据不齐就拒,不换路、
不降级成探索),别的(交互、画布、试用)走 compose——拿不到正式身份;
- 父 Run 固定的上传/记录源版本、选项(遮罩、只读、数据目录视图)与来源照原样带进派生的子运行(origin 里记
`parent_run_id`),不失去父关系;超时、失败、遮罩、空指标照实交回 `ok=False`,绝不冒充成功;
- 交回的结果与交互工作台同一种形状:metrics + queries 数组(每条带 query_id / execution_id / attempt、工件、来源、
参数材料),节点据此把每条 query artifact 以合法引用记进父 Run 的证据。"""
extra_entity_inputs = kwargs.pop("entity_inputs", None)
extra_from_source = kwargs.pop("from_source", None)
args = CaliberQueryArgs(**kwargs)
from app.db.base import SessionLocal
from app.db.models import Run
parent = str(getattr(ctx, "run_id", "") or "")
if not parent:
raise QueryCoreRefused("caliber_query 只能在一次运行里调用:没有父运行,就没有证据可以接回去",
NO_PARENT, status_code=409)
try:
async with SessionLocal() as session:
# 用途、主体、选项、固定的数据版本一律以**父运行的记录**为准(engine/runner 存下的那一份),ToolContext
# 里那几项只当兜底:工具在哪条路上被调(调用工具节点、Agent、协作成员)都拿到同一个答案,不会被某个节点
# 少的几项悄悄换掉。父运行记录读不回来(单测直接调工具)就按 ToolContext 给的算。
parent_row = await session.get(Run, parent)
if parent_row is not None:
run_class = str(parent_row.run_class or "")
options = dict(parent_row.options or {})
versions = parent_row.data_versions
actor = parent_row.started_by
trace = parent_row.origin
else:
run_class = str(getattr(ctx, "run_class", "") or "")
options = dict(getattr(ctx, "run_options", None) or {})
versions = getattr(ctx, "data_versions", None)
actor = getattr(ctx, "started_by", None)
trace = getattr(ctx, "origin", None)
purpose = HOST_PURPOSE if run_class == "formal" else DEFAULT_PURPOSE
declared_budget = options.get(BUDGET_OPTION)
workflow_id = await _workflow_id(session, args.workflow)
effective_inputs = dict(args.inputs or {})
alignments: list[dict[str, Any]] = []
if effective_inputs or extra_entity_inputs:
from app.auth.chat_policy import restricted_actor
from app.tools.ontology import align_caliber_inputs
is_restricted = await restricted_actor(session, actor)
effective_inputs, alignments = await align_caliber_inputs(
session,
workflow_id=workflow_id,
card_id=args.card,
inputs=effective_inputs,
entity_inputs=extra_entity_inputs if isinstance(extra_entity_inputs, Mapping) else None,
from_source=str(extra_from_source) if extra_from_source else None,
restricted=is_restricted,
)
outcome = await execute(
session, workflow_id=workflow_id, card_id=args.card, metric_ids=args.metric_ids,
inputs=effective_inputs,
purpose=purpose, actor=actor, parent_run_id=parent, run_options=options,
data_versions=versions if isinstance(versions, Mapping) else None,
budget=declared_budget if isinstance(declared_budget, Mapping) else None, trace=trace,
# 晚解析的源(发布得晚、推荐版变了、动态引用)在父 Run 上补固定:把这次补固定追加进父 Run 的事件流
emit=getattr(ctx, "emit", None))
except QueryCoreRefused as e:
# 拒(没发起)在工具这一层是一条失败,不是一份结果:机器码带在报错里,节点、Agent 的失败信息按它认这一档,
# 不用去解析中文
raise ValueError(f"{e.message}({e.code})") from None
out_payload: dict[str, Any] = {"tool": CALIBER_QUERY, "kind": CALIBER_QUERY, **outcome.as_dict()}
if alignments:
out_payload["ontology_alignment"] = alignments
violated = [m for m in out_payload.get("metrics") or [] if isinstance(m, Mapping) and m.get("contract_findings")]
emit_fn = getattr(ctx, "emit", None)
if violated and callable(emit_fn):
import inspect
from app.core.events import EventType
first_finding = (violated[0].get("contract_findings") or [""])[0]
detail = (
str(first_finding.get("detail") or first_finding.get("code") or "触发指标数值契约异常")
if isinstance(first_finding, Mapping)
else (str(first_finding) or "触发指标数值契约异常")
)
emitted = emit_fn(
EventType.LOG,
level="warn",
code="metric_contract_violated",
workflow_id=out_payload.get("workflow_id"),
card_id=out_payload.get("card_id"),
metric=violated[0].get("id"),
metrics=[m.get("id") for m in violated],
metric_names=[m.get("name") or m.get("id") for m in violated],
reason=detail,
message="指标" + "".join(f"「{m.get('name') or m.get('id')}」" for m in violated) + f"触发数值契约告警:{detail}",
)
if inspect.isawaitable(emitted):
await emitted
return out_payload
async def _workflow_id(session: AsyncSession, ref: str) -> str:
"""工具参数里的工作流:先按 id 认;认不到再按名字认,且只有一个同名时才认(多个同名照实拒,不替它挑一个)。"""
from sqlalchemy import select
from app.db.models import Workflow
ref = str(ref or "").strip()
workflow = await session.get(Workflow, ref)
if workflow is not None:
return workflow.id
found = list((await session.execute(select(Workflow).where(Workflow.name == ref))).scalars())
if not found:
raise QueryCoreRefused(f"找不到口径所在的工作流「{ref}」:可能已被删除,或名字写错了",
NOT_FOUND, status_code=404)
if len(found) > 1:
raise QueryCoreRefused(f"有 {len(found)} 个工作流都叫「{ref}」:请用工作流 id 点名要哪一份口径",
NOT_FOUND, status_code=409)
return found[0].id
def query_entries(payload: Any) -> list[dict[str, Any]]:
"""`caliber_query` 交回的结果里的查询条目——**多条**(一张卡这一次可能执行好几条取数)。
不是 caliber_query 的结果(名字不对、没有 queries 数组)返回空列表:调用方按工具名认身份,不看结果长得像不像。
单条查询的数据源工具走 engine/evidence.query_entry_fields,不走这里;两支都由 capabilities.registry.query_items
摊成同一种条目,再由 snapshot_owned 逐条认工件。"""
data = payload
if isinstance(data, str):
text = data.lstrip()
if not text.startswith("{"):
return []
try:
data = json.loads(text)
except ValueError:
return []
if not isinstance(data, dict) or str(data.get("kind") or data.get("tool") or "") != CALIBER_QUERY:
return []
out: list[dict[str, Any]] = []
for item in data.get("queries") or []:
if not isinstance(item, Mapping) or not item.get("artifact"):
continue
out.append({
"artifact": str(item["artifact"]),
# 数据源的源名:和数据源查询的条目写在同一个位置(证据面板、遮罩、数据目录按它认源)
"source": item.get("data_source"),
# 这条查询在这份口径里的出处(workflow + 版本 + 取数节点):数据源条目没有这一项
"origin": dict(item["source"]) if isinstance(item.get("source"), Mapping) else None,
"columns": [str(c) for c in item.get("columns") or []],
"rows": int(item.get("row_count") or 0), "truncated": bool(item.get("truncated")),
"query_id": item.get("query_id"), "execution_id": item.get("execution_id"),
"attempt": item.get("attempt"),
**({"call_id": item["call_id"]} if item.get("call_id") else {}),
# 复算要的材料:这次参数输入的哈希、源模板哈希、承载方式与绑定指纹(值本身与 SQL 正文都不在这里)
**({"parameters": dict(item["parameters"])} if isinstance(item.get("parameters"), Mapping) else {}),
**({"tables": [str(t) for t in item["tables"]]} if item.get("tables") else {}),
**({"schema_artifact": item["schema_artifact"]} if item.get("schema_artifact") else {}),
})
return out
__all__ = ["BUDGET_OPTION", "BUDGET_REFUSED", "CALIBER_QUERY", "CARD_MISSING", "DEFAULT_MAX_QUERIES",
"DEFAULT_PURPOSE", "DEFAULT_TIMEOUT_S", "EMPTY", "EVIDENCE", "EXECUTION", "HOST_PURPOSE", "INCOMPATIBLE",
"INPUTS_REFUSED", "MANIFEST_REQUIRED", "MANIFEST_UNREADABLE", "METRIC_MASKED", "METRIC_MISSING",
"METRIC_NO_VALUE", "NOT_AUTHORIZED", "NOT_FOUND", "NOT_PUBLISHED", "NO_PARENT", "NO_RUN",
"OUTPUT_UNSUPPORTED", "PARAMETERS_REFUSED", "PARENT_RUN_KEY", "PHASES", "PREPARED_INPUT", "PREPARED_NODE",
"PREPARED_OUTPUT", "QUERY_MISSING", "REBOUND", "REFUSAL_CODES", "SELECTION_REFUSED", "UNKNOWN_INPUT",
"VERSION_MISSING", "VERSION_MOVED", "CaliberQueryArgs", "Measurement", "Outcome", "QueryCoreRefused",
"caliber_query", "exact", "execute", "execute_formal", "execute_prepared", "origin_kind", "prepared_graph",
"query_entries", "typed"]