mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat: Add user directory support for microagents (#9333)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
94fe052561
commit
0fb1a712d5
@ -2,6 +2,7 @@ import asyncio
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import openhands
|
||||
@ -33,6 +34,8 @@ GLOBAL_MICROAGENTS_DIR = os.path.join(
|
||||
'microagents',
|
||||
)
|
||||
|
||||
USER_MICROAGENTS_DIR = Path.home() / '.openhands' / 'microagents'
|
||||
|
||||
|
||||
class Memory:
|
||||
"""
|
||||
@ -77,6 +80,9 @@ class Memory:
|
||||
# from typically OpenHands/microagents (i.e., the PUBLIC microagents)
|
||||
self._load_global_microagents()
|
||||
|
||||
# Load user microagents from ~/.openhands/microagents/
|
||||
self._load_user_microagents()
|
||||
|
||||
def on_event(self, event: Event):
|
||||
"""Handle an event from the event stream."""
|
||||
asyncio.get_event_loop().run_until_complete(self._on_event(event))
|
||||
@ -274,6 +280,41 @@ class Memory:
|
||||
if isinstance(r_agent, RepoMicroagent):
|
||||
self.repo_microagents[name] = r_agent
|
||||
|
||||
def _load_user_microagents(self) -> None:
|
||||
"""
|
||||
Loads microagents from the user's home directory (~/.openhands/microagents/)
|
||||
Creates the directory if it doesn't exist.
|
||||
"""
|
||||
try:
|
||||
# Create the user microagents directory if it doesn't exist
|
||||
os.makedirs(USER_MICROAGENTS_DIR, exist_ok=True)
|
||||
|
||||
# Load microagents from user directory
|
||||
repo_agents, knowledge_agents = load_microagents_from_dir(
|
||||
USER_MICROAGENTS_DIR
|
||||
)
|
||||
|
||||
# Add user microagents to the collections
|
||||
# User microagents can override global ones with the same name
|
||||
for name, agent in knowledge_agents.items():
|
||||
if isinstance(agent, KnowledgeMicroagent):
|
||||
self.knowledge_microagents[name] = agent
|
||||
logger.debug(f'Loaded user knowledge microagent: {name}')
|
||||
|
||||
for name, agent in repo_agents.items():
|
||||
if isinstance(agent, RepoMicroagent):
|
||||
self.repo_microagents[name] = agent
|
||||
logger.debug(f'Loaded user repo microagent: {name}')
|
||||
|
||||
if repo_agents or knowledge_agents:
|
||||
logger.info(
|
||||
f'Loaded {len(repo_agents) + len(knowledge_agents)} user microagents from {USER_MICROAGENTS_DIR}'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f'Failed to load user microagents from {USER_MICROAGENTS_DIR}: {str(e)}'
|
||||
)
|
||||
|
||||
def get_microagent_mcp_tools(self) -> list[MCPConfig]:
|
||||
"""
|
||||
Get MCP tools from all repo microagents (always active)
|
||||
|
||||
217
tests/unit/test_user_microagents.py
Normal file
217
tests/unit/test_user_microagents.py
Normal file
@ -0,0 +1,217 @@
|
||||
"""Tests for user directory microagent loading."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.memory.memory import Memory
|
||||
from openhands.microagent import KnowledgeMicroagent, MicroagentType, RepoMicroagent
|
||||
from openhands.storage import get_file_store
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_user_microagents_dir():
|
||||
"""Create a temporary directory to simulate ~/.openhands/microagents/."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
user_dir = Path(temp_dir)
|
||||
|
||||
# Create test knowledge agent
|
||||
knowledge_agent = """---
|
||||
name: user_knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- user-test
|
||||
- personal
|
||||
---
|
||||
|
||||
# User Knowledge Agent
|
||||
|
||||
Personal knowledge and guidelines.
|
||||
"""
|
||||
(user_dir / 'user_knowledge.md').write_text(knowledge_agent)
|
||||
|
||||
# Create test repo agent
|
||||
repo_agent = """---
|
||||
name: user_repo
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
---
|
||||
|
||||
# User Repository Agent
|
||||
|
||||
Personal repository-specific instructions.
|
||||
"""
|
||||
(user_dir / 'user_repo.md').write_text(repo_agent)
|
||||
|
||||
yield user_dir
|
||||
|
||||
|
||||
def test_user_microagents_loading(temp_user_microagents_dir):
|
||||
"""Test that user microagents are loaded from ~/.openhands/microagents/."""
|
||||
with patch(
|
||||
'openhands.memory.memory.USER_MICROAGENTS_DIR', str(temp_user_microagents_dir)
|
||||
):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create event stream and memory
|
||||
file_store = get_file_store('local', temp_dir)
|
||||
event_stream = EventStream('test', file_store)
|
||||
memory = Memory(event_stream, 'test_sid')
|
||||
|
||||
# Check that user microagents were loaded
|
||||
assert 'user_knowledge' in memory.knowledge_microagents
|
||||
assert 'user_repo' in memory.repo_microagents
|
||||
|
||||
# Verify the loaded agents
|
||||
user_knowledge = memory.knowledge_microagents['user_knowledge']
|
||||
assert isinstance(user_knowledge, KnowledgeMicroagent)
|
||||
assert user_knowledge.type == MicroagentType.KNOWLEDGE
|
||||
assert 'user-test' in user_knowledge.triggers
|
||||
assert 'personal' in user_knowledge.triggers
|
||||
|
||||
user_repo = memory.repo_microagents['user_repo']
|
||||
assert isinstance(user_repo, RepoMicroagent)
|
||||
assert user_repo.type == MicroagentType.REPO_KNOWLEDGE
|
||||
|
||||
|
||||
def test_user_microagents_directory_creation():
|
||||
"""Test that user microagents directory is created if it doesn't exist."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
non_existent_dir = Path(temp_dir) / 'non_existent' / 'microagents'
|
||||
|
||||
with patch(
|
||||
'openhands.memory.memory.USER_MICROAGENTS_DIR', str(non_existent_dir)
|
||||
):
|
||||
with tempfile.TemporaryDirectory() as temp_store_dir:
|
||||
# Create event stream and memory
|
||||
file_store = get_file_store('local', temp_store_dir)
|
||||
event_stream = EventStream('test', file_store)
|
||||
Memory(event_stream, 'test_sid')
|
||||
|
||||
# Check that the directory was created
|
||||
assert non_existent_dir.exists()
|
||||
assert non_existent_dir.is_dir()
|
||||
|
||||
|
||||
def test_user_microagents_override_global():
|
||||
"""Test that user microagents can override global ones with the same name."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
user_dir = Path(temp_dir)
|
||||
|
||||
# Create a user microagent with the same name as a global one
|
||||
# (assuming there's a global 'github' microagent)
|
||||
github_agent = """---
|
||||
name: github
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- github
|
||||
- git
|
||||
---
|
||||
|
||||
# Personal GitHub Agent
|
||||
|
||||
My personal GitHub workflow and preferences.
|
||||
"""
|
||||
(user_dir / 'github.md').write_text(github_agent)
|
||||
|
||||
with patch('openhands.memory.memory.USER_MICROAGENTS_DIR', str(user_dir)):
|
||||
with tempfile.TemporaryDirectory() as temp_store_dir:
|
||||
# Create event stream and memory
|
||||
file_store = get_file_store('local', temp_store_dir)
|
||||
event_stream = EventStream('test', file_store)
|
||||
memory = Memory(event_stream, 'test_sid')
|
||||
|
||||
# Check that the user microagent is loaded
|
||||
if 'github' in memory.knowledge_microagents:
|
||||
github_microagent = memory.knowledge_microagents['github']
|
||||
# The user version should contain our personal content
|
||||
assert 'My personal GitHub workflow' in github_microagent.content
|
||||
|
||||
|
||||
def test_user_microagents_loading_error_handling():
|
||||
"""Test error handling when user microagents directory has issues."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
user_dir = Path(temp_dir)
|
||||
|
||||
# Create an invalid microagent file
|
||||
invalid_agent = """---
|
||||
name: invalid
|
||||
type: invalid_type
|
||||
---
|
||||
|
||||
# Invalid Agent
|
||||
"""
|
||||
(user_dir / 'invalid.md').write_text(invalid_agent)
|
||||
|
||||
with patch('openhands.memory.memory.USER_MICROAGENTS_DIR', str(user_dir)):
|
||||
with tempfile.TemporaryDirectory() as temp_store_dir:
|
||||
# Create event stream and memory - should not crash
|
||||
file_store = get_file_store('local', temp_store_dir)
|
||||
event_stream = EventStream('test', file_store)
|
||||
memory = Memory(event_stream, 'test_sid')
|
||||
|
||||
# Memory should still be created despite the invalid microagent
|
||||
assert memory is not None
|
||||
# The invalid microagent should not be loaded
|
||||
assert 'invalid' not in memory.knowledge_microagents
|
||||
assert 'invalid' not in memory.repo_microagents
|
||||
|
||||
|
||||
def test_user_microagents_empty_directory():
|
||||
"""Test behavior when user microagents directory is empty."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
empty_dir = Path(temp_dir)
|
||||
|
||||
with patch('openhands.memory.memory.USER_MICROAGENTS_DIR', str(empty_dir)):
|
||||
with tempfile.TemporaryDirectory() as temp_store_dir:
|
||||
# Create event stream and memory
|
||||
file_store = get_file_store('local', temp_store_dir)
|
||||
event_stream = EventStream('test', file_store)
|
||||
memory = Memory(event_stream, 'test_sid')
|
||||
|
||||
# Memory should be created successfully
|
||||
assert memory is not None
|
||||
# No user microagents should be loaded, but global ones might be
|
||||
# (we can't assert the exact count since global microagents may exist)
|
||||
|
||||
|
||||
def test_user_microagents_nested_directories(temp_user_microagents_dir):
|
||||
"""Test loading user microagents from nested directories."""
|
||||
# Create nested microagent
|
||||
nested_dir = temp_user_microagents_dir / 'personal' / 'tools'
|
||||
nested_dir.mkdir(parents=True)
|
||||
|
||||
nested_agent = """---
|
||||
name: personal_tool
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- personal-tool
|
||||
---
|
||||
|
||||
# Personal Tool Agent
|
||||
|
||||
My personal development tools and workflows.
|
||||
"""
|
||||
(nested_dir / 'tool.md').write_text(nested_agent)
|
||||
|
||||
with patch(
|
||||
'openhands.memory.memory.USER_MICROAGENTS_DIR', str(temp_user_microagents_dir)
|
||||
):
|
||||
with tempfile.TemporaryDirectory() as temp_store_dir:
|
||||
# Create event stream and memory
|
||||
file_store = get_file_store('local', temp_store_dir)
|
||||
event_stream = EventStream('test', file_store)
|
||||
memory = Memory(event_stream, 'test_sid')
|
||||
|
||||
# Check that nested microagent was loaded
|
||||
# The name should be derived from the relative path
|
||||
assert 'personal/tools/tool' in memory.knowledge_microagents
|
||||
|
||||
nested_microagent = memory.knowledge_microagents['personal/tools/tool']
|
||||
assert isinstance(nested_microagent, KnowledgeMicroagent)
|
||||
assert 'personal-tool' in nested_microagent.triggers
|
||||
Loading…
x
Reference in New Issue
Block a user