mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 15:12:00 +00:00
03c18c4d78
一、文案(前端约 1600 处,后端传到界面的约 880 处) - 术语统一:工作流层面用「运行」(继续运行、重新运行、试运行),节点和工具层面用「执行」,不再用「跑」; 总时长 / 执行时长 / 等待审批;服务端;助手;Agent;token;工作流;固定版本;模型接入;系统提示;批准 / 驳回;受管级别 - 口语改书面:取不到、没取回来、读不懂、连不上、多半、照样、兜底、揉成这类说法改成「无法 / 未能 / 失败 / 请」;去掉拟人写法 - 不再露出实现细节:界面上不写 checkpoint、同一拍、哈希(凭证处除外),不写改版历史(「以前…」「老数据」); 报错、校验、门禁、自动修复里裸写的配置键名和枚举值换成界面上的中文标签,由新的 backend/app/engine/labels.py 统一提供; 发布弹窗里结论句的选项不再是 off / require_citation - 同一件事只留一种说法:生成阶段文案、出具档位说明、画布锁定提示各合成一份 - 去掉界面上的工业场景示例(MES、ERP、「张工」「王工」)和真实产品名,换成通用示例 二、说错的提示(都有测试证明) - 数据库测连接失败时的专用提示以前从不出现(按报错文字全等比较,永远比不上):改为按错误类型判断,不再拼出重复的前缀 - 「设置 → 供应商」改指「设置 → 模型接入」;「在设置里建数据源」改指「数据」页 - 缺少 API Key、模型接入已停用的报错不再夹英文,写明去哪里填 - 两处原因说错(「这种引用在后续版本支持」「这一版还不支持」)改成真实原因 - 节点说明里不准确的说法改正:代码节点在 microVM 或系统沙箱中执行;报告撰写、口径卡的说明不再说过头 - 导出的证据 CSV 里两列同名,第三列改为「片段编号」 三、前后端对齐 - 前端按后端原文识别日志、报错的地方,新旧两种写法都认得(历史运行里存的是旧文字),逐条补了新旧原文的核对 - 后端交回模型的原指令放进单独的 for_model 字段,前端在解析响应时去掉,不会显示到界面上 四、检查 - 检查脚本里按原文匹配的断言改成新文案;改完会变成永远成立的反向断言逐条修正,并做了变异验证 - 新增 check-copy(静态扫描前端界面字符串和后端上界面的文案):再出现「跑」「后端」「多半」「令牌」、 界面上的 Copilot、裸写的配置键名、半角标点等就报错;确需保留的写进 scripts/copy-allow.json 并注明理由
447 lines
23 KiB
Python
447 lines
23 KiB
Python
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 里做完,口径卡只做标量运算。列的顺序就是口径卡里 rows[0][n] 的 n
|
||
_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")}),
|
||
# 查询工具交回来的是 JSON 文本 {columns, rows},解析成对象口径卡才取得到里面的数
|
||
_n("parse", "transform", "解析查询结果", mode="json", template="{{ nodes.fetch }}",
|
||
assign_to="q"),
|
||
_n("caliber", "metrics", "周报口径卡", caliber="周报口径", caliber_version="v1",
|
||
metrics=[
|
||
{"id": "gmv", "name": "销售额", "unit": "元", "decimals": 2, "format": "thousands",
|
||
"expression": "vars.q.rows[0][0]"},
|
||
{"id": "orders", "name": "订单数", "unit": "单", "format": "thousands",
|
||
"expression": "vars.q.rows[0][2]"},
|
||
# 分母为 0 时记为空值(缺输入),交给契约按 expected 降档,而不是整张卡失败
|
||
{"id": "wow", "name": "环比增幅", "unit": "%", "decimals": 1, "format": "plain",
|
||
"expression": "round((vars.q.rows[0][0] - vars.q.rows[0][1]) / vars.q.rows[0][1] * 100, 1)"
|
||
" if vars.q.rows[0][1] else None"},
|
||
{"id": "aov", "name": "客单价", "unit": "元", "decimals": 2, "format": "thousands",
|
||
"expression": "round(vars.q.rows[0][0] / vars.q.rows[0][2], 2) if vars.q.rows[0][2] else None"},
|
||
{"id": "refund_rate", "name": "退款率", "decimals": 4, "format": "percent_of_ratio",
|
||
"expression": "vars.q.rows[0][3] / vars.q.rows[0][2] if vars.q.rows[0][2] else None"},
|
||
]),
|
||
# 受管出具要求报告撰写节点写明最严的三项(发布门禁 G3),模板照着写,拿来就能发布成受管
|
||
_n("write", "report", "报告撰写", metrics_from=["caliber"],
|
||
numbers="strict", on_violation="fail", claims="require_citation",
|
||
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", "parse"), _e("parse", "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"),
|
||
],
|
||
},
|
||
]
|
||
|
||
|
||
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)
|