feat(multi-agent): 在现有架构上直接实现多智能体模式实验功能
放弃完全隔离策略,改为在现有 MainTerminal/SubAgentManager/SubAgentTask 主链路 按对话级开关 metadata.multi_agent_mode=true 增加多智能体分支。 新增模块: - modules/multi_agent/__init__.py: 模块入口 - modules/multi_agent/role_store.py: 角色 Markdown Frontmatter 解析与归档 - modules/multi_agent/state.py: 多智能体会话状态机与消息格式化 - modules/multi_agent/prompts.py: 主智能体(Team Leader) + 子智能体提示词 - modules/multi_agent/tools.py: 9 个主智能体工具 + 4 个子智能体工具定义 - server/multi_agent.py: /multiagent/new 页面 + /api/multiagent/* 蓝图 现有代码改动: - modules/sub_agent/task.py: 扩展 multi_agent_mode/multi_agent_state/display_name 字段, 增加 ask_master/ask_other_agent/answer_other_agent/list_active_sub_agents 工具处理逻辑, 子智能体自然结束 assistant 输出即本轮结束(不调用 finish_task),上下文保留。 - modules/sub_agent/manager.py: create_sub_agent 增加 multi_agent_mode/role_id/display_name 参数, 增加 get_or_create_multi_agent_state/get_multi_agent_state/inject_message_to_sub_agent/_on_multi_agent_task_done 方法。 - core/main_terminal_parts/tools_definition/agent_tools.py: 多智能体模式下用 modules.multi_agent.tools 替换旧版工具集。 - core/main_terminal_parts/context/messages.py: 多智能体模式下追加 Team Leader 系统提示词。 - core/main_terminal_parts/tools_execution.py: create_sub_agent handler 增加多智能体分支,新增 send_message_to_sub_agent/ask_sub_agent/answer_sub_agent_question/create_custom_agent/list_agents/list_active_sub_agents handler。 - core/web_terminal.py: load_conversation 时检测 metadata.multi_agent_mode 设置 self.multi_agent_mode。 - server/app_legacy.py: 注册 multi_agent_bp 蓝图。 前端改动: - static/src/auth/LoginApp.vue: 登录页增加'多智能体模式(beta)'按钮 - static/src/app/methods/ui/route.ts: 识别 /multiagent/new 和 /multiagent/conv_xxx 路径,进入多智能体模式并创建带 metadata.multi_agent_mode=true 的对话 - static/src/app/state.ts: 增加 multiAgentMode 状态字段 数据: - ~/.astrion/astrion/host/mutiagents/agents/: 4 个预置角色 ui-operator / full-stack-engineer / code-reviewer / researcher - ~/.astrion/astrion/host/mutiagents/conversations/: 会话数据 验证:所有 Python 文件语法检查通过;冒烟测试 test.test_server_refactor_smoke 6 项全通过;前端构建通过(6.04s);模块导入与功能断言测试全部通过。
This commit is contained in:
parent
e8b857f3ae
commit
811974d6e7
@ -278,6 +278,16 @@ class MessagesMixin:
|
||||
"frozen_disabled_tools_prompt",
|
||||
lambda: self._format_disabled_tool_notice() or "",
|
||||
)
|
||||
|
||||
# 多智能体模式提示词:当会话 metadata.multi_agent_mode 为真时追加
|
||||
if getattr(self, "multi_agent_mode", False):
|
||||
try:
|
||||
from modules.multi_agent.prompts import build_multi_agent_master_prompt
|
||||
multi_agent_prompt = build_multi_agent_master_prompt(self.project_path, base="")
|
||||
if multi_agent_prompt:
|
||||
messages.append({"role": "system", "content": multi_agent_prompt})
|
||||
except Exception as exc:
|
||||
logger.warning(f"[messages] 注入多智能体 prompt 失败: {exc}")
|
||||
if disabled_notice:
|
||||
messages.append({
|
||||
"role": "system",
|
||||
|
||||
@ -91,6 +91,14 @@ DISABLE_LENGTH_CHECK = True
|
||||
|
||||
class ToolsDefinitionAgentToolsMixin:
|
||||
def _build_agent_tools(self) -> List[Dict]:
|
||||
# 多智能体模式下,主智能体不再使用旧版的 4 个子智能体工具
|
||||
# 而是用 modules/multi_agent/tools.build_master_tools_for_conversation() 返回的新集
|
||||
if getattr(self, "multi_agent_mode", False):
|
||||
try:
|
||||
from modules.multi_agent.tools import build_master_tools_for_conversation
|
||||
return build_master_tools_for_conversation()
|
||||
except Exception as exc:
|
||||
logger.warning(f"[tools] 加载多智能体工具失败,回退旧版: {exc}")
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
|
||||
@ -1728,66 +1728,245 @@ class MainTerminalToolsExecutionMixin:
|
||||
)
|
||||
|
||||
elif tool_name == "create_sub_agent":
|
||||
result = self.sub_agent_manager.create_sub_agent(
|
||||
agent_id=arguments.get("agent_id"),
|
||||
summary=arguments.get("summary", ""),
|
||||
task=arguments.get("task", ""),
|
||||
deliverables_dir=arguments.get("deliverables_dir", ""),
|
||||
run_in_background=arguments.get("run_in_background", False),
|
||||
timeout_seconds=arguments.get("timeout_seconds"),
|
||||
thinking_mode=arguments.get("thinking_mode"),
|
||||
conversation_id=self.context_manager.current_conversation_id
|
||||
)
|
||||
# 多智能体模式:create_sub_agent 走新签名,需要 role_id/display_name/multi_agent_mode
|
||||
if getattr(self, "multi_agent_mode", False):
|
||||
role_id = arguments.get("role_id")
|
||||
if not role_id:
|
||||
result = {"success": False, "error": "多智能体模式下 create_sub_agent 必须指定 role_id"}
|
||||
else:
|
||||
try:
|
||||
from modules.multi_agent.role_store import load_preset_role
|
||||
from modules.multi_agent.prompts import build_multi_agent_sub_agent_prompt
|
||||
role = load_preset_role(role_id)
|
||||
if not role:
|
||||
result = {"success": False, "error": f"角色不存在: {role_id}"}
|
||||
else:
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
multi_agent_state = self.sub_agent_manager.get_or_create_multi_agent_state(conv_id)
|
||||
# 分配 agent_id:如果未传入则自动递增
|
||||
agent_id = arguments.get("agent_id")
|
||||
if not agent_id:
|
||||
agent_id = multi_agent_state.next_agent_id_for_role(role_id)
|
||||
# 构造显示名
|
||||
display_name = role.display_name(int(agent_id))
|
||||
# 构造多智能体版系统提示词
|
||||
workspace_path = str(getattr(self, "project_path", ""))
|
||||
system_prompt = build_multi_agent_sub_agent_prompt(role.body_prompt, display_name, workspace_path)
|
||||
# 构造 task_message(作为 Team Leader 的任务发布)
|
||||
from modules.multi_agent.state import build_master_dispatch_text
|
||||
task_message = build_master_dispatch_text(arguments.get("task", ""))
|
||||
# 加载并覆盖作业:multi_agent_mode/sub_agent_manager.create_sub_agent 的作业路径
|
||||
summary_text = (arguments.get("summary") or f"{role.name}作业")[:80]
|
||||
deliverables_dir = arguments.get("deliverables_dir", f"sub_agent_results/agent_{agent_id}")
|
||||
thinking_mode = arguments.get("thinking_mode") or role.thinking_mode or "fast"
|
||||
# 走原行 发事件创建(避免后期重建提供重复工能重费,直接使用 multi_agent_mode=True 调用)
|
||||
result = self.sub_agent_manager.create_sub_agent(
|
||||
agent_id=int(agent_id),
|
||||
summary=summary_text,
|
||||
task=arguments.get("task", ""),
|
||||
deliverables_dir=deliverables_dir,
|
||||
run_in_background=bool(arguments.get("run_in_background", True)),
|
||||
timeout_seconds=arguments.get("timeout_seconds"),
|
||||
conversation_id=conv_id,
|
||||
model_key=role.model_key,
|
||||
thinking_mode=thinking_mode,
|
||||
multi_agent_mode=True,
|
||||
role_id=role_id,
|
||||
display_name=display_name,
|
||||
)
|
||||
# 在多智能体模式下,主进程 create_sub_agent 总是后台启动,
|
||||
# 主智能体不需要阻塞等待,而是通过子智能体输出转发拿进度。
|
||||
except Exception as exc:
|
||||
logger.exception("[multi_agent] create_sub_agent failed")
|
||||
result = {"success": False, "error": str(exc)}
|
||||
else:
|
||||
result = self.sub_agent_manager.create_sub_agent(
|
||||
agent_id=arguments.get("agent_id"),
|
||||
summary=arguments.get("summary", ""),
|
||||
task=arguments.get("task", ""),
|
||||
deliverables_dir=arguments.get("deliverables_dir", ""),
|
||||
run_in_background=arguments.get("run_in_background", False),
|
||||
timeout_seconds=arguments.get("timeout_seconds"),
|
||||
thinking_mode=arguments.get("thinking_mode"),
|
||||
conversation_id=self.context_manager.current_conversation_id
|
||||
)
|
||||
|
||||
# 如果不是后台运行,阻塞等待完成
|
||||
if not arguments.get("run_in_background", False) and result.get("success"):
|
||||
task_id = result.get("task_id")
|
||||
wait_result = self.sub_agent_manager.wait_for_completion(
|
||||
task_id=task_id,
|
||||
timeout_seconds=arguments.get("timeout_seconds")
|
||||
)
|
||||
# 合并结果:保留创建元数据,使用执行结果作为主体展示,
|
||||
# 避免 wait_result 里的 success=False + 旧 message 导致
|
||||
# 「create_sub_agent 失败:子智能体 X 已创建」的误导性文案。
|
||||
creation_meta = {
|
||||
"agent_id": result.get("agent_id"),
|
||||
"task_id": result.get("task_id"),
|
||||
"deliverables_dir": result.get("deliverables_dir"),
|
||||
"run_in_background": False,
|
||||
}
|
||||
execution_message = (
|
||||
wait_result.get("message")
|
||||
or wait_result.get("system_message")
|
||||
or result.get("message")
|
||||
)
|
||||
result = {
|
||||
**result,
|
||||
**wait_result,
|
||||
**creation_meta,
|
||||
"message": execution_message,
|
||||
}
|
||||
# 阻塞式执行不需要额外插入 system 消息
|
||||
result.pop("system_message", None)
|
||||
# 标记已通知,避免后续轮询再插入 system 消息
|
||||
try:
|
||||
task = self.sub_agent_manager.tasks.get(task_id)
|
||||
if isinstance(task, dict):
|
||||
task["notified"] = True
|
||||
task["updated_at"] = time.time()
|
||||
self.sub_agent_manager._save_state()
|
||||
except Exception:
|
||||
pass
|
||||
# 如果不是后台运行,阻塞等待完成
|
||||
if not arguments.get("run_in_background", False) and result.get("success"):
|
||||
task_id = result.get("task_id")
|
||||
wait_result = self.sub_agent_manager.wait_for_completion(
|
||||
task_id=task_id,
|
||||
timeout_seconds=arguments.get("timeout_seconds")
|
||||
)
|
||||
# 合并结果:保留创建元数据,使用执行结果作为主体展示,
|
||||
# 避免 wait_result 里的 success=False + 旧 message 导致
|
||||
# 「create_sub_agent 失败:子智能体 X 已创建」的误导性文案。
|
||||
creation_meta = {
|
||||
"agent_id": result.get("agent_id"),
|
||||
"task_id": result.get("task_id"),
|
||||
"deliverables_dir": result.get("deliverables_dir"),
|
||||
"run_in_background": False,
|
||||
}
|
||||
execution_message = (
|
||||
wait_result.get("message")
|
||||
or wait_result.get("system_message")
|
||||
or result.get("message")
|
||||
)
|
||||
result = {
|
||||
**result,
|
||||
**wait_result,
|
||||
**creation_meta,
|
||||
"message": execution_message,
|
||||
}
|
||||
# 阻塞式执行不需要额外插入 system 消息
|
||||
result.pop("system_message", None)
|
||||
# 标记已通知,避免后续轮询再插入 system 消息
|
||||
try:
|
||||
task = self.sub_agent_manager.tasks.get(task_id)
|
||||
if isinstance(task, dict):
|
||||
task["notified"] = True
|
||||
task["updated_at"] = time.time()
|
||||
self.sub_agent_manager._save_state()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elif tool_name == "terminate_sub_agent":
|
||||
result = self.sub_agent_manager.terminate_sub_agent(
|
||||
agent_id=arguments.get("agent_id")
|
||||
)
|
||||
# 多智能体模式:同步状态到 MultiAgentState
|
||||
if getattr(self, "multi_agent_mode", False):
|
||||
try:
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
||||
if state:
|
||||
state.mark_status(int(arguments.get("agent_id")), "terminated")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elif tool_name == "get_sub_agent_status":
|
||||
result = self.sub_agent_manager.get_sub_agent_status(
|
||||
agent_ids=arguments.get("agent_ids", [])
|
||||
)
|
||||
|
||||
# 多智能体模式专属工具:send_message_to_sub_agent / ask_sub_agent / answer_sub_agent_question / create_custom_agent / list_agents / list_active_sub_agents
|
||||
elif tool_name == "send_message_to_sub_agent":
|
||||
if not getattr(self, "multi_agent_mode", False):
|
||||
result = {"success": False, "error": "该工具仅在多智能体模式下可用"}
|
||||
else:
|
||||
try:
|
||||
from modules.multi_agent.state import build_master_message_to_sub_agent
|
||||
agent_id = int(arguments.get("agent_id", 0))
|
||||
message = arguments.get("message", "")
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
||||
if not state:
|
||||
result = {"success": False, "error": "多智能体状态未就绪"}
|
||||
else:
|
||||
# 构造消息文本并插入子对话
|
||||
text = build_master_message_to_sub_agent(message)
|
||||
ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text)
|
||||
if not ok:
|
||||
result = {"success": False, "error": f"子智能体 {agent_id} 不存在或已结束"}
|
||||
else:
|
||||
result = {"success": True, "agent_id": agent_id}
|
||||
except Exception as exc:
|
||||
result = {"success": False, "error": str(exc)}
|
||||
|
||||
elif tool_name == "ask_sub_agent":
|
||||
if not getattr(self, "multi_agent_mode", False):
|
||||
result = {"success": False, "error": "该工具仅在多智能体模式下可用"}
|
||||
else:
|
||||
try:
|
||||
from modules.multi_agent.state import format_multi_agent_message, TYPE_ASK
|
||||
agent_id = int(arguments.get("agent_id", 0))
|
||||
question = arguments.get("question", "")
|
||||
timeout = int(arguments.get("timeout_seconds", 600))
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
||||
if not state:
|
||||
result = {"success": False, "error": "多智能体状态未就绪"}
|
||||
else:
|
||||
# 构造提问并插入子对话(子智能体下一轮 assistant 输出作为回答插入主对话)
|
||||
text = format_multi_agent_message(
|
||||
display_name="Team Leader",
|
||||
msg_type=TYPE_ASK,
|
||||
content=question,
|
||||
target=state.get_instance(agent_id).display_name if state.get_instance(agent_id) else f"Agent_{agent_id}",
|
||||
)
|
||||
ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text)
|
||||
if not ok:
|
||||
result = {"success": False, "error": f"子智能体 {agent_id} 不存在"}
|
||||
else:
|
||||
# 阻塞等待子智能体下一轮输出作为回答
|
||||
answer = await state.wait_for_answer(
|
||||
question_id=f"ask_sub_agent_{int(time.time())}",
|
||||
agent_id=agent_id,
|
||||
timeout=timeout,
|
||||
)
|
||||
result = {"success": True, "answer": answer}
|
||||
except asyncio.TimeoutError:
|
||||
result = {"success": False, "error": "等待子智能体回答超时"}
|
||||
except Exception as exc:
|
||||
result = {"success": False, "error": str(exc)}
|
||||
|
||||
elif tool_name == "answer_sub_agent_question":
|
||||
if not getattr(self, "multi_agent_mode", False):
|
||||
result = {"success": False, "error": "该工具仅在多智能体模式下可用"}
|
||||
else:
|
||||
try:
|
||||
question_id = arguments.get("question_id", "")
|
||||
answer = arguments.get("answer", "")
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
||||
if not state:
|
||||
result = {"success": False, "error": "多智能体状态未就绪"}
|
||||
else:
|
||||
ok = state.provide_answer(question_id, answer)
|
||||
result = {"success": bool(ok), "question_id": question_id}
|
||||
except Exception as exc:
|
||||
result = {"success": False, "error": str(exc)}
|
||||
|
||||
elif tool_name == "create_custom_agent":
|
||||
try:
|
||||
from modules.multi_agent.role_store import RoleConfig, save_custom_role, list_roles
|
||||
role_id = arguments.get("role_id", "").strip()
|
||||
name = arguments.get("name", "").strip()
|
||||
body_prompt = arguments.get("body_prompt", "").strip()
|
||||
description = arguments.get("description", "").strip()
|
||||
thinking_mode_arg = arguments.get("thinking_mode", "fast")
|
||||
if not role_id or not name or not body_prompt:
|
||||
result = {"success": False, "error": "role_id/name/body_prompt 必填"}
|
||||
else:
|
||||
existing_ids = {r.role_id for r in list_roles()}
|
||||
if role_id in existing_ids:
|
||||
result = {"success": False, "error": f"角色 {role_id} 已存在"}
|
||||
else:
|
||||
role = RoleConfig(role_id=role_id, name=name, description=description, body_prompt=body_prompt, thinking_mode=thinking_mode_arg)
|
||||
f = save_custom_role(role)
|
||||
result = {"success": True, "role_id": role_id, "file": str(f)}
|
||||
except Exception as exc:
|
||||
result = {"success": False, "error": str(exc)}
|
||||
|
||||
elif tool_name == "list_agents":
|
||||
try:
|
||||
from modules.multi_agent.role_store import list_roles
|
||||
roles = list_roles()
|
||||
result = {"success": True, "roles": [r.to_dict() for r in roles]}
|
||||
except Exception as exc:
|
||||
result = {"success": False, "error": str(exc)}
|
||||
|
||||
elif tool_name == "list_active_sub_agents":
|
||||
try:
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
||||
if not state:
|
||||
result = {"success": True, "agents": []}
|
||||
else:
|
||||
result = {"success": True, "agents": [a.to_dict() for a in state.list_all()]}
|
||||
except Exception as exc:
|
||||
result = {"success": False, "error": str(exc)}
|
||||
|
||||
elif tool_name == "trigger_easter_egg":
|
||||
result = self.easter_egg_manager.trigger_effect(arguments.get("effect"))
|
||||
|
||||
|
||||
@ -347,6 +347,14 @@ class WebTerminal(MainTerminal):
|
||||
self.pending_permission_mode = str(meta.get("pending_permission_mode") or "").strip().lower() or None
|
||||
self.pending_execution_mode = str(meta.get("pending_execution_mode") or "").strip().lower() or None
|
||||
self.pending_network_permission = str(meta.get("pending_network_permission") or "").strip().lower() or None
|
||||
# 多智能体模式:以会话 metadata.multi_agent_mode 为唯一切换开关
|
||||
self.multi_agent_mode = bool(meta.get("multi_agent_mode", False))
|
||||
# 同步主智能体 sub_agent_manager 的 开关
|
||||
try:
|
||||
if hasattr(self, "sub_agent_manager"):
|
||||
self.sub_agent_manager.multi_agent_mode = self.multi_agent_mode
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
# 重置相关状态
|
||||
|
||||
53
modules/multi_agent/__init__.py
Normal file
53
modules/multi_agent/__init__.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""多智能体模式核心实现。
|
||||
|
||||
由 conversations metadata.multi_agent_mode = true 触发启用,
|
||||
不另起隔离链路,直接复用现有 MainTerminal、SubAgentManager、SubAgentTask
|
||||
但通过对话级开关注入多智能体版工具集、prompt、消息路由。
|
||||
|
||||
关键组件:
|
||||
- RoleConfig:角色 Markdown Frontmatter 解析与归档
|
||||
- MultiAgentState:一个多智能体会话的运行态状态机
|
||||
- 消息格式构造:format_multi_agent_message
|
||||
- 工具定义:master 侧、sub_agent 侧扩展
|
||||
- prompt 构造:build_multi_agent_master_prompt / build_multi_agent_sub_agent_prompt
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from .state import MultiAgentState, format_multi_agent_message
|
||||
from .role_store import (
|
||||
RoleConfig,
|
||||
load_preset_role,
|
||||
list_roles,
|
||||
save_custom_role,
|
||||
build_role_system_prompt,
|
||||
)
|
||||
from .prompts import (
|
||||
build_multi_agent_master_prompt,
|
||||
build_multi_agent_sub_agent_prompt,
|
||||
MULTI_AGENT_MASTER_PROMPT_BODY,
|
||||
MULTI_AGENT_SUB_AGENT_PROMPT_BODY,
|
||||
)
|
||||
from .tools import (
|
||||
MULTI_AGENT_MASTER_TOOLS,
|
||||
MULTI_AGENT_SUB_AGENT_TOOLS,
|
||||
build_master_tools_for_conversation,
|
||||
build_sub_agent_tools_for_role,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"MultiAgentState",
|
||||
"format_multi_agent_message",
|
||||
"RoleConfig",
|
||||
"load_preset_role",
|
||||
"list_roles",
|
||||
"save_custom_role",
|
||||
"build_role_system_prompt",
|
||||
"build_multi_agent_master_prompt",
|
||||
"build_multi_agent_sub_agent_prompt",
|
||||
"MULTI_AGENT_MASTER_PROMPT_BODY",
|
||||
"MULTI_AGENT_SUB_AGENT_PROMPT_BODY",
|
||||
"MULTI_AGENT_MASTER_TOOLS",
|
||||
"MULTI_AGENT_SUB_AGENT_TOOLS",
|
||||
"build_master_tools_for_conversation",
|
||||
"build_sub_agent_tools_for_role",
|
||||
]
|
||||
156
modules/multi_agent/prompts.py
Normal file
156
modules/multi_agent/prompts.py
Normal file
@ -0,0 +1,156 @@
|
||||
"""多智能体模式的系统提示词。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
MULTI_AGENT_MASTER_PROMPT_BODY = """# 多智能体模式
|
||||
|
||||
你是 **Team Leader**(团队领导者),负责协调多个子智能体分工协作完成用户的复杂任务。
|
||||
|
||||
## 工作原则
|
||||
|
||||
- **主动分工**:除非任务极其简单或明确不需要子智能体,否则主动把任务拆解并指派给合适的角色。
|
||||
- **明确指令**:用 `send_message_to_sub_agent` 发任务时,写清楚任务目标、范围、产出要求。
|
||||
- **及时回答**:当子智能体通过 `ask_master` 提问时,必须尽快通过 `answer_sub_agent_question` 回答。
|
||||
- **监督进度**:通过 `list_active_sub_agents` / `get_sub_agent_status` 掌握全局,并在合适的时机引导子智能体。
|
||||
- **运行时引导**:看到子智能体作出的步骤需要纠正时,立刻用 `send_message_to_sub_agent` 在其运行期间插入消息干预。
|
||||
- **明确问答**:当你需要一个具体的、可被回答的小问题被某个子智能体处理时,用 `ask_sub_agent` 阻塞等待一轮回答。
|
||||
|
||||
## 工具清单(多智能体模式专属)
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| `create_sub_agent` | 创建一个子智能体实例,指定 role_id |
|
||||
| `terminate_sub_agent` | 强制终止子智能体 |
|
||||
| `send_message_to_sub_agent` | 向子智能体插入引导消息/任务,不等待回复 |
|
||||
| `ask_sub_agent` | 向子智能体提出明确问题,阻塞等待一轮回答 |
|
||||
| `answer_sub_agent_question` | 回答子智能体通过 `ask_master` 提出的问题 |
|
||||
| `create_custom_agent` | 创建/保存自定义角色到后端 |
|
||||
| `list_agents` | 列出可用角色 |
|
||||
| `list_active_sub_agents` | 列出当前会话中活跃的子智能体 |
|
||||
| `get_sub_agent_status` | 查询指定子智能体的详细状态 |
|
||||
|
||||
**注意**:你现在仍拥有原本的全部工具(文件读写、终端、搜索、MCP、skill、memory 等)。以上只列出多智能体模式新增的工具——它们**替换**了原有的 `create_sub_agent` / `close_sub_agent` / `get_sub_agent_status`,使用语义有变化。
|
||||
|
||||
## 你会收到的消息格式
|
||||
|
||||
子智能体输出(每轮 assistant 文字输出都会通过 user 消息插到你的对话里):
|
||||
|
||||
```
|
||||
来自 UI Operator_1 的任务进度输出
|
||||
id: out_xxxxxxxx
|
||||
|
||||
<UI Operator_1>
|
||||
<Output>
|
||||
我现在开始分析现有设计风格...
|
||||
</Output>
|
||||
</UI Operator_1>
|
||||
```
|
||||
|
||||
子智能体向你提问:
|
||||
|
||||
```
|
||||
来自 Full-Stack Engineer_1 的提问
|
||||
id: ask_fse_001
|
||||
|
||||
<Full-Stack Engineer_1>
|
||||
<Ask>
|
||||
我应该使用 JWT 还是 Session Cookie?
|
||||
</Ask>
|
||||
</Full-Stack Engineer_1>
|
||||
```
|
||||
|
||||
**回答提问**必须用 `answer_sub_agent_question` 工具,传入 `question_id`(即消息里的 id)和 answer 文本。回答不会以 user 消息插入,而是直接返回到子智能体的 `ask_master` 工具结果中。
|
||||
|
||||
## 关于显示名
|
||||
|
||||
- 主智能体固定显示名:`Team Leader`
|
||||
- 子智能体显示名:`{角色名}_{agent_id}`,如 `UI Operator_1`、`Full-Stack Engineer_2`
|
||||
- 一个角色可以有多个实例(同 role_id 多 agent_id)
|
||||
|
||||
## 关于通信协议的三条硬性原则
|
||||
|
||||
1. **接收方决定插入方式**:子智能体收到消息后,由它自己的状态决定是 inline 穿插还是开启新轮任务。你不要操心插入位置,只负责发起。
|
||||
2. **回答走工具结果而非 user 消息**:你回答子智能体提问用的是 `answer_sub_agent_question`,回答内容是工具结果,不需要写出 XML 包裹。
|
||||
3. **任务发布/消息/引导都走自然 XML 格式**:调用 `send_message_to_sub_agent` 时只写正文,后端会自动包成 `来自 Team Leader 的消息 / 任务发布` 格式插入子对话。
|
||||
|
||||
## 关于团队全局可见
|
||||
|
||||
子智能体之间通过 `ask_other_agent` / `answer_other_agent` 直接通信,会并行在自己的对话内进行。你只需要在 prompt 里要求子智能体「如要向其他子智能体提问,必须同时直接给你输出一条汇报」,这样你能掌握全局。但你**不需要**手动转发它们之间的问答。
|
||||
"""
|
||||
|
||||
|
||||
MULTI_AGENT_SUB_AGENT_PROMPT_BODY = """# 多智能体身份
|
||||
|
||||
你是智能体集群团队的一员。你的团队通过分工协作完成复杂任务,主智能体 **Team Leader** 负责督导全局。
|
||||
|
||||
# 在任务中
|
||||
|
||||
- 不要频繁输出内容,不重要的内容会污染主智能体上下文
|
||||
- 只汇报关键步骤
|
||||
- 任务完成后给出详细结论
|
||||
- 自然结束输出即本轮任务结束;上下文会被保留,Team Leader 可能会再次发消息让你继续
|
||||
|
||||
# 沟通工具
|
||||
|
||||
- **需要 Team Leader 决策时**:调用 `ask_master` 工具,传入 question 文本
|
||||
- 工具会阻塞等待 Team Leader 通过 `answer_sub_agent_question` 给出回答
|
||||
- 你的 question 会以 XML 「提问」格式被插入主对话
|
||||
- **要问其他子智能体时**:调用 `ask_other_agent`,传入 target_agent_id 与 question
|
||||
- 等待对方调用 `answer_other_agent` 回答
|
||||
- **要回答其他子智能体的提问时**:调用 `answer_other_agent`,传入 source_agent_id 与 question_id 和 answer
|
||||
- 你的回答直接作为对方 `ask_other_agent` 工具的结果返回(不会以 user 消息插入对话)
|
||||
- **查询当前活跃子智能体**:调用 `list_active_sub_agents`
|
||||
|
||||
# 关于向你团队「汇报」的强制要求
|
||||
|
||||
**如果你要向其他子智能体提问,必须同时直接输出一条汇报给 Team Leader**(在你的普通文本输出里),说明:
|
||||
1. 你为什么要问这个问题
|
||||
2. 你问了谁
|
||||
3. 你期望得到什么
|
||||
|
||||
不能偷偷沟通,Team Leader 需要看到完整协作流程。
|
||||
|
||||
# 输出格式
|
||||
|
||||
你每轮的普通 assistant 文字输出都会被自动捕获并以如下格式插入到主对话:
|
||||
|
||||
```
|
||||
来自 {你的显示名} 的任务进度输出
|
||||
id: out_xxxxxxxx
|
||||
|
||||
<{你的显示名}>
|
||||
<Output>
|
||||
{你的输出}
|
||||
</Output>
|
||||
</{你的显示名}>
|
||||
```
|
||||
|
||||
你不需要自己包裹 XML,直接输出正文即可。
|
||||
"""
|
||||
|
||||
|
||||
def build_multi_agent_master_prompt(workspace_path: str, base: str = "") -> str:
|
||||
"""构造主智能体(Team Leader)的系统提示词。
|
||||
|
||||
`base` 一般为现有 MainTerminal 的 base 提示词(环境/工具概览等),
|
||||
我们在末尾追加多智能体模式专属正文。
|
||||
"""
|
||||
if base and base.strip():
|
||||
return f"{base.rstrip()}\n\n{MULTI_AGENT_MASTER_PROMPT_BODY}\n"
|
||||
return f"{MULTI_AGENT_MASTER_PROMPT_BODY}\n"
|
||||
|
||||
|
||||
def build_multi_agent_sub_agent_prompt(role_body: str, display_name: str, workspace_path: str) -> str:
|
||||
"""构造子智能体的系统提示词。
|
||||
|
||||
`role_body` 为该角色 Markdown 文件 frontmatter 之后的自定义 prompt。
|
||||
`display_name` 为该实例的显示名(如 `UI Operator_1`)。
|
||||
"""
|
||||
header = MULTI_AGENT_SUB_AGENT_PROMPT_BODY.rstrip()
|
||||
return (
|
||||
f"{header}\n\n"
|
||||
f"# 你的显示名\n\n你的显示名是 `{display_name}`。\n\n"
|
||||
f"# 你的专属设定\n\n{role_body.strip()}\n"
|
||||
)
|
||||
208
modules/multi_agent/role_store.py
Normal file
208
modules/multi_agent/role_store.py
Normal file
@ -0,0 +1,208 @@
|
||||
"""多智能体角色存储与解析。
|
||||
|
||||
角色定义文件格式(Markdown + Frontmatter):
|
||||
|
||||
---
|
||||
id: ui-operator
|
||||
name: UI Operator
|
||||
description: 界面设计与前端实现
|
||||
model: ""
|
||||
thinking_mode: fast
|
||||
skills:
|
||||
- frontend-design
|
||||
---
|
||||
|
||||
自定义 prompt body...
|
||||
|
||||
角色目录:
|
||||
- ~/.astrion/astrion/host/mutiagents/agents/<role_id>.md
|
||||
|
||||
存储约定见 .astrion/memory/multi_agent_mode_design.md。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
try:
|
||||
from config.paths import IS_HOST_MODE, RUNTIME_ROOT
|
||||
except ImportError:
|
||||
# 防止直接 import 时 config 尚未加载,提供回退值
|
||||
IS_HOST_MODE = True
|
||||
RUNTIME_ROOT = str(Path.home() / ".astrion" / "astrion")
|
||||
|
||||
# 复用项目的 MUTIAGENTS 数据目录(保留原拼写 "mutiagents",见项目记忆)
|
||||
DEFAULT_MUTIAGENTS_DIR = Path(RUNTIME_ROOT) / ("host" if IS_HOST_MODE else "web") / "mutiagents"
|
||||
AGENTS_DIR_NAME = "agents"
|
||||
|
||||
FRONTMATTER_RE = re.compile(r"^---\s*\n(?P<body>.*?)\n---\s*\n(?P<rest>.*)$", re.S)
|
||||
|
||||
|
||||
def _agents_dir() -> Path:
|
||||
"""返回角色根目录(按运行模式自动选择 host/web)。"""
|
||||
base = Path(DEFAULT_MUTIAGENTS_DIR)
|
||||
out = base / AGENTS_DIR_NAME
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
return out
|
||||
|
||||
|
||||
def _parse_frontmatter(text: str) -> tuple[Dict[str, Any], str]:
|
||||
"""解析 Markdown frontmatter,返回 (元数据 dict, body)。"""
|
||||
m = FRONTMATTER_RE.match(text)
|
||||
if not m:
|
||||
return {}, text
|
||||
raw_meta = m.group("body") or ""
|
||||
body = m.group("rest") or ""
|
||||
meta: Dict[str, Any] = {}
|
||||
# 简易解析(k: v 与 k: 列表项,不引入 yaml 依赖)
|
||||
current_key: Optional[str] = None
|
||||
for line in raw_meta.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if line.startswith(" - ") or line.startswith("- "):
|
||||
value = line.lstrip(" ").lstrip("- ").strip()
|
||||
if current_key and isinstance(meta.get(current_key), list):
|
||||
meta[current_key].append(value)
|
||||
continue
|
||||
if ":" in line:
|
||||
k, v = line.split(":", 1)
|
||||
k = k.strip()
|
||||
v = v.strip().strip('"').strip("'")
|
||||
if v:
|
||||
meta[k] = v
|
||||
current_key = None
|
||||
else:
|
||||
# 可能是列表块
|
||||
meta[k] = []
|
||||
current_key = k
|
||||
return meta, body.strip()
|
||||
|
||||
|
||||
def load_role_from_file(file_path: Path) -> Optional["RoleConfig"]:
|
||||
"""从 Markdown 文件加载角色定义。"""
|
||||
if not file_path.exists() or not file_path.is_file():
|
||||
return None
|
||||
text = file_path.read_text(encoding="utf-8")
|
||||
meta, body = _parse_frontmatter(text)
|
||||
if not meta.get("id") or not meta.get("name"):
|
||||
return None
|
||||
return RoleConfig(
|
||||
role_id=str(meta["id"]),
|
||||
name=str(meta["name"]),
|
||||
description=str(meta.get("description") or ""),
|
||||
body_prompt=body,
|
||||
model_key=(str(meta["model"]) if meta.get("model") else None) or None,
|
||||
thinking_mode=str(meta.get("thinking_mode") or "fast"),
|
||||
skills=list(meta.get("skills") or []),
|
||||
source_file=str(file_path),
|
||||
)
|
||||
|
||||
|
||||
def load_preset_role(role_id: str) -> Optional["RoleConfig"]:
|
||||
"""从预设/自定义角色目录加载一个角色。"""
|
||||
f = _agents_dir() / f"{role_id}.md"
|
||||
return load_role_from_file(f)
|
||||
|
||||
|
||||
def list_roles() -> List["RoleConfig"]:
|
||||
"""列出全部角色(预设 + 用户自定义)。"""
|
||||
agents_dir = _agents_dir()
|
||||
if not agents_dir.exists():
|
||||
return []
|
||||
roles: List[RoleConfig] = []
|
||||
for p in sorted(agents_dir.glob("*.md")):
|
||||
r = load_role_from_file(p)
|
||||
if r and r.role_id:
|
||||
roles.append(r)
|
||||
return roles
|
||||
|
||||
|
||||
def save_custom_role(role: "RoleConfig") -> Path:
|
||||
"""把自定义角色保存到 agents 目录。"""
|
||||
f = _agents_dir() / f"{role.role_id}.md"
|
||||
f.parent.mkdir(parents=True, exist_ok=True)
|
||||
f.write_text(_serialize_role(role), encoding="utf-8")
|
||||
return f
|
||||
|
||||
|
||||
def _serialize_role(role: "RoleConfig") -> str:
|
||||
"""把 RoleConfig 序列化为 Markdown frontmatter 字符串。"""
|
||||
lines = ["---"]
|
||||
lines.append(f"id: {role.role_id}")
|
||||
lines.append(f"name: {role.name}")
|
||||
if role.description:
|
||||
# 单行描述转义
|
||||
desc = role.description.replace('"', '\\"')
|
||||
lines.append(f'description: "{desc}"')
|
||||
if role.model_key:
|
||||
lines.append(f"model: {role.model_key}")
|
||||
lines.append(f"thinking_mode: {role.thinking_mode or 'fast'}")
|
||||
if role.skills:
|
||||
lines.append("skills:")
|
||||
for s in role.skills:
|
||||
lines.append(f" - {s}")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append(role.body_prompt or "")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_role_system_prompt(role: "RoleConfig") -> str:
|
||||
"""生成子智能体的「专属 prompt」部分(frontmatter 之后的 body)。"""
|
||||
return role.body_prompt or ""
|
||||
|
||||
|
||||
class RoleConfig:
|
||||
"""一个多智能体角色配置。"""
|
||||
|
||||
__slots__ = (
|
||||
"role_id",
|
||||
"name",
|
||||
"description",
|
||||
"body_prompt",
|
||||
"model_key",
|
||||
"thinking_mode",
|
||||
"skills",
|
||||
"source_file",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
role_id: str,
|
||||
name: str,
|
||||
description: str = "",
|
||||
body_prompt: str = "",
|
||||
model_key: Optional[str] = None,
|
||||
thinking_mode: str = "fast",
|
||||
skills: Optional[List[str]] = None,
|
||||
source_file: str = "",
|
||||
):
|
||||
self.role_id = role_id
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.body_prompt = body_prompt
|
||||
self.model_key = model_key
|
||||
self.thinking_mode = thinking_mode or "fast"
|
||||
self.skills = list(skills or [])
|
||||
self.source_file = source_file
|
||||
|
||||
def display_name(self, agent_id: int) -> str:
|
||||
"""根据实例 id 生成显示名。"""
|
||||
return f"{self.name}_{agent_id}"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"role_id": self.role_id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"model_key": self.model_key,
|
||||
"thinking_mode": self.thinking_mode,
|
||||
"skills": self.skills,
|
||||
"source_file": self.source_file,
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<RoleConfig id={self.role_id} name={self.name}>"
|
||||
339
modules/multi_agent/state.py
Normal file
339
modules/multi_agent/state.py
Normal file
@ -0,0 +1,339 @@
|
||||
"""多智能体会话状态机。
|
||||
|
||||
一个 MultiAgentState 绑定到一个多智能体对话的 conversation_id,维护:
|
||||
- 已创建的子智能体实例(agent_id ↔ role_id ↔ display_name ↔ task_id ↔ status)
|
||||
- 待插入到主对话的待发 user 消息队列(pending_master_messages)
|
||||
- 主智能体工具调用 answer_sub_agent_question / answer_other_agent 写回答案的 futomap
|
||||
- 子智能体调用 ask_master / ask_other_agent 时挂起的 futomap
|
||||
|
||||
关键约定(来自 .astrion/memory/multi_agent_mode_design.md):
|
||||
- 消息格式:`来自 {显示名} 的{类型}\\nid: {消息id}\\n\\n<{显示名}>\\n<{标签}>\\n{内容}\\n</{标签}>\\n</{显示名}>`
|
||||
- 接收方决定插入方式:
|
||||
- 子智能体 ask 阻塞等待 → main 调 answer_* 返回到工具结果
|
||||
- 子智能体 idle 状态 → 主对话的 pending_master_messages 直接插入新轮 user 消息
|
||||
- 子智能体 running 中 → inline 插入到当前末尾(在下一轮 model 调用前合并 messages)
|
||||
- 通信是「工具调用提问」+「回答返回到工具结果」;其他场景(输出/进度/完成/任务发布/消息/回答)
|
||||
才以 user 消息格式插入对话。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from modules.sub_agent.task import SubAgentTask
|
||||
|
||||
# ---------- 消息类型常量 ----------
|
||||
TYPE_TASK = "Task" # 主→子 任务发布
|
||||
TYPE_OUTPUT = "Output" # 子→主 进度/完成输出(统一)
|
||||
TYPE_ASK = "Ask" # 子→主 / 子→子 提问
|
||||
TYPE_ANSWER = "Answer" # 主→子 / 子→子 回答(不插入对话,仅做工具结果)
|
||||
TYPE_MESSAGE = "Message" # 任意方向 消息
|
||||
# 内部枚举到此
|
||||
|
||||
QUESTION_PREFIX_ASK_MASTER = "ask_master"
|
||||
QUESTION_PREFIX_ASK_OTHER = "ask_other"
|
||||
|
||||
|
||||
def format_multi_agent_message(
|
||||
*,
|
||||
display_name: str,
|
||||
msg_type: str,
|
||||
content: str,
|
||||
msg_id: Optional[str] = None,
|
||||
target: Optional[str] = None,
|
||||
extra_attrs: Optional[Dict[str, str]] = None,
|
||||
) -> str:
|
||||
"""按统一格式构造 user 消息字符串。
|
||||
|
||||
Args:
|
||||
display_name: 发出方显示名(如 UI Operator_1 / Team Leader)
|
||||
msg_type: 消息类型,对应上方 TYPE_* 常量
|
||||
content: 消息正文
|
||||
msg_id: 消息 id;不传则自动生成
|
||||
target: 接收方显示名(用于子→子 提问时标明对谁提问)
|
||||
extra_attrs: 额外标签属性(如 question_id="ask_xxx")
|
||||
"""
|
||||
if not msg_id:
|
||||
msg_id = f"msg_{uuid.uuid4().hex[:10]}"
|
||||
|
||||
# 第一行:自然语言前缀(含 target 标识)
|
||||
if target:
|
||||
prefix = f"来自 {display_name} 向 {target} 的{msg_type_to_text(msg_type)}"
|
||||
else:
|
||||
prefix = f"来自 {display_name} 的{msg_type_to_text(msg_type)}"
|
||||
|
||||
# 第二行:id
|
||||
id_line = f"id: {msg_id}"
|
||||
|
||||
# 属性 attr 字符串
|
||||
attrs = ""
|
||||
if target:
|
||||
attrs += f' target="{target}"'
|
||||
if extra_attrs:
|
||||
for k, v in extra_attrs.items():
|
||||
attrs += f' {k}="{v}"'
|
||||
|
||||
# XML 包裹
|
||||
tag = msg_type
|
||||
xml = (
|
||||
f"<{display_name}>\n"
|
||||
f"<{tag}{attrs}>\n"
|
||||
f"{content}\n"
|
||||
f"</{tag}>\n"
|
||||
f"</{display_name}>"
|
||||
)
|
||||
|
||||
return f"{prefix}\n{id_line}\n\n{xml}"
|
||||
|
||||
|
||||
def msg_type_to_text(msg_type: str) -> str:
|
||||
"""把 TYPE_* 转为中文短语,用于 prompt 前缀。"""
|
||||
mapping = {
|
||||
TYPE_TASK: "任务发布",
|
||||
TYPE_OUTPUT: "任务进度输出",
|
||||
TYPE_ASK: "提问",
|
||||
TYPE_ANSWER: "回答",
|
||||
TYPE_MESSAGE: "消息",
|
||||
}
|
||||
return mapping.get(msg_type, msg_type)
|
||||
|
||||
|
||||
def build_master_dispatch_text(task: str, msg_id: Optional[str] = None) -> str:
|
||||
"""主智能体发布任务时插入到子智能体对话的 user 消息文本。"""
|
||||
return format_multi_agent_message(
|
||||
display_name="Team Leader",
|
||||
msg_type=TYPE_TASK,
|
||||
content=task,
|
||||
msg_id=msg_id,
|
||||
)
|
||||
|
||||
|
||||
def build_sub_agent_output_text(display_name: str, content: str, msg_id: Optional[str] = None) -> str:
|
||||
"""子智能体输出(进度或完成)插入到主对话的 user 消息文本。"""
|
||||
return format_multi_agent_message(
|
||||
display_name=display_name,
|
||||
msg_type=TYPE_OUTPUT,
|
||||
content=content,
|
||||
msg_id=msg_id,
|
||||
)
|
||||
|
||||
|
||||
def build_sub_agent_ask_master_text(display_name: str, question: str, question_id: str) -> str:
|
||||
"""子智能体向主智能体提问时插入到主对话的 user 消息文本。"""
|
||||
return format_multi_agent_message(
|
||||
display_name=display_name,
|
||||
msg_type=TYPE_ASK,
|
||||
content=question,
|
||||
msg_id=question_id,
|
||||
)
|
||||
|
||||
|
||||
def build_sub_agent_ask_other_text(
|
||||
display_name: str,
|
||||
target_display: str,
|
||||
question: str,
|
||||
question_id: str,
|
||||
) -> str:
|
||||
"""子智能体向另一个子智能体提问时插入到目标子智能体对话的文本。"""
|
||||
return format_multi_agent_message(
|
||||
display_name=display_name,
|
||||
msg_type=TYPE_ASK,
|
||||
content=question,
|
||||
msg_id=question_id,
|
||||
target=target_display,
|
||||
)
|
||||
|
||||
|
||||
def build_master_message_to_sub_agent(message: str, msg_id: Optional[str] = None) -> str:
|
||||
"""主智能体 send_message_to_sub_agent 时插入子对话的 user 消息文本。"""
|
||||
return format_multi_agent_message(
|
||||
display_name="Team Leader",
|
||||
msg_type=TYPE_MESSAGE,
|
||||
content=message,
|
||||
msg_id=msg_id,
|
||||
)
|
||||
|
||||
|
||||
def build_master_answer_to_sub_agent(
|
||||
display_name: str,
|
||||
target_display: str,
|
||||
answer: str,
|
||||
question_id: str,
|
||||
) -> str:
|
||||
"""主智能体回答插入到子对话(仅当子智能体 not waiting 或 idle 时走 user 消息路径)。"""
|
||||
return format_multi_agent_message(
|
||||
display_name=display_name,
|
||||
msg_type=TYPE_ANSWER,
|
||||
content=answer,
|
||||
msg_id=question_id,
|
||||
target=target_display,
|
||||
extra_attrs={"question_id": question_id},
|
||||
)
|
||||
|
||||
|
||||
# ---------- 运行态状态机 ----------
|
||||
@dataclass
|
||||
class AgentInstance:
|
||||
"""一个多智能体会话中已创建的子智能体实例。"""
|
||||
|
||||
agent_id: int
|
||||
role_id: str
|
||||
display_name: str
|
||||
task_id: str
|
||||
status: str = "running" # running / idle / terminated / failed / timeout
|
||||
summary: str = ""
|
||||
created_at: float = field(default_factory=lambda: datetime.now().timestamp())
|
||||
last_output: str = ""
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"agent_id": self.agent_id,
|
||||
"role_id": self.role_id,
|
||||
"display_name": self.display_name,
|
||||
"task_id": self.task_id,
|
||||
"status": self.status,
|
||||
"summary": self.summary,
|
||||
"created_at": self.created_at,
|
||||
"last_output": self.last_output,
|
||||
}
|
||||
|
||||
|
||||
class MultiAgentState:
|
||||
"""绑到一个 conversation_id 的多智能体运行态。
|
||||
|
||||
线程安全:所有 pubic 方法均假设在 SubAgentManager 的事件循环线程中调用,
|
||||
或者由 chat task 主线程通过 manager 的 _run_coro 进入此循环。
|
||||
跨线程访问通过 manager._run_coro 桥接,避免直接调用。
|
||||
"""
|
||||
|
||||
def __init__(self, conversation_id: str):
|
||||
self.conversation_id = conversation_id
|
||||
# agent_id 映射;同一会话里 agent_id 唯一
|
||||
self.agents: Dict[int, AgentInstance] = {}
|
||||
# task_id -> agent_id(便于在 SubAgentTask 完成时回写)
|
||||
self.task_id_to_agent_id: Dict[str, int] = {}
|
||||
# 主智能体待插入消息队列(每条都是字符串,由 chat task 取走)
|
||||
self.pending_master_messages: List[str] = []
|
||||
# ask_master / ask_other_agent 的等待 future
|
||||
# key = question_id, value = asyncio.Future (结果为 answer str 或 Exception)
|
||||
self.pending_questions: Dict[str, asyncio.Future] = {}
|
||||
# 一个 agent 可能同时只阻塞在一个 ask 工具上(最简实现)
|
||||
# key = agent_id, value = question_id(表示当前 agent 正阻塞等待)
|
||||
self.agent_blocking_question: Dict[int, str] = {}
|
||||
# 角色实例计数:role_id -> 已分配的最大 agent_id(数字)
|
||||
# 用于创建新实例时自动递增编号,但允许调用方显式指定
|
||||
self.role_counters: Dict[str, int] = {}
|
||||
|
||||
# ----- 创建/查询 -----
|
||||
def next_agent_id_for_role(self, role_id: str) -> int:
|
||||
"""为指定角色分配下一个 agent_id 编号。"""
|
||||
n = self.role_counters.get(role_id, 0) + 1
|
||||
self.role_counters[role_id] = n
|
||||
return n
|
||||
|
||||
def register_instance(self, instance: AgentInstance) -> None:
|
||||
if instance.agent_id in self.agents:
|
||||
raise ValueError(f"agent_id {instance.agent_id} 已存在")
|
||||
self.agents[instance.agent_id] = instance
|
||||
self.task_id_to_agent_id[instance.task_id] = instance.agent_id
|
||||
|
||||
def get_instance(self, agent_id: int) -> Optional[AgentInstance]:
|
||||
return self.agents.get(agent_id)
|
||||
|
||||
def get_instance_by_task_id(self, task_id: str) -> Optional[AgentInstance]:
|
||||
aid = self.task_id_to_agent_id.get(task_id)
|
||||
if aid is None:
|
||||
return None
|
||||
return self.agents.get(aid)
|
||||
|
||||
def list_active(self) -> List[AgentInstance]:
|
||||
return [a for a in self.agents.values() if a.status == "running" or a.status == "idle"]
|
||||
|
||||
def list_all(self) -> List[AgentInstance]:
|
||||
return list(self.agents.values())
|
||||
|
||||
def mark_status(self, agent_id: int, status: str, last_output: str = "") -> None:
|
||||
a = self.agents.get(agent_id)
|
||||
if a:
|
||||
a.status = status
|
||||
if last_output:
|
||||
a.last_output = last_output
|
||||
|
||||
# ----- 主对话注入 -----
|
||||
def push_master_message(self, message_text: str) -> None:
|
||||
"""把一条 user 消息追加到主对话待插入队列。"""
|
||||
self.pending_master_messages.append(message_text)
|
||||
|
||||
def drain_master_messages(self) -> List[str]:
|
||||
"""取出(清空)所有待插入主对话的消息。"""
|
||||
msgs = self.pending_master_messages
|
||||
self.pending_master_messages = []
|
||||
return msgs
|
||||
|
||||
def has_pending_master_messages(self) -> bool:
|
||||
return len(self.pending_master_messages) > 0
|
||||
|
||||
# ----- 阻塞问答 -----
|
||||
async def wait_for_answer(self, question_id: str, agent_id: int, timeout: float = 600.0) -> str:
|
||||
"""子智能体 ask_* 工具调用后阻塞等待答案。
|
||||
|
||||
返回 answer 字符串;超时/取消抛 asyncio.TimeoutError 或 CancelledError。
|
||||
"""
|
||||
if question_id in self.pending_questions:
|
||||
raise RuntimeError(f"question_id 已存在: {question_id}")
|
||||
fut: asyncio.Future = asyncio.get_event_loop().create_future()
|
||||
self.pending_questions[question_id] = fut
|
||||
self.agent_blocking_question[agent_id] = question_id
|
||||
try:
|
||||
return await asyncio.wait_for(fut, timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise
|
||||
finally:
|
||||
self.pending_questions.pop(question_id, None)
|
||||
if self.agent_blocking_question.get(agent_id) == question_id:
|
||||
self.agent_blocking_question.pop(agent_id, None)
|
||||
|
||||
def provide_answer(self, question_id: str, answer: str) -> bool:
|
||||
"""主/其他子智能体 answer_* 工具调用时回写答案。
|
||||
|
||||
返回 True 表示找到等待中的 future;False 表示无等待方或已超时。
|
||||
"""
|
||||
fut = self.pending_questions.get(question_id)
|
||||
if not fut or fut.done():
|
||||
return False
|
||||
try:
|
||||
fut.set_result(answer)
|
||||
except asyncio.InvalidStateError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_agent_blocking(self, agent_id: int) -> bool:
|
||||
return agent_id in self.agent_blocking_question
|
||||
|
||||
def get_blocking_question_id(self, agent_id: int) -> Optional[str]:
|
||||
return self.agent_blocking_question.get(agent_id)
|
||||
|
||||
# ----- 持久化(最简版) -----
|
||||
def to_snapshot(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"conversation_id": self.conversation_id,
|
||||
"agents": [a.to_dict() for a in self.agents.values()],
|
||||
"role_counters": self.role_counters,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_snapshot(cls, snapshot: Dict[str, Any]) -> "MultiAgentState":
|
||||
state = cls(conversation_id=snapshot.get("conversation_id", ""))
|
||||
state.role_counters = dict(snapshot.get("role_counters") or {})
|
||||
for a_data in snapshot.get("agents") or []:
|
||||
a = AgentInstance(**a_data)
|
||||
state.agents[a.agent_id] = a
|
||||
if a.task_id:
|
||||
state.task_id_to_agent_id[a.task_id] = a.agent_id
|
||||
return state
|
||||
323
modules/multi_agent/tools.py
Normal file
323
modules/multi_agent/tools.py
Normal file
@ -0,0 +1,323 @@
|
||||
"""多智能体工具定义(OpenAI Function Calling 格式)。
|
||||
|
||||
主智能体侧(master):替换原有的 4 个旧版子智能体工具,新增 9 个多智能体工具。
|
||||
子智能体侧(sub_agent):在现有 8 个基础工具之外新增 4 个通信工具。
|
||||
|
||||
工具处理函数(handler)不在这里实现,而是在 SubAgentManager/SubAgentTask 中
|
||||
注册回调,工具执行入口仍走 SubAgentManager.execute_tool_for_sub_agent。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
def _inject_intent(properties: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""为所有 properties 注入 intent 字段(与现有工具规范保持一致)。"""
|
||||
out: Dict[str, Any] = {}
|
||||
for k, v in properties.items():
|
||||
if k == "intent":
|
||||
out[k] = v
|
||||
continue
|
||||
out[k] = v
|
||||
# 在末尾追加 intent 字段
|
||||
if "intent" not in out:
|
||||
out["intent"] = {
|
||||
"type": "string",
|
||||
"description": "用不超过15个字向用户说明你要做什么,例如:派遣UI Operator设计配色。",
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
# ----- 主智能体工具 -----
|
||||
def _master_tool_create_sub_agent() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_sub_agent",
|
||||
"description": (
|
||||
"创建一个属于多智能体团队的子智能体实例并启动。必须指定 role_id。"
|
||||
"实例会注入该角色的专属 prompt 与多智能体协作工具(ask_master 等)。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"role_id": {
|
||||
"type": "string",
|
||||
"description": "角色标识,例如 'ui-operator'/'full-stack-engineer'/'code-reviewer'/'researcher'。先用 list_agents 查看可用角色。",
|
||||
},
|
||||
"task": {
|
||||
"type": "string",
|
||||
"description": "要交给该子智能体执行的任务描述。要求包含:目标、范围、产出、注意事项。",
|
||||
},
|
||||
"agent_id": {
|
||||
"type": "integer",
|
||||
"description": "(可选)手动指定实例编号;不传时自动递增。",
|
||||
},
|
||||
"deliverables_dir": {
|
||||
"type": "string",
|
||||
"description": "(可选)交付目录相对路径,留空则用 sub_agent_results/agent_{N}。",
|
||||
},
|
||||
"timeout_seconds": {"type": "integer", "description": "超时秒数,默认 600。"},
|
||||
"thinking_mode": {
|
||||
"type": "string",
|
||||
"enum": ["fast", "thinking"],
|
||||
"description": "(可选)覆盖角色默认思考模式。不填使用角色配置。",
|
||||
},
|
||||
}),
|
||||
"required": ["role_id", "task", "thinking_mode"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_terminate_sub_agent() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminate_sub_agent",
|
||||
"description": "强制终止指定子智能体实例。终止后无法恢复,已生成的文件保留。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"agent_id": {"type": "integer", "description": "要终止的子智能体编号。"},
|
||||
}),
|
||||
"required": ["agent_id"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_send_message_to_sub_agent() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "send_message_to_sub_agent",
|
||||
"description": (
|
||||
"运行时引导/干预:向指定子智能体插入一条引导消息或新任务,立刻返回不等待回复。"
|
||||
"用于看到子智能体中间输出后立即纠正方向、追加要求。消息接收方根据当前状态"
|
||||
"(running/idle)自行决定是 inline 插入还是触发新一轮任务。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"agent_id": {"type": "integer", "description": "目标子智能体编号。"},
|
||||
"message": {"type": "string", "description": "要插入的消息或新任务正文。"},
|
||||
}),
|
||||
"required": ["agent_id", "message"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_ask_sub_agent() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "ask_sub_agent",
|
||||
"description": (
|
||||
"向指定子智能体提出一个明确问题并阻塞等待一轮回答(不是发起任务,是问问题)。"
|
||||
"问题会以 `来自 Team Leader 的提问` 格式插入子对话,子智能体下一轮 assistant 输出"
|
||||
"作为回答返回到此工具结果中。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"agent_id": {"type": "integer", "description": "目标子智能体编号。"},
|
||||
"question": {"type": "string", "description": "要询问的问题,应简短明确。"},
|
||||
"timeout_seconds": {"type": "integer", "description": "等待回答超时秒数,默认 600。"},
|
||||
}),
|
||||
"required": ["agent_id", "question"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_answer_sub_agent_question() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "answer_sub_agent_question",
|
||||
"description": (
|
||||
"回答子智能体通过 ask_master 工具提出的问题。回答内容会直接返回到子智能体"
|
||||
"ask_master 工具的 tool_call 结果里(不会以 user 消息插入子对话)。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"question_id": {"type": "string", "description": "提问消息里给出的 id,如 ask_xxx。"},
|
||||
"answer": {"type": "string", "description": "给子智能体的回答正文。"},
|
||||
}),
|
||||
"required": ["question_id", "answer"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_create_custom_agent() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_custom_agent",
|
||||
"description": "创建/保存一个自定义角色到后端(~/.astrion/astrion/host/mutiagents/agents/)。后续可用 create_sub_agent 指定该角色。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"role_id": {"type": "string", "description": "角色标识(英文小写下划线,如 api-designer)。"},
|
||||
"name": {"type": "string", "description": "显示名(如 API Designer)。"},
|
||||
"description": {"type": "string", "description": "一句话简述职责。"},
|
||||
"body_prompt": {"type": "string", "description": "角色的自定义 prompt body(Markdown)。"},
|
||||
"thinking_mode": {"type": "string", "enum": ["fast", "thinking"], "description": "默认思考模式。"},
|
||||
}),
|
||||
"required": ["role_id", "name", "body_prompt"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_list_agents() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_agents",
|
||||
"description": "列出所有可用的预置/自定义角色(role_id / name / 描述)。用于在 create_sub_agent 前选角色。",
|
||||
"parameters": {"type": "object", "properties": _inject_intent({})},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_list_active_sub_agents() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_active_sub_agents",
|
||||
"description": "列出当前多智能体会话中所有活跃/已创建的子智能体实例(agent_id/role/display_name/status)。",
|
||||
"parameters": {"type": "object", "properties": _inject_intent({})},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_get_sub_agent_status() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_sub_agent_status",
|
||||
"description": "查询一个或多个子智能体的详细状态。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"agent_ids": {"type": "array", "items": {"type": "integer"}, "description": "要查询的子智能体编号列表。"},
|
||||
}),
|
||||
"required": ["agent_ids"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
MULTI_AGENT_MASTER_TOOLS: List[Dict[str, Any]] = [
|
||||
_master_tool_create_sub_agent(),
|
||||
_master_tool_terminate_sub_agent(),
|
||||
_master_tool_send_message_to_sub_agent(),
|
||||
_master_tool_ask_sub_agent(),
|
||||
_master_tool_answer_sub_agent_question(),
|
||||
_master_tool_create_custom_agent(),
|
||||
_master_tool_list_agents(),
|
||||
_master_tool_list_active_sub_agents(),
|
||||
_master_tool_get_sub_agent_status(),
|
||||
]
|
||||
|
||||
|
||||
# ----- 子智能体工具 -----
|
||||
def _sub_tool_ask_master() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "ask_master",
|
||||
"description": (
|
||||
"向 Team Leader(主智能体)提问,工具调用会阻塞等待主智能体通过 answer_sub_agent_question 给出回答。"
|
||||
"用于需要主智能体决策的场合。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"question": {"type": "string", "description": "向 Team Leader 提问的内容。"},
|
||||
"question_id": {"type": "string", "description": "(可选)问题 id;不传自动生成。"},
|
||||
}),
|
||||
"required": ["question"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _sub_tool_ask_other_agent() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "ask_other_agent",
|
||||
"description": (
|
||||
"向另一个子智能体提问,阻塞等待对方调用 answer_other_agent 回答。"
|
||||
"**注意**:调用此工具的同时,你必须在你的文本输出里向 Team Leader 输出一条汇报,"
|
||||
"说明你为何问、问谁、期望什么——不要偷偷沟通。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"target_agent_id": {"type": "integer", "description": "目标子智能体编号。"},
|
||||
"question": {"type": "string", "description": "提问内容。"},
|
||||
"question_id": {"type": "string", "description": "(可选)问题 id。"},
|
||||
}),
|
||||
"required": ["target_agent_id", "question"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _sub_tool_answer_other_agent() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "answer_other_agent",
|
||||
"description": (
|
||||
"回答其他子智能体通过 ask_other_agent 提出的问题。"
|
||||
"answer 内容会直接返回到对方 ask_other_agent 工具结果中。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"source_agent_id": {"type": "integer", "description": "提问方 agent_id。"},
|
||||
"question_id": {"type": "string", "description": "提问消息中的 id。"},
|
||||
"answer": {"type": "string", "description": "回答内容。"},
|
||||
}),
|
||||
"required": ["source_agent_id", "question_id", "answer"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _sub_tool_list_active_sub_agents() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_active_sub_agents",
|
||||
"description": "查询当前多智能体会话中所有活跃/已创建的子智能体。",
|
||||
"parameters": {"type": "object", "properties": _inject_intent({})},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
MULTI_AGENT_SUB_AGENT_TOOLS: List[Dict[str, Any]] = [
|
||||
_sub_tool_ask_master(),
|
||||
_sub_tool_ask_other_agent(),
|
||||
_sub_tool_answer_other_agent(),
|
||||
_sub_tool_list_active_sub_agents(),
|
||||
]
|
||||
|
||||
|
||||
# ----- 构造函数(供现有 tools_definition 调用) -----
|
||||
def build_master_tools_for_conversation() -> List[Dict[str, Any]]:
|
||||
"""返回主智能体在多智能体模式下应额外提供的工具列表。"""
|
||||
return list(MULTI_AGENT_MASTER_TOOLS)
|
||||
|
||||
|
||||
def build_sub_agent_tools_for_role() -> List[Dict[str, Any]]:
|
||||
"""返回子智能体在多智能体模式下应额外提供的工具列表。"""
|
||||
return list(MULTI_AGENT_SUB_AGENT_TOOLS)
|
||||
@ -56,6 +56,9 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
self.container_session: Optional["ContainerHandle"] = container_session
|
||||
self.host_execution_mode: str = "sandbox"
|
||||
self.terminal: Optional["WebTerminal"] = None
|
||||
# 多智能体模式:为每个启用 multi_agent_mode 的会话维护一个 MultiAgentState
|
||||
# key = conversation_id, value = MultiAgentState
|
||||
self.multi_agent_states: Dict[str, Any] = {}
|
||||
|
||||
self.base_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
@ -66,6 +69,8 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
self._event_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._loop_thread: Optional[threading.Thread] = None
|
||||
self._state_lock = threading.Lock()
|
||||
# agent_id -> SubAgentTask 映射(供多智能体消息注入使用)
|
||||
self._sub_agent_instances: Dict[int, Any] = {}
|
||||
|
||||
self._load_state()
|
||||
try:
|
||||
@ -137,8 +142,16 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
run_in_background: bool = False,
|
||||
model_key: Optional[str] = None,
|
||||
thinking_mode: Optional[str] = None,
|
||||
multi_agent_mode: bool = False,
|
||||
role_id: Optional[str] = None,
|
||||
display_name: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""创建子智能体任务并启动协程。"""
|
||||
"""创建子智能体任务并启动协程。
|
||||
|
||||
参数 multi_agent_mode: True 时启用多智能体模式。
|
||||
参数 role_id: 多智能体模式下的角色标诶。
|
||||
参数 display_name: 多智能体模式下的显示名(如 UI Operator_1)。
|
||||
"""
|
||||
validation_error = self._validate_create_params(agent_id, summary, task, deliverables_dir)
|
||||
if validation_error:
|
||||
return {"success": False, "error": validation_error}
|
||||
@ -214,6 +227,25 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
self._mark_agent_id_used(conversation_id, agent_id)
|
||||
self._save_state()
|
||||
|
||||
# 多智能体模式:为该会话创建或复用 MultiAgentState
|
||||
multi_agent_state = None
|
||||
if multi_agent_mode:
|
||||
multi_agent_state = self.get_or_create_multi_agent_state(conversation_id)
|
||||
# 把实例注册到 state
|
||||
from modules.multi_agent.state import AgentInstance
|
||||
inst = AgentInstance(
|
||||
agent_id=agent_id,
|
||||
role_id=role_id or "",
|
||||
display_name=display_name or f"Agent_{agent_id}",
|
||||
task_id=task_id,
|
||||
status="running",
|
||||
summary=summary,
|
||||
)
|
||||
try:
|
||||
multi_agent_state.register_instance(inst)
|
||||
except ValueError:
|
||||
return {"success": False, "error": f"agent_id {agent_id} 已在该会话中使用"}
|
||||
|
||||
sub_agent = SubAgentTask(
|
||||
manager=self,
|
||||
task_record=task_record,
|
||||
@ -221,19 +253,30 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
system_prompt=system_prompt,
|
||||
model_key=model_key,
|
||||
thinking_mode=thinking_mode,
|
||||
multi_agent_mode=multi_agent_mode,
|
||||
multi_agent_state=multi_agent_state,
|
||||
display_name=display_name,
|
||||
)
|
||||
task_coro = sub_agent.run()
|
||||
asyncio_task = self._run_coro(task_coro)
|
||||
sub_agent._task = asyncio_task
|
||||
self._running_tasks[task_id] = asyncio_task
|
||||
# 缓存 sub_agent 实例供给多智能体模式 Poli注入使用
|
||||
self._sub_agent_instances[agent_id] = sub_agent
|
||||
|
||||
def _on_done(fut):
|
||||
self._running_tasks.pop(task_id, None)
|
||||
self._sub_agent_instances.pop(agent_id, None)
|
||||
self.reconcile_task_states(conversation_id=conversation_id)
|
||||
# 多智能体模式:结束时把状态写回 MultiAgentState
|
||||
if multi_agent_mode and multi_agent_state:
|
||||
self._on_multi_agent_task_done(task_id, agent_id, multi_agent_state, sub_agent)
|
||||
|
||||
asyncio_task.add_done_callback(_on_done)
|
||||
|
||||
message = f"子智能体{agent_id} 已创建,任务ID: {task_id}"
|
||||
if multi_agent_mode and display_name:
|
||||
message = f"{display_name} 已创建,任务ID: {task_id}"
|
||||
print(f"{OUTPUT_FORMATS['info']} {message}")
|
||||
|
||||
return {
|
||||
@ -244,6 +287,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
"message": message,
|
||||
"deliverables_dir": str(deliverables_path),
|
||||
"run_in_background": run_in_background,
|
||||
"display_name": display_name,
|
||||
}
|
||||
|
||||
def wait_for_completion(
|
||||
@ -482,6 +526,8 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
return {"success": False, "error": "子智能体管理器未绑定终端,无法执行工具"}
|
||||
|
||||
try:
|
||||
# 多智能体模式常见问答工具已在 SubAgentTask._execute_multi_agent_tool 中处理
|
||||
# 这里只处理实际通过主进程执行的工具
|
||||
if tool_name == "search_workspace":
|
||||
return await handle_search_workspace(self.project_path, self.terminal, arguments)
|
||||
if tool_name == "read_mediafile":
|
||||
@ -497,6 +543,69 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
logger.exception(f"[SubAgent] 工具执行异常: {tool_name}")
|
||||
return {"success": False, "error": f"工具执行异常: {exc}"}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 多智能体模式:状态管理、外部接口、消息注入
|
||||
# ------------------------------------------------------------------
|
||||
def get_or_create_multi_agent_state(self, conversation_id: str):
|
||||
"""获取或为该会话创建 MultiAgentState。"""
|
||||
from modules.multi_agent.state import MultiAgentState
|
||||
state = self.multi_agent_states.get(conversation_id)
|
||||
if state:
|
||||
return state
|
||||
state = MultiAgentState(conversation_id=conversation_id)
|
||||
self.multi_agent_states[conversation_id] = state
|
||||
return state
|
||||
|
||||
def get_multi_agent_state(self, conversation_id: str):
|
||||
"""获取该会话的多智能体状态。"""
|
||||
return self.multi_agent_states.get(conversation_id)
|
||||
|
||||
def drop_multi_agent_state(self, conversation_id: str) -> None:
|
||||
"""删除会话状态(会话结束时调用)。"""
|
||||
self.multi_agent_states.pop(conversation_id, None)
|
||||
|
||||
def inject_message_to_sub_agent(self, agent_id: int, message_text: str) -> bool:
|
||||
"""同事件循环中向子智能体上下文插入 user 消息。
|
||||
|
||||
适用于 ask_other_agent / send_message_to_sub_agent / answer_sub_agent_question_
|
||||
(非阻塞到工具结果的路径)。返回 True 表示成功注入。
|
||||
"""
|
||||
# 查找该 agent_id 对应的 running SubAgentTask
|
||||
sub_agent = self._find_sub_agent_task_by_agent_id(agent_id)
|
||||
if not sub_agent:
|
||||
return False
|
||||
sub_agent.messages.append({"role": "user", "content": message_text})
|
||||
return True
|
||||
|
||||
def _find_sub_agent_task_by_agent_id(self, agent_id: int) -> Optional[Any]:
|
||||
"""通过遍历创建中的 task 查找活 SubAgentTask 实例。
|
||||
|
||||
这是个 helper:在主实现中我们需要保留从 agent_id 到 SubAgentTask 的引用。
|
||||
理论上可以在 create_sub_agent 时把 sub_agent 存起来,这里使用 rs safer贪心法:
|
||||
遊历 _running_tasks 不为可行,因为 asyncio.Task 不抽不包含 SubAgentTask引用。
|
||||
我们改为 `SubAgentTask` 对象列表供查询。
|
||||
"""
|
||||
# 优先查缓存:create_sub_agent 时的字段
|
||||
for inst in self._sub_agent_instances.values():
|
||||
if inst.agent_id == agent_id:
|
||||
return inst
|
||||
return None
|
||||
|
||||
def _on_multi_agent_task_done(self, task_id: str, agent_id: int, state: Any, sub_agent: Any) -> None:
|
||||
"""SubAgentTask 结束回调会调这个更新 MultiAgentState 实例状态。"""
|
||||
# 取出当前 status(由 _finalize_task 设置)
|
||||
final_task = self.tasks.get(task_id) or {}
|
||||
final_status = final_task.get("status") or "terminated"
|
||||
# 允许的自然退出:running -> idle(未走 _finalize) vs failed/timeout
|
||||
# SubAgentTask 在多智能体模式下没有 _finalize_task,我们手动赋值 idle
|
||||
# 如果状态被结 _finalize 为 failed/timeout,则保持该状态
|
||||
if final_status in TERMINAL_STATUSES:
|
||||
state.mark_status(agent_id, final_status, last_output=str(final_task.get("final_result") or ""))
|
||||
elif final_status == "terminated":
|
||||
state.mark_status(agent_id, "terminated")
|
||||
else:
|
||||
state.mark_status(agent_id, "idle")
|
||||
|
||||
def _get_runtime_path(self, host_path: Path) -> str:
|
||||
"""将宿主机路径映射为容器内路径(仅用于提示展示)。"""
|
||||
if not self.container_session or getattr(self.container_session, "mode", None) != "docker":
|
||||
|
||||
@ -21,9 +21,19 @@ from utils.logger import setup_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from modules.sub_agent.manager import SubAgentManager
|
||||
from modules.multi_agent.state import MultiAgentState
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
# 多智能体模式下额外加载的工具定义
|
||||
def _load_multi_agent_sub_agent_tools() -> List[Dict[str, Any]]:
|
||||
try:
|
||||
from modules.multi_agent.tools import build_sub_agent_tools_for_role
|
||||
return build_sub_agent_tools_for_role()
|
||||
except Exception as exc:
|
||||
logger.warning(f"[SubAgentTask] 加载多智能体工具失败: {exc}")
|
||||
return []
|
||||
|
||||
|
||||
class SubAgentTask:
|
||||
"""单个后台子智能体任务。"""
|
||||
@ -36,6 +46,10 @@ class SubAgentTask:
|
||||
system_prompt: str,
|
||||
model_key: Optional[str],
|
||||
thinking_mode: Optional[str],
|
||||
*,
|
||||
multi_agent_mode: bool = False,
|
||||
multi_agent_state: Optional["MultiAgentState"] = None,
|
||||
display_name: Optional[str] = None,
|
||||
):
|
||||
self.manager = manager
|
||||
self.task_record = task_record
|
||||
@ -73,6 +87,12 @@ class SubAgentTask:
|
||||
self._cancelled = False
|
||||
self._task: Optional[asyncio.Task] = None
|
||||
|
||||
# 多智能体模式相关字段
|
||||
self.multi_agent_mode = bool(multi_agent_mode)
|
||||
self.multi_agent_state = multi_agent_state
|
||||
# display_name 不传时回退为 'Agent_{agent_id}'
|
||||
self.display_name = display_name or f"Agent_{self.agent_id}"
|
||||
|
||||
def emit(self, type_: str, data: Dict[str, Any]) -> None:
|
||||
"""输出一行 JSONL 到 progress 文件并缓存。"""
|
||||
line = json.dumps({"type": type_, **data}, ensure_ascii=False)
|
||||
@ -100,8 +120,13 @@ class SubAgentTask:
|
||||
|
||||
async def _run_loop(self) -> None:
|
||||
client, model_key = self._build_client()
|
||||
tools = list(SUB_AGENT_TOOLS)
|
||||
tools.append(FINISH_TOOL)
|
||||
if self.multi_agent_mode:
|
||||
tools = list(SUB_AGENT_TOOLS)
|
||||
tools.extend(_load_multi_agent_sub_agent_tools())
|
||||
# 多智能体模式下不要求 finish_task,自然输出结束即本轮任务结束
|
||||
else:
|
||||
tools = list(SUB_AGENT_TOOLS)
|
||||
tools.append(FINISH_TOOL)
|
||||
|
||||
start_time = time.time()
|
||||
max_turns = 50
|
||||
@ -123,6 +148,10 @@ class SubAgentTask:
|
||||
if usage:
|
||||
self._apply_usage(usage)
|
||||
|
||||
# 多智能体模式:把 assistant 文本输出作为进度/完成 output 转发到主对话
|
||||
if self.multi_agent_mode and self.multi_agent_state and assistant_message.strip():
|
||||
self._forward_output_to_master(assistant_message)
|
||||
|
||||
final_message: Dict[str, Any] = {"role": "assistant", "content": assistant_message}
|
||||
if reasoning:
|
||||
final_message["reasoning_content"] = reasoning
|
||||
@ -131,6 +160,11 @@ class SubAgentTask:
|
||||
self.messages.append(final_message)
|
||||
|
||||
if not tool_calls:
|
||||
# 多智能体模式:没有 tool_calls 表示本轮结束,进入 idle 状态
|
||||
if self.multi_agent_mode:
|
||||
self._mark_idle()
|
||||
return
|
||||
# 普通模式:prompt 并要求继续 / finish_task
|
||||
self.messages.append({
|
||||
"role": "user",
|
||||
"content": "如果你已经完成了任务,请调用 finish_task 工具提交完成报告。如果还没有完成,请继续执行任务。",
|
||||
@ -165,6 +199,26 @@ class SubAgentTask:
|
||||
|
||||
await self._write_failure("任务执行超过最大轮次限制", max_turns_exceeded=True)
|
||||
|
||||
def _forward_output_to_master(self, output_text: str) -> None:
|
||||
"""把子智能体的 assistant 文本输出转发成主对话的 user 消息。"""
|
||||
if not self.multi_agent_state:
|
||||
return
|
||||
try:
|
||||
from modules.multi_agent.state import build_sub_agent_output_text
|
||||
msg = build_sub_agent_output_text(self.display_name, output_text.strip())
|
||||
self.multi_agent_state.push_master_message(msg)
|
||||
# 同时记录到实例状态,供 list_active_sub_agents 使用
|
||||
inst = self.multi_agent_state.get_instance(self.agent_id)
|
||||
if inst:
|
||||
inst.last_output = output_text[:500]
|
||||
except Exception as exc:
|
||||
logger.warning(f"[SubAgentTask] forward output to master failed: {exc}")
|
||||
|
||||
def _mark_idle(self) -> None:
|
||||
"""多智能体模式下,子智能体自然结束']=本轮任务 结束,进入 idle 状态。"""
|
||||
if self.multi_agent_state:
|
||||
self.multi_agent_state.mark_status(self.agent_id, "idle")
|
||||
|
||||
def _build_client(self) -> tuple:
|
||||
"""加载模型配置并初始化 DeepSeekClient。"""
|
||||
config_path = self.manager.models_config_file
|
||||
@ -262,9 +316,78 @@ class SubAgentTask:
|
||||
return {"_raw": raw}
|
||||
|
||||
async def _execute_tool(self, name: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""通过 manager 调用主进程执行工具。"""
|
||||
"""通过 manager 调用主进程执行工具。
|
||||
|
||||
多智能体模式下,对于通信工具(ask_master / ask_other_agent / answer_other_agent/
|
||||
list_active_sub_agents)在主进程内直接处理,不再转发到 WebTerminal。
|
||||
"""
|
||||
if self.multi_agent_mode and self.multi_agent_state:
|
||||
result = await self._execute_multi_agent_tool(name, args)
|
||||
if result is not None:
|
||||
return result
|
||||
return await self.manager.execute_tool_for_sub_agent(name, args)
|
||||
|
||||
async def _execute_multi_agent_tool(self, name: str, args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""处理多智能体模式专属的通信工具。返回 None 表示不 属于多智能体工具。"""
|
||||
state = self.multi_agent_state
|
||||
if not state:
|
||||
return None
|
||||
try:
|
||||
if name == "ask_master":
|
||||
question = str(args.get("question") or "").strip()
|
||||
question_id = str(args.get("question_id") or f"ask_master_{uuid.uuid4().hex[:10]}")
|
||||
if not question:
|
||||
return {"success": False, "error": "question 不能为空"}
|
||||
# 插入到主对话
|
||||
from modules.multi_agent.state import build_sub_agent_ask_master_text
|
||||
msg = build_sub_agent_ask_master_text(self.display_name, question, question_id)
|
||||
state.push_master_message(msg)
|
||||
inst = state.get_instance(self.agent_id)
|
||||
if inst:
|
||||
inst.last_output = f"[ask_master] {question[:200]}"
|
||||
# 阻塞等待回答(状态标为正在等待主智能体回答)
|
||||
state.mark_status(self.agent_id, "running")
|
||||
answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600))
|
||||
state.mark_status(self.agent_id, "running")
|
||||
return {"success": True, "answer": answer, "question_id": question_id}
|
||||
|
||||
if name == "ask_other_agent":
|
||||
target_id = int(args.get("target_agent_id") or 0)
|
||||
question = str(args.get("question") or "").strip()
|
||||
question_id = str(args.get("question_id") or f"ask_other_{uuid.uuid4().hex[:10]}")
|
||||
if not target_id or not question:
|
||||
return {"success": False, "error": "参数缺失"}
|
||||
# 查找目标实例
|
||||
target_inst = state.get_instance(target_id)
|
||||
if not target_inst:
|
||||
return {"success": False, "error": f"agent {target_id} 不存在"}
|
||||
# 构造提问消息并插入到目标子对话;同时要求其在下一轮调用 answer_other_agent
|
||||
from modules.multi_agent.state import build_sub_agent_ask_other_text
|
||||
target_display = target_inst.display_name
|
||||
msg = build_sub_agent_ask_other_text(self.display_name, target_display, question, question_id)
|
||||
self.manager.inject_message_to_sub_agent(target_id, msg)
|
||||
# 阻塞等待回答
|
||||
answer = await state.wait_for_answer(question_id, self.agent_id, timeout=float(args.get("timeout_seconds") or 600))
|
||||
return {"success": True, "answer": answer, "question_id": question_id}
|
||||
|
||||
if name == "answer_other_agent":
|
||||
source_id = int(args.get("source_agent_id") or 0)
|
||||
question_id = str(args.get("question_id") or "")
|
||||
answer = str(args.get("answer") or "").strip()
|
||||
if not question_id or not answer:
|
||||
return {"success": False, "error": "参数缺失"}
|
||||
ok = state.provide_answer(question_id, answer)
|
||||
return {"success": bool(ok), "question_id": question_id}
|
||||
|
||||
if name == "list_active_sub_agents":
|
||||
return {"success": True, "agents": [a.to_dict() for a in state.list_all()]}
|
||||
except asyncio.TimeoutError:
|
||||
return {"success": False, "error": "等待回答超时", "question_id": args.get("question_id")}
|
||||
except Exception as exc:
|
||||
logger.exception(f"[SubAgent] 多智能体工具异常: {name}")
|
||||
return {"success": False, "error": f"多智能体工具异常: {exc}"}
|
||||
return None
|
||||
|
||||
def _update_stats(self, name: str) -> None:
|
||||
if name == "read_file":
|
||||
self.stats["files_read"] += 1
|
||||
|
||||
@ -33,6 +33,7 @@ from server.usage import usage_bp
|
||||
from server.status import status_bp
|
||||
from server.tasks import tasks_bp
|
||||
from server.api_v1 import api_v1_bp
|
||||
from server.multi_agent import multi_agent_bp
|
||||
from server.socket_handlers import socketio
|
||||
from server.security import attach_security_hooks
|
||||
from werkzeug.utils import secure_filename
|
||||
@ -296,6 +297,7 @@ app.register_blueprint(usage_bp)
|
||||
app.register_blueprint(status_bp)
|
||||
app.register_blueprint(tasks_bp)
|
||||
app.register_blueprint(api_v1_bp)
|
||||
app.register_blueprint(multi_agent_bp)
|
||||
|
||||
# 安全钩子(CSRF 校验 + 响应头)
|
||||
attach_security_hooks(app)
|
||||
|
||||
199
server/multi_agent.py
Normal file
199
server/multi_agent.py
Normal file
@ -0,0 +1,199 @@
|
||||
"""多智能体模式 server 路由。
|
||||
|
||||
- `/multiagent/new` 返回主 SPA 入口(与 `/new` 一样返回 static/index.html)
|
||||
前端通过路径识别多智能体模式后,在创建对话时写入 metadata.multi_agent_mode=true
|
||||
- `/api/multiagent/conversations` POST 创建多智能体对话(写入 metadata)
|
||||
- `/api/multiagent/roles` GET 列出可用角色
|
||||
- `/api/multiagent/roles` POST 创建自定义角色
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from flask import Blueprint, current_app, jsonify, request, session
|
||||
|
||||
from server.auth_helpers import api_login_required, get_current_username
|
||||
from server.context import get_user_resources
|
||||
|
||||
multi_agent_bp = Blueprint("multi_agent", __name__)
|
||||
|
||||
|
||||
@multi_agent_bp.route("/multiagent/new")
|
||||
@api_login_required
|
||||
def multi_agent_new_page():
|
||||
"""多智能体模式入口,返回与 /new 相同的 SPA index.html。"""
|
||||
return current_app.send_static_file("index.html")
|
||||
|
||||
|
||||
@multi_agent_bp.route("/api/multiagent/roles", methods=["GET"])
|
||||
@api_login_required
|
||||
def list_roles_api():
|
||||
"""列出全部可用角色(预置+自定义)。"""
|
||||
try:
|
||||
from modules.multi_agent.role_store import list_roles
|
||||
roles = list_roles()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"roles": [r.to_dict() for r in roles],
|
||||
})
|
||||
except Exception as exc:
|
||||
return jsonify({"success": False, "error": str(exc)}), 500
|
||||
|
||||
|
||||
@multi_agent_bp.route("/api/multiagent/roles", methods=["POST"])
|
||||
@api_login_required
|
||||
def create_role_api():
|
||||
"""创建自定义角色。body: { role_id, name, description?, body_prompt, thinking_mode? }"""
|
||||
try:
|
||||
from modules.multi_agent.role_store import RoleConfig, save_custom_role, list_roles
|
||||
data = request.get_json() or {}
|
||||
role_id = str(data.get("role_id") or "").strip()
|
||||
name = str(data.get("name") or "").strip()
|
||||
body_prompt = str(data.get("body_prompt") or "").strip()
|
||||
description = str(data.get("description") or "").strip()
|
||||
thinking_mode = str(data.get("thinking_mode") or "fast").strip()
|
||||
if not role_id or not name or not body_prompt:
|
||||
return jsonify({"success": False, "error": "role_id/name/body_prompt 必填"}), 400
|
||||
if thinking_mode not in {"fast", "thinking"}:
|
||||
thinking_mode = "fast"
|
||||
# 不允许覆盖已存在的同名角色
|
||||
existing = {r.role_id for r in list_roles()}
|
||||
if role_id in existing:
|
||||
return jsonify({"success": False, "error": f"角色 {role_id} 已存在"}), 409
|
||||
role = RoleConfig(
|
||||
role_id=role_id,
|
||||
name=name,
|
||||
description=description,
|
||||
body_prompt=body_prompt,
|
||||
thinking_mode=thinking_mode,
|
||||
)
|
||||
saved = save_custom_role(role)
|
||||
return jsonify({"success": True, "role_id": role_id, "file": str(saved)})
|
||||
except Exception as exc:
|
||||
return jsonify({"success": False, "error": str(exc)}), 500
|
||||
|
||||
|
||||
@multi_agent_bp.route("/api/multiagent/conversations", methods=["POST"])
|
||||
@api_login_required
|
||||
def create_multi_agent_conversation():
|
||||
"""创建多智能体模式对话。在 metadata 中写入 multi_agent_mode=true。
|
||||
|
||||
body: { workspace_id?, thinking_mode?, run_mode?, preserve_mode? }
|
||||
"""
|
||||
import time as _time
|
||||
from server.conversation import _get_active_workspace_task, _resolve_target_terminal_for_workspace
|
||||
from modules.personalization_manager import load_personalization_config
|
||||
try:
|
||||
from server.user_workspace import UserWorkspace # noqa
|
||||
except Exception:
|
||||
UserWorkspace = None # type: ignore
|
||||
|
||||
username = get_current_username()
|
||||
if not username:
|
||||
return jsonify({"success": False, "error": "未登录"}), 401
|
||||
|
||||
data = request.get_json() or {}
|
||||
target_workspace_id = (data.get("workspace_id") or "").strip()
|
||||
|
||||
terminal, workspace = get_user_resources(username)
|
||||
if not terminal or not workspace:
|
||||
return jsonify({"success": False, "error": "工作区未就绪"}), 503
|
||||
|
||||
if target_workspace_id:
|
||||
try:
|
||||
terminal, workspace = _resolve_target_terminal_for_workspace(
|
||||
username, target_workspace_id, terminal, workspace
|
||||
)
|
||||
except ValueError as exc:
|
||||
return jsonify({"success": False, "error": str(exc)}), 404
|
||||
except RuntimeError as exc:
|
||||
return jsonify({"success": False, "error": str(exc)}), 503
|
||||
|
||||
preserve_mode = bool(data.get("preserve_mode"))
|
||||
thinking_mode = data.get("thinking_mode") if preserve_mode and "thinking_mode" in data else None
|
||||
run_mode = data.get("mode") if preserve_mode and "mode" in data else None
|
||||
|
||||
effective_workspace_id = target_workspace_id or session.get("workspace_id") or "default"
|
||||
active_task = _get_active_workspace_task(username=username, workspace_id=effective_workspace_id)
|
||||
|
||||
try:
|
||||
prefs = load_personalization_config(workspace.data_dir)
|
||||
except Exception:
|
||||
prefs = {}
|
||||
|
||||
cm = getattr(getattr(terminal, "context_manager", None), "conversation_manager", None)
|
||||
if not cm:
|
||||
return jsonify({"success": False, "error": "对话管理器未初始化"}), 500
|
||||
|
||||
safe_run_mode = run_mode
|
||||
if safe_run_mode not in {"fast", "thinking", "deep"}:
|
||||
candidate = (prefs or {}).get("default_run_mode")
|
||||
safe_run_mode = candidate if candidate in {"fast", "thinking", "deep"} else "fast"
|
||||
safe_thinking = bool(thinking_mode) if thinking_mode is not None else safe_run_mode != "fast"
|
||||
default_permission_mode = (prefs or {}).get("default_permission_mode")
|
||||
if default_permission_mode not in ("readonly", "approval", "auto_approval", "unrestricted"):
|
||||
default_permission_mode = None
|
||||
|
||||
previous_cm_current = getattr(cm, "current_conversation_id", None)
|
||||
|
||||
conversation_id = cm.create_conversation(
|
||||
project_path=str(workspace.project_path),
|
||||
thinking_mode=safe_thinking,
|
||||
run_mode=safe_run_mode,
|
||||
initial_messages=[],
|
||||
model_key=(prefs or {}).get("default_model") or getattr(terminal, "model_key", None),
|
||||
metadata_overrides={
|
||||
"permission_mode": default_permission_mode or getattr(terminal, "get_permission_mode", lambda: "unrestricted")(),
|
||||
"execution_mode": getattr(terminal, "get_execution_mode", lambda: "sandbox")(),
|
||||
"multi_agent_mode": True,
|
||||
},
|
||||
)
|
||||
try:
|
||||
cm.current_conversation_id = previous_cm_current
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 触发对话列表更新事件
|
||||
try:
|
||||
from server.app_legacy import socketio
|
||||
socketio.emit('conversation_list_update', {
|
||||
'action': 'created',
|
||||
'conversation_id': conversation_id,
|
||||
}, room=f"user_{username}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"conversation_id": conversation_id,
|
||||
"multi_agent_mode": True,
|
||||
}), 201
|
||||
|
||||
|
||||
@multi_agent_bp.route("/api/multiagent/active_sub_agents", methods=["GET"])
|
||||
@api_login_required
|
||||
def list_active_sub_agents_api():
|
||||
"""查询当前会话所有子智能体实例(多智能体模式专用)。"""
|
||||
username = get_current_username()
|
||||
if not username:
|
||||
return jsonify({"success": False, "error": "未登录"}), 401
|
||||
conversation_id = (request.args.get("conversation_id") or "").strip()
|
||||
if not conversation_id:
|
||||
return jsonify({"success": False, "error": "缺少 conversation_id 参数"}), 400
|
||||
terminal, _ = get_user_resources(username)
|
||||
if not terminal:
|
||||
return jsonify({"success": False, "error": "工作区未就绪"}), 503
|
||||
sub_agent_manager = getattr(terminal, "sub_agent_manager", None)
|
||||
if not sub_agent_manager:
|
||||
return jsonify({"success": False, "error": "子智能体管理器未就绪"}), 503
|
||||
state = sub_agent_manager.get_multi_agent_state(conversation_id)
|
||||
if not state:
|
||||
return jsonify({"success": True, "agents": []})
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"agents": [a.to_dict() for a in state.list_all()],
|
||||
})
|
||||
|
||||
|
||||
__all__ = ["multi_agent_bp"]
|
||||
@ -31,6 +31,88 @@ export const routeMethods = {
|
||||
this.currentConversationTitle = '';
|
||||
this.titleTypingText = '';
|
||||
const path = window.location.pathname.replace(/^\/+/, '');
|
||||
// 检查多智能体模式入口
|
||||
if (path === 'multiagent/new' || path === 'multiagent') {
|
||||
this.multiAgentMode = true;
|
||||
this.currentConversationId = null;
|
||||
this.currentConversationTitle = '多智能体模式';
|
||||
this.titleReady = true;
|
||||
this.suppressTitleTyping = false;
|
||||
this.startTitleTyping('多智能体模式', { animate: false });
|
||||
this.initialRouteResolved = true;
|
||||
this.refreshBlankHeroState();
|
||||
// 多智能体模式下自动创建一个带 metadata.multi_agent_mode=true 的新对话
|
||||
try {
|
||||
const resp = await fetch('/api/multiagent/conversations', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result && result.success && result.conversation_id) {
|
||||
this.currentConversationId = result.conversation_id;
|
||||
// 拉取完整会话信息
|
||||
try {
|
||||
const loadResp = await fetch(`/api/conversations/${result.conversation_id}/load`, { method: 'PUT' });
|
||||
const loadResult = await loadResp.json();
|
||||
if (loadResult.success) {
|
||||
if (typeof loadResult.run_mode === 'string') {
|
||||
this.runMode = loadResult.run_mode;
|
||||
this.thinkingMode = typeof loadResult.thinking_mode === 'boolean' ? loadResult.thinking_mode : loadResult.run_mode !== 'fast';
|
||||
}
|
||||
if (typeof loadResult.model_key === 'string' && loadResult.model_key) this.modelSet(loadResult.model_key);
|
||||
this.currentConversationTitle = loadResult.title || '多智能体模式';
|
||||
this.startTitleTyping(this.currentConversationTitle, { animate: false });
|
||||
history.replaceState({ conversationId: result.conversation_id }, '', `/multiagent/${result.conversation_id.replace(/^conv_/, '')}`);
|
||||
this.logMessageState('bootstrapRoute:multi-agent-loaded');
|
||||
}
|
||||
} catch (_e) {
|
||||
// 加载失败也继续,主对话 已创建
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[multiagent] 初始化多智能体对话失败:', error);
|
||||
}
|
||||
await this.restoreComposerDraftState('bootstrap-route:multiagent');
|
||||
return;
|
||||
}
|
||||
// 当 URL 是 /multiagent/conv_xxx 时也走多智能体模式
|
||||
if (path.startsWith('multiagent/')) {
|
||||
this.multiAgentMode = true;
|
||||
const convPart = path.slice('multiagent/'.length);
|
||||
const convId = convPart.startsWith('conv_') ? convPart : `conv_${convPart}`;
|
||||
try {
|
||||
const resp = await fetch(`/api/conversations/${convId}/load`, { method: 'PUT' });
|
||||
const result = await resp.json();
|
||||
if (result.success) {
|
||||
if (typeof result.run_mode === 'string') {
|
||||
this.runMode = result.run_mode;
|
||||
this.thinkingMode = typeof result.thinking_mode === 'boolean' ? result.thinking_mode : result.run_mode !== 'fast';
|
||||
} else if (typeof result.thinking_mode === 'boolean') {
|
||||
this.thinkingMode = result.thinking_mode;
|
||||
this.runMode = result.thinking_mode ? 'thinking' : 'fast';
|
||||
}
|
||||
if (typeof result.model_key === 'string' && result.model_key) this.modelSet(result.model_key);
|
||||
this.currentConversationId = convId;
|
||||
this.currentConversationTitle = result.title || '多智能体模式';
|
||||
this.titleReady = true;
|
||||
this.suppressTitleTyping = false;
|
||||
this.startTitleTyping(this.currentConversationTitle, { animate: false });
|
||||
history.replaceState({ conversationId: convId }, '', `/multiagent/${this.stripConversationPrefix(convId)}`);
|
||||
this.initialRouteResolved = true;
|
||||
await this.restoreComposerDraftState('bootstrap-route:multiagent-existing');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[multiagent] 加载多智能体对话失败:', error);
|
||||
}
|
||||
// 加载失败回退到 multiagent/new 路径
|
||||
history.replaceState({}, '', '/multiagent/new');
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
// 非多智能体模式:清除标志
|
||||
this.multiAgentMode = false;
|
||||
if (!path || this.isExplicitNewConversationRoute()) {
|
||||
this.currentConversationId = null;
|
||||
this.currentConversationTitle = '新对话';
|
||||
|
||||
@ -6,6 +6,8 @@ export function dataState() {
|
||||
// 路由相关
|
||||
initialRouteResolved: false,
|
||||
dropToolEvents: false,
|
||||
// 多智能体模式开关
|
||||
multiAgentMode: false,
|
||||
|
||||
// 轮询模式标志(禁用 WebSocket 事件处理)
|
||||
usePollingMode: true,
|
||||
|
||||
@ -38,6 +38,14 @@
|
||||
宿主机模式(免登录)
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="auth-secondary-button auth-tertiary-button"
|
||||
@click="enterMultiAgent"
|
||||
title="多智能体协作 beta"
|
||||
>
|
||||
多智能体模式(beta)
|
||||
</button>
|
||||
|
||||
<div class="auth-error">{{ error }}</div>
|
||||
<div class="auth-link">还没有账号?<a href="/register">点击注册</a></div>
|
||||
</section>
|
||||
@ -96,6 +104,11 @@ const login = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const enterMultiAgent = () => {
|
||||
// 直接跳转,后端 @login_required 会确保已登录后重定向回来
|
||||
window.location.href = '/multiagent/new';
|
||||
};
|
||||
|
||||
const hostLogin = async () => {
|
||||
hostSubmitting.value = true;
|
||||
error.value = '';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user