Files
AgentLab/backend/app/api/copilot_context.py
T
ThearchyHelios 1a6082f033 大库的对象索引、探查范围和后台探查(next-round 03 A1),schema_view 预筛与按需加入、剖析事实的后端(A2、遗留 e 的底座)
为什么:几千张表的库以前用不了——探查每个源最多取 200 个对象、按数据库列出的顺序截断,同步请求、整份替换、不排队;
落在窗口外的表在目录、look、剖析里被说成「表已不在」。

做了什么:
- db/models_sources.py:source_objects(对象索引一行一个对象:范围、原因、结构指纹与 struct_version、first_seen / last_seen /
  removed_at、reflected_at、按需加入的留痕)、introspect_jobs / introspect_tasks(后台探查任务与逐表状态)。
- data/source_objects.py:每种方言一条字典查询建对象索引(失败退回 Inspector,上限 5 万,截断时不判删除);探查范围规则
  (options.introspect_scope:通配、按 name_key、排除优先、显式列出 / 移出;有目录的表、已发布模板写死 SQL 用到的表 pinned;
  命中超上限拒绝并说出命中数);唯一的 rebuild_schema_cache 从范围内已反射的行物化 schema_cache(形状不变,另加 scope 摘要);
  三种「不在」分开说(removed / missing / out_of_scope / reflect_failed);按需加入(当场只读反射、落盘、留痕、进程内按源加锁)。
- data/introspect_jobs.py:探查改成后台任务(core/job_runtime,lane kind introspect,和同源起草、剖析串行;按批反射、逐表记状态、
  可停止、读完的照样物化;重启标中断不续跑)。
- api/datasources.py:POST /introspect 返回 202 和任务,dry_run 同步只刷新索引、返回命中数;/introspect-jobs 读任务、停止;
  /introspect-scope 读、预览、保存规则;/schema?table= 带剖析事实、说清三种「不在」;DataSourceOut 加 schema_scope、introspect_job。
- schema_view:search_tables(BM25 倒排,遮罩列只用列名,范围外只按名字和注释、权重低一档)、profile_lines(剖析事实一节,
  附「只供写查询参考」,遮罩按渲染那一刻重滤,上传表格快照不一致时不给)、absent_detail。
- look:候选超过 PREFILTER_LIMIT 时只给预筛结果 + 已用到的表 + 一跳关联;点名或挑中范围外的表按需加入(每次查看最多 4 张),
  look 记录加 added_to_scope、prefiltered;表详情带剖析事实。
- tools/datasource:db_schema__ 加 keyword、不传 table 时只列前 50 张;点名范围外的表按需加入(每次运行最多 8 张);查询结果
  照实列「未按目录检查的表」(unchecked_tables,不新增检查规则);建工具时读剖析事实。
- 剖析:_relation_ends 分清「范围外,可以加入」和「表结构里没有」;任务记上传表格的 snapshot_id;last_facts。表助手、/schema
  用同一段剖析事实。
- 目录接口:清单只列范围内,absent_reason 分开说;GET /catalog/search 查对象索引,POST /catalog/scope/add 加入范围。
- 第 1 波留下的一处:check_valid_filter 和 stored_valid_filter 拼有效记录条件时按 quote_name 强制加引号。
- 工具参数纠错:空着的可选参数不止一个时,只认拼写明显接近的那一个(db_schema 多了 keyword 以后 tabel 仍能纠成 table)。
- 测试:3,000 张表的合成库(对象索引、缺省范围超上限拒绝、规则圈 400 张后台读完、停止、重启标中断、和剖析排队、同步起草
  409)、删除与结构版本、Inspector 回退、截断不判删除、范围规则、pinned、方言金样;改了同步探查的老测试改为等任务做完。
2026-10-16 14:17:06 +08:00

111 lines
7.4 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.
"""助手的数据上下文: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 里组装)。
这里提供: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:动笔前看,一次把要用的表都点上
· 用户说了一条数据事实:先看那张表的现有目录,再提目录修改
· 要看就先看,看完再规划,不要先搭节点再回头看;这次对话里已经看过的表直接点名看,不要只给需求"""
#: 系统代为查看时写进记录的需求(界面上显示)
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) -> LookService:
"""按入口组装这一轮的查看服务。读目录、读能力登记都用各自的短会话,开流前后都能调。
spec 是助手的模型规格:挑选模型第一次要用时才按它拿(关思考、额度按 look/pick.PICK_MAX_TOKENS)。graph 是现有工作流,概览里列出它查过的表。
answer:问数据(意图为回答),能力只列只读的。catalog_view:表详情里给哪一份数据目录(data/catalog_view.py),不给是当前目录。
trail:按需加入探查范围时的留痕(哪一轮对话:{conversation_id};谁取这次请求的身份)。
"""
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))
if "capability" in kinds:
made.append(await CapabilityKind.load(picker=picker, read_only=answer))
return LookService(made)
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__ = [
"LOOK_WHEN_PROMPT", "SQL_RULES_PROMPT", "SYSTEM_LOOK_FIX_NEED", "SYSTEM_LOOK_UPGRADE_NEED", "graph_tables",
"look_service", "section", "system_look",
]