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

456 lines
28 KiB
Python
Raw Normal View History

"""种子数据: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 完成")