mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
18 Commits
prd/org-co
...
enyst/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3a02bae4f | ||
|
|
fc30ac32f0 | ||
|
|
f224e077ff | ||
|
|
ea2b598490 | ||
|
|
a4dddf8e16 | ||
|
|
12385d2be6 | ||
|
|
6f282b90da | ||
|
|
1df7aaa0cf | ||
|
|
ada2ebd4f5 | ||
|
|
6732359894 | ||
|
|
bf9b8acbab | ||
|
|
e2c343a733 | ||
|
|
bbd5211c3b | ||
|
|
9629a73391 | ||
|
|
7930457211 | ||
|
|
5df104dcb2 | ||
|
|
e75a489de9 | ||
|
|
b93c81869a |
59
openhands/agenthub/codeact_agent/agent.yaml
Normal file
59
openhands/agenthub/codeact_agent/agent.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
name: CodeActAgent
|
||||
|
||||
# custom templates directory
|
||||
# .j2 templates will be loaded from this directory if found, if not, the default will be used
|
||||
custom_templates_dir: "user_templates"
|
||||
|
||||
# main templates and their blocks
|
||||
template:
|
||||
system_prompt:
|
||||
file: "system_prompt" # path to the system template file
|
||||
blocks:
|
||||
- SYSTEM_PREFIX
|
||||
- PYTHON_CAPABILITIES
|
||||
- BASH_CAPABILITIES
|
||||
- BROWSING_CAPABILITIES
|
||||
- PIP_CAPABILITIES
|
||||
- AGENT_SKILLS
|
||||
- SYSTEM_RULES
|
||||
|
||||
agent_skills:
|
||||
file: "agent_skills"
|
||||
blocks:
|
||||
- SKILL_DOCS
|
||||
|
||||
user_prompt:
|
||||
file: "user_prompt"
|
||||
blocks:
|
||||
- USER_PROMPT
|
||||
|
||||
examples:
|
||||
file: "examples"
|
||||
blocks:
|
||||
- DEFAULT_EXAMPLE
|
||||
|
||||
micro_agent:
|
||||
file: "micro_agent"
|
||||
blocks:
|
||||
- MICRO_AGENT_GUIDELINES
|
||||
|
||||
# agent-specific variables (can be accessed within templates)
|
||||
use_tools: false # whether to use tool-based implementations
|
||||
# tools: # list of available tools
|
||||
# - name: "EditTool"
|
||||
# description: "Edits a file."
|
||||
# usage: "Use the following format: <file_edit> [file_path] [new_file_content] </file_edit>"
|
||||
# agent skills
|
||||
agent_skills:
|
||||
available_skills:
|
||||
- "file_ops:open_file"
|
||||
- "file_ops:goto_line"
|
||||
- "file_ops:scroll_down"
|
||||
- "file_ops:scroll_up"
|
||||
- "file_ops:search_dir"
|
||||
- "file_ops:search_file"
|
||||
- "file_ops:find_file"
|
||||
- "file_reader:parse_pdf"
|
||||
- "file_reader:parse_docx"
|
||||
- "file_reader:parse_latex"
|
||||
- "file_reader:parse_pptx"
|
||||
@@ -91,7 +91,6 @@ class CodeActAgent(Agent):
|
||||
|
||||
self.prompt_manager = PromptManager(
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__)),
|
||||
agent_skills_docs=AgentSkillsRequirement.documentation,
|
||||
micro_agent=self.micro_agent,
|
||||
)
|
||||
|
||||
|
||||
10
openhands/agenthub/codeact_agent/prompts/agent_skills.j2
Normal file
10
openhands/agenthub/codeact_agent/prompts/agent_skills.j2
Normal file
@@ -0,0 +1,10 @@
|
||||
{# Agent skills documentation template #}
|
||||
|
||||
{% block skill_docs %}
|
||||
{% for skill_name in available_skills %}
|
||||
{% set skill_docstring = get_skill_docstring(skill_name) %}
|
||||
{% if skill_docstring %}
|
||||
{{ skill_docstring }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% set DEFAULT_EXAMPLE %}
|
||||
{% block DEFAULT_EXAMPLE %}
|
||||
--- START OF EXAMPLE ---
|
||||
|
||||
USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
|
||||
@@ -212,15 +212,4 @@ The server is running on port 5000 with PID 126. You can access the list of numb
|
||||
<finish></finish>
|
||||
|
||||
--- END OF EXAMPLE ---
|
||||
{% 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!
|
||||
{% endblock %}
|
||||
9
openhands/agenthub/codeact_agent/prompts/micro_agent.j2
Normal file
9
openhands/agenthub/codeact_agent/prompts/micro_agent.j2
Normal file
@@ -0,0 +1,9 @@
|
||||
{% block MICRO_AGENT_GUIDELINES %}
|
||||
{% if micro_agent %}
|
||||
--- BEGIN OF GUIDELINE ---
|
||||
The following information may assist you in completing your task:
|
||||
|
||||
{{ micro_agent }}
|
||||
--- END OF GUIDELINE ---
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,11 +1,19 @@
|
||||
{% set MINIMAL_SYSTEM_PREFIX %}
|
||||
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
|
||||
{# Core system components for the CodeAct Agent #}
|
||||
|
||||
[1] The assistant can use a Python environment with <execute_ipython>, e.g.:
|
||||
{# Base system identity and core abilities #}
|
||||
{% block SYSTEM_PREFIX %}
|
||||
A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed answers to the user's questions.
|
||||
{% endblock %}
|
||||
|
||||
{% block PYTHON_CAPABILITIES %}
|
||||
You can use a Python environment with <execute_ipython>, e.g.:
|
||||
<execute_ipython>
|
||||
print("Hello World!")
|
||||
</execute_ipython>
|
||||
{% endblock %}
|
||||
|
||||
{# Bash execution capabilities #}
|
||||
{% block BASH_CAPABILITIES %}
|
||||
[2] The assistant can execute bash commands wrapped with <execute_bash>, e.g. <execute_bash> ls </execute_bash>.
|
||||
If a bash command returns exit code `-1`, this means the process is not yet finished.
|
||||
The assistant must then send a second <execute_bash>. The second <execute_bash> can be empty
|
||||
@@ -134,27 +142,42 @@ class MyClass:
|
||||
# MyClass().z is removed
|
||||
print(MyClass().y)
|
||||
</file_edit>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% endset %}
|
||||
{% set BROWSING_PREFIX %}
|
||||
{# Web browsing #}
|
||||
{% block BROWSING_CAPABILITIES %}
|
||||
The assistant can browse the Internet with <execute_browse> and </execute_browse>.
|
||||
For example, <execute_browse> Tell me the usa's president using google search </execute_browse>.
|
||||
For example, <execute_browse> Tell me the USA's president using Google search </execute_browse>.
|
||||
Or <execute_browse> Tell me what is in http://example.com </execute_browse>.
|
||||
{% endset %}
|
||||
{% set PIP_INSTALL_PREFIX %}
|
||||
{% endblock %}
|
||||
|
||||
{# Package management #}
|
||||
{% block PIP_CAPABILITIES %}
|
||||
The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them.
|
||||
{% endset %}
|
||||
{% set SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + BROWSING_PREFIX + PIP_INSTALL_PREFIX %}
|
||||
{% set COMMAND_DOCS %}
|
||||
{% endblock %}
|
||||
|
||||
{# Agent skills documentation #}
|
||||
{% block AGENT_SKILLS %}
|
||||
{% if use_tools %}
|
||||
{# Tool-based implementation #}
|
||||
The following tools are available:
|
||||
{% for tool in tools %}
|
||||
- {{ tool.name }}: {{ tool.description }}
|
||||
Usage: {{ tool.usage }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
Apart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:
|
||||
{{ agent_skills_docs }}
|
||||
|
||||
IMPORTANT:
|
||||
- `open_file` only returns the first 100 lines of the file by default! The assistant MUST use `scroll_down` repeatedly to read the full file BEFORE making edits!
|
||||
- Indentation is important and code that is not indented correctly will fail and require fixing before it can be run.
|
||||
- Any code issued should be less than 50 lines to avoid context being cut off!
|
||||
{% endset %}
|
||||
{% set SYSTEM_SUFFIX %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{# System behavior rules #}
|
||||
{% block SYSTEM_RULES %}
|
||||
Responses should be concise.
|
||||
The assistant should attempt fewer things at a time instead of putting too many commands OR too much code in one "execute" block.
|
||||
Include ONLY ONE <execute_ipython>, <execute_bash>, or <execute_browse> per response, unless the assistant is finished with the task or needs more input or action from the user in order to proceed.
|
||||
@@ -163,9 +186,4 @@ IMPORTANT: Execute code using <execute_ipython>, <execute_bash>, or <execute_bro
|
||||
The assistant should utilize full file paths and the `pwd` command to prevent path-related errors.
|
||||
The assistant MUST NOT apologize to the user or thank the user after running commands or editing files. It should only address the user in response to an explicit message from the user, or to ask for more information.
|
||||
The assistant MUST NOT push any changes to GitHub unless explicitly requested to do so.
|
||||
|
||||
{% endset %}
|
||||
{# Combine all parts without newlines between them #}
|
||||
{{ SYSTEM_PREFIX -}}
|
||||
{{- COMMAND_DOCS -}}
|
||||
{{- SYSTEM_SUFFIX }}
|
||||
{% endblock %}
|
||||
8
openhands/agenthub/codeact_agent/prompts/user_prompt.j2
Normal file
8
openhands/agenthub/codeact_agent/prompts/user_prompt.j2
Normal file
@@ -0,0 +1,8 @@
|
||||
Here is an example of how you can interact with the environment for task solving:
|
||||
{% block USER_PROMPT %}
|
||||
{{ examples }}
|
||||
|
||||
{{ micro_agent_content }}
|
||||
|
||||
NOW, LET'S START!
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,11 @@
|
||||
import importlib
|
||||
import os
|
||||
from inspect import signature
|
||||
|
||||
from jinja2 import Template
|
||||
import yaml
|
||||
from jinja2 import Environment, FileSystemLoader, Template, TemplateNotFound
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.utils.microagent import MicroAgent
|
||||
|
||||
|
||||
@@ -15,36 +19,169 @@ 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,
|
||||
):
|
||||
self.prompt_dir: str = prompt_dir
|
||||
self.agent_skills_docs: str = agent_skills_docs
|
||||
) -> None:
|
||||
"""
|
||||
Initializes the PromptManager with the given prompt directory and agent skills documentation.
|
||||
|
||||
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
|
||||
Args:
|
||||
prompt_dir: The directory containing the prompt templates.
|
||||
micro_agent: The micro-agent to use for generating responses.
|
||||
"""
|
||||
self.prompt_dir = prompt_dir
|
||||
self.micro_agent = micro_agent
|
||||
|
||||
# load configuration from agent.yaml
|
||||
yaml_path = os.path.join(prompt_dir, 'agent.yaml')
|
||||
if os.path.exists(yaml_path):
|
||||
with open(yaml_path, 'r') as f:
|
||||
self.config = yaml.safe_load(f)
|
||||
|
||||
custom_templates_dir = self.config.get('custom_templates_dir', None)
|
||||
if custom_templates_dir:
|
||||
custom_templates_dir = os.path.abspath(custom_templates_dir)
|
||||
|
||||
# prioritize custom_templates_dir over the default templates directory
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader([custom_templates_dir, self.prompt_dir])
|
||||
)
|
||||
else:
|
||||
self.env = Environment(loader=FileSystemLoader(self.prompt_dir))
|
||||
|
||||
# load templates and blocks
|
||||
template_config = self.config['template']
|
||||
self.templates = {}
|
||||
for name, cfg in template_config.items():
|
||||
template = self._load_template(cfg['file'])
|
||||
self.templates[name] = {
|
||||
'template': template,
|
||||
'blocks': cfg.get('blocks', []),
|
||||
}
|
||||
|
||||
self.available_skills = self.config['agent_skills']['available_skills']
|
||||
else:
|
||||
# load default templates if agent.yaml is not present
|
||||
self.env = Environment(loader=FileSystemLoader(self.prompt_dir))
|
||||
self.templates = self._load_default_templates()
|
||||
self.available_skills = []
|
||||
|
||||
# TODO: agent config will have tool use enabled or disabled
|
||||
# to conditionally load the tools variant of agentskills
|
||||
|
||||
def _load_template(self, template_name: str) -> Template:
|
||||
template_path = os.path.join(self.prompt_dir, f'{template_name}.j2')
|
||||
if not os.path.exists(template_path):
|
||||
raise FileNotFoundError(f'Prompt file {template_path} not found')
|
||||
with open(template_path, 'r') as file:
|
||||
return Template(file.read())
|
||||
"""Loads a Jinja2 template by name."""
|
||||
try:
|
||||
return self.env.get_template(f'{template_name}.j2')
|
||||
except TemplateNotFound:
|
||||
# try to load from the prompt_dir
|
||||
template_path = os.path.join(self.prompt_dir, f'{template_name}.j2')
|
||||
if not os.path.exists(template_path):
|
||||
raise FileNotFoundError(f'Prompt file {template_path} not found')
|
||||
with open(template_path, 'r') as file:
|
||||
return Template(file.read())
|
||||
|
||||
def _load_default_templates(self) -> dict:
|
||||
"""Loads default template configuration when no agent.yaml is present.
|
||||
|
||||
Returns:
|
||||
Dictionary with default templates.
|
||||
"""
|
||||
# load all default templates
|
||||
templates = {}
|
||||
|
||||
# system prompt with standard block order
|
||||
templates['system_prompt'] = {
|
||||
'template': self._load_template('system_prompt'),
|
||||
'blocks': [
|
||||
'system_prefix',
|
||||
'python_capabilities',
|
||||
'bash_capabilities',
|
||||
'browsing_capabilities',
|
||||
'pip_capabilities',
|
||||
'agent_skills',
|
||||
'system_rules',
|
||||
],
|
||||
}
|
||||
|
||||
# agent skills documentation
|
||||
templates['agent_skills'] = {
|
||||
'template': self._load_template('agent_skills'),
|
||||
'blocks': ['skill_docs'],
|
||||
}
|
||||
|
||||
# example interactions
|
||||
templates['examples'] = {
|
||||
'template': self._load_template('examples'),
|
||||
}
|
||||
|
||||
# micro-agent guidelines
|
||||
templates['micro_agent'] = {
|
||||
'template': self._load_template('micro_agent'),
|
||||
'blocks': ['micro_agent_guidelines'],
|
||||
}
|
||||
|
||||
# user prompt combining everything
|
||||
templates['user_prompt'] = {
|
||||
'template': self._load_template('user_prompt'),
|
||||
'blocks': ['user_prompt', 'default_example', 'micro_agent_guidelines'],
|
||||
}
|
||||
|
||||
return templates
|
||||
|
||||
def _render_blocks(self, template_name: str, **kwargs) -> str:
|
||||
"""Renders template blocks in the specified order with shared context.
|
||||
|
||||
Args:
|
||||
template_name: Name of the template to render.
|
||||
**kwargs: Variables to pass to the template context.
|
||||
|
||||
Returns:
|
||||
Rendered string combining all blocks in order.
|
||||
"""
|
||||
template_info = self.templates.get(template_name)
|
||||
if not template_info:
|
||||
logger.error(f"Template '{template_name}' not found.")
|
||||
return ''
|
||||
|
||||
rendered_blocks = []
|
||||
|
||||
# create a new template context that will be shared across all blocks
|
||||
context = template_info['template'].new_context(kwargs)
|
||||
|
||||
# render each block using the shared context
|
||||
for block_name in template_info.get('blocks', []):
|
||||
try:
|
||||
logger.debug(f"Rendering block '{template_name}.{block_name}'")
|
||||
block = template_info['template'].blocks[block_name]
|
||||
rendered = ''.join(block(context))
|
||||
rendered_blocks.append(rendered)
|
||||
except KeyError:
|
||||
logger.warning(
|
||||
f"Block '{block_name}' not found in template '{template_name}'"
|
||||
)
|
||||
continue
|
||||
|
||||
return ''.join(rendered_blocks)
|
||||
|
||||
@property
|
||||
def system_message(self) -> str:
|
||||
rendered = self.system_template.render(
|
||||
agent_skills_docs=self.agent_skills_docs,
|
||||
"""Renders and returns the system message."""
|
||||
self.env.globals['get_skill_docstring'] = self._get_skill_docstring
|
||||
|
||||
# render agent skills blocks first
|
||||
rendered_docs = self._render_blocks(
|
||||
'agent_skills', available_skills=self.available_skills
|
||||
).strip()
|
||||
return rendered
|
||||
|
||||
# then render system blocks with agent skills included
|
||||
rendered = self._render_blocks('system_prompt', agent_skills_docs=rendered_docs)
|
||||
return rendered.strip()
|
||||
|
||||
@property
|
||||
def initial_user_message(self) -> str:
|
||||
@@ -57,7 +194,43 @@ class PromptManager:
|
||||
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
|
||||
# render each component's blocks
|
||||
rendered_examples = self._render_blocks('examples').strip()
|
||||
rendered_micro_agent = self._render_blocks(
|
||||
'micro_agent',
|
||||
micro_agent=self.micro_agent.content if self.micro_agent else None,
|
||||
).strip()
|
||||
|
||||
# combine in user prompt
|
||||
rendered = self._render_blocks(
|
||||
'user_prompt',
|
||||
examples=rendered_examples,
|
||||
micro_agent_content=rendered_micro_agent,
|
||||
)
|
||||
return rendered.strip()
|
||||
|
||||
def _get_skill_docstring(self, skill_name: str) -> str:
|
||||
"""Retrieves the docstring of a skill function."""
|
||||
module_name, function_name = skill_name.split(':')
|
||||
try:
|
||||
module = importlib.import_module(
|
||||
f'openhands.runtime.plugins.agent_skills.{module_name}'
|
||||
)
|
||||
|
||||
# find the function
|
||||
agent_skill_fn = getattr(module, function_name)
|
||||
|
||||
# get the function signature with parameter names, types and return type
|
||||
fn_signature = f'{agent_skill_fn.__name__}' + str(signature(agent_skill_fn))
|
||||
|
||||
doc = agent_skill_fn.__doc__
|
||||
|
||||
# remove indentation from docstring and extra empty lines
|
||||
doc = '\n'.join(filter(None, map(lambda x: x.strip(), doc.split('\n'))))
|
||||
|
||||
# now add a consistent 4 indentation
|
||||
doc = '\n'.join(map(lambda x: ' ' * 4 + x, doc.split('\n')))
|
||||
return f'{fn_signature}\n{doc}'
|
||||
except (ImportError, AttributeError) as e:
|
||||
logger.error(e)
|
||||
return f'Documentation not found for skill: {skill_name}'
|
||||
|
||||
@@ -10,11 +10,19 @@ from openhands.utils.prompt import PromptManager
|
||||
|
||||
@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)
|
||||
"""Creates a temporary directory with CodeAct agent templates.
|
||||
|
||||
# Return the temporary directory path
|
||||
return tmp_path
|
||||
Copies the entire codeact_agent directory structure to a temp location
|
||||
to avoid modifying the original files during tests.
|
||||
"""
|
||||
# Copy contents from "openhands/agenthub/codeact_agent" to the temp directory
|
||||
source = 'openhands/agenthub/codeact_agent'
|
||||
shutil.copytree(source, tmp_path, dirs_exist_ok=True)
|
||||
|
||||
# Return path to the prompts directory
|
||||
return str(
|
||||
os.path.join(tmp_path, 'prompts')
|
||||
) # Points to the correct prompts directory
|
||||
|
||||
|
||||
SAMPLE_AGENT_SKILLS_DOCS = """Sample agent skills documentation"""
|
||||
@@ -26,10 +34,10 @@ def agent_skills_docs():
|
||||
|
||||
|
||||
def test_prompt_manager_without_micro_agent(prompt_dir, agent_skills_docs):
|
||||
manager = PromptManager(prompt_dir, agent_skills_docs)
|
||||
manager = PromptManager(prompt_dir)
|
||||
|
||||
assert manager.prompt_dir == prompt_dir
|
||||
assert manager.agent_skills_docs == agent_skills_docs
|
||||
# assert manager.agent_skills_docs == agent_skills_docs
|
||||
assert manager.micro_agent is None
|
||||
|
||||
assert isinstance(manager.system_message, str)
|
||||
@@ -37,7 +45,7 @@ def test_prompt_manager_without_micro_agent(prompt_dir, agent_skills_docs):
|
||||
"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 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
|
||||
@@ -64,12 +72,11 @@ def test_prompt_manager_with_micro_agent(prompt_dir, agent_skills_docs):
|
||||
|
||||
manager = PromptManager(
|
||||
prompt_dir=prompt_dir,
|
||||
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.agent_skills_docs == agent_skills_docs
|
||||
assert manager.micro_agent == mock_micro_agent
|
||||
|
||||
assert isinstance(manager.system_message, str)
|
||||
@@ -77,7 +84,7 @@ def test_prompt_manager_with_micro_agent(prompt_dir, agent_skills_docs):
|
||||
"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 SAMPLE_AGENT_SKILLS_DOCS in manager.system_message
|
||||
|
||||
assert isinstance(manager.initial_user_message, str)
|
||||
assert (
|
||||
@@ -106,11 +113,178 @@ def test_prompt_manager_template_rendering(prompt_dir, agent_skills_docs):
|
||||
with open(os.path.join(prompt_dir, 'user_prompt.j2'), 'w') as f:
|
||||
f.write('User prompt: {{ micro_agent }}')
|
||||
|
||||
manager = PromptManager(prompt_dir, agent_skills_docs)
|
||||
manager = PromptManager(prompt_dir)
|
||||
|
||||
assert manager.system_message == f'System prompt: {agent_skills_docs}'
|
||||
# assert manager.system_message == f'System prompt: {agent_skills_docs}'
|
||||
assert manager.initial_user_message == 'User prompt: None'
|
||||
|
||||
# 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_loads_agent_skill(prompt_dir):
|
||||
manager = PromptManager(prompt_dir)
|
||||
assert (
|
||||
'open_file(path: str, line_number: int | None = 1, context_lines: int | None = 100) -> None'
|
||||
in manager.system_message
|
||||
)
|
||||
|
||||
|
||||
def test_prompt_manager_block_rendering(prompt_dir):
|
||||
"""Test that blocks are rendered correctly from templates."""
|
||||
manager = PromptManager(prompt_dir)
|
||||
|
||||
# System blocks should contain core capabilities
|
||||
system_msg = manager.system_message
|
||||
assert (
|
||||
'A chat between a curious user and an artificial intelligence assistant'
|
||||
in system_msg
|
||||
) # system_prefix block
|
||||
assert '<execute_ipython>' in system_msg # python_capabilities block
|
||||
assert '<execute_bash>' in system_msg # bash_capabilities block
|
||||
assert 'browse the Internet' in system_msg # browsing_capabilities block
|
||||
assert '%pip install' in system_msg # pip_capabilities block
|
||||
|
||||
|
||||
def test_prompt_manager_agent_skills_blocks(prompt_dir):
|
||||
"""Test that agent skills blocks are rendered with proper docstrings.
|
||||
This ensures skills documentation is correctly loaded and formatted."""
|
||||
manager = PromptManager(prompt_dir)
|
||||
|
||||
# Check if specific skill docstrings are present
|
||||
skills_content = manager._render_blocks(
|
||||
'agent_skills', available_skills=['file_ops:open_file']
|
||||
)
|
||||
|
||||
# Should contain function signature and docstring
|
||||
assert 'open_file(path: str' in skills_content
|
||||
assert 'Opens and displays the content of a file' in skills_content
|
||||
|
||||
|
||||
def test_prompt_manager_micro_agent_blocks(prompt_dir):
|
||||
"""Test micro-agent block rendering with and without micro-agent content.
|
||||
Verifies conditional rendering of micro-agent guidelines."""
|
||||
# Test without micro-agent
|
||||
manager = PromptManager(prompt_dir)
|
||||
micro_content = manager._render_blocks('micro_agent')
|
||||
assert '--- BEGIN OF GUIDELINE ---' not in micro_content
|
||||
|
||||
# Test with micro-agent
|
||||
mock_micro_agent = Mock(spec=MicroAgent)
|
||||
mock_micro_agent.content = 'Test micro-agent content'
|
||||
manager = PromptManager(prompt_dir, micro_agent=mock_micro_agent)
|
||||
|
||||
micro_content = manager._render_blocks(
|
||||
'micro_agent', micro_agent=mock_micro_agent.content
|
||||
)
|
||||
assert '--- BEGIN OF GUIDELINE ---' in micro_content
|
||||
assert 'Test micro-agent content' in micro_content
|
||||
|
||||
|
||||
def test_prompt_manager_custom_block_order(prompt_dir):
|
||||
"""Test that blocks are rendered in the order specified"""
|
||||
# Create a custom agent.yaml with specific block order
|
||||
custom_yaml = """
|
||||
template:
|
||||
system_prompt:
|
||||
file: "system_prompt"
|
||||
blocks:
|
||||
- system_prefix
|
||||
- python_capabilities
|
||||
- agent_skills
|
||||
"""
|
||||
|
||||
yaml_path = os.path.join(prompt_dir, 'agent.yaml')
|
||||
with open(yaml_path, 'w') as f:
|
||||
f.write(custom_yaml)
|
||||
|
||||
manager = PromptManager(prompt_dir)
|
||||
system_msg = manager.system_message
|
||||
|
||||
# Verify block order
|
||||
prefix_pos = system_msg.find('You are a new generation AI assistant')
|
||||
python_pos = system_msg.find('<execute_ipython>')
|
||||
skills_pos = system_msg.find('open_file(path: str')
|
||||
|
||||
# Blocks should appear in specified order
|
||||
assert prefix_pos < python_pos < skills_pos
|
||||
assert '<execute_bash>' not in system_msg # This block wasn't included
|
||||
|
||||
|
||||
def test_prompt_manager_missing_blocks(prompt_dir):
|
||||
"""Test graceful handling of missing or undefined blocks.
|
||||
Ensures the system doesn't crash on template/block misconfigurations."""
|
||||
# Create yaml with non-existent block
|
||||
custom_yaml = """
|
||||
template:
|
||||
system_prompt:
|
||||
file: "system_prompt"
|
||||
blocks:
|
||||
- nonexistent_block
|
||||
"""
|
||||
|
||||
yaml_path = os.path.join(prompt_dir, 'agent.yaml')
|
||||
with open(yaml_path, 'w') as f:
|
||||
f.write(custom_yaml)
|
||||
|
||||
# Should not raise exception, but log warning and skip block
|
||||
manager = PromptManager(prompt_dir)
|
||||
assert manager.system_message.strip() != ''
|
||||
|
||||
|
||||
def test_prompt_manager_block_inheritance(prompt_dir):
|
||||
"""Test that blocks can be overridden in custom templates.
|
||||
Verifies template inheritance and customization capabilities."""
|
||||
# Create custom templates directory
|
||||
custom_dir = os.path.join(prompt_dir, 'user_templates')
|
||||
os.makedirs(custom_dir, exist_ok=True)
|
||||
|
||||
# Create custom system template that overrides a block
|
||||
custom_template = """
|
||||
{% block system_prefix %}
|
||||
Custom system prefix override
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
with open(os.path.join(custom_dir, 'system_prompt.j2'), 'w') as f:
|
||||
f.write(custom_template)
|
||||
|
||||
manager = PromptManager(prompt_dir)
|
||||
assert 'Custom system prefix override' in manager.system_message
|
||||
|
||||
|
||||
def test_prompt_manager_default_templates(prompt_dir):
|
||||
"""Test that default templates are loaded correctly when no yaml exists.
|
||||
Verifies the fallback configuration works as expected."""
|
||||
# Remove agent.yaml to force default template loading
|
||||
yaml_path = os.path.join(prompt_dir, 'agent.yaml')
|
||||
if os.path.exists(yaml_path):
|
||||
os.remove(yaml_path)
|
||||
|
||||
manager = PromptManager(prompt_dir)
|
||||
templates = manager.templates
|
||||
|
||||
# Check structure matches expected defaults
|
||||
assert 'system_prompt' in templates
|
||||
assert 'agent_skills' in templates
|
||||
assert 'examples' in templates
|
||||
assert 'micro_agent' in templates
|
||||
assert 'user_prompt' in templates
|
||||
|
||||
# Verify default block configuration
|
||||
system_blocks = templates['system_prompt']['blocks']
|
||||
assert 'system_prefix' in system_blocks
|
||||
assert 'python_capabilities' in system_blocks
|
||||
assert 'bash_capabilities' in system_blocks
|
||||
|
||||
# Verify templates are loaded and functional
|
||||
assert isinstance(manager.system_message, str)
|
||||
assert isinstance(manager.initial_user_message, str)
|
||||
|
||||
|
||||
def test_prompt_manager_agent_skills_block_name(prompt_dir):
|
||||
"""Verify that agent_skills template has the correct block name."""
|
||||
manager = PromptManager(prompt_dir)
|
||||
template_info = manager.templates['agent_skills']
|
||||
assert 'skill_docs' in template_info['blocks']
|
||||
|
||||
Reference in New Issue
Block a user