agent-Specialization/server/multi_agent.py
JOJO 811974d6e7 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);模块导入与功能断言测试全部通过。
2026-07-12 03:26:02 +08:00

199 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""多智能体模式 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"]