|
import io
|
|
from pathlib import Path
|
|
from typing import Union
|
|
|
|
import frontmatter
|
|
from pydantic import BaseModel
|
|
|
|
from openhands.core.exceptions import (
|
|
MicroAgentValidationError,
|
|
)
|
|
from openhands.core.logger import openhands_logger as logger
|
|
from openhands.microagent.types import MicroAgentMetadata, MicroAgentType
|
|
|
|
|
|
class BaseMicroAgent(BaseModel):
|
|
"""Base class for all microagents."""
|
|
|
|
name: str
|
|
content: str
|
|
metadata: MicroAgentMetadata
|
|
source: str
|
|
type: MicroAgentType
|
|
|
|
@classmethod
|
|
def load(
|
|
cls, path: Union[str, Path], file_content: str | None = None
|
|
) -> 'BaseMicroAgent':
|
|
"""Load a microagent from a markdown file with frontmatter."""
|
|
path = Path(path) if isinstance(path, str) else path
|
|
|
|
|
|
if file_content is None:
|
|
with open(path) as f:
|
|
file_content = f.read()
|
|
|
|
|
|
if path.name == '.openhands_instructions':
|
|
return RepoMicroAgent(
|
|
name='repo_legacy',
|
|
content=file_content,
|
|
metadata=MicroAgentMetadata(name='repo_legacy'),
|
|
source=str(path),
|
|
type=MicroAgentType.REPO_KNOWLEDGE,
|
|
)
|
|
|
|
file_io = io.StringIO(file_content)
|
|
loaded = frontmatter.load(file_io)
|
|
content = loaded.content
|
|
try:
|
|
metadata = MicroAgentMetadata(**loaded.metadata)
|
|
except Exception as e:
|
|
raise MicroAgentValidationError(f'Error loading metadata: {e}') from e
|
|
|
|
|
|
subclass_map = {
|
|
MicroAgentType.KNOWLEDGE: KnowledgeMicroAgent,
|
|
MicroAgentType.REPO_KNOWLEDGE: RepoMicroAgent,
|
|
MicroAgentType.TASK: TaskMicroAgent,
|
|
}
|
|
if metadata.type not in subclass_map:
|
|
raise ValueError(f'Unknown microagent type: {metadata.type}')
|
|
|
|
agent_class = subclass_map[metadata.type]
|
|
return agent_class(
|
|
name=metadata.name,
|
|
content=content,
|
|
metadata=metadata,
|
|
source=str(path),
|
|
type=metadata.type,
|
|
)
|
|
|
|
|
|
class KnowledgeMicroAgent(BaseMicroAgent):
|
|
"""Knowledge micro-agents provide specialized expertise that's triggered by keywords in conversations. They help with:
|
|
- Language best practices
|
|
- Framework guidelines
|
|
- Common patterns
|
|
- Tool usage
|
|
"""
|
|
|
|
def __init__(self, **data):
|
|
super().__init__(**data)
|
|
if self.type != MicroAgentType.KNOWLEDGE:
|
|
raise ValueError('KnowledgeMicroAgent must have type KNOWLEDGE')
|
|
|
|
def match_trigger(self, message: str) -> str | None:
|
|
"""Match a trigger in the message.
|
|
|
|
It returns the first trigger that matches the message.
|
|
"""
|
|
message = message.lower()
|
|
for trigger in self.triggers:
|
|
if trigger.lower() in message:
|
|
return trigger
|
|
return None
|
|
|
|
@property
|
|
def triggers(self) -> list[str]:
|
|
return self.metadata.triggers
|
|
|
|
|
|
class RepoMicroAgent(BaseMicroAgent):
|
|
"""MicroAgent specialized for repository-specific knowledge and guidelines.
|
|
|
|
RepoMicroAgents are loaded from `.openhands/microagents/repo.md` files within repositories
|
|
and contain private, repository-specific instructions that are automatically loaded when
|
|
working with that repository. They are ideal for:
|
|
- Repository-specific guidelines
|
|
- Team practices and conventions
|
|
- Project-specific workflows
|
|
- Custom documentation references
|
|
"""
|
|
|
|
def __init__(self, **data):
|
|
super().__init__(**data)
|
|
if self.type != MicroAgentType.REPO_KNOWLEDGE:
|
|
raise ValueError('RepoMicroAgent must have type REPO_KNOWLEDGE')
|
|
|
|
|
|
class TaskMicroAgent(BaseMicroAgent):
|
|
"""MicroAgent specialized for task-based operations."""
|
|
|
|
def __init__(self, **data):
|
|
super().__init__(**data)
|
|
if self.type != MicroAgentType.TASK:
|
|
raise ValueError('TaskMicroAgent must have type TASK')
|
|
|
|
|
|
def load_microagents_from_dir(
|
|
microagent_dir: Union[str, Path],
|
|
) -> tuple[
|
|
dict[str, RepoMicroAgent], dict[str, KnowledgeMicroAgent], dict[str, TaskMicroAgent]
|
|
]:
|
|
"""Load all microagents from the given directory.
|
|
|
|
Note, legacy repo instructions will not be loaded here.
|
|
|
|
Args:
|
|
microagent_dir: Path to the microagents directory (e.g. .openhands/microagents)
|
|
|
|
Returns:
|
|
Tuple of (repo_agents, knowledge_agents, task_agents) dictionaries
|
|
"""
|
|
if isinstance(microagent_dir, str):
|
|
microagent_dir = Path(microagent_dir)
|
|
|
|
repo_agents = {}
|
|
knowledge_agents = {}
|
|
task_agents = {}
|
|
|
|
|
|
logger.debug(f'Loading agents from {microagent_dir}')
|
|
if microagent_dir.exists():
|
|
for file in microagent_dir.rglob('*.md'):
|
|
logger.debug(f'Checking file {file}...')
|
|
|
|
if file.name == 'README.md':
|
|
continue
|
|
try:
|
|
agent = BaseMicroAgent.load(file)
|
|
if isinstance(agent, RepoMicroAgent):
|
|
repo_agents[agent.name] = agent
|
|
elif isinstance(agent, KnowledgeMicroAgent):
|
|
knowledge_agents[agent.name] = agent
|
|
elif isinstance(agent, TaskMicroAgent):
|
|
task_agents[agent.name] = agent
|
|
logger.debug(f'Loaded agent {agent.name} from {file}')
|
|
except Exception as e:
|
|
raise ValueError(f'Error loading agent from {file}: {e}')
|
|
|
|
return repo_agents, knowledge_agents, task_agents
|
|
|