- 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>
412 lines
14 KiB
Python
412 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""EvoAgent - Core agent implementation for EvoTraders.
|
|
|
|
This module provides the main EvoAgent class built on AgentScope's ReActAgent,
|
|
with integrated tools, skills, and memory management based on CoPaw design.
|
|
|
|
Key features:
|
|
- Workspace-driven configuration from Markdown files
|
|
- Dynamic skill loading from skills/active directories
|
|
- Tool-guard security interception
|
|
- Hook system for extensibility
|
|
- Runtime skill and prompt reloading
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Type, TYPE_CHECKING
|
|
|
|
from agentscope.agent import ReActAgent
|
|
from agentscope.memory import InMemoryMemory
|
|
from agentscope.message import Msg
|
|
from agentscope.tool import Toolkit
|
|
|
|
from .tool_guard import ToolGuardMixin
|
|
from .hooks import (
|
|
HookManager,
|
|
BootstrapHook,
|
|
MemoryCompactionHook,
|
|
HOOK_PRE_REASONING,
|
|
)
|
|
from ..prompts.builder import (
|
|
PromptBuilder,
|
|
build_system_prompt_from_workspace,
|
|
)
|
|
from ..agent_workspace import load_agent_workspace_config
|
|
from ..skills_manager import SkillsManager
|
|
|
|
if TYPE_CHECKING:
|
|
from agentscope.formatter import FormatterBase
|
|
from agentscope.model import ModelWrapperBase
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EvoAgent(ToolGuardMixin, ReActAgent):
|
|
"""EvoAgent with integrated tools, skills, and memory management.
|
|
|
|
This agent extends ReActAgent with:
|
|
- Workspace-driven configuration from AGENTS.md/SOUL.md/PROFILE.md/etc.
|
|
- Dynamic skill loading from skills/active directories
|
|
- Tool-guard security interception (via ToolGuardMixin)
|
|
- Hook system for extensibility (bootstrap, memory compaction)
|
|
- Runtime skill and prompt reloading
|
|
|
|
MRO note
|
|
~~~~~~~~
|
|
``ToolGuardMixin`` overrides ``_acting`` and ``_reasoning`` via
|
|
Python's MRO: EvoAgent → ToolGuardMixin → ReActAgent.
|
|
|
|
Example:
|
|
agent = EvoAgent(
|
|
agent_id="fundamentals_analyst",
|
|
config_name="smoke_fullstack",
|
|
workspace_dir=Path("runs/smoke_fullstack/agents/fundamentals_analyst"),
|
|
model=model_instance,
|
|
formatter=formatter_instance,
|
|
)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
agent_id: str,
|
|
config_name: str,
|
|
workspace_dir: Path,
|
|
model: "ModelWrapperBase",
|
|
formatter: "FormatterBase",
|
|
skills_manager: Optional[SkillsManager] = None,
|
|
sys_prompt: Optional[str] = None,
|
|
max_iters: int = 10,
|
|
memory: Optional[Any] = None,
|
|
enable_tool_guard: bool = True,
|
|
enable_bootstrap_hook: bool = True,
|
|
enable_memory_compaction: bool = False,
|
|
memory_manager: Optional[Any] = None,
|
|
memory_compact_threshold: Optional[int] = None,
|
|
env_context: Optional[str] = None,
|
|
prompt_files: Optional[List[str]] = None,
|
|
):
|
|
"""Initialize EvoAgent.
|
|
|
|
Args:
|
|
agent_id: Unique identifier for this agent
|
|
config_name: Run configuration name (e.g., "smoke_fullstack")
|
|
workspace_dir: Agent workspace directory containing markdown files
|
|
model: LLM model instance
|
|
formatter: Message formatter instance
|
|
skills_manager: Optional SkillsManager instance
|
|
sys_prompt: Optional override for system prompt
|
|
max_iters: Maximum reasoning-acting iterations
|
|
memory: Optional memory instance (defaults to InMemoryMemory)
|
|
enable_tool_guard: Enable tool-guard security interception
|
|
enable_bootstrap_hook: Enable bootstrap guidance on first interaction
|
|
enable_memory_compaction: Enable automatic memory compaction
|
|
memory_manager: Optional memory manager for compaction
|
|
memory_compact_threshold: Token threshold for memory compaction
|
|
env_context: Optional environment context to prepend to system prompt
|
|
prompt_files: List of markdown files to load (defaults to standard set)
|
|
"""
|
|
self.agent_id = agent_id
|
|
self.config_name = config_name
|
|
self.workspace_dir = Path(workspace_dir)
|
|
self._skills_manager = skills_manager or SkillsManager()
|
|
self._env_context = env_context
|
|
self._prompt_files = prompt_files
|
|
|
|
# Initialize tool guard
|
|
if enable_tool_guard:
|
|
self._init_tool_guard()
|
|
|
|
# Load agent configuration from workspace
|
|
self._agent_config = self._load_agent_config()
|
|
|
|
# Build or use provided system prompt
|
|
if sys_prompt is not None:
|
|
self._sys_prompt = sys_prompt
|
|
else:
|
|
self._sys_prompt = self._build_system_prompt()
|
|
|
|
# Create toolkit with skills
|
|
toolkit = self._create_toolkit()
|
|
|
|
# Initialize hook manager
|
|
self._hook_manager = HookManager()
|
|
|
|
# Initialize parent ReActAgent
|
|
super().__init__(
|
|
name=agent_id,
|
|
model=model,
|
|
sys_prompt=self._sys_prompt,
|
|
toolkit=toolkit,
|
|
memory=memory or InMemoryMemory(),
|
|
formatter=formatter,
|
|
max_iters=max_iters,
|
|
)
|
|
|
|
# Register hooks
|
|
self._register_hooks(
|
|
enable_bootstrap=enable_bootstrap_hook,
|
|
enable_memory_compaction=enable_memory_compaction,
|
|
memory_manager=memory_manager,
|
|
memory_compact_threshold=memory_compact_threshold,
|
|
)
|
|
|
|
logger.info(
|
|
"EvoAgent initialized: %s (workspace: %s)",
|
|
agent_id,
|
|
workspace_dir,
|
|
)
|
|
|
|
def _load_agent_config(self) -> Dict[str, Any]:
|
|
"""Load agent configuration from workspace.
|
|
|
|
Returns:
|
|
Agent configuration dictionary
|
|
"""
|
|
config_path = self.workspace_dir / "agent.yaml"
|
|
if config_path.exists():
|
|
loaded = load_agent_workspace_config(config_path)
|
|
return dict(loaded.values)
|
|
return {}
|
|
|
|
def _build_system_prompt(self) -> str:
|
|
"""Build system prompt from workspace markdown files.
|
|
|
|
Uses PromptBuilder to load and combine AGENTS.md, SOUL.md,
|
|
PROFILE.md, and other configured files.
|
|
|
|
Returns:
|
|
Complete system prompt string
|
|
"""
|
|
prompt = build_system_prompt_from_workspace(
|
|
workspace_dir=self.workspace_dir,
|
|
enabled_files=self._prompt_files,
|
|
agent_id=self.agent_id,
|
|
extra_context=self._env_context,
|
|
)
|
|
return prompt
|
|
|
|
def _create_toolkit(self) -> Toolkit:
|
|
"""Create and populate toolkit with agent skills.
|
|
|
|
Loads skills from the agent's active skills directory and
|
|
registers them with the toolkit.
|
|
|
|
Returns:
|
|
Configured Toolkit instance
|
|
"""
|
|
toolkit = Toolkit(
|
|
agent_skill_instruction=(
|
|
"<system-info>You have access to specialized skills. "
|
|
"Each skill lives in a directory and is described by SKILL.md. "
|
|
"Follow the skill instructions when they are relevant to the current task."
|
|
"</system-info>"
|
|
),
|
|
agent_skill_template="- {name} (dir: {dir}): {description}",
|
|
)
|
|
|
|
# Register skills from active directory
|
|
active_skills_dir = self._skills_manager.get_agent_active_root(
|
|
self.config_name,
|
|
self.agent_id,
|
|
)
|
|
|
|
if active_skills_dir.exists():
|
|
for skill_dir in sorted(active_skills_dir.iterdir()):
|
|
if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists():
|
|
try:
|
|
toolkit.register_agent_skill(str(skill_dir))
|
|
logger.debug("Registered skill: %s", skill_dir.name)
|
|
except Exception as e:
|
|
logger.error(
|
|
"Failed to register skill '%s': %s",
|
|
skill_dir.name,
|
|
e,
|
|
)
|
|
|
|
return toolkit
|
|
|
|
def _register_hooks(
|
|
self,
|
|
enable_bootstrap: bool,
|
|
enable_memory_compaction: bool,
|
|
memory_manager: Optional[Any],
|
|
memory_compact_threshold: Optional[int],
|
|
) -> None:
|
|
"""Register agent hooks.
|
|
|
|
Args:
|
|
enable_bootstrap: Enable bootstrap hook
|
|
enable_memory_compaction: Enable memory compaction hook
|
|
memory_manager: Memory manager instance
|
|
memory_compact_threshold: Token threshold for compaction
|
|
"""
|
|
# Bootstrap hook - checks BOOTSTRAP.md on first interaction
|
|
if enable_bootstrap:
|
|
bootstrap_hook = BootstrapHook(
|
|
workspace_dir=self.workspace_dir,
|
|
language="zh",
|
|
)
|
|
self._hook_manager.register(
|
|
hook_type=HOOK_PRE_REASONING,
|
|
hook_name="bootstrap",
|
|
hook=bootstrap_hook,
|
|
)
|
|
logger.debug("Registered bootstrap hook")
|
|
|
|
# Memory compaction hook
|
|
if enable_memory_compaction and memory_manager is not None:
|
|
compaction_hook = MemoryCompactionHook(
|
|
memory_manager=memory_manager,
|
|
memory_compact_threshold=memory_compact_threshold,
|
|
)
|
|
self._hook_manager.register(
|
|
hook_type=HOOK_PRE_REASONING,
|
|
hook_name="memory_compaction",
|
|
hook=compaction_hook,
|
|
)
|
|
logger.debug("Registered memory compaction hook")
|
|
|
|
async def _reasoning(self, **kwargs) -> Msg:
|
|
"""Override reasoning to execute pre-reasoning hooks.
|
|
|
|
Args:
|
|
**kwargs: Arguments for reasoning
|
|
|
|
Returns:
|
|
Response message
|
|
"""
|
|
# Execute pre-reasoning hooks
|
|
kwargs = await self._hook_manager.execute(
|
|
hook_type=HOOK_PRE_REASONING,
|
|
agent=self,
|
|
kwargs=kwargs,
|
|
)
|
|
|
|
# Call parent (which may be ToolGuardMixin's _reasoning)
|
|
return await super()._reasoning(**kwargs)
|
|
|
|
def reload_skills(self, active_skill_dirs: Optional[List[Path]] = None) -> None:
|
|
"""Reload skills at runtime.
|
|
|
|
Rebuilds the toolkit with current skills from the active directory.
|
|
|
|
Args:
|
|
active_skill_dirs: Optional list of specific skill directories to load
|
|
"""
|
|
logger.info("Reloading skills for agent: %s", self.agent_id)
|
|
|
|
# Create new toolkit
|
|
new_toolkit = Toolkit(
|
|
agent_skill_instruction=(
|
|
"<system-info>You have access to specialized skills. "
|
|
"Each skill lives in a directory and is described by SKILL.md. "
|
|
"Follow the skill instructions when they are relevant to the current task."
|
|
"</system-info>"
|
|
),
|
|
agent_skill_template="- {name} (dir: {dir}): {description}",
|
|
)
|
|
|
|
# Register skills
|
|
if active_skill_dirs is None:
|
|
active_skills_dir = self._skills_manager.get_agent_active_root(
|
|
self.config_name,
|
|
self.agent_id,
|
|
)
|
|
if active_skills_dir.exists():
|
|
active_skill_dirs = [
|
|
d for d in active_skills_dir.iterdir()
|
|
if d.is_dir() and (d / "SKILL.md").exists()
|
|
]
|
|
else:
|
|
active_skill_dirs = []
|
|
|
|
for skill_dir in active_skill_dirs:
|
|
if skill_dir.exists() and (skill_dir / "SKILL.md").exists():
|
|
try:
|
|
new_toolkit.register_agent_skill(str(skill_dir))
|
|
logger.debug("Reloaded skill: %s", skill_dir.name)
|
|
except Exception as e:
|
|
logger.error(
|
|
"Failed to reload skill '%s': %s",
|
|
skill_dir.name,
|
|
e,
|
|
)
|
|
|
|
# Replace toolkit
|
|
self.toolkit = new_toolkit
|
|
logger.info("Skills reloaded for agent: %s", self.agent_id)
|
|
|
|
def rebuild_sys_prompt(self) -> None:
|
|
"""Rebuild and replace the system prompt at runtime.
|
|
|
|
Useful after updating AGENTS.md, SOUL.md, PROFILE.md, etc.
|
|
to ensure the prompt reflects the latest configuration.
|
|
|
|
Updates both self._sys_prompt and the first system-role
|
|
message stored in self.memory.content.
|
|
"""
|
|
logger.info("Rebuilding system prompt for agent: %s", self.agent_id)
|
|
|
|
# Reload agent config in case it changed
|
|
self._agent_config = self._load_agent_config()
|
|
|
|
# Rebuild prompt
|
|
self._sys_prompt = self._build_system_prompt()
|
|
|
|
# Update memory if system message exists
|
|
if hasattr(self, "memory") and self.memory.content:
|
|
for msg, _marks in self.memory.content:
|
|
if getattr(msg, "role", None) == "system":
|
|
msg.content = self._sys_prompt
|
|
logger.debug("Updated system message in memory")
|
|
break
|
|
|
|
logger.info("System prompt rebuilt for agent: %s", self.agent_id)
|
|
|
|
async def reply(
|
|
self,
|
|
msg: Msg | List[Msg] | None = None,
|
|
structured_model: Optional[Type[Any]] = None,
|
|
) -> Msg:
|
|
"""Process a message and return a response.
|
|
|
|
Args:
|
|
msg: Input message(s) from user
|
|
structured_model: Optional pydantic model for structured output
|
|
|
|
Returns:
|
|
Response message
|
|
"""
|
|
# Handle list of messages
|
|
if isinstance(msg, list):
|
|
# Process each message in sequence
|
|
for m in msg[:-1]:
|
|
await self.memory.add(m)
|
|
msg = msg[-1] if msg else None
|
|
|
|
return await super().reply(msg=msg, structured_model=structured_model)
|
|
|
|
def get_agent_info(self) -> Dict[str, Any]:
|
|
"""Get agent information.
|
|
|
|
Returns:
|
|
Dictionary with agent metadata
|
|
"""
|
|
return {
|
|
"agent_id": self.agent_id,
|
|
"config_name": self.config_name,
|
|
"workspace_dir": str(self.workspace_dir),
|
|
"skills_count": len([
|
|
s for s in self._skills_manager.list_active_skill_metadata(
|
|
self.config_name,
|
|
self.agent_id,
|
|
)
|
|
]),
|
|
"registered_hooks": self._hook_manager.list_hooks(),
|
|
}
|
|
|
|
|
|
__all__ = ["EvoAgent"]
|