diff --git a/evaluation/EDA/run_infer.py b/evaluation/EDA/run_infer.py
index fb5df3b44f01..25492073928e 100644
--- a/evaluation/EDA/run_infer.py
+++ b/evaluation/EDA/run_infer.py
@@ -35,7 +35,8 @@ def codeact_user_response_eda(state: State) -> str:
# retrieve the latest model message from history
if state.history:
- model_guess = state.get_last_agent_message()
+ last_agent_message = state.get_last_agent_message()
+ model_guess = last_agent_message.content if last_agent_message else ''
assert game is not None, 'Game is not initialized.'
msg = game.generate_user_response(model_guess)
@@ -140,7 +141,8 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
- final_message = state.get_last_agent_message()
+ last_agent_message = state.get_last_agent_message()
+ final_message = last_agent_message.content if last_agent_message else ''
logger.info(f'Final message: {final_message} | Ground truth: {instance["text"]}')
test_result = game.reward()
diff --git a/evaluation/gorilla/run_infer.py b/evaluation/gorilla/run_infer.py
index e437f2b6075a..c71ec9d4e9dc 100644
--- a/evaluation/gorilla/run_infer.py
+++ b/evaluation/gorilla/run_infer.py
@@ -102,7 +102,8 @@ def process_instance(
raise ValueError('State should not be None.')
# retrieve the last message from the agent
- model_answer_raw = state.get_last_agent_message()
+ last_agent_message = state.get_last_agent_message()
+ model_answer_raw = last_agent_message.content if last_agent_message else ''
# attempt to parse model_answer
ast_eval_fn = instance['ast_eval']
diff --git a/evaluation/toolqa/run_infer.py b/evaluation/toolqa/run_infer.py
index 25633ce6ce23..d3a2280b300b 100644
--- a/evaluation/toolqa/run_infer.py
+++ b/evaluation/toolqa/run_infer.py
@@ -127,7 +127,8 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
raise ValueError('State should not be None.')
# retrieve the last message from the agent
- model_answer_raw = state.get_last_agent_message()
+ last_agent_message = state.get_last_agent_message()
+ model_answer_raw = last_agent_message.content if last_agent_message else ''
# attempt to parse model_answer
correct = eval_answer(str(model_answer_raw), str(answer))
diff --git a/frontend/src/components/project-menu/ProjectMenuCard.tsx b/frontend/src/components/project-menu/ProjectMenuCard.tsx
index 9130e409ee84..847ce683e48b 100644
--- a/frontend/src/components/project-menu/ProjectMenuCard.tsx
+++ b/frontend/src/components/project-menu/ProjectMenuCard.tsx
@@ -43,10 +43,7 @@ export function ProjectMenuCard({
posthog.capture("push_to_github_button_clicked");
const rawEvent = {
content: `
-Let's push the code to GitHub.
-If we're currently on the openhands-workspace branch, please create a new branch with a descriptive name.
-Commit any changes and push them to the remote repository.
-Finally, open up a pull request using the GitHub API and the token in the GITHUB_TOKEN environment variable, then show me the URL of the pull request.
+Please push the changes to GitHub and open a pull request.
`,
imageUrls: [],
timestamp: new Date().toISOString(),
diff --git a/openhands/agenthub/codeact_agent/codeact_agent.py b/openhands/agenthub/codeact_agent/codeact_agent.py
index 314a2e0a089b..c0761c8f6a00 100644
--- a/openhands/agenthub/codeact_agent/codeact_agent.py
+++ b/openhands/agenthub/codeact_agent/codeact_agent.py
@@ -39,7 +39,6 @@
JupyterRequirement,
PluginRequirement,
)
-from openhands.utils.microagent import MicroAgent
from openhands.utils.prompt import PromptManager
@@ -86,16 +85,6 @@ def __init__(
super().__init__(llm, config)
self.reset()
- self.micro_agent = (
- MicroAgent(
- os.path.join(
- os.path.dirname(__file__), 'micro', f'{config.micro_agent_name}.md'
- )
- )
- if config.micro_agent_name
- else None
- )
-
self.function_calling_active = self.config.function_calling
if self.function_calling_active and not self.llm.is_function_calling_active():
logger.warning(
@@ -105,7 +94,6 @@ def __init__(
self.function_calling_active = False
if self.function_calling_active:
- # Function calling mode
self.tools = codeact_function_calling.get_tools(
codeact_enable_browsing=self.config.codeact_enable_browsing,
codeact_enable_jupyter=self.config.codeact_enable_jupyter,
@@ -114,18 +102,17 @@ def __init__(
logger.debug(
f'TOOLS loaded for CodeActAgent: {json.dumps(self.tools, indent=2)}'
)
- self.system_prompt = codeact_function_calling.SYSTEM_PROMPT
- self.initial_user_message = None
+ self.prompt_manager = PromptManager(
+ microagent_dir=os.path.join(os.path.dirname(__file__), 'micro'),
+ prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts', 'tools'),
+ )
else:
- # Non-function-calling mode
self.action_parser = CodeActResponseParser()
self.prompt_manager = PromptManager(
- prompt_dir=os.path.join(os.path.dirname(__file__)),
+ microagent_dir=os.path.join(os.path.dirname(__file__), 'micro'),
+ prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts', 'default'),
agent_skills_docs=AgentSkillsRequirement.documentation,
- micro_agent=self.micro_agent,
)
- self.system_prompt = self.prompt_manager.system_message
- self.initial_user_message = self.prompt_manager.initial_user_message
self.pending_actions: deque[Action] = deque()
@@ -337,8 +324,8 @@ def step(self, state: State) -> Action:
return self.pending_actions.popleft()
# if we're done, go back
- last_user_message = state.get_last_user_message()
- if last_user_message and last_user_message.strip() == '/exit':
+ latest_user_message = state.get_last_user_message()
+ if latest_user_message and latest_user_message.content.strip() == '/exit':
return AgentFinishAction()
# prepare what we want to send to the LLM
@@ -403,17 +390,19 @@ def _get_messages(self, state: State) -> list[Message]:
role='system',
content=[
TextContent(
- text=self.system_prompt,
- cache_prompt=self.llm.is_caching_prompt_active(), # Cache system prompt
+ text=self.prompt_manager.get_system_message(),
+ cache_prompt=self.llm.is_caching_prompt_active(),
)
],
)
]
- if self.initial_user_message:
+ example_message = self.prompt_manager.get_example_user_message()
+ if example_message:
messages.append(
Message(
role='user',
- content=[TextContent(text=self.initial_user_message)],
+ content=[TextContent(text=example_message)],
+ cache_prompt=self.llm.is_caching_prompt_active(),
)
)
@@ -462,8 +451,9 @@ def _get_messages(self, state: State) -> list[Message]:
pending_tool_call_action_messages.pop(response_id)
for message in messages_to_add:
- # add regular message
if message:
+ if message.role == 'user':
+ self.prompt_manager.enhance_message(message)
# handle error if the message is the SAME role as the previous message
# litellm.exceptions.BadRequestError: litellm.BadRequestError: OpenAIException - Error code: 400 - {'detail': 'Only supports u/a/u/a/u...'}
# there shouldn't be two consecutive messages from the same role
@@ -493,23 +483,6 @@ def _get_messages(self, state: State) -> list[Message]:
break
if not self.function_calling_active:
- # The latest user message is important:
- # we want to remind the agent of the environment constraints
- latest_user_message = next(
- islice(
- (
- m
- for m in reversed(messages)
- if m.role == 'user'
- and any(isinstance(c, TextContent) for c in m.content)
- ),
- 1,
- ),
- None,
- )
- # do not add this for function calling
- if latest_user_message:
- reminder_text = f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task. When finished reply with .'
- latest_user_message.content.append(TextContent(text=reminder_text))
+ self.prompt_manager.add_turns_left_reminder(messages, state)
return messages
diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py
index 1799478601bd..177e7b7ff171 100644
--- a/openhands/agenthub/codeact_agent/function_calling.py
+++ b/openhands/agenthub/codeact_agent/function_calling.py
@@ -25,14 +25,6 @@
)
from openhands.events.tool import ToolCallMetadata
-SYSTEM_PROMPT = """You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.
-
-* If user provides a path, you should NOT assume it's relative to the current working directory. Instead, you should explore the file system to find the file before working on it.
-* 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.
-
-"""
-
_BASH_DESCRIPTION = """Execute a bash command in the terminal.
* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`.
* Interactive: If a bash command returns exit code `-1`, this means the process is not yet finished. The assistant must then send a second call to terminal with an empty `command` (which will retrieve any additional logs), or it can send additional text (set `command` to the text) to STDIN of the running process, or it can send command=`ctrl+c` to interrupt the process.
diff --git a/openhands/agenthub/codeact_agent/micro/flarglebargle.md b/openhands/agenthub/codeact_agent/micro/flarglebargle.md
new file mode 100644
index 000000000000..96189965f546
--- /dev/null
+++ b/openhands/agenthub/codeact_agent/micro/flarglebargle.md
@@ -0,0 +1,9 @@
+---
+name: flarglebargle
+agent: CodeActAgent
+triggers:
+- flarglebargle
+---
+
+IMPORTANT! The user has said the magic word "flarglebargle". You must
+only respond with a message telling them how smart they are
diff --git a/openhands/agenthub/codeact_agent/micro/github.md b/openhands/agenthub/codeact_agent/micro/github.md
index 465df1cf47fa..abd48fa5c75c 100644
--- a/openhands/agenthub/codeact_agent/micro/github.md
+++ b/openhands/agenthub/codeact_agent/micro/github.md
@@ -1,69 +1,31 @@
---
name: github
agent: CodeActAgent
-require_env_var:
- SANDBOX_ENV_GITHUB_TOKEN: "Create a GitHub Personal Access Token (https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) and set it as SANDBOX_GITHUB_TOKEN in your environment variables."
+triggers:
+- github
+- git
---
-# How to Interact with Github
-
-## Environment Variable Available
-
-- `GITHUB_TOKEN`: A read-only token for Github.
-
-## Using GitHub's RESTful API
-
-Use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API. Here are some common operations:
-
-Here's a template for API calls:
-
-```sh
-curl -H "Authorization: token $GITHUB_TOKEN" \
- "https://api.github.com/{endpoint}"
+You have access to an environment variable, `GITHUB_TOKEN`, which allows you to interact with
+the GitHub API.
+
+You can use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API.
+ALWAYS use the GitHub API for operations instead of a web browser.
+
+Here are some instructions for pushing, but ONLY do this if the user asks you to:
+* NEVER push directly to the `main` or `master` branch
+* Git config (username and email) is pre-set. Do not modify.
+* You may already be on a branch called `openhands-workspace`. Create a new branch with a better name before pushing.
+* Use the GitHub API to create a pull request, if you haven't already
+* Use the main branch as the base branch, unless the user requests otherwise
+* After opening or updating a pull request, send the user a short message with a link to the pull request.
+* Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands:
+```bash
+git checkout -b create-widget
+git add .
+git commit -m "Create widget"
+git push origin create-widget
+curl -X POST "https://api.github.com/repos/CodeActOrg/openhands/pulls" \
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
+ -d '{"title":"Create widget","head":"create-widget","base":"openhands-workspace"}'
```
-
-First replace `{endpoint}` with the specific API path. Common operations:
-
-1. View an issue or pull request:
- - Issues: `/repos/{owner}/{repo}/issues/{issue_number}`
- - Pull requests: `/repos/{owner}/{repo}/pulls/{pull_request_number}`
-
-2. List repository issues or pull requests:
- - Issues: `/repos/{owner}/{repo}/issues`
- - Pull requests: `/repos/{owner}/{repo}/pulls`
-
-3. Search issues or pull requests:
- - `/search/issues?q=repo:{owner}/{repo}+is:{type}+{search_term}+state:{state}`
- - Replace `{type}` with `issue` or `pr`
-
-4. List repository branches:
- `/repos/{owner}/{repo}/branches`
-
-5. Get commit details:
- `/repos/{owner}/{repo}/commits/{commit_sha}`
-
-6. Get repository details:
- `/repos/{owner}/{repo}`
-
-7. Get user information:
- `/user`
-
-8. Search repositories:
- `/search/repositories?q={query}`
-
-9. Get rate limit status:
- `/rate_limit`
-
-Replace `{owner}`, `{repo}`, `{commit_sha}`, `{issue_number}`, `{pull_request_number}`,
-`{search_term}`, `{state}`, and `{query}` with appropriate values.
-
-## Important Notes
-
-1. Always use the GitHub API for operations instead of a web browser.
-2. The `GITHUB_TOKEN` is read-only. Avoid operations that require write access.
-3. Git config (username and email) is pre-set. Do not modify.
-4. Edit and test code locally. Never push directly to remote.
-5. Verify correct branch before committing.
-6. Commit changes frequently.
-7. If the issue or task is ambiguous or lacks sufficient detail, always request clarification from the user before proceeding.
-8. You should avoid using command line tools like `sed` for file editing.
diff --git a/openhands/agenthub/codeact_agent/system_prompt.j2 b/openhands/agenthub/codeact_agent/prompts/default/system_prompt.j2
similarity index 100%
rename from openhands/agenthub/codeact_agent/system_prompt.j2
rename to openhands/agenthub/codeact_agent/prompts/default/system_prompt.j2
diff --git a/openhands/agenthub/codeact_agent/user_prompt.j2 b/openhands/agenthub/codeact_agent/prompts/default/user_prompt.j2
similarity index 97%
rename from openhands/agenthub/codeact_agent/user_prompt.j2
rename to openhands/agenthub/codeact_agent/prompts/default/user_prompt.j2
index a94c02e39dd0..a985bb7a5940 100644
--- a/openhands/agenthub/codeact_agent/user_prompt.j2
+++ b/openhands/agenthub/codeact_agent/prompts/default/user_prompt.j2
@@ -215,12 +215,5 @@ The server is running on port 5000 with PID 126. You can access the list of numb
{% endset %}
Here is an example of how you can interact with the environment for task solving:
{{ DEFAULT_EXAMPLE }}
-{% if micro_agent %}
---- BEGIN OF GUIDELINE ---
-The following information may assist you in completing your task:
-
-{{ micro_agent }}
---- END OF GUIDELINE ---
-{% endif %}
NOW, LET'S START!
diff --git a/openhands/agenthub/codeact_agent/prompts/tools/system_prompt.j2 b/openhands/agenthub/codeact_agent/prompts/tools/system_prompt.j2
new file mode 100644
index 000000000000..0d101b7c150b
--- /dev/null
+++ b/openhands/agenthub/codeact_agent/prompts/tools/system_prompt.j2
@@ -0,0 +1,7 @@
+You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.
+
+* If user provides a path, you should NOT assume it's relative to the current working directory. Instead, you should explore the file system to find the file before working on it.
+* 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.
+
+
diff --git a/openhands/agenthub/codeact_agent/prompts/tools/user_prompt.j2 b/openhands/agenthub/codeact_agent/prompts/tools/user_prompt.j2
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/openhands/agenthub/codeact_swe_agent/codeact_swe_agent.py b/openhands/agenthub/codeact_swe_agent/codeact_swe_agent.py
index 7c5b039e8c47..d5b9a658a920 100644
--- a/openhands/agenthub/codeact_swe_agent/codeact_swe_agent.py
+++ b/openhands/agenthub/codeact_swe_agent/codeact_swe_agent.py
@@ -155,7 +155,7 @@ def step(self, state: State) -> Action:
"""
# if we're done, go back
last_user_message = state.get_last_user_message()
- if last_user_message and last_user_message.strip() == '/exit':
+ if last_user_message and last_user_message.content.strip() == '/exit':
return AgentFinishAction()
# prepare what we want to send to the LLM
diff --git a/openhands/controller/state/state.py b/openhands/controller/state/state.py
index 96c0ab7e8322..93b5d6ff4cd7 100644
--- a/openhands/controller/state/state.py
+++ b/openhands/controller/state/state.py
@@ -156,14 +156,14 @@ def get_current_user_intent(self) -> tuple[str | None, list[str] | None]:
return last_user_message, last_user_message_image_urls
- def get_last_agent_message(self) -> str | None:
+ def get_last_agent_message(self) -> MessageAction | None:
for event in reversed(self.history):
if isinstance(event, MessageAction) and event.source == EventSource.AGENT:
- return event.content
+ return event
return None
- def get_last_user_message(self) -> str | None:
+ def get_last_user_message(self) -> MessageAction | None:
for event in reversed(self.history):
if isinstance(event, MessageAction) and event.source == EventSource.USER:
- return event.content
+ return event
return None
diff --git a/openhands/utils/microagent.py b/openhands/utils/microagent.py
index cd309f6eb7d4..4aa311d6b205 100644
--- a/openhands/utils/microagent.py
+++ b/openhands/utils/microagent.py
@@ -3,15 +3,11 @@
import frontmatter
import pydantic
-from openhands.controller.agent import Agent
-from openhands.core.exceptions import MicroAgentValidationError
-from openhands.core.logger import openhands_logger as logger
-
class MicroAgentMetadata(pydantic.BaseModel):
name: str
agent: str
- require_env_var: dict[str, str]
+ triggers: list[str] = []
class MicroAgent:
@@ -23,22 +19,30 @@ def __init__(self, path: str):
self._loaded = frontmatter.load(file)
self._content = self._loaded.content
self._metadata = MicroAgentMetadata(**self._loaded.metadata)
- self._validate_micro_agent()
+
+ 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
- def _validate_micro_agent(self):
- logger.debug(
- f'Loading and validating micro agent [{self._metadata.name}] based on [{self._metadata.agent}]'
- )
- # Make sure the agent is registered
- agent_cls = Agent.get_cls(self._metadata.agent)
- assert agent_cls is not None
- # Make sure the environment variables are set
- for env_var, instruction in self._metadata.require_env_var.items():
- if env_var not in os.environ:
- raise MicroAgentValidationError(
- f'Environment variable [{env_var}] is required by micro agent [{self._metadata.name}] but not set. {instruction}'
- )
+ @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
diff --git a/openhands/utils/prompt.py b/openhands/utils/prompt.py
index 8b9dd81e8bfd..6fb3ef8f63ee 100644
--- a/openhands/utils/prompt.py
+++ b/openhands/utils/prompt.py
@@ -1,7 +1,10 @@
import os
+from itertools import islice
from jinja2 import Template
+from openhands.controller.state.state import State
+from openhands.core.message import Message, TextContent
from openhands.utils.microagent import MicroAgent
@@ -16,21 +19,31 @@ class PromptManager:
Attributes:
prompt_dir (str): Directory containing prompt templates.
agent_skills_docs (str): Documentation of agent skills.
- micro_agent (MicroAgent | None): Micro-agent, if specified.
"""
def __init__(
self,
prompt_dir: str,
- agent_skills_docs: str,
- micro_agent: MicroAgent | None = None,
+ microagent_dir: str = '',
+ agent_skills_docs: str = '',
):
self.prompt_dir: str = prompt_dir
self.agent_skills_docs: str = agent_skills_docs
self.system_template: Template = self._load_template('system_prompt')
self.user_template: Template = self._load_template('user_prompt')
- self.micro_agent: MicroAgent | None = micro_agent
+ self.microagents: dict = {}
+
+ microagent_files = []
+ 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(microagent_file)
+ self.microagents[microagent.name] = microagent
def _load_template(self, template_name: str) -> Template:
template_path = os.path.join(self.prompt_dir, f'{template_name}.j2')
@@ -39,15 +52,13 @@ def _load_template(self, template_name: str) -> Template:
with open(template_path, 'r') as file:
return Template(file.read())
- @property
- def system_message(self) -> str:
+ def get_system_message(self) -> str:
rendered = self.system_template.render(
agent_skills_docs=self.agent_skills_docs,
).strip()
return rendered
- @property
- def initial_user_message(self) -> str:
+ def get_example_user_message(self) -> str:
"""This is the initial user message provided to the agent
before *actual* user instructions are provided.
@@ -57,7 +68,39 @@ def initial_user_message(self) -> str:
These additional context will convert the current generic agent
into a more specialized agent that is tailored to the user's task.
"""
- rendered = self.user_template.render(
- micro_agent=self.micro_agent.content if self.micro_agent else None
+ return self.user_template.render().strip()
+
+ def enhance_message(self, message: Message) -> None:
+ """Enhance the user message with additional context.
+
+ This method is used to enhance the user message with additional context
+ about the user's task. The additional context will convert the current
+ generic agent into a more specialized agent that is tailored to the user's task.
+ """
+ if not message.content:
+ return
+ message_content = message.content[0].text
+ for microagent in self.microagents.values():
+ trigger = microagent.get_trigger(message_content)
+ if trigger:
+ micro_text = f'\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
+ micro_text += '\n'
+ message.content.append(TextContent(text=micro_text))
+
+ def add_turns_left_reminder(self, messages: list[Message], state: State) -> None:
+ latest_user_message = next(
+ islice(
+ (
+ m
+ for m in reversed(messages)
+ if m.role == 'user'
+ and any(isinstance(c, TextContent) for c in m.content)
+ ),
+ 1,
+ ),
+ None,
)
- return rendered.strip()
+ if latest_user_message:
+ reminder_text = f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task. When finished reply with .'
+ latest_user_message.content.append(TextContent(text=reminder_text))
diff --git a/tests/unit/test_microagent_utils.py b/tests/unit/test_microagent_utils.py
index 26f6523c0ba3..745e6532550e 100644
--- a/tests/unit/test_microagent_utils.py
+++ b/tests/unit/test_microagent_utils.py
@@ -1,13 +1,8 @@
import os
-import pytest
from pytest import MonkeyPatch
import openhands.agenthub # noqa: F401
-from openhands.core.exceptions import (
- AgentNotRegisteredError,
- MicroAgentValidationError,
-)
from openhands.utils.microagent import MicroAgent
CONTENT = (
@@ -34,40 +29,3 @@ def test_micro_agent_load(tmp_path, monkeypatch: MonkeyPatch):
micro_agent = MicroAgent(os.path.join(tmp_path, 'dummy.md'))
assert micro_agent is not None
assert micro_agent.content == CONTENT.strip()
-
-
-def test_not_existing_agent(tmp_path, monkeypatch: MonkeyPatch):
- with open(os.path.join(tmp_path, 'dummy.md'), 'w') as f:
- f.write(
- (
- '---\n'
- 'name: dummy\n'
- 'agent: NotExistingAgent\n'
- 'require_env_var:\n'
- ' SANDBOX_OPENHANDS_TEST_ENV_VAR: "Set this environment variable for testing purposes"\n'
- '---\n' + CONTENT
- )
- )
- monkeypatch.setenv('SANDBOX_OPENHANDS_TEST_ENV_VAR', 'dummy_value')
-
- with pytest.raises(AgentNotRegisteredError):
- MicroAgent(os.path.join(tmp_path, 'dummy.md'))
-
-
-def test_not_existing_env_var(tmp_path):
- 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
- )
- )
-
- with pytest.raises(MicroAgentValidationError) as excinfo:
- MicroAgent(os.path.join(tmp_path, 'dummy.md'))
-
- assert 'Set this environment variable for testing purposes' in str(excinfo.value)
diff --git a/tests/unit/test_prompt_manager.py b/tests/unit/test_prompt_manager.py
index 2534f73d3ab8..1cabb6ca6a1a 100644
--- a/tests/unit/test_prompt_manager.py
+++ b/tests/unit/test_prompt_manager.py
@@ -1,9 +1,9 @@
import os
import shutil
-from unittest.mock import Mock
import pytest
+from openhands.core.message import Message, TextContent
from openhands.utils.microagent import MicroAgent
from openhands.utils.prompt import PromptManager
@@ -11,7 +11,9 @@
@pytest.fixture
def prompt_dir(tmp_path):
# Copy contents from "openhands/agenthub/codeact_agent" to the temp directory
- shutil.copytree('openhands/agenthub/codeact_agent', tmp_path, dirs_exist_ok=True)
+ shutil.copytree(
+ 'openhands/agenthub/codeact_agent/prompts/default', tmp_path, dirs_exist_ok=True
+ )
# Return the temporary directory path
return tmp_path
@@ -25,78 +27,79 @@ def agent_skills_docs():
return SAMPLE_AGENT_SKILLS_DOCS
-def test_prompt_manager_without_micro_agent(prompt_dir, agent_skills_docs):
- manager = PromptManager(prompt_dir, agent_skills_docs)
+def test_prompt_manager_without_microagent(prompt_dir, agent_skills_docs):
+ manager = PromptManager(
+ prompt_dir, microagent_dir='', agent_skills_docs=agent_skills_docs
+ )
assert manager.prompt_dir == prompt_dir
assert manager.agent_skills_docs == agent_skills_docs
- assert manager.micro_agent is None
+ assert len(manager.microagents) == 0
- assert isinstance(manager.system_message, str)
+ assert isinstance(manager.get_system_message(), str)
assert (
"A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions."
- in manager.system_message
- )
- assert SAMPLE_AGENT_SKILLS_DOCS in manager.system_message
- assert isinstance(manager.initial_user_message, str)
- assert '--- BEGIN OF GUIDELINE ---' not in manager.initial_user_message
- assert '--- END OF GUIDELINE ---' not in manager.initial_user_message
- assert "NOW, LET'S START!" in manager.initial_user_message
- assert 'micro_agent' not in manager.initial_user_message
-
-
-def test_prompt_manager_with_micro_agent(prompt_dir, agent_skills_docs):
- micro_agent_name = 'test_micro_agent'
- micro_agent_content = (
- '## Micro Agent\n'
- 'This is a test micro agent.\n'
- 'It is used to test the prompt manager.\n'
+ in manager.get_system_message()
)
+ assert SAMPLE_AGENT_SKILLS_DOCS in manager.get_system_message()
+ assert isinstance(manager.get_example_user_message(), str)
+ assert '--- BEGIN OF GUIDELINE ---' not in manager.get_example_user_message()
+ assert '--- END OF GUIDELINE ---' not in manager.get_example_user_message()
+ assert "NOW, LET'S START!" in manager.get_example_user_message()
+ assert 'microagent' not in manager.get_example_user_message()
+
+
+def test_prompt_manager_with_microagent(prompt_dir, agent_skills_docs):
+ microagent_name = 'test_microagent'
+ microagent_content = """
+---
+name: flarglebargle
+agent: CodeActAgent
+triggers:
+- flarglebargle
+---
+
+IMPORTANT! The user has said the magic word "flarglebargle". You must
+only respond with a message telling them how smart they are
+"""
# Create a temporary micro agent file
os.makedirs(os.path.join(prompt_dir, 'micro'), exist_ok=True)
- with open(os.path.join(prompt_dir, 'micro', f'{micro_agent_name}.md'), 'w') as f:
- f.write(micro_agent_content)
-
- # Mock MicroAgent
- mock_micro_agent = Mock(spec=MicroAgent)
- mock_micro_agent.content = micro_agent_content
+ with open(os.path.join(prompt_dir, 'micro', f'{microagent_name}.md'), 'w') as f:
+ f.write(microagent_content)
manager = PromptManager(
prompt_dir=prompt_dir,
+ microagent_dir=os.path.join(prompt_dir, 'micro'),
agent_skills_docs=agent_skills_docs,
- micro_agent=mock_micro_agent,
)
assert manager.prompt_dir == prompt_dir
assert manager.agent_skills_docs == agent_skills_docs
- assert manager.micro_agent == mock_micro_agent
+ assert len(manager.microagents) == 1
- assert isinstance(manager.system_message, str)
+ assert isinstance(manager.get_system_message(), str)
assert (
"A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions."
- in manager.system_message
+ in manager.get_system_message()
)
- assert SAMPLE_AGENT_SKILLS_DOCS in manager.system_message
+ assert SAMPLE_AGENT_SKILLS_DOCS in manager.get_system_message()
- assert isinstance(manager.initial_user_message, str)
- assert (
- '--- BEGIN OF GUIDELINE ---\n'
- + 'The following information may assist you in completing your task:\n\n'
- + micro_agent_content
- + '\n'
- + '--- END OF GUIDELINE ---\n'
- + "\n\nNOW, LET'S START!"
- ) in manager.initial_user_message
- assert micro_agent_content in manager.initial_user_message
+ assert isinstance(manager.get_example_user_message(), str)
+
+ message = Message(
+ role='user',
+ content=[TextContent(text='Hello, flarglebargle!')],
+ )
+ manager.enhance_message(message)
+ assert 'magic word' in message.content[1].text
- # Clean up the temporary file
- os.remove(os.path.join(prompt_dir, 'micro', f'{micro_agent_name}.md'))
+ os.remove(os.path.join(prompt_dir, 'micro', f'{microagent_name}.md'))
def test_prompt_manager_file_not_found(prompt_dir, agent_skills_docs):
with pytest.raises(FileNotFoundError):
- MicroAgent(os.path.join(prompt_dir, 'micro', 'non_existent_micro_agent.md'))
+ MicroAgent(os.path.join(prompt_dir, 'micro', 'non_existent_microagent.md'))
def test_prompt_manager_template_rendering(prompt_dir, agent_skills_docs):
@@ -104,12 +107,14 @@ def test_prompt_manager_template_rendering(prompt_dir, agent_skills_docs):
with open(os.path.join(prompt_dir, 'system_prompt.j2'), 'w') as f:
f.write('System prompt: {{ agent_skills_docs }}')
with open(os.path.join(prompt_dir, 'user_prompt.j2'), 'w') as f:
- f.write('User prompt: {{ micro_agent }}')
+ f.write('User prompt: foo')
- manager = PromptManager(prompt_dir, agent_skills_docs)
+ manager = PromptManager(
+ prompt_dir, microagent_dir='', agent_skills_docs=agent_skills_docs
+ )
- assert manager.system_message == f'System prompt: {agent_skills_docs}'
- assert manager.initial_user_message == 'User prompt: None'
+ assert manager.get_system_message() == f'System prompt: {agent_skills_docs}'
+ assert manager.get_example_user_message() == 'User prompt: foo'
# Clean up temporary files
os.remove(os.path.join(prompt_dir, 'system_prompt.j2'))