Spaces:
Build error
Build error
File size: 6,893 Bytes
51ff9e5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
import asyncio
import os
import subprocess
import sys
import time
from dataclasses import dataclass
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import Action, IPythonRunCellAction
from openhands.events.observation import IPythonRunCellObservation
from openhands.runtime.plugins.jupyter.execute_server import JupyterKernel
from openhands.runtime.plugins.requirement import Plugin, PluginRequirement
from openhands.runtime.utils import find_available_tcp_port
from openhands.utils.shutdown_listener import should_continue
@dataclass
class JupyterRequirement(PluginRequirement):
name: str = 'jupyter'
class JupyterPlugin(Plugin):
name: str = 'jupyter'
kernel_gateway_port: int
kernel_id: str
gateway_process: asyncio.subprocess.Process | subprocess.Popen
python_interpreter_path: str
async def initialize(
self, username: str, kernel_id: str = 'openhands-default'
) -> None:
self.kernel_gateway_port = find_available_tcp_port(40000, 49999)
self.kernel_id = kernel_id
is_local_runtime = os.environ.get('LOCAL_RUNTIME_MODE') == '1'
is_windows = sys.platform == 'win32'
if not is_local_runtime:
# Non-LocalRuntime
prefix = f'su - {username} -s '
# cd to code repo, setup all env vars and run micromamba
poetry_prefix = (
'cd /openhands/code\n'
'export POETRY_VIRTUALENVS_PATH=/openhands/poetry;\n'
'export PYTHONPATH=/openhands/code:$PYTHONPATH;\n'
'export MAMBA_ROOT_PREFIX=/openhands/micromamba;\n'
'/openhands/micromamba/bin/micromamba run -n openhands '
)
else:
# LocalRuntime
prefix = ''
code_repo_path = os.environ.get('OPENHANDS_REPO_PATH')
if not code_repo_path:
raise ValueError(
'OPENHANDS_REPO_PATH environment variable is not set. '
'This is required for the jupyter plugin to work with LocalRuntime.'
)
# The correct environment is ensured by the PATH in LocalRuntime.
poetry_prefix = f'cd {code_repo_path}\n'
if is_windows:
# Windows-specific command format
jupyter_launch_command = (
f'cd /d "{code_repo_path}" && '
'poetry run jupyter kernelgateway '
'--KernelGatewayApp.ip=0.0.0.0 '
f'--KernelGatewayApp.port={self.kernel_gateway_port}'
)
logger.debug(f'Jupyter launch command (Windows): {jupyter_launch_command}')
# Using synchronous subprocess.Popen for Windows as asyncio.create_subprocess_shell
# has limitations on Windows platforms
self.gateway_process = subprocess.Popen( # type: ignore[ASYNC101] # noqa: ASYNC101
jupyter_launch_command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
text=True,
)
# Windows-specific stdout handling with synchronous time.sleep
# as asyncio has limitations on Windows for subprocess operations
output = ''
while should_continue():
if self.gateway_process.stdout is None:
time.sleep(1) # type: ignore[ASYNC101] # noqa: ASYNC101
continue
line = self.gateway_process.stdout.readline()
if not line:
time.sleep(1) # type: ignore[ASYNC101] # noqa: ASYNC101
continue
output += line
if 'at' in line:
break
time.sleep(1) # type: ignore[ASYNC101] # noqa: ASYNC101
logger.debug('Waiting for jupyter kernel gateway to start...')
logger.debug(
f'Jupyter kernel gateway started at port {self.kernel_gateway_port}. Output: {output}'
)
else:
# Unix systems (Linux/macOS)
jupyter_launch_command = (
f"{prefix}/bin/bash << 'EOF'\n"
f'{poetry_prefix}'
'poetry run jupyter kernelgateway '
'--KernelGatewayApp.ip=0.0.0.0 '
f'--KernelGatewayApp.port={self.kernel_gateway_port}\n'
'EOF'
)
logger.debug(f'Jupyter launch command: {jupyter_launch_command}')
# Using asyncio.create_subprocess_shell instead of subprocess.Popen
# to avoid ASYNC101 linting error
self.gateway_process = await asyncio.create_subprocess_shell(
jupyter_launch_command,
stderr=asyncio.subprocess.STDOUT,
stdout=asyncio.subprocess.PIPE,
)
# read stdout until the kernel gateway is ready
output = ''
while should_continue() and self.gateway_process.stdout is not None:
line_bytes = await self.gateway_process.stdout.readline()
line = line_bytes.decode('utf-8')
output += line
if 'at' in line:
break
await asyncio.sleep(1)
logger.debug('Waiting for jupyter kernel gateway to start...')
logger.debug(
f'Jupyter kernel gateway started at port {self.kernel_gateway_port}. Output: {output}'
)
_obs = await self.run(
IPythonRunCellAction(code='import sys; print(sys.executable)')
)
self.python_interpreter_path = _obs.content.strip()
async def _run(self, action: Action) -> IPythonRunCellObservation:
"""Internal method to run a code cell in the jupyter kernel."""
if not isinstance(action, IPythonRunCellAction):
raise ValueError(
f'Jupyter plugin only supports IPythonRunCellAction, but got {action}'
)
if not hasattr(self, 'kernel'):
self.kernel = JupyterKernel(
f'localhost:{self.kernel_gateway_port}', self.kernel_id
)
if not self.kernel.initialized:
await self.kernel.initialize()
# Execute the code and get structured output
output = await self.kernel.execute(action.code, timeout=action.timeout)
# Extract text content and image URLs from the structured output
text_content = output.get('text', '')
image_urls = output.get('images', [])
return IPythonRunCellObservation(
content=text_content,
code=action.code,
image_urls=image_urls if image_urls else None,
)
async def run(self, action: Action) -> IPythonRunCellObservation:
obs = await self._run(action)
return obs
|