from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import JSONResponse from openhands.core.logger import openhands_logger as logger from openhands.events.event_filter import EventFilter from openhands.events.serialization.event import event_to_dict from openhands.runtime.base import Runtime 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 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: """Retrieve the runtime configuration. Currently, this is the session ID and runtime ID (if available). """ runtime = conversation.runtime runtime_id = runtime.runtime_id if hasattr(runtime, 'runtime_id') else None session_id = runtime.sid if hasattr(runtime, 'sid') else None return JSONResponse( content={ 'runtime_id': runtime_id, 'session_id': session_id, } ) @app.get('/vscode-url') async def get_vscode_url(conversation: ServerConversation = Depends(get_conversation)) -> JSONResponse: """Get the VSCode URL. This endpoint allows getting the VSCode URL. Args: request (Request): The incoming FastAPI request object. Returns: JSONResponse: A JSON response indicating the success of the operation. """ try: runtime: Runtime = conversation.runtime logger.debug(f'Runtime type: {type(runtime)}') logger.debug(f'Runtime VSCode URL: {runtime.vscode_url}') return JSONResponse( status_code=status.HTTP_200_OK, content={'vscode_url': runtime.vscode_url} ) except Exception as e: logger.error(f'Error getting VSCode URL: {e}') return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ 'vscode_url': None, 'error': f'Error getting VSCode URL: {e}', }, ) @app.get('/web-hosts') 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. Args: request (Request): The incoming FastAPI request object. Returns: JSONResponse: A JSON response indicating the success of the operation. """ try: runtime: Runtime = conversation.runtime logger.debug(f'Runtime type: {type(runtime)}') logger.debug(f'Runtime hosts: {runtime.web_hosts}') return JSONResponse(status_code=200, content={'hosts': runtime.web_hosts}) except Exception as e: logger.error(f'Error getting runtime hosts: {e}') return JSONResponse( status_code=500, content={ 'hosts': None, 'error': f'Error getting runtime hosts: {e}', }, ) @app.get('/events') async def search_events( start_id: int = 0, end_id: int | None = None, reverse: bool = False, filter: EventFilter | None = None, limit: int = 20, conversation: ServerConversation = Depends(get_conversation), ): """Search through the event stream with filtering and pagination. Args: request: The incoming request object start_id: Starting ID in the event stream. Defaults to 0 end_id: Ending ID in the event stream reverse: Whether to retrieve events in reverse order. Defaults to False. filter: Filter for events limit: Maximum number of events to return. Must be between 1 and 100. Defaults to 20 Returns: dict: Dictionary containing: - events: List of matching events - has_more: Whether there are more matching events after this batch Raises: HTTPException: If conversation is not found ValueError: If limit is less than 1 or greater than 100 """ if limit < 0 or limit > 100: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid limit' ) # Get matching events from the stream event_stream = conversation.event_stream events = list( event_stream.search_events( start_id=start_id, end_id=end_id, reverse=reverse, filter=filter, limit=limit + 1, ) ) # Check if there are more events has_more = len(events) > limit if has_more: events = events[:limit] # Remove the extra event events_json = [event_to_dict(event) for event in events] return { 'events': events_json, 'has_more': has_more, } @app.post('/events') 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})