agent-Specialization/server/multi_agent.py
JOJO 692748d567 fix(multi-agent): 修复消息渲染、分类、存储与滚动抖动
- 子智能体消息渲染为左侧对话气泡
- 多智能体通知细分为进度/完成/提问三类
- 修复多智能体会话存储到 mutiagents 目录并迁移数据
- 修复 token-statistics API 支持多智能体会话
- 修复 ma_debug 未定义报错
- 移除 .message-block content-visibility 修复滚动抖动
2026-07-12 23:09:56 +08:00

234 lines
9.7 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, 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")
@login_required
def multi_agent_new_page():
"""多智能体模式入口,返回与 /new 相同的 SPA index.html。"""
return current_app.send_static_file("index.html")
@multi_agent_bp.route("/multiagent/<path:conversation_id>")
@login_required
def multi_agent_conversation_page(conversation_id: str):
"""多智能体模式 指定会话 URL返回 SPA index.html 让前端路由处理。"""
return current_app.send_static_file("index.html")
@multi_agent_bp.route("/api/multiagent/rebuild-index", methods=["POST"])
@api_login_required
def rebuild_conversation_index_api():
"""强制从磁盘重建对话索引,补全 multi_agent_mode 等新字段。"""
try:
username = get_current_username()
if not username:
return jsonify({"success": False, "error": "未登录"}), 401
terminal, _ = get_user_resources(username)
if not terminal:
return jsonify({"success": False, "error": "工作区未就绪"}), 503
ctx_manager = getattr(terminal, "context_manager", None)
if not ctx_manager:
return jsonify({"success": False, "error": "上下文管理器未初始化"}), 503
ma_manager = getattr(ctx_manager, "multi_agent_conversation_manager", None)
if not ma_manager:
return jsonify({"success": False, "error": "多智能体对话管理器未初始化"}), 503
rebuilt = ma_manager._rebuild_index_from_files()
ma_manager._save_index(rebuilt)
return jsonify({"success": True, "index_size": len(rebuilt)})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@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 = {}
ctx_manager = getattr(terminal, "context_manager", None)
if not ctx_manager:
return jsonify({"success": False, "error": "上下文管理器未初始化"}), 500
cm = getattr(ctx_manager, "multi_agent_conversation_manager", None) or getattr(ctx_manager, "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(ctx_manager, "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,
},
)
# 恢复 context_manager 的当前对话(不要影响普通对话的 current_conversation_id
try:
ctx_manager.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"]