abox-code-interpreter 0.1.0__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.
- abox_code_interpreter-0.1.0.dist-info/METADATA +39 -0
- abox_code_interpreter-0.1.0.dist-info/RECORD +12 -0
- abox_code_interpreter-0.1.0.dist-info/WHEEL +4 -0
- abox_code_interpreter-0.1.0.dist-info/licenses/LICENSE +10 -0
- abox_code_interpreter-0.1.0.dist-info/licenses/NOTICE +1 -0
- agentbox_code_interpreter/__init__.py +14 -0
- agentbox_code_interpreter/charts.py +232 -0
- agentbox_code_interpreter/code_interpreter_async.py +419 -0
- agentbox_code_interpreter/code_interpreter_sync.py +415 -0
- agentbox_code_interpreter/constants.py +3 -0
- agentbox_code_interpreter/exceptions.py +20 -0
- agentbox_code_interpreter/models.py +523 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import httpx
|
|
3
|
+
|
|
4
|
+
from typing import cast, Optional, Dict, overload, Union, List
|
|
5
|
+
from httpx import Client
|
|
6
|
+
from agentbox import Sandbox as BaseSandbox, InvalidArgumentException
|
|
7
|
+
from agentbox.api.client_sync import get_transport
|
|
8
|
+
|
|
9
|
+
from agentbox_code_interpreter.constants import (
|
|
10
|
+
DEFAULT_TEMPLATE,
|
|
11
|
+
JUPYTER_PORT,
|
|
12
|
+
DEFAULT_TIMEOUT,
|
|
13
|
+
)
|
|
14
|
+
from agentbox_code_interpreter.models import (
|
|
15
|
+
ExecutionError,
|
|
16
|
+
Execution,
|
|
17
|
+
RunCodeLanguage,
|
|
18
|
+
Context,
|
|
19
|
+
Result,
|
|
20
|
+
extract_exception,
|
|
21
|
+
parse_output,
|
|
22
|
+
OutputHandler,
|
|
23
|
+
OutputMessage,
|
|
24
|
+
)
|
|
25
|
+
from agentbox_code_interpreter.exceptions import (
|
|
26
|
+
format_execution_timeout_error,
|
|
27
|
+
format_request_timeout_error,
|
|
28
|
+
format_sandbox_killed_error,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Sandbox(BaseSandbox):
|
|
35
|
+
"""
|
|
36
|
+
AgentBox cloud sandbox is a secure and isolated cloud environment.
|
|
37
|
+
|
|
38
|
+
The sandbox allows you to:
|
|
39
|
+
- Access Linux OS
|
|
40
|
+
- Create, list, and delete files and directories
|
|
41
|
+
- Run commands
|
|
42
|
+
- Run isolated code
|
|
43
|
+
- Access the internet
|
|
44
|
+
|
|
45
|
+
See the [Code Interpreter guide](https://docs.agentbox.ru/en/sdk/code-interpreter/).
|
|
46
|
+
|
|
47
|
+
Use the `Sandbox.create()` to create a new sandbox.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
```python
|
|
51
|
+
from agentbox_code_interpreter import Sandbox
|
|
52
|
+
|
|
53
|
+
sandbox = Sandbox.create()
|
|
54
|
+
```
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
default_template = DEFAULT_TEMPLATE
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def _jupyter_url(self) -> str:
|
|
61
|
+
# Honors the `sandbox_url` option and the `AGENTBOX_SANDBOX_URL` environment
|
|
62
|
+
# variable, same as the base SDK does for envd requests.
|
|
63
|
+
sandbox_url = cast(Optional[str], self.connection_config._sandbox_url)
|
|
64
|
+
if sandbox_url:
|
|
65
|
+
return sandbox_url
|
|
66
|
+
return f"{'http' if self.connection_config.debug else 'https'}://{self.get_host(JUPYTER_PORT)}"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def _client(self) -> Client:
|
|
70
|
+
# TODO: Remove later
|
|
71
|
+
# Use a dedicated HTTP/1.1 transport for Jupyter requests.
|
|
72
|
+
#
|
|
73
|
+
# The base SDK's shared transport now defaults to http2=True. With
|
|
74
|
+
# HTTP/2, multiple requests are multiplexed over a single TCP
|
|
75
|
+
# connection, so when a client cancels a request (e.g. the caller
|
|
76
|
+
# disconnects from the streaming `/execute` endpoint) the server
|
|
77
|
+
# may not detect the disconnect: only the HTTP/2 stream is
|
|
78
|
+
# cancelled, the underlying TCP connection stays open.
|
|
79
|
+
#
|
|
80
|
+
# Forcing HTTP/1.1 here keeps the 1:1 mapping between TCP
|
|
81
|
+
# connection and request, so client disconnects propagate to the
|
|
82
|
+
# server as a TCP close and long-running executions can be
|
|
83
|
+
# cancelled reliably.
|
|
84
|
+
return Client(transport=get_transport(self.connection_config, http2=False))
|
|
85
|
+
|
|
86
|
+
def _handle_connection_error(self, err: Exception) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Raises a descriptive exception if the connection error was caused by
|
|
89
|
+
the sandbox being killed mid-request. If the sandbox is still running
|
|
90
|
+
(or its state can't be determined), returns so the caller can re-raise
|
|
91
|
+
the original error.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
running = self.is_running()
|
|
95
|
+
except Exception:
|
|
96
|
+
# The state check itself failed, so we can't tell whether the
|
|
97
|
+
# sandbox was killed — let the caller re-raise the original error
|
|
98
|
+
# instead of wrongly claiming the sandbox is gone.
|
|
99
|
+
return
|
|
100
|
+
if not running:
|
|
101
|
+
raise format_sandbox_killed_error() from err
|
|
102
|
+
|
|
103
|
+
@overload
|
|
104
|
+
def run_code(
|
|
105
|
+
self,
|
|
106
|
+
code: str,
|
|
107
|
+
language: Optional[RunCodeLanguage] = None,
|
|
108
|
+
on_stdout: Optional[OutputHandler[OutputMessage]] = None,
|
|
109
|
+
on_stderr: Optional[OutputHandler[OutputMessage]] = None,
|
|
110
|
+
on_result: Optional[OutputHandler[Result]] = None,
|
|
111
|
+
on_error: Optional[OutputHandler[ExecutionError]] = None,
|
|
112
|
+
envs: Optional[Dict[str, str]] = None,
|
|
113
|
+
timeout: Optional[float] = None,
|
|
114
|
+
request_timeout: Optional[float] = None,
|
|
115
|
+
) -> Execution:
|
|
116
|
+
"""
|
|
117
|
+
Runs the code for the specified language.
|
|
118
|
+
|
|
119
|
+
Specify the `language` or `context` option to run the code as a different language or in a different `Context`.
|
|
120
|
+
If no language is specified, Python is used.
|
|
121
|
+
|
|
122
|
+
You can reference previously defined variables, imports, and functions in the code.
|
|
123
|
+
|
|
124
|
+
:param code: Code to execute
|
|
125
|
+
:param language: Language to use for code execution. If not defined, the default Python context is used.
|
|
126
|
+
:param on_stdout: Callback for stdout messages
|
|
127
|
+
:param on_stderr: Callback for stderr messages
|
|
128
|
+
:param on_result: Callback for the `Result` object
|
|
129
|
+
:param on_error: Callback for the `ExecutionError` object
|
|
130
|
+
:param envs: Custom environment variables
|
|
131
|
+
:param timeout: Timeout for the code execution in **seconds**
|
|
132
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
133
|
+
|
|
134
|
+
:return: `Execution` result object
|
|
135
|
+
"""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
@overload
|
|
139
|
+
def run_code(
|
|
140
|
+
self,
|
|
141
|
+
code: str,
|
|
142
|
+
context: Optional[Context] = None,
|
|
143
|
+
on_stdout: Optional[OutputHandler[OutputMessage]] = None,
|
|
144
|
+
on_stderr: Optional[OutputHandler[OutputMessage]] = None,
|
|
145
|
+
on_result: Optional[OutputHandler[Result]] = None,
|
|
146
|
+
on_error: Optional[OutputHandler[ExecutionError]] = None,
|
|
147
|
+
envs: Optional[Dict[str, str]] = None,
|
|
148
|
+
timeout: Optional[float] = None,
|
|
149
|
+
request_timeout: Optional[float] = None,
|
|
150
|
+
) -> Execution:
|
|
151
|
+
"""
|
|
152
|
+
Runs the code in the specified context, if not specified, the default context is used.
|
|
153
|
+
|
|
154
|
+
Specify the `language` or `context` option to run the code as a different language or in a different `Context`.
|
|
155
|
+
|
|
156
|
+
You can reference previously defined variables, imports, and functions in the code.
|
|
157
|
+
|
|
158
|
+
:param code: Code to execute
|
|
159
|
+
:param context: Concrete context to run the code in. If not specified, the default context for the language is used. It's mutually exclusive with the language.
|
|
160
|
+
:param on_stdout: Callback for stdout messages
|
|
161
|
+
:param on_stderr: Callback for stderr messages
|
|
162
|
+
:param on_result: Callback for the `Result` object
|
|
163
|
+
:param on_error: Callback for the `ExecutionError` object
|
|
164
|
+
:param envs: Custom environment variables
|
|
165
|
+
:param timeout: Timeout for the code execution in **seconds**
|
|
166
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
167
|
+
|
|
168
|
+
:return: `Execution` result object
|
|
169
|
+
"""
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
def run_code(
|
|
173
|
+
self,
|
|
174
|
+
code: str,
|
|
175
|
+
language: Optional[str] = None,
|
|
176
|
+
context: Optional[Context] = None,
|
|
177
|
+
on_stdout: Optional[OutputHandler[OutputMessage]] = None,
|
|
178
|
+
on_stderr: Optional[OutputHandler[OutputMessage]] = None,
|
|
179
|
+
on_result: Optional[OutputHandler[Result]] = None,
|
|
180
|
+
on_error: Optional[OutputHandler[ExecutionError]] = None,
|
|
181
|
+
envs: Optional[Dict[str, str]] = None,
|
|
182
|
+
timeout: Optional[float] = None,
|
|
183
|
+
request_timeout: Optional[float] = None,
|
|
184
|
+
) -> Execution:
|
|
185
|
+
logger.debug(f"Executing code {code}")
|
|
186
|
+
|
|
187
|
+
if language and context:
|
|
188
|
+
raise InvalidArgumentException(
|
|
189
|
+
"You can provide context or language, but not both at the same time."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
timeout = None if timeout == 0 else (timeout or DEFAULT_TIMEOUT)
|
|
193
|
+
request_timeout = request_timeout or self.connection_config.request_timeout
|
|
194
|
+
context_id = context.id if context else None
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
headers: Dict[str, str] = {
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
"Agentbox-Sandbox-Id": self.sandbox_id,
|
|
200
|
+
"Agentbox-Sandbox-Port": str(JUPYTER_PORT),
|
|
201
|
+
}
|
|
202
|
+
if self._envd_access_token:
|
|
203
|
+
headers["X-Access-Token"] = self._envd_access_token
|
|
204
|
+
if self.traffic_access_token:
|
|
205
|
+
headers["Agentbox-Traffic-Access-Token"] = self.traffic_access_token
|
|
206
|
+
|
|
207
|
+
with self._client.stream(
|
|
208
|
+
"POST",
|
|
209
|
+
f"{self._jupyter_url}/execute",
|
|
210
|
+
json={
|
|
211
|
+
"code": code,
|
|
212
|
+
"context_id": context_id,
|
|
213
|
+
"language": language,
|
|
214
|
+
"env_vars": envs,
|
|
215
|
+
},
|
|
216
|
+
headers=headers,
|
|
217
|
+
# `timeout` bounds the execution, `request_timeout` only the
|
|
218
|
+
# connect. Every non-connect phase must carry `timeout`: the
|
|
219
|
+
# SDK's pyqwest-backed transport collapses the per-phase
|
|
220
|
+
# timeouts into a single whole-request deadline and takes the
|
|
221
|
+
# longest of them, so leaving `request_timeout` on the write
|
|
222
|
+
# and pool phases would raise the floor to
|
|
223
|
+
# `max(timeout, request_timeout)` and silently ignore any
|
|
224
|
+
# `timeout` shorter than it. This matches the JS SDK, which
|
|
225
|
+
# aborts the execution on a `timeout`-long timer.
|
|
226
|
+
# `timeout=0` disables the deadline entirely; the transport
|
|
227
|
+
# still bounds connect on its own.
|
|
228
|
+
timeout=(
|
|
229
|
+
httpx.Timeout(timeout, connect=request_timeout)
|
|
230
|
+
if timeout is not None
|
|
231
|
+
else httpx.Timeout(None)
|
|
232
|
+
),
|
|
233
|
+
) as response:
|
|
234
|
+
err = extract_exception(response)
|
|
235
|
+
if err:
|
|
236
|
+
raise err
|
|
237
|
+
|
|
238
|
+
execution = Execution()
|
|
239
|
+
|
|
240
|
+
for line in response.iter_lines():
|
|
241
|
+
parse_output(
|
|
242
|
+
execution,
|
|
243
|
+
line,
|
|
244
|
+
on_stdout=on_stdout,
|
|
245
|
+
on_stderr=on_stderr,
|
|
246
|
+
on_result=on_result,
|
|
247
|
+
on_error=on_error,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return execution
|
|
251
|
+
except httpx.ReadTimeout:
|
|
252
|
+
raise format_execution_timeout_error()
|
|
253
|
+
except httpx.TimeoutException:
|
|
254
|
+
raise format_request_timeout_error()
|
|
255
|
+
except (httpx.ReadError, httpx.RemoteProtocolError) as err:
|
|
256
|
+
self._handle_connection_error(err)
|
|
257
|
+
raise
|
|
258
|
+
|
|
259
|
+
def create_code_context(
|
|
260
|
+
self,
|
|
261
|
+
cwd: Optional[str] = None,
|
|
262
|
+
language: Optional[RunCodeLanguage] = None,
|
|
263
|
+
request_timeout: Optional[float] = None,
|
|
264
|
+
) -> Context:
|
|
265
|
+
"""
|
|
266
|
+
Creates a new context to run code in.
|
|
267
|
+
|
|
268
|
+
:param cwd: Set the current working directory for the context, defaults to `/home/user`
|
|
269
|
+
:param language: Language of the context. If not specified, defaults to Python
|
|
270
|
+
:param request_timeout: Timeout for the request in **milliseconds**
|
|
271
|
+
|
|
272
|
+
:return: Context object
|
|
273
|
+
"""
|
|
274
|
+
logger.debug(f"Creating new {language} context")
|
|
275
|
+
|
|
276
|
+
data = {}
|
|
277
|
+
if language:
|
|
278
|
+
data["language"] = language
|
|
279
|
+
if cwd:
|
|
280
|
+
data["cwd"] = cwd
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
headers: Dict[str, str] = {
|
|
284
|
+
"Content-Type": "application/json",
|
|
285
|
+
"Agentbox-Sandbox-Id": self.sandbox_id,
|
|
286
|
+
"Agentbox-Sandbox-Port": str(JUPYTER_PORT),
|
|
287
|
+
}
|
|
288
|
+
if self.traffic_access_token:
|
|
289
|
+
headers["Agentbox-Traffic-Access-Token"] = self.traffic_access_token
|
|
290
|
+
|
|
291
|
+
response = self._client.post(
|
|
292
|
+
f"{self._jupyter_url}/contexts",
|
|
293
|
+
json=data,
|
|
294
|
+
headers=headers,
|
|
295
|
+
timeout=request_timeout or self.connection_config.request_timeout,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
err = extract_exception(response)
|
|
299
|
+
if err:
|
|
300
|
+
raise err
|
|
301
|
+
|
|
302
|
+
data = response.json()
|
|
303
|
+
return Context.from_json(data)
|
|
304
|
+
except httpx.TimeoutException:
|
|
305
|
+
raise format_request_timeout_error()
|
|
306
|
+
except (httpx.ReadError, httpx.RemoteProtocolError) as err:
|
|
307
|
+
self._handle_connection_error(err)
|
|
308
|
+
raise
|
|
309
|
+
|
|
310
|
+
def remove_code_context(
|
|
311
|
+
self,
|
|
312
|
+
context: Union[Context, str],
|
|
313
|
+
) -> None:
|
|
314
|
+
"""
|
|
315
|
+
Removes a context.
|
|
316
|
+
|
|
317
|
+
:param context: Context to remove. Can be a Context object or a context ID string.
|
|
318
|
+
|
|
319
|
+
:return: None
|
|
320
|
+
"""
|
|
321
|
+
context_id = context.id if isinstance(context, Context) else context
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
headers: Dict[str, str] = {
|
|
325
|
+
"Content-Type": "application/json",
|
|
326
|
+
"Agentbox-Sandbox-Id": self.sandbox_id,
|
|
327
|
+
"Agentbox-Sandbox-Port": str(JUPYTER_PORT),
|
|
328
|
+
}
|
|
329
|
+
if self.traffic_access_token:
|
|
330
|
+
headers["Agentbox-Traffic-Access-Token"] = self.traffic_access_token
|
|
331
|
+
|
|
332
|
+
response = self._client.delete(
|
|
333
|
+
f"{self._jupyter_url}/contexts/{context_id}",
|
|
334
|
+
headers=headers,
|
|
335
|
+
timeout=self.connection_config.request_timeout,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
err = extract_exception(response)
|
|
339
|
+
if err:
|
|
340
|
+
raise err
|
|
341
|
+
except httpx.TimeoutException:
|
|
342
|
+
raise format_request_timeout_error()
|
|
343
|
+
except (httpx.ReadError, httpx.RemoteProtocolError) as err:
|
|
344
|
+
self._handle_connection_error(err)
|
|
345
|
+
raise
|
|
346
|
+
|
|
347
|
+
def list_code_contexts(self) -> List[Context]:
|
|
348
|
+
"""
|
|
349
|
+
List all contexts.
|
|
350
|
+
|
|
351
|
+
:return: List of contexts.
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
headers: Dict[str, str] = {
|
|
355
|
+
"Content-Type": "application/json",
|
|
356
|
+
"Agentbox-Sandbox-Id": self.sandbox_id,
|
|
357
|
+
"Agentbox-Sandbox-Port": str(JUPYTER_PORT),
|
|
358
|
+
}
|
|
359
|
+
if self.traffic_access_token:
|
|
360
|
+
headers["Agentbox-Traffic-Access-Token"] = self.traffic_access_token
|
|
361
|
+
|
|
362
|
+
response = self._client.get(
|
|
363
|
+
f"{self._jupyter_url}/contexts",
|
|
364
|
+
headers=headers,
|
|
365
|
+
timeout=self.connection_config.request_timeout,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
err = extract_exception(response)
|
|
369
|
+
if err:
|
|
370
|
+
raise err
|
|
371
|
+
|
|
372
|
+
data = response.json()
|
|
373
|
+
return [Context.from_json(context_data) for context_data in data]
|
|
374
|
+
except httpx.TimeoutException:
|
|
375
|
+
raise format_request_timeout_error()
|
|
376
|
+
except (httpx.ReadError, httpx.RemoteProtocolError) as err:
|
|
377
|
+
self._handle_connection_error(err)
|
|
378
|
+
raise
|
|
379
|
+
|
|
380
|
+
def restart_code_context(
|
|
381
|
+
self,
|
|
382
|
+
context: Union[Context, str],
|
|
383
|
+
) -> None:
|
|
384
|
+
"""
|
|
385
|
+
Restart a context.
|
|
386
|
+
|
|
387
|
+
:param context: Context to restart. Can be a Context object or a context ID string.
|
|
388
|
+
|
|
389
|
+
:return: None
|
|
390
|
+
"""
|
|
391
|
+
context_id = context.id if isinstance(context, Context) else context
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
headers: Dict[str, str] = {
|
|
395
|
+
"Content-Type": "application/json",
|
|
396
|
+
"Agentbox-Sandbox-Id": self.sandbox_id,
|
|
397
|
+
"Agentbox-Sandbox-Port": str(JUPYTER_PORT),
|
|
398
|
+
}
|
|
399
|
+
if self.traffic_access_token:
|
|
400
|
+
headers["Agentbox-Traffic-Access-Token"] = self.traffic_access_token
|
|
401
|
+
|
|
402
|
+
response = self._client.post(
|
|
403
|
+
f"{self._jupyter_url}/contexts/{context_id}/restart",
|
|
404
|
+
headers=headers,
|
|
405
|
+
timeout=self.connection_config.request_timeout,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
err = extract_exception(response)
|
|
409
|
+
if err:
|
|
410
|
+
raise err
|
|
411
|
+
except httpx.TimeoutException:
|
|
412
|
+
raise format_request_timeout_error()
|
|
413
|
+
except (httpx.ReadError, httpx.RemoteProtocolError) as err:
|
|
414
|
+
self._handle_connection_error(err)
|
|
415
|
+
raise
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from agentbox import TimeoutException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def format_request_timeout_error() -> Exception:
|
|
5
|
+
return TimeoutException(
|
|
6
|
+
"Request timed out — the 'request_timeout' option can be used to increase this timeout",
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def format_execution_timeout_error() -> Exception:
|
|
11
|
+
return TimeoutException(
|
|
12
|
+
"Execution timed out — the 'timeout' option can be used to increase this timeout",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def format_sandbox_killed_error() -> Exception:
|
|
17
|
+
return TimeoutException(
|
|
18
|
+
"The sandbox was killed while the request was in progress. This can happen when the sandbox times out or is killed manually. "
|
|
19
|
+
"You can modify the sandbox timeout by passing 'timeout' when starting the sandbox or calling '.set_timeout' on the sandbox with the desired timeout",
|
|
20
|
+
)
|