mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
refactor: restructure microagents system (#5886)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Graham Neubig <neubig@gmail.com>
This commit is contained in:
parent
8983d719bd
commit
c1b514e9d3
@ -1,6 +1,7 @@
|
||||
---
|
||||
name: repo
|
||||
agent: CodeAct
|
||||
type: repo
|
||||
agent: CodeActAgent
|
||||
---
|
||||
This repository contains the code for OpenHands, an automated AI software engineer. It has a Python backend
|
||||
(in the `openhands` directory) and React frontend (in the `frontend` directory).
|
||||
|
||||
@ -71,6 +71,7 @@ ENV VIRTUAL_ENV=/app/.venv \
|
||||
COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
RUN playwright install --with-deps chromium
|
||||
|
||||
COPY --chown=openhands:app --chmod=770 ./microagents ./microagents
|
||||
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
|
||||
COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
|
||||
COPY --chown=openhands:app --chmod=770 ./openhands/agenthub ./openhands/agenthub
|
||||
|
||||
163
microagents/README.md
Normal file
163
microagents/README.md
Normal file
@ -0,0 +1,163 @@
|
||||
# OpenHands MicroAgents
|
||||
|
||||
MicroAgents are specialized prompts that enhance OpenHands with domain-specific knowledge and task-specific workflows. They help developers by providing expert guidance, automating common tasks, and ensuring consistent practices across projects. Each microagent is designed to excel in a specific area, from Git operations to code review processes.
|
||||
|
||||
## Sources of Microagents
|
||||
|
||||
OpenHands loads microagents from two sources:
|
||||
|
||||
### 1. Shareable Microagents (Public)
|
||||
This directory (`OpenHands/microagents/`) contains shareable microagents that are:
|
||||
- Available to all OpenHands users
|
||||
- Maintained in the OpenHands repository
|
||||
- Perfect for reusable knowledge and common workflows
|
||||
|
||||
Directory structure:
|
||||
```
|
||||
OpenHands/microagents/
|
||||
├── knowledge/ # Keyword-triggered expertise
|
||||
│ ├── git.md # Git operations
|
||||
│ ├── testing.md # Testing practices
|
||||
│ └── docker.md # Docker guidelines
|
||||
└── tasks/ # Interactive workflows
|
||||
├── pr_review.md # PR review process
|
||||
├── bug_fix.md # Bug fixing workflow
|
||||
└── feature.md # Feature implementation
|
||||
```
|
||||
|
||||
### 2. Repository Instructions (Private)
|
||||
Each repository can have its own instructions in `.openhands/microagents/repo.md`. These instructions are:
|
||||
- Private to that repository
|
||||
- Automatically loaded when working with that repository
|
||||
- Perfect for repository-specific guidelines and team practices
|
||||
|
||||
Example repository structure:
|
||||
```
|
||||
your-repository/
|
||||
└── .openhands/
|
||||
└── microagents/
|
||||
└── repo.md # Repository-specific instructions
|
||||
└── knowledges/ # Private micro-agents that are only available inside this repo
|
||||
└── tasks/ # Private micro-agents that are only available inside this repo
|
||||
```
|
||||
|
||||
|
||||
## Loading Order
|
||||
|
||||
When OpenHands works with a repository, it:
|
||||
1. Loads repository-specific instructions from `.openhands/microagents/repo.md` if present
|
||||
2. Loads relevant knowledge agents based on keywords in conversations
|
||||
3. Enable task agent if user select one of them
|
||||
|
||||
## Types of MicroAgents
|
||||
|
||||
All microagents use markdown files with YAML frontmatter.
|
||||
|
||||
|
||||
### 1. Knowledge Agents
|
||||
|
||||
Knowledge agents provide specialized expertise that's triggered by keywords in conversations. They help with:
|
||||
- Language best practices
|
||||
- Framework guidelines
|
||||
- Common patterns
|
||||
- Tool usage
|
||||
|
||||
Key characteristics:
|
||||
- **Trigger-based**: Activated by specific keywords in conversations
|
||||
- **Context-aware**: Provide relevant advice based on file types and content
|
||||
- **Reusable**: Knowledge can be applied across multiple projects
|
||||
- **Versioned**: Support multiple versions of tools/frameworks
|
||||
|
||||
You can see an example of a knowledge-based agent in [OpenHands's github microagent](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/github.md).
|
||||
|
||||
### 2. Repository Agents
|
||||
|
||||
Repository agents provide repository-specific knowledge and guidelines. They are:
|
||||
- Loaded from `.openhands/microagents/repo.md`
|
||||
- Specific to individual repositories
|
||||
- Automatically activated for their repository
|
||||
- Perfect for team practices and project conventions
|
||||
|
||||
Key features:
|
||||
- **Project-specific**: Contains guidelines unique to the repository
|
||||
- **Team-focused**: Enforces team conventions and practices
|
||||
- **Always active**: Automatically loaded for the repository
|
||||
- **Locally maintained**: Updated with the project
|
||||
|
||||
You can see an example of a repo agent in [the agent for the OpenHands repo itself](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands/microagents/repo.md).
|
||||
|
||||
### 3. Task Agents
|
||||
|
||||
Task agents provide interactive workflows that guide users through common development tasks. They:
|
||||
- Accept user inputs
|
||||
- Follow predefined steps
|
||||
- Adapt to context
|
||||
- Provide consistent results
|
||||
|
||||
Key capabilities:
|
||||
- **Interactive**: Guide users through complex processes
|
||||
- **Validating**: Check inputs and conditions
|
||||
- **Flexible**: Adapt to different scenarios
|
||||
- **Reproducible**: Ensure consistent outcomes
|
||||
|
||||
Example workflow:
|
||||
You can see an example of a task-based agent in [OpenHands's pull request updating microagent](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks/update_pr_description.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
### When to Contribute
|
||||
|
||||
1. **Knowledge Agents** - When you have:
|
||||
- Language/framework best practices
|
||||
- Tool usage patterns
|
||||
- Common problem solutions
|
||||
- General development guidelines
|
||||
|
||||
2. **Task Agents** - When you have:
|
||||
- Repeatable workflows
|
||||
- Multi-step processes
|
||||
- Common development tasks
|
||||
- Standard procedures
|
||||
|
||||
3. **Repository Agents** - When you need:
|
||||
- Project-specific guidelines
|
||||
- Team conventions and practices
|
||||
- Custom workflow documentation
|
||||
- Repository-specific setup instructions
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **For Knowledge Agents**:
|
||||
- Choose distinctive triggers
|
||||
- Focus on one area of expertise
|
||||
- Include practical examples
|
||||
- Use file patterns when relevant
|
||||
- Keep knowledge general and reusable
|
||||
|
||||
2. **For Task Agents**:
|
||||
- Break workflows into clear steps
|
||||
- Validate user inputs
|
||||
- Provide helpful defaults
|
||||
- Include usage examples
|
||||
- Make steps adaptable
|
||||
|
||||
3. **For Repository Agents**:
|
||||
- Document clear setup instructions
|
||||
- Include repository structure details
|
||||
- Specify testing and build procedures
|
||||
- List environment requirements
|
||||
- Maintain up-to-date team practices
|
||||
|
||||
### Submission Process
|
||||
|
||||
1. Create your agent file in the appropriate directory:
|
||||
- `knowledge/` for expertise (public, shareable)
|
||||
- `tasks/` for workflows (public, shareable)
|
||||
- Note: Repository agents should remain in their respective repositories' `.openhands/microagents/` directory
|
||||
2. Test thoroughly
|
||||
3. Submit a pull request to OpenHands
|
||||
|
||||
|
||||
## License
|
||||
|
||||
All microagents are subject to the same license as OpenHands. See the root LICENSE file for details.
|
||||
@ -1,5 +1,7 @@
|
||||
---
|
||||
name: flarglebargle
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- flarglebargle
|
||||
@ -1,5 +1,7 @@
|
||||
---
|
||||
name: github
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- github
|
||||
@ -26,4 +28,3 @@ git checkout -b create-widget && git add . && git commit -m "Create widget" && g
|
||||
curl -X POST "https://api.github.com/repos/$ORG_NAME/$REPO_NAME/pulls" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-d '{"title":"Create widget","head":"create-widget","base":"openhands-workspace"}'
|
||||
```
|
||||
@ -1,5 +1,7 @@
|
||||
---
|
||||
name: npm
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- npm
|
||||
20
microagents/tasks/address_pr_comments.md
Normal file
20
microagents/tasks/address_pr_comments.md
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: address_pr_comments
|
||||
type: task
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
inputs:
|
||||
- name: PR_URL
|
||||
description: "URL of the pull request"
|
||||
required: true
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch name corresponds to the pull request"
|
||||
required: true
|
||||
---
|
||||
|
||||
First, check the branch {{ BRANCH_NAME }} and read the diff against the main branch to understand the purpose.
|
||||
|
||||
This branch corresponds to this PR {{ PR_URL }}
|
||||
|
||||
Next, you should use the GitHub API to read the reviews and comments on this PR and address them.
|
||||
28
microagents/tasks/get_test_to_pass.md
Normal file
28
microagents/tasks/get_test_to_pass.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: get_test_to_pass
|
||||
type: task
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
inputs:
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch for the agent to work on"
|
||||
required: true
|
||||
- name: TEST_COMMAND_TO_RUN
|
||||
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
|
||||
required: true
|
||||
- name: FUNCTION_TO_FIX
|
||||
description: "The name of function to fix"
|
||||
required: false
|
||||
- name: FILE_FOR_FUNCTION
|
||||
description: "The path of the file that contains the function"
|
||||
required: false
|
||||
---
|
||||
|
||||
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
|
||||
|
||||
{%- if FUNCTION_TO_FIX and FILE_FOR_FUNCTION %}
|
||||
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
|
||||
{%- endif %}
|
||||
|
||||
PLEASE DO NOT modify the tests by yourselves -- Let me know if you think some of the tests are incorrect.
|
||||
22
microagents/tasks/update_pr_description.md
Normal file
22
microagents/tasks/update_pr_description.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: update_pr_description
|
||||
type: task
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
inputs:
|
||||
- name: PR_URL
|
||||
description: "URL of the pull request"
|
||||
type: string
|
||||
required: true
|
||||
validation:
|
||||
pattern: "^https://github.com/.+/.+/pull/[0-9]+$"
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch name corresponds to the pull request"
|
||||
type: string
|
||||
required: true
|
||||
---
|
||||
|
||||
Please check the branch "{{ BRANCH_NAME }}" and look at the diff against the main branch. This branch belongs to this PR "{{ PR_URL }}".
|
||||
|
||||
Once you understand the purpose of the diff, please use Github API to read the existing PR description, and update it to be more reflective of the changes we've made when necessary.
|
||||
22
microagents/tasks/update_test_for_new_implementation.md
Normal file
22
microagents/tasks/update_test_for_new_implementation.md
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: update_test_for_new_implementation
|
||||
type: task
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
inputs:
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch for the agent to work on"
|
||||
required: true
|
||||
- name: TEST_COMMAND_TO_RUN
|
||||
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
|
||||
required: true
|
||||
---
|
||||
|
||||
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
|
||||
|
||||
{%- if FUNCTION_TO_FIX and FILE_FOR_FUNCTION %}
|
||||
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
|
||||
{%- endif %}
|
||||
|
||||
PLEASE DO NOT modify the tests by yourselves -- Let me know if you think some of the tests are incorrect.
|
||||
@ -4,6 +4,7 @@ from collections import deque
|
||||
|
||||
from litellm import ModelResponse
|
||||
|
||||
import openhands
|
||||
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
@ -104,7 +105,10 @@ class CodeActAgent(Agent):
|
||||
f'TOOLS loaded for CodeActAgent: {json.dumps(self.tools, indent=2, ensure_ascii=False).replace("\\n", "\n")}'
|
||||
)
|
||||
self.prompt_manager = PromptManager(
|
||||
microagent_dir=os.path.join(os.path.dirname(__file__), 'micro')
|
||||
microagent_dir=os.path.join(
|
||||
os.path.dirname(os.path.dirname(openhands.__file__)),
|
||||
'microagents',
|
||||
)
|
||||
if self.config.use_microagents
|
||||
else None,
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
|
||||
|
||||
@ -11,7 +11,9 @@ from openhands.core.exceptions import (
|
||||
)
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.utils.prompt import PromptManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openhands.utils.prompt import PromptManager
|
||||
|
||||
|
||||
class Agent(ABC):
|
||||
@ -34,7 +36,7 @@ class Agent(ABC):
|
||||
self.llm = llm
|
||||
self.config = config
|
||||
self._complete = False
|
||||
self.prompt_manager: PromptManager | None = None
|
||||
self.prompt_manager: 'PromptManager' | None = None
|
||||
|
||||
@property
|
||||
def complete(self) -> bool:
|
||||
|
||||
@ -91,11 +91,6 @@ class UserCancelledError(Exception):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class MicroAgentValidationError(Exception):
|
||||
def __init__(self, message='Micro agent validation failed'):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class OperationCancelled(Exception):
|
||||
"""Exception raised when an operation is cancelled (e.g. by a keyboard interrupt)."""
|
||||
|
||||
@ -204,3 +199,21 @@ class BrowserUnavailableException(Exception):
|
||||
message='Browser environment is not available, please check if has been initialized',
|
||||
):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
# ============================================
|
||||
# Microagent Exceptions
|
||||
# ============================================
|
||||
|
||||
|
||||
class MicroAgentError(Exception):
|
||||
"""Base exception for all microagent errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MicroAgentValidationError(MicroAgentError):
|
||||
"""Raised when there's a validation error in microagent metadata."""
|
||||
|
||||
def __init__(self, message='Micro agent validation failed'):
|
||||
super().__init__(message)
|
||||
|
||||
19
openhands/microagent/__init__.py
Normal file
19
openhands/microagent/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
from .microagent import (
|
||||
BaseMicroAgent,
|
||||
KnowledgeMicroAgent,
|
||||
RepoMicroAgent,
|
||||
TaskMicroAgent,
|
||||
load_microagents_from_dir,
|
||||
)
|
||||
from .types import MicroAgentMetadata, MicroAgentType, TaskInput
|
||||
|
||||
__all__ = [
|
||||
'BaseMicroAgent',
|
||||
'KnowledgeMicroAgent',
|
||||
'RepoMicroAgent',
|
||||
'TaskMicroAgent',
|
||||
'MicroAgentMetadata',
|
||||
'MicroAgentType',
|
||||
'TaskInput',
|
||||
'load_microagents_from_dir',
|
||||
]
|
||||
164
openhands/microagent/microagent.py
Normal file
164
openhands/microagent/microagent.py
Normal file
@ -0,0 +1,164 @@
|
||||
import io
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import frontmatter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openhands.core.exceptions import (
|
||||
MicroAgentValidationError,
|
||||
)
|
||||
from openhands.microagent.types import MicroAgentMetadata, MicroAgentType
|
||||
|
||||
|
||||
class BaseMicroAgent(BaseModel):
|
||||
"""Base class for all microagents."""
|
||||
|
||||
name: str
|
||||
content: str
|
||||
metadata: MicroAgentMetadata
|
||||
source: str # path to the file
|
||||
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
|
||||
|
||||
# Only load directly from path if file_content is not provided
|
||||
if file_content is None:
|
||||
with open(path) as f:
|
||||
file_content = f.read()
|
||||
|
||||
# Legacy repo instructions are stored in .openhands_instructions
|
||||
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
|
||||
|
||||
# Create appropriate subclass based on type
|
||||
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.
|
||||
|
||||
Args:
|
||||
microagent_dir: Path to the microagents directory.
|
||||
|
||||
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 = {}
|
||||
|
||||
# Load all agents
|
||||
for file in microagent_dir.rglob('*.md'):
|
||||
# skip README.md
|
||||
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
|
||||
except Exception as e:
|
||||
raise ValueError(f'Error loading agent from {file}: {e}')
|
||||
|
||||
return repo_agents, knowledge_agents, task_agents
|
||||
29
openhands/microagent/types.py
Normal file
29
openhands/microagent/types.py
Normal file
@ -0,0 +1,29 @@
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class MicroAgentType(str, Enum):
|
||||
"""Type of microagent."""
|
||||
|
||||
KNOWLEDGE = 'knowledge'
|
||||
REPO_KNOWLEDGE = 'repo'
|
||||
TASK = 'task'
|
||||
|
||||
|
||||
class MicroAgentMetadata(BaseModel):
|
||||
"""Metadata for all microagents."""
|
||||
|
||||
name: str = 'default'
|
||||
type: MicroAgentType = Field(default=MicroAgentType.KNOWLEDGE)
|
||||
version: str = Field(default='1.0.0')
|
||||
agent: str = Field(default='CodeActAgent')
|
||||
triggers: list[str] = [] # optional, only exists for knowledge microagents
|
||||
|
||||
|
||||
class TaskInput(BaseModel):
|
||||
"""Input parameter for task-based agents."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
required: bool = True
|
||||
@ -29,11 +29,18 @@ from openhands.events.event import Event
|
||||
from openhands.events.observation import (
|
||||
CmdOutputObservation,
|
||||
ErrorObservation,
|
||||
FileReadObservation,
|
||||
NullObservation,
|
||||
Observation,
|
||||
UserRejectObservation,
|
||||
)
|
||||
from openhands.events.serialization.action import ACTION_TYPE_TO_CLASS
|
||||
from openhands.microagent import (
|
||||
BaseMicroAgent,
|
||||
KnowledgeMicroAgent,
|
||||
RepoMicroAgent,
|
||||
TaskMicroAgent,
|
||||
)
|
||||
from openhands.runtime.plugins import (
|
||||
JupyterRequirement,
|
||||
PluginRequirement,
|
||||
@ -219,34 +226,73 @@ class Runtime(FileEditRuntimeMixin):
|
||||
self.log('info', f'Cloning repo: {selected_repository}')
|
||||
self.run_action(action)
|
||||
|
||||
def get_custom_microagents(self, selected_repository: str | None) -> list[str]:
|
||||
custom_microagents_content = []
|
||||
custom_microagents_dir = Path('.openhands') / 'microagents'
|
||||
|
||||
dir_name = str(custom_microagents_dir)
|
||||
def get_microagents_from_selected_repo(
|
||||
self, selected_repository: str | None
|
||||
) -> list[BaseMicroAgent]:
|
||||
loaded_microagents: list[BaseMicroAgent] = []
|
||||
dir_name = Path('.openhands') / 'microagents'
|
||||
if selected_repository:
|
||||
dir_name = str(
|
||||
Path(selected_repository.split('/')[1]) / custom_microagents_dir
|
||||
)
|
||||
dir_name = Path('/workspace') / selected_repository.split('/')[1] / dir_name
|
||||
|
||||
# Legacy Repo Instructions
|
||||
# Check for legacy .openhands_instructions file
|
||||
obs = self.read(FileReadAction(path='.openhands_instructions'))
|
||||
if isinstance(obs, ErrorObservation):
|
||||
self.log('debug', 'openhands_instructions not present')
|
||||
else:
|
||||
openhands_instructions = obs.content
|
||||
self.log('info', f'openhands_instructions: {openhands_instructions}')
|
||||
custom_microagents_content.append(openhands_instructions)
|
||||
self.log(
|
||||
'debug',
|
||||
f'openhands_instructions not present, trying to load from {dir_name}',
|
||||
)
|
||||
obs = self.read(
|
||||
FileReadAction(path=str(dir_name / '.openhands_instructions'))
|
||||
)
|
||||
|
||||
files = self.list_files(dir_name)
|
||||
if isinstance(obs, FileReadObservation):
|
||||
self.log('info', 'openhands_instructions microagent loaded.')
|
||||
loaded_microagents.append(
|
||||
BaseMicroAgent.load(
|
||||
path='.openhands_instructions', file_content=obs.content
|
||||
)
|
||||
)
|
||||
|
||||
self.log('info', f'Found {len(files)} custom microagents.')
|
||||
# Check for local repository microagents
|
||||
files = self.list_files(str(dir_name))
|
||||
self.log('info', f'Found {len(files)} local microagents.')
|
||||
if 'repo.md' in files:
|
||||
obs = self.read(FileReadAction(path=str(dir_name / 'repo.md')))
|
||||
if isinstance(obs, FileReadObservation):
|
||||
self.log('info', 'repo.md microagent loaded.')
|
||||
loaded_microagents.append(
|
||||
RepoMicroAgent.load(
|
||||
path=str(dir_name / 'repo.md'), file_content=obs.content
|
||||
)
|
||||
)
|
||||
|
||||
for fname in files:
|
||||
content = self.read(
|
||||
FileReadAction(path=str(custom_microagents_dir / fname))
|
||||
).content
|
||||
custom_microagents_content.append(content)
|
||||
if 'knowledge' in files:
|
||||
knowledge_dir = dir_name / 'knowledge'
|
||||
_knowledge_microagents_files = self.list_files(str(knowledge_dir))
|
||||
for fname in _knowledge_microagents_files:
|
||||
obs = self.read(FileReadAction(path=str(knowledge_dir / fname)))
|
||||
if isinstance(obs, FileReadObservation):
|
||||
self.log('info', f'knowledge/{fname} microagent loaded.')
|
||||
loaded_microagents.append(
|
||||
KnowledgeMicroAgent.load(
|
||||
path=str(knowledge_dir / fname), file_content=obs.content
|
||||
)
|
||||
)
|
||||
|
||||
return custom_microagents_content
|
||||
if 'tasks' in files:
|
||||
tasks_dir = dir_name / 'tasks'
|
||||
_tasks_microagents_files = self.list_files(str(tasks_dir))
|
||||
for fname in _tasks_microagents_files:
|
||||
obs = self.read(FileReadAction(path=str(tasks_dir / fname)))
|
||||
if isinstance(obs, FileReadObservation):
|
||||
self.log('info', f'tasks/{fname} microagent loaded.')
|
||||
loaded_microagents.append(
|
||||
TaskMicroAgent.load(
|
||||
path=str(tasks_dir / fname), file_content=obs.content
|
||||
)
|
||||
)
|
||||
return loaded_microagents
|
||||
|
||||
def run_action(self, action: Action) -> Observation:
|
||||
"""Run an action and return the resulting observation.
|
||||
|
||||
@ -11,6 +11,7 @@ from openhands.core.schema.agent import AgentState
|
||||
from openhands.events.action import ChangeAgentStateAction
|
||||
from openhands.events.event import EventSource
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.microagent import BaseMicroAgent
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.security import SecurityAnalyzer, options
|
||||
@ -203,10 +204,10 @@ class AgentSession:
|
||||
|
||||
self.runtime.clone_repo(github_token, selected_repository)
|
||||
if agent.prompt_manager:
|
||||
microagents = await call_sync_from_async(
|
||||
self.runtime.get_custom_microagents, selected_repository
|
||||
microagents: list[BaseMicroAgent] = await call_sync_from_async(
|
||||
self.runtime.get_microagents_from_selected_repo, selected_repository
|
||||
)
|
||||
agent.prompt_manager.load_microagent_files(microagents)
|
||||
agent.prompt_manager.load_microagents(microagents)
|
||||
|
||||
logger.debug(
|
||||
f'Runtime initialized with plugins: {[plugin.name for plugin in self.runtime.plugins]}'
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
import os
|
||||
|
||||
import frontmatter
|
||||
import pydantic
|
||||
|
||||
|
||||
class MicroAgentMetadata(pydantic.BaseModel):
|
||||
name: str = 'default'
|
||||
agent: str = ''
|
||||
triggers: list[str] = []
|
||||
|
||||
|
||||
class MicroAgent:
|
||||
def __init__(self, path: str | None = None, content: str | None = None):
|
||||
if path and not content:
|
||||
self.path = path
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f'Micro agent file {path} is not found')
|
||||
with open(path, 'r') as file:
|
||||
loaded = frontmatter.load(file)
|
||||
self._content = loaded.content
|
||||
self._metadata = MicroAgentMetadata(**loaded.metadata)
|
||||
elif content and not path:
|
||||
metadata, self._content = frontmatter.parse(content)
|
||||
self._metadata = MicroAgentMetadata(**metadata)
|
||||
else:
|
||||
raise Exception('You must pass either path or file content, but not both.')
|
||||
|
||||
def get_trigger(self, message: str) -> str | None:
|
||||
message = message.lower()
|
||||
for trigger in self.triggers:
|
||||
if trigger.lower() in message:
|
||||
return trigger
|
||||
return None
|
||||
|
||||
@property
|
||||
def content(self) -> str:
|
||||
return self._content
|
||||
|
||||
@property
|
||||
def metadata(self) -> MicroAgentMetadata:
|
||||
return self._metadata
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._metadata.name
|
||||
|
||||
@property
|
||||
def triggers(self) -> list[str]:
|
||||
return self._metadata.triggers
|
||||
|
||||
@property
|
||||
def agent(self) -> str:
|
||||
return self._metadata.agent
|
||||
@ -5,7 +5,12 @@ from jinja2 import Template
|
||||
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.message import Message, TextContent
|
||||
from openhands.utils.microagent import MicroAgent
|
||||
from openhands.microagent import (
|
||||
BaseMicroAgent,
|
||||
KnowledgeMicroAgent,
|
||||
RepoMicroAgent,
|
||||
load_microagents_from_dir,
|
||||
)
|
||||
|
||||
|
||||
class PromptManager:
|
||||
@ -28,31 +33,44 @@ class PromptManager:
|
||||
microagent_dir: str | None = None,
|
||||
disabled_microagents: list[str] | None = None,
|
||||
):
|
||||
self.disabled_microagents: list[str] = disabled_microagents or []
|
||||
self.prompt_dir: str = prompt_dir
|
||||
|
||||
self.system_template: Template = self._load_template('system_prompt')
|
||||
self.user_template: Template = self._load_template('user_prompt')
|
||||
self.microagents: dict = {}
|
||||
|
||||
microagent_files = []
|
||||
self.knowledge_microagents: dict[str, KnowledgeMicroAgent] = {}
|
||||
self.repo_microagents: dict[str, RepoMicroAgent] = {}
|
||||
|
||||
if microagent_dir:
|
||||
microagent_files = [
|
||||
os.path.join(microagent_dir, f)
|
||||
for f in os.listdir(microagent_dir)
|
||||
if f.endswith('.md')
|
||||
]
|
||||
for microagent_file in microagent_files:
|
||||
microagent = MicroAgent(path=microagent_file)
|
||||
if (
|
||||
disabled_microagents is None
|
||||
or microagent.name not in disabled_microagents
|
||||
):
|
||||
self.microagents[microagent.name] = microagent
|
||||
# Only load KnowledgeMicroAgents
|
||||
repo_microagents, knowledge_microagents, _ = load_microagents_from_dir(
|
||||
microagent_dir
|
||||
)
|
||||
assert all(
|
||||
isinstance(microagent, KnowledgeMicroAgent)
|
||||
for microagent in knowledge_microagents.values()
|
||||
)
|
||||
for name, microagent in knowledge_microagents.items():
|
||||
if name not in self.disabled_microagents:
|
||||
self.knowledge_microagents[name] = microagent
|
||||
assert all(
|
||||
isinstance(microagent, RepoMicroAgent)
|
||||
for microagent in repo_microagents.values()
|
||||
)
|
||||
for name, microagent in repo_microagents.items():
|
||||
if name not in self.disabled_microagents:
|
||||
self.repo_microagents[name] = microagent
|
||||
|
||||
def load_microagent_files(self, microagent_files: list[str]):
|
||||
for microagent_file in microagent_files:
|
||||
microagent = MicroAgent(content=microagent_file)
|
||||
self.microagents[microagent.name] = microagent
|
||||
def load_microagents(self, microagents: list[BaseMicroAgent]):
|
||||
# Only keep KnowledgeMicroAgents and RepoMicroAgents
|
||||
for microagent in microagents:
|
||||
if microagent.name in self.disabled_microagents:
|
||||
continue
|
||||
if isinstance(microagent, KnowledgeMicroAgent):
|
||||
self.knowledge_microagents[microagent.name] = microagent
|
||||
elif isinstance(microagent, RepoMicroAgent):
|
||||
self.repo_microagents[microagent.name] = microagent
|
||||
|
||||
def _load_template(self, template_name: str) -> Template:
|
||||
if self.prompt_dir is None:
|
||||
@ -65,7 +83,10 @@ class PromptManager:
|
||||
|
||||
def get_system_message(self) -> str:
|
||||
repo_instructions = ''
|
||||
for microagent in self.microagents.values():
|
||||
assert (
|
||||
len(self.repo_microagents) <= 1
|
||||
), f'Expecting at most one repo microagent, but found {len(self.repo_microagents)}: {self.repo_microagents.keys()}'
|
||||
for microagent in self.repo_microagents.values():
|
||||
# We assume these are the repo instructions
|
||||
if len(microagent.triggers) == 0:
|
||||
if repo_instructions:
|
||||
@ -95,8 +116,8 @@ class PromptManager:
|
||||
if not message.content:
|
||||
return
|
||||
message_content = message.content[0].text
|
||||
for microagent in self.microagents.values():
|
||||
trigger = microagent.get_trigger(message_content)
|
||||
for microagent in self.knowledge_microagents.values():
|
||||
trigger = microagent.match_trigger(message_content)
|
||||
if trigger:
|
||||
micro_text = f'<extra_info>\nThe following information has been included based on a keyword match for "{trigger}". It may or may not be relevant to the user\'s request.'
|
||||
micro_text += '\n\n' + microagent.content
|
||||
|
||||
@ -39,7 +39,8 @@ from openhands.llm.llm import LLM
|
||||
|
||||
@pytest.fixture
|
||||
def agent() -> CodeActAgent:
|
||||
agent = CodeActAgent(llm=LLM(LLMConfig()), config=AgentConfig())
|
||||
config = AgentConfig()
|
||||
agent = CodeActAgent(llm=LLM(LLMConfig()), config=config)
|
||||
agent.llm = Mock()
|
||||
agent.llm.config = Mock()
|
||||
agent.llm.config.max_message_chars = 100
|
||||
|
||||
@ -1,31 +1,145 @@
|
||||
import os
|
||||
"""Tests for the microagent system."""
|
||||
|
||||
from pytest import MonkeyPatch
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import openhands.agenthub # noqa: F401
|
||||
from openhands.utils.microagent import MicroAgent
|
||||
import pytest
|
||||
|
||||
from openhands.core.exceptions import MicroAgentValidationError
|
||||
from openhands.microagent import (
|
||||
BaseMicroAgent,
|
||||
KnowledgeMicroAgent,
|
||||
MicroAgentMetadata,
|
||||
MicroAgentType,
|
||||
RepoMicroAgent,
|
||||
TaskMicroAgent,
|
||||
load_microagents_from_dir,
|
||||
)
|
||||
|
||||
CONTENT = (
|
||||
'# dummy header\n' 'dummy content\n' '## dummy subheader\n' 'dummy subcontent\n'
|
||||
)
|
||||
|
||||
|
||||
def test_micro_agent_load(tmp_path, monkeypatch: MonkeyPatch):
|
||||
with open(os.path.join(tmp_path, 'dummy.md'), 'w') as f:
|
||||
f.write(
|
||||
(
|
||||
'---\n'
|
||||
'name: dummy\n'
|
||||
'agent: CodeActAgent\n'
|
||||
'require_env_var:\n'
|
||||
' SANDBOX_OPENHANDS_TEST_ENV_VAR: "Set this environment variable for testing purposes"\n'
|
||||
'---\n' + CONTENT
|
||||
)
|
||||
)
|
||||
def test_legacy_micro_agent_load(tmp_path):
|
||||
"""Test loading of legacy microagents."""
|
||||
legacy_file = tmp_path / '.openhands_instructions'
|
||||
legacy_file.write_text(CONTENT)
|
||||
|
||||
# Patch the required environment variable
|
||||
monkeypatch.setenv('SANDBOX_OPENHANDS_TEST_ENV_VAR', 'dummy_value')
|
||||
micro_agent = BaseMicroAgent.load(legacy_file)
|
||||
assert isinstance(micro_agent, RepoMicroAgent)
|
||||
assert micro_agent.name == 'repo_legacy'
|
||||
assert micro_agent.content == CONTENT
|
||||
assert micro_agent.type == MicroAgentType.REPO_KNOWLEDGE
|
||||
|
||||
micro_agent = MicroAgent(os.path.join(tmp_path, 'dummy.md'))
|
||||
assert micro_agent is not None
|
||||
assert micro_agent.content == CONTENT.strip()
|
||||
|
||||
@pytest.fixture
|
||||
def temp_microagents_dir():
|
||||
"""Create a temporary directory with test microagents."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
|
||||
# Create test knowledge agent
|
||||
knowledge_agent = """---
|
||||
name: test_knowledge_agent
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- test
|
||||
- pytest
|
||||
---
|
||||
|
||||
# Test Guidelines
|
||||
|
||||
Testing best practices and guidelines.
|
||||
"""
|
||||
(root / 'knowledge.md').write_text(knowledge_agent)
|
||||
|
||||
# Create test repo agent
|
||||
repo_agent = """---
|
||||
name: test_repo_agent
|
||||
type: repo
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
---
|
||||
|
||||
# Test Repository Agent
|
||||
|
||||
Repository-specific test instructions.
|
||||
"""
|
||||
(root / 'repo.md').write_text(repo_agent)
|
||||
|
||||
# Create test task agent
|
||||
task_agent = """---
|
||||
name: test_task
|
||||
type: task
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
---
|
||||
|
||||
# Test Task
|
||||
|
||||
Test task content
|
||||
"""
|
||||
(root / 'task.md').write_text(task_agent)
|
||||
|
||||
yield root
|
||||
|
||||
|
||||
def test_knowledge_agent():
|
||||
"""Test knowledge agent functionality."""
|
||||
agent = KnowledgeMicroAgent(
|
||||
name='test',
|
||||
content='Test content',
|
||||
metadata=MicroAgentMetadata(
|
||||
name='test', type=MicroAgentType.KNOWLEDGE, triggers=['test', 'pytest']
|
||||
),
|
||||
source='test.md',
|
||||
type=MicroAgentType.KNOWLEDGE,
|
||||
)
|
||||
|
||||
assert agent.match_trigger('running a test') == 'test'
|
||||
assert agent.match_trigger('using pytest') == 'test'
|
||||
assert agent.match_trigger('no match here') is None
|
||||
assert agent.triggers == ['test', 'pytest']
|
||||
|
||||
|
||||
def test_load_microagents(temp_microagents_dir):
|
||||
"""Test loading microagents from directory."""
|
||||
repo_agents, knowledge_agents, task_agents = load_microagents_from_dir(
|
||||
temp_microagents_dir
|
||||
)
|
||||
|
||||
# Check knowledge agents
|
||||
assert len(knowledge_agents) == 1
|
||||
agent = knowledge_agents['test_knowledge_agent']
|
||||
assert isinstance(agent, KnowledgeMicroAgent)
|
||||
assert 'test' in agent.triggers
|
||||
|
||||
# Check repo agents
|
||||
assert len(repo_agents) == 1
|
||||
agent = repo_agents['test_repo_agent']
|
||||
assert isinstance(agent, RepoMicroAgent)
|
||||
|
||||
# Check task agents
|
||||
assert len(task_agents) == 1
|
||||
agent = task_agents['test_task']
|
||||
assert isinstance(agent, TaskMicroAgent)
|
||||
|
||||
|
||||
def test_invalid_agent_type(temp_microagents_dir):
|
||||
"""Test loading agent with invalid type."""
|
||||
invalid_agent = """---
|
||||
name: test_invalid
|
||||
type: invalid
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
---
|
||||
|
||||
Invalid agent content
|
||||
"""
|
||||
(temp_microagents_dir / 'invalid.md').write_text(invalid_agent)
|
||||
|
||||
with pytest.raises(MicroAgentValidationError):
|
||||
BaseMicroAgent.load(temp_microagents_dir / 'invalid.md')
|
||||
|
||||
@ -24,7 +24,8 @@ def mock_llm():
|
||||
@pytest.fixture
|
||||
def codeact_agent(mock_llm):
|
||||
config = AgentConfig()
|
||||
return CodeActAgent(mock_llm, config)
|
||||
agent = CodeActAgent(mock_llm, config)
|
||||
return agent
|
||||
|
||||
|
||||
def response_mock(content: str, tool_call_id: str):
|
||||
|
||||
@ -4,7 +4,7 @@ import shutil
|
||||
import pytest
|
||||
|
||||
from openhands.core.message import Message, TextContent
|
||||
from openhands.utils.microagent import MicroAgent
|
||||
from openhands.microagent import BaseMicroAgent
|
||||
from openhands.utils.prompt import PromptManager
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ def test_prompt_manager_with_microagent(prompt_dir):
|
||||
microagent_content = """
|
||||
---
|
||||
name: flarglebargle
|
||||
type: knowledge
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- flarglebargle
|
||||
@ -44,7 +45,8 @@ only respond with a message telling them how smart they are
|
||||
)
|
||||
|
||||
assert manager.prompt_dir == prompt_dir
|
||||
assert len(manager.microagents) == 1
|
||||
assert len(manager.repo_microagents) == 0
|
||||
assert len(manager.knowledge_microagents) == 1
|
||||
|
||||
assert isinstance(manager.get_system_message(), str)
|
||||
assert (
|
||||
@ -66,7 +68,9 @@ only respond with a message telling them how smart they are
|
||||
|
||||
def test_prompt_manager_file_not_found(prompt_dir):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
MicroAgent(os.path.join(prompt_dir, 'micro', 'non_existent_microagent.md'))
|
||||
BaseMicroAgent.load(
|
||||
os.path.join(prompt_dir, 'micro', 'non_existent_microagent.md')
|
||||
)
|
||||
|
||||
|
||||
def test_prompt_manager_template_rendering(prompt_dir):
|
||||
@ -93,6 +97,7 @@ def test_prompt_manager_disabled_microagents(prompt_dir):
|
||||
microagent1_content = """
|
||||
---
|
||||
name: Test Microagent 1
|
||||
type: knowledge
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- test1
|
||||
@ -103,6 +108,7 @@ Test microagent 1 content
|
||||
microagent2_content = """
|
||||
---
|
||||
name: Test Microagent 2
|
||||
type: knowledge
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- test2
|
||||
@ -125,9 +131,9 @@ Test microagent 2 content
|
||||
disabled_microagents=['Test Microagent 1'],
|
||||
)
|
||||
|
||||
assert len(manager.microagents) == 1
|
||||
assert 'Test Microagent 2' in manager.microagents
|
||||
assert 'Test Microagent 1' not in manager.microagents
|
||||
assert len(manager.knowledge_microagents) == 1
|
||||
assert 'Test Microagent 2' in manager.knowledge_microagents
|
||||
assert 'Test Microagent 1' not in manager.knowledge_microagents
|
||||
|
||||
# Test that all microagents are enabled by default
|
||||
manager = PromptManager(
|
||||
@ -135,9 +141,9 @@ Test microagent 2 content
|
||||
microagent_dir=os.path.join(prompt_dir, 'micro'),
|
||||
)
|
||||
|
||||
assert len(manager.microagents) == 2
|
||||
assert 'Test Microagent 1' in manager.microagents
|
||||
assert 'Test Microagent 2' in manager.microagents
|
||||
assert len(manager.knowledge_microagents) == 2
|
||||
assert 'Test Microagent 1' in manager.knowledge_microagents
|
||||
assert 'Test Microagent 2' in manager.knowledge_microagents
|
||||
|
||||
# Clean up temporary files
|
||||
os.remove(os.path.join(prompt_dir, 'micro', f'{microagent1_name}.md'))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user