mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
Fix microagent loading with trailing slashes and nested directories (#6239)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -8,6 +8,7 @@ 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
|
||||
|
||||
|
||||
@@ -132,8 +133,10 @@ def load_microagents_from_dir(
|
||||
]:
|
||||
"""Load all microagents from the given directory.
|
||||
|
||||
Note, legacy repo instructions will not be loaded here.
|
||||
|
||||
Args:
|
||||
microagent_dir: Path to the microagents directory.
|
||||
microagent_dir: Path to the microagents directory (e.g. .openhands/microagents)
|
||||
|
||||
Returns:
|
||||
Tuple of (repo_agents, knowledge_agents, task_agents) dictionaries
|
||||
@@ -145,20 +148,24 @@ def load_microagents_from_dir(
|
||||
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}')
|
||||
# Load all agents from .openhands/microagents directory
|
||||
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}...')
|
||||
# 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
|
||||
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
|
||||
|
||||
@@ -610,10 +610,14 @@ def parse_unified_diff(text):
|
||||
# - Start at line 1 in the old file and show 6 lines
|
||||
# - Start at line 1 in the new file and show 6 lines
|
||||
old = int(h.group(1)) # Starting line in old file
|
||||
old_len = int(h.group(2)) if len(h.group(2)) > 0 else 1 # Number of lines in old file
|
||||
old_len = (
|
||||
int(h.group(2)) if len(h.group(2)) > 0 else 1
|
||||
) # Number of lines in old file
|
||||
|
||||
new = int(h.group(3)) # Starting line in new file
|
||||
new_len = int(h.group(4)) if len(h.group(4)) > 0 else 1 # Number of lines in new file
|
||||
new_len = (
|
||||
int(h.group(4)) if len(h.group(4)) > 0 else 1
|
||||
) # Number of lines in new file
|
||||
|
||||
h = None
|
||||
break
|
||||
@@ -622,7 +626,9 @@ def parse_unified_diff(text):
|
||||
for n in hunk:
|
||||
# Each line in a unified diff starts with a space (context), + (addition), or - (deletion)
|
||||
# The first character is the kind, the rest is the line content
|
||||
kind = n[0] if len(n) > 0 else ' ' # Empty lines in the hunk are treated as context lines
|
||||
kind = (
|
||||
n[0] if len(n) > 0 else ' '
|
||||
) # Empty lines in the hunk are treated as context lines
|
||||
line = n[1:] if len(n) > 1 else ''
|
||||
|
||||
# Process the line based on its kind
|
||||
|
||||
@@ -4,10 +4,13 @@ import copy
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import string
|
||||
import tempfile
|
||||
from abc import abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
from zipfile import ZipFile
|
||||
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
@@ -37,9 +40,7 @@ from openhands.events.observation import (
|
||||
from openhands.events.serialization.action import ACTION_TYPE_TO_CLASS
|
||||
from openhands.microagent import (
|
||||
BaseMicroAgent,
|
||||
KnowledgeMicroAgent,
|
||||
RepoMicroAgent,
|
||||
TaskMicroAgent,
|
||||
load_microagents_from_dir,
|
||||
)
|
||||
from openhands.runtime.plugins import (
|
||||
JupyterRequirement,
|
||||
@@ -228,21 +229,37 @@ class Runtime(FileEditRuntimeMixin):
|
||||
def get_microagents_from_selected_repo(
|
||||
self, selected_repository: str | None
|
||||
) -> list[BaseMicroAgent]:
|
||||
"""Load microagents from the selected repository.
|
||||
If selected_repository is None, load microagents from the current workspace.
|
||||
|
||||
This is the main entry point for loading microagents.
|
||||
"""
|
||||
|
||||
loaded_microagents: list[BaseMicroAgent] = []
|
||||
dir_name = Path('.openhands') / 'microagents'
|
||||
workspace_root = Path(self.config.workspace_mount_path_in_sandbox)
|
||||
microagents_dir = workspace_root / '.openhands' / 'microagents'
|
||||
repo_root = None
|
||||
if selected_repository:
|
||||
dir_name = Path('/workspace') / selected_repository.split('/')[1] / dir_name
|
||||
repo_root = workspace_root / selected_repository.split('/')[1]
|
||||
microagents_dir = repo_root / '.openhands' / 'microagents'
|
||||
self.log(
|
||||
'info',
|
||||
f'Selected repo: {selected_repository}, loading microagents from {microagents_dir} (inside runtime)',
|
||||
)
|
||||
|
||||
# Legacy Repo Instructions
|
||||
# Check for legacy .openhands_instructions file
|
||||
obs = self.read(FileReadAction(path='.openhands_instructions'))
|
||||
if isinstance(obs, ErrorObservation):
|
||||
obs = self.read(
|
||||
FileReadAction(path=str(workspace_root / '.openhands_instructions'))
|
||||
)
|
||||
if isinstance(obs, ErrorObservation) and repo_root is not None:
|
||||
# If the instructions file is not found in the workspace root, try to load it from the repo root
|
||||
self.log(
|
||||
'debug',
|
||||
f'openhands_instructions not present, trying to load from {dir_name}',
|
||||
f'.openhands_instructions not present, trying to load from repository {microagents_dir=}',
|
||||
)
|
||||
obs = self.read(
|
||||
FileReadAction(path=str(dir_name / '.openhands_instructions'))
|
||||
FileReadAction(path=str(repo_root / '.openhands_instructions'))
|
||||
)
|
||||
|
||||
if isinstance(obs, FileReadObservation):
|
||||
@@ -253,44 +270,40 @@ class Runtime(FileEditRuntimeMixin):
|
||||
)
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
)
|
||||
# Load microagents from directory
|
||||
files = self.list_files(str(microagents_dir))
|
||||
if files:
|
||||
self.log('info', f'Found {len(files)} files in microagents directory.')
|
||||
zip_path = self.copy_from(str(microagents_dir))
|
||||
microagent_folder = tempfile.mkdtemp()
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
# Properly handle the zip file
|
||||
with ZipFile(zip_path, 'r') as zip_file:
|
||||
zip_file.extractall(microagent_folder)
|
||||
|
||||
# Add debug print of directory structure
|
||||
self.log('debug', 'Microagent folder structure:')
|
||||
for root, _, files in os.walk(microagent_folder):
|
||||
relative_path = os.path.relpath(root, microagent_folder)
|
||||
self.log('debug', f'Directory: {relative_path}/')
|
||||
for file in files:
|
||||
self.log('debug', f' File: {os.path.join(relative_path, file)}')
|
||||
|
||||
# Clean up the temporary zip file
|
||||
zip_path.unlink()
|
||||
# Load all microagents using the existing function
|
||||
repo_agents, knowledge_agents, task_agents = load_microagents_from_dir(
|
||||
microagent_folder
|
||||
)
|
||||
self.log(
|
||||
'info',
|
||||
f'Loaded {len(repo_agents)} repo agents, {len(knowledge_agents)} knowledge agents, and {len(task_agents)} task agents',
|
||||
)
|
||||
loaded_microagents.extend(repo_agents.values())
|
||||
loaded_microagents.extend(knowledge_agents.values())
|
||||
loaded_microagents.extend(task_agents.values())
|
||||
shutil.rmtree(microagent_folder)
|
||||
|
||||
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:
|
||||
|
||||
@@ -26,7 +26,7 @@ test_mount_path = ''
|
||||
project_dir = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
sandbox_test_folder = '/openhands/workspace'
|
||||
sandbox_test_folder = '/workspace'
|
||||
|
||||
|
||||
def _get_runtime_sid(runtime: Runtime) -> str:
|
||||
@@ -233,9 +233,10 @@ def _load_runtime(
|
||||
if use_workspace:
|
||||
test_mount_path = os.path.join(config.workspace_base, 'rt')
|
||||
elif temp_dir is not None:
|
||||
test_mount_path = os.path.join(temp_dir, sid)
|
||||
test_mount_path = temp_dir
|
||||
else:
|
||||
test_mount_path = None
|
||||
config.workspace_base = test_mount_path
|
||||
config.workspace_mount_path = test_mount_path
|
||||
|
||||
# Mounting folder specific for this test inside the sandbox
|
||||
|
||||
@@ -210,7 +210,7 @@ done && echo "success"
|
||||
def test_cmd_run(temp_dir, runtime_cls, run_as_openhands):
|
||||
runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
|
||||
try:
|
||||
obs = _run_cmd_action(runtime, 'ls -l /openhands/workspace')
|
||||
obs = _run_cmd_action(runtime, 'ls -l /workspace')
|
||||
assert obs.exit_code == 0
|
||||
|
||||
obs = _run_cmd_action(runtime, 'ls -l')
|
||||
@@ -377,7 +377,7 @@ def test_copy_to_non_existent_directory(temp_dir, runtime_cls):
|
||||
def test_overwrite_existing_file(temp_dir, runtime_cls):
|
||||
runtime = _load_runtime(temp_dir, runtime_cls)
|
||||
try:
|
||||
sandbox_dir = '/openhands/workspace'
|
||||
sandbox_dir = '/workspace'
|
||||
|
||||
obs = _run_cmd_action(runtime, f'ls -alh {sandbox_dir}')
|
||||
assert obs.exit_code == 0
|
||||
|
||||
@@ -52,7 +52,7 @@ def test_simple_cmd_ipython_and_fileop(temp_dir, runtime_cls, run_as_openhands):
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert obs.content.strip() == (
|
||||
'Hello, `World`!\n'
|
||||
'[Jupyter current working directory: /openhands/workspace]\n'
|
||||
'[Jupyter current working directory: /workspace]\n'
|
||||
'[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ def test_simple_cmd_ipython_and_fileop(temp_dir, runtime_cls, run_as_openhands):
|
||||
|
||||
assert obs.content == ''
|
||||
# event stream runtime will always use absolute path
|
||||
assert obs.path == '/openhands/workspace/hello.sh'
|
||||
assert obs.path == '/workspace/hello.sh'
|
||||
|
||||
# Test read file (file should exist)
|
||||
action_read = FileReadAction(path='hello.sh')
|
||||
@@ -85,7 +85,7 @@ def test_simple_cmd_ipython_and_fileop(temp_dir, runtime_cls, run_as_openhands):
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
assert obs.content == 'echo "Hello, World!"\n'
|
||||
assert obs.path == '/openhands/workspace/hello.sh'
|
||||
assert obs.path == '/workspace/hello.sh'
|
||||
|
||||
# clean up
|
||||
action = CmdRunAction(command='rm -rf hello.sh')
|
||||
@@ -188,7 +188,7 @@ def test_ipython_simple(temp_dir, runtime_cls):
|
||||
obs.content.strip()
|
||||
== (
|
||||
'1\n'
|
||||
'[Jupyter current working directory: /openhands/workspace]\n'
|
||||
'[Jupyter current working directory: /workspace]\n'
|
||||
'[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
|
||||
).strip()
|
||||
)
|
||||
@@ -224,7 +224,7 @@ def test_ipython_package_install(temp_dir, runtime_cls, run_as_openhands):
|
||||
# import should not error out
|
||||
assert obs.content.strip() == (
|
||||
'[Code executed successfully with no output]\n'
|
||||
'[Jupyter current working directory: /openhands/workspace]\n'
|
||||
'[Jupyter current working directory: /workspace]\n'
|
||||
'[Jupyter Python interpreter: /openhands/poetry/openhands-ai-5O4_aCHf-py3.12/bin/python]'
|
||||
)
|
||||
|
||||
@@ -273,16 +273,16 @@ def test_ipython_file_editor_permissions_as_openhands(temp_dir, runtime_cls):
|
||||
# Try to use file editor in openhands sandbox directory - should work
|
||||
test_code = """
|
||||
# Create file
|
||||
print(file_editor(command='create', path='/openhands/workspace/test.txt', file_text='Line 1\\nLine 2\\nLine 3'))
|
||||
print(file_editor(command='create', path='/workspace/test.txt', file_text='Line 1\\nLine 2\\nLine 3'))
|
||||
|
||||
# View file
|
||||
print(file_editor(command='view', path='/openhands/workspace/test.txt'))
|
||||
print(file_editor(command='view', path='/workspace/test.txt'))
|
||||
|
||||
# Edit file
|
||||
print(file_editor(command='str_replace', path='/openhands/workspace/test.txt', old_str='Line 2', new_str='New Line 2'))
|
||||
print(file_editor(command='str_replace', path='/workspace/test.txt', old_str='Line 2', new_str='New Line 2'))
|
||||
|
||||
# Undo edit
|
||||
print(file_editor(command='undo_edit', path='/openhands/workspace/test.txt'))
|
||||
print(file_editor(command='undo_edit', path='/workspace/test.txt'))
|
||||
"""
|
||||
action = IPythonRunCellAction(code=test_code)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
@@ -297,7 +297,7 @@ print(file_editor(command='undo_edit', path='/openhands/workspace/test.txt'))
|
||||
assert 'undone successfully' in obs.content
|
||||
|
||||
# Clean up
|
||||
action = CmdRunAction(command='rm -f /openhands/workspace/test.txt')
|
||||
action = CmdRunAction(command='rm -f /workspace/test.txt')
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
@@ -314,7 +314,7 @@ print(file_editor(command='undo_edit', path='/openhands/workspace/test.txt'))
|
||||
|
||||
def test_file_read_and_edit_via_oh_aci(runtime_cls, run_as_openhands):
|
||||
runtime = _load_runtime(None, runtime_cls, run_as_openhands)
|
||||
sandbox_dir = '/openhands/workspace'
|
||||
sandbox_dir = '/workspace'
|
||||
|
||||
actions = [
|
||||
{
|
||||
|
||||
197
tests/runtime/test_microagent.py
Normal file
197
tests/runtime/test_microagent.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""Tests for microagent loading in runtime."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from conftest import (
|
||||
_close_test_runtime,
|
||||
_load_runtime,
|
||||
)
|
||||
|
||||
from openhands.microagent import KnowledgeMicroAgent, RepoMicroAgent, TaskMicroAgent
|
||||
|
||||
|
||||
def _create_test_microagents(test_dir: str):
|
||||
"""Create test microagent files in the given directory."""
|
||||
microagents_dir = Path(test_dir) / '.openhands' / 'microagents'
|
||||
microagents_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create test knowledge agent
|
||||
knowledge_dir = microagents_dir / 'knowledge'
|
||||
knowledge_dir.mkdir(exist_ok=True)
|
||||
knowledge_agent = """---
|
||||
name: test_knowledge_agent
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- test
|
||||
- pytest
|
||||
---
|
||||
|
||||
# Test Guidelines
|
||||
|
||||
Testing best practices and guidelines.
|
||||
"""
|
||||
(knowledge_dir / '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.
|
||||
"""
|
||||
(microagents_dir / 'repo.md').write_text(repo_agent)
|
||||
|
||||
# Create test task agent in a nested directory
|
||||
task_dir = microagents_dir / 'tasks' / 'nested'
|
||||
task_dir.mkdir(parents=True, exist_ok=True)
|
||||
task_agent = """---
|
||||
name: test_task
|
||||
type: task
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
---
|
||||
|
||||
# Test Task
|
||||
|
||||
Test task content
|
||||
"""
|
||||
(task_dir / 'task.md').write_text(task_agent)
|
||||
|
||||
# Create legacy repo instructions
|
||||
legacy_instructions = """# Legacy Instructions
|
||||
|
||||
These are legacy repository instructions.
|
||||
"""
|
||||
(Path(test_dir) / '.openhands_instructions').write_text(legacy_instructions)
|
||||
|
||||
|
||||
def test_load_microagents_with_trailing_slashes(
|
||||
temp_dir, runtime_cls, run_as_openhands
|
||||
):
|
||||
"""Test loading microagents when directory paths have trailing slashes."""
|
||||
# Create test files
|
||||
_create_test_microagents(temp_dir)
|
||||
runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
|
||||
try:
|
||||
# Load microagents
|
||||
loaded_agents = runtime.get_microagents_from_selected_repo(None)
|
||||
|
||||
# Verify all agents are loaded
|
||||
knowledge_agents = [
|
||||
a for a in loaded_agents if isinstance(a, KnowledgeMicroAgent)
|
||||
]
|
||||
repo_agents = [a for a in loaded_agents if isinstance(a, RepoMicroAgent)]
|
||||
task_agents = [a for a in loaded_agents if isinstance(a, TaskMicroAgent)]
|
||||
|
||||
# Check knowledge agents
|
||||
assert len(knowledge_agents) == 1
|
||||
agent = knowledge_agents[0]
|
||||
assert agent.name == 'test_knowledge_agent'
|
||||
assert 'test' in agent.triggers
|
||||
assert 'pytest' in agent.triggers
|
||||
|
||||
# Check repo agents (including legacy)
|
||||
assert len(repo_agents) == 2 # repo.md + .openhands_instructions
|
||||
repo_names = {a.name for a in repo_agents}
|
||||
assert 'test_repo_agent' in repo_names
|
||||
assert 'repo_legacy' in repo_names
|
||||
|
||||
# Check task agents
|
||||
assert len(task_agents) == 1
|
||||
agent = task_agents[0]
|
||||
assert agent.name == 'test_task'
|
||||
|
||||
finally:
|
||||
_close_test_runtime(runtime)
|
||||
|
||||
|
||||
def test_load_microagents_with_selected_repo(temp_dir, runtime_cls, run_as_openhands):
|
||||
"""Test loading microagents from a selected repository."""
|
||||
# Create test files in a repository-like structure
|
||||
repo_dir = Path(temp_dir) / 'OpenHands'
|
||||
repo_dir.mkdir(parents=True)
|
||||
_create_test_microagents(str(repo_dir))
|
||||
|
||||
runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
|
||||
try:
|
||||
# Load microagents with selected repository
|
||||
loaded_agents = runtime.get_microagents_from_selected_repo(
|
||||
'All-Hands-AI/OpenHands'
|
||||
)
|
||||
|
||||
# Verify all agents are loaded
|
||||
knowledge_agents = [
|
||||
a for a in loaded_agents if isinstance(a, KnowledgeMicroAgent)
|
||||
]
|
||||
repo_agents = [a for a in loaded_agents if isinstance(a, RepoMicroAgent)]
|
||||
task_agents = [a for a in loaded_agents if isinstance(a, TaskMicroAgent)]
|
||||
|
||||
# Check knowledge agents
|
||||
assert len(knowledge_agents) == 1
|
||||
agent = knowledge_agents[0]
|
||||
assert agent.name == 'test_knowledge_agent'
|
||||
assert 'test' in agent.triggers
|
||||
assert 'pytest' in agent.triggers
|
||||
|
||||
# Check repo agents (including legacy)
|
||||
assert len(repo_agents) == 2 # repo.md + .openhands_instructions
|
||||
repo_names = {a.name for a in repo_agents}
|
||||
assert 'test_repo_agent' in repo_names
|
||||
assert 'repo_legacy' in repo_names
|
||||
|
||||
# Check task agents
|
||||
assert len(task_agents) == 1
|
||||
agent = task_agents[0]
|
||||
assert agent.name == 'test_task'
|
||||
|
||||
finally:
|
||||
_close_test_runtime(runtime)
|
||||
|
||||
|
||||
def test_load_microagents_with_missing_files(temp_dir, runtime_cls, run_as_openhands):
|
||||
"""Test loading microagents when some files are missing."""
|
||||
# Create only repo.md, no other files
|
||||
microagents_dir = Path(temp_dir) / '.openhands' / 'microagents'
|
||||
microagents_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
repo_agent = """---
|
||||
name: test_repo_agent
|
||||
type: repo
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
---
|
||||
|
||||
# Test Repository Agent
|
||||
|
||||
Repository-specific test instructions.
|
||||
"""
|
||||
(microagents_dir / 'repo.md').write_text(repo_agent)
|
||||
|
||||
runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
|
||||
try:
|
||||
# Load microagents
|
||||
loaded_agents = runtime.get_microagents_from_selected_repo(None)
|
||||
|
||||
# Verify only repo agent is loaded
|
||||
knowledge_agents = [
|
||||
a for a in loaded_agents if isinstance(a, KnowledgeMicroAgent)
|
||||
]
|
||||
repo_agents = [a for a in loaded_agents if isinstance(a, RepoMicroAgent)]
|
||||
task_agents = [a for a in loaded_agents if isinstance(a, TaskMicroAgent)]
|
||||
|
||||
assert len(knowledge_agents) == 0
|
||||
assert len(repo_agents) == 1
|
||||
assert len(task_agents) == 0
|
||||
|
||||
agent = repo_agents[0]
|
||||
assert agent.name == 'test_repo_agent'
|
||||
|
||||
finally:
|
||||
_close_test_runtime(runtime)
|
||||
@@ -1,12 +1,10 @@
|
||||
import pytest
|
||||
from openhands.resolver.patching.apply import apply_diff
|
||||
from openhands.resolver.patching.exceptions import HunkApplyException
|
||||
from openhands.resolver.patching.patch import parse_diff, diffobj
|
||||
from openhands.resolver.patching.patch import diffobj, parse_diff
|
||||
|
||||
|
||||
def test_patch_apply_with_empty_lines():
|
||||
# The original file has no indentation and uses \n line endings
|
||||
original_content = "# PR Viewer\n\nThis React application allows you to view open pull requests from GitHub repositories in a GitHub organization. By default, it uses the All-Hands-AI organization.\n\n## Setup"
|
||||
original_content = '# PR Viewer\n\nThis React application allows you to view open pull requests from GitHub repositories in a GitHub organization. By default, it uses the All-Hands-AI organization.\n\n## Setup'
|
||||
|
||||
# The patch has spaces at the start of each line and uses \n line endings
|
||||
patch = """diff --git a/README.md b/README.md
|
||||
@@ -19,18 +17,20 @@ index b760a53..5071727 100644
|
||||
-This React application allows you to view open pull requests from GitHub repositories in a GitHub organization. By default, it uses the All-Hands-AI organization.
|
||||
+This React application was created by Graham Neubig and OpenHands. It allows you to view open pull requests from GitHub repositories in a GitHub organization. By default, it uses the All-Hands-AI organization."""
|
||||
|
||||
print("Original content lines:")
|
||||
print('Original content lines:')
|
||||
for i, line in enumerate(original_content.splitlines(), 1):
|
||||
print(f"{i}: {repr(line)}")
|
||||
print(f'{i}: {repr(line)}')
|
||||
|
||||
print("\nPatch lines:")
|
||||
print('\nPatch lines:')
|
||||
for i, line in enumerate(patch.splitlines(), 1):
|
||||
print(f"{i}: {repr(line)}")
|
||||
print(f'{i}: {repr(line)}')
|
||||
|
||||
changes = parse_diff(patch)
|
||||
print("\nParsed changes:")
|
||||
print('\nParsed changes:')
|
||||
for change in changes:
|
||||
print(f"Change(old={change.old}, new={change.new}, line={repr(change.line)}, hunk={change.hunk})")
|
||||
print(
|
||||
f'Change(old={change.old}, new={change.new}, line={repr(change.line)}, hunk={change.hunk})'
|
||||
)
|
||||
diff = diffobj(header=None, changes=changes, text=patch)
|
||||
|
||||
# Apply the patch
|
||||
@@ -38,10 +38,10 @@ index b760a53..5071727 100644
|
||||
|
||||
# The patch should be applied successfully
|
||||
expected_result = [
|
||||
"# PR Viewer",
|
||||
"",
|
||||
"This React application was created by Graham Neubig and OpenHands. It allows you to view open pull requests from GitHub repositories in a GitHub organization. By default, it uses the All-Hands-AI organization.",
|
||||
"",
|
||||
"## Setup"
|
||||
'# PR Viewer',
|
||||
'',
|
||||
'This React application was created by Graham Neubig and OpenHands. It allows you to view open pull requests from GitHub repositories in a GitHub organization. By default, it uses the All-Hands-AI organization.',
|
||||
'',
|
||||
'## Setup',
|
||||
]
|
||||
assert result == expected_result
|
||||
assert result == expected_result
|
||||
|
||||
@@ -143,3 +143,65 @@ Invalid agent content
|
||||
|
||||
with pytest.raises(MicroAgentValidationError):
|
||||
BaseMicroAgent.load(temp_microagents_dir / 'invalid.md')
|
||||
|
||||
|
||||
def test_load_microagents_with_nested_dirs(temp_microagents_dir):
|
||||
"""Test loading microagents from nested directories."""
|
||||
# Create nested knowledge agent
|
||||
nested_dir = temp_microagents_dir / 'nested' / 'dir'
|
||||
nested_dir.mkdir(parents=True)
|
||||
nested_agent = """---
|
||||
name: nested_knowledge_agent
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- nested
|
||||
---
|
||||
|
||||
# Nested Test Guidelines
|
||||
|
||||
Testing nested directory loading.
|
||||
"""
|
||||
(nested_dir / 'nested.md').write_text(nested_agent)
|
||||
|
||||
repo_agents, knowledge_agents, task_agents = load_microagents_from_dir(
|
||||
temp_microagents_dir
|
||||
)
|
||||
|
||||
# Check that we can find the nested agent
|
||||
assert len(knowledge_agents) == 2 # Original + nested
|
||||
agent = knowledge_agents['nested_knowledge_agent']
|
||||
assert isinstance(agent, KnowledgeMicroAgent)
|
||||
assert 'nested' in agent.triggers
|
||||
|
||||
|
||||
def test_load_microagents_with_trailing_slashes(temp_microagents_dir):
|
||||
"""Test loading microagents when directory paths have trailing slashes."""
|
||||
# Create a directory with trailing slash
|
||||
knowledge_dir = temp_microagents_dir / 'knowledge/'
|
||||
knowledge_dir.mkdir(exist_ok=True)
|
||||
knowledge_agent = """---
|
||||
name: trailing_knowledge_agent
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- trailing
|
||||
---
|
||||
|
||||
# Trailing Slash Test
|
||||
|
||||
Testing loading with trailing slashes.
|
||||
"""
|
||||
(knowledge_dir / 'trailing.md').write_text(knowledge_agent)
|
||||
|
||||
repo_agents, knowledge_agents, task_agents = load_microagents_from_dir(
|
||||
str(temp_microagents_dir) + '/' # Add trailing slash to test
|
||||
)
|
||||
|
||||
# Check that we can find the agent despite trailing slashes
|
||||
assert len(knowledge_agents) == 2 # Original + trailing
|
||||
agent = knowledge_agents['trailing_knowledge_agent']
|
||||
assert isinstance(agent, KnowledgeMicroAgent)
|
||||
assert 'trailing' in agent.triggers
|
||||
|
||||
Reference in New Issue
Block a user