Files
AgentLab/backend/app/seed.py
T
ThearchyHelios 8d9a712eae 业务本体与标准实体:跨源键值对齐、对账共变算子、健康证据链与实体助手
- 新增标准业务实体(CanonicalEntity)模型、确定性跨源编码翻译、后台候选实体扫描与文档驱动实体智能助手
- 新增指标本体图谱(factor / reconciles_with / upstream_driver)、DAG 防环校验以及 ontology_neighbors / source_health 内置工具
- 新增跨源对账(analysis_reconcile)与滞后共变(analysis_cochange)分析算子,打通 [[h:H1]] 数据源健康证据链、报告导出与本体消融评测
- 数据目录新增「标准业务实体」视图与候选审阅工作台,口径页集成指标本体拓扑面板与端到端浏览器检查
2026-11-12 12:57:53 +08:00

576 lines
34 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.
from __future__ import annotations
import logging
import os
from typing import Any
from sqlalchemy import func, select
from app.core.crypto import encrypt
from app.db.base import SessionLocal
from app.db.models import Provider, Skill, Workflow, WorkflowVersion
from app.engine.schema import GraphSpec
from app.providers import catalog
logger = logging.getLogger(__name__)
def _n(node_id: str, node_type: str, label: str, **config: Any) -> dict[str, Any]:
return {
"id": node_id,
"type": node_type,
"position": {"x": 0, "y": 0},
"data": {"label": label, "config": config},
}
def _e(source: str, target: str, handle: str | None = None) -> dict[str, Any]:
edge: dict[str, Any] = {"source": source, "target": target}
if handle:
edge["sourceHandle"] = handle
return edge
# --------------------------------------------------------------------------
# 内置模板:打开就有得玩,也是各种节点用法的活文档
# --------------------------------------------------------------------------
TEMPLATES: list[dict[str, Any]] = [
{
"name": "① 最小问答",
"description": "用一个「模型调用」节点完成从输入到成果的完整流程,适合作为起点。",
"tags": ["入门"],
"nodes": [
_n("start", "input", "提问", fields=[{"name": "question", "required": True,
"description": "你想问什么"}]),
_n("answer", "llm", "回答", system="你是一个耐心、准确的助手。",
prompt="{{ input.question }}", assign_to="answer"),
_n("done", "output", "成果", fields=[{"name": "回答", "value": "{{ vars.answer }}"}]),
],
"edges": [_e("start", "answer"), _e("answer", "done")],
},
{
"name": "② 研究助手(Agent + 工具)",
"description": "Agent 自主决定搜索和抓取哪些网页,再汇总成带出处的简报。",
"tags": ["Agent", "工具"],
"nodes": [
_n("start", "input", "研究主题", fields=[{"name": "topic", "required": True}]),
_n("research", "agent", "调研 Agent",
system="你是严谨的研究员。先搜索再抓取原文,只写有依据的内容。",
prompt="围绕这个主题做调研:{{ input.topic }}",
tools=["web_search", "web_fetch", "current_time"],
max_steps=8, approval="never", assign_to="findings"),
_n("brief", "llm", "写成简报",
system="你是一位把复杂信息讲清楚的编辑。",
prompt="把下面的调研结果整理成中文简报,分「结论 / 关键发现 / 待确认」三段,"
"每条结论后面标注来源链接:\n\n{{ vars.findings }}",
assign_to="brief"),
_n("done", "output", "简报", fields=[
{"name": "简报", "value": "{{ vars.brief }}"},
{"name": "调研过程", "value": "{{ nodes.research.tool_calls | json }}"}]),
],
"edges": [_e("start", "research"), _e("research", "brief"), _e("brief", "done")],
},
{
"name": "③ 带人工审批的发布流",
"description": "起草 → 人工审批 → 批准即发布、驳回则改写。演示运行中断与断点恢复。",
"tags": ["人工审批", "分支"],
"nodes": [
_n("start", "input", "主题", fields=[{"name": "topic", "required": True}]),
_n("draft", "llm", "起草", system="你是品牌文案。",
prompt="为「{{ input.topic }}」写一条 100 字以内的对外公告。",
assign_to="draft"),
_n("review", "human", "人工审批", mode="approve",
title="是否批准发布这条公告?",
message="草稿:\n\n{{ vars.draft }}"),
_n("publish", "transform", "标记发布", mode="template",
template="✅ 已批准发布\n\n{{ vars.draft }}", assign_to="final"),
_n("rewrite", "llm", "按意见改写",
prompt="下面这条公告被驳回了,理由是「{{ nodes.review.note }}」。请据此改写:\n\n{{ vars.draft }}",
assign_to="final"),
_n("done", "output", "成果", fields=[
{"name": "结果", "value": "{{ vars.final }}"},
{"name": "是否通过", "value": "{{ nodes.review.approved }}"}]),
],
"edges": [
_e("start", "draft"), _e("draft", "review"),
_e("review", "publish", "approved"), _e("review", "rewrite", "rejected"),
_e("publish", "done"), _e("rewrite", "done"),
],
},
{
"name": "④ 知识库问答(RAG)",
"description": "先检索再回答,检索不到时明确告知。使用前请在知识库页上传文档。",
"tags": ["RAG", "知识库"],
"nodes": [
_n("start", "input", "提问", fields=[{"name": "question", "required": True},
{"name": "collection", "default": "default"}]),
_n("search", "retrieve", "检索知识库", query="{{ input.question }}",
collection="{{ input.collection }}", limit=5, alpha=0.5, assign_to="context"),
_n("gate", "branch", "是否检索到内容", mode="expression",
cases=[{"key": "hit", "condition": "nodes.search.count > 0", "label": "有命中"}]),
_n("answer", "llm", "基于资料回答",
system="只根据提供的资料回答,每句话后面用 [编号] 标注依据。资料不足就直接说不确定。",
prompt="资料:\n{{ vars.context }}\n\n问题:{{ input.question }}",
assign_to="answer"),
_n("miss", "transform", "没有资料", mode="template",
template="知识库里没有找到与「{{ input.question }}」相关的内容,请先上传相关文档。",
assign_to="answer"),
_n("done", "output", "答案", fields=[
{"name": "答案", "value": "{{ vars.answer }}"},
{"name": "引用片段", "value": "{{ nodes.search.hits | json }}"}]),
],
"edges": [
_e("start", "search"), _e("search", "gate"),
_e("gate", "answer", "hit"), _e("gate", "miss", "default"),
_e("answer", "done"), _e("miss", "done"),
],
},
{
"name": "⑤ 代码数据处理(沙箱 + 校验)",
"description": "模型编写 Python 代码并在沙箱中执行,输出经 JSON Schema 校验,不合格时自动重试。",
"tags": ["沙箱", "结构化"],
"nodes": [
_n("start", "input", "分析需求", fields=[
{"name": "task", "required": True, "default": "统计 1 到 100 里所有质数的个数与总和"}]),
_n("write", "llm", "写代码",
system="你是数据工程师。只输出可直接运行的 Python 代码,不要解释、不要 markdown 围栏。"
"结果必须用 print(json.dumps(...)) 输出一个 JSON 对象。",
prompt="任务:{{ input.task }}", assign_to="code"),
_n("run", "code", "沙箱执行", language="python", code="{{ vars.code }}",
timeout=30, network=False, fail_fast=False, assign_to="result"),
_n("check", "validate", "校验结果", source="{{ nodes.run.stdout }}",
max_retries=2, repair_with_llm=True, fail_fast=False,
schema={"type": "object", "minProperties": 1}),
_n("done", "output", "成果", fields=[
{"name": "结果", "value": "{{ nodes.check.data | json }}"},
{"name": "代码", "value": "{{ vars.code }}"},
{"name": "校验通过", "value": "{{ nodes.check.valid }}"}]),
],
"edges": [_e("start", "write"), _e("write", "run"), _e("run", "check"), _e("check", "done")],
},
{
"name": "⑥ 多 Agent 协作",
"description": "调度者按进展分派任务给研究员、批评家和写手,直到目标达成。",
"tags": ["多 Agent"],
"nodes": [
_n("start", "input", "目标", fields=[{"name": "goal", "required": True}]),
_n("team", "supervisor", "协作团队", goal="{{ input.goal }}", max_rounds=6,
agents=[
{"name": "researcher", "description": "查资料、找事实",
"system": "你是研究员,负责收集事实和数据,给出来源。",
"tools": ["web_search", "web_fetch"], "max_steps": 4},
{"name": "critic", "description": "挑毛病、找漏洞",
"system": "你是批评家。指出前面工作里的事实错误、逻辑漏洞和遗漏,要具体。",
"tools": [], "max_steps": 2},
{"name": "writer", "description": "整合成最终文稿",
"system": "你是写手,把团队成果整合成结构清晰的中文文稿。",
"tools": [], "max_steps": 2},
], assign_to="result"),
_n("done", "output", "成果", fields=[
{"name": "结果", "value": "{{ vars.result }}"},
{"name": "协作记录", "value": "{{ nodes.team.transcript | json }}"}]),
],
"edges": [_e("start", "team"), _e("team", "done")],
},
{
"name": "⑧ 周报(口径卡 + 三档出具)",
"description": "用口径卡计算指标、按出具契约三档出具的示例:取数固定,报告只能引用指标清单中的数字,"
"出具时逐一核对,无法核对的数字会被标出并降档出具。",
"tags": ["出具", "口径卡", "周报"],
"nodes": [
_n("start", "input", "周期", fields=[
{"name": "week", "default": "2026-W37", "description": "报告周期"}]),
# 取数节点:产出本身就是源数据(evidence_role=source),口径卡读它记得下出处
_n("fetch", "code", "取数(数据底座)", language="python", network=False,
assign_to="agg", evidence_role="source",
code=(
"# 固定层:取数与汇总。真实场景换成对数据库/API 的查询,\n"
"# 结果会作为快照落工件库,出具溯源指回这里。\n"
"import json\n"
"print(json.dumps({'orders': 1234, 'amount': 45678.5,\n"
" 'amount_prev': 42010.0, 'returns': 29}))"
)),
_n("caliber", "metrics", "周报口径卡",
caliber="周报口径", caliber_version="v1", assign_to="m",
metrics=[
{"id": "orders", "name": "订单数", "unit": "单",
"expression": "vars.agg.orders"},
{"id": "amount", "name": "销售额", "unit": "元",
"expression": "vars.agg.amount"},
{"id": "wow", "name": "环比增幅", "unit": "%",
"expression": "round((vars.agg.amount - vars.agg.amount_prev) / vars.agg.amount_prev * 100, 1)"},
{"id": "return_rate", "name": "退货率", "unit": "%",
"expression": "round(vars.agg.returns / vars.agg.orders * 100, 2)"},
]),
_n("narrate", "llm", "叙述(关节层)",
system="你是周报撰写人。只允许引用下面指标清单里出现的数值,"
"一个清单外的数字都不要写——包括字数统计、序号补充说明等元内容。"
"只输出周报正文本身,不要任何撰写说明。"
"你的自由度在于:挑哪些变化值得说、怎么归因。",
prompt="{{ nodes.caliber.text }}\n\n请为 {{ input.week }} 写一段简短的周报正文。",
assign_to="report"),
_n("done", "output", "出具",
fields=[
{"name": "周报", "value": "{{ vars.report }}"},
{"name": "指标清单", "value": "{{ nodes.caliber.text }}"}],
contract={
"metrics_from": ["caliber"],
"narrative": "{{ vars.report }}",
"required": ["amount", "orders"],
"expected": ["wow", "return_rate"],
"allow_numbers": ["37", "120"],
"strict": False,
}),
],
"edges": [
_e("start", "fetch"), _e("fetch", "caliber"),
_e("caliber", "narrate"), _e("narrate", "done"),
],
},
{
"name": "⑨ 可追溯周报",
"description": "报告中的每个数字都可追溯出处:「调用工具」节点查询数据,口径卡登记指标,报告撰写节点只写"
"引用标记,数字由系统填入,成果节点按引用逐段核对后三档出具。使用前请接入名为 shop 的数据源(含 orders "
"表的 week、amount、refunded 列),或上传同名表格,再按你的口径修改 SQL。报告撰写节点已按受管级别设置核对规则。",
"tags": ["出具", "口径卡", "周报", "证据"],
"nodes": [
_n("start", "input", "周期", fields=[
{"name": "week", "default": "2026-W37", "description": "报告周期(ISO 周)"},
{"name": "prev_week", "default": "2026-W36", "description": "对比的上一周期"}]),
# 聚合放在 SQL 里做完,口径卡只做标量运算
_n("fetch", "tool", "取数(查库)", tool="db_query__shop", args={"sql": (
"SELECT\n"
" SUM(CASE WHEN week = '{{ input.week }}' THEN amount ELSE 0 END) AS gmv,\n"
" SUM(CASE WHEN week = '{{ input.prev_week }}' THEN amount ELSE 0 END) AS gmv_prev,\n"
" SUM(CASE WHEN week = '{{ input.week }}' THEN 1 ELSE 0 END) AS order_cnt,\n"
" SUM(CASE WHEN week = '{{ input.week }}' AND refunded = 1 THEN 1 ELSE 0 END) AS refund_cnt\n"
"FROM orders")}),
# 口径卡用 cell(查询节点, 行, 列名) 直接取查询结果的一格:每个数点开都追到这次查询的那一格(查询快照),
# 列的顺序改了也取不错。不必先经数据整形解析
_n("caliber", "metrics", "周报口径卡", caliber="周报口径", caliber_version="v1",
metrics=[
{"id": "gmv", "name": "销售额", "unit": "元", "decimals": 2, "format": "thousands",
"expression": "cell(nodes.fetch, 0, 'gmv')"},
{"id": "orders", "name": "订单数", "unit": "单", "format": "thousands",
"expression": "cell(nodes.fetch, 0, 'order_cnt')"},
# 分母为 0 时记为空值(缺输入),交给契约按 expected 降档,而不是整张卡失败
{"id": "wow", "name": "环比增幅", "unit": "%", "decimals": 1, "format": "plain",
"expression": "round((cell(nodes.fetch, 0, 'gmv') - cell(nodes.fetch, 0, 'gmv_prev'))"
" / cell(nodes.fetch, 0, 'gmv_prev') * 100, 1)"
" if cell(nodes.fetch, 0, 'gmv_prev') else None"},
{"id": "aov", "name": "客单价", "unit": "元", "decimals": 2, "format": "thousands",
"expression": "round(cell(nodes.fetch, 0, 'gmv') / cell(nodes.fetch, 0, 'order_cnt'), 2)"
" if cell(nodes.fetch, 0, 'order_cnt') else None"},
{"id": "refund_rate", "name": "退款率", "decimals": 4, "format": "percent_of_ratio",
"expression": "cell(nodes.fetch, 0, 'refund_cnt') / cell(nodes.fetch, 0, 'order_cnt')"
" if cell(nodes.fetch, 0, 'order_cnt') else None"},
]),
# 受管出具要求报告撰写节点写明最严的几项(发布门禁 G3,含分析说法核对设为严格),模板照着写,拿来就能发布成受管
_n("write", "report", "报告撰写", metrics_from=["caliber"],
numbers="strict", on_violation="fail", claims="require_citation", analysis_claims="strict",
system="你是周报撰写人:结论先行,只写有数据支撑的判断。",
instructions="为 {{ input.week }} 写一份简短的周报:先用一两句话总结本周,"
"再分点说销售额、订单、客单价和退款的变化。"),
_n("done", "output", "出具",
fields=[{"name": "周报", "value": "{{ nodes.write.text }}"}],
contract={
"report_from": "write",
"metrics_from": ["caliber"],
"required": ["gmv", "orders"],
"expected": ["wow", "aov", "refund_rate"],
"strict": True,
}),
],
"edges": [
_e("start", "fetch"), _e("fetch", "caliber"),
_e("caliber", "write"), _e("write", "done"),
],
},
{
"name": "⑦ 批量处理(循环)",
"description": "对列表逐项处理后汇总,演示「循环」节点「循环体」和「结束」两个出口的连法。",
"tags": ["循环"],
"nodes": [
_n("start", "input", "条目列表", fields=[
{"name": "items", "required": True,
"default": ["降低获客成本", "提升续费率", "缩短上线周期"],
"description": "JSON 数组或每行一条"}]),
_n("each", "loop", "逐条循环", mode="foreach", items="{{ input.items }}",
item_var="item", max_iterations=10),
_n("handle", "llm", "处理单条",
prompt="针对「{{ vars.item }}」给出一条可执行的建议,不超过 40 字。",
assign_to="one"),
_n("collect", "transform", "累积结果", mode="expression",
expression="(vars.collected or '') + '· ' + str(vars.one) + '\\n'",
assign_to="collected"),
_n("done", "output", "汇总", fields=[
{"name": "逐条建议", "value": "{{ vars.collected }}"},
{"name": "处理条数", "value": "{{ nodes.each.iteration }}"}]),
],
"edges": [
_e("start", "each"),
_e("each", "handle", "body"), _e("handle", "collect"), _e("collect", "each"),
_e("each", "done", "done"),
],
},
{
# 流程模板(应用平台 §3.7):图里只写岗位名,不写具体的人——做成应用时在应用里把「财务」「总监」绑到人或组。
# 不依赖任何外部端点:发布、做成应用、会签、超时升级都能直接演示
"name": "⑩ 报销审批(会签)",
"description": "员工发起报销,财务岗位全体同意才通过,1 小时内没处理完升级给总监;任何一人驳回即退回。",
"tags": ["流程", "人工审批", "会签"],
"nodes": [
_n("start", "input", "发起报销", fields=[
{"name": "applicant", "required": True, "description": "报销人"},
{"name": "amount", "type": "number", "minimum": 0.01, "required": True, "description": "金额(元)"},
{"name": "reason", "required": True, "description": "事由"}]),
# 标题带上金额:同一个人的两笔报销,待办列表里一眼分得开
_n("review", "human", "财务会签", mode="approve",
title="报销审批:{{ input.applicant }} · {{ input.amount }} 元",
message="报销人:{{ input.applicant }}\n金额(元):{{ input.amount }}\n事由:{{ input.reason }}",
assignee={"kind": "slot", "slot": "finance"}, policy="all", due="PT1H",
on_timeout={"action": "escalate", "to_slot": "director"}),
_n("approved", "output", "已通过", fields=[
{"name": "结论", "value": "已通过"}, {"name": "会签备注", "value": "{{ nodes.review.note }}"}]),
_n("rejected", "output", "已退回", fields=[
{"name": "结论", "value": "已退回"}, {"name": "退回原因", "value": "{{ nodes.review.note }}"}]),
],
"edges": [_e("start", "review"), _e("review", "approved", "approved"), _e("review", "rejected", "rejected")],
},
{
# 分析能力一轮(docs/design/analysis-round/00-总方案.md §3.7):模板定骨架——分析卡固定做哪几项分析写在图里,判定由
# 系统按平台的方法和阈值算、系统写判定语,报告撰写节点只引用。照模板 ⑨ 的写法,拿来就能按受管级别发布
"name": "⑪ 异动分析(可追溯)",
"description": "周销售额的异动分析,每个判断都可追溯:两个「调用工具」节点取数(整体按周、按地区两周),分析卡固定做"
"两项分析——这一周的变化在不在常规波动范围内、变化主要来自哪个地区(拆开后各段之和和总数核对闭合),报告撰写节点只引用"
"判定语和数,判断词逐句核对。使用前请接入名为 shop 的数据源(含 orders 表的 order_date、region、amount 列),或上传"
"同名表格。SQL 按 SQLite 的写法,按周汇总时期次取每周第一天(周一)的日期;接别的库时换成那个库的写法。报告撰写节点已按"
"受管级别设置核对规则。",
"tags": ["分析", "出具", "周报", "证据"],
"nodes": [
_n("start", "input", "周期", fields=[
{"name": "week", "default": "2026-10-12", "description": "本周的第一天(周一)"},
{"name": "prev_week", "default": "2026-10-05", "description": "上周的第一天(周一)"}]),
# 期次在 SQL 里由日期列算出来(每周第一天的日期):分析卡据此认出每一期覆盖哪几天,本期过没过完核对得了。
# 倒序排:第 0 行是本周、第 1 行是上周,变化拆解按这两格核对闭合
_n("weekly", "tool", "整体按周(26 周)", tool="db_query__shop", args={"sql": (
"SELECT date(order_date, '-6 days', 'weekday 1') AS \"周\", SUM(amount) AS \"销售额\"\n"
"FROM orders\n"
"WHERE order_date >= date('{{ input.week }}', '-175 days')"
" AND order_date < date('{{ input.week }}', '+7 days')\n"
"GROUP BY date(order_date, '-6 days', 'weekday 1')\n"
"ORDER BY \"周\" DESC")}),
_n("by_region", "tool", "按地区两周", tool="db_query__shop", args={"sql": (
"SELECT region AS \"地区\", date(order_date, '-6 days', 'weekday 1') AS \"周\", SUM(amount) AS \"销售额\"\n"
"FROM orders\n"
"WHERE order_date >= '{{ input.prev_week }}' AND order_date < date('{{ input.week }}', '+7 days')\n"
"GROUP BY region, date(order_date, '-6 days', 'weekday 1')\n"
"ORDER BY \"地区\", \"周\"")}),
# 阈值不写在图里:取设置里的分析一组(发起时快照);要固定阈值时在这张卡上写「阈值覆盖」
_n("check", "analysis", "异动分析卡", items=[
{"id": "wow", "op": "baseline", "from": "weekly",
"params": {"period": "周", "measure": "销售额", "target": "{{ input.week }}", "grain": "week",
"label": "销售额"},
"hypothesis": "本周销售额的变化在不在常规波动范围内"},
{"id": "where", "op": "decompose", "mode": "additive", "from": "by_region",
"params": {"by": ["地区"], "period": "周", "base": "{{ input.prev_week }}", "target": "{{ input.week }}",
"measure": "销售额", "headline": {"base": "weekly.r1.销售额", "target": "weekly.r0.销售额"},
"label": "销售额"},
"hypothesis": "本周销售额的变化主要来自哪个地区"},
]),
# 受管出具要求报告撰写节点写明最严的几项(发布门禁 G3,含分析说法核对设为严格),模板照着写
_n("write", "report", "报告撰写", numbers="strict", on_violation="fail", claims="require_citation",
analysis_claims="strict",
system="你是经营分析撰写人:结论先行,判断照分析结论写,只写有数据支撑的内容。",
instructions="为 {{ input.week }} 开始的这一周写一份销售额异动分析:先用一句话说本周销售额和上周比变了多少,"
"再按分析卡的两项结论说这个变化在不在常规波动范围内、主要来自哪个地区,最后用一张表列出各地区的"
"变化。判断照分析结论写、直接引用判定语,没有分析结论的判断不下;有一项没有做成的,照实写原因。"),
# 图里没有口径卡:报告直接引用查询和分析结论的格,契约开「单元格引用」(受管级别要写明)
_n("done", "output", "出具",
fields=[{"name": "异动分析", "value": "{{ nodes.write.text }}"}],
contract={"report_from": "write", "cells": True, "strict": True}),
],
"edges": [
_e("start", "weekly"), _e("weekly", "by_region"), _e("by_region", "check"),
_e("check", "write"), _e("write", "done"),
],
},
{
"name": "⑫ 跨源指标异动与对账诊断",
"description": "先核查数据源健康与跨源对账口径,再做同源维度定位与已确认本体穿透,出具带引用与非因果约束的诊断报告。",
"tags": ["分析", "本体", "对账", "证据"],
"nodes": [
_n("start", "input", "诊断问题", fields=[
{"name": "question", "required": True,
"default": "星河智造昨日设备有效运转率为何异常异动?请先核查数据源健康与跨源对账,再定位重点产线。",
"description": "要排查的指标异常或跨源对账问题"},
{"name": "period", "default": "2026-10-21", "description": "目标统计周期"}]),
_n("investigate", "agent", "跨源本体排查 Agent",
system="你是跨源指标诊断研究员:严格遵循先核数据源健康与对账、再同源拆解切片、最后沿已确认本体穿透的三段式纪律;"
"一旦发现源表断更、断档或对账失配即停止业务下钻。",
prompt="围绕统计周期 {{ input.period }} 排查问题:{{ input.question }}\n"
"1. 先调用本体邻居查询查看目标口径已确认的构成因子、跨源对账镜像与上游驱动关系,并调用数据源健康核查检查"
"涉及的数据源是否存在断更、断档、行数骤降或新增未识别编码;若存在对账镜像,调用口径查询取数后做跨源对账。\n"
"2. 若数据源健康正常且主指标超出波动基线,先在同源做变化拆解锁定贡献最大的业务实体切片。\n"
"3. 将异常切片传入本体邻居查询获取对齐后的调用参数,调用口径查询拉取已确认邻居指标并检验伴随异动(最多穿透 2 跳)。",
tools=[
"caliber_query",
"ontology_neighbors",
"source_health",
"analysis_comparable",
"analysis_baseline",
"analysis_decompose",
"analysis_reconcile",
"analysis_cochange",
],
approval="dangerous",
assign_to="investigation_notes"),
_n("write", "report", "诊断报告撰写", numbers="strict", on_violation="fail", claims="require_citation",
analysis_claims="strict",
system="你是经营诊断撰写人:结论先行,判断严格引用分析结论与数据源健康判定语,统计伴随异动绝不写成因果定论。",
instructions="围绕统计周期 [[i:period]] 回答:{{ input.question }}(排查摘要:{{ vars.investigation_notes }})。"
"按「核查结论 / 数据源健康与跨源对账 / 切片定位与伴随异动」三段组织正文:数据源健康结论引用 [[h:H1]],"
"分析判定直接引用 [[a:A1]] 等判定语,口径与单元格按证据引用;伴随异动一律表述为统计同步异动,"
"没有明确规则依据时写成待核实的推测。"),
_n("done", "output", "出具",
fields=[{"name": "跨源诊断报告", "value": "{{ nodes.write.text }}"}],
contract={"report_from": "write", "cells": True, "strict": True}),
],
"edges": [
_e("start", "investigate"), _e("investigate", "write"), _e("write", "done"),
],
},
]
SKILLS: list[dict[str, Any]] = [
{
"name": "结构化分析",
"description": "把开放问题拆成结论先行的分析",
"instructions": (
"分析任何问题时遵循:\n"
"1. 先给结论,再给理由,不要铺垫\n"
"2. 区分「事实」「推断」「假设」,并明确标注\n"
"3. 给出至少一个反面视角\n"
"4. 指出当前信息不足以判断的部分,不要硬答"
),
"tags": ["通用"],
},
{
"name": "批判性审阅",
"description": "只指出问题,不复述优点",
"instructions": (
"审阅内容时:\n"
"1. 只指出问题,不要复述优点\n"
"2. 每条问题写清楚「问题是什么 / 为什么是问题 / 怎么改」\n"
"3. 按严重程度排序,最致命的放最前面\n"
"4. 没有问题就直说没有,不要为凑数硬挑"
),
"tags": ["审阅"],
},
{
"name": "中文写作",
"description": "简洁自然、避免翻译腔的中文",
"instructions": (
"写中文时:\n"
"1. 不用「进行」「作出」这类空动词,直接用实义动词\n"
"2. 少用被动句和长定语从句\n"
"3. 术语首次出现时用括号给出英文原文\n"
"4. 能用短句就不写长句"
),
"tags": ["写作"],
},
]
async def seed_defaults() -> None:
"""首次启动时灌入默认 provider、模板工作流和 Skill。
已经存在的不会被覆盖 —— 这个函数每次启动都会跑,必须是幂等的。
"""
async with SessionLocal() as session:
# --- provider ---
count = (await session.execute(select(func.count(Provider.id)))).scalar() or 0
if count == 0:
session.add(
Provider(
name="演示模型(无需 API Key)",
kind="mock",
models=catalog.MOCK_MODELS,
default_model="mock-fast",
enabled=True,
)
)
# 环境里已有 Anthropic 凭据就自动配好,省得再手填一遍
token = os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN")
if token:
base_url = os.environ.get("ANTHROPIC_BASE_URL")
is_gateway = bool(base_url) and not os.environ.get("ANTHROPIC_API_KEY")
session.add(
Provider(
name="Anthropic" + ("(网关)" if is_gateway else ""),
kind="anthropic",
base_url=base_url,
api_key=encrypt(token),
models=catalog.ANTHROPIC_MODELS,
default_model="claude-opus-5",
enabled=True,
# 中转网关通常认 Authorization: Bearer 而不是 x-api-key
extra={"auth_style": "bearer"} if is_gateway else {},
)
)
logger.info("已从环境变量自动创建 Anthropic provider")
if os.environ.get("OPENAI_API_KEY"):
session.add(
Provider(
name="OpenAI",
kind="openai",
api_key=encrypt(os.environ["OPENAI_API_KEY"]),
models=catalog.OPENAI_MODELS,
default_model="gpt-5.2",
enabled=True,
)
)
await session.commit()
# --- Skill ---
existing_skills = {
s for (s,) in await session.execute(select(Skill.name))
}
for skill in SKILLS:
if skill["name"] not in existing_skills:
session.add(Skill(**skill))
await session.commit()
# --- 模板工作流 ---
from app.engine.layout import auto_layout
existing = {n for (n,) in await session.execute(select(Workflow.name))}
added = 0
for template in TEMPLATES:
if template["name"] in existing:
continue
spec = auto_layout(
GraphSpec.model_validate(
{"nodes": template["nodes"], "edges": template["edges"]}
)
)
graph = spec.model_dump(mode="json")
workflow = Workflow(
name=template["name"],
description=template["description"],
graph=graph,
tags=template["tags"],
is_template=True,
)
session.add(workflow)
await session.flush()
session.add(
WorkflowVersion(workflow_id=workflow.id, version=1, graph=graph, note="内置模板")
)
added += 1
if added:
await session.commit()
logger.info("已导入 %d 个内置模板", added)