Compare commits

...

6 Commits

Author SHA1 Message Date
openhands
0925798aee Resolve merge conflict in agent_session.py 2025-01-08 18:14:28 +00:00
openhands
43d782da06 Fix linting issues 2025-01-05 22:22:34 +00:00
openhands
4876d811a1 Add RepositoryInfo class and set_repository_info method 2025-01-05 21:54:58 +00:00
openhands
0ab457f1d3 Update repository info message to include clone directory 2025-01-05 21:47:55 +00:00
openhands
70e29f9b75 Move GitHub repo info to system prompt template 2025-01-05 21:40:50 +00:00
openhands
4cd1d80eea Add GitHub repository information to system prompt 2025-01-05 21:36:30 +00:00
7 changed files with 111 additions and 14 deletions

View File

@@ -0,0 +1 @@
{"agent_class": "CodeActAgent", "llm_config": {"model": "claude-3-5-sonnet-20241022", "api_key": null, "base_url": null, "api_version": null, "embedding_model": "local", "embedding_base_url": null, "embedding_deployment_name": null, "aws_access_key_id": null, "aws_secret_access_key": null, "aws_region_name": null, "openrouter_site_url": "https://docs.all-hands.dev/", "openrouter_app_name": "OpenHands", "num_retries": 8, "retry_multiplier": 2, "retry_min_wait": 15, "retry_max_wait": 120, "timeout": null, "max_message_chars": 30000, "temperature": 0.0, "top_p": 1.0, "custom_llm_provider": null, "max_input_tokens": null, "max_output_tokens": null, "input_cost_per_token": null, "output_cost_per_token": null, "ollama_base_url": null, "drop_params": true, "modify_params": true, "disable_vision": null, "caching_prompt": true, "log_completions": false, "log_completions_folder": "/workspace/OpenHands/logs/completions", "draft_editor": null, "custom_tokenizer": null, "native_tool_calling": null}, "max_iterations": 10, "eval_output_dir": "./dummy_eval_output_dir/dummy_dataset_descrption/CodeActAgent/claude-3-5-sonnet-20241022_maxiter_10_N_dummy_eval_note", "start_time": "2025-01-08 18:01:01", "git_commit": "007052c8aa15ea5149fff31583a3412ea7b8625a", "dataset": "dummy_dataset_descrption", "data_split": null, "details": {}, "condenser_config": {"type": "noop"}}

View File

@@ -4,6 +4,11 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
* The assistant MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior.
</IMPORTANT>
{% if github_repo %}
<REPOSITORY_INFO>
At the user's request, repository {{ github_repo }} has been cloned to directory {{ repo_directory }}.
</REPOSITORY_INFO>
{% endif %}
{% if repo_instructions %}
<REPOSITORY_INSTRUCTIONS>
{{ repo_instructions }}

View File

