# -*- coding: utf-8 -*- """Agent command handler for system commands. This module handles system commands like /save, /compact, /skills, /reload, etc. 参考CoPaw设计,为EvoAgent提供命令处理能力。 """ import logging from abc import ABC, abstractmethod from dataclasses import dataclass, field from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Protocol if TYPE_CHECKING: from .agent import EvoAgent logger = logging.getLogger(__name__) @dataclass class CommandResult: """命令执行结果""" success: bool message: str data: Dict[str, Any] = field(default_factory=dict) class CommandContext: """命令执行上下文""" def __init__(self, agent: "EvoAgent", raw_query: str, args: str = ""): self.agent = agent self.raw_query = raw_query self.args = args self.config_name = getattr(agent, "config_name", "default") self.agent_id = getattr(agent, "agent_id", "unknown") class CommandHandler(ABC): """命令处理器抽象基类""" @abstractmethod async def handle(self, ctx: CommandContext) -> CommandResult: """处理命令""" pass class SaveCommandHandler(CommandHandler): """处理 /save 命令 - 保存内容到MEMORY.md""" async def handle(self, ctx: CommandContext) -> CommandResult: message = ctx.args.strip() if not message: return CommandResult( success=False, message="Usage: /save \n请提供要保存的内容。" ) try: memory_path = self._get_memory_path(ctx) memory_path.parent.mkdir(parents=True, exist_ok=True) timestamp = self._get_timestamp() entry = f"\n## {timestamp}\n\n{message}\n" with open(memory_path, "a", encoding="utf-8") as f: f.write(entry) return CommandResult( success=True, message=f"✅ 内容已保存到 MEMORY.md\n- 路径: {memory_path}\n- 长度: {len(message)} 字符", data={"path": str(memory_path), "length": len(message)} ) except Exception as e: logger.error(f"Failed to save to MEMORY.md: {e}") return CommandResult( success=False, message=f"❌ 保存失败: {str(e)}" ) def _get_memory_path(self, ctx: CommandContext) -> Path: """获取MEMORY.md路径""" from backend.agents.skills_manager import SkillsManager sm = SkillsManager() asset_dir = sm.get_agent_asset_dir(ctx.config_name, ctx.agent_id) return asset_dir / "MEMORY.md" def _get_timestamp(self) -> str: """获取当前时间戳""" from datetime import datetime return datetime.now().strftime("%Y-%m-%d %H:%M:%S") class CompactCommandHandler(CommandHandler): """处理 /compact 命令 - 压缩记忆""" async def handle(self, ctx: CommandContext) -> CommandResult: try: agent = ctx.agent memory_manager = getattr(agent, "memory_manager", None) if memory_manager is None: return CommandResult( success=False, message="❌ Memory Manager 未启用\n\n- 记忆压缩功能不可用\n- 请在配置中启用 memory_manager" ) messages = await self._get_messages(agent) if not messages: return CommandResult( success=False, message="⚠️ 没有可压缩的消息\n\n- 当前记忆为空\n- 无需执行压缩" ) compact_content = await memory_manager.compact_memory(messages) await self._update_compressed_summary(agent, compact_content) return CommandResult( success=True, message=f"✅ 记忆压缩完成\n\n- 压缩了 {len(messages)} 条消息\n- 摘要长度: {len(compact_content)} 字符", data={"message_count": len(messages), "summary_length": len(compact_content)} ) except Exception as e: logger.error(f"Failed to compact memory: {e}") return CommandResult( success=False, message=f"❌ 压缩失败: {str(e)}" ) async def _get_messages(self, agent: "EvoAgent") -> List[Any]: """获取Agent的记忆消息""" memory = getattr(agent, "memory", None) if memory is None: return [] return await memory.get_memory() if hasattr(memory, "get_memory") else [] async def _update_compressed_summary(self, agent: "EvoAgent", content: str) -> None: """更新压缩摘要""" memory = getattr(agent, "memory", None) if memory and hasattr(memory, "update_compressed_summary"): await memory.update_compressed_summary(content) class SkillsListCommandHandler(CommandHandler): """处理 /skills list 命令 - 列出已激活技能""" async def handle(self, ctx: CommandContext) -> CommandResult: try: from backend.agents.skills_manager import SkillsManager sm = SkillsManager() active_skills = sm.list_active_skill_metadata(ctx.config_name, ctx.agent_id) catalog = sm.list_agent_skill_catalog(ctx.config_name, ctx.agent_id) lines = ["📋 技能列表", ""] if active_skills: lines.append("✅ 已激活技能:") for skill in active_skills: lines.append(f" • {skill.name} - {skill.description[:50]}...") else: lines.append("⚠️ 当前没有激活的技能") lines.append("") lines.append(f"📚 可用技能总数: {len(catalog)}") lines.append("💡 使用 /skills enable 启用技能") return CommandResult( success=True, message="\n".join(lines), data={ "active_count": len(active_skills), "catalog_count": len(catalog), "active": [s.skill_name for s in active_skills] } ) except Exception as e: logger.error(f"Failed to list skills: {e}") return CommandResult( success=False, message=f"❌ 获取技能列表失败: {str(e)}" ) class SkillsEnableCommandHandler(CommandHandler): """处理 /skills enable 命令 - 启用技能""" async def handle(self, ctx: CommandContext) -> CommandResult: skill_name = ctx.args.strip() if not skill_name: return CommandResult( success=False, message="Usage: /skills enable \n请提供技能名称。" ) try: from backend.agents.skills_manager import SkillsManager sm = SkillsManager() result = sm.update_agent_skill_overrides( ctx.config_name, ctx.agent_id, enable=[skill_name] ) return CommandResult( success=True, message=f"✅ 技能已启用: {skill_name}\n\n已启用技能: {', '.join(result['enabled_skills'])}", data=result ) except Exception as e: logger.error(f"Failed to enable skill: {e}") return CommandResult( success=False, message=f"❌ 启用技能失败: {str(e)}" ) class SkillsDisableCommandHandler(CommandHandler): """处理 /skills disable 命令 - 禁用技能""" async def handle(self, ctx: CommandContext) -> CommandResult: skill_name = ctx.args.strip() if not skill_name: return CommandResult( success=False, message="Usage: /skills disable \n请提供技能名称。" ) try: from backend.agents.skills_manager import SkillsManager sm = SkillsManager() result = sm.update_agent_skill_overrides( ctx.config_name, ctx.agent_id, disable=[skill_name] ) return CommandResult( success=True, message=f"✅ 技能已禁用: {skill_name}\n\n已禁用技能: {', '.join(result['disabled_skills'])}", data=result ) except Exception as e: logger.error(f"Failed to disable skill: {e}") return CommandResult( success=False, message=f"❌ 禁用技能失败: {str(e)}" ) class SkillsInstallCommandHandler(CommandHandler): """处理 /skills install 命令 - 安装技能""" async def handle(self, ctx: CommandContext) -> CommandResult: skill_name = ctx.args.strip() if not skill_name: return CommandResult( success=False, message="Usage: /skills install \n请提供技能名称。" ) try: from backend.agents.skills_manager import SkillsManager from backend.agents.skill_loader import load_skill_from_dir sm = SkillsManager() # 查找技能源目录 source_dir = self._resolve_skill_source(sm, skill_name) if not source_dir: return CommandResult( success=False, message=f"❌ 技能未找到: {skill_name}\n\n请检查技能名称是否正确,或技能是否存在于 builtin/customized 目录。" ) # 加载并验证技能 skill_info = load_skill_from_dir(source_dir) if not skill_info: return CommandResult( success=False, message=f"❌ 技能加载失败: {skill_name}\n\n技能格式可能不正确。" ) # 安装到agent的installed目录 installed_root = sm.get_agent_installed_root(ctx.config_name, ctx.agent_id) target_dir = installed_root / skill_name import shutil if target_dir.exists(): shutil.rmtree(target_dir) shutil.copytree(source_dir, target_dir) return CommandResult( success=True, message=f"✅ 技能已安装: {skill_name}\n\n- 名称: {skill_info.get('name', skill_name)}\n- 版本: {skill_info.get('version', 'unknown')}\n- 路径: {target_dir}", data={"skill_name": skill_name, "target_dir": str(target_dir)} ) except Exception as e: logger.error(f"Failed to install skill: {e}") return CommandResult( success=False, message=f"❌ 安装技能失败: {str(e)}" ) def _resolve_skill_source(self, sm: "SkillsManager", skill_name: str) -> Optional[Path]: """解析技能源目录""" for root in [sm.customized_root, sm.builtin_root]: candidate = root / skill_name if candidate.exists() and (candidate / "SKILL.md").exists(): return candidate return None class ReloadCommandHandler(CommandHandler): """处理 /reload 命令 - 重新加载配置""" async def handle(self, ctx: CommandContext) -> CommandResult: try: agent = ctx.agent # 重新加载配置 if hasattr(agent, "reload_config"): await agent.reload_config() # 重新加载技能 from backend.agents.skills_manager import SkillsManager sm = SkillsManager() # 刷新技能同步 active_root = sm.get_agent_active_root(ctx.config_name, ctx.agent_id) if active_root.exists(): # 清除缓存,强制重新加载 import shutil for item in active_root.iterdir(): if item.is_dir(): shutil.rmtree(item) return CommandResult( success=True, message="✅ 配置已重新加载\n\n- Agent配置已刷新\n- 技能缓存已清除\n- 请重启对话以应用所有更改", data={"config_name": ctx.config_name, "agent_id": ctx.agent_id} ) except Exception as e: logger.error(f"Failed to reload config: {e}") return CommandResult( success=False, message=f"❌ 重新加载失败: {str(e)}" ) class StatusCommandHandler(CommandHandler): """处理 /status 命令 - 显示Agent状态""" async def handle(self, ctx: CommandContext) -> CommandResult: try: agent = ctx.agent lines = ["📊 Agent 状态", ""] lines.append(f"🆔 Agent ID: {ctx.agent_id}") lines.append(f"⚙️ Config: {ctx.config_name}") # 模型信息 model = getattr(agent, "model", None) if model: lines.append(f"🤖 Model: {model}") # 记忆状态 memory = getattr(agent, "memory", None) if memory: msg_count = len(getattr(memory, "content", [])) lines.append(f"💾 Memory: {msg_count} messages") # 技能状态 from backend.agents.skills_manager import SkillsManager sm = SkillsManager() active_skills = sm.list_active_skill_metadata(ctx.config_name, ctx.agent_id) lines.append(f"🔧 Active Skills: {len(active_skills)}") # 工具组状态 toolkit = getattr(agent, "toolkit", None) if toolkit: groups = getattr(toolkit, "tool_groups", {}) active_groups = [name for name, g in groups.items() if getattr(g, "active", False)] lines.append(f"🛠️ Active Tool Groups: {', '.join(active_groups) if active_groups else 'None'}") return CommandResult( success=True, message="\n".join(lines), data={ "agent_id": ctx.agent_id, "config_name": ctx.config_name, "active_skills_count": len(active_skills) } ) except Exception as e: logger.error(f"Failed to get status: {e}") return CommandResult( success=False, message=f"❌ 获取状态失败: {str(e)}" ) class HelpCommandHandler(CommandHandler): """处理 /help 命令 - 显示帮助""" async def handle(self, ctx: CommandContext) -> CommandResult: help_text = """📖 EvoAgent 命令帮助 可用命令: /save - 保存内容到 MEMORY.md /compact - 压缩记忆 /skills list - 列出已激活技能 /skills enable - 启用技能 /skills disable - 禁用技能 /skills install - 安装技能 /reload - 重新加载配置 /status - 显示Agent状态 /help - 显示此帮助信息 提示: • 所有命令以 / 开头 • 命令不区分大小写 • 使用 Tab 键可自动补全命令 """ return CommandResult(success=True, message=help_text) class AgentCommandDispatcher: """Agent命令分发器 参考CoPaw的CommandHandler设计,为EvoAgent提供统一的命令处理入口。 """ # 支持的系统命令 SYSTEM_COMMANDS = frozenset({ "save", "compact", "skills", "reload", "status", "help" }) def __init__(self): self._handlers: Dict[str, CommandHandler] = {} self._subcommands: Dict[str, Dict[str, CommandHandler]] = {} self._register_default_handlers() def _register_default_handlers(self) -> None: """注册默认命令处理器""" self._handlers["save"] = SaveCommandHandler() self._handlers["compact"] = CompactCommandHandler() self._handlers["reload"] = ReloadCommandHandler() self._handlers["status"] = StatusCommandHandler() self._handlers["help"] = HelpCommandHandler() # 子命令: /skills list/enable/disable/install self._subcommands["skills"] = { "list": SkillsListCommandHandler(), "enable": SkillsEnableCommandHandler(), "disable": SkillsDisableCommandHandler(), "install": SkillsInstallCommandHandler(), } def is_command(self, query: str | None) -> bool: """检查是否为命令 Args: query: 用户输入字符串 Returns: True 如果是系统命令 """ if not isinstance(query, str) or not query.startswith("/"): return False parts = query.strip().lstrip("/").split() if not parts: return False cmd = parts[0].lower() # 检查主命令 if cmd in self.SYSTEM_COMMANDS: return True return False async def handle(self, agent: "EvoAgent", query: str) -> CommandResult: """处理命令 Args: agent: EvoAgent实例 query: 命令字符串 Returns: 命令执行结果 """ if not self.is_command(query): return CommandResult( success=False, message=f"未知命令: {query}\n使用 /help 查看可用命令。" ) # 解析命令和参数 parts = query.strip().lstrip("/").split(maxsplit=1) cmd = parts[0].lower() args = parts[1] if len(parts) > 1 else "" logger.info(f"Processing command: {cmd}, args: {args}") # 处理子命令 (e.g., /skills list) if cmd in self._subcommands: sub_parts = args.split(maxsplit=1) sub_cmd = sub_parts[0].lower() if sub_parts else "" sub_args = sub_parts[1] if len(sub_parts) > 1 else "" handlers = self._subcommands[cmd] handler = handlers.get(sub_cmd) if handler is None: available = ", ".join(handlers.keys()) return CommandResult( success=False, message=f"未知子命令: {sub_cmd}\n可用子命令: {available}" ) ctx = CommandContext(agent, query, sub_args) return await handler.handle(ctx) # 处理主命令 handler = self._handlers.get(cmd) if handler is None: return CommandResult( success=False, message=f"命令未实现: {cmd}" ) ctx = CommandContext(agent, query, args) return await handler.handle(ctx) # 便捷函数 def create_command_dispatcher() -> AgentCommandDispatcher: """创建命令分发器实例""" return AgentCommandDispatcher()