mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 13:52:43 +08:00
Add back microagent files with special handling for user inputs (#8139)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
parent
49939c1f02
commit
34c13c8824
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -5,7 +5,7 @@
|
||||
/frontend/ @rbren @amanape
|
||||
|
||||
# Evaluation code owners
|
||||
/evaluation/ @xingyaoww @neubig
|
||||
/evaluation/ @xingyaoww @neubig
|
||||
|
||||
# Documentation code owners
|
||||
/docs/ @mamoodi
|
||||
|
||||
65
microagents/add_repo_inst.md
Normal file
65
microagents/add_repo_inst.md
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
name: add_repo_inst
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- /add_repo_inst
|
||||
inputs:
|
||||
- name: REPO_FOLDER_NAME
|
||||
description: "Branch for the agent to work on"
|
||||
---
|
||||
|
||||
Please browse the current repository under /workspace/{{ REPO_FOLDER_NAME }}, look at the documentation and relevant code, and understand the purpose of this repository.
|
||||
|
||||
Specifically, I want you to create a `.openhands/microagents/repo.md` file. This file should contain succinct information that summarizes (1) the purpose of this repository, (2) the general setup of this repo, and (3) a brief description of the structure of this repo.
|
||||
|
||||
Here's an example:
|
||||
```markdown
|
||||
---
|
||||
name: repo
|
||||
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).
|
||||
|
||||
## General Setup:
|
||||
To set up the entire repo, including frontend and backend, run `make build`.
|
||||
You don't need to do this unless the user asks you to, or if you're trying to run the entire application.
|
||||
|
||||
Before pushing any changes, you should ensure that any lint errors or simple test errors have been fixed.
|
||||
|
||||
* If you've made changes to the backend, you should run `pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml`
|
||||
* If you've made changes to the frontend, you should run `cd frontend && npm run lint:fix && npm run build ; cd ..`
|
||||
|
||||
If either command fails, it may have automatically fixed some issues. You should fix any issues that weren't automatically fixed,
|
||||
then re-run the command to ensure it passes.
|
||||
|
||||
## Repository Structure
|
||||
Backend:
|
||||
- Located in the `openhands` directory
|
||||
- Testing:
|
||||
- All tests are in `tests/unit/test_*.py`
|
||||
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
|
||||
- Write all tests with pytest
|
||||
|
||||
Frontend:
|
||||
- Located in the `frontend` directory
|
||||
- Prerequisites: A recent version of NodeJS / NPM
|
||||
- Setup: Run `npm install` in the frontend directory
|
||||
- Testing:
|
||||
- Run tests: `npm run test`
|
||||
- To run specific tests: `npm run test -- -t "TestName"`
|
||||
- Building:
|
||||
- Build for production: `npm run build`
|
||||
- Environment Variables:
|
||||
- Set in `frontend/.env` or as environment variables
|
||||
- Available variables: VITE_BACKEND_HOST, VITE_USE_TLS, VITE_INSECURE_SKIP_VERIFY, VITE_FRONTEND_PORT
|
||||
- Internationalization:
|
||||
- Generate i18n declaration file: `npm run make-i18n`
|
||||
```
|
||||
|
||||
Now, please write a similar markdown for the current repository.
|
||||
Read all the GitHub workflows under .github/ of the repository (if this folder exists) to understand the CI checks (e.g., linter, pre-commit), and include those in the repo.md file.
|
||||
19
microagents/address_pr_comments.md
Normal file
19
microagents/address_pr_comments.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: address_pr_comments
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- /address_pr_comments
|
||||
inputs:
|
||||
- name: PR_URL
|
||||
description: "URL of the pull request"
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch name corresponds to the pull request"
|
||||
---
|
||||
|
||||
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.
|
||||
23
microagents/fix_test.md
Normal file
23
microagents/fix_test.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
name: fix_test
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- /fix_test
|
||||
inputs:
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch for the agent to work on"
|
||||
- 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`"
|
||||
- name: FUNCTION_TO_FIX
|
||||
description: "The name of function to fix"
|
||||
- name: FILE_FOR_FUNCTION
|
||||
description: "The path of the file that contains the function"
|
||||
---
|
||||
|
||||
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
|
||||
|
||||
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
|
||||
|
||||
PLEASE DO NOT modify the tests by yourself -- Let me know if you think some of the tests are incorrect.
|
||||
21
microagents/update_pr_description.md
Normal file
21
microagents/update_pr_description.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
name: update_pr_description
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- /update_pr_description
|
||||
inputs:
|
||||
- name: PR_URL
|
||||
description: "URL of the pull request"
|
||||
type: string
|
||||
validation:
|
||||
pattern: "^https://github.com/.+/.+/pull/[0-9]+$"
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch name corresponds to the pull request"
|
||||
type: string
|
||||
---
|
||||
|
||||
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.
|
||||
19
microagents/update_test.md
Normal file
19
microagents/update_test.md
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: update_test
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- /update_test
|
||||
inputs:
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch for the agent to work on"
|
||||
- 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`"
|
||||
---
|
||||
|
||||
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
|
||||
|
||||
The current implementation of the code is correct BUT the test functions {{ FUNCTION_TO_FIX }} in file {{ FILE_FOR_FUNCTION }} are failing.
|
||||
|
||||
Please update the test file so that they pass with the current version of the implementation.
|
||||
@ -1,4 +1,5 @@
|
||||
import io
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
@ -9,7 +10,7 @@ from openhands.core.exceptions import (
|
||||
MicroagentValidationError,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.microagent.types import MicroagentMetadata, MicroagentType
|
||||
from openhands.microagent.types import InputMetadata, MicroagentMetadata, MicroagentType
|
||||
|
||||
|
||||
class BaseMicroagent(BaseModel):
|
||||
@ -91,13 +92,24 @@ class BaseMicroagent(BaseModel):
|
||||
subclass_map = {
|
||||
MicroagentType.KNOWLEDGE: KnowledgeMicroagent,
|
||||
MicroagentType.REPO_KNOWLEDGE: RepoMicroagent,
|
||||
MicroagentType.TASK: TaskMicroagent,
|
||||
}
|
||||
|
||||
# Infer the agent type:
|
||||
# 1. If triggers exist -> KNOWLEDGE (optional)
|
||||
# 2. Else (no triggers) -> REPO (always active)
|
||||
# 1. If inputs exist -> TASK
|
||||
# 2. If triggers exist -> KNOWLEDGE
|
||||
# 3. Else (no triggers) -> REPO (always active)
|
||||
inferred_type: MicroagentType
|
||||
if metadata.triggers:
|
||||
if metadata.inputs:
|
||||
inferred_type = MicroagentType.TASK
|
||||
# Add a trigger for the agent name if not already present
|
||||
trigger = f'/{metadata.name}'
|
||||
if not metadata.triggers or trigger not in metadata.triggers:
|
||||
if not metadata.triggers:
|
||||
metadata.triggers = [trigger]
|
||||
else:
|
||||
metadata.triggers.append(trigger)
|
||||
elif metadata.triggers:
|
||||
inferred_type = MicroagentType.KNOWLEDGE
|
||||
else:
|
||||
# No triggers, default to REPO
|
||||
@ -122,7 +134,9 @@ class BaseMicroagent(BaseModel):
|
||||
|
||||
|
||||
class KnowledgeMicroagent(BaseMicroagent):
|
||||
"""Knowledge micro-agents provide specialized expertise that's triggered by keywords in conversations. They help with:
|
||||
"""Knowledge micro-agents provide specialized expertise that's triggered by keywords in conversations.
|
||||
|
||||
They help with:
|
||||
- Language best practices
|
||||
- Framework guidelines
|
||||
- Common patterns
|
||||
@ -131,8 +145,8 @@ class KnowledgeMicroagent(BaseMicroagent):
|
||||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
if self.type != MicroagentType.KNOWLEDGE:
|
||||
raise ValueError('KnowledgeMicroagent must have type KNOWLEDGE')
|
||||
if self.type not in [MicroagentType.KNOWLEDGE, MicroagentType.TASK]:
|
||||
raise ValueError('KnowledgeMicroagent must have type KNOWLEDGE or TASK')
|
||||
|
||||
def match_trigger(self, message: str) -> str | None:
|
||||
"""Match a trigger in the message.
|
||||
@ -171,6 +185,57 @@ class RepoMicroagent(BaseMicroagent):
|
||||
)
|
||||
|
||||
|
||||
class TaskMicroagent(KnowledgeMicroagent):
|
||||
"""TaskMicroagent is a special type of KnowledgeMicroagent that requires user input.
|
||||
|
||||
These microagents are triggered by a special format: "/{agent_name}"
|
||||
and will prompt the user for any required inputs before proceeding.
|
||||
"""
|
||||
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
if self.type != MicroagentType.TASK:
|
||||
raise ValueError(
|
||||
f'TaskMicroagent initialized with incorrect type: {self.type}'
|
||||
)
|
||||
|
||||
# Append a prompt to ask for missing variables
|
||||
self._append_missing_variables_prompt()
|
||||
|
||||
def _append_missing_variables_prompt(self) -> None:
|
||||
"""Append a prompt to ask for missing variables."""
|
||||
# Check if the content contains any variables or has inputs defined
|
||||
if not self.requires_user_input() and not self.metadata.inputs:
|
||||
return
|
||||
|
||||
prompt = "\n\nIf the user didn't provide any of these variables, ask the user to provide them first before the agent can proceed with the task."
|
||||
self.content += prompt
|
||||
|
||||
def extract_variables(self, content: str) -> list[str]:
|
||||
"""Extract variables from the content.
|
||||
|
||||
Variables are in the format ${variable_name}.
|
||||
"""
|
||||
pattern = r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
|
||||
matches = re.findall(pattern, content)
|
||||
return matches
|
||||
|
||||
def requires_user_input(self) -> bool:
|
||||
"""Check if this microagent requires user input.
|
||||
|
||||
Returns True if the content contains variables in the format ${variable_name}.
|
||||
"""
|
||||
# Check if the content contains any variables
|
||||
variables = self.extract_variables(self.content)
|
||||
logger.debug(f'This microagent requires user input: {variables}')
|
||||
return len(variables) > 0
|
||||
|
||||
@property
|
||||
def inputs(self) -> list[InputMetadata]:
|
||||
"""Get the inputs for this microagent."""
|
||||
return self.metadata.inputs
|
||||
|
||||
|
||||
def load_microagents_from_dir(
|
||||
microagent_dir: Union[str, Path],
|
||||
) -> tuple[dict[str, RepoMicroagent], dict[str, KnowledgeMicroagent]]:
|
||||
@ -182,7 +247,7 @@ def load_microagents_from_dir(
|
||||
microagent_dir: Path to the microagents directory (e.g. .openhands/microagents)
|
||||
|
||||
Returns:
|
||||
Tuple of (repo_agents, knowledge_agents, task_agents) dictionaries
|
||||
Tuple of (repo_agents, knowledge_agents) dictionaries
|
||||
"""
|
||||
if isinstance(microagent_dir, str):
|
||||
microagent_dir = Path(microagent_dir)
|
||||
@ -202,6 +267,7 @@ def load_microagents_from_dir(
|
||||
if isinstance(agent, RepoMicroagent):
|
||||
repo_agents[agent.name] = agent
|
||||
elif isinstance(agent, KnowledgeMicroagent):
|
||||
# Both KnowledgeMicroagent and TaskMicroagent go into knowledge_agents
|
||||
knowledge_agents[agent.name] = agent
|
||||
except MicroagentValidationError as e:
|
||||
# For validation errors, include the original exception
|
||||
|
||||
@ -12,6 +12,14 @@ class MicroagentType(str, Enum):
|
||||
|
||||
KNOWLEDGE = 'knowledge' # Optional microagent, triggered by keywords
|
||||
REPO_KNOWLEDGE = 'repo' # Always active microagent
|
||||
TASK = 'task' # Special type for task microagents that require user input
|
||||
|
||||
|
||||
class InputMetadata(BaseModel):
|
||||
"""Metadata for task microagent inputs."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class MicroagentMetadata(BaseModel):
|
||||
@ -22,6 +30,7 @@ class MicroagentMetadata(BaseModel):
|
||||
version: str = Field(default='1.0.0')
|
||||
agent: str = Field(default='CodeActAgent')
|
||||
triggers: list[str] = [] # optional, only exists for knowledge microagents
|
||||
inputs: list[InputMetadata] = [] # optional, only exists for task microagents
|
||||
mcp_tools: MCPConfig | None = (
|
||||
None # optional, for microagents that provide additional MCP tools
|
||||
)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""Tests for microagent loading in runtime."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@ -13,7 +14,13 @@ from conftest import (
|
||||
from openhands.core.config import MCPConfig
|
||||
from openhands.core.config.mcp_config import MCPStdioServerConfig
|
||||
from openhands.mcp.utils import add_mcp_tools_to_agent
|
||||
from openhands.microagent import KnowledgeMicroagent, RepoMicroagent
|
||||
from openhands.microagent.microagent import (
|
||||
BaseMicroagent,
|
||||
KnowledgeMicroagent,
|
||||
RepoMicroagent,
|
||||
TaskMicroagent,
|
||||
)
|
||||
from openhands.microagent.types import MicroagentType
|
||||
|
||||
|
||||
def _create_test_microagents(test_dir: str):
|
||||
@ -173,6 +180,176 @@ Repository-specific test instructions.
|
||||
_close_test_runtime(runtime)
|
||||
|
||||
|
||||
def test_task_microagent_creation():
|
||||
"""Test that a TaskMicroagent is created correctly."""
|
||||
content = """---
|
||||
name: test_task
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- /test_task
|
||||
inputs:
|
||||
- name: TEST_VAR
|
||||
description: "Test variable"
|
||||
---
|
||||
|
||||
This is a test task microagent with a variable: ${test_var}.
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert agent.type == MicroagentType.TASK
|
||||
assert agent.name == 'test_task'
|
||||
assert '/test_task' in agent.triggers
|
||||
assert "If the user didn't provide any of these variables" in agent.content
|
||||
|
||||
|
||||
def test_task_microagent_variable_extraction():
|
||||
"""Test that variables are correctly extracted from the content."""
|
||||
content = """---
|
||||
name: test_task
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- /test_task
|
||||
inputs:
|
||||
- name: var1
|
||||
description: "Variable 1"
|
||||
---
|
||||
|
||||
This is a test with variables: ${var1}, ${var2}, and ${var3}.
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
variables = agent.extract_variables(agent.content)
|
||||
assert set(variables) == {'var1', 'var2', 'var3'}
|
||||
assert agent.requires_user_input()
|
||||
|
||||
|
||||
def test_knowledge_microagent_no_prompt():
|
||||
"""Test that a regular KnowledgeMicroagent doesn't get the prompt."""
|
||||
content = """---
|
||||
name: test_knowledge
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- test_knowledge
|
||||
---
|
||||
|
||||
This is a test knowledge microagent.
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
|
||||
assert isinstance(agent, KnowledgeMicroagent)
|
||||
assert agent.type == MicroagentType.KNOWLEDGE
|
||||
assert "If the user didn't provide any of these variables" not in agent.content
|
||||
|
||||
|
||||
def test_task_microagent_trigger_addition():
|
||||
"""Test that a trigger is added if not present."""
|
||||
content = """---
|
||||
name: test_task
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
inputs:
|
||||
- name: TEST_VAR
|
||||
description: "Test variable"
|
||||
---
|
||||
|
||||
This is a test task microagent.
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert '/test_task' in agent.triggers
|
||||
|
||||
|
||||
def test_task_microagent_no_duplicate_trigger():
|
||||
"""Test that a trigger is not duplicated if already present."""
|
||||
content = """---
|
||||
name: test_task
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- /test_task
|
||||
- another_trigger
|
||||
inputs:
|
||||
- name: TEST_VAR
|
||||
description: "Test variable"
|
||||
---
|
||||
|
||||
This is a test task microagent.
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert agent.triggers.count('/test_task') == 1 # No duplicates
|
||||
assert len(agent.triggers) == 2
|
||||
assert 'another_trigger' in agent.triggers
|
||||
assert '/test_task' in agent.triggers
|
||||
|
||||
|
||||
def test_task_microagent_match_trigger():
|
||||
"""Test that a task microagent matches its trigger correctly."""
|
||||
content = """---
|
||||
name: test_task
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- /test_task
|
||||
inputs:
|
||||
- name: TEST_VAR
|
||||
description: "Test variable"
|
||||
---
|
||||
|
||||
This is a test task microagent.
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert agent.match_trigger('/test_task') == '/test_task'
|
||||
assert agent.match_trigger(' /test_task ') == '/test_task'
|
||||
assert agent.match_trigger('This contains /test_task') == '/test_task'
|
||||
assert agent.match_trigger('/other_task') is None
|
||||
|
||||
|
||||
def test_default_tools_microagent_exists():
|
||||
"""Test that the default-tools microagent exists in the global microagents directory."""
|
||||
# Get the path to the global microagents directory
|
||||
|
||||
@ -173,7 +173,7 @@ def test_invalid_microagent_type(temp_microagents_dir):
|
||||
# Create a microagent with an invalid type
|
||||
invalid_agent = """---
|
||||
name: invalid_type_agent
|
||||
type: task
|
||||
type: invalid_type
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
@ -196,7 +196,8 @@ This microagent has an invalid type.
|
||||
# Check that the error message contains helpful information
|
||||
error_msg = str(excinfo.value)
|
||||
assert 'invalid_type.md' in error_msg
|
||||
assert 'Invalid "type" value: "task"' in error_msg
|
||||
assert 'Invalid "type" value: "invalid_type"' in error_msg
|
||||
assert 'Valid types are:' in error_msg
|
||||
assert '"knowledge"' in error_msg
|
||||
assert '"repo"' in error_msg
|
||||
assert '"task"' in error_msg
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user