"""会商第二步席位「技能识别」强化(弱模型友好)。 痛点:席位是一次自定义 agent run,其配置技能(agent ``config.yaml → skills``)只通过 **系统提示**里的 ```` 块注入(列出 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}"