feat(backend): implement API to fetch contents of PLAN.md (#11795)

This commit is contained in:
Hiep Le
2025-11-30 13:29:13 +07:00
committed by GitHub
parent 156d0686c4
commit d62bb81c3b
4 changed files with 1039 additions and 22 deletions

View File

@@ -1,7 +1,9 @@
"""Sandboxed Conversation router for OpenHands Server."""
import asyncio
import os
import sys
import tempfile
from datetime import datetime
from typing import Annotated, AsyncGenerator
from uuid import UUID
@@ -49,9 +51,21 @@ from openhands.app_server.config import (
depends_app_conversation_start_task_service,
depends_db_session,
depends_httpx_client,
depends_sandbox_service,
depends_sandbox_spec_service,
depends_user_context,
get_app_conversation_service,
)
from openhands.app_server.sandbox.sandbox_models import (
AGENT_SERVER,
SandboxStatus,
)
from openhands.app_server.sandbox.sandbox_service import SandboxService
from openhands.app_server.sandbox.sandbox_spec_service import SandboxSpecService
from openhands.app_server.utils.docker_utils import (
replace_localhost_hostname_for_docker,
)
from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace
router = APIRouter(prefix='/app-conversations', tags=['Conversations'])
app_conversation_service_dependency = depends_app_conversation_service()
@@ -61,6 +75,8 @@ app_conversation_start_task_service_dependency = (
user_context_dependency = depends_user_context()
db_session_dependency = depends_db_session()
httpx_client_dependency = depends_httpx_client()
sandbox_service_dependency = depends_sandbox_service()
sandbox_spec_service_dependency = depends_sandbox_spec_service()
# Read methods
@@ -289,6 +305,101 @@ async def batch_get_app_conversation_start_tasks(
return start_tasks
@router.get('/{conversation_id}/file')
async def read_conversation_file(
conversation_id: UUID,
file_path: Annotated[
str,
Query(title='Path to the file to read within the sandbox workspace'),
] = '/workspace/project/PLAN.md',
app_conversation_service: AppConversationService = (
app_conversation_service_dependency
),
sandbox_service: SandboxService = sandbox_service_dependency,
sandbox_spec_service: SandboxSpecService = sandbox_spec_service_dependency,
) -> str:
"""Read a file from a specific conversation's sandbox workspace.
Returns the content of the file at the specified path if it exists, otherwise returns an empty string.
Args:
conversation_id: The UUID of the conversation
file_path: Path to the file to read within the sandbox workspace
Returns:
The content of the file or an empty string if the file doesn't exist
"""
# Get the conversation info
conversation = await app_conversation_service.get_app_conversation(conversation_id)
if not conversation:
return ''
# Get the sandbox info
sandbox = await sandbox_service.get_sandbox(conversation.sandbox_id)
if not sandbox or sandbox.status != SandboxStatus.RUNNING:
return ''
# Get the sandbox spec to find the working directory
sandbox_spec = await sandbox_spec_service.get_sandbox_spec(sandbox.sandbox_spec_id)
if not sandbox_spec:
return ''
# Get the agent server URL
if not sandbox.exposed_urls:
return ''
agent_server_url = None
for exposed_url in sandbox.exposed_urls:
if exposed_url.name == AGENT_SERVER:
agent_server_url = exposed_url.url
break
if not agent_server_url:
return ''
agent_server_url = replace_localhost_hostname_for_docker(agent_server_url)
# Create remote workspace
remote_workspace = AsyncRemoteWorkspace(
host=agent_server_url,
api_key=sandbox.session_api_key,
working_dir=sandbox_spec.working_dir,
)
# Read the file at the specified path
temp_file_path = None
try:
# Create a temporary file path to download the remote file
with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as temp_file:
temp_file_path = temp_file.name
# Download the file from remote system
result = await remote_workspace.file_download(
source_path=file_path,
destination_path=temp_file_path,
)
if result.success:
# Read the content from the temporary file
with open(temp_file_path, 'rb') as f:
content = f.read()
# Decode bytes to string
return content.decode('utf-8')
except Exception:
# If there's any error reading the file, return empty string
pass
finally:
# Clean up the temporary file
if temp_file_path:
try:
os.unlink(temp_file_path)
except Exception:
# Ignore errors during cleanup
pass
return ''
async def _consume_remaining(
async_iter, db_session: AsyncSession, httpx_client: httpx.AsyncClient
):

View File

@@ -217,7 +217,9 @@ class DockerSandboxService(SandboxService):
sandboxes = []
for container in all_containers:
if container.name.startswith(self.container_name_prefix):
if container.name and container.name.startswith(
self.container_name_prefix
):
sandbox_info = await self._container_to_checked_sandbox_info(
container
)

View File

@@ -7,7 +7,7 @@ def stop_all_containers(prefix: str) -> None:
containers = docker_client.containers.list(all=True)
for container in containers:
try:
if container.name.startswith(prefix):
if container.name and container.name.startswith(prefix):
container.stop()
except docker.errors.APIError:
pass