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

337 lines
17 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.

import asyncio
import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
try:
from config import (
OUTPUT_FORMATS, DATA_DIR, PROMPTS_DIR, NEED_CONFIRMATION,
MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE,
MAX_READ_FILE_CHARS, READ_TOOL_DEFAULT_MAX_CHARS,
READ_TOOL_DEFAULT_CONTEXT_BEFORE, READ_TOOL_DEFAULT_CONTEXT_AFTER,
READ_TOOL_MAX_CONTEXT_BEFORE, READ_TOOL_MAX_CONTEXT_AFTER,
READ_TOOL_DEFAULT_MAX_MATCHES, READ_TOOL_MAX_MATCHES,
READ_TOOL_MAX_FILE_SIZE,
TERMINAL_SANDBOX_MOUNT_PATH,
TERMINAL_SANDBOX_MODE,
TERMINAL_SANDBOX_CPUS,
TERMINAL_SANDBOX_MEMORY,
PROJECT_MAX_STORAGE_MB,
CUSTOM_TOOLS_ENABLED,
)
except ImportError:
import sys
project_root = Path(__file__).resolve().parents[2]
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from config import (
OUTPUT_FORMATS, DATA_DIR, PROMPTS_DIR, NEED_CONFIRMATION,
MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE,
MAX_READ_FILE_CHARS, READ_TOOL_DEFAULT_MAX_CHARS,
READ_TOOL_DEFAULT_CONTEXT_BEFORE, READ_TOOL_DEFAULT_CONTEXT_AFTER,
READ_TOOL_MAX_CONTEXT_BEFORE, READ_TOOL_MAX_CONTEXT_AFTER,
READ_TOOL_DEFAULT_MAX_MATCHES, READ_TOOL_MAX_MATCHES,
READ_TOOL_MAX_FILE_SIZE,
TERMINAL_SANDBOX_MOUNT_PATH,
TERMINAL_SANDBOX_MODE,
TERMINAL_SANDBOX_CPUS,
TERMINAL_SANDBOX_MEMORY,
PROJECT_MAX_STORAGE_MB,
CUSTOM_TOOLS_ENABLED,
)
from modules.file_manager import FileManager
from modules.search_engine import SearchEngine
from modules.terminal_ops import TerminalOperator
from modules.memory_manager import MemoryManager
from modules.terminal_manager import TerminalManager
from modules.todo_manager import TodoManager
from modules.sub_agent import SubAgentManager
from modules.webpage_extractor import extract_webpage_content, tavily_extract
from modules.ocr_client import OCRClient
from modules.easter_egg_manager import EasterEggManager
from modules.personalization_manager import (
load_personalization_config,
build_personalization_prompt,
)
from modules.skills_manager import (
get_skills_catalog,
build_skills_list,
merge_enabled_skills,
build_skills_prompt,
infer_private_skills_dir,
)
from modules.custom_tool_registry import CustomToolRegistry, build_default_tool_category
from modules.custom_tool_executor import CustomToolExecutor
from modules.mcp_server_registry import build_default_mcp_category
try:
from config.limits import THINKING_FAST_INTERVAL
except ImportError:
THINKING_FAST_INTERVAL = 10
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES
from utils.api_client import DeepSeekClient
from utils.context_manager import ContextManager
from utils.tool_result_formatter import format_tool_result_for_context
from utils.logger import setup_logger
from config.model_profiles import (
get_model_profile,
get_model_prompt_replacements,
get_model_context_window,
)
logger = setup_logger(__name__)
DISABLE_LENGTH_CHECK = True
PERMISSION_MODES = {"readonly", "approval", "auto_approval", "unrestricted"}
class MainTerminalToolsPolicyMixin:
def _ensure_runtime_tool_overrides(self) -> Dict[str, bool]:
overrides = getattr(self, "runtime_tool_category_overrides", None)
if not isinstance(overrides, dict):
overrides = {}
self.runtime_tool_category_overrides = overrides
return overrides
def _prune_runtime_tool_overrides(self) -> None:
overrides = self._ensure_runtime_tool_overrides()
valid_keys = set(getattr(self, "tool_categories_map", {}).keys())
for key in list(overrides.keys()):
if key not in valid_keys:
overrides.pop(key, None)
def _apply_runtime_tool_overrides(self) -> None:
overrides = self._ensure_runtime_tool_overrides()
if not overrides:
return
categories = getattr(self, "tool_categories_map", {}) or {}
for key, value in list(overrides.items()):
if key not in categories:
overrides.pop(key, None)
continue
forced = getattr(self, "admin_forced_category_states", {}).get(key)
if isinstance(forced, bool):
continue
self.tool_category_states[key] = bool(value)
def apply_personalization_preferences(self, config: Optional[Dict[str, Any]] = None, *, apply_default_model: bool = True):
"""Apply persisted personalization settings that affect runtime behavior."""
try:
effective_config = config or load_personalization_config(self.data_dir)
except Exception:
effective_config = {}
# 工具意图开关
self.tool_intent_enabled = bool(effective_config.get("tool_intent_enabled"))
# Skill 强约束开关
self.skill_strict_terminal_enabled = bool(effective_config.get("skill_strict_terminal_enabled", False))
self.skill_strict_sub_agent_enabled = bool(effective_config.get("skill_strict_sub_agent_enabled", False))
self.skill_strict_run_command_foreground_enabled = bool(
effective_config.get("skill_strict_run_command_foreground_enabled", False)
)
self.skill_strict_run_command_background_enabled = bool(
effective_config.get("skill_strict_run_command_background_enabled", False)
)
# 解析当前启用的 skills用于强约束“仅对已启用 skill 生效”)
try:
skills_catalog = get_skills_catalog(private_dir=infer_private_skills_dir(self.data_dir))
enabled_skills = merge_enabled_skills(
effective_config.get("enabled_skills") if isinstance(effective_config, dict) else None,
skills_catalog,
effective_config.get("skills_catalog_snapshot") if isinstance(effective_config, dict) else None,
)
self.enabled_skill_ids = set(enabled_skills or [])
except Exception:
self.enabled_skill_ids = set()
interval = effective_config.get("thinking_interval")
if isinstance(interval, int) and interval > 0:
self.thinking_fast_interval = interval
else:
self.thinking_fast_interval = THINKING_FAST_INTERVAL
disabled_categories = []
raw_disabled = effective_config.get("disabled_tool_categories")
if isinstance(raw_disabled, list):
disabled_categories = [
key for key in raw_disabled
if isinstance(key, str) and key in self.tool_categories_map
]
self.default_disabled_tool_categories = disabled_categories
# 图片压缩模式传递给上下文
img_mode = effective_config.get("image_compression")
if isinstance(img_mode, str):
self.context_manager.image_compression_mode = img_mode
# Reset category states to defaults before applying overrides
for key, category in self.tool_categories_map.items():
self.tool_category_states[key] = False if key in disabled_categories else category.default_enabled
self._prune_runtime_tool_overrides()
self._apply_runtime_tool_overrides()
self._refresh_disabled_tools()
# 默认模型偏好(优先应用,再处理运行模式)
preferred_model = effective_config.get("default_model")
if apply_default_model and isinstance(preferred_model, str) and preferred_model != self.model_key:
try:
self.set_model(preferred_model)
except Exception as exc:
logger.warning("忽略无效默认模型: %s (%s)", preferred_model, exc)
preferred_mode = effective_config.get("default_run_mode")
if isinstance(preferred_mode, str):
normalized_mode = preferred_mode.strip().lower()
if normalized_mode in {"fast", "thinking", "deep"} and normalized_mode != self.run_mode:
try:
self.set_run_mode(normalized_mode)
except ValueError:
logger.warning("忽略无效默认运行模式: %s", preferred_mode)
# 静默禁用工具提示
self.silent_tool_disable = bool(effective_config.get("silent_tool_disable"))
permission_mode = effective_config.get("default_permission_mode")
if isinstance(permission_mode, str) and permission_mode in PERMISSION_MODES:
self.default_permission_mode = permission_mode
else:
self.default_permission_mode = "unrestricted"
if not getattr(self, "current_permission_mode", None):
self.current_permission_mode = self.default_permission_mode
def get_permission_mode(self) -> str:
mode = str(getattr(self, "current_permission_mode", "unrestricted") or "unrestricted")
if mode not in PERMISSION_MODES:
return "unrestricted"
return mode
def set_permission_mode(self, mode: str, *, persist: bool = True, conversation_id: Optional[str] = None) -> str:
normalized = str(mode or "").strip().lower()
if normalized not in PERMISSION_MODES:
raise ValueError("无效权限模式,仅支持 readonly / approval / auto_approval / unrestricted")
self.current_permission_mode = normalized
if not persist:
return normalized
conv_id = conversation_id or getattr(getattr(self, "context_manager", None), "current_conversation_id", None)
if conv_id and getattr(self, "context_manager", None):
try:
self.context_manager._get_conversation_manager_for_id(conv_id).update_conversation_metadata(conv_id,
{"permission_mode": normalized},
)
if self.context_manager.current_conversation_id == conv_id:
self.context_manager.conversation_metadata["permission_mode"] = normalized
except Exception:
pass
return normalized
def set_tool_category_enabled(self, category: str, enabled: bool) -> None:
"""设置工具类别的启用状态 / Toggle tool category enablement."""
categories = self.tool_categories_map
if category not in categories:
raise ValueError(f"未知的工具类别: {category}")
forced = self.admin_forced_category_states.get(category)
if isinstance(forced, bool) and forced != enabled:
raise ValueError("该类别被管理员强制为启用/禁用,无法修改")
final_enabled = bool(enabled)
self.tool_category_states[category] = final_enabled
self._ensure_runtime_tool_overrides()[category] = final_enabled
self._refresh_disabled_tools()
def set_admin_policy(
self,
categories: Optional[Dict[str, "ToolCategory"]] = None,
forced_category_states: Optional[Dict[str, Optional[bool]]] = None,
disabled_models: Optional[List[str]] = None,
) -> None:
"""应用管理员策略(工具分类、强制开关、模型禁用)。"""
if categories:
self.tool_categories_map = dict(categories)
# 保证自定义工具分类存在(仅当功能启用)
if self.custom_tools_enabled and "custom" not in self.tool_categories_map:
self.tool_categories_map["custom"] = type(next(iter(TOOL_CATEGORIES.values())))(
label="自定义工具",
tools=[],
default_enabled=True,
silent_when_disabled=False,
)
if getattr(self, "mcp_tools_enabled", False) and "mcp" not in self.tool_categories_map:
default_mcp_cat = build_default_mcp_category()
self.tool_categories_map["mcp"] = type(next(iter(TOOL_CATEGORIES.values())))(
label=default_mcp_cat["label"],
tools=["list_mcp_servers"],
default_enabled=True,
silent_when_disabled=False,
)
elif "mcp" in self.tool_categories_map:
self.tool_categories_map["mcp"].tools = ["list_mcp_servers"]
# 重新构建启用状态映射,保留已有值
new_states: Dict[str, bool] = {}
for key, cat in self.tool_categories_map.items():
if key in self.tool_category_states:
new_states[key] = self.tool_category_states[key]
else:
new_states[key] = cat.default_enabled
self.tool_category_states = new_states
# 清理已被移除的类别
for removed in list(self.tool_category_states.keys()):
if removed not in self.tool_categories_map:
self.tool_category_states.pop(removed, None)
self.admin_forced_category_states = forced_category_states or {}
self.admin_disabled_models = disabled_models or []
self._prune_runtime_tool_overrides()
self._apply_runtime_tool_overrides()
self._refresh_disabled_tools()
def get_tool_settings_snapshot(self) -> List[Dict[str, object]]:
"""获取工具类别状态快照 / Return tool category states snapshot."""
snapshot: List[Dict[str, object]] = []
categories = self.tool_categories_map
for key, category in categories.items():
forced = self.admin_forced_category_states.get(key)
enabled = self.tool_category_states.get(key, category.default_enabled)
if isinstance(forced, bool):
enabled = forced
snapshot.append({
"id": key,
"label": category.label,
"enabled": enabled,
"tools": list(category.tools),
"locked": isinstance(forced, bool),
"locked_state": forced if isinstance(forced, bool) else None,
})
return snapshot
def _refresh_disabled_tools(self) -> None:
"""刷新禁用工具列表 / Refresh disabled tool set."""
disabled: Set[str] = set()
notice: Set[str] = set()
categories = self.tool_categories_map
for key, category in categories.items():
state = self.tool_category_states.get(key, category.default_enabled)
forced = self.admin_forced_category_states.get(key)
if isinstance(forced, bool):
state = forced
if not state:
disabled.update(category.tools)
if not getattr(category, "silent_when_disabled", False):
notice.update(category.tools)
self.disabled_tools = disabled
self.disabled_notice_tools = notice
def _format_disabled_tool_notice(self) -> Optional[str]:
"""生成禁用工具提示信息 / Format disabled tool notice."""
if getattr(self, "silent_tool_disable", False):
return None
if not self.disabled_notice_tools:
return None
lines = ["=== 工具可用性提醒 ==="]
for tool_name in sorted(self.disabled_notice_tools):
lines.append(f"{tool_name}:已被用户禁用")
lines.append("=== 提示结束 ===")
return "\n".join(lines)