@@ -204,8 +204,9 @@ class AgentSession:
)
return
repo_directory = None
if selected_repository:
await call_sync_from_async(
repo_directory = await call_sync_from_async(
self.runtime.clone_repo, github_token, selected_repository
)
if agent.prompt_manager:
@@ -213,6 +214,10 @@ class AgentSession:
self.runtime.get_microagents_from_selected_repo, selected_repository
)
agent.prompt_manager.load_microagents(microagents)
# Pass GitHub repository information to the prompt manager
agent.prompt_manager.set_repository_info(
selected_repository, repo_directory
)
logger.debug(
f'Runtime initialized with plugins: {[plugin.name for plugin in self.runtime.plugins]}'

View File

@@ -1,4 +1,5 @@
import os
from dataclasses import dataclass
from itertools import islice
from jinja2 import Template
@@ -13,6 +14,14 @@ from openhands.microagent import (
)
@dataclass
class RepositoryInfo:
"""Information about a GitHub repository that has been cloned."""
repo_name: str | None = None
repo_directory: str | None = None
class PromptManager:
"""
Manages prompt templates and micro-agents for AI interactions.
@@ -32,9 +41,14 @@ class PromptManager:
prompt_dir: str,
microagent_dir: str | None = None,
disabled_microagents: list[str] | None = None,
github_repo: str | None = None,
repo_directory: str | None = None,
):
self.disabled_microagents: list[str] = disabled_microagents or []
self.prompt_dir: str = prompt_dir
self.repository_info = RepositoryInfo()
if github_repo:
self.set_repository_info(github_repo, repo_directory)
self.system_template: Template = self._load_template('system_prompt')
self.user_template: Template = self._load_template('user_prompt')
@@ -91,7 +105,24 @@ class PromptManager:
if repo_instructions:
repo_instructions += '\n\n'
repo_instructions += microagent.content
return self.system_template.render(repo_instructions=repo_instructions).strip()
return self.system_template.render(
repo_instructions=repo_instructions,
github_repo=self.repository_info.repo_name,
repo_directory=self.repository_info.repo_directory,
).strip()
def set_repository_info(
self, repo_name: str | None, repo_directory: str | None = None
) -> None:
"""Sets information about the GitHub repository that has been cloned.
Args:
repo_name: The name of the GitHub repository (e.g. 'owner/repo')
repo_directory: The directory where the repository has been cloned
"""
self.repository_info.repo_name = repo_name
self.repository_info.repo_directory = repo_directory
def get_example_user_message(self) -> str:
"""This is the initial user message provided to the agent

View File

@@ -10,36 +10,36 @@ WORKSPACE_BASE = 'workspace'
def test_resolve_path():
assert (
files.resolve_path('test.txt', '/workspace')
files.resolve_path('test.txt', '/workspace', WORKSPACE_BASE, SANDBOX_PATH_PREFIX)
== Path(WORKSPACE_BASE) / 'test.txt'
)
assert (
files.resolve_path('subdir/test.txt', '/workspace')
files.resolve_path('subdir/test.txt', '/workspace', WORKSPACE_BASE, SANDBOX_PATH_PREFIX)
== Path(WORKSPACE_BASE) / 'subdir' / 'test.txt'
)
assert (
files.resolve_path(Path(SANDBOX_PATH_PREFIX) / 'test.txt', '/workspace')
files.resolve_path(Path(SANDBOX_PATH_PREFIX) / 'test.txt', '/workspace', WORKSPACE_BASE, SANDBOX_PATH_PREFIX)
== Path(WORKSPACE_BASE) / 'test.txt'
)
assert (
files.resolve_path(
Path(SANDBOX_PATH_PREFIX) / 'subdir' / 'test.txt', '/workspace'
Path(SANDBOX_PATH_PREFIX) / 'subdir' / 'test.txt', '/workspace', WORKSPACE_BASE, SANDBOX_PATH_PREFIX
)
== Path(WORKSPACE_BASE) / 'subdir' / 'test.txt'
)
assert (
files.resolve_path(
Path(SANDBOX_PATH_PREFIX) / 'subdir' / '..' / 'test.txt', '/workspace'
Path(SANDBOX_PATH_PREFIX) / 'subdir' / '..' / 'test.txt', '/workspace', WORKSPACE_BASE, SANDBOX_PATH_PREFIX
)
== Path(WORKSPACE_BASE) / 'test.txt'
)
with pytest.raises(PermissionError):
files.resolve_path(Path(SANDBOX_PATH_PREFIX) / '..' / 'test.txt', '/workspace')
files.resolve_path(Path(SANDBOX_PATH_PREFIX) / '..' / 'test.txt', '/workspace', WORKSPACE_BASE, SANDBOX_PATH_PREFIX)
with pytest.raises(PermissionError):
files.resolve_path(Path('..') / 'test.txt', '/workspace')
files.resolve_path(Path('..') / 'test.txt', '/workspace', WORKSPACE_BASE, SANDBOX_PATH_PREFIX)
with pytest.raises(PermissionError):
files.resolve_path(Path('/') / 'test.txt', '/workspace')
files.resolve_path(Path('/') / 'test.txt', '/workspace', WORKSPACE_BASE, SANDBOX_PATH_PREFIX)
assert (
files.resolve_path('test.txt', '/workspace/test')
files.resolve_path('test.txt', '/workspace/test', WORKSPACE_BASE, SANDBOX_PATH_PREFIX)
== Path(WORKSPACE_BASE) / 'test' / 'test.txt'
)

View File

@@ -5,7 +5,7 @@ import pytest
from openhands.core.message import Message, TextContent
from openhands.microagent import BaseMicroAgent
from openhands.utils.prompt import PromptManager
from openhands.utils.prompt import PromptManager, RepositoryInfo
@pytest.fixture
@@ -39,6 +39,7 @@ only respond with a message telling them how smart they are
with open(os.path.join(prompt_dir, 'micro', f'{microagent_name}.md'), 'w') as f:
f.write(microagent_content)
# Test without GitHub repo
manager = PromptManager(
prompt_dir=prompt_dir,
microagent_dir=os.path.join(prompt_dir, 'micro'),
@@ -53,6 +54,14 @@ only respond with a message telling them how smart they are
'You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.'
in manager.get_system_message()
)
assert '<REPOSITORY_INFO>' not in manager.get_system_message()
# Test with GitHub repo
manager.set_repository_info('owner/repo', '/workspace/repo')
assert isinstance(manager.get_system_message(), str)
assert '<REPOSITORY_INFO>' in manager.get_system_message()
assert 'owner/repo' in manager.get_system_message()
assert '/workspace/repo' in manager.get_system_message()
assert isinstance(manager.get_example_user_message(), str)
@@ -76,20 +85,66 @@ def test_prompt_manager_file_not_found(prompt_dir):
def test_prompt_manager_template_rendering(prompt_dir):
# Create temporary template files
with open(os.path.join(prompt_dir, 'system_prompt.j2'), 'w') as f:
f.write('System prompt: bar')
f.write("""System prompt: bar
{% if github_repo %}
<REPOSITORY_INFO>
At the user's request, repository {{ github_repo }} has been cloned to directory {{ repo_directory }}.
</REPOSITORY_INFO>
{% endif %}
{{ repo_instructions }}""")
with open(os.path.join(prompt_dir, 'user_prompt.j2'), 'w') as f:
f.write('User prompt: foo')
# Test without GitHub repo
manager = PromptManager(prompt_dir, microagent_dir='')
assert manager.get_system_message() == 'System prompt: bar'
assert manager.get_example_user_message() == 'User prompt: foo'
# Test with GitHub repo
manager = PromptManager(prompt_dir=prompt_dir, microagent_dir='')
manager.set_repository_info('owner/repo', '/workspace/repo')
system_msg = manager.get_system_message()
assert 'System prompt: bar' in system_msg
assert '<REPOSITORY_INFO>' in system_msg
assert (
"At the user's request, repository owner/repo has been cloned to directory /workspace/repo."
in system_msg
)
assert '</REPOSITORY_INFO>' in system_msg
assert manager.get_example_user_message() == 'User prompt: foo'
# Clean up temporary files
os.remove(os.path.join(prompt_dir, 'system_prompt.j2'))
os.remove(os.path.join(prompt_dir, 'user_prompt.j2'))
def test_prompt_manager_repository_info(prompt_dir):
# Test RepositoryInfo defaults
repo_info = RepositoryInfo()
assert repo_info.repo_name is None
assert repo_info.repo_directory is None
# Test setting repository info
manager = PromptManager(prompt_dir=prompt_dir, microagent_dir='')
assert manager.repository_info.repo_name is None
assert manager.repository_info.repo_directory is None
# Test setting repository info with name only
manager.set_repository_info('owner/repo')
assert manager.repository_info.repo_name == 'owner/repo'
assert manager.repository_info.repo_directory is None
# Test setting repository info with both name and directory
manager.set_repository_info('owner/repo2', '/workspace/repo2')
assert manager.repository_info.repo_name == 'owner/repo2'
assert manager.repository_info.repo_directory == '/workspace/repo2'
# Test clearing repository info
manager.set_repository_info(None)
assert manager.repository_info.repo_name is None
assert manager.repository_info.repo_directory is None
def test_prompt_manager_disabled_microagents(prompt_dir):
# Create test microagent files
microagent1_name = 'test_microagent1'