agent-Specialization/server/multi_agent.py

427 lines
18 KiB
Python
Raw Permalink 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
import json
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
from modules.multi_agent.role_store import (
RoleConfig,
list_roles,
load_preset_role,
save_custom_role,
delete_custom_role,
is_preset_role,
sync_preset_roles,
)
from config.paths import CUSTOM_ROLES_DIR, WEB_PRESET_ROLES_DIR
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
def _get_role_dirs() -> tuple:
"""根据 session 判断当前登录模式,返回 (runtime_dir, custom_dir)。
- host 模式runtime_dir = custom_dir = host/mutiagents/agents
- web/docker 模式runtime_dir = web/mutiagents/agents, custom_dir = users/<user>/personal/mutiagents/agents
"""
from flask import session as _session
is_host_session = bool(_session.get('host_mode'))
if is_host_session:
return CUSTOM_ROLES_DIR, CUSTOM_ROLES_DIR
# web 模式
username = get_current_username()
if not username:
return WEB_PRESET_ROLES_DIR, None
_, workspace = get_user_resources(username)
if not workspace or not workspace.data_dir:
return WEB_PRESET_ROLES_DIR, None
from modules.multi_agent.role_store import infer_custom_roles_dir
custom = infer_custom_roles_dir(workspace.data_dir)
return WEB_PRESET_ROLES_DIR, str(custom) if custom else None
@multi_agent_bp.route("/api/multiagent/roles", methods=["GET"])
@api_login_required
def list_roles_api():
"""列出全部可用角色(预置+用户自定义)。"""
try:
runtime_dir, custom_dir = _get_role_dirs()
roles = list_roles(runtime_dir=runtime_dir, custom_dir=custom_dir)
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:
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()
model_key = str(data.get("model_key") or "").strip() or None
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"
# 不允许覆盖预设角色
if is_preset_role(role_id):
return jsonify({"success": False, "error": f"不能覆盖预设角色 {role_id}"}), 409
# 不允许覆盖已存在的用户自定义角色
runtime_dir, custom_dir = _get_role_dirs()
existing = {r.role_id for r in list_roles(runtime_dir=runtime_dir, custom_dir=custom_dir)}
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,
model_key=model_key,
is_custom=True,
)
saved = save_custom_role(role, custom_dir=custom_dir)
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/roles/<role_id>", methods=["PUT"])
@api_login_required
def update_role_api(role_id: str):
"""更新用户自定义角色。不能更新预设角色。"""
try:
data = request.get_json() or {}
runtime_dir, custom_dir = _get_role_dirs()
# 加载现有角色(先查自定义覆盖,再查预设)
role = load_preset_role(role_id, runtime_dir=runtime_dir, custom_dir=custom_dir)
if not role:
return jsonify({"success": False, "error": f"角色 {role_id} 不存在"}), 404
# 更新字段
if "name" in data:
role.name = str(data["name"]).strip() or role.name
if "description" in data:
role.description = str(data["description"] or "").strip()
if "body_prompt" in data:
role.body_prompt = str(data["body_prompt"] or "").strip()
if "thinking_mode" in data:
tm = str(data["thinking_mode"] or "fast").strip()
if tm in {"fast", "thinking"}:
role.thinking_mode = tm
if "model_key" in data:
role.model_key = str(data["model_key"] or "").strip() or None
if "skills" in data:
role.skills = list(data["skills"] or [])
role.is_custom = True
saved = save_custom_role(role, custom_dir=custom_dir)
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/roles/<role_id>", methods=["DELETE"])
@api_login_required
def delete_role_api(role_id: str):
"""删除用户自定义角色。不能删除预设角色。"""
try:
if is_preset_role(role_id):
return jsonify({"success": False, "error": f"不能删除预设角色 {role_id}"}), 403
_, custom_dir = _get_role_dirs()
deleted = delete_custom_role(role_id, custom_dir=custom_dir)
if not deleted:
return jsonify({"success": False, "error": f"角色 {role_id} 不存在或无法删除"}), 404
return jsonify({"success": True, "role_id": role_id})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@multi_agent_bp.route("/api/multiagent/settings", methods=["GET"])
@api_login_required
def get_multi_agent_settings_api():
"""获取多智能体模式设置(子智能体压缩阈值等)。"""
try:
username = get_current_username()
if not username:
return jsonify({"success": False, "error": "未登录"}), 401
_, workspace = get_user_resources(username)
if not workspace:
return jsonify({"success": False, "error": "工作区未就绪"}), 503
# 从个人化配置中读取子智能体压缩阈值
from modules.personalization_manager import load_personalization_config
prefs = load_personalization_config(workspace.data_dir) or {}
compress_threshold = prefs.get("sub_agent_compress_threshold_tokens", 150000)
return jsonify({
"success": True,
"settings": {
"sub_agent_compress_threshold_tokens": compress_threshold,
},
})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@multi_agent_bp.route("/api/multiagent/settings", methods=["PUT"])
@api_login_required
def update_multi_agent_settings_api():
"""更新多智能体模式设置。"""
try:
username = get_current_username()
if not username:
return jsonify({"success": False, "error": "未登录"}), 401
_, workspace = get_user_resources(username)
if not workspace:
return jsonify({"success": False, "error": "工作区未就绪"}), 503
data = request.get_json() or {}
settings = data.get("settings") or {}
# 更新子智能体压缩阈值
threshold = settings.get("sub_agent_compress_threshold_tokens")
if threshold is not None:
threshold = int(threshold)
if threshold < 10000:
return jsonify({"success": False, "error": "压缩阈值不能小于 10000"}), 400
from modules.personalization_manager import load_personalization_config, save_personalization_config
prefs = load_personalization_config(workspace.data_dir) or {}
prefs["sub_agent_compress_threshold_tokens"] = threshold
save_personalization_config(workspace.data_dir, prefs)
return jsonify({"success": True})
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/models", methods=["GET"])
@api_login_required
def list_sub_agent_models_api():
"""列出子智能体可用的模型列表(从 sub_agent_models.json 读取,脱敏后返回)。"""
try:
from config.sub_agent import SUB_AGENT_MODELS_CONFIG_FILE
from pathlib import Path as _Path
import json as _json
config_path = SUB_AGENT_MODELS_CONFIG_FILE
if not _Path(config_path).exists():
return jsonify({"success": True, "models": [], "default_model": ""})
raw = _json.loads(_Path(config_path).read_text(encoding="utf-8"))
models_raw = raw.get("models", []) if isinstance(raw, dict) else (raw if isinstance(raw, list) else [])
default_model = str(raw.get("default_model", "")) if isinstance(raw, dict) else ""
# 脱敏:只返回 name / modes / multimodal / max_output / max_context
safe_models = []
for m in models_raw:
if not isinstance(m, dict):
continue
safe_models.append({
"name": m.get("name") or m.get("model_name") or "",
"modes": m.get("modes") or m.get("mode") or "",
"multimodal": m.get("multimodal") or "none",
"max_output": m.get("max_output") or m.get("max_tokens") or 0,
"max_context": m.get("max_context") or m.get("context_window") or 0,
})
return jsonify({
"success": True,
"models": safe_models,
"default_model": default_model,
})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@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)
agents: List[Dict[str, Any]] = []
if state:
for a in state.list_all():
d = a.to_dict()
task = sub_agent_manager.tasks.get(a.task_id)
if isinstance(task, dict):
d["last_tool"] = task.get("last_tool")
stats_file = Path(task.get("stats_file", ""))
if stats_file.exists():
try:
stats = json.loads(stats_file.read_text(encoding="utf-8"))
d["current_context_tokens"] = stats.get("current_context_tokens", 0)
except Exception:
d["current_context_tokens"] = 0
else:
d["current_context_tokens"] = 0
agents.append(d)
if not state:
return jsonify({"success": True, "agents": []})
return jsonify({
"success": True,
"agents": agents,
})
__all__ = ["multi_agent_bp"]