lm-deluge 0.0.76__py3-none-any.whl → 0.0.79__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.
@@ -0,0 +1,523 @@
1
+ import secrets
2
+
3
+ from lm_deluge.tool import Tool
4
+
5
+
6
+ class ModalSandbox:
7
+ def __init__(self, app_name: str | None = None, *, block_network: bool = False):
8
+ import modal
9
+
10
+ app_name = app_name or secrets.token_urlsafe(32)
11
+ app = modal.App.lookup(app_name, create_if_missing=True)
12
+ self.app = app
13
+ self.block_network = block_network
14
+ self.sb = modal.Sandbox.create(app=app, block_network=block_network)
15
+ self.last_process = None
16
+ self._destroyed = False
17
+
18
+ def __enter__(self):
19
+ """Synchronous context manager entry (use async with for async support)."""
20
+ return self
21
+
22
+ def __exit__(self, exc_type, exc_val, exc_tb):
23
+ """Synchronous context manager exit - cleanup sandbox."""
24
+ if not self._destroyed:
25
+ self._destroy()
26
+ return False
27
+
28
+ def __del__(self):
29
+ """Cleanup sandbox when garbage collected (backup cleanup)."""
30
+ if not self._destroyed:
31
+ try:
32
+ self._destroy()
33
+ except Exception:
34
+ # Ignore errors during cleanup in __del__
35
+ pass
36
+
37
+ @staticmethod
38
+ async def _safe_read(process, max_lines: int = 25, max_chars: int = 2500):
39
+ result = await process.stdout.read.aio()
40
+
41
+ if len(result) > max_chars:
42
+ result = result[-max_chars:]
43
+
44
+ lines = result.splitlines()
45
+ lines = lines[-max_lines:]
46
+
47
+ return "\n".join(lines)
48
+
49
+ async def _exec(
50
+ self, cmd: list[str], timeout: int = 5, check: bool = False
51
+ ) -> str | None:
52
+ process = await self.sb.exec.aio(*cmd, timeout=timeout)
53
+ self.last_process = process
54
+ if check:
55
+ return await self._safe_read(process)
56
+
57
+ async def _read(self, limit: int = 25):
58
+ if not self.last_process:
59
+ return None
60
+ return await self._safe_read(self.last_process)
61
+
62
+ def _get_credentials(self):
63
+ if self.block_network:
64
+ return None
65
+ creds = self.sb.create_connect_token(user_metadata={"user_id": "foo"})
66
+
67
+ return creds # f"URL: {creds.url}; Token: {creds.token}"
68
+
69
+ def _destroy(self):
70
+ """Destroy the sandbox and mark as destroyed."""
71
+ if not self._destroyed:
72
+ self.sb.terminate()
73
+ self._destroyed = True
74
+
75
+ def get_tools(self):
76
+ bash_tool = Tool(
77
+ name="bash",
78
+ description=(
79
+ "Execute a bash command in the sandbox environment. "
80
+ "Provide the command as a list of strings (e.g., ['ls', '-la']). "
81
+ "Optionally set a timeout in seconds and check=True to immediately read the output."
82
+ ),
83
+ run=self._exec,
84
+ parameters={
85
+ "cmd": {
86
+ "type": "array",
87
+ "description": "The command to execute as a list of strings (e.g., ['python', 'script.py'])",
88
+ "items": {"type": "string"},
89
+ },
90
+ "timeout": {
91
+ "type": "integer",
92
+ "description": "Timeout in seconds for the command execution (default: 5)",
93
+ },
94
+ "check": {
95
+ "type": "boolean",
96
+ "description": "If true, immediately read and return the last line of stdout (default: false)",
97
+ },
98
+ },
99
+ required=["cmd"],
100
+ )
101
+
102
+ stdout_tool = Tool(
103
+ name="read_stdout",
104
+ description=(
105
+ "Read the most recent stdout output from the bash shell. "
106
+ "ONLY returns stdout from the most recent command, "
107
+ "cannot be used to get output from previous commands. "
108
+ "Returns the last `limit` lines of stdout (default: 25 lines)."
109
+ ),
110
+ run=self._read,
111
+ parameters={
112
+ "limit": {
113
+ "type": "integer",
114
+ "description": "Maximum number of recent lines to return (default: 25)",
115
+ }
116
+ },
117
+ required=[],
118
+ )
119
+
120
+ tunnel_tool = Tool(
121
+ name="tunnel",
122
+ description=(
123
+ "Opens a tunnel on port 8080 and returns a URL and token to connect to it. "
124
+ "Useful for exposing a local server or application to the user. "
125
+ "Only works when network is enabled (block_network=False)."
126
+ ),
127
+ run=self._get_credentials,
128
+ parameters={},
129
+ required=[],
130
+ )
131
+
132
+ return [bash_tool, stdout_tool, tunnel_tool]
133
+
134
+
135
+ class DaytonaSandbox:
136
+ def __init__(
137
+ self,
138
+ api_key: str | None = None,
139
+ api_url: str | None = None,
140
+ target: str | None = None,
141
+ sandbox_id: str | None = None,
142
+ language: str = "python",
143
+ auto_start: bool = True,
144
+ ):
145
+ """
146
+ Initialize a Daytona sandbox.
147
+
148
+ Args:
149
+ api_key: Daytona API key (if None, will look for DAYTONA_API_KEY env var)
150
+ api_url: Daytona API URL (if None, will look for DAYTONA_API_URL env var)
151
+ target: Daytona target (if None, will look for DAYTONA_TARGET env var)
152
+ sandbox_id: ID of existing sandbox to connect to (if None, creates a new one)
153
+ language: Programming language for the sandbox (default: python)
154
+ auto_start: Whether to automatically start the sandbox if stopped
155
+ """
156
+ import os
157
+
158
+ self.api_key = api_key or os.getenv("DAYTONA_API_KEY")
159
+ self.api_url = api_url or os.getenv("DAYTONA_API_URL")
160
+ self.target = target or os.getenv("DAYTONA_TARGET")
161
+ self.sandbox_id = sandbox_id
162
+ self.language = language
163
+ self.auto_start = auto_start
164
+ self.sandbox = None
165
+ self.client = None
166
+ self._initialized = False
167
+ self._destroyed = False
168
+
169
+ async def __aenter__(self):
170
+ """Async context manager entry - initialize sandbox."""
171
+ await self._ensure_initialized()
172
+ return self
173
+
174
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
175
+ """Async context manager exit - cleanup sandbox."""
176
+ if not self._destroyed:
177
+ await self._destroy()
178
+ return False
179
+
180
+ def __del__(self):
181
+ """Cleanup sandbox when garbage collected (backup cleanup).
182
+
183
+ Note: This attempts sync cleanup which may not work perfectly for async resources.
184
+ Prefer using 'async with' for guaranteed cleanup.
185
+ """
186
+ if not self._destroyed and self.sandbox:
187
+ import warnings
188
+
189
+ warnings.warn(
190
+ "DaytonaSandbox was not properly cleaned up. "
191
+ "Use 'async with DaytonaSandbox(...) as sandbox:' for automatic cleanup.",
192
+ ResourceWarning,
193
+ stacklevel=2,
194
+ )
195
+
196
+ async def _ensure_initialized(self):
197
+ """Lazy initialization of sandbox"""
198
+ if self._initialized:
199
+ return
200
+
201
+ from daytona_sdk import ( # type: ignore
202
+ AsyncDaytona,
203
+ CreateSandboxBaseParams,
204
+ DaytonaConfig,
205
+ )
206
+
207
+ # Initialize client with config
208
+ if self.api_key or self.api_url or self.target:
209
+ config = DaytonaConfig(
210
+ api_key=self.api_key, api_url=self.api_url, target=self.target
211
+ )
212
+ self.client = AsyncDaytona(config)
213
+ else:
214
+ # Use environment variables
215
+ self.client = AsyncDaytona()
216
+
217
+ if self.sandbox_id:
218
+ # Connect to existing sandbox - use find_one with id label
219
+ sandboxes = await self.client.list(labels={"id": self.sandbox_id})
220
+ if not sandboxes or not sandboxes.items:
221
+ raise ValueError(f"Sandbox with ID {self.sandbox_id} not found")
222
+ self.sandbox = sandboxes.items[0]
223
+ else:
224
+ # Create new sandbox with default configuration
225
+ params = CreateSandboxBaseParams(language=self.language) # type: ignore
226
+ self.sandbox = await self.client.create(params) # type: ignore
227
+ self.sandbox_id = self.sandbox.id
228
+
229
+ # Start sandbox if needed
230
+ if self.auto_start and self.sandbox.state != "started":
231
+ await self.sandbox.start()
232
+
233
+ self._initialized = True
234
+
235
+ async def _exec(
236
+ self,
237
+ command: str,
238
+ timeout: int = 30,
239
+ cwd: str | None = None,
240
+ env: dict | None = None,
241
+ ) -> str:
242
+ """
243
+ Execute a shell command in the sandbox.
244
+
245
+ Args:
246
+ command: Shell command to execute
247
+ timeout: Timeout in seconds (None for no timeout)
248
+ cwd: Working directory for the command
249
+ env: Environment variables for the command
250
+
251
+ Returns:
252
+ Command output and exit code information
253
+ """
254
+ await self._ensure_initialized()
255
+
256
+ # Execute command using the process interface
257
+ # API: exec(command, cwd=".", env=None, timeout=None) -> ExecutionResponse
258
+ assert self.sandbox, "no sandbox"
259
+ result = await self.sandbox.process.exec(
260
+ command=command, cwd=cwd or ".", env=env, timeout=timeout
261
+ )
262
+
263
+ # ExecutionResponse has .result (output) and .exit_code
264
+ output = result.result or ""
265
+
266
+ # Include exit code if non-zero
267
+ if result.exit_code != 0:
268
+ output = f"[Exit code: {result.exit_code}]\n{output}"
269
+
270
+ # Limit output to last 5000 characters to avoid overwhelming the LLM
271
+ if len(output) > 5000:
272
+ output = "...[truncated]...\n" + output[-5000:]
273
+
274
+ return output or "(no output)"
275
+
276
+ async def _read_file(self, path: str, max_size: int = 50000) -> str:
277
+ """
278
+ Read a file from the sandbox.
279
+
280
+ Args:
281
+ path: Path to the file in the sandbox
282
+ max_size: Maximum file size in bytes to read
283
+
284
+ Returns:
285
+ File contents as string
286
+ """
287
+ await self._ensure_initialized()
288
+
289
+ # API: download_file(remote_path, timeout=1800) -> bytes
290
+ assert self.sandbox, "no sandbox"
291
+ content_bytes = await self.sandbox.fs.download_file(path)
292
+ content = content_bytes.decode("utf-8", errors="replace")
293
+
294
+ if len(content) > max_size:
295
+ return f"File too large ({len(content)} bytes). First {max_size} bytes:\n{content[:max_size]}"
296
+
297
+ return content
298
+
299
+ async def _write_file(self, path: str, content: str) -> str:
300
+ """
301
+ Write content to a file in the sandbox.
302
+
303
+ Args:
304
+ path: Path to the file in the sandbox
305
+ content: Content to write
306
+
307
+ Returns:
308
+ Success message
309
+ """
310
+ await self._ensure_initialized()
311
+ assert self.sandbox, "no sandbox"
312
+
313
+ # API: upload_file(file: bytes, remote_path: str, timeout=1800) -> None
314
+ content_bytes = content.encode("utf-8")
315
+ await self.sandbox.fs.upload_file(content_bytes, path)
316
+ return f"Successfully wrote {len(content)} bytes to {path}"
317
+
318
+ async def _list_files(self, path: str = ".", pattern: str | None = None) -> str:
319
+ """
320
+ List files in a directory.
321
+
322
+ Args:
323
+ path: Directory path to list
324
+ pattern: Optional glob pattern to filter files
325
+
326
+ Returns:
327
+ Formatted list of files
328
+ """
329
+ await self._ensure_initialized()
330
+ assert self.sandbox, "no sandbox"
331
+
332
+ if pattern:
333
+ # API: find_files(path, pattern) -> List[Match]
334
+ matches = await self.sandbox.fs.find_files(path=path, pattern=pattern)
335
+ if not matches:
336
+ return f"No files matching '{pattern}' found in {path}"
337
+
338
+ # Format the matches
339
+ files = [match.file for match in matches]
340
+ return "\n".join(files)
341
+ else:
342
+ # API: list_files(path) -> List[FileInfo]
343
+ file_infos = await self.sandbox.fs.list_files(path=path)
344
+
345
+ if not file_infos:
346
+ return f"No files found in {path}"
347
+
348
+ # Format the output with file info
349
+ lines = []
350
+ for info in file_infos:
351
+ # FileInfo has .name, .size, .mode, .is_dir, etc
352
+ if info.is_dir:
353
+ lines.append(f"{info.name}/")
354
+ else:
355
+ lines.append(f"{info.name} ({info.size} bytes)")
356
+ return "\n".join(lines)
357
+
358
+ async def _get_preview_link(self, port: int = 8080) -> str:
359
+ """
360
+ Get a preview link for exposing a port.
361
+
362
+ Args:
363
+ port: Port number to expose
364
+
365
+ Returns:
366
+ Preview URL and token information
367
+ """
368
+ await self._ensure_initialized()
369
+ assert self.sandbox, "no sandbox"
370
+ preview = await self.sandbox.get_preview_link(port)
371
+
372
+ result = f"URL: {preview.url}"
373
+ if hasattr(preview, "token") and preview.token:
374
+ result += f"\nToken: {preview.token}"
375
+
376
+ return result
377
+
378
+ async def _get_working_dir(self) -> str:
379
+ """Get the current working directory in the sandbox."""
380
+ await self._ensure_initialized()
381
+ assert self.sandbox, "no sandbox"
382
+ return await self.sandbox.get_work_dir()
383
+
384
+ async def _destroy(self):
385
+ """Delete the sandbox and clean up resources."""
386
+ if self.sandbox and not self._destroyed:
387
+ await self.sandbox.delete()
388
+ self._destroyed = True
389
+ self._initialized = False
390
+ self.sandbox = None
391
+
392
+ def get_tools(self):
393
+ """Return list of tools for LLM use."""
394
+ bash_tool = Tool(
395
+ name="bash",
396
+ description=(
397
+ "Execute a bash command in the Daytona sandbox environment. "
398
+ "The command runs in a persistent Linux environment. "
399
+ "Provide the command as a string (e.g., 'ls -la' or 'python script.py'). "
400
+ "Output is truncated to the last 5000 characters if longer. "
401
+ "Exit codes are included in output if non-zero."
402
+ ),
403
+ run=self._exec,
404
+ parameters={
405
+ "command": {
406
+ "type": "string",
407
+ "description": "The shell command to execute (e.g., 'ls -la', 'python script.py')",
408
+ },
409
+ "timeout": {
410
+ "type": "integer",
411
+ "description": "Timeout in seconds for the command execution (default: 30)",
412
+ },
413
+ "cwd": {
414
+ "type": "string",
415
+ "description": "Working directory for the command (default: current directory)",
416
+ },
417
+ "env": {
418
+ "type": "object",
419
+ "description": "Environment variables for the command (optional)",
420
+ },
421
+ },
422
+ required=["command"],
423
+ )
424
+
425
+ read_file_tool = Tool(
426
+ name="read_file",
427
+ description=(
428
+ "Read the contents of a file from the sandbox filesystem. "
429
+ "Provide the absolute or relative path to the file. "
430
+ "Files larger than 50KB are truncated."
431
+ ),
432
+ run=self._read_file,
433
+ parameters={
434
+ "path": {
435
+ "type": "string",
436
+ "description": "Path to the file to read (e.g., '/home/user/script.py')",
437
+ },
438
+ "max_size": {
439
+ "type": "integer",
440
+ "description": "Maximum file size in bytes to read (default: 50000)",
441
+ },
442
+ },
443
+ required=["path"],
444
+ )
445
+
446
+ write_file_tool = Tool(
447
+ name="write_file",
448
+ description=(
449
+ "Write content to a file in the sandbox filesystem. "
450
+ "Creates the file if it doesn't exist, overwrites if it does. "
451
+ "Parent directories must exist."
452
+ ),
453
+ run=self._write_file,
454
+ parameters={
455
+ "path": {
456
+ "type": "string",
457
+ "description": "Path where to write the file (e.g., '/home/user/script.py')",
458
+ },
459
+ "content": {
460
+ "type": "string",
461
+ "description": "Content to write to the file",
462
+ },
463
+ },
464
+ required=["path", "content"],
465
+ )
466
+
467
+ list_files_tool = Tool(
468
+ name="list_files",
469
+ description=(
470
+ "List files and directories in the sandbox filesystem. "
471
+ "Useful for exploring the sandbox environment and finding files. "
472
+ "Optionally filter by glob pattern (e.g., '*.py', '**/*.txt')."
473
+ ),
474
+ run=self._list_files,
475
+ parameters={
476
+ "path": {
477
+ "type": "string",
478
+ "description": "Directory path to list (default: current directory)",
479
+ },
480
+ "pattern": {
481
+ "type": "string",
482
+ "description": "Glob pattern to filter files (e.g., '*.py', '**/*.txt')",
483
+ },
484
+ },
485
+ required=[],
486
+ )
487
+
488
+ preview_tool = Tool(
489
+ name="get_preview_link",
490
+ description=(
491
+ "Get a public URL to access a port in the sandbox. "
492
+ "Useful for exposing web servers or applications running in the sandbox. "
493
+ "Returns a URL and authentication token if needed."
494
+ ),
495
+ run=self._get_preview_link,
496
+ parameters={
497
+ "port": {
498
+ "type": "integer",
499
+ "description": "Port number to expose (default: 8080)",
500
+ },
501
+ },
502
+ required=[],
503
+ )
504
+
505
+ workdir_tool = Tool(
506
+ name="get_working_directory",
507
+ description=(
508
+ "Get the current working directory path in the sandbox. "
509
+ "Useful for understanding the sandbox environment layout."
510
+ ),
511
+ run=self._get_working_dir,
512
+ parameters={},
513
+ required=[],
514
+ )
515
+
516
+ return [
517
+ bash_tool,
518
+ read_file_tool,
519
+ write_file_tool,
520
+ list_files_tool,
521
+ preview_tool,
522
+ workdir_tool,
523
+ ]
@@ -138,4 +138,19 @@ GOOGLE_MODELS = {
138
138
  "output_cost": 0.4,
139
139
  "reasoning_model": True,
140
140
  },
