Compare commits

...

5 Commits

Author SHA1 Message Date
openhands
7561518e4c Fix lint errors 2025-05-19 16:55:19 +00:00
openhands
2cd503f033 Fix merge conflicts with main branch 2025-05-19 16:53:24 +00:00
openhands
2d434ad49f Rename RuntimeInfo to ContextualInfo 2025-05-17 22:31:31 +00:00
openhands
81c0253d53 Rename memory.set_runtime_info to memory.set_contextual_info 2025-05-17 22:27:48 +00:00
openhands
9cdde313d8 Add context_message parameter to conversation creation endpoint 2025-05-17 22:22:15 +00:00
15 changed files with 92 additions and 51 deletions

View File

@@ -37,3 +37,8 @@ Today's date is {{ runtime_info.date }} (UTC).
{% endif %}
</RUNTIME_INFORMATION>
{% endif %}
{% if runtime_info and runtime_info.context_message -%}
<CONTEXT_MESSAGE>
{{ runtime_info.context_message }}
</CONTEXT_MESSAGE>
{% endif %}

View File

@@ -154,7 +154,7 @@ def create_memory(
if runtime:
# sets available hosts
memory.set_runtime_info(runtime, {})
memory.set_contextual_info(runtime, {})
# loads microagents from repo/.openhands/microagents
microagents: list[BaseMicroagent] = runtime.get_microagents_from_selected_repo(

View File

@@ -75,6 +75,7 @@ class RecallObservation(Observation):
additional_agent_instructions: str = ''
date: str = ''
custom_secrets_descriptions: dict[str, str] = field(default_factory=dict)
context_message: str | None = None
# knowledge
microagent_knowledge: list[MicroagentKnowledge] = field(default_factory=list)

View File

@@ -41,7 +41,7 @@ from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.mcp import MCPObservation
from openhands.events.observation.observation import Observation
from openhands.events.serialization.event import truncate_content
from openhands.utils.prompt import PromptManager, RepositoryInfo, RuntimeInfo
from openhands.utils.prompt import PromptManager, RepositoryInfo, ContextualInfo
class ConversationMemory:
@@ -455,14 +455,14 @@ class ConversationMemory:
date = obs.date
if obs.runtime_hosts or obs.additional_agent_instructions:
runtime_info = RuntimeInfo(
runtime_info = ContextualInfo(
available_hosts=obs.runtime_hosts,
additional_agent_instructions=obs.additional_agent_instructions,
date=date,
custom_secrets_descriptions=obs.custom_secrets_descriptions,
)
else:
runtime_info = RuntimeInfo(
runtime_info = ContextualInfo(
date=date,
custom_secrets_descriptions=obs.custom_secrets_descriptions,
)

View File

@@ -22,7 +22,7 @@ from openhands.microagent import (
load_microagents_from_dir,
)
from openhands.runtime.base import Runtime
from openhands.utils.prompt import RepositoryInfo, RuntimeInfo
from openhands.utils.prompt import RepositoryInfo, ContextualInfo
GLOBAL_MICROAGENTS_DIR = os.path.join(
os.path.dirname(os.path.dirname(openhands.__file__)),
@@ -31,8 +31,8 @@ GLOBAL_MICROAGENTS_DIR = os.path.join(
class Memory:
"""
Memory is a component that listens to the EventStream for information retrieval actions
"""Memory is a component that listens to the EventStream for information retrieval actions.
(a RecallAction) and publishes observations with the content (such as RecallObservation).
"""
@@ -64,7 +64,7 @@ class Memory:
# Store repository / runtime info to send them to the templating later
self.repository_info: RepositoryInfo | None = None
self.runtime_info: RuntimeInfo | None = None
self.runtime_info: ContextualInfo | None = None
# Load global microagents (Knowledge + Repo)
# from typically OpenHands/microagents (i.e., the PUBLIC microagents)
@@ -131,7 +131,6 @@ class Memory:
This method collects information from all available repo microagents and concatenates their contents.
Multiple repo microagents are supported, and their contents will be concatenated with newlines between them.
"""
# Create WORKSPACE_CONTEXT info:
# - repository_info
# - runtime_info
@@ -180,6 +179,9 @@ class Memory:
custom_secrets_descriptions=self.runtime_info.custom_secrets_descriptions
if self.runtime_info is not None
else {},
context_message=self.runtime_info.context_message
if self.runtime_info and self.runtime_info.context_message is not None
else None,
)
return obs
return None
@@ -189,7 +191,6 @@ class Memory:
event: RecallAction,
) -> RecallObservation | None:
"""When a microagent action triggers microagents, create a RecallObservation with structured data."""
# Find any matched microagents based on the query
microagent_knowledge = self._find_microagent_knowledge(event.query)
@@ -235,8 +236,7 @@ class Memory:
def load_user_workspace_microagents(
self, user_microagents: list[BaseMicroagent]
) -> None:
"""
This method loads microagents from a user's cloned repo or workspace directory.
"""This method loads microagents from a user's cloned repo or workspace directory.
This is typically called from agent_session or setup once the workspace is cloned.
"""
@@ -250,9 +250,7 @@ class Memory:
self.repo_microagents[user_microagent.name] = user_microagent
def _load_global_microagents(self) -> None:
"""
Loads microagents from the global microagents_dir
"""
"""Loads microagents from the global microagents_dir."""
repo_agents, knowledge_agents = load_microagents_from_dir(
GLOBAL_MICROAGENTS_DIR
)
@@ -264,8 +262,7 @@ class Memory:
self.repo_microagents[name] = agent
def get_microagent_mcp_tools(self) -> list[MCPConfig]:
"""
Get MCP tools from all repo microagents (always active)
"""Get MCP tools from all repo microagents (always active).
Returns:
A list of MCP tools configurations from microagents
@@ -289,8 +286,11 @@ class Memory:
else:
self.repository_info = None
def set_runtime_info(
self, runtime: Runtime, custom_secrets_descriptions: dict[str, str]
def set_contextual_info(
self,
runtime: Runtime,
custom_secrets_descriptions: dict[str, str],
context_message: str | None = None,
) -> None:
"""Store runtime info (web hosts, ports, etc.)."""
# e.g. { '127.0.0.1': 8080 }
@@ -298,15 +298,18 @@ class Memory:
date = str(utc_now.date())
if runtime.web_hosts or runtime.additional_agent_instructions:
self.runtime_info = RuntimeInfo(
self.runtime_info = ContextualInfo(
available_hosts=runtime.web_hosts,
additional_agent_instructions=runtime.additional_agent_instructions,
date=date,
custom_secrets_descriptions=custom_secrets_descriptions,
context_message=context_message,
)
else:
self.runtime_info = RuntimeInfo(
date=date, custom_secrets_descriptions=custom_secrets_descriptions
self.runtime_info = ContextualInfo(
date=date,
custom_secrets_descriptions=custom_secrets_descriptions,
context_message=context_message,
)
def send_error_message(self, message_id: str, message: str):

View File

@@ -6,10 +6,8 @@ import socketio
from openhands.core.config import AppConfig
from openhands.events.action import MessageAction
from openhands.events.event_store import EventStore
from openhands.server.config.server_config import ServerConfig
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.data_models.conversation_info import ConversationInfo
from openhands.server.monitoring import MonitoringListener
from openhands.server.session.conversation import Conversation
from openhands.storage.conversation.conversation_store import ConversationStore
@@ -83,6 +81,7 @@ class ConversationManager(ABC):
user_id: str | None,
initial_user_msg: MessageAction | None = None,
replay_json: str | None = None,
context_message: str | None = None,
) -> AgentLoopInfo:
"""Start an event loop if one is not already running"""

View File

@@ -245,12 +245,13 @@ class StandaloneConversationManager(ConversationManager):
user_id: str | None,
initial_user_msg: MessageAction | None = None,
replay_json: str | None = None,
context_message: str | None = None,
) -> AgentLoopInfo:
logger.info(f'maybe_start_agent_loop:{sid}', extra={'session_id': sid})
session = self._local_agent_loops_by_sid.get(sid)
if not session:
session = await self._start_agent_loop(
sid, settings, user_id, initial_user_msg, replay_json
sid, settings, user_id, initial_user_msg, replay_json, context_message
)
return self._agent_loop_info_from_session(session)
@@ -261,6 +262,7 @@ class StandaloneConversationManager(ConversationManager):
user_id: str | None,
initial_user_msg: MessageAction | None = None,
replay_json: str | None = None,
context_message: str | None = None,
) -> Session:
logger.info(f'starting_agent_loop:{sid}', extra={'session_id': sid})
@@ -304,7 +306,9 @@ class StandaloneConversationManager(ConversationManager):
)
self._local_agent_loops_by_sid[sid] = session
asyncio.create_task(
session.initialize_agent(settings, initial_user_msg, replay_json)
session.initialize_agent(
settings, initial_user_msg, replay_json, context_message
)
)
# This does not get added when resuming an existing conversation
try:
@@ -475,7 +479,7 @@ class StandaloneConversationManager(ConversationManager):
continue
results.append(self._agent_loop_info_from_session(session))
return results
def _agent_loop_info_from_session(self, session: Session):
return AgentLoopInfo(
conversation_id=session.sid,
@@ -485,7 +489,7 @@ class StandaloneConversationManager(ConversationManager):
)
def _get_conversation_url(self, conversation_id: str):
return f"/conversations/{conversation_id}"
return f'/conversations/{conversation_id}'
def _last_updated_at_key(conversation: ConversationMetadata) -> float:

View File

@@ -61,6 +61,7 @@ class InitSessionRequest(BaseModel):
image_urls: list[str] | None = None
replay_json: str | None = None
suggested_task: SuggestedTask | None = None
context_message: str | None = None
model_config = {'extra': 'forbid'}
@@ -84,6 +85,7 @@ async def _create_new_conversation(
replay_json: str | None,
conversation_trigger: ConversationTrigger = ConversationTrigger.GUI,
attach_convo_id: bool = False,
context_message: str | None = None,
) -> AgentLoopInfo:
logger.info(
'Creating conversation',
@@ -120,6 +122,7 @@ async def _create_new_conversation(
session_init_args['selected_repository'] = selected_repository
session_init_args['custom_secrets'] = custom_secrets
session_init_args['selected_branch'] = selected_branch
session_init_args['context_message'] = context_message
conversation_init_data = ConversationInitData(**session_init_args)
logger.info('Loading conversation store')
conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
@@ -169,6 +172,7 @@ async def _create_new_conversation(
user_id,
initial_user_msg=initial_message_action,
replay_json=replay_json,
context_message=context_message,
)
logger.info(f'Finished initializing conversation {agent_loop_info.conversation_id}')
return agent_loop_info
@@ -195,6 +199,7 @@ async def new_conversation(
replay_json = data.replay_json
suggested_task = data.suggested_task
git_provider = data.git_provider
context_message = data.context_message
conversation_trigger = ConversationTrigger.GUI
@@ -222,6 +227,7 @@ async def new_conversation(
image_urls=image_urls,
replay_json=replay_json,
conversation_trigger=conversation_trigger,
context_message=context_message,
)
return InitSessionResponse(
@@ -287,8 +293,12 @@ async def search_conversations(
running_conversations = await conversation_manager.get_running_agent_loops(
user_id, conversation_ids
)
connection_ids_to_conversation_ids = await conversation_manager.get_connections(filter_to_sids=conversation_ids)
agent_loop_info = await conversation_manager.get_agent_loop_info(filter_to_sids=conversation_ids)
connection_ids_to_conversation_ids = await conversation_manager.get_connections(
filter_to_sids=conversation_ids
)
agent_loop_info = await conversation_manager.get_agent_loop_info(
filter_to_sids=conversation_ids
)
agent_loop_info_by_conversation_id = {info.conversation_id: info for info in agent_loop_info}
result = ConversationInfoResultSet(
results=await wait_all(
@@ -296,7 +306,8 @@ async def search_conversations(
conversation=conversation,
is_running=conversation.conversation_id in running_conversations,
num_connections=sum(
1 for conversation_id in connection_ids_to_conversation_ids.values()
1
for conversation_id in connection_ids_to_conversation_ids.values()
if conversation_id == conversation.conversation_id
),
agent_loop_info=agent_loop_info_by_conversation_id.get(conversation.conversation_id),

View File

@@ -16,7 +16,11 @@ from openhands.core.schema.agent import AgentState
from openhands.events.action import ChangeAgentStateAction, MessageAction
from openhands.events.event import Event, EventSource
from openhands.events.stream import EventStream
from openhands.integrations.provider import CUSTOM_SECRETS_TYPE, PROVIDER_TOKEN_TYPE, ProviderHandler
from openhands.integrations.provider import (
CUSTOM_SECRETS_TYPE,
PROVIDER_TOKEN_TYPE,
ProviderHandler,
)
from openhands.mcp import add_mcp_tools_to_agent
from openhands.memory.memory import Memory
from openhands.microagent.microagent import BaseMicroagent
@@ -91,6 +95,7 @@ class AgentSession:
selected_branch: str | None = None,
initial_message: MessageAction | None = None,
replay_json: str | None = None,
context_message: str | None = None,
) -> None:
"""Starts the Agent session
Parameters:
@@ -116,7 +121,9 @@ class AgentSession:
finished = False # For monitoring
runtime_connected = False
custom_secrets_handler = UserSecrets(custom_secrets=custom_secrets if custom_secrets else {})
custom_secrets_handler = UserSecrets(
custom_secrets=custom_secrets if custom_secrets else {}
)
try:
self._create_security_analyzer(config.security.security_analyzer)
@@ -144,7 +151,8 @@ class AgentSession:
self.memory = await self._create_memory(
selected_repository=selected_repository,
repo_directory=repo_directory,
custom_secrets_descriptions=custom_secrets_handler.get_custom_secrets_descriptions()
custom_secrets_descriptions=custom_secrets_handler.get_custom_secrets_descriptions(),
context_message=context_message,
)
# NOTE: this needs to happen before controller is created
@@ -315,7 +323,7 @@ class AgentSession:
provider_tokens=git_provider_tokens
or cast(PROVIDER_TOKEN_TYPE, MappingProxyType({}))
)
# Merge git provider tokens with custom secrets before passing over to runtime
env_vars.update(await provider_handler.get_env_vars(expose_secrets=True))
self.runtime = runtime_cls(
@@ -415,7 +423,11 @@ class AgentSession:
return controller
async def _create_memory(
self, selected_repository: str | None, repo_directory: str | None, custom_secrets_descriptions: dict[str, str]
self,
selected_repository: str | None,
repo_directory: str | None,
custom_secrets_descriptions: dict[str, str],
context_message: str | None = None,
) -> Memory:
memory = Memory(
event_stream=self.event_stream,
@@ -425,7 +437,9 @@ class AgentSession:
if self.runtime:
# sets available hosts and other runtime info
memory.set_runtime_info(self.runtime, custom_secrets_descriptions)
memory.set_contextual_info(
self.runtime, custom_secrets_descriptions, context_message
)
# loads microagents from repo/.openhands/microagents
microagents: list[BaseMicroagent] = await call_sync_from_async(

View File

@@ -14,6 +14,7 @@ class ConversationInitData(Settings):
selected_repository: str | None = Field(default=None)
replay_json: str | None = Field(default=None)
selected_branch: str | None = Field(default=None)
context_message: str | None = Field(default=None)
model_config = {
'arbitrary_types_allowed': True,

View File

@@ -91,6 +91,7 @@ class Session:
settings: Settings,
initial_message: MessageAction | None,
replay_json: str | None,
context_message: str | None = None,
) -> None:
self.agent_session.event_stream.add_event(
AgentStateChangedObservation('', AgentState.LOADING),
@@ -160,6 +161,7 @@ class Session:
selected_repository = settings.selected_repository
selected_branch = settings.selected_branch
custom_secrets = settings.custom_secrets
context_message = settings.context_message
try:
await self.agent_session.start(
@@ -176,6 +178,7 @@ class Session:
selected_branch=selected_branch,
initial_message=initial_message,
replay_json=replay_json,
context_message=context_message,
)
except MicroagentValidationError as e:
self.logger.exception(f'Error creating agent_session: {e}')

View File

@@ -10,11 +10,12 @@ from openhands.events.observation.agent import MicroagentKnowledge
@dataclass
class RuntimeInfo:
class ContextualInfo:
date: str
available_hosts: dict[str, int] = field(default_factory=dict)
additional_agent_instructions: str = ''
custom_secrets_descriptions: dict[str, str] = field(default_factory=dict)
context_message: str | None = None
@dataclass
@@ -26,8 +27,7 @@ class RepositoryInfo:
class PromptManager:
"""
Manages prompt templates and includes information from the user's workspace micro-agents and global micro-agents.
"""Manages prompt templates and includes information from the user's workspace micro-agents and global micro-agents.
This class is dedicated to loading and rendering prompts (system prompt, user prompt).
@@ -58,8 +58,9 @@ class PromptManager:
return self.system_template.render().strip()
def get_example_user_message(self) -> str:
"""This is an initial user message that can be provided to the agent
before *actual* user instructions are provided.
"""This is an initial user message that can be provided to the agent.
Before *actual* user instructions are provided.
It can be used to provide a demonstration of how the agent
should behave in order to solve the user's task. And it may
@@ -67,13 +68,12 @@ class PromptManager:
These additional context will convert the current generic agent
into a more specialized agent that is tailored to the user's task.
"""
return self.user_template.render().strip()
def build_workspace_context(
self,
repository_info: RepositoryInfo | None,
runtime_info: RuntimeInfo | None,
runtime_info: ContextualInfo | None,
repo_instructions: str = '',
) -> str:
"""Renders the additional info template with the stored repository/runtime info."""

View File

@@ -37,7 +37,7 @@ from openhands.events.observation.files import FileEditObservation, FileReadObse
from openhands.events.observation.reject import UserRejectObservation
from openhands.events.tool import ToolCallMetadata
from openhands.memory.conversation_memory import ConversationMemory
from openhands.utils.prompt import PromptManager, RepositoryInfo, RuntimeInfo
from openhands.utils.prompt import PromptManager, RepositoryInfo, ContextualInfo
@pytest.fixture
@@ -706,7 +706,7 @@ def test_process_events_with_environment_microagent_observation(conversation_mem
assert isinstance(call_args['repository_info'], RepositoryInfo)
assert call_args['repository_info'].repo_name == 'test-repo'
assert call_args['repository_info'].repo_directory == '/path/to/repo'
assert isinstance(call_args['runtime_info'], RuntimeInfo)
assert isinstance(call_args['runtime_info'], ContextualInfo)
assert call_args['runtime_info'].available_hosts == {'localhost': 8080}
assert (
call_args['repo_instructions']

View File

@@ -26,7 +26,7 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
ActionExecutionClient,
)
from openhands.storage.memory import InMemoryFileStore
from openhands.utils.prompt import PromptManager, RepositoryInfo, RuntimeInfo
from openhands.utils.prompt import PromptManager, RepositoryInfo, ContextualInfo
@pytest.fixture
@@ -377,7 +377,7 @@ async def test_custom_secrets_descriptions():
}
# Set runtime info with custom secrets
memory.set_runtime_info(mock_runtime, custom_secrets)
memory.set_contextual_info(mock_runtime, custom_secrets)
# Set repository info
memory.set_repository_info('test-owner/test-repo', '/workspace/test-repo')
@@ -431,7 +431,7 @@ def test_custom_secrets_descriptions_serialization(prompt_dir):
'DATABASE_URL': 'Connection string for the database',
'SECRET_TOKEN': 'Authentication token for secure operations',
}
runtime_info = RuntimeInfo(
runtime_info = ContextualInfo(
date='2025-05-15',
available_hosts={'test-host.example.com': 8080},
additional_agent_instructions='Test instructions',

View File

@@ -7,7 +7,7 @@ from openhands.controller.state.state import State
from openhands.core.message import Message, TextContent
from openhands.events.observation.agent import MicroagentKnowledge
from openhands.microagent import BaseMicroagent
from openhands.utils.prompt import PromptManager, RepositoryInfo, RuntimeInfo
from openhands.utils.prompt import PromptManager, RepositoryInfo, ContextualInfo
@pytest.fixture
@@ -214,7 +214,7 @@ each of which has a corresponding port:
# Create repository and runtime information
repo_info = RepositoryInfo(repo_name='owner/repo', repo_directory='/workspace/repo')
runtime_info = RuntimeInfo(
runtime_info = ContextualInfo(
date='02/12/1232',
available_hosts={'example.com': 8080},
additional_agent_instructions='You know everything about this runtime.',