Compare commits

...

1 Commits

Author SHA1 Message Date
openhands
16ed83082f Add /conversation/{conv_id}/microagents endpoint 2025-06-07 22:00:00 +00:00
2 changed files with 219 additions and 5 deletions

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
@@ -10,11 +11,15 @@ from openhands.server.session.conversation import ServerConversation
from openhands.server.shared import conversation_manager
from openhands.server.utils import get_conversation
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 +36,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 +68,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 +152,76 @@ 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] = []
@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 = agent_session.memory
# 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=[]
)
)
# Add knowledge microagents
for name, agent in memory.knowledge_microagents.items():
microagents.append(
MicroagentResponse(
name=name,
type='knowledge',
content=agent.content,
triggers=agent.triggers,
)
)
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

@@ -0,0 +1,136 @@
import json
from unittest.mock import MagicMock, patch
import pytest
from fastapi.responses import JSONResponse
from openhands.microagent.microagent import KnowledgeMicroagent, RepoMicroagent
from openhands.microagent.types import MicroagentMetadata, MicroagentType
from openhands.server.routes.conversation import get_microagents
from openhands.server.session.conversation import ServerConversation
@pytest.mark.asyncio
async def test_get_microagents():
"""Test the get_microagents function directly."""
# Create mock microagents
repo_microagent = RepoMicroagent(
name='test_repo',
content='This is a test repo microagent',
metadata=MicroagentMetadata(
name='test_repo', type=MicroagentType.REPO_KNOWLEDGE
),
source='test_source',
type=MicroagentType.REPO_KNOWLEDGE,
)
knowledge_microagent = KnowledgeMicroagent(
name='test_knowledge',
content='This is a test knowledge microagent',
metadata=MicroagentMetadata(
name='test_knowledge',
type=MicroagentType.KNOWLEDGE,
triggers=['test', 'knowledge'],
),
source='test_source',
type=MicroagentType.KNOWLEDGE,
)
# Mock the agent session and memory
mock_memory = MagicMock()
mock_memory.repo_microagents = {'test_repo': repo_microagent}
mock_memory.knowledge_microagents = {'test_knowledge': knowledge_microagent}
mock_agent_session = MagicMock()
mock_agent_session.memory = mock_memory
# Create a mock ServerConversation
mock_conversation = MagicMock(spec=ServerConversation)
mock_conversation.sid = 'test_sid'
# Mock the conversation manager
with patch(
'openhands.server.routes.conversation.conversation_manager'
) as mock_manager:
# Set up the mocks
mock_manager.get_agent_session.return_value = mock_agent_session
# Call the function directly
response = await get_microagents(conversation=mock_conversation)
# Verify the response
assert isinstance(response, JSONResponse)
assert response.status_code == 200
# Parse the JSON content
content = json.loads(response.body)
assert 'microagents' in content
assert len(content['microagents']) == 2
# Check repo microagent
repo_agent = next(m for m in content['microagents'] if m['name'] == 'test_repo')
assert repo_agent['type'] == 'repo'
assert repo_agent['content'] == 'This is a test repo microagent'
assert repo_agent['triggers'] == []
# Check knowledge microagent
knowledge_agent = next(
m for m in content['microagents'] if m['name'] == 'test_knowledge'
)
assert knowledge_agent['type'] == 'knowledge'
assert knowledge_agent['content'] == 'This is a test knowledge microagent'
assert knowledge_agent['triggers'] == ['test', 'knowledge']
@pytest.mark.asyncio
async def test_get_microagents_no_agent_session():
"""Test the get_microagents function when no agent session is found."""
# Create a mock ServerConversation
mock_conversation = MagicMock(spec=ServerConversation)
mock_conversation.sid = 'test_sid'
# Mock the conversation manager
with patch(
'openhands.server.routes.conversation.conversation_manager'
) as mock_manager:
# Set up the mocks
mock_manager.get_agent_session.return_value = None
# Call the function directly
response = await get_microagents(conversation=mock_conversation)
# Verify the response
assert isinstance(response, JSONResponse)
assert response.status_code == 404
# Parse the JSON content
content = json.loads(response.body)
assert 'error' in content
assert 'Agent session not found' in content['error']
@pytest.mark.asyncio
async def test_get_microagents_exception():
"""Test the get_microagents function when an exception occurs."""
# Create a mock ServerConversation
mock_conversation = MagicMock(spec=ServerConversation)
mock_conversation.sid = 'test_sid'
# Mock the conversation manager
with patch(
'openhands.server.routes.conversation.conversation_manager'
) as mock_manager:
# Set up the mocks to raise an exception
mock_manager.get_agent_session.side_effect = Exception('Test exception')
# Call the function directly
response = await get_microagents(conversation=mock_conversation)
# Verify the response
assert isinstance(response, JSONResponse)
assert response.status_code == 500
# Parse the JSON content
content = json.loads(response.body)
assert 'error' in content
assert 'Test exception' in content['error']