141
+ # Gemini 3 models - advanced reasoning with thought signatures
142
+ "gemini-3-pro-preview": {
143
+ "id": "gemini-3-pro-preview",
144
+ "name": "gemini-3-pro-preview",
145
+ "api_base": "https://generativelanguage.googleapis.com/v1alpha",
146
+ "api_key_env_var": "GEMINI_API_KEY",
147
+ "supports_json": True,
148
+ "supports_logprobs": False,
149
+ "api_spec": "gemini",
150
+ "input_cost": 2.0, # <200k tokens
151
+ "cached_input_cost": 0.5, # estimated
152
+ "output_cost": 12.0, # <200k tokens
153
+ # Note: >200k tokens pricing is $4/$18 per million
154
+ "reasoning_model": True,
155
+ },
141
156
  }
@@ -61,4 +61,14 @@ OPENROUTER_MODELS = {
61
61
  "cache_write_cost": 0.6,
62
62
  "output_cost": 2.20,
63
63
  },
64
+ "olmo-3-32b-think-openrouter": {
65
+ "id": "olmo-3-32b-think-openrouter",
66
+ "name": "allenai/olmo-3-32b-think",
67
+ "api_base": "https://openrouter.ai/api/v1",
68
+ "api_key_env_var": "OPENROUTER_API_KEY",
69
+ "supports_json": True,
70
+ "api_spec": "openai",
71
+ "input_cost": 0.2,
72
+ "output_cost": 35,
73
+ },
64
74
  }