138 lines
7.3 KiB
Python
138 lines
7.3 KiB
Python
"""会商第二步席位「技能识别」强化(弱模型友好)。
|
||
|
||
痛点:席位是一次自定义 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}"
|