Files
yawan-workbench/server/app/seed.py
T

456 lines
28 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.
"""种子数据:26 内容标签 + 预置 Agent(含 rules 核心)+ 默认 Provider"""
from sqlmodel import Session, select
from . import db as _db_mod
from .db import engine as _default_engine
from .models import (ContentTag, Agent, ModelProvider, Segment, KnowledgeBase, Organization, AppSetting,
AgentSkill, AgentSkillBinding, AgentPromptTemplate)
from .catalog import sync_provider_catalog
from .config import DEFAULT_PROVIDERS
TAGS = [
# (name, category, definition, usage, is_core)
("开场", "结构", "扎心暖场/承诺钩子/开场降档", "案例/悬念30秒入题,第一件产品尽量前置", True),
("转场", "结构", "段间衔接(话题自然滑入,禁报幕)", "产品随动作进场,不宣告", False),
("收尾", "结构", "信念收尾:最后30秒纯信念不卖货", "三段式金句收", True),
("钩子", "结构", "收口钩子=导图次日真实框架", "技法名与导图一致,不写死待确认道具", True),
("预告", "结构", "中途预告/下一场承诺", "只留一个带日期的承诺", False),
("心法", "内容", "认知/心态/信念升级", "概念有画面、论证有对照;金句只点缀不作产品引入", True),
("痛点", "内容", "焦虑唤醒/扎心一刀/痛点直击", "直击不绕弯,不留情面", True),
("动机铺垫", "内容", "讲解前先讲为什么要听(1-2句动机+原理)", "每个知识点/技法前的为什么铺垫,压缩时不删", True),
("案例", "内容", "学员案例/姐妹故事", "一人分饰两角、具体到细节", True),
("见证", "内容", "产品见证/逆袭故事", "卖货时的真实感来源", False),
("冷知识", "内容", "身体冷知识/科学证据", "必须准确(前列腺位置/括约肌无天然润滑等)", False),
("金句", "内容", "金句收口/三段式金句", "扎心话术收口干脆", True),
("共鸣", "内容", "情感共鸣/温柔引导型低声区", "全稿低谷段,气息写、放慢、压低声", False),
("技法", "技法", "外显手法:手活/口活/体位/步骤骨架", "第一步…第二步…第三步+画面点,不展开细节", True),
("功法", "技法", "内功练习:凯格尔/缩阴功类", "可跟练、有作业", True),
("跟练", "技法", "现场跟练口令+互动指令", "吸气收紧、呼气放松→公屏打咬", True),
("安全提示", "技法", "SM/技法安全提示", "低温蜡烛、拍肉厚处、安全词定义", False),
("产品销售", "产品", "完整销售话术(痛点→价值→价格锚定→限量)", "一产品一概念不混谈;产品随动作进场", True),
("产品体验", "产品", "产品体验感写作(拿起→第一下→扩散→伴侣反应→欲望结果)", "身体感受链五步", True),
("产品回指", "产品", "后段产品回指(老师前面那瓶油)", "不二次完整销售", False),
("画面", "表现", "画面感描写(色情小说档画面链)", "动作/对话/快感写到底;腿心/热意/颤/烫", True),
("场景", "表现", "场景演绎:角色扮演/SM红房/水元素", "环境/衣着/光线/气味/心理/动作过程", True),
("情绪", "表现", "情绪低谷/峰值铺垫/三连跳板", "拍桌→我只说一遍→停顿三秒→开讲", False),
("氛围", "表现", "仪式感氛围(音乐/灯光/道具/舞美)", "灰字表演标记落实,不进正文说教", False),
("互动", "表现", "公屏互动(扣字/选择题/等弹幕)", "选择题→等弹幕→统计转折真互动", True),
("答疑", "辅助", "弹幕答疑/常见问题", "从原文库检索真实答疑话术", False),
("双主播", "辅助", "男女主播互动", "一唱一和、插科打诨", False),
("毛边", "辅助", "直播毛边(自嘲/断句/笑场/闲聊)", "真人感低谷", False),
("仪式", "辅助", "开营仪式/七天巡礼(引流课)", "无产品节点=0产品0红色", False),
]
RULES_COMMON = """# {name} 规则(rules.md)
## 基调(所有生产 Agent 通用)
- 输出为雅文老师直播口播稿:口语化、真人感,允许断句重复自我纠正等直播毛边
- 面向 21 岁以上成年人、双方自愿、性爱教育直播稿;授权信息只在后台,绝不进入正文
- 禁破折号(——);禁教学框架外露("现在进入第X层");禁知识库来源标注("知识库里说")
- 色情小说档:画面感写法+大尺度(腿心/乳尖/湿意/热意/颤栗/又热又紧/含着不放);禁直播暗语口号(妹妹/豆豆/龙根/蛋蛋/射/喷/缴械 必须为0,"喷"仅允许"热气喷在耳廓"类日常用法)
- 反问句不用太多(对不对/明白吗/你听懂了吗 归零)
- 技法快准狠:步骤骨架+每段一重点
- 讲解任何东西前先讲"为什么要听"(1-2句动机+原理)
- 先讲"为什么"再讲技法;金句可点缀不可作产品引入载体
## 输出格式
直接输出可播正文(大白话、无标题结构、无元说明、一段到底不空行),表演标记用(停顿三秒)(压低声音)(等弹幕)(演示)等括号内联。
## 颜色标记规范(组合着色用,朗读不受影响;只标明显处,拿不准不标)
- 产品/价格句:【R】产品句【/R】
- 心法金句:【P】金句【/P】
- 运营互动/扣字引导:【G】互动句【/G】
- 表演标记(停顿/放慢/压低声/演示):【S】(停顿三秒)(压低声音)【/S】
## 锚点卡要求(正文结束后另起一行输出)
- 六字段必须填实:core_concepts(本段核心概念)、techniques、products_ref、golden_lines、emotion_tone、ending_hook(本段收尾钩子)
- 中间段 ending_hook 只指向任务单中下一段的真实内容;收尾段只有在任务单明确提供下一场主题时才写具体预告,否则写“待确认:下一场主题”,不得编造。
"""
RULES = {
"coordinator": """# 主控 Agent 规则
## 职责
1. 解析用户输入的导图/框架/思维,逐节点转录(读不准的词必须列入待确认清单,绝不猜测)
2. 每个导图节点标注内容标签(心法/技法/产品/…)与时间范围,生成任务单 segments
3. 时间总和=直播时长;导图标签超时则压缩(过渡并入相邻块),压缩决策写进说明
4. 导图是内容+时间的权威大纲:只对时间不对内容会被否定;导图独有内容块必须补齐
5. 产品节点原样保留(产品放哪段由内容设计定)
6. 输出 JSON:{"segments":[{title,content_tag,time_range,mindmap_node,product_ref}], "pending_confirms":[...], "notes":[...]}
""",
"opening": RULES_COMMON.format(name="开场段 Agent") + """
## 开场段特有规则
- 开场降档:性感画面从开场撤下,换自嘲暖场("一米五八一百二十斤照样是尤物")+承诺钩子("今晚带走三样本事,不花一分钱")
- 快:案例/悬念 30 秒内见主题
- 焦虑唤醒从三连砍成一段,紧跟希望桥(自曝蜕变/学员逆袭)
- 第一件产品尽量前置(开场30分钟内),如任务单该段含产品节点
""",
"xinfa": RULES_COMMON.format(name="心法段 Agent") + """
## 心法段特有规则
- 概念必须有画面,论证必须有对照("少年=凌晨跑十条街买豆浆"式)
- 金句只作点缀,不作产品引入载体("男人的硬是他最后的尊严"式心法叙事不能引入产品)
- 可含"为什么"铺垫(1-2句动机+原理),压缩时不删
- 情绪基调服从锚点卡(如上游是扎心开场,此处接住情绪落差)
""",
"jifa": RULES_COMMON.format(name="技法段 Agent") + """
## 技法段特有规则
- 技法讲解概述式:第一步…第二步…第三步 步骤骨架+画面点,不展开细节描写
- 每个卖点前必有痛点技法展示:可跟练的具体动作+互动指令(如"吸气收紧、呼气放松"→公屏打"咬")
- 每件技法给:怎么找(定位手法)→怎么按(按压手法)→跟练指令→作业
- 技法必须占主体、可跟练;心法/案例/画面压缩为铺垫(每段1-2句)
- 结尾钩子与锚点卡衔接(如技法→产品回指点)
""",
"product": RULES_COMMON.format(name="产品销售 Agent") + """
## 产品销售特有规则
- 一产品一概念不混谈:水润就讲水润(出水/湿润),绝不掺养护(pH/清洁/异味/乳酸杆菌);"养护"字样清零
- 产品体验感写作五步:拿起(触感温度气味)→用在自己身上的第一下→感觉扩散→伴侣反应→欲望结果
- 产品随动作进场((拿第一瓶)),绝不宣告式进场("老师要把第一个产品拉出来了"禁用)
- 价格以当轮指令为准(占位符标"待确认",不擅自定价)
- 销售三段式:需求→价值→价格锚定→限量;一场只允许一次完整销售话术,其余段只回指
- 从挂接知识库检索产品事实(功效/概念边界),引用来源后台记录
""",
"ending": RULES_COMMON.format(name="收尾段 Agent") + """
## 收尾段特有规则
- 最后30秒纯信念:不卖货不吓人("你学这些不是为了伺候他"),三段式金句收
- 结尾收在信念上,整场不被定性成广告
- 衔接钩子段(次日预告),技法名与导图次日真实框架一致
""",
"hook": RULES_COMMON.format(name="钩子段 Agent") + """
## 钩子段特有规则
- 收口钩子=导图次日真实框架,说出第二天具体解决什么问题
- 技法名与导图一致;拿不准的道具细节标灰字待确认,别写死
- 预告了必须兑现:只预告本场真的会教/下一天真的会讲的内容
""",
"qc_structure": """# 结构质检 Agent 规则
逐条检查并输出 JSON:{"checks":[{id,name,status:"PASS|FAIL",issues:[{location,desc,suggestion,severity}]}], "verdict":"PASS|FAIL"}
检查清单(五类):
1. 逻辑硬伤:同篇自相矛盾(生理机制前后打架/同产品两个价/同一技法一名两用/段序错乱/数据不自洽/语义反字/时序张力)
2. 笔误错字:漏字/同音字/人称混乱/产品名错字/表演标记缺左括号(扫所有(…)配对)
3. 结构残留:提词备注留在正文/上半场无下半场/他稿时间线
4. 时间轴:段标题时间标注与任务单一致
5. 质检报告必须逐条对证正文——报告断言与正文不符=FAIL(表面功夫零容忍)
""",
"qc_content": """# 内容质检 Agent 规则
输出同结构质检格式。检查清单:
1. 每个卖点前是否有痛点技法展示(可跟练动作+互动指令)?只有心法铺垫=FAIL
2. 一产品一概念:产品铺垫是否混入他品概念?"养护"字样是否残留?(水润≠养护,全稿"养护"清零)
3. 技法密度:全稿是否有可跟练技法清单?案例/心法段是否压过技法段?
4. 讲解前是否有"为什么"铺垫(1-2句)?
5. 删讲解不删画面:光讲道理=FAIL
6. 产品数量是否=任务单产品节点数?
""",
"qc_style": """# 风格尺度质检 Agent 规则
输出同结构质检格式。检查清单(词清单验证):
1. MUST=0 禁词:妹妹/豆豆/龙根/蛋蛋/射/喷/缴械/对不对/明白吗/你听懂了吗("喷"仅允许"热气喷在耳廓"类日常用法)
2. SHOULD≥3 小说感词:腿心/热意/颤/烫
3. 破折号(——)为0
4. 无教学框架外露、无知识库来源标注
5. 表演标记覆盖率:每段有演示/互动/画面描写或灰字表演标记
6. 反问句密度:对不对/明白吗归零
""",
"qc_cross": """# 跨稿质检 Agent 规则
输出同结构质检格式。检查清单:
1. day N-1 结尾预告的技法是否逐条兑现(读备稿库/框架库检索对账)
2. 价格/术语跨稿一致(以当轮指令为准,不擅自统一)
3. 产品名与产品知识库台账对齐
4. 回指句一致性:产品/技法退场后相关回指句是否残留
5. 钩子与导图次日真实框架一致
""",
"composer": """# 组合 Agent 规则
1. 按任务单时间轴拼装各段(段标题带时间标注,大块灰字分隔)
2. 一致性总检(5项):跨段矛盾/回指断裂/时间线打架/情绪曲线断裂/钩子兑现
3. 结构性不一致→打回对应段(绝不擅自改内容);微衔接→缝合编辑但必须标记
4. 生成说明:生成依据/时间压缩决策/跨稿对账/合规处理/占位符待确认
5. 输出六色标记 docx(黑=原文/红=产品价格/紫=金句/绿=互动/灰=表演标记/蓝=修改)
""",
"style_advisor": """# 风格顾问 Agent(服务型,只读供给)
职责:输出"本场风格校准卡"供生产 Agent 使用:
- 称呼暗语表(本场口径,以当轮指令为准)
- 开场模式建议(从挂接知识库检索历史范稿)
- 金句密度/句式特征/幽默模式要点
- 从挂接知识库检索相关案例/原文片段(带来源)
""",
"product_advisor": """# 产品顾问 Agent(服务型,只读供给)
职责:查询挂接的产品知识库,输出产品事实卡:
- 产品功效/概念边界(如:水润=水润概念,绝不混养护)
- 价格口径(以当轮指令为准,知识库价格仅参考)
- 体验感写作要点(五步链)
- 每条事实带来源块(库/文件/章节),供质检核对
""",
"digital_human_distill": """# 数字人蒸馏 Agent(内置)
## 职责
- 阅读用户上传的文档和音视频转写,提炼可证据追溯的人格档案
- 重点归纳文风、语气、叙事方式、情绪推进、痛点/痒点切入、销售逻辑和行动转场
- 只输出人格表达方式,不擅自添加事实、疗效承诺、人物经历或原文没有的禁词
- 支持补充素材蒸馏:保留已确认版本,合并新增文档后生成待测试的新版本
- 每次蒸馏都标明证据强度;原话必须逐字来自素材,证据不足明确写出
- 新版本必须经过人工试写/确认后才可生效,不能覆盖已发布版本
""",
}
AGENTS = [
# (name, role, tags, kb_ids(seed后填), rules_key, builtin)
("主控 Agent", "coordinator", [], [], "coordinator"),
("开场段 Agent", "production", ["开场"], ["风格档案库"], "opening"),
("心法段 Agent", "production", ["心法", "动机铺垫"], ["风格档案库", "训练营框架库"], "xinfa"),
("技法段 Agent", "production", ["技法", "功法", "跟练"], ["风格档案库", "直播原文库"], "jifa"),
("产品销售 Agent", "production", ["产品销售", "产品体验"], ["产品知识库", "风格档案库"], "product"),
("收尾段 Agent", "production", ["收尾"], ["风格档案库"], "ending"),
("钩子段 Agent", "production", ["钩子"], ["训练营框架库"], "hook"),
("结构质检", "qc", [], ["质检规则库"], "qc_structure"),
("内容质检", "qc", [], ["质检规则库", "产品知识库"], "qc_content"),
("风格尺度质检", "qc", [], ["质检规则库"], "qc_style"),
("跨稿质检", "qc", [], ["备稿库", "训练营框架库"], "qc_cross"),
("组合 Agent", "composer", [], [], "composer"),
("风格顾问", "service", [], ["风格档案库"], "style_advisor"),
("产品顾问", "service", [], ["产品知识库"], "product_advisor"),
("数字人蒸馏 Agent", "service", [], [], "digital_human_distill"),
]
# 内置技能只扩展执行方式,不改变任务、知识来源和质量门硬约束。
SKILLS = [
("大白话口播输出", "默认中文口播表达", "用大白话、短句和自然停顿写作;正文单段到底,不写标题、清单或分析说明。"),
("知识来源约束", "生产 Agent 的来源边界", "事实、产品信息、数据和方法只能来自本段注入的知识材料或任务原文;资料缺失时标待确认,不凭常识补写。"),
("锚点连续性", "段间衔接与钩子兑现", "先承接上游锚点卡的情绪、概念和收尾钩子,再进入本段;结尾只引向画布真实下游节点,不虚构预告。"),
("素材证据提炼", "数字人蒸馏的证据边界", "只从上传文档、转写和已确认对话中提炼表达特征;原话逐字引用,无法核实的内容标记证据不足,不把推测写成人格事实。"),
("增量蒸馏与版本", "数字人补充素材", "每次新增文档素材都合并到当前档案生成新候选版本,保留来源和版本号;候选必须人工试写确认后才生效,不覆盖已发布版本。"),
("痛点痒点销售结构", "数字人内容策略提炼", "分别提炼痛点切入、痒点/欲望切入、销售逻辑和行动转场,记录素材依据;只学习结构和表达方式,不凭空增加承诺。"),
]
PROMPT_TEMPLATES = [
{
"name": "内容段生产模板",
"desc": "适用于开场、心法、技法、产品、收尾和钩子等生产 Agent。",
"role_prompt": "你是{agent_name},负责{agent_role}类型的直播内容生产。",
"task_template": """请完成以下内容段任务:
标题:{title}
内容标签:{content_tag}
时间范围:{time_range}
导图节点原文:{mindmap_node}
产品节点:{product_ref}
""",
"constraints": [
"先说明为什么要讲,再给出本段方法、案例或互动。",
"正文用大白话和自然口语表达,避免分析过程和元说明。",
"知识材料只用于事实核对,资料未提供的信息标记待确认。",
"正文结束后输出完整锚点卡 JSON,供下游段落承接。",
],
"examples": [],
"output_schema": {
"type": "object",
"required": ["core_concepts", "techniques", "products_ref", "golden_lines", "emotion_tone", "ending_hook"],
"properties": {field: {"type": "string"} for field in (
"core_concepts", "techniques", "products_ref", "golden_lines", "emotion_tone", "ending_hook")},
},
"style": "大白话、短句、自然停顿、适合直接播读",
"builtin": True,
},
]
def default_canvas() -> dict:
"""默认画布(完整场):开场→心法→(技法1∥技法2)→产品→收尾→钩子"""
return {
"nodes": [
{"node_id": "n1", "agent_id": 2, "enabled": True}, # 开场段 Agent
{"node_id": "n2", "agent_id": 3, "enabled": True}, # 心法段 Agent
{"node_id": "n3", "agent_id": 4, "enabled": True}, # 技法段 Agent
{"node_id": "n4", "agent_id": 5, "enabled": True}, # 产品销售 Agent
{"node_id": "n5", "agent_id": 6, "enabled": True}, # 收尾段 Agent
{"node_id": "n6", "agent_id": 7, "enabled": True}, # 钩子段 Agent
],
"edges": [
{"from": "n1", "to": "n2"},
{"from": "n2", "to": "n3"},
{"from": "n3", "to": "n4"},
{"from": "n4", "to": "n5"},
{"from": "n5", "to": "n6"},
],
}
def topo_waves(nodes: list, edges: list) -> list[list[str]]:
"""依赖图拓扑分波次:返回 [[node_id,...], ...]"""
enabled = {n["node_id"] for n in nodes if n.get("enabled", True)}
deps: dict[str, set] = {nid: set() for nid in enabled}
for e in edges:
if e["from"] in deps and e["to"] in deps:
deps[e["to"]].add(e["from"])
waves, done = [], set()
while len(done) < len(enabled):
wave = [nid for nid, d in deps.items()
if nid not in done and d.issubset(done)]
if not wave:
break # 环:剩余节点独立成波
waves.append(wave)
done.update(wave)
return waves
def upstream_anchor_cards(session: Session, campaign_id: int, node_id: str,
edges: list) -> list[dict]:
"""画布上游节点的锚点卡(边指向当前节点的所有启用上游)"""
ups = {e["from"] for e in edges if e["to"] == node_id}
if not ups:
return []
cards = []
segs = session.exec(select(Segment).where(Segment.campaign_id == campaign_id)).all()
for up in ups:
best = None
for s in segs:
if (s.node_id == up and s.status in {"approved", "human_review"}
and s.anchor_card and (best is None or s.id > best.id)):
best = s
if best and best.anchor_card:
cards.append({"node_id": up, "title": best.title, **best.anchor_card})
return cards
def _current_engine():
"""动态取当前引擎:测试替换 db.engine 后 seed 也跟随,避免写错库。"""
return _db_mod.engine
def seed():
global engine
engine = _current_engine() # 测试可整体替换 db.engine,seed 跟随
with Session(engine) as s:
if not s.exec(select(Organization)).first():
s.add(Organization(name="默认工作区", slug="default", enabled=True))
s.commit()
from .models import Canvas
with Session(engine) as s:
# 画布
existing_canvases = s.exec(select(Canvas)).first()
seeded_marker = s.get(AppSetting, "seed.canvas_defaults_initialized")
# 默认画布只在首次初始化时写入。画布是用户可永久删除的资源,不能因为
# 表被删空或服务重启就悄悄重新出现。
if not existing_canvases and not seeded_marker:
s.add(Canvas(name="默认流水线(完整场)", nodes=default_canvas()["nodes"],
edges=default_canvas()["edges"]))
# 模板:无产品场 / 纯心法场
no_product = [n for n in default_canvas()["nodes"] if n["node_id"] != "n4"]
no_product_edges = [e for e in default_canvas()["edges"]
if e["from"] != "n4" and e["to"] != "n4"]
s.add(Canvas(name="【模板】无产品场", nodes=no_product, edges=no_product_edges))
pure_xinfa = [
{"node_id": "n1", "agent_id": 2, "enabled": True},
{"node_id": "n2", "agent_id": 3, "enabled": True},
{"node_id": "n3", "agent_id": 3, "enabled": True},
{"node_id": "n5", "agent_id": 6, "enabled": True},
]
s.add(Canvas(name="【模板】纯心法场", nodes=pure_xinfa,
edges=[{"from": "n1", "to": "n2"}, {"from": "n2", "to": "n3"},
{"from": "n3", "to": "n5"}]))
s.add(AppSetting(key="seed.canvas_defaults_initialized", value="1"))
s.commit()
print("画布: 默认流水线 + 【模板】无产品场 + 【模板】纯心法场")
elif existing_canvases and not seeded_marker:
# 兼容已经存在的旧生产库:只写初始化标记,不补写或覆盖任何画布。
s.add(AppSetting(key="seed.canvas_defaults_initialized", value="1"))
s.commit()
with Session(engine) as s:
# 标签
if not s.exec(select(ContentTag)).first():
for name, cat, d, u, core in TAGS:
s.add(ContentTag(name=name, category=cat, definition=d, usage=u, is_core=core))
s.commit()
print(f"标签: {len(TAGS)} 个")
# Provider
if not s.exec(select(ModelProvider)).first():
for p in DEFAULT_PROVIDERS:
s.add(ModelProvider(**{k: v for k, v in p.items() if k in
("name", "base_url", "api_key", "models", "default_model", "builtin", "note")}))
s.commit()
print(f"Provider: {len(DEFAULT_PROVIDERS)} 个")
# Provider 只保存连接信息;用户可选的模型统一同步到 ModelCatalog。
for provider in s.exec(select(ModelProvider)).all():
sync_provider_catalog(s, provider)
s.commit()
# 提示模板独立于 Agent 规则保存,旧库升级后自动补齐内置模板。
templates_by_name = {item.name: item for item in s.exec(select(AgentPromptTemplate)).all()}
for payload in PROMPT_TEMPLATES:
if payload["name"] not in templates_by_name:
template = AgentPromptTemplate(**payload)
s.add(template)
s.commit()
s.refresh(template)
templates_by_name[template.name] = template
default_template = templates_by_name.get("内容段生产模板")
# Agent
if not s.exec(select(Agent)).first():
kbs = {k.name: k.id for k in s.exec(select(KnowledgeBase)).all()}
for name, role, tags, kb_names, rules_key in AGENTS:
kb_ids = [kbs[n] for n in kb_names if n in kbs]
s.add(Agent(name=name, role=role, content_tags=tags, kb_ids=kb_ids,
prompt_template_id=default_template.id if default_template and role == "production" else None,
rules=RULES[rules_key], builtin=True,
model_name="deepseek-chat" if role in ("coordinator", "qc", "composer") else ""))
s.commit()
print(f"Agent: {len(AGENTS)} 个")
elif default_template:
changed = False
for agent in s.exec(select(Agent).where(Agent.role == "production")).all():
if agent.prompt_template_id is None:
agent.prompt_template_id = default_template.id
s.add(agent)
changed = True
if changed:
s.commit()
# 旧数据库可能已经有部分内置 Agent;逐项补齐后来加入的内置 Agent,
# 同时保留管理员对现有 Agent 的停用/规则修改。
existing_names = {item.name for item in s.exec(select(Agent)).all()}
kbs = {k.name: k.id for k in s.exec(select(KnowledgeBase)).all()}
# 旧数据库升级时补齐后来加入的内置 Agent(保留管理员对已有 Agent 的修改)。
missing_builtin = [item for item in AGENTS
if item[0] not in existing_names and
(item[1] == "qc" or item[0] == "数字人蒸馏 Agent")]
for name, role, tags, kb_names, rules_key in missing_builtin:
s.add(Agent(name=name, role=role, content_tags=tags,
kb_ids=[kbs[n] for n in kb_names if n in kbs],
rules=RULES[rules_key], builtin=True,
model_name="deepseek-chat"))
if missing_builtin:
s.commit()
print("补齐缺失内置 Agent: " + ", ".join(item[0] for item in missing_builtin))
# 技能库:无论 Agent 是否已存在,都补齐缺失的内置技能,避免旧数据库升级后技能为空。
skills_by_name = {skill.name: skill for skill in s.exec(select(AgentSkill)).all()}
for name, desc, instructions in SKILLS:
skill = skills_by_name.get(name)
if not skill:
skill = AgentSkill(name=name, desc=desc, instructions=instructions,
enabled=True, builtin=True)
s.add(skill)
s.commit()
s.refresh(skill)
skills_by_name[name] = skill
# 只给没有任何绑定的内置 Agent 加默认技能,不覆盖用户已有配置。
default_skill_ids = {name: skill.id for name, skill in skills_by_name.items()
if name in {item[0] for item in SKILLS}}
for agent in s.exec(select(Agent)).all():
if agent.role not in ("production", "composer") and agent.name != "数字人蒸馏 Agent":
continue
if s.exec(select(AgentSkillBinding).where(
AgentSkillBinding.agent_id == agent.id)).first():
continue
names = ["大白话口播输出", "知识来源约束"]
if agent.role == "composer":
names.append("锚点连续性")
if agent.name == "数字人蒸馏 Agent":
names = ["素材证据提炼", "增量蒸馏与版本", "痛点痒点销售结构"]
for name in names:
skill_id = default_skill_ids.get(name)
if skill_id:
s.add(AgentSkillBinding(agent_id=agent.id, skill_id=skill_id))
s.commit()
if __name__ == "__main__":
from .db import init_db
init_db()
seed()
print("seed 完成")