opensandbox-code-interpreter 0.1.2.dev0__py3-none-any.whl → 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_interpreter/__init__.py +1 -1
- code_interpreter/adapters/__init__.py +1 -1
- code_interpreter/adapters/code_adapter.py +92 -29
- code_interpreter/adapters/converter/__init__.py +1 -1
- code_interpreter/adapters/converter/code_execution_converter.py +1 -1
- code_interpreter/adapters/factory.py +1 -1
- code_interpreter/code_interpreter.py +180 -13
- code_interpreter/models/__init__.py +1 -1
- code_interpreter/models/code.py +1 -1
- code_interpreter/models/code_sync.py +2 -1
- code_interpreter/services/__init__.py +1 -1
- code_interpreter/services/code.py +18 -4
- code_interpreter/sync/__init__.py +1 -1
- code_interpreter/sync/adapters/__init__.py +1 -1
- code_interpreter/sync/adapters/code_adapter.py +104 -17
- code_interpreter/sync/adapters/factory.py +1 -1
- code_interpreter/sync/code_interpreter.py +162 -9
- code_interpreter/sync/services/__init__.py +1 -1
- code_interpreter/sync/services/code.py +15 -1
- {opensandbox_code_interpreter-0.1.2.dev0.dist-info → opensandbox_code_interpreter-1.1.1.dist-info}/METADATA +21 -14
- opensandbox_code_interpreter-1.1.1.dist-info/RECORD +24 -0
- {opensandbox_code_interpreter-0.1.2.dev0.dist-info → opensandbox_code_interpreter-1.1.1.dist-info}/WHEEL +1 -1
- opensandbox_code_interpreter-0.1.2.dev0.dist-info/RECORD +0 -24
- {opensandbox_code_interpreter-0.1.2.dev0.dist-info → opensandbox_code_interpreter-1.1.1.dist-info}/licenses/LICENSE +0 -0
code_interpreter/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025
|
|
2
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
@@ -22,6 +22,7 @@ API clients and handling SSE streaming for real-time code execution.
|
|
|
22
22
|
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
|
+
import time
|
|
25
26
|
|
|
26
27
|
import httpx
|
|
27
28
|
from opensandbox.adapters.converter.event_node import EventNode
|
|
@@ -32,6 +33,7 @@ from opensandbox.adapters.converter.execution_event_dispatcher import (
|
|
|
32
33
|
ExecutionEventDispatcher,
|
|
33
34
|
)
|
|
34
35
|
from opensandbox.adapters.converter.response_handler import (
|
|
36
|
+
extract_request_id,
|
|
35
37
|
handle_api_error,
|
|
36
38
|
require_parsed,
|
|
37
39
|
)
|
|
@@ -39,6 +41,7 @@ from opensandbox.config import ConnectionConfig
|
|
|
39
41
|
from opensandbox.exceptions import InvalidArgumentException, SandboxApiException
|
|
40
42
|
from opensandbox.models.execd import Execution, ExecutionHandlers
|
|
41
43
|
from opensandbox.models.sandboxes import SandboxEndpoint
|
|
44
|
+
from opensandbox.transport import unwrap_retry_transport
|
|
42
45
|
|
|
43
46
|
from code_interpreter.adapters.converter.code_execution_converter import (
|
|
44
47
|
CodeExecutionConverter,
|
|
@@ -49,18 +52,28 @@ from code_interpreter.services.code import Codes
|
|
|
49
52
|
logger = logging.getLogger(__name__)
|
|
50
53
|
|
|
51
54
|
|
|
55
|
+
def _normalize_sse_event(event_dict: dict) -> dict:
|
|
56
|
+
if "type" in event_dict and "timestamp" in event_dict:
|
|
57
|
+
return event_dict
|
|
58
|
+
if "code" in event_dict and "message" in event_dict:
|
|
59
|
+
return {
|
|
60
|
+
"type": "error",
|
|
61
|
+
"timestamp": int(time.time() * 1000),
|
|
62
|
+
"error": {
|
|
63
|
+
"ename": str(event_dict["code"]),
|
|
64
|
+
"evalue": str(event_dict["message"]),
|
|
65
|
+
"traceback": [],
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
return event_dict
|
|
69
|
+
|
|
70
|
+
|
|
52
71
|
class CodesAdapter(Codes):
|
|
53
72
|
"""
|
|
54
73
|
Adapter implementation for code execution service.
|
|
55
74
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
API calls and SSE streaming for real-time code execution output.
|
|
59
|
-
|
|
60
|
-
Similar to CommandServiceAdapter, this adapter uses:
|
|
61
|
-
- Generated API clients for simple operations (create_context, interrupt)
|
|
62
|
-
- Direct httpx SSE streaming for run
|
|
63
|
-
- ExceptionConverter for unified exception handling
|
|
75
|
+
Handles standard API calls plus direct httpx SSE streaming for
|
|
76
|
+
real-time code execution output.
|
|
64
77
|
"""
|
|
65
78
|
|
|
66
79
|
RUN_CODE_PATH = "/code"
|
|
@@ -85,10 +98,7 @@ class CodesAdapter(Codes):
|
|
|
85
98
|
timeout_seconds = self.connection_config.request_timeout.total_seconds()
|
|
86
99
|
timeout = httpx.Timeout(timeout_seconds)
|
|
87
100
|
|
|
88
|
-
headers =
|
|
89
|
-
"User-Agent": self.connection_config.user_agent,
|
|
90
|
-
**self.connection_config.headers,
|
|
91
|
-
}
|
|
101
|
+
headers = self.execd_endpoint.build_request_headers(self.connection_config)
|
|
92
102
|
|
|
93
103
|
# Execd API does not require authentication
|
|
94
104
|
self._client = Client(
|
|
@@ -96,12 +106,19 @@ class CodesAdapter(Codes):
|
|
|
96
106
|
timeout=timeout,
|
|
97
107
|
)
|
|
98
108
|
|
|
99
|
-
#
|
|
109
|
+
# Adapter clients must own their transports: aclose() closes them,
|
|
110
|
+
# and the shared/user connection_config transport must survive that.
|
|
111
|
+
self._owns_clients = self.connection_config.owns_transport
|
|
112
|
+
main_transport: httpx.AsyncBaseTransport | None
|
|
113
|
+
if self._owns_clients:
|
|
114
|
+
main_transport = self.connection_config.new_owned_transport()
|
|
115
|
+
else:
|
|
116
|
+
main_transport = self.connection_config.transport
|
|
100
117
|
self._httpx_client = httpx.AsyncClient(
|
|
101
118
|
base_url=base_url,
|
|
102
119
|
headers=headers,
|
|
103
120
|
timeout=timeout,
|
|
104
|
-
transport=
|
|
121
|
+
transport=main_transport,
|
|
105
122
|
)
|
|
106
123
|
self._client.set_async_httpx_client(self._httpx_client)
|
|
107
124
|
|
|
@@ -111,6 +128,14 @@ class CodesAdapter(Codes):
|
|
|
111
128
|
"Accept": "text/event-stream",
|
|
112
129
|
"Cache-Control": "no-cache",
|
|
113
130
|
}
|
|
131
|
+
# SSE bootstraps bypass the retry wrapper: request bodies are
|
|
132
|
+
# not replayable and a non-idempotent status opt-in would cause
|
|
133
|
+
# duplicate execution on a resent SSE POST.
|
|
134
|
+
sse_transport = (
|
|
135
|
+
self.connection_config.new_owned_transport()
|
|
136
|
+
if self._owns_clients
|
|
137
|
+
else self.connection_config.transport
|
|
138
|
+
)
|
|
114
139
|
self._sse_client = httpx.AsyncClient(
|
|
115
140
|
headers=sse_headers,
|
|
116
141
|
timeout=httpx.Timeout(
|
|
@@ -119,7 +144,7 @@ class CodesAdapter(Codes):
|
|
|
119
144
|
write=timeout_seconds,
|
|
120
145
|
pool=None,
|
|
121
146
|
),
|
|
122
|
-
transport=
|
|
147
|
+
transport=unwrap_retry_transport(sse_transport),
|
|
123
148
|
)
|
|
124
149
|
|
|
125
150
|
async def _get_client(self):
|
|
@@ -135,6 +160,21 @@ class CodesAdapter(Codes):
|
|
|
135
160
|
"""Return SSE client (read timeout disabled) for execd streaming."""
|
|
136
161
|
return self._sse_client
|
|
137
162
|
|
|
163
|
+
async def aclose(self) -> None:
|
|
164
|
+
"""Release the adapter-owned HTTP clients.
|
|
165
|
+
|
|
166
|
+
Never closes the shared ``connection_config.transport``; the sandbox
|
|
167
|
+
stays usable after ``aclose()``. Clients wrapping a user-supplied
|
|
168
|
+
transport are left open (httpx would close the caller's transport
|
|
169
|
+
with them).
|
|
170
|
+
"""
|
|
171
|
+
if not self._owns_clients:
|
|
172
|
+
return
|
|
173
|
+
try:
|
|
174
|
+
await self._httpx_client.aclose()
|
|
175
|
+
finally:
|
|
176
|
+
await self._sse_client.aclose()
|
|
177
|
+
|
|
138
178
|
async def create_context(self, language: str) -> CodeContext:
|
|
139
179
|
"""
|
|
140
180
|
Creates a new execution context for code interpretation.
|
|
@@ -197,7 +237,9 @@ class CodesAdapter(Codes):
|
|
|
197
237
|
)
|
|
198
238
|
handle_api_error(response_obj, "List code contexts")
|
|
199
239
|
parsed_list = require_parsed(response_obj, list, "List code contexts")
|
|
200
|
-
return [
|
|
240
|
+
return [
|
|
241
|
+
CodeExecutionConverter.from_api_code_context(c) for c in parsed_list
|
|
242
|
+
]
|
|
201
243
|
except Exception as e:
|
|
202
244
|
logger.error("Failed to list contexts", exc_info=e)
|
|
203
245
|
raise ExceptionConverter.to_sandbox_exception(e) from e
|
|
@@ -243,14 +285,18 @@ class CodesAdapter(Codes):
|
|
|
243
285
|
"""
|
|
244
286
|
Executes code within the specified context using SSE streaming.
|
|
245
287
|
|
|
246
|
-
|
|
288
|
+
Uses direct httpx
|
|
247
289
|
streaming to handle SSE responses from the execd service.
|
|
248
290
|
"""
|
|
249
291
|
if not code.strip():
|
|
250
292
|
raise InvalidArgumentException("Code cannot be empty")
|
|
251
293
|
|
|
252
294
|
try:
|
|
253
|
-
if
|
|
295
|
+
if (
|
|
296
|
+
context is not None
|
|
297
|
+
and language is not None
|
|
298
|
+
and context.language != language
|
|
299
|
+
):
|
|
254
300
|
raise InvalidArgumentException(
|
|
255
301
|
f"language '{language}' must match context.language '{context.language}'"
|
|
256
302
|
)
|
|
@@ -280,13 +326,12 @@ class CodesAdapter(Codes):
|
|
|
280
326
|
await response.aread()
|
|
281
327
|
error_body = response.text
|
|
282
328
|
logger.error(
|
|
283
|
-
"Failed to run code. Status:
|
|
284
|
-
response.status_code,
|
|
285
|
-
error_body,
|
|
329
|
+
f"Failed to run code. Status: {response.status_code}, Body: {error_body}"
|
|
286
330
|
)
|
|
287
331
|
raise SandboxApiException(
|
|
288
|
-
message=f"Failed to run code. Status code: {response.status_code}",
|
|
332
|
+
message=f"Failed to run code. Status code: {response.status_code}, Body: {error_body}",
|
|
289
333
|
status_code=response.status_code,
|
|
334
|
+
request_id=extract_request_id(response.headers),
|
|
290
335
|
)
|
|
291
336
|
|
|
292
337
|
dispatcher = ExecutionEventDispatcher(execution, handlers)
|
|
@@ -301,22 +346,20 @@ class CodesAdapter(Codes):
|
|
|
301
346
|
data = data[5:].strip()
|
|
302
347
|
|
|
303
348
|
try:
|
|
304
|
-
event_dict = json.loads(data)
|
|
349
|
+
event_dict = _normalize_sse_event(json.loads(data))
|
|
305
350
|
event_node = EventNode(**event_dict)
|
|
306
351
|
await dispatcher.dispatch(event_node)
|
|
307
352
|
except json.JSONDecodeError:
|
|
308
|
-
logger.debug("Failed to parse SSE line:
|
|
353
|
+
logger.debug(f"Failed to parse SSE line: {line}")
|
|
309
354
|
continue
|
|
310
355
|
except Exception as e:
|
|
311
|
-
logger.error("Error processing event:
|
|
356
|
+
logger.error(f"Error processing event: {data}", exc_info=e)
|
|
312
357
|
continue
|
|
313
358
|
|
|
314
359
|
return execution
|
|
315
360
|
|
|
316
361
|
except Exception as e:
|
|
317
|
-
logger.error(
|
|
318
|
-
"Failed to run code (length: %s)", len(code), exc_info=e
|
|
319
|
-
)
|
|
362
|
+
logger.error(f"Failed to run code (length: {len(code)})", exc_info=e)
|
|
320
363
|
raise ExceptionConverter.to_sandbox_exception(e) from e
|
|
321
364
|
|
|
322
365
|
async def interrupt(self, execution_id: str) -> None:
|
|
@@ -339,3 +382,23 @@ class CodesAdapter(Codes):
|
|
|
339
382
|
except Exception as e:
|
|
340
383
|
logger.error("Failed to interrupt code execution", exc_info=e)
|
|
341
384
|
raise ExceptionConverter.to_sandbox_exception(e) from e
|
|
385
|
+
|
|
386
|
+
async def ping(self) -> bool:
|
|
387
|
+
"""
|
|
388
|
+
Check if the execd code execution service is alive.
|
|
389
|
+
|
|
390
|
+
Uses the shared generated API client so the ping exercises the same
|
|
391
|
+
HTTP client and endpoint that serve code execution requests.
|
|
392
|
+
"""
|
|
393
|
+
try:
|
|
394
|
+
from opensandbox.api.execd.api.health import ping as ping_api
|
|
395
|
+
|
|
396
|
+
client = await self._get_client()
|
|
397
|
+
response_obj = await ping_api.asyncio_detailed(client=client)
|
|
398
|
+
|
|
399
|
+
handle_api_error(response_obj, "Ping code interpreter")
|
|
400
|
+
return True
|
|
401
|
+
|
|
402
|
+
except Exception as e:
|
|
403
|
+
logger.debug(f"Code interpreter ping failed: {e}")
|
|
404
|
+
return False
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025
|
|
2
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
@@ -21,12 +21,17 @@ functionality with specialized code execution features, including multi-language
|
|
|
21
21
|
support, session management, and variable persistence.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
import asyncio
|
|
25
|
+
import contextlib
|
|
24
26
|
import logging
|
|
27
|
+
import time
|
|
28
|
+
from datetime import timedelta
|
|
25
29
|
|
|
26
30
|
from opensandbox.exceptions import (
|
|
27
31
|
InvalidArgumentException,
|
|
28
32
|
SandboxException,
|
|
29
33
|
SandboxInternalException,
|
|
34
|
+
SandboxReadyTimeoutException,
|
|
30
35
|
)
|
|
31
36
|
from opensandbox.sandbox import Sandbox
|
|
32
37
|
|
|
@@ -35,6 +40,21 @@ from code_interpreter.services.code import Codes
|
|
|
35
40
|
|
|
36
41
|
logger = logging.getLogger(__name__)
|
|
37
42
|
|
|
43
|
+
DEFAULT_READY_TIMEOUT = timedelta(seconds=30)
|
|
44
|
+
DEFAULT_HEALTH_CHECK_POLLING_INTERVAL = timedelta(milliseconds=200)
|
|
45
|
+
|
|
46
|
+
# Strict health check script: verifies the code interpreter runtime (Jupyter
|
|
47
|
+
# kernel gateway) is actually serving inside the sandbox. execd starts serving
|
|
48
|
+
# /ping before the entrypoint launches Jupyter, and the setup stage may run
|
|
49
|
+
# short-lived "jupyter kernelspec" helpers, so a daemon ping or a process-name
|
|
50
|
+
# grep cannot prove the runtime is ready. Probing the Jupyter listen port
|
|
51
|
+
# (127.0.0.1:${JUPYTER_PORT:-44771}, same default as the entrypoint) only
|
|
52
|
+
# passes once the server accepts connections.
|
|
53
|
+
RUNTIME_PROCESS_CHECK_COMMAND = (
|
|
54
|
+
"bash -c 'exec 3<>/dev/tcp/127.0.0.1/${JUPYTER_PORT:-44771}' "
|
|
55
|
+
"&& exit 0 || exit 1"
|
|
56
|
+
)
|
|
57
|
+
|
|
38
58
|
|
|
39
59
|
class CodeInterpreter:
|
|
40
60
|
"""
|
|
@@ -45,7 +65,7 @@ class CodeInterpreter:
|
|
|
45
65
|
|
|
46
66
|
Key Features:
|
|
47
67
|
|
|
48
|
-
- Multi-language Code Execution:
|
|
68
|
+
- Multi-language Code Execution: Python, JavaScript, TypeScript, Go, Bash
|
|
49
69
|
- Session Management: Persistent execution contexts with variable state
|
|
50
70
|
- Sandbox Integration: Full access to underlying sandbox file system and command execution
|
|
51
71
|
- Streaming Execution: Real-time code execution with output streaming
|
|
@@ -54,11 +74,12 @@ class CodeInterpreter:
|
|
|
54
74
|
Usage Example:
|
|
55
75
|
|
|
56
76
|
```python
|
|
57
|
-
#
|
|
58
|
-
|
|
77
|
+
# The sandbox must use the opensandbox/code-interpreter image (or a
|
|
78
|
+
# derivative); the strict health check requires the Jupyter runtime.
|
|
59
79
|
sandbox = await Sandbox.create(
|
|
60
|
-
"
|
|
61
|
-
|
|
80
|
+
"opensandbox/code-interpreter:latest",
|
|
81
|
+
entrypoint=["/opt/code-interpreter/code-interpreter.sh"],
|
|
82
|
+
resource={"cpu": "1", "memory": "2Gi"},
|
|
62
83
|
)
|
|
63
84
|
|
|
64
85
|
# Then create a code interpreter wrapping the sandbox
|
|
@@ -68,9 +89,10 @@ class CodeInterpreter:
|
|
|
68
89
|
from code_interpreter.models.code import SupportedLanguage
|
|
69
90
|
context = await interpreter.codes.create_context(SupportedLanguage.PYTHON)
|
|
70
91
|
result = await interpreter.codes.run("print('Hello World')", context=context)
|
|
71
|
-
print(result.
|
|
92
|
+
print(result.text) # Output: Hello World
|
|
72
93
|
|
|
73
94
|
# Access underlying sandbox for file operations
|
|
95
|
+
from opensandbox.models.filesystem import WriteEntry
|
|
74
96
|
await interpreter.sandbox.files.write_files([
|
|
75
97
|
WriteEntry(path="data.txt", data="Hello")
|
|
76
98
|
])
|
|
@@ -79,10 +101,16 @@ class CodeInterpreter:
|
|
|
79
101
|
context=context,
|
|
80
102
|
)
|
|
81
103
|
|
|
82
|
-
#
|
|
104
|
+
# Clean up: closing the interpreter releases its HTTP clients; the
|
|
105
|
+
# underlying sandbox lifecycle stays caller-owned.
|
|
106
|
+
await interpreter.aclose()
|
|
83
107
|
await sandbox.kill()
|
|
84
108
|
await sandbox.close()
|
|
85
109
|
```
|
|
110
|
+
|
|
111
|
+
CodeInterpreter also supports the async context-manager protocol; exiting
|
|
112
|
+
the ``async with`` block releases the interpreter's HTTP clients (not the
|
|
113
|
+
sandbox).
|
|
86
114
|
"""
|
|
87
115
|
|
|
88
116
|
def __init__(self, sandbox: Sandbox, code_service: Codes) -> None:
|
|
@@ -142,6 +170,24 @@ class CodeInterpreter:
|
|
|
142
170
|
"""
|
|
143
171
|
return self._sandbox.commands
|
|
144
172
|
|
|
173
|
+
async def aclose(self) -> None:
|
|
174
|
+
"""
|
|
175
|
+
Release resources owned by the code interpreter.
|
|
176
|
+
|
|
177
|
+
Closes the HTTP clients used by the code execution service. The
|
|
178
|
+
underlying sandbox is NOT affected; manage its lifecycle separately
|
|
179
|
+
(``sandbox.kill()`` / ``sandbox.close()``). Safe to call multiple times.
|
|
180
|
+
"""
|
|
181
|
+
aclose = getattr(self._code_service, "aclose", None)
|
|
182
|
+
if aclose is not None:
|
|
183
|
+
await aclose()
|
|
184
|
+
|
|
185
|
+
async def __aenter__(self) -> "CodeInterpreter":
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
189
|
+
await self.aclose()
|
|
190
|
+
|
|
145
191
|
@property
|
|
146
192
|
def metrics(self):
|
|
147
193
|
"""
|
|
@@ -169,14 +215,114 @@ class CodeInterpreter:
|
|
|
169
215
|
"""
|
|
170
216
|
return self._code_service
|
|
171
217
|
|
|
218
|
+
async def ping(self) -> bool:
|
|
219
|
+
"""
|
|
220
|
+
Check if the code execution service (execd) is responsive.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
True if the code execution service is responsive, False otherwise
|
|
224
|
+
"""
|
|
225
|
+
return await self._code_service.ping()
|
|
226
|
+
|
|
227
|
+
async def is_healthy(self) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Check if the code interpreter is healthy (strict check).
|
|
230
|
+
|
|
231
|
+
Healthy means both:
|
|
232
|
+
|
|
233
|
+
- the code execution service (execd) answers ``GET /ping``; and
|
|
234
|
+
- the code interpreter runtime (Jupyter kernel gateway) is serving
|
|
235
|
+
inside the sandbox, verified by probing its listen port through the
|
|
236
|
+
execd command API.
|
|
237
|
+
|
|
238
|
+
Exceptions raised by either leg are treated as unhealthy.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
True if healthy, False otherwise
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
return await self.ping() and await self._is_runtime_process_alive()
|
|
245
|
+
except Exception:
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
async def _is_runtime_process_alive(self) -> bool:
|
|
249
|
+
"""
|
|
250
|
+
Check if the code interpreter runtime (Jupyter) is serving.
|
|
251
|
+
|
|
252
|
+
Probes the runtime listen port through the sandbox command service and
|
|
253
|
+
treats a failed command (non-zero exit surfaced as an execution error)
|
|
254
|
+
as an unhealthy runtime.
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
execution = await self._sandbox.commands.run(
|
|
258
|
+
RUNTIME_PROCESS_CHECK_COMMAND
|
|
259
|
+
)
|
|
260
|
+
return execution.error is None
|
|
261
|
+
except Exception:
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
async def check_ready(
|
|
265
|
+
self,
|
|
266
|
+
timeout: timedelta,
|
|
267
|
+
polling_interval: timedelta,
|
|
268
|
+
) -> None:
|
|
269
|
+
"""
|
|
270
|
+
Wait for the code interpreter to pass the strict health check with polling.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
SandboxReadyTimeoutException: if the health check doesn't pass within timeout
|
|
274
|
+
"""
|
|
275
|
+
logger.info(
|
|
276
|
+
f"Waiting for code interpreter {self.id} to pass health check "
|
|
277
|
+
f"(timeout: {timeout.total_seconds()}s)"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
deadline = time.monotonic() + timeout.total_seconds()
|
|
281
|
+
attempt = 0
|
|
282
|
+
|
|
283
|
+
while time.monotonic() < deadline:
|
|
284
|
+
attempt += 1
|
|
285
|
+
if await self.is_healthy():
|
|
286
|
+
logger.info(
|
|
287
|
+
f"Code interpreter {self.id} passed health check "
|
|
288
|
+
f"after {attempt} attempts"
|
|
289
|
+
)
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
remaining = deadline - time.monotonic()
|
|
293
|
+
if remaining <= 0:
|
|
294
|
+
break
|
|
295
|
+
await asyncio.sleep(min(polling_interval.total_seconds(), remaining))
|
|
296
|
+
|
|
297
|
+
raise SandboxReadyTimeoutException(
|
|
298
|
+
f"Code interpreter {self.id} health check timed out after "
|
|
299
|
+
f"{timeout.total_seconds()}s ({attempt} attempts). The code execution "
|
|
300
|
+
f"service (execd) or the interpreter runtime (Jupyter) did not become "
|
|
301
|
+
f"ready. Pass skip_health_check=True to skip this check."
|
|
302
|
+
)
|
|
303
|
+
|
|
172
304
|
@classmethod
|
|
173
|
-
async def create(
|
|
305
|
+
async def create(
|
|
306
|
+
cls,
|
|
307
|
+
sandbox: Sandbox,
|
|
308
|
+
*,
|
|
309
|
+
ready_timeout: timedelta = DEFAULT_READY_TIMEOUT,
|
|
310
|
+
health_check_polling_interval: timedelta = DEFAULT_HEALTH_CHECK_POLLING_INTERVAL,
|
|
311
|
+
skip_health_check: bool = False,
|
|
312
|
+
) -> "CodeInterpreter":
|
|
174
313
|
"""
|
|
175
314
|
Creates a CodeInterpreter from an existing Sandbox instance.
|
|
176
315
|
|
|
177
316
|
This factory method handles the creation and initialization of CodeInterpreter
|
|
178
317
|
services, including the code execution service and language configuration.
|
|
179
318
|
|
|
319
|
+
By default a strict health check runs before the interpreter is returned:
|
|
320
|
+
the code execution service (execd) must answer ``GET /ping`` AND the
|
|
321
|
+
code interpreter runtime (Jupyter kernel gateway) must be serving,
|
|
322
|
+
both within ``ready_timeout``. execd starts serving before the runtime
|
|
323
|
+
launches, so the daemon ping alone is not enough. Set
|
|
324
|
+
``skip_health_check=True`` to opt out.
|
|
325
|
+
|
|
180
326
|
CodeInterpreter must be created by wrapping an existing Sandbox instance with
|
|
181
327
|
code execution capabilities. This design ensures clear separation of concerns:
|
|
182
328
|
- Sandbox handles infrastructure (containers, resources, networking)
|
|
@@ -184,6 +330,10 @@ class CodeInterpreter:
|
|
|
184
330
|
|
|
185
331
|
Args:
|
|
186
332
|
sandbox: Existing sandbox instance to wrap with code execution capabilities
|
|
333
|
+
ready_timeout: Maximum time to wait for the code execution service health check
|
|
334
|
+
health_check_polling_interval: Time between health check attempts
|
|
335
|
+
skip_health_check: If True, do not wait for the code execution service
|
|
336
|
+
to become ready; the returned interpreter may fail on first use
|
|
187
337
|
|
|
188
338
|
Returns:
|
|
189
339
|
CodeInterpreter instance wrapping the sandbox
|
|
@@ -191,24 +341,41 @@ class CodeInterpreter:
|
|
|
191
341
|
Raises:
|
|
192
342
|
InvalidArgumentException: If sandbox is not provided
|
|
193
343
|
SandboxException: If creation fails
|
|
344
|
+
SandboxReadyTimeoutException: If the code execution service health check
|
|
345
|
+
times out and ``skip_health_check`` is False
|
|
194
346
|
SandboxInternalException: If internal service initialization fails
|
|
195
347
|
"""
|
|
196
348
|
if sandbox is None:
|
|
197
349
|
raise InvalidArgumentException("Sandbox instance must be provided")
|
|
198
350
|
|
|
199
|
-
logger.info("Creating code interpreter from sandbox:
|
|
351
|
+
logger.info(f"Creating code interpreter from sandbox: {sandbox.id}")
|
|
200
352
|
|
|
201
353
|
factory = AdapterFactory(sandbox.connection_config)
|
|
202
354
|
|
|
203
355
|
try:
|
|
204
356
|
# Connect to the execd daemon endpoint for code execution services
|
|
205
357
|
from opensandbox.constants import DEFAULT_EXECD_PORT
|
|
358
|
+
|
|
206
359
|
code_interpreter_endpoint = await sandbox.get_endpoint(DEFAULT_EXECD_PORT)
|
|
207
|
-
code_execution_service = factory.create_code_execution_service(
|
|
360
|
+
code_execution_service = factory.create_code_execution_service(
|
|
361
|
+
code_interpreter_endpoint
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
interpreter = cls(sandbox, code_execution_service)
|
|
365
|
+
try:
|
|
366
|
+
if not skip_health_check:
|
|
367
|
+
await interpreter.check_ready(
|
|
368
|
+
ready_timeout, health_check_polling_interval
|
|
369
|
+
)
|
|
370
|
+
except Exception:
|
|
371
|
+
# Don't leak the service's clients on a failed creation.
|
|
372
|
+
with contextlib.suppress(Exception):
|
|
373
|
+
await interpreter.aclose()
|
|
374
|
+
raise
|
|
208
375
|
|
|
209
|
-
logger.info("Code interpreter
|
|
376
|
+
logger.info(f"Code interpreter {sandbox.id} created successfully")
|
|
210
377
|
|
|
211
|
-
return
|
|
378
|
+
return interpreter
|
|
212
379
|
except Exception as e:
|
|
213
380
|
if isinstance(e, SandboxException):
|
|
214
381
|
raise
|
code_interpreter/models/code.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025
|
|
2
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
@@ -27,6 +27,7 @@ class SupportedLanguageSync:
|
|
|
27
27
|
GO = "go"
|
|
28
28
|
TYPESCRIPT = "typescript"
|
|
29
29
|
BASH = "bash"
|
|
30
|
+
JAVASCRIPT = "javascript"
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class CodeContextSync(BaseModel):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025
|
|
2
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
@@ -38,10 +38,10 @@ class Codes(Protocol):
|
|
|
38
38
|
Supported Languages:
|
|
39
39
|
|
|
40
40
|
- Python: Full Python 3.x support with package management
|
|
41
|
-
- JavaScript
|
|
41
|
+
- JavaScript: ES6+ with npm package support
|
|
42
|
+
- TypeScript
|
|
43
|
+
- Go
|
|
42
44
|
- Bash: Shell scripting with full system access
|
|
43
|
-
- Java: Compilation and execution with classpath management
|
|
44
|
-
- Kotlin: Script and compiled Kotlin execution
|
|
45
45
|
|
|
46
46
|
Key Features:
|
|
47
47
|
|
|
@@ -212,3 +212,17 @@ class Codes(Protocol):
|
|
|
212
212
|
SandboxException: If interruption fails
|
|
213
213
|
"""
|
|
214
214
|
...
|
|
215
|
+
|
|
216
|
+
async def ping(self) -> bool:
|
|
217
|
+
"""
|
|
218
|
+
Check if the code execution service (execd) is alive.
|
|
219
|
+
|
|
220
|
+
The ping targets the execd daemon endpoint used by this service
|
|
221
|
+
(``GET /ping``). It does not verify that a specific language runtime
|
|
222
|
+
is ready, only that the daemon serving code execution requests is
|
|
223
|
+
responsive.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if the code execution service is responsive, False otherwise
|
|
227
|
+
"""
|
|
228
|
+
...
|