mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-10 07:18:10 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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}'},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user