Initial commit of integrated agent system

This commit is contained in:
cillin
2026-03-30 17:46:44 +08:00
commit 0fa413380c
337 changed files with 75268 additions and 0 deletions

View File

@@ -0,0 +1,543 @@
# -*- 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 <message> 命令 - 保存内容到MEMORY.md"""
async def handle(self, ctx: CommandContext) -> CommandResult:
message = ctx.args.strip()
if not message:
return CommandResult(
success=False,
message="Usage: /save <message>\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 <name> 启用技能")
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 <name> 命令 - 启用技能"""
async def handle(self, ctx: CommandContext) -> CommandResult:
skill_name = ctx.args.strip()
if not skill_name:
return CommandResult(
success=False,
message="Usage: /skills enable <skill_name>\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 <name> 命令 - 禁用技能"""
async def handle(self, ctx: CommandContext) -> CommandResult:
skill_name = ctx.args.strip()
if not skill_name:
return CommandResult(
success=False,
message="Usage: /skills disable <skill_name>\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 <name> 命令 - 安装技能"""
async def handle(self, ctx: CommandContext) -> CommandResult:
skill_name = ctx.args.strip()
if not skill_name:
return CommandResult(
success=False,
message="Usage: /skills install <skill_name>\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 <message> - 保存内容到 MEMORY.md
/compact - 压缩记忆
/skills list - 列出已激活技能
/skills enable <name> - 启用技能
/skills disable <name>- 禁用技能
/skills install <name>- 安装技能
/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()