mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 05:31:59 +00:00
2f90f0eb3a
- QueryCore 固定执行(K09):按已发布的不可变口径跑一次,交回类型化标量、发布/manifest 引用与逐查询证据 - 依赖闭包(K08):按选中的指标算出要拷的取数节点、要的入口字段,接进 derive / compose / metrics - SQL 模板参数完整性(K01):试跑、组合问数、派生图、运行时取数共用一套语义,缺参数在渲染前就拒 - 受限字段变更服务(K03)与口径领域边界(K01):改动打在整图深拷贝上,身份与证据归属不可改 - 版本化受控模板绑定(K04):业务口径按显式绑定落到 SQL,对不上就报 unsupported / needs_update - 验证记录(K05)与最小审阅:样例匹配不等于正式批准,审批人只从服务端身份取 - 发布依据清单(K07):发布时按不可变图物化内容寻址清单,正式用途按它判 - 增量重导入(K11)与 B/L/R 三方合并、受限自然语言修改提案(K12):只产候选,落图仍走既有写路径 - 接口与前端:口径草稿页接上字段变更、绑定状态、验证与重导入,新增 caliber-drafts / studio 检查脚本 验证:前端 tsc -b 通过。
85 lines
5.3 KiB
Python
85 lines
5.3 KiB
Python
"""问数据的运行怎么发起:开登录后 user 角色的那几条规则,HTTP 发起(api/runs.start_run)和服务端跑的一轮(chat/job.py)共用。
|
|
|
|
以前只有 start_run 一处(浏览器发 POST /api/runs)。一轮问数据搬到服务端之后,发起运行的是这一轮的后台任务,规则不能另抄一份——
|
|
使用者的问数据运行只许白名单里的节点和工具、记忆固定为自己的范围、查询结果在工具边界遮、数据源一律只读,这几条一处说清。
|
|
请求体能不能带记忆范围、知识库,能不能发正式运行,这些是 HTTP 请求本身的事,仍在 start_run 里判。
|
|
|
|
**口径查询工具是服务端补上的(K14)。** 使用者的图里不用自己画一个「按口径取数」:准备发起时在这里替它绑——有授权的
|
|
口径至少有一份时,把 `caliber_query`(只读、按资产授权收窄)加进这张图里各个 Agent 的 `tools`;一份也没有就原样不动,
|
|
模型不会拿到一个一调就被拒的工具。模型因此在工具清单里看得到它(chat_policy 的白名单里有它、copilot 的工具清单按白名单
|
|
列),能自己从 look 给出的**有授权的**口径里挑一张调用(look/calibers 只列这些)。绑上不等于放开数据:真正执行时
|
|
QueryCore 统一按资产授权核(querycore 的 `_require_caliber`,没有授权当场 403 `querycore_not_authorized`)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from typing import Any
|
|
|
|
|
|
class ChatRunRefused(Exception):
|
|
"""这张图使用者在问数据里不能运行:str(e) 就是给人看的原因(HTTP 发起时是 403 的 detail)。"""
|
|
|
|
|
|
async def user_chat_run(session: Any, graph: Any, actor: str | None) -> tuple[str, dict[str, Any], dict[str, Any]]:
|
|
"""开登录后 user 角色发起问数据运行:交回 (记忆范围, 运行选项, 这次要运行的图)。图里有白名单之外的节点、工具抛 ChatRunRefused。
|
|
|
|
记忆固定为这位使用者自己的范围、知识库取设置里的默认值(记忆、知识库的内容不经遮罩,见 auth/chat_policy 开头);查询结果
|
|
在工具边界就遮(事件、回答里只有遮过的值——它读得到自己这次运行的事件,api/deps.py 的 _run_scope);数据源查询一律只读,
|
|
写语句不交给审批。交回的图是 `bind_caliber_tool` 补过口径查询工具的那一份(调用方拿它发起,见模块说明)。`session` 只用来
|
|
读这个人有授权的口径(apps/access.usable_calibers),发起本身仍由调用方负责。"""
|
|
from app.auth.chat_policy import actor_name, chat_graph_problem, chat_memory_scope
|
|
from app.tools.datasource import MASK_RESULTS_OPTION, READ_ONLY_OPTION
|
|
|
|
if problem := chat_graph_problem(graph):
|
|
raise ChatRunRefused(problem)
|
|
# 主体归一成名字:记进记忆范围、判「是不是普通使用者」都按同一个名字算(调用方给的可能是 Principal)
|
|
name = actor_name(actor)
|
|
bound = await bind_caliber_tool(session, graph, name)
|
|
return chat_memory_scope(name), {MASK_RESULTS_OPTION: True, READ_ONLY_OPTION: True}, bound
|
|
|
|
|
|
async def bind_caliber_tool(session: Any, graph: Any, actor: str | None) -> dict[str, Any]:
|
|
"""普通使用者的图里补上口径查询工具,交回这次要运行的图(搭建者及以上、没有可用口径、图不成形都原样交回)。
|
|
|
|
- 只补 **Agent 节点的 tools**:调用工具节点一次只调它写死的那个工具,补不进去(要按口径取数就把 tool 写成 caliber_query);
|
|
- 只在**这份授权确实有口径可用**时补(apps/access.usable_calibers):一份也没有就原样交回,免得模型拿到一个一调就被拒
|
|
的工具;builder / admin / 关登录不补(auth/chat_policy.restricted_actor)——它们本来就能自己在画布上绑;
|
|
- 改的是**副本**(deepcopy):调用方的请求体、这一轮的图都不动。"""
|
|
from app.auth.chat_policy import restricted_actor
|
|
from app.engine.launch import node_setting
|
|
from app.engine.schema import GraphSpec
|
|
from app.ledger.querycore import CALIBER_QUERY
|
|
|
|
if not isinstance(graph, dict) or not graph.get("nodes"):
|
|
return graph
|
|
try:
|
|
spec = GraphSpec.model_validate(graph)
|
|
except ValueError:
|
|
return graph # 图不成形:交给 launch_run 照常报校验问题
|
|
if not any(str(node.type) == "agent" for node in spec.nodes):
|
|
return graph
|
|
if not await restricted_actor(session, actor):
|
|
return graph
|
|
from app.apps.access import usable_calibers
|
|
|
|
if not await usable_calibers(session, actor):
|
|
return graph
|
|
nodes = copy.deepcopy(graph["nodes"])
|
|
for node in nodes:
|
|
if not isinstance(node, dict) or str(node.get("type")) != "agent":
|
|
continue
|
|
here = node.get("data")
|
|
if not isinstance(here, dict):
|
|
continue
|
|
config = here.setdefault("config", {})
|
|
if not isinstance(config, dict):
|
|
continue
|
|
tools = [str(t) for t in (node_setting(spec, spec.node_map()[str(node.get("id"))], "tools") or [])]
|
|
if CALIBER_QUERY in tools:
|
|
continue
|
|
config["tools"] = [*tools, CALIBER_QUERY]
|
|
return {**graph, "nodes": nodes}
|
|
|
|
|
|
__all__ = ["ChatRunRefused", "bind_caliber_tool", "user_chat_run"]
|