Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion openhands/agent_server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
LocalhostCORSMiddleware,
ValidateSessionAPIKeyMiddleware,
)
from openhands.agent_server.server_details_router import server_details_router


@asynccontextmanager
Expand All @@ -28,13 +29,20 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
yield


api = FastAPI(description="OpenHands Local Server", lifespan=api_lifespan)
api = FastAPI(
title="OpenHands Agent Server",
description=(
"OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
),
lifespan=api_lifespan,
)
config = get_default_config()


# Add routers
api.include_router(conversation_event_router)
api.include_router(conversation_router)
api.include_router(server_details_router)

# Add middleware
api.add_middleware(LocalhostCORSMiddleware, allow_origins=config.allow_cors_origins)
Expand Down
46 changes: 39 additions & 7 deletions openhands/agent_server/conversation_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,50 @@
from typing import Annotated
from uuid import UUID

from fastapi import APIRouter, HTTPException, Query, status
from fastapi import APIRouter, Body, HTTPException, Query, status
from pydantic import SecretStr

from openhands.agent_server.config import get_default_config
from openhands.agent_server.conversation_service import (
get_default_conversation_service,
)
from openhands.agent_server.models import (
ConversationInfo,
ConversationPage,
ConversationSortOrder,
SendMessageRequest,
StartConversationRequest,
Success,
)
from openhands.sdk.conversation.state import AgentExecutionStatus
from openhands.sdk.llm.llm import LLM
from openhands.sdk.llm.message import TextContent
from openhands.sdk.tool.spec import ToolSpec


router = APIRouter(prefix="/conversations")
conversation_service = get_default_conversation_service()
config = get_default_config()

# Examples

START_CONVERSATION_EXAMPLES = [
StartConversationRequest(
llm=LLM(
model="litellm_proxy/anthropic/claude-sonnet-4-20250514",
base_url="https://llm-proxy.app.all-hands.dev",
api_key=SecretStr("secret"),
Comment thread
enyst marked this conversation as resolved.
Outdated
),
tools=[
ToolSpec(name="BashTool", params={"working_dir": config.workspace_path}),
ToolSpec(name="FileEditor"),
ToolSpec(name="TaskTracker"),
],
initial_message=SendMessageRequest(
role="user", content=[TextContent(text="Flip a coin!")]
),
).model_dump(exclude_defaults=True)
]


# Read methods
Expand All @@ -44,7 +71,7 @@ async def search_conversations(
Query(title="Sort order for conversations"),
] = ConversationSortOrder.CREATED_AT_DESC,
) -> ConversationPage:
"""Search / List local conversations"""
"""Search / List conversations"""
assert limit > 0
assert limit <= 100
return await conversation_service.search_conversations(
Expand All @@ -59,14 +86,14 @@ async def count_conversations(
Query(title="Optional filter by agent execution status"),
] = None,
) -> int:
"""Count local conversations matching the given filters"""
"""Count conversations matching the given filters"""
count = await conversation_service.count_conversations(status)
return count


@router.get("/{conversation_id}", responses={404: {"description": "Item not found"}})
async def get_conversation(conversation_id: UUID) -> ConversationInfo:
"""Get a local conversation given an id"""
"""Given an id, get a conversation"""
conversation = await conversation_service.get_conversation(conversation_id)
if conversation is None:
raise HTTPException(status.HTTP_404_NOT_FOUND)
Expand All @@ -77,7 +104,7 @@ async def get_conversation(conversation_id: UUID) -> ConversationInfo:
async def batch_get_conversations(
ids: Annotated[list[UUID], Query()],
) -> list[ConversationInfo | None]:
"""Get a batch of local conversations given their ids, returning null for
"""Get a batch of conversations given their ids, returning null for
any missing item"""
assert len(ids) < 100
conversations = await conversation_service.batch_get_conversations(ids)
Expand All @@ -89,9 +116,11 @@ async def batch_get_conversations(

@router.post("/")
async def start_conversation(
request: StartConversationRequest,
request: Annotated[
StartConversationRequest, Body(examples=START_CONVERSATION_EXAMPLES)
],
) -> ConversationInfo:
"""Start a local conversation"""
"""Start a conversation in the local environment."""
info = await conversation_service.start_conversation(request)
return info

Expand All @@ -100,6 +129,7 @@ async def start_conversation(
"/{conversation_id}/pause", responses={404: {"description": "Item not found"}}
)
async def pause_conversation(conversation_id: UUID) -> Success:
"""Pause a conversation, allowing it to be resumed later."""
paused = await conversation_service.pause_conversation(conversation_id)
if not paused:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
Expand All @@ -110,6 +140,7 @@ async def pause_conversation(conversation_id: UUID) -> Success:
"/{conversation_id}/resume", responses={404: {"description": "Item not found"}}
)
async def resume_conversation(conversation_id: UUID) -> Success:
"""Resume a paused conversation."""
resumed = await conversation_service.resume_conversation(conversation_id)
if not resumed:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
Expand All @@ -118,6 +149,7 @@ async def resume_conversation(conversation_id: UUID) -> Success:

@router.delete("/{conversation_id}", responses={404: {"description": "Item not found"}})
async def delete_conversation(conversation_id: UUID) -> Success:
"""Permanently delete a conversation."""
deleted = await conversation_service.delete_conversation(conversation_id)
if not deleted:
raise HTTPException(status.HTTP_400_BAD_REQUEST)
Expand Down
2 changes: 2 additions & 0 deletions openhands/agent_server/conversation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
StoredConversation,
)
from openhands.agent_server.pub_sub import Subscriber
from openhands.agent_server.server_details_router import update_last_execution_time
from openhands.agent_server.utils import utc_now
from openhands.sdk import EventBase, Message
from openhands.sdk.conversation.state import AgentExecutionStatus
Expand Down Expand Up @@ -264,6 +265,7 @@ class _EventSubscriber(Subscriber):

async def __call__(self, event: EventBase):
self.service.stored.updated_at = utc_now()
update_last_execution_time()


@dataclass
Expand Down
38 changes: 38 additions & 0 deletions openhands/agent_server/server_details_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import time

from fastapi import APIRouter
from pydantic import BaseModel


server_details_router = APIRouter(prefix="")
_start_time = time.time()
_last_event_time = time.time()


class ServerInfo(BaseModel):
uptime: float
idle_time: float


def update_last_execution_time():
global _last_event_time
_last_event_time = time.time()


@server_details_router.get("/alive")
async def alive():
return {"status": "ok"}


@server_details_router.get("/health")
async def health() -> str:
return "OK"


@server_details_router.get("/server_info")
async def get_server_info() -> ServerInfo:
now = time.time()
return ServerInfo(
uptime=now - _start_time,
idle_time=now - _last_event_time,
)
Loading