feat: Add microagents UI to conversation context menu (#8984)

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
This commit is contained in:
Xingyao Wang
2025-06-11 11:12:27 -04:00
committed by GitHub
parent f27b02411b
commit 3f50eb0079
16 changed files with 734 additions and 47 deletions

View File

@@ -9,6 +9,7 @@ from openhands.events.action import MessageAction
from openhands.server.config.server_config import ServerConfig
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.monitoring import MonitoringListener
from openhands.server.session.agent_session import AgentSession
from openhands.server.session.conversation import ServerConversation
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.data_models.settings import Settings
@@ -114,6 +115,17 @@ class ConversationManager(ABC):
async def close_session(self, sid: str):
"""Close a session."""
@abstractmethod
def get_agent_session(self, sid: str) -> AgentSession | None:
"""Get the agent session for a given session ID.
Args:
sid: The session ID.
Returns:
The agent session, or None if not found.
"""
@abstractmethod
async def get_agent_loop_info(
self, user_id: str | None = None, filter_to_sids: set[str] | None = None

View File

@@ -307,7 +307,9 @@ class DockerNestedConversationManager(ConversationManager):
await asyncio.sleep(1)
except Exception as e:
logger.warning('error_stopping_container', extra={"sid": sid, "error": str(e)})
logger.warning(
'error_stopping_container', extra={'sid': sid, 'error': str(e)}
)
container.stop()
async def get_agent_loop_info(
@@ -364,6 +366,15 @@ class DockerNestedConversationManager(ConversationManager):
file_store=file_store,
)
def get_agent_session(self, sid: str):
"""Get the agent session for a given session ID.
Args:
sid: The session ID.
Returns:
The agent session, or None if not found.
"""
raise ValueError('unsupported_operation')
async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
conversation_store_class = self._conversation_store_class
if not conversation_store_class:

View File

@@ -15,7 +15,7 @@ from openhands.events.stream import EventStreamSubscriber, session_exists
from openhands.server.config.server_config import ServerConfig
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.monitoring import MonitoringListener
from openhands.server.session.agent_session import WAIT_TIME_BEFORE_CLOSE
from openhands.server.session.agent_session import AgentSession, WAIT_TIME_BEFORE_CLOSE
from openhands.server.session.conversation import ServerConversation
from openhands.server.session.session import ROOM_KEY, Session
from openhands.storage.conversation.conversation_store import ConversationStore
@@ -112,7 +112,7 @@ class StandaloneConversationManager(ConversationManager):
end_time = time.time()
logger.info(
f'ServerConversation {c.sid} connected in {end_time - start_time} seconds',
extra={'session_id': sid}
extra={'session_id': sid},
)
self._active_conversations[sid] = (c, 1)
return c
@@ -356,6 +356,20 @@ class StandaloneConversationManager(ConversationManager):
if session:
await self._close_session(sid)
def get_agent_session(self, sid: str) -> AgentSession | None:
"""Get the agent session for a given session ID.
Args:
sid: The session ID.
Returns:
The agent session, or None if not found.
"""
session = self._local_agent_loops_by_sid.get(sid)
if session:
return session.agent_session
return None
async def _close_session(self, sid: str):
logger.info(f'_close_session:{sid}', extra={'session_id': sid})

View File

@@ -1,5 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from openhands.core.logger import openhands_logger as logger
from openhands.events.event_filter import EventFilter
@@ -9,12 +10,18 @@ from openhands.server.dependencies import get_dependencies
from openhands.server.session.conversation import ServerConversation
from openhands.server.shared import conversation_manager
from openhands.server.utils import get_conversation
from openhands.microagent.types import InputMetadata
from openhands.memory.memory import Memory
app = APIRouter(prefix='/api/conversations/{conversation_id}', dependencies=get_dependencies())
app = APIRouter(
prefix='/api/conversations/{conversation_id}', dependencies=get_dependencies()
)
@app.get('/config')
async def get_remote_runtime_config(conversation: ServerConversation = Depends(get_conversation)) -> JSONResponse:
async def get_remote_runtime_config(
conversation: ServerConversation = Depends(get_conversation),
) -> JSONResponse:
"""Retrieve the runtime configuration.
Currently, this is the session ID and runtime ID (if available).
@@ -31,7 +38,9 @@ async def get_remote_runtime_config(conversation: ServerConversation = Depends(g
@app.get('/vscode-url')
async def get_vscode_url(conversation: ServerConversation = Depends(get_conversation)) -> JSONResponse:
async def get_vscode_url(
conversation: ServerConversation = Depends(get_conversation),
) -> JSONResponse:
"""Get the VSCode URL.
This endpoint allows getting the VSCode URL.
@@ -61,7 +70,9 @@ async def get_vscode_url(conversation: ServerConversation = Depends(get_conversa
@app.get('/web-hosts')
async def get_hosts(conversation: ServerConversation = Depends(get_conversation)) -> JSONResponse:
async def get_hosts(
conversation: ServerConversation = Depends(get_conversation),
) -> JSONResponse:
"""Get the hosts used by the runtime.
This endpoint allows getting the hosts used by the runtime.
@@ -143,7 +154,92 @@ async def search_events(
@app.post('/events')
async def add_event(request: Request, conversation: ServerConversation = Depends(get_conversation)):
async def add_event(
request: Request, conversation: ServerConversation = Depends(get_conversation)
):
data = request.json()
await conversation_manager.send_to_event_stream(conversation.sid, data)
return JSONResponse({'success': True})
class MicroagentResponse(BaseModel):
"""Response model for microagents endpoint."""
name: str
type: str
content: str
triggers: list[str] = []
inputs: list[InputMetadata] = []
tools: list[str] = []
@app.get('/microagents')
async def get_microagents(
conversation: ServerConversation = Depends(get_conversation),
) -> JSONResponse:
"""Get all microagents associated with the conversation.
This endpoint returns all repository and knowledge microagents that are loaded for the conversation.
Returns:
JSONResponse: A JSON response containing the list of microagents.
"""
try:
# Get the agent session for this conversation
agent_session = conversation_manager.get_agent_session(conversation.sid)
if not agent_session:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={'error': 'Agent session not found for this conversation'},
)
# Access the memory to get the microagents
memory: Memory | None = agent_session.memory
if memory is None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={
'error': 'Memory is not yet initialized for this conversation'
},
)
# Prepare the response
microagents = []
# Add repo microagents
for name, agent in memory.repo_microagents.items():
microagents.append(
MicroagentResponse(
name=name,
type='repo',
content=agent.content,
triggers=[],
inputs=agent.metadata.inputs,
tools=[server.name for server in agent.metadata.mcp_tools.stdio_servers] if agent.metadata.mcp_tools else [],
)
)
# Add knowledge microagents
for name, agent in memory.knowledge_microagents.items():
microagents.append(
MicroagentResponse(
name=name,
type='knowledge',
content=agent.content,
triggers=agent.triggers,
inputs=agent.metadata.inputs,
tools=[server.name for server in agent.metadata.mcp_tools.stdio_servers] if agent.metadata.mcp_tools else [],
)
)
return JSONResponse(
status_code=status.HTTP_200_OK,
content={'microagents': [m.dict() for m in microagents]},
)
except Exception as e:
logger.error(f'Error getting microagents: {e}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'error': f'Error getting microagents: {e}'},
)

View File

@@ -51,6 +51,7 @@ class AgentSession:
controller: AgentController | None = None
runtime: Runtime | None = None
security_analyzer: SecurityAnalyzer | None = None
memory: Memory | None = None
_starting: bool = False
_started_at: float = 0
_closed: bool = False