- Add agent core modules (agent_core, factory, registry, skill_loader) - Add runtime system for agent execution management - Add REST API for agents, workspaces, and runtime control - Add process supervisor for agent lifecycle management - Add workspace template system with agent profiles - Add frontend RuntimeView and runtime API integration - Add per-agent skill workspaces for smoke_fullstack run - Refactor skill system with active/installed separation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
544 lines
19 KiB
Python
544 lines
19 KiB
Python
# -*- 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()
|