mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:41:57 +00:00
46a2988dc1
- 重构问数据执行架构为「快速直查(fast)」与「深度分析(deep)」双模式,内置语义预命中(Grounding)注入指标卡片、标准实体编码与表目录血缘 - 快速直查免工作流生成单节点执行,新增零额外模型调用的数字证据归一与封存(direct_evidence),打通可点击数字溯源与 /verify 校验 - 深度分析与助手编排接入跨源本体穿透(自动升级重跑与一键沿本体排查关联数据源),优化数据源范围智能收敛与往返缓存 - 补齐端到端回归测试矩阵与中英文全量文档说明
190 lines
15 KiB
Python
190 lines
15 KiB
Python
"""助手的数据上下文:system 里只给概览,要看什么由模型自己 look(look 文档第二部分,总方案第五部分)。
|
||
|
||
**以前。** 每一轮开始前服务端按库的大小定怎么给:小库塞详细摘要,大库先用模型挑表、挑不出就退回全部表名。这一步
|
||
完全不看这一轮要做什么——用户说一句「你好」,助手也先花几秒挑表,再带着上万字的表结构去生成;挑表模型照提示说
|
||
「不需要」,反而被记成失败、退回列出全部表名(look 文档 §1.3、§1.4)。
|
||
|
||
**现在。** system 里每个库一行概览(app/look/tables.py),工具清单不再列表名。模型要看表时输出一行 look,生成过程
|
||
(api/copilot_gen.py)读到就停下,由查看服务(app/look/service.py)一次给齐:点名直取;只给需求时候选不超过 12 张
|
||
全给,否则挑选模型按需求挑;补两跳以内的中间表;全部字段、数据目录、连接条件;遮罩列只给列名和类型。闲聊只调一次
|
||
模型,数据类请求一次查看到位。
|
||
|
||
**各入口看什么**(总方案 §5.3):画布、问数据看表和能力(问数据只列只读能力);发布前修复、升级证据看表,由系统按要修
|
||
的 SQL 代为查看(look 文档 D5);应用润色看工作流版本和能力(在 copilot.py 里组装)。问数据另看已发布口径(next-round 02
|
||
做法 4),两档按身份分(K14):搭建者及以上用 use_caliber 选用一份口径做骨架(它的取数 SQL 由服务端拷进图里),「什么时候
|
||
该看」换成 LOOK_WHEN_ANSWER_PROMPT,输出协议多 USE_CALIBER_PROMPT 一段;**普通使用者**只列它有授权的那些版本(look/calibers
|
||
按 actor 收窄),不给 use_caliber,给的是「在 agent 里调口径查询工具 caliber_query」的写法,「什么时候该看」换成
|
||
LOOK_WHEN_QUERY_PROMPT。运行时写 SQL 的 Agent 不看口径(拍板 L2:保持独立解读,「等价已证明」才有意义)。
|
||
|
||
这里提供:look_service(按入口组装查看服务,开流之前调:读目录、读能力登记要用会话)、section(system 里数据源那一节)、
|
||
system_look(系统代为查看)、写 SQL 的硬性要求和「什么时候该看」的判断标准。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import Mapping
|
||
from typing import Any
|
||
|
||
from app.look.base import LookResult
|
||
from app.look.capabilities import CapabilityKind
|
||
from app.look.pick import PICK_MAX_TOKENS, Picker # noqa: F401 - copilot.py 按这个额度取挑选模型的规格
|
||
from app.look.service import LookService
|
||
from app.look.tables import TableKind, graph_tables
|
||
from app.providers.factory import ModelSpec
|
||
|
||
# ==========================================================================
|
||
# 写给模型的文字(不上界面)
|
||
# ==========================================================================
|
||
|
||
SQL_RULES_PROMPT = (
|
||
"\n\n写 SQL 的硬性要求:\n"
|
||
# 这三条都是真实踩过的:Oracle 上漏 schema 前缀直接 ORA-00942,
|
||
# 写惯 MySQL 的模型会顺手写 LIMIT,而一条没有行数限制的查询能拖垮生产库
|
||
"- 表名照抄查看结果里的全名(含 schema 前缀),漏掉前缀在 Oracle 上会直接报 ORA-00942\n"
|
||
"- 认准方言:Oracle 用 FETCH FIRST n ROWS ONLY,不是 LIMIT\n"
|
||
"- 一次只写一条语句;分号拼接会被拒\n"
|
||
# 真实踩过:列名是照着业务说法猜的,跑到查库那一步才「no such column」
|
||
"- 列名只照查看结果里列出来的写;没看过的表,动笔前先 look,别猜字段名。搭完自查会拿 SQL 里的列名去对结构,"
|
||
"对不上会打回来让你改\n"
|
||
"- 比率、增幅、占比这类派生计算进口径卡(metrics 节点);查询结果里原样的数,报告撰写节点(report)"
|
||
"可以直接用 [[v:Q1.r0.列名]] 引用。别让 llm 节点直接对数字做算术\n"
|
||
# 真实运行里报告表格的表头全是英文缩写的原始列名:[[table:]] 原样用查询结果的列名
|
||
"- 结果列用中文别名(AS 月份、AS 曝光量),报告里的表格表头、引用直接可读。按上面标的方言加标识符引号:"
|
||
"MySQL / MariaDB 用反引号(AS `月份`),PostgreSQL、Oracle、SQLite 用双引号(AS \"月份\"),不要用单引号"
|
||
"(那是字符串);只给最外层 SELECT 的结果列起别名,WHERE、JOIN、GROUP BY 里照旧写原列名\n"
|
||
)
|
||
|
||
#: 什么时候该看(look 文档 D4):画布、问数据共用
|
||
LOOK_WHEN_PROMPT = """\
|
||
什么时候该看:
|
||
· 闲聊、解释、改提示词、改流程结构、改不碰 SQL 的节点:不看
|
||
· 问数据:查库交给 agent 节点,它运行时会自己查表结构。只有从概览看不出该用哪个库、用户点了表名、用户说了口径时
|
||
才看一次,把看到的表名写进 agent 的提示词,运行时少走弯路
|
||
· 直接写 SQL 的取数节点、修改已有的 SQL:动笔前看,一次把要用的表都点上
|
||
· 用户说了一条数据事实:先看那张表的现有目录,再提目录修改
|
||
· 要看就先看,看完再规划,不要先搭节点再回头看;这次对话里已经看过的表直接点名看,不要只给需求"""
|
||
|
||
#: 问数据里口径这一档怎么用(api/copilot 按身份给):
|
||
#: look = 只看(列有授权的口径与参数,不选用、不调工具);compose = 搭建者及以上,look 后用 use_caliber 把口径带进图;
|
||
#: query = 普通使用者,look 后让 agent 调口径查询工具(caliber_query)取数——那条路不把口径的 SQL 拷进使用者的图里
|
||
CALIBER_LOOK = "look"
|
||
CALIBER_COMPOSE = "compose"
|
||
CALIBER_QUERY = "query"
|
||
|
||
#: 问数据里(能选用已发布口径时)的「什么时候该看」:问数据那一条按口径账本改写(next-round 02 做法 4 ④),别的照旧
|
||
LOOK_WHEN_ANSWER_PROMPT = LOOK_WHEN_PROMPT.replace(
|
||
""" · 问数据:查库交给 agent 节点,它运行时会自己查表结构。只有从概览看不出该用哪个库、用户点了表名、用户说了口径时
|
||
才看一次,把看到的表名写进 agent 的提示词,运行时少走弯路""",
|
||
""" · 问数据:先看已发布口径(kind 写 caliber)。问法和口径的定义完全对得上时用 use_caliber 选用它;口径只覆盖问题的
|
||
一部分时 use_caliber 选用那一部分,其余交给 agent 补;问法多了口径没有的条件(比如「含退款」「只看新客」)时不要套
|
||
口径,交给 agent 去查,系统会自动拿它的 SQL 和口径对账。查库交给 agent 时它运行时会自己查表结构:只有从概览看不出
|
||
该用哪个库、用户点了表名时才看表,把看到的表名写进 agent 的提示词""")
|
||
|
||
#: 问数据里**普通使用者**那一档的「什么时候该看」(K14):先看有授权的口径,对得上就让 agent 调口径查询工具取数——
|
||
#: 这条路上没有 use_caliber(把口径的取数 SQL 拷进使用者的图是搭建者才做的事),数得来自一次固定口径执行
|
||
LOOK_WHEN_QUERY_PROMPT = LOOK_WHEN_PROMPT.replace(
|
||
""" · 问数据:查库交给 agent 节点,它运行时会自己查表结构。只有从概览看不出该用哪个库、用户点了表名、用户说了口径时
|
||
才看一次,把看到的表名写进 agent 的提示词,运行时少走弯路""",
|
||
""" · 问数据:先看已发布口径(kind 写 caliber)——列出来的都是**你有授权的**那些版本。问法和某张口径对得上时
|
||
**不要自己写取数 SQL**:搭一个 agent 节点,在它的 prompt 里让它照查看结果的「取数写法」调用口径查询工具算出那几个
|
||
指标(口径身份、指标 id、inputs 照抄;相对时间先取当前日期再折算成开始/结束),交回的指标值写进报告。口径只覆盖问题
|
||
的一部分时,其余交给 agent 查底表补;问题里有多出口径没有的条件(含退款、只看新客这类)时不要套口径,交给 agent 去查。
|
||
查库交给 agent 时它运行时会自己查表结构:只有从概览看不出该用哪个库、用户点了表名时才看表,把看到的表名写进提示词""")
|
||
|
||
#: 问数据里输出协议多的一段:选用口径(ledger/compose.py)。只在这一轮能看已发布口径时接在协议后面。
|
||
#: 两个 id 写清楚(真实验收里模型第一次常把 card 写成自己起的 c1,被拒之后才改对):card 是那一版工作流图里口径卡的节点 id,
|
||
#: 照抄查看结果;id 是这次选用自己起的名字,生成的口径卡用它当节点 id
|
||
USE_CALIBER_PROMPT = """- 选用已发布口径(只在问数据里、look 过 caliber 之后):
|
||
{"op":"use_caliber","id":"c1","workflow_id":"…","version":3,"card":"card","metrics":["指标id"],"inputs":{"字段":"值"}}
|
||
照抄查看结果里给的那一行写法:workflow_id、version、card 原样照抄,只改 id、metrics、inputs。两个 id 别弄混:
|
||
· card 填那一版工作流图里口径卡的节点 id(查看结果的写法里已经写好,例如 "card"),指明选哪一张卡,不是你起的名字;
|
||
· id 是你给这次选用起的名字(简短英文小写,例如 c1),系统生成的口径卡就用它当节点 id,取数节点是 c1_<原 id>。
|
||
系统把入口连到取数、取数连到口径卡;你**不要**自己写这几个节点,也不要改它们。接下来只需把口径卡连到报告
|
||
(add_edge c1 → report,这里的 c1 是你起的 id),报告里用 [[m:指标id]] 引用。inputs 按口径的入口字段填(日期、门店这类
|
||
问题里给出的条件),填不对会被拒收、原因会交回给你改;口径对不上问法时不要选用"""
|
||
|
||
#: 系统代为查看时写进记录的需求(界面上显示)
|
||
SYSTEM_LOOK_FIX_NEED = "要修的工作流里,查库节点的 SQL 用到的表"
|
||
SYSTEM_LOOK_UPGRADE_NEED = "要升级的工作流里,查库节点的 SQL 用到的表"
|
||
|
||
|
||
async def look_service(sources: list[Any], *, spec: ModelSpec | None, graph: Mapping[str, Any] | None = None,
|
||
kinds: tuple[str, ...] = ("table", "capability"), answer: bool = False,
|
||
catalog_view: Any = None, trail: Mapping[str, Any] | None = None,
|
||
session: Any = None, actor: Any = None, caliber: str = CALIBER_LOOK,
|
||
ontology: bool = True) -> LookService:
|
||
"""按入口组装这一轮的查看服务。读目录、读能力登记都用各自的短会话,开流前后都能调。
|
||
|
||
spec 是助手的模型规格:挑选模型第一次要用时才按它拿(关思考、额度按 look/pick.PICK_MAX_TOKENS)。graph 是现有工作流,概览里列出它查过的表。
|
||
answer:问数据(意图为回答),能力只列只读的。catalog_view:表详情里给哪一份数据目录(data/catalog_view.py),不给是当前目录。
|
||
trail:按需加入探查范围时的留痕(哪一轮对话:{conversation_id};谁取这次请求的身份)。
|
||
kinds 里有 caliber 时用 session(调用方开流之前的会话)读已发布口径:compose 那一档另判能不能组合(look/calibers
|
||
的 compose),query 那一档只给按口径查询工具取数的写法(普通使用者,K14)。`actor` 是这次是谁:交给 CaliberKind.load
|
||
按资产授权收窄(普通使用者只列有授权的那些版本;不给就取当前请求的身份)。
|
||
"""
|
||
from app.look.calibers import CaliberKind
|
||
|
||
picker = Picker(spec)
|
||
made: list[Any] = []
|
||
if "table" in kinds:
|
||
made.append(await TableKind.load(sources, graph=graph, picker=picker, catalog_view=catalog_view, trail=trail,
|
||
ontology=ontology))
|
||
if "capability" in kinds:
|
||
made.append(await CapabilityKind.load(picker=picker, read_only=answer))
|
||
if "caliber" in kinds and session is not None:
|
||
made.append(await CaliberKind.load(session, picker=picker, compose=caliber == CALIBER_COMPOSE, actor=actor,
|
||
query=caliber == CALIBER_QUERY, ontology=ontology))
|
||
return LookService(made)
|
||
|
||
|
||
def composing(looks: LookService) -> bool:
|
||
"""这一轮能不能选用已发布口径(问数据里读好了能组合的口径卡,搭建者及以上那一档)。"""
|
||
kind = looks.kinds.get("caliber")
|
||
return (kind is not None and getattr(kind, "options", None) is not None
|
||
and not getattr(kind, "query", False))
|
||
|
||
|
||
def look_when(looks: LookService) -> str:
|
||
"""「什么时候该看」:问数据里能选用已发布口径的用一份,普通使用者(不能 use_caliber、要让它调口径查询工具)用另一份。"""
|
||
kind = looks.kinds.get("caliber")
|
||
if kind is not None and getattr(kind, "query", False):
|
||
return LOOK_WHEN_QUERY_PROMPT
|
||
return LOOK_WHEN_ANSWER_PROMPT if composing(looks) else LOOK_WHEN_PROMPT
|
||
|
||
|
||
def composer(looks: LookService) -> Any | None:
|
||
"""GraphApplier 用的组合器(ledger/compose.Composer);这一轮不能选用口径时为 None。"""
|
||
if not composing(looks):
|
||
return None
|
||
from app.ledger.compose import Composer
|
||
|
||
return Composer(list(looks.kinds["caliber"].options))
|
||
|
||
|
||
async def section(looks: LookService) -> str:
|
||
"""system 里数据源那一节:各类对象的概览,有数据源时再加写 SQL 的硬性要求。什么都没有时是空串,不占提示词。"""
|
||
overview = await looks.overview()
|
||
if not overview:
|
||
return ""
|
||
tables = looks.kinds.get("table")
|
||
return "\n\n" + overview + (SQL_RULES_PROMPT if tables is not None and tables.plans else "")
|
||
|
||
|
||
async def system_look(looks: LookService, graph: Mapping[str, Any] | None, sources: list[Any], *,
|
||
need: str) -> LookResult | None:
|
||
"""系统代为查看(look 文档 D5):图里调用工具节点的 SQL 用到的表,点名查看,不调挑选模型。没有就是 None。
|
||
|
||
记录、文字和模型主动查看的完全一样,只是 by=system、不占模型的次数;模型仍然可以再看。
|
||
"""
|
||
targets = [{"kind": "table", "source": source, "name": table}
|
||
for source, tables in graph_tables(graph, sources).items() for table in tables]
|
||
if not targets or "table" not in looks.kinds:
|
||
return None
|
||
return await looks.look({"op": "look", "need": need, "targets": targets}, by="system")
|
||
|
||
|
||
__all__ = [
|
||
"CALIBER_COMPOSE", "CALIBER_LOOK", "CALIBER_QUERY", "LOOK_WHEN_ANSWER_PROMPT", "LOOK_WHEN_PROMPT",
|
||
"LOOK_WHEN_QUERY_PROMPT", "SQL_RULES_PROMPT", "SYSTEM_LOOK_FIX_NEED", "SYSTEM_LOOK_UPGRADE_NEED",
|
||
"USE_CALIBER_PROMPT", "composer", "composing", "graph_tables", "look_service", "look_when", "section", "system_look",
|
||
]
|