mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
25 Commits
feature/te
...
fix-agent-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd7c1db423 | ||
|
|
145a4bdc81 | ||
|
|
c25701fc1b | ||
|
|
bec05943a0 | ||
|
|
2c5018f529 | ||
|
|
d596fd2782 | ||
|
|
21c2253634 | ||
|
|
b95d54020c | ||
|
|
0e54bab56a | ||
|
|
bb5817cb56 | ||
|
|
f109a2ad95 | ||
|
|
956b3b4ab7 | ||
|
|
66781fc0f6 | ||
|
|
143293db44 | ||
|
|
c26185daf0 | ||
|
|
16da353508 | ||
|
|
c21ddaf1f1 | ||
|
|
801b134c7f | ||
|
|
5b063cc11b | ||
|
|
b1a18d5330 | ||
|
|
38b5198c24 | ||
|
|
dba25f5c46 | ||
|
|
c59abb5305 | ||
|
|
bd9fc5551b | ||
|
|
d80c3767ae |
@@ -2,7 +2,6 @@ import json
|
||||
import os
|
||||
from collections import deque
|
||||
|
||||
import openhands
|
||||
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
@@ -77,14 +76,7 @@ class CodeActAgent(Agent):
|
||||
f'TOOLS loaded for CodeActAgent: {json.dumps(self.tools, indent=2, ensure_ascii=False).replace("\\n", "\n")}'
|
||||
)
|
||||
self.prompt_manager = PromptManager(
|
||||
microagent_dir=os.path.join(
|
||||
os.path.dirname(os.path.dirname(openhands.__file__)),
|
||||
'microagents',
|
||||
)
|
||||
if self.config.enable_prompt_extensions
|
||||
else None,
|
||||
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
|
||||
disabled_microagents=self.config.disabled_microagents,
|
||||
)
|
||||
|
||||
# Create a ConversationMemory instance
|
||||
@@ -222,8 +214,6 @@ class CodeActAgent(Agent):
|
||||
|
||||
# enhance the user message with additional context based on keywords matched
|
||||
if msg.role == 'user':
|
||||
self.prompt_manager.enhance_message(msg)
|
||||
|
||||
# Add double newline between consecutive user messages
|
||||
if prev_role == 'user' and len(msg.content) > 0:
|
||||
# Find the first TextContent in the message to add newlines
|
||||
@@ -232,7 +222,6 @@ class CodeActAgent(Agent):
|
||||
# If the previous message was also from a user, prepend two newlines to ensure separation
|
||||
content_item.text = '\n\n' + content_item.text
|
||||
break
|
||||
|
||||
results.append(msg)
|
||||
prev_role = msg.role
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class AppConfig(BaseModel):
|
||||
file_uploads_allowed_extensions: Allowed file extensions. `['.*']` allows all.
|
||||
cli_multiline_input: Whether to enable multiline input in CLI. When disabled,
|
||||
input is read line by line. When enabled, input continues until /exit command.
|
||||
microagents_dir: Directory containing global microagents.
|
||||
"""
|
||||
|
||||
llms: dict[str, LLMConfig] = Field(default_factory=dict)
|
||||
@@ -82,6 +83,10 @@ class AppConfig(BaseModel):
|
||||
daytona_target: str = Field(default='us')
|
||||
cli_multiline_input: bool = Field(default=False)
|
||||
conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds
|
||||
microagents_dir: str = Field(
|
||||
default='microagents',
|
||||
description='Directory containing global microagents',
|
||||
)
|
||||
|
||||
defaults_dict: ClassVar[dict] = {}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from openhands.core.schema import AgentState
|
||||
from openhands.core.setup import (
|
||||
create_agent,
|
||||
create_controller,
|
||||
create_memory,
|
||||
create_runtime,
|
||||
generate_sid,
|
||||
)
|
||||
@@ -102,6 +103,7 @@ async def run_controller(
|
||||
|
||||
event_stream = runtime.event_stream
|
||||
|
||||
|
||||
replay_events: list[Event] | None = None
|
||||
if config.replay_trajectory_path:
|
||||
logger.info('Trajectory replay is enabled')
|
||||
|
||||
@@ -82,5 +82,8 @@ class ActionTypeSchema(BaseModel):
|
||||
SEND_PR: str = Field(default='send_pr')
|
||||
"""Send a PR to github."""
|
||||
|
||||
RECALL: str = Field(default='recall')
|
||||
"""Retrieves data from a file or other storage."""
|
||||
|
||||
|
||||
ActionType = ActionTypeSchema()
|
||||
|
||||
@@ -49,5 +49,8 @@ class ObservationTypeSchema(BaseModel):
|
||||
CONDENSE: str = Field(default='condense')
|
||||
"""Result of a condensation operation."""
|
||||
|
||||
RECALL: str = Field(default='recall')
|
||||
"""Result of a recall operation."""
|
||||
|
||||
|
||||
ObservationType = ObservationTypeSchema()
|
||||
|
||||
@@ -16,6 +16,7 @@ from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events import EventStream
|
||||
from openhands.events.event import Event
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.memory.memory import Memory
|
||||
from openhands.microagent.microagent import BaseMicroAgent
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.base import Runtime
|
||||
@@ -105,6 +106,39 @@ def create_runtime(
|
||||
return runtime
|
||||
|
||||
|
||||
def create_memory(
|
||||
microagents_dir: str,
|
||||
agent: Agent,
|
||||
runtime: Runtime,
|
||||
event_stream: EventStream,
|
||||
selected_repository: str | None = None,
|
||||
) -> Memory:
|
||||
# If the agent config has disabled microagents, use them
|
||||
disabled_microagents = agent.config.disabled_microagents
|
||||
|
||||
mem = Memory(
|
||||
event_stream=event_stream,
|
||||
microagents_dir=microagents_dir,
|
||||
disabled_microagents=disabled_microagents,
|
||||
)
|
||||
|
||||
if agent.prompt_manager and runtime:
|
||||
# sets available hosts
|
||||
mem.set_runtime_info(runtime.web_hosts)
|
||||
|
||||
# loads microagents from repo/.openhands/microagents
|
||||
microagents: list[BaseMicroAgent] = runtime.get_microagents_from_selected_repo(
|
||||
selected_repository
|
||||
)
|
||||
mem.load_user_workspace_microagents(microagents)
|
||||
|
||||
if selected_repository:
|
||||
repo_directory = selected_repository.split('/')[1]
|
||||
if repo_directory:
|
||||
mem.set_repository_info(selected_repository, repo_directory)
|
||||
return mem
|
||||
|
||||
|
||||
def create_agent(config: AppConfig) -> Agent:
|
||||
agent_cls: Type[Agent] = Agent.get_cls(config.default_agent)
|
||||
agent_config = config.get_agent_config(config.default_agent)
|
||||
@@ -114,6 +148,7 @@ def create_agent(config: AppConfig) -> Agent:
|
||||
config=agent_config,
|
||||
)
|
||||
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
|
||||
@@ -95,3 +95,15 @@ class AgentDelegateAction(Action):
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f"I'm asking {self.agent} for help with this task."
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecallAction(Action):
|
||||
# This action is used for retrieving data, e.g., from memory or a knowledge base.
|
||||
query: dict[str, Any] = field(default_factory=dict)
|
||||
thought: str = ''
|
||||
action: str = ActionType.RECALL
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f'Retrieved data for: {self.query}'
|
||||
|
||||
@@ -27,6 +27,13 @@ class AgentCondensationObservation(Observation):
|
||||
return self.content
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecallObservation(Observation):
|
||||
"""The output of a recall action."""
|
||||
|
||||
observation: str = ObservationType.RECALL
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentThinkObservation(Observation):
|
||||
"""The output of a think action.
|
||||
|
||||
@@ -8,6 +8,7 @@ from openhands.events.action.agent import (
|
||||
AgentRejectAction,
|
||||
AgentThinkAction,
|
||||
ChangeAgentStateAction,
|
||||
RecallAction,
|
||||
)
|
||||
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
|
||||
from openhands.events.action.commands import (
|
||||
@@ -37,6 +38,7 @@ actions = (
|
||||
AgentDelegateAction,
|
||||
ChangeAgentStateAction,
|
||||
MessageAction,
|
||||
RecallAction,
|
||||
)
|
||||
|
||||
ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]
|
||||
|
||||
@@ -27,6 +27,7 @@ class EventStreamSubscriber(str, Enum):
|
||||
RESOLVER = 'openhands_resolver'
|
||||
SERVER = 'server'
|
||||
RUNTIME = 'runtime'
|
||||
MEMORY = 'memory'
|
||||
MAIN = 'main'
|
||||
TEST = 'test'
|
||||
|
||||
|
||||
202
openhands/memory/memory.py
Normal file
202
openhands/memory/memory.py
Normal file
@@ -0,0 +1,202 @@
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action.agent import RecallAction
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.events.event import Event, EventSource
|
||||
from openhands.events.observation.agent import (
|
||||
RecallObservation,
|
||||
)
|
||||
from openhands.events.stream import EventStream, EventStreamSubscriber
|
||||
from openhands.microagent import (
|
||||
BaseMicroAgent,
|
||||
KnowledgeMicroAgent,
|
||||
RepoMicroAgent,
|
||||
load_microagents_from_dir,
|
||||
)
|
||||
from openhands.utils.prompt import PromptManager, RepositoryInfo, RuntimeInfo
|
||||
|
||||
|
||||
class Memory:
|
||||
"""
|
||||
Memory is a component that listens to the EventStream for either user MessageAction (to create
|
||||
a RecallAction) or a RecallAction (to produce a RecallObservation).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
event_stream: EventStream,
|
||||
microagents_dir: str,
|
||||
disabled_microagents: list[str] | None = None,
|
||||
):
|
||||
self.event_stream = event_stream
|
||||
self.microagents_dir = microagents_dir
|
||||
self.disabled_microagents = disabled_microagents or []
|
||||
# Subscribe to events
|
||||
self.event_stream.subscribe(
|
||||
EventStreamSubscriber.MEMORY,
|
||||
self.on_event,
|
||||
'Memory',
|
||||
)
|
||||
# Load global microagents (Knowledge + Repo).
|
||||
self._load_global_microagents()
|
||||
|
||||
# Additional placeholders to store user workspace microagents if needed
|
||||
self.repo_microagents: dict[str, RepoMicroAgent] = {}
|
||||
self.knowledge_microagents: dict[str, KnowledgeMicroAgent] = {}
|
||||
|
||||
# Track whether we've seen the first user message
|
||||
self._first_user_message_seen = False
|
||||
|
||||
# Store repository / runtime info to send them to the templating later
|
||||
self.repository_info: RepositoryInfo | None = None
|
||||
self.runtime_info: RuntimeInfo | None = None
|
||||
|
||||
# TODO: enable_prompt_extensions
|
||||
|
||||
def _load_global_microagents(self) -> None:
|
||||
"""
|
||||
Loads microagents from the global microagents_dir.
|
||||
This is effectively what used to happen in PromptManager.
|
||||
"""
|
||||
repo_agents, knowledge_agents, _ = load_microagents_from_dir(
|
||||
self.microagents_dir
|
||||
)
|
||||
for name, agent in knowledge_agents.items():
|
||||
if name in self.disabled_microagents:
|
||||
continue
|
||||
if isinstance(agent, KnowledgeMicroAgent):
|
||||
self.knowledge_microagents[name] = agent
|
||||
for name, agent in repo_agents.items():
|
||||
if name in self.disabled_microagents:
|
||||
continue
|
||||
if isinstance(agent, RepoMicroAgent):
|
||||
self.repo_microagents[name] = agent
|
||||
|
||||
def set_repository_info(self, repo_name: str, repo_directory: str) -> None:
|
||||
"""Store repository info so we can reference it in an observation."""
|
||||
self.repository_info = RepositoryInfo(repo_name, repo_directory)
|
||||
self.prompt_manager.set_repository_info(self.repository_info)
|
||||
|
||||
def set_runtime_info(self, runtime_hosts: dict[str, int]) -> None:
|
||||
"""Store runtime info (web hosts, ports, etc.)."""
|
||||
# e.g. { '127.0.0.1': 8080 }
|
||||
self.runtime_info = RuntimeInfo(available_hosts=runtime_hosts)
|
||||
self.prompt_manager.set_runtime_info(self.runtime_info)
|
||||
|
||||
def on_event(self, event: Event):
|
||||
"""Handle an event from the event stream."""
|
||||
if isinstance(event, MessageAction):
|
||||
if event.source == 'user':
|
||||
# If this is the first user message, create and add a RecallObservation
|
||||
# with info about repo and runtime.
|
||||
if not self._first_user_message_seen:
|
||||
self._first_user_message_seen = True
|
||||
self._on_first_user_message(event)
|
||||
# continue with the next handler, to include microagents if suitable for this user message
|
||||
self._on_user_message_action(event)
|
||||
elif isinstance(event, RecallAction):
|
||||
self._on_recall_action(event)
|
||||
|
||||
def _on_first_user_message(self, event: MessageAction):
|
||||
"""Create and add to the stream a RecallObservation carrying info about repo and runtime."""
|
||||
# Build the same text that used to be appended to the first user message
|
||||
repo_instructions = ''
|
||||
assert (
|
||||
len(self.repo_microagents) <= 1
|
||||
), f'Expecting at most one repo microagent, but found {len(self.repo_microagents)}: {self.repo_microagents.keys()}'
|
||||
for microagent in self.repo_microagents.values():
|
||||
# We assume these are the repo instructions
|
||||
if repo_instructions:
|
||||
repo_instructions += '\n\n'
|
||||
repo_instructions += microagent.content
|
||||
|
||||
# Now wrap it in a RecallObservation, rather than altering the user message:
|
||||
obs = RecallObservation(
|
||||
content=self.prompt_manager.build_additional_info_text(repo_instructions)
|
||||
)
|
||||
self.event_stream.add_event(obs, EventSource.ENVIRONMENT)
|
||||
|
||||
def _on_user_message_action(self, event: MessageAction):
|
||||
"""Replicates old microagent logic: if a microagent triggers on user text,
|
||||
we embed it in an <extra_info> block and post a RecallObservation."""
|
||||
if event.source != 'user':
|
||||
return
|
||||
|
||||
# If there's no text, do nothing
|
||||
user_text = event.content.strip()
|
||||
if not user_text:
|
||||
return
|
||||
# Gather all triggered microagents
|
||||
microagent_blocks = []
|
||||
for name, agent in self.knowledge_microagents.items():
|
||||
trigger = agent.match_trigger(user_text)
|
||||
if trigger:
|
||||
logger.info("Microagent '%s' triggered by keyword '%s'", name, trigger)
|
||||
micro_text = (
|
||||
f'<extra_info>\n'
|
||||
f'The following information has been included based on a keyword match for "{trigger}". '
|
||||
f"It may or may not be relevant to the user's request.\n\n"
|
||||
f'{agent.content}\n'
|
||||
f'</extra_info>'
|
||||
)
|
||||
microagent_blocks.append(micro_text)
|
||||
|
||||
if microagent_blocks:
|
||||
# Combine all triggered microagents into a single RecallObservation
|
||||
combined_text = '\n'.join(microagent_blocks)
|
||||
obs = RecallObservation(content=combined_text)
|
||||
self.event_stream.add_event(
|
||||
obs, event.source if event.source else EventSource.ENVIRONMENT
|
||||
)
|
||||
|
||||
def _on_recall_action(self, event: RecallAction):
|
||||
"""If a RecallAction explicitly arrives, handle it."""
|
||||
assert isinstance(event, RecallAction)
|
||||
|
||||
user_query = event.query.get('keywords', [])
|
||||
matched_content = self.find_microagent_content(user_query)
|
||||
obs = RecallObservation(content=matched_content)
|
||||
self.event_stream.add_event(
|
||||
obs, event.source if event.source else EventSource.ENVIRONMENT
|
||||
)
|
||||
|
||||
def find_microagent_content(self, keywords: list[str]) -> str:
|
||||
"""Replicate the same microagent logic."""
|
||||
matched_texts: list[str] = []
|
||||
for name, agent in self.knowledge_microagents.items():
|
||||
for kw in keywords:
|
||||
trigger = agent.match_trigger(kw)
|
||||
if trigger:
|
||||
logger.info(
|
||||
"Microagent '%s' triggered by explicit RecallAction keyword '%s'",
|
||||
name,
|
||||
trigger,
|
||||
)
|
||||
block = (
|
||||
f'<extra_info>\n'
|
||||
f"(via RecallAction) Included knowledge from microagent '{name}', triggered by '{trigger}'\n\n"
|
||||
f'{agent.content}\n'
|
||||
f'</extra_info>'
|
||||
)
|
||||
matched_texts.append(block)
|
||||
return '\n'.join(matched_texts)
|
||||
|
||||
def load_user_workspace_microagents(
|
||||
self, user_microagents: list[BaseMicroAgent]
|
||||
) -> None:
|
||||
"""
|
||||
If you want to load microagents from a user's cloned repo or workspace directory,
|
||||
call this from agent_session or setup once the workspace is cloned.
|
||||
"""
|
||||
logger.info(
|
||||
'Loading user workspace microagents: %s', [m.name for m in user_microagents]
|
||||
)
|
||||
for ma in user_microagents:
|
||||
if ma.name in self.disabled_microagents:
|
||||
continue
|
||||
if isinstance(ma, KnowledgeMicroAgent):
|
||||
self.knowledge_microagents[ma.name] = ma
|
||||
elif isinstance(ma, RepoMicroAgent):
|
||||
self.repo_microagents[ma.name] = ma
|
||||
|
||||
def set_prompt_manager(self, prompt_manager: PromptManager):
|
||||
self.prompt_manager = prompt_manager
|
||||
@@ -14,7 +14,8 @@ from openhands.core.schema.agent import AgentState
|
||||
from openhands.events.action import ChangeAgentStateAction, MessageAction
|
||||
from openhands.events.event import EventSource
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.microagent import BaseMicroAgent
|
||||
from openhands.memory.memory import Memory
|
||||
from openhands.microagent.microagent import BaseMicroAgent
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
|
||||
@@ -125,6 +126,13 @@ class AgentSession:
|
||||
agent_to_llm_config=agent_to_llm_config,
|
||||
agent_configs=agent_configs,
|
||||
)
|
||||
|
||||
self.memory = await self._create_memory(
|
||||
microagents_dir=config.microagents_dir,
|
||||
agent=agent,
|
||||
selected_repository=selected_repository,
|
||||
)
|
||||
|
||||
if github_token:
|
||||
self.event_stream.set_secrets(
|
||||
{
|
||||
@@ -252,26 +260,14 @@ class AgentSession:
|
||||
)
|
||||
return False
|
||||
|
||||
repo_directory = None
|
||||
if selected_repository:
|
||||
repo_directory = await call_sync_from_async(
|
||||
await call_sync_from_async(
|
||||
self.runtime.clone_repo,
|
||||
github_token,
|
||||
selected_repository,
|
||||
selected_branch,
|
||||
)
|
||||
|
||||
if agent.prompt_manager:
|
||||
agent.prompt_manager.set_runtime_info(self.runtime)
|
||||
microagents: list[BaseMicroAgent] = await call_sync_from_async(
|
||||
self.runtime.get_microagents_from_selected_repo, selected_repository
|
||||
)
|
||||
agent.prompt_manager.load_microagents(microagents)
|
||||
if selected_repository and repo_directory:
|
||||
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]}'
|
||||
)
|
||||
@@ -334,6 +330,34 @@ class AgentSession:
|
||||
|
||||
return controller
|
||||
|
||||
async def _create_memory(
|
||||
self, microagents_dir: str, agent: Agent, selected_repository: str | None
|
||||
) -> Memory:
|
||||
# If the agent config has disabled microagents, use them
|
||||
disabled = agent.config.disabled_microagents
|
||||
|
||||
mem = Memory(
|
||||
event_stream=self.event_stream,
|
||||
microagents_dir=microagents_dir,
|
||||
disabled_microagents=disabled,
|
||||
)
|
||||
|
||||
if agent.prompt_manager and self.runtime:
|
||||
# sets available hosts
|
||||
mem.set_runtime_info(self.runtime.web_hosts)
|
||||
|
||||
# loads microagents from repo/.openhands/microagents
|
||||
microagents: list[BaseMicroAgent] = await call_sync_from_async(
|
||||
self.runtime.get_microagents_from_selected_repo, selected_repository
|
||||
)
|
||||
mem.load_user_workspace_microagents(microagents)
|
||||
|
||||
if selected_repository:
|
||||
repo_directory = selected_repository.split('/')[1]
|
||||
if repo_directory:
|
||||
mem.set_repository_info(selected_repository, repo_directory)
|
||||
return mem
|
||||
|
||||
def _maybe_restore_state(self) -> State | None:
|
||||
"""Helper method to handle state restore logic."""
|
||||
restored_state = None
|
||||
|
||||
@@ -58,7 +58,7 @@ def check_llama_index():
|
||||
if not LLAMA_INDEX_AVAILABLE:
|
||||
raise ImportError(
|
||||
'llama_index and its dependencies are not installed. '
|
||||
'To use memory features, please run: poetry install --with llama-index.'
|
||||
'To use long-term memory features, please run: poetry install --with llama-index.'
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,15 +5,8 @@ from itertools import islice
|
||||
from jinja2 import Template
|
||||
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.logger import openhands_logger
|
||||
from openhands.core.message import Message, TextContent
|
||||
from openhands.microagent import (
|
||||
BaseMicroAgent,
|
||||
KnowledgeMicroAgent,
|
||||
RepoMicroAgent,
|
||||
load_microagents_from_dir,
|
||||
)
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.microagent.microagent import RepoMicroAgent
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -31,73 +24,26 @@ class RepositoryInfo:
|
||||
|
||||
class PromptManager:
|
||||
"""
|
||||
Manages prompt templates and micro-agents for AI interactions.
|
||||
Manages prompt templates and includes information from the user's workspace micro-agents and global micro-agents.
|
||||
|
||||
This class handles loading and rendering of system and user prompt templates,
|
||||
as well as loading micro-agent specifications. It provides methods to access
|
||||
rendered system and initial user messages for AI interactions.
|
||||
This class is dedicated toloading and rendering prompts (system prompt, user prompt).
|
||||
|
||||
Attributes:
|
||||
prompt_dir (str): Directory containing prompt templates.
|
||||
microagent_dir (str): Directory containing microagent specifications.
|
||||
disabled_microagents (list[str] | None): List of microagents to disable. If None, all microagents are enabled.
|
||||
prompt_dir: Directory containing prompt templates.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prompt_dir: str,
|
||||
microagent_dir: str | None = None,
|
||||
disabled_microagents: list[str] | None = None,
|
||||
):
|
||||
self.disabled_microagents: list[str] = disabled_microagents or []
|
||||
self.prompt_dir: str = prompt_dir
|
||||
self.repository_info: RepositoryInfo | None = None
|
||||
self.system_template: Template = self._load_template('system_prompt')
|
||||
self.user_template: Template = self._load_template('user_prompt')
|
||||
self.additional_info_template: Template = self._load_template('additional_info')
|
||||
self.runtime_info = RuntimeInfo(available_hosts={})
|
||||
|
||||
self.knowledge_microagents: dict[str, KnowledgeMicroAgent] = {}
|
||||
self.repo_microagents: dict[str, RepoMicroAgent] = {}
|
||||
|
||||
if microagent_dir:
|
||||
# This loads micro-agents from the microagent_dir
|
||||
# which is typically the OpenHands/microagents (i.e., the PUBLIC microagents)
|
||||
|
||||
# Only load KnowledgeMicroAgents
|
||||
repo_microagents, knowledge_microagents, _ = load_microagents_from_dir(
|
||||
microagent_dir
|
||||
)
|
||||
assert all(
|
||||
isinstance(microagent, KnowledgeMicroAgent)
|
||||
for microagent in knowledge_microagents.values()
|
||||
)
|
||||
for name, microagent in knowledge_microagents.items():
|
||||
if name not in self.disabled_microagents:
|
||||
self.knowledge_microagents[name] = microagent
|
||||
assert all(
|
||||
isinstance(microagent, RepoMicroAgent)
|
||||
for microagent in repo_microagents.values()
|
||||
)
|
||||
for name, microagent in repo_microagents.items():
|
||||
if name not in self.disabled_microagents:
|
||||
self.repo_microagents[name] = microagent
|
||||
|
||||
def load_microagents(self, microagents: list[BaseMicroAgent]) -> None:
|
||||
"""Load microagents from a list of BaseMicroAgents.
|
||||
|
||||
This is typically used when loading microagents from inside a repo.
|
||||
"""
|
||||
openhands_logger.info('Loading microagents: %s', [m.name for m in microagents])
|
||||
# Only keep KnowledgeMicroAgents and RepoMicroAgents
|
||||
for microagent in microagents:
|
||||
if microagent.name in self.disabled_microagents:
|
||||
continue
|
||||
if isinstance(microagent, KnowledgeMicroAgent):
|
||||
self.knowledge_microagents[microagent.name] = microagent
|
||||
elif isinstance(microagent, RepoMicroAgent):
|
||||
self.repo_microagents[microagent.name] = microagent
|
||||
|
||||
def _load_template(self, template_name: str) -> Template:
|
||||
if self.prompt_dir is None:
|
||||
raise ValueError('Prompt directory is not set')
|
||||
@@ -110,23 +56,17 @@ class PromptManager:
|
||||
def get_system_message(self) -> str:
|
||||
return self.system_template.render().strip()
|
||||
|
||||
def set_runtime_info(self, runtime: Runtime) -> None:
|
||||
self.runtime_info.available_hosts = runtime.web_hosts
|
||||
def set_runtime_info(self, runtime_info: RuntimeInfo) -> None:
|
||||
self.runtime_info = runtime_info
|
||||
|
||||
def set_repository_info(
|
||||
self,
|
||||
repo_name: str,
|
||||
repo_directory: str,
|
||||
) -> None:
|
||||
"""Sets information about the GitHub repository that has been cloned.
|
||||
def set_repository_info(self, repository_info: RepositoryInfo) -> None:
|
||||
"""Stores info about a cloned repository for rendering the template.
|
||||
|
||||
Args:
|
||||
repo_name: The name of the GitHub repository (e.g. 'owner/repo')
|
||||
repo_directory: The directory where the repository has been cloned
|
||||
repo_name: The name of the repository.
|
||||
repo_directory: The directory of the repository.
|
||||
"""
|
||||
self.repository_info = RepositoryInfo(
|
||||
repo_name=repo_name, repo_directory=repo_directory
|
||||
)
|
||||
self.repository_info = repository_info
|
||||
|
||||
def get_example_user_message(self) -> str:
|
||||
"""This is the initial user message provided to the agent
|
||||
@@ -141,40 +81,6 @@ class PromptManager:
|
||||
|
||||
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
|
||||
|
||||
# if there were other texts included, they were before the user message
|
||||
# so the last TextContent is the user message
|
||||
# content can be a list of TextContent or ImageContent
|
||||
message_content = ''
|
||||
for content in reversed(message.content):
|
||||
if isinstance(content, TextContent):
|
||||
message_content = content.text
|
||||
break
|
||||
|
||||
if not message_content:
|
||||
return
|
||||
|
||||
for microagent in self.knowledge_microagents.values():
|
||||
trigger = microagent.match_trigger(message_content)
|
||||
if trigger:
|
||||
openhands_logger.info(
|
||||
"Microagent '%s' triggered by keyword '%s'",
|
||||
microagent.name,
|
||||
trigger,
|
||||
)
|
||||
micro_text = f'<extra_info>\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</extra_info>'
|
||||
message.content.append(TextContent(text=micro_text))
|
||||
|
||||
def add_examples_to_initial_message(self, message: Message) -> None:
|
||||
"""Add example_message to the first user message."""
|
||||
@@ -188,31 +94,29 @@ class PromptManager:
|
||||
self,
|
||||
message: Message,
|
||||
) -> None:
|
||||
"""Adds information about the repository and runtime to the initial user message.
|
||||
|
||||
Args:
|
||||
message: The initial user message to add information to.
|
||||
"""
|
||||
repo_instructions = ''
|
||||
assert (
|
||||
len(self.repo_microagents) <= 1
|
||||
), f'Expecting at most one repo microagent, but found {len(self.repo_microagents)}: {self.repo_microagents.keys()}'
|
||||
for microagent in self.repo_microagents.values():
|
||||
# We assume these are the repo instructions
|
||||
if repo_instructions:
|
||||
repo_instructions += '\n\n'
|
||||
repo_instructions += microagent.content
|
||||
Previously inserted the rendered template at the start of the user's first message.
|
||||
If we've switched to using a separate RecallObservation in Memory, we can safely remove
|
||||
or comment out the direct insertion code below—but we still keep the method for
|
||||
scenarios where we want to read or manipulate the template output.
|
||||
"""
|
||||
# Old code that forcibly modified the user message:
|
||||
#
|
||||
# info_block = self.build_additional_info_text(repo_instructions)
|
||||
# if info_block:
|
||||
# message.content.insert(0, TextContent(text=info_block))
|
||||
#
|
||||
# Now we comment it out or remove to avoid "injecting" directly.
|
||||
pass
|
||||
|
||||
additional_info = self.additional_info_template.render(
|
||||
repository_instructions=repo_instructions,
|
||||
def build_additional_info_text(self, repo_instructions: str = '') -> str:
|
||||
"""Renders the additional_info_template with the stored repository/runtime info."""
|
||||
return self.additional_info_template.render(
|
||||
repository_info=self.repository_info,
|
||||
repository_instructions=repo_instructions,
|
||||
runtime_info=self.runtime_info,
|
||||
).strip()
|
||||
|
||||
# Insert the new content at the start of the TextContent list
|
||||
if additional_info:
|
||||
message.content.insert(0, TextContent(text=additional_info))
|
||||
|
||||
def add_turns_left_reminder(self, messages: list[Message], state: State) -> None:
|
||||
latest_user_message = next(
|
||||
islice(
|
||||
|
||||
@@ -9,6 +9,7 @@ from openhands.core.config import AppConfig, LLMConfig
|
||||
from openhands.events import EventStream, EventStreamSubscriber
|
||||
from openhands.llm import LLM
|
||||
from openhands.llm.metrics import Metrics
|
||||
from openhands.memory.memory import Memory
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.server.monitoring import MonitoringListener
|
||||
from openhands.server.session.agent_session import AgentSession
|
||||
@@ -23,18 +24,24 @@ def mock_agent():
|
||||
llm = MagicMock(spec=LLM)
|
||||
metrics = MagicMock(spec=Metrics)
|
||||
llm_config = MagicMock(spec=LLMConfig)
|
||||
agent_config = MagicMock()
|
||||
|
||||
# Configure the LLM config
|
||||
llm_config.model = 'test-model'
|
||||
llm_config.base_url = 'http://test'
|
||||
llm_config.max_message_chars = 1000
|
||||
|
||||
# Configure the agent config
|
||||
agent_config.disabled_microagents = []
|
||||
|
||||
# Set up the chain of mocks
|
||||
llm.metrics = metrics
|
||||
llm.config = llm_config
|
||||
agent.llm = llm
|
||||
agent.name = 'test-agent'
|
||||
agent.sandbox_plugins = []
|
||||
agent.config = agent_config
|
||||
agent.prompt_manager = None
|
||||
|
||||
return agent
|
||||
|
||||
@@ -80,7 +87,10 @@ async def test_agent_session_start_with_no_state(mock_agent):
|
||||
self.test_initial_state = state
|
||||
super().set_initial_state(*args, state=state, **kwargs)
|
||||
|
||||
# Patch AgentController and State.restore_from_session to fail
|
||||
# Create a mock Memory
|
||||
mock_memory = MagicMock(spec=Memory)
|
||||
|
||||
# Patch AgentController, EventStream, State.restore_from_session, and Memory
|
||||
with patch(
|
||||
'openhands.server.session.agent_session.AgentController', SpyAgentController
|
||||
), patch(
|
||||
@@ -89,6 +99,9 @@ async def test_agent_session_start_with_no_state(mock_agent):
|
||||
), patch(
|
||||
'openhands.controller.state.state.State.restore_from_session',
|
||||
side_effect=Exception('No state found'),
|
||||
), patch(
|
||||
'openhands.server.session.agent_session.Memory',
|
||||
return_value=mock_memory,
|
||||
):
|
||||
await session.start(
|
||||
runtime_name='test-runtime',
|
||||
@@ -162,7 +175,10 @@ async def test_agent_session_start_with_restored_state(mock_agent):
|
||||
self.test_initial_state = state
|
||||
super().set_initial_state(*args, state=state, **kwargs)
|
||||
|
||||
# Patch AgentController and State.restore_from_session to succeed
|
||||
# Create a mock Memory
|
||||
mock_memory = MagicMock(spec=Memory)
|
||||
|
||||
# Patch AgentController, EventStream, State.restore_from_session, and Memory
|
||||
with patch(
|
||||
'openhands.server.session.agent_session.AgentController', SpyAgentController
|
||||
), patch(
|
||||
@@ -171,6 +187,9 @@ async def test_agent_session_start_with_restored_state(mock_agent):
|
||||
), patch(
|
||||
'openhands.controller.state.state.State.restore_from_session',
|
||||
return_value=mock_restored_state,
|
||||
), patch(
|
||||
'openhands.server.session.agent_session.Memory',
|
||||
return_value=mock_memory,
|
||||
):
|
||||
await session.start(
|
||||
runtime_name='test-runtime',
|
||||
|
||||
Reference in New Issue
Block a user