tinyagent-py 0.0.8__py3-none-any.whl → 0.0.11__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.
- tinyagent/__init__.py +2 -1
- tinyagent/code_agent/__init__.py +12 -0
- tinyagent/code_agent/example.py +176 -0
- tinyagent/code_agent/helper.py +173 -0
- tinyagent/code_agent/modal_sandbox.py +478 -0
- tinyagent/code_agent/providers/__init__.py +4 -0
- tinyagent/code_agent/providers/base.py +152 -0
- tinyagent/code_agent/providers/modal_provider.py +202 -0
- tinyagent/code_agent/tiny_code_agent.py +573 -0
- tinyagent/code_agent/tools/__init__.py +3 -0
- tinyagent/code_agent/tools/example_tools.py +41 -0
- tinyagent/code_agent/utils.py +120 -0
- tinyagent/hooks/__init__.py +2 -1
- tinyagent/prompts/code_agent.yaml +329 -0
- {tinyagent_py-0.0.8.dist-info → tinyagent_py-0.0.11.dist-info}/METADATA +138 -5
- tinyagent_py-0.0.11.dist-info/RECORD +32 -0
- tinyagent_py-0.0.8.dist-info/RECORD +0 -20
- {tinyagent_py-0.0.8.dist-info → tinyagent_py-0.0.11.dist-info}/WHEEL +0 -0
- {tinyagent_py-0.0.8.dist-info → tinyagent_py-0.0.11.dist-info}/licenses/LICENSE +0 -0
- {tinyagent_py-0.0.8.dist-info → tinyagent_py-0.0.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,478 @@
|
|
1
|
+
# TinyAgent Modal sandbox utilities
|
2
|
+
# ---------------------------------
|
3
|
+
|
4
|
+
from __future__ import annotations
|
5
|
+
|
6
|
+
import inspect
|
7
|
+
import json
|
8
|
+
import os
|
9
|
+
import sys
|
10
|
+
from textwrap import dedent
|
11
|
+
from typing import Any, Iterable, Sequence, Tuple
|
12
|
+
|
13
|
+
import modal
|
14
|
+
from modal.stream_type import StreamType
|
15
|
+
|
16
|
+
# ---------------------------------------------------------------------------
|
17
|
+
# Configuration
|
18
|
+
# ---------------------------------------------------------------------------
|
19
|
+
|
20
|
+
# By default we use the interpreter version running this code.
|
21
|
+
DEFAULT_PYTHON_VERSION: str = f"{sys.version_info.major}.{sys.version_info.minor}"
|
22
|
+
PYTHON_VERSION: str = os.getenv("TINYAGENT_PYTHON_VERSION", DEFAULT_PYTHON_VERSION)
|
23
|
+
|
24
|
+
# Simple ANSI colour helper for interactive feedback
|
25
|
+
COLOR = {
|
26
|
+
"HEADER": "\033[95m",
|
27
|
+
"BLUE": "\033[94m",
|
28
|
+
"GREEN": "\033[92m",
|
29
|
+
"RED": "\033[91m",
|
30
|
+
"ENDC": "\033[0m",
|
31
|
+
}
|
32
|
+
|
33
|
+
# ---------------------------------------------------------------------------
|
34
|
+
# Small utilities
|
35
|
+
# ---------------------------------------------------------------------------
|
36
|
+
|
37
|
+
def get_source(obj: Any) -> str:
|
38
|
+
"""Return a *dedented* string with the source code of *obj*.
|
39
|
+
|
40
|
+
Raises ValueError if the source cannot be inspected. A dedicated utility
|
41
|
+
avoids a hard dependency on the public `inspect.getsource` semantics
|
42
|
+
scattered throughout the rest of the library.
|
43
|
+
"""
|
44
|
+
try:
|
45
|
+
return dedent(inspect.getsource(obj))
|
46
|
+
except (OSError, TypeError) as exc: # pragma: no cover – environment specific
|
47
|
+
raise ValueError(f"Unable to retrieve source for {obj!r}") from exc
|
48
|
+
|
49
|
+
|
50
|
+
# ---------------------------------------------------------------------------
|
51
|
+
# Sandbox creation helpers
|
52
|
+
# ---------------------------------------------------------------------------
|
53
|
+
|
54
|
+
def create_sandbox(
|
55
|
+
modal_secrets: modal.Secret, # Same semantics as the demo util
|
56
|
+
*,
|
57
|
+
timeout: int = 5 * 60,
|
58
|
+
volumes: dict | None = None,
|
59
|
+
workdir: str | None = None,
|
60
|
+
python_version: str | None = None,
|
61
|
+
apt_packages: Sequence[str] | None = None,
|
62
|
+
default_packages: Sequence[str] | None = None,
|
63
|
+
pip_install: Sequence[str] | None = None,
|
64
|
+
image_name: str = "tinyagent-sandbox-image",
|
65
|
+
app_name: str = "persistent-code-session",
|
66
|
+
**sandbox_kwargs,
|
67
|
+
) -> Tuple[modal.Sandbox, modal.App]:
|
68
|
+
"""Create (or lookup) a `modal.Sandbox` pre-configured for code execution.
|
69
|
+
|
70
|
+
The parameters largely bubble up to the underlying *modal* API while
|
71
|
+
providing TinyAgent-friendly defaults. Developers can override any aspect
|
72
|
+
of the environment without patching library internals.
|
73
|
+
"""
|
74
|
+
|
75
|
+
# Resolve defaults ------------------------------------------------------
|
76
|
+
python_version = python_version or PYTHON_VERSION
|
77
|
+
|
78
|
+
if apt_packages is None:
|
79
|
+
# Always install the basics required for most workflows
|
80
|
+
apt_packages = ("git", "curl", "nodejs", "npm")
|
81
|
+
|
82
|
+
if default_packages is None:
|
83
|
+
default_packages = (
|
84
|
+
"asyncpg>=0.27.0",
|
85
|
+
"aiosqlite>=0.18.0",
|
86
|
+
"gradio>=3.50.0",
|
87
|
+
"jinja2",
|
88
|
+
"pyyaml",
|
89
|
+
"cloudpickle",
|
90
|
+
"modal",
|
91
|
+
"nest-asyncio",
|
92
|
+
"mcp[cli]",
|
93
|
+
"PyGithub",
|
94
|
+
"fastmcp",
|
95
|
+
"gitingest",
|
96
|
+
)
|
97
|
+
|
98
|
+
full_pip_list = set(default_packages).union(pip_install or [])
|
99
|
+
|
100
|
+
# Build image -----------------------------------------------------------
|
101
|
+
agent_image = (
|
102
|
+
modal.Image.debian_slim(python_version=python_version)
|
103
|
+
.apt_install(*apt_packages)
|
104
|
+
.pip_install(*full_pip_list)
|
105
|
+
)
|
106
|
+
|
107
|
+
# Re-use a named Modal app so subsequent calls share cached state / layers
|
108
|
+
app = modal.App.lookup(app_name, create_if_missing=True)
|
109
|
+
|
110
|
+
with modal.enable_output():
|
111
|
+
sandbox = modal.Sandbox.create(
|
112
|
+
image=agent_image,
|
113
|
+
timeout=timeout,
|
114
|
+
app=app,
|
115
|
+
volumes=volumes or {},
|
116
|
+
workdir=workdir,
|
117
|
+
secrets=[modal_secrets],
|
118
|
+
**sandbox_kwargs,
|
119
|
+
)
|
120
|
+
|
121
|
+
return sandbox, app
|
122
|
+
|
123
|
+
|
124
|
+
# ---------------------------------------------------------------------------
|
125
|
+
# Streaming execution helpers
|
126
|
+
# ---------------------------------------------------------------------------
|
127
|
+
|
128
|
+
|
129
|
+
def _pretty(header: str, text: str) -> None: # Convenience printing wrapper
|
130
|
+
print(f"{COLOR[header]}{text}{COLOR['ENDC']}")
|
131
|
+
|
132
|
+
|
133
|
+
def run_streaming(
|
134
|
+
command: Sequence[str],
|
135
|
+
sb: modal.Sandbox,
|
136
|
+
*,
|
137
|
+
prefix: str = "📦",
|
138
|
+
) -> Tuple[str, str]:
|
139
|
+
"""Run *command* inside *sb* streaming stdout/stderr in real-time."""
|
140
|
+
|
141
|
+
_pretty("HEADER", f"{prefix}: Running in sandbox")
|
142
|
+
_pretty("GREEN", " ".join(map(str, command)))
|
143
|
+
|
144
|
+
exc = sb.exec(
|
145
|
+
*command,
|
146
|
+
stdout=StreamType.PIPE,
|
147
|
+
stderr=StreamType.PIPE,
|
148
|
+
)
|
149
|
+
|
150
|
+
stdout_lines: list[str] = []
|
151
|
+
stderr_lines: list[str] = []
|
152
|
+
|
153
|
+
# Forward STDOUT lines as they arrive
|
154
|
+
try:
|
155
|
+
for line in exc.stdout: # type: ignore[attr-defined]
|
156
|
+
print(f"{COLOR['BLUE']}OUT: {line.rstrip()}{COLOR['ENDC']}")
|
157
|
+
stdout_lines.append(line)
|
158
|
+
except Exception as e: # noqa: BLE001
|
159
|
+
_pretty("RED", f"Error during stdout streaming: {e}")
|
160
|
+
|
161
|
+
# Forward STDERR after stdout stream completes
|
162
|
+
try:
|
163
|
+
for line in exc.stderr: # type: ignore[attr-defined]
|
164
|
+
print(f"{COLOR['RED']}ERR: {line.rstrip()}{COLOR['ENDC']}")
|
165
|
+
stderr_lines.append(line)
|
166
|
+
except Exception as e: # noqa: BLE001
|
167
|
+
_pretty("RED", f"Error during stderr streaming: {e}")
|
168
|
+
|
169
|
+
exc.wait()
|
170
|
+
if exc.returncode != 0:
|
171
|
+
_pretty("HEADER", f"{prefix}: Failed with exitcode {exc.returncode}")
|
172
|
+
else:
|
173
|
+
_pretty("GREEN", f"{prefix}: Completed successfully")
|
174
|
+
|
175
|
+
return "".join(stdout_lines), "".join(stderr_lines)
|
176
|
+
|
177
|
+
|
178
|
+
# Specialisation for quick *python ‑c* jobs ------------------------------
|
179
|
+
|
180
|
+
def run_streaming_python(code: str, sb: modal.Sandbox) -> Tuple[str, str]:
|
181
|
+
"""Convenience wrapper to execute *code* via `python -c` with printing flush."""
|
182
|
+
|
183
|
+
full_code = "from functools import partial\nprint = partial(print, flush=True)\n" + code
|
184
|
+
return run_streaming(["python", "-c", full_code], sb)
|
185
|
+
|
186
|
+
|
187
|
+
# ---------------------------------------------------------------------------
|
188
|
+
# Stateful session wrapper
|
189
|
+
# ---------------------------------------------------------------------------
|
190
|
+
|
191
|
+
class SandboxSession:
|
192
|
+
"""Maintain a persistent sandbox instance across multiple executions."""
|
193
|
+
|
194
|
+
def __init__(
|
195
|
+
self,
|
196
|
+
modal_secrets: modal.Secret,
|
197
|
+
*,
|
198
|
+
timeout: int = 5 * 60,
|
199
|
+
**create_kwargs,
|
200
|
+
) -> None:
|
201
|
+
self.modal_secrets = modal_secrets
|
202
|
+
self.timeout = timeout
|
203
|
+
self._create_kwargs = create_kwargs
|
204
|
+
self.sandbox: modal.Sandbox | None = None
|
205
|
+
self.app: modal.App | None = None
|
206
|
+
self._driver_proc: "modal.container_process.ContainerProcess" | None = None
|
207
|
+
|
208
|
+
# Public API -----------------------------------------------------------
|
209
|
+
|
210
|
+
def ensure_sandbox(self) -> modal.Sandbox:
|
211
|
+
if self.sandbox is None:
|
212
|
+
self.sandbox, self.app = create_sandbox(
|
213
|
+
self.modal_secrets,
|
214
|
+
timeout=self.timeout,
|
215
|
+
**self._create_kwargs,
|
216
|
+
)
|
217
|
+
_pretty("GREEN", "📦: Created new sandbox session")
|
218
|
+
return self.sandbox
|
219
|
+
|
220
|
+
def run_python(self, code: str) -> Tuple[str, str]:
|
221
|
+
"""Shortcut for *python -c* streaming runs."""
|
222
|
+
return run_streaming_python(code, self.ensure_sandbox())
|
223
|
+
|
224
|
+
def run(self, command: Sequence[str]) -> Tuple[str, str]:
|
225
|
+
"""Run arbitrary command with streaming output."""
|
226
|
+
return run_streaming(command, self.ensure_sandbox())
|
227
|
+
|
228
|
+
def terminate(self) -> None:
|
229
|
+
if self.sandbox is not None:
|
230
|
+
# Terminate driver first (if any)
|
231
|
+
self.terminate_driver()
|
232
|
+
self.sandbox.terminate()
|
233
|
+
self.sandbox = None
|
234
|
+
_pretty("HEADER", "📦: Terminated sandbox session")
|
235
|
+
|
236
|
+
# Context-manager interface -------------------------------------------
|
237
|
+
|
238
|
+
def __enter__(self) -> "SandboxSession":
|
239
|
+
self.ensure_sandbox()
|
240
|
+
return self
|
241
|
+
|
242
|
+
def __exit__(self, exc_type, exc_val, exc_tb): # noqa: D401
|
243
|
+
self.terminate()
|
244
|
+
|
245
|
+
# -------------------------------------------------------------------
|
246
|
+
# Driver / stateful Python execution helpers
|
247
|
+
# -------------------------------------------------------------------
|
248
|
+
|
249
|
+
def start_driver(self) -> "modal.container_process.ContainerProcess":
|
250
|
+
"""Launch *driver_program* inside the sandbox (if not already running)."""
|
251
|
+
|
252
|
+
import modal.container_process # Local import to avoid hard dep at top
|
253
|
+
|
254
|
+
sandbox = self.ensure_sandbox()
|
255
|
+
|
256
|
+
if self._driver_proc is not None:
|
257
|
+
return self._driver_proc # Already running
|
258
|
+
|
259
|
+
driver_code = get_source(driver_program) + "\n\ndriver_program()"
|
260
|
+
|
261
|
+
self._driver_proc = sandbox.exec(
|
262
|
+
"python",
|
263
|
+
"-c",
|
264
|
+
driver_code,
|
265
|
+
stdout=StreamType.PIPE,
|
266
|
+
stderr=StreamType.PIPE,
|
267
|
+
)
|
268
|
+
|
269
|
+
_pretty("GREEN", "🚀 Driver program started in sandbox")
|
270
|
+
return self._driver_proc
|
271
|
+
|
272
|
+
def run_stateful(self, code: Any) -> str: # noqa: ANN401 – allow flexible input
|
273
|
+
"""Execute *code* (or the source of *obj*) in the persistent driver with streaming.
|
274
|
+
|
275
|
+
If *code* is not a string, the helper attempts to obtain its
|
276
|
+
source using :pyfunc:`get_source` so that users can conveniently
|
277
|
+
pass in callables or other Python objects directly.
|
278
|
+
"""
|
279
|
+
|
280
|
+
# Accept arbitrary objects for convenience ------------------------
|
281
|
+
if not isinstance(code, str):
|
282
|
+
try:
|
283
|
+
code = get_source(code)
|
284
|
+
except ValueError as exc: # Fallback to string representation
|
285
|
+
_pretty(
|
286
|
+
"RED",
|
287
|
+
f"⚠️ Unable to retrieve source for object {code!r}: {exc}. Falling back to str(obj).",
|
288
|
+
)
|
289
|
+
code = str(code)
|
290
|
+
|
291
|
+
proc = self.start_driver()
|
292
|
+
return run_code_streaming(proc, code)
|
293
|
+
|
294
|
+
def terminate_driver(self) -> None:
|
295
|
+
"""Terminate the stateful driver process if running."""
|
296
|
+
|
297
|
+
if self._driver_proc is not None:
|
298
|
+
try:
|
299
|
+
self._driver_proc.terminate()
|
300
|
+
except Exception: # noqa: BLE001
|
301
|
+
pass
|
302
|
+
self._driver_proc = None
|
303
|
+
_pretty("HEADER", "📦: Driver process terminated")
|
304
|
+
|
305
|
+
|
306
|
+
# ---------------------------------------------------------------------------
|
307
|
+
# Stateful in-sandbox code execution helper
|
308
|
+
# ---------------------------------------------------------------------------
|
309
|
+
|
310
|
+
# Below we *copy verbatim* the proven driver + streaming logic from the demo
|
311
|
+
# script so as not to break existing behaviour. Only minimal stylistic tweaks
|
312
|
+
# (type annotations, comments) are applied.
|
313
|
+
|
314
|
+
|
315
|
+
def driver_program():
|
316
|
+
import json
|
317
|
+
import sys
|
318
|
+
import time
|
319
|
+
import builtins
|
320
|
+
|
321
|
+
globals: dict[str, Any] = {}
|
322
|
+
|
323
|
+
# Store original stdout and stderr
|
324
|
+
original_stdout = sys.stdout
|
325
|
+
original_stderr = sys.stderr
|
326
|
+
original_print = builtins.print
|
327
|
+
|
328
|
+
def streaming_print(*args, sep=' ', end='\n', file=None, flush=False):
|
329
|
+
"""Custom print function that streams output immediately"""
|
330
|
+
if file is None or file == sys.stdout:
|
331
|
+
# Convert args to string like normal print
|
332
|
+
output = sep.join(str(arg) for arg in args) + end
|
333
|
+
# Send immediately as JSON
|
334
|
+
original_stdout.write(json.dumps({"type": "stdout", "content": output}) + '\n')
|
335
|
+
original_stdout.flush()
|
336
|
+
elif file == sys.stderr:
|
337
|
+
# Convert args to string like normal print
|
338
|
+
output = sep.join(str(arg) for arg in args) + end
|
339
|
+
# Send immediately as JSON
|
340
|
+
original_stdout.write(json.dumps({"type": "stderr", "content": output}) + '\n')
|
341
|
+
original_stdout.flush()
|
342
|
+
else:
|
343
|
+
# Use original print for other files
|
344
|
+
original_print(*args, sep=sep, end=end, file=file, flush=flush)
|
345
|
+
|
346
|
+
class StreamingStdout:
|
347
|
+
def write(self, text):
|
348
|
+
if text and not text.isspace():
|
349
|
+
original_stdout.write(json.dumps({"type": "stdout", "content": text}) + '\n')
|
350
|
+
original_stdout.flush()
|
351
|
+
return len(text)
|
352
|
+
|
353
|
+
def flush(self):
|
354
|
+
original_stdout.flush()
|
355
|
+
|
356
|
+
def fileno(self):
|
357
|
+
return original_stdout.fileno()
|
358
|
+
|
359
|
+
def isatty(self):
|
360
|
+
return False
|
361
|
+
|
362
|
+
def readable(self):
|
363
|
+
return False
|
364
|
+
|
365
|
+
def writable(self):
|
366
|
+
return True
|
367
|
+
|
368
|
+
def seekable(self):
|
369
|
+
return False
|
370
|
+
|
371
|
+
class StreamingStderr:
|
372
|
+
def write(self, text):
|
373
|
+
if text and not text.isspace():
|
374
|
+
original_stdout.write(json.dumps({"type": "stderr", "content": text}) + '\n')
|
375
|
+
original_stdout.flush()
|
376
|
+
return len(text)
|
377
|
+
|
378
|
+
def flush(self):
|
379
|
+
original_stdout.flush()
|
380
|
+
|
381
|
+
def fileno(self):
|
382
|
+
return original_stderr.fileno()
|
383
|
+
|
384
|
+
def isatty(self):
|
385
|
+
return False
|
386
|
+
|
387
|
+
def readable(self):
|
388
|
+
return False
|
389
|
+
|
390
|
+
def writable(self):
|
391
|
+
return True
|
392
|
+
|
393
|
+
def seekable(self):
|
394
|
+
return False
|
395
|
+
|
396
|
+
while True:
|
397
|
+
try:
|
398
|
+
command = json.loads(input())
|
399
|
+
if (code := command.get("code")) is None:
|
400
|
+
original_stdout.write(json.dumps({"error": "No code to execute"}) + '\n')
|
401
|
+
original_stdout.flush()
|
402
|
+
continue
|
403
|
+
|
404
|
+
# Send start marker
|
405
|
+
original_stdout.write(json.dumps({"type": "start"}) + '\n')
|
406
|
+
original_stdout.flush()
|
407
|
+
|
408
|
+
# Replace print and stdout/stderr for streaming
|
409
|
+
builtins.print = streaming_print
|
410
|
+
sys.stdout = StreamingStdout()
|
411
|
+
sys.stderr = StreamingStderr()
|
412
|
+
|
413
|
+
try:
|
414
|
+
exec(code, globals)
|
415
|
+
except Exception as e:
|
416
|
+
# Send error through stderr
|
417
|
+
sys.stderr.write(f"Execution Error: {e}\n")
|
418
|
+
finally:
|
419
|
+
# Restore original stdout/stderr/print
|
420
|
+
sys.stdout = original_stdout
|
421
|
+
sys.stderr = original_stderr
|
422
|
+
builtins.print = original_print
|
423
|
+
|
424
|
+
# Send completion marker
|
425
|
+
original_stdout.write(json.dumps({"type": "complete"}) + '\n')
|
426
|
+
original_stdout.flush()
|
427
|
+
|
428
|
+
except EOFError:
|
429
|
+
break
|
430
|
+
except Exception as e:
|
431
|
+
original_stdout.write(json.dumps({"type": "error", "message": str(e)}) + '\n')
|
432
|
+
original_stdout.flush()
|
433
|
+
|
434
|
+
|
435
|
+
def run_code_streaming(
|
436
|
+
p: "modal.container_process.ContainerProcess",
|
437
|
+
code: str,
|
438
|
+
) -> str:
|
439
|
+
"""Send *code* to an already-running `driver_program` process and stream output."""
|
440
|
+
|
441
|
+
p.stdin.write(json.dumps({"code": code}))
|
442
|
+
p.stdin.write("\n")
|
443
|
+
p.stdin.drain()
|
444
|
+
|
445
|
+
buffer = ""
|
446
|
+
|
447
|
+
for chunk in p.stdout:
|
448
|
+
buffer += chunk
|
449
|
+
while buffer.strip():
|
450
|
+
try:
|
451
|
+
result, idx = json.JSONDecoder().raw_decode(buffer)
|
452
|
+
except json.JSONDecodeError:
|
453
|
+
break # Need more data
|
454
|
+
|
455
|
+
buffer = buffer[idx:].lstrip()
|
456
|
+
_type = result.get("type")
|
457
|
+
if _type == "start":
|
458
|
+
print("🔄 Executing code…")
|
459
|
+
elif _type == "stdout":
|
460
|
+
print(result["content"], end="")
|
461
|
+
elif _type == "stderr":
|
462
|
+
print(f"\033[91m{result['content']}\033[0m", end="")
|
463
|
+
elif _type == "complete":
|
464
|
+
print("✅ Execution complete")
|
465
|
+
return buffer
|
466
|
+
elif result.get("error"):
|
467
|
+
print(f"❌ Error: {result['error']}")
|
468
|
+
return buffer
|
469
|
+
|
470
|
+
|
471
|
+
# Convenience shortcut --------------------------------------------------------
|
472
|
+
|
473
|
+
def start_sandbox_session(modal_secrets: modal.Secret, **kwargs) -> SandboxSession:
|
474
|
+
"""Helper returning an *active* `SandboxSession` instance."""
|
475
|
+
|
476
|
+
session = SandboxSession(modal_secrets, **kwargs)
|
477
|
+
session.ensure_sandbox()
|
478
|
+
return session
|
@@ -0,0 +1,152 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import Dict, List, Any, Optional
|
3
|
+
from tinyagent.hooks.logging_manager import LoggingManager
|
4
|
+
|
5
|
+
|
6
|
+
class CodeExecutionProvider(ABC):
|
7
|
+
"""
|
8
|
+
Abstract base class for code execution providers.
|
9
|
+
|
10
|
+
This class defines the interface that all code execution providers must implement.
|
11
|
+
It allows for easy extension to support different execution environments
|
12
|
+
(Modal, Docker, local execution, cloud functions, etc.) with minimal code changes.
|
13
|
+
"""
|
14
|
+
|
15
|
+
def __init__(
|
16
|
+
self,
|
17
|
+
log_manager: LoggingManager,
|
18
|
+
default_python_codes: Optional[List[str]] = None,
|
19
|
+
code_tools: List[Dict[str, Any]] = None,
|
20
|
+
pip_packages: List[str] = None,
|
21
|
+
secrets: Dict[str, Any] = None,
|
22
|
+
lazy_init: bool = True,
|
23
|
+
**kwargs
|
24
|
+
):
|
25
|
+
self.log_manager = log_manager
|
26
|
+
self.default_python_codes = default_python_codes or []
|
27
|
+
self.code_tools = code_tools or []
|
28
|
+
self.pip_packages = pip_packages or []
|
29
|
+
self.secrets = secrets or {}
|
30
|
+
self.lazy_init = lazy_init
|
31
|
+
self.kwargs = kwargs
|
32
|
+
self.executed_default_codes = False
|
33
|
+
self._globals_dict = kwargs.get("globals_dict", {})
|
34
|
+
self._locals_dict = kwargs.get("locals_dict", {})
|
35
|
+
self._user_variables = {}
|
36
|
+
self.code_tools_definitions = []
|
37
|
+
|
38
|
+
@abstractmethod
|
39
|
+
async def execute_python(
|
40
|
+
self,
|
41
|
+
code_lines: List[str],
|
42
|
+
timeout: int = 120
|
43
|
+
) -> Dict[str, Any]:
|
44
|
+
"""
|
45
|
+
Execute Python code and return the result.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
code_lines: List of Python code lines to execute
|
49
|
+
timeout: Maximum execution time in seconds
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
Dictionary containing execution results with keys:
|
53
|
+
- printed_output: stdout from the execution
|
54
|
+
- return_value: the return value if any
|
55
|
+
- stderr: stderr from the execution
|
56
|
+
- error_traceback: exception traceback if any error occurred
|
57
|
+
"""
|
58
|
+
pass
|
59
|
+
|
60
|
+
@abstractmethod
|
61
|
+
async def cleanup(self):
|
62
|
+
"""Clean up any resources used by the provider."""
|
63
|
+
pass
|
64
|
+
|
65
|
+
def add_tools(self, tools: List[Any]) -> None:
|
66
|
+
"""
|
67
|
+
Add tools to the execution environment.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
tools: List of tool objects to add
|
71
|
+
"""
|
72
|
+
import cloudpickle
|
73
|
+
|
74
|
+
tools_str_list = ["import cloudpickle"]
|
75
|
+
tools_str_list.append("###########<tools>###########\n")
|
76
|
+
for tool in tools:
|
77
|
+
tools_str_list.append(
|
78
|
+
f"globals()['{tool._tool_metadata['name']}'] = cloudpickle.loads({cloudpickle.dumps(tool)})"
|
79
|
+
)
|
80
|
+
tools_str_list.append("\n\n")
|
81
|
+
tools_str_list.append("###########</tools>###########\n")
|
82
|
+
tools_str_list.append("\n\n")
|
83
|
+
self.code_tools_definitions.extend(tools_str_list)
|
84
|
+
|
85
|
+
def set_user_variables(self, variables: Dict[str, Any]) -> None:
|
86
|
+
"""
|
87
|
+
Set user variables that will be available in the Python environment.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
variables: Dictionary of variable name -> value pairs
|
91
|
+
"""
|
92
|
+
import cloudpickle
|
93
|
+
|
94
|
+
self._user_variables = variables.copy()
|
95
|
+
|
96
|
+
# Add variables to the execution environment by serializing them
|
97
|
+
# This ensures they are available when code is executed
|
98
|
+
variables_str_list = ["import cloudpickle"]
|
99
|
+
variables_str_list.append("###########<user_variables>###########\n")
|
100
|
+
|
101
|
+
for var_name, var_value in variables.items():
|
102
|
+
# Serialize the variable and add it to globals
|
103
|
+
serialized_var = cloudpickle.dumps(var_value)
|
104
|
+
variables_str_list.append(
|
105
|
+
f"globals()['{var_name}'] = cloudpickle.loads({serialized_var})"
|
106
|
+
)
|
107
|
+
|
108
|
+
variables_str_list.append("\n###########</user_variables>###########\n")
|
109
|
+
variables_str_list.append("\n")
|
110
|
+
|
111
|
+
# Remove any existing user variables from default codes
|
112
|
+
self._remove_existing_user_variables()
|
113
|
+
|
114
|
+
# Add new variables to default codes at the beginning (after tools if any)
|
115
|
+
# This ensures variables are available from the start
|
116
|
+
if variables_str_list:
|
117
|
+
# Find where to insert (after tools section if it exists)
|
118
|
+
insert_index = 0
|
119
|
+
for i, code in enumerate(self.default_python_codes):
|
120
|
+
if "###########</tools>###########" in code:
|
121
|
+
insert_index = i + 1
|
122
|
+
break
|
123
|
+
|
124
|
+
# Insert the variables code
|
125
|
+
for j, var_code in enumerate(variables_str_list):
|
126
|
+
self.default_python_codes.insert(insert_index + j, var_code)
|
127
|
+
|
128
|
+
def _remove_existing_user_variables(self) -> None:
|
129
|
+
"""Remove existing user variables from default python codes."""
|
130
|
+
# Find and remove the user variables section
|
131
|
+
start_index = None
|
132
|
+
end_index = None
|
133
|
+
|
134
|
+
for i, code in enumerate(self.default_python_codes):
|
135
|
+
if "###########<user_variables>###########" in code:
|
136
|
+
start_index = i - 1 if i > 0 and "import cloudpickle" in self.default_python_codes[i-1] else i
|
137
|
+
elif "###########</user_variables>###########" in code:
|
138
|
+
end_index = i + 2 # Include the newline after
|
139
|
+
break
|
140
|
+
|
141
|
+
if start_index is not None and end_index is not None:
|
142
|
+
# Remove the old variables section
|
143
|
+
del self.default_python_codes[start_index:end_index]
|
144
|
+
|
145
|
+
def get_user_variables(self) -> Dict[str, Any]:
|
146
|
+
"""
|
147
|
+
Get a copy of current user variables.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
Dictionary of current user variables
|
151
|
+
"""
|
152
|
+
return self._user_variables.copy()
|