deerflow-code/offline-backend-20260512/backend/app/gateway/roundtable_seat_skills.py
2026-09-07 18:24:55 +08:00

138 lines
7.3 KiB
Python
Raw Permalink 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.

"""会商第二步席位「技能识别」强化(弱模型友好)。
痛点:席位是一次自定义 agent run,其配置技能(agent ``config.yaml → skills``)只通过
**系统提示**里的 ``<skill_system>`` 块注入(列出 name/description + SKILL.md 路径,靠模型
自己 ``read_file`` 并遵循)。强模型会照做;**弱模型(如 deepseek-chat)往往无视系统提示里
的技能块,从不 read_file SKILL.md**,于是配置的技能形同虚设——只凭模型自身知识作答。
解法:在**派给席位的本轮任务消息(human turn)**里,显式把该席位配置的每个技能(名称 +
描述 + SKILL.md 容器路径)列出来,并下达硬指令「先 read_file 对应 SKILL.md、按其说明用工具
去取信息 / 走流程,再产出交付」。把指令放进 user turn(而非仅系统提示)对弱模型的牵引力
强得多——这与本仓已有的弱模型补偿手法(``_SEAT_NO_ASK``、总控派活强化、AI 写作 SKILL.md
注入,见 ``project-ai-writing-skill-agent``)一脉相承。
**默认关闭、按需开启(2026-06-20 改造)**:不再全域生效。是否对某席位注入由两个开关
取 **OR** 决定——
1. **智能体级**:该席位 agent 的 ``config.yaml → seat_skill_directive``(智能体编辑页
可开关);
2. **业务链条级**:本次会商所选业务链条的同名开关,由调用方读出后以 ``chain_enabled``
传入(前端手动与后台挂起两条路径都按席位透传)。
任一为真即注入;两者都假 → 返回空串、原样使用任务文本(零副作用)。
两条席位执行路径共用本 helper:
- 前端手动:``multi_agent._special_run``(seat / seat_parallel 角色);
- 后台挂起:``roundtable_inprocess_gateway.InProcessRoundtableGateway.run_seat``。
``ROUNDTABLE_SEAT_SKILL_DIRECTIVE`` 仅作**总开关(kill-switch)**:默认放行,显式设为
0/false 时彻底关闭本功能(不论两个开关如何),用于线上一键熔断。它**不再** force-on。
"""
from __future__ import annotations
import logging
import os
logger = logging.getLogger(__name__)
_TRUTHY = frozenset({"1", "true", "yes", "on", "y"})
_FALSY = frozenset({"0", "false", "no", "off", "n"})
# 编排能力是圆桌总控的专属能力。两种拼写分别对应内置 tool 名和历史 custom skill
# 目录名;席位侧无论 agent 配置或业务链条开关如何,都不得把它们暴露进技能强化提示。
COORDINATOR_ONLY_SKILL_NAMES = frozenset({"agent_orchestration", "agent-orchestration"})
_SEAT_ROLE_BOUNDARY = (
"【圆桌席位角色边界(最高优先级)】你是负责完成具体任务的执行席位,不是总控智能体。"
"编排、派活和调度其它智能体的能力只允许总控智能体使用。"
"你不得加载、读取或调用 agent_orchestration / agent-orchestration,"
"不得向其它智能体派活,也不要声称准备使用编排技能;请直接完成上方交给你的具体任务并提交结果。"
)
def append_seat_role_boundary(task: str) -> str:
"""在席位本轮 human 消息末尾追加不可越权的角色边界。"""
return f"{task}\n\n---\n\n{_SEAT_ROLE_BOUNDARY}"
def _master_enabled() -> bool:
"""``ROUNDTABLE_SEAT_SKILL_DIRECTIVE`` 总开关:默认放行(kill-switch)。
空 / 未设 / 非法 = 放行;仅显式 0/false/no/off/n 时整体关闭。注意它**只熔断不强开**,
具体是否注入仍由 agent / chain 两个开关取 OR 决定。
"""
return (os.getenv("ROUNDTABLE_SEAT_SKILL_DIRECTIVE") or "").strip().lower() not in _FALSY
def build_seat_skill_directive(agent_id: str | None, *, chain_enabled: bool = False) -> str:
"""为某席位 agent 构建「必须先读 SKILL.md 再用技能」的本轮任务追加指令。
是否注入由 **agent 开关(config.yaml)OR chain 开关(``chain_enabled``)** 决定,
默认两者皆关 → 不注入。返回 ``""`` 表示无需追加(总开关熔断 / 两开关都关 / 无 agent_id /
该席位没有配置技能 / 解析失败),调用方原样使用任务文本,零副作用。
"""
if not _master_enabled() or not agent_id:
return ""
try:
from deerflow.config.agents_config import load_agent_config
cfg = load_agent_config(agent_id)
# agent 级开关 OR chain 级开关(默认关)。任一打开才继续,否则零副作用返回。
agent_enabled = bool(cfg and cfg.seat_skill_directive)
if not (agent_enabled or chain_enabled):
return ""
configured = [
name
for name in (cfg.skills or [])
if name and name.strip().lower() not in COORDINATOR_ONLY_SKILL_NAMES
] if cfg else []
if not configured:
return ""
from deerflow.skills.storage import get_or_new_skill_storage
try:
from deerflow.config import get_app_config
container_base_path = get_app_config().skills.container_path
except Exception: # noqa: BLE001 — 配置读不到时退回默认挂载路径
container_base_path = "/mnt/skills"
by_name = {s.name: s for s in get_or_new_skill_storage().load_skills(enabled_only=True)}
# 保留席位 config 里的配置顺序,剔除已删除 / 未启用的技能名。
wanted = [by_name[name] for name in configured if name in by_name]
if not wanted:
return ""
lines: list[str] = [
"【你已配置以下专属技能,必须优先使用——这是硬性要求,不要只凭自身知识作答】",
"完成本轮交付前,请先判断哪些技能与任务相关,**用 read_file 工具读取对应技能的 SKILL.md 全文**,"
"严格按其中的说明(检索方式 / 接口 / 流程 / 输出格式)去**获取真实信息或执行处理流程**"
"(其中可能要求你调用 bash / write_file 等工具),再据此产出交付:",
"",
]
for i, s in enumerate(wanted, 1):
desc = (s.description or "").strip().replace("\n", " ")
lines.append(f"{i}. 「{s.name}」{desc}".rstrip())
lines.append(f" - 技能说明文件(先 read_file 它):{s.get_container_file_path(container_base_path)}")
lines.append("")
lines.append(
"注意:只要某个技能与本轮任务相关,**务必先 read_file 它的 SKILL.md 并按其指引调用工具**,"
"不要跳过技能直接凭记忆回答;技能里写明的接口 / 命令要真正执行,不要假装调用。"
)
return "\n".join(lines)
except Exception: # noqa: BLE001 — 强化失败绝不能拖垮席位本身的执行
logger.debug("build seat skill directive failed for agent %s", agent_id, exc_info=True)
return ""
def append_seat_skill_directive(agent_id: str | None, task: str, *, chain_enabled: bool = False) -> str:
"""把技能识别指令追加到席位本轮任务文本后(无指令时原样返回)。
``chain_enabled``:本次会商所选业务链条是否打开「技能识别强化」。与 agent 级开关取 OR。
"""
directive = build_seat_skill_directive(agent_id, chain_enabled=chain_enabled)
if not directive:
return task
return f"{task}\n\n---\n\n{directive}"