fix: 会商席位与总控角色隔离,编排能力只归总控
This commit is contained in:
1 parent
ff80cc110d
commit
3384f2e51c
10 files changed
+488
-125
No files matched your search
+103
-25
@@ -40,7 +40,74 @@ logger = logging.getLogger(__name__)
|
||||
WRITING_SETUP_MAX_ROUNDS = 8
|
||||
REPORT_STRUCTURE_SETUP_MAX_ROUNDS = 8
|
||||
|
||||
_ES_QUERY_ROUTING_OPEN_TAG = '<skill_routing skill="es_query">'
|
||||
_ES_QUERY_ROUTING_OPEN_TAG = '<skill_routing skill="es_query">'
|
||||
|
||||
_SKILLS_BLOCKED_BY_EXCLUDED_TOOL: dict[str, frozenset[str]] = {
|
||||
# 内置 tool 与历史 custom skill 目录存在两种拼写;禁用编排工具时两者都必须从系统提示和
|
||||
# runtime skill allowlist 中消失,防止误配的席位仍看到「加载编排技能」。
|
||||
"agent_orchestration": frozenset({"agent_orchestration", "agent-orchestration"}),
|
||||
}
|
||||
|
||||
_ROUNDTABLE_COORDINATOR_AGENT_ID = "roundtable-coordinator"
|
||||
|
||||
_NON_COORDINATOR_ORCHESTRATION_BOUNDARY = """
|
||||
<orchestration_capability_boundary>
|
||||
你不是多智能体圆桌总控,因此没有编排、派活或调度其它智能体的权限。
|
||||
不得加载、读取、检索或调用 agent_orchestration / agent-orchestration,
|
||||
不得声称准备使用编排技能;请直接完成当前问答或你的本职任务。
|
||||
</orchestration_capability_boundary>
|
||||
""".strip()
|
||||
|
||||
|
||||
def _enforce_coordinator_only_tool_exclusions(
|
||||
agent_id: str | None,
|
||||
excluded_tools: list[str] | None,
|
||||
) -> list[str] | None:
|
||||
"""Make orchestration unavailable to every agent except the fixed roundtable coordinator."""
|
||||
if agent_id == _ROUNDTABLE_COORDINATOR_AGENT_ID:
|
||||
return excluded_tools
|
||||
effective = list(excluded_tools or [])
|
||||
if "agent_orchestration" not in effective:
|
||||
effective.append("agent_orchestration")
|
||||
return effective
|
||||
|
||||
|
||||
def _append_orchestration_capability_boundary(system_prompt: str, agent_id: str | None) -> str:
|
||||
"""Reinforce the physical tool/skill gate in every non-coordinator system prompt."""
|
||||
if agent_id == _ROUNDTABLE_COORDINATOR_AGENT_ID:
|
||||
return system_prompt
|
||||
return f"{system_prompt}\n\n{_NON_COORDINATOR_ORCHESTRATION_BOUNDARY}"
|
||||
|
||||
|
||||
def _filter_available_skills_for_excluded_tools(
|
||||
available_skills: list[str] | None,
|
||||
excluded_tools: list[str] | None,
|
||||
) -> list[str] | None:
|
||||
"""让工具级禁用同步收紧 skill allowlist,必要时把「全部」展开为安全列表。"""
|
||||
if not excluded_tools:
|
||||
return available_skills
|
||||
excluded = {name.strip().lower() for name in excluded_tools if name}
|
||||
blocked = {
|
||||
skill_name
|
||||
for tool_name, skill_names in _SKILLS_BLOCKED_BY_EXCLUDED_TOOL.items()
|
||||
if tool_name in excluded
|
||||
for skill_name in skill_names
|
||||
}
|
||||
if not blocked:
|
||||
return available_skills
|
||||
if available_skills is None:
|
||||
# ``None`` 原本表示「全部启用技能可见」,无法表达「除了总控专属技能之外的全部」。
|
||||
# 此时显式展开存储中的启用技能;扫描失败则 fail closed,避免角色隔离失效。
|
||||
try:
|
||||
from deerflow.skills.storage import get_or_new_skill_storage
|
||||
|
||||
available_skills = [
|
||||
skill.name for skill in get_or_new_skill_storage().load_skills(enabled_only=True)
|
||||
]
|
||||
except Exception: # noqa: BLE001
|
||||
logger.warning("Failed to expand skills while applying excluded-tool boundary", exc_info=True)
|
||||
available_skills = []
|
||||
return [name for name in available_skills if name.strip().lower() not in blocked]
|
||||
|
||||
|
||||
def _log_es_query_routing_registration(
|
||||
@@ -535,8 +602,11 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
||||
|
||||
if not is_writing_mode_report_path(writing_artifact_path):
|
||||
writing_artifact_path = None
|
||||
agent_id = validate_agent_id(cfg.get("agent_id") or cfg.get("agent_name"))
|
||||
requested_excluded_tools = cfg.get("excluded_tools") or None
|
||||
agent_id = validate_agent_id(cfg.get("agent_id") or cfg.get("agent_name"))
|
||||
requested_excluded_tools = _enforce_coordinator_only_tool_exclusions(
|
||||
agent_id,
|
||||
cfg.get("excluded_tools") or None,
|
||||
)
|
||||
|
||||
try:
|
||||
agent_config = load_agent_config(agent_id) if not is_bootstrap and not is_writing_setup and not is_report_structure_setup else None
|
||||
@@ -585,10 +655,14 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
||||
|
||||
from deerflow.agents.deep_research.retrieval import merge_collector_skill_allowlist, runtime_extra_skills
|
||||
|
||||
runtime_available_skills = merge_collector_skill_allowlist(
|
||||
agent_id, runtime_available_skills, runtime_extra_skills(cfg)
|
||||
)
|
||||
prompt_available_skills = set(runtime_available_skills) if runtime_available_skills is not None else None
|
||||
runtime_available_skills = merge_collector_skill_allowlist(
|
||||
agent_id, runtime_available_skills, runtime_extra_skills(cfg)
|
||||
)
|
||||
runtime_available_skills = _filter_available_skills_for_excluded_tools(
|
||||
runtime_available_skills,
|
||||
requested_excluded_tools,
|
||||
)
|
||||
prompt_available_skills = set(runtime_available_skills) if runtime_available_skills is not None else None
|
||||
|
||||
config["metadata"].update(
|
||||
{
|
||||
@@ -669,13 +743,14 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
||||
# Special bootstrap agent with minimal prompt for initial custom agent creation flow
|
||||
return create_agent(
|
||||
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, force_disable_thinking=thinking_force_disabled, app_config=resolved_app_config),
|
||||
tools=get_available_tools(
|
||||
model_name=model_name,
|
||||
subagent_enabled=subagent_enabled,
|
||||
is_scheduled_run=cfg.get("is_scheduled_run", False),
|
||||
excluded_tools=requested_excluded_tools,
|
||||
app_config=resolved_app_config,
|
||||
)
|
||||
tools=get_available_tools(
|
||||
model_name=model_name,
|
||||
subagent_enabled=subagent_enabled,
|
||||
is_scheduled_run=cfg.get("is_scheduled_run", False),
|
||||
excluded_tools=requested_excluded_tools,
|
||||
allow_agent_orchestration=agent_id == _ROUNDTABLE_COORDINATOR_AGENT_ID,
|
||||
app_config=resolved_app_config,
|
||||
)
|
||||
+ [setup_agent],
|
||||
middleware=_build_middlewares(config, model_name=model_name, app_config=resolved_app_config),
|
||||
system_prompt=apply_prompt_template(
|
||||
@@ -698,11 +773,12 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
||||
default_tools = get_available_tools(
|
||||
model_name=model_name,
|
||||
groups=agent_config.tool_groups if agent_config else None,
|
||||
subagent_enabled=subagent_enabled,
|
||||
is_scheduled_run=cfg.get("is_scheduled_run", False),
|
||||
excluded_tools=requested_excluded_tools,
|
||||
app_config=resolved_app_config,
|
||||
)
|
||||
subagent_enabled=subagent_enabled,
|
||||
is_scheduled_run=cfg.get("is_scheduled_run", False),
|
||||
excluded_tools=requested_excluded_tools,
|
||||
allow_agent_orchestration=agent_id == _ROUNDTABLE_COORDINATOR_AGENT_ID,
|
||||
app_config=resolved_app_config,
|
||||
)
|
||||
if agent_id == FORCED_RESEARCH_AGENT_ID:
|
||||
default_tools = filter_forced_research_tools(default_tools)
|
||||
if is_notebook_mode:
|
||||
@@ -744,8 +820,8 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
||||
writing_mode=writing_mode,
|
||||
writing_artifact_path=writing_artifact_path,
|
||||
)
|
||||
else:
|
||||
system_prompt = apply_prompt_template(
|
||||
else:
|
||||
system_prompt = apply_prompt_template(
|
||||
subagent_enabled=subagent_enabled,
|
||||
max_concurrent_subagents=max_concurrent_subagents,
|
||||
agent_id=agent_id,
|
||||
@@ -758,10 +834,12 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
||||
rag_mode_enabled=rag_mode_enabled,
|
||||
is_notebook_mode=is_notebook_mode,
|
||||
notebook_source_ids=notebook_source_ids,
|
||||
is_scheduled_run=bool(cfg.get("is_scheduled_run", False)),
|
||||
)
|
||||
|
||||
_log_es_query_routing_registration(
|
||||
is_scheduled_run=bool(cfg.get("is_scheduled_run", False)),
|
||||
)
|
||||
|
||||
system_prompt = _append_orchestration_capability_boundary(system_prompt, agent_id)
|
||||
|
||||
_log_es_query_routing_registration(
|
||||
system_prompt=system_prompt,
|
||||
app_config=resolved_app_config,
|
||||
agent_id=agent_id,
|
||||
|
||||
Reference in new issue
Block a user