Compare commits

...

25 Commits

Author SHA1 Message Date
openhands
bd7c1db423 Fix agent session tests to mock Memory component 2025-03-05 03:08:50 +00:00
openhands
145a4bdc81 Resolve merge conflicts with main branch 2025-02-28 07:46:18 +00:00
Engel Nyst
c25701fc1b add memory.py 2025-02-24 17:32:52 +01:00
Engel Nyst
bec05943a0 Merge branch 'main' of github.com:All-Hands-AI/OpenHands into enyst/retrieve-prompt 2025-02-24 17:32:00 +01:00
Engel Nyst
2c5018f529 Merge branch 'main' of github.com:All-Hands-AI/OpenHands into enyst/retrieve-prompt 2025-02-24 00:21:21 +01:00
Engel Nyst
d596fd2782 refactor info in the first user message to a recalled observation 2025-02-24 00:13:28 +01:00
Engel Nyst
21c2253634 fix disabled microagents 2025-02-23 21:20:23 +01:00
Engel Nyst
b95d54020c refactor prompt manager to manage the view, memory manages info retrieval 2025-02-23 21:11:00 +01:00
Engel Nyst
0e54bab56a rename to memory 2025-02-23 20:03:47 +01:00
Engel Nyst
bb5817cb56 rename main module to memory 2025-02-23 20:01:48 +01:00
Engel Nyst
f109a2ad95 rename memory to long term memory 2025-02-23 19:52:32 +01:00
Engel Nyst
956b3b4ab7 create memory 2025-02-23 00:23:41 +01:00
Engel Nyst
66781fc0f6 fix subscriber 2025-02-23 00:08:55 +01:00
Engel Nyst
143293db44 fix logic 2025-02-23 00:06:59 +01:00
Engel Nyst
c26185daf0 dont want to fight o1 right now, will revisit 2025-02-22 23:57:02 +01:00
Engel Nyst
16da353508 refactor prompt extensions 2025-02-22 23:39:32 +01:00
Engel Nyst
c21ddaf1f1 add recall action and observation 2025-02-22 22:00:52 +01:00
Engel Nyst
801b134c7f fix tests 2025-02-22 20:36:59 +01:00
Engel Nyst
5b063cc11b add tests 2025-02-22 20:18:34 +01:00
Engel Nyst
b1a18d5330 retrieve tokens usage for an event 2025-02-22 20:14:23 +01:00
Engel Nyst
38b5198c24 fix not initialized 2025-02-22 19:52:58 +01:00
Engel Nyst
dba25f5c46 clean up 2025-02-22 19:20:14 +01:00
Engel Nyst
c59abb5305 test accumulation 2025-02-22 19:04:12 +01:00
Engel Nyst
bd9fc5551b add response_id 2025-02-22 18:59:48 +01:00
Engel Nyst
d80c3767ae track used tokens 2025-02-22 18:48:35 +01:00
15 changed files with 360 additions and 152 deletions

View File

@@ -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

View File

@@ -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] = {}

View File

@@ -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')

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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}'

View File

@@ -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.

View File

@@ -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]

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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.'
)

View File

@@ -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(

View File

@@ -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',