tinyagent-py 0.0.13__py3-none-any.whl → 0.0.16__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/code_agent/helper.py +2 -2
- tinyagent/code_agent/modal_sandbox.py +1 -1
- tinyagent/code_agent/providers/__init__.py +14 -1
- tinyagent/code_agent/providers/base.py +181 -7
- tinyagent/code_agent/providers/modal_provider.py +150 -27
- tinyagent/code_agent/providers/seatbelt_provider.py +1065 -0
- tinyagent/code_agent/safety.py +6 -2
- tinyagent/code_agent/tiny_code_agent.py +973 -12
- tinyagent/code_agent/utils.py +263 -2
- tinyagent/hooks/__init__.py +3 -1
- tinyagent/hooks/jupyter_notebook_callback.py +1464 -0
- tinyagent/hooks/token_tracker.py +564 -0
- tinyagent/prompts/summarize.yaml +96 -0
- tinyagent/prompts/truncation.yaml +13 -0
- tinyagent/tiny_agent.py +811 -49
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.16.dist-info}/METADATA +25 -1
- tinyagent_py-0.0.16.dist-info/RECORD +38 -0
- tinyagent_py-0.0.13.dist-info/RECORD +0 -33
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.16.dist-info}/WHEEL +0 -0
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.16.dist-info}/licenses/LICENSE +0 -0
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.16.dist-info}/top_level.txt +0 -0
tinyagent/code_agent/utils.py
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
import sys
|
2
2
|
import cloudpickle
|
3
|
-
|
3
|
+
import subprocess
|
4
|
+
import os
|
5
|
+
from typing import Dict, Any, List, Tuple
|
4
6
|
from .safety import validate_code_safety, function_safety_context
|
7
|
+
import shlex
|
8
|
+
import yaml
|
9
|
+
from pathlib import Path
|
10
|
+
import re
|
5
11
|
|
6
12
|
|
7
13
|
def clean_response(resp: Dict[str, Any]) -> Dict[str, Any]:
|
@@ -17,6 +23,118 @@ def clean_response(resp: Dict[str, Any]) -> Dict[str, Any]:
|
|
17
23
|
return {k: v for k, v in resp.items() if k in ['printed_output', 'return_value', 'stderr', 'error_traceback']}
|
18
24
|
|
19
25
|
|
26
|
+
def truncate_output(output: str, max_tokens: int = 3000, max_lines: int = 250) -> Tuple[str, bool, int, int]:
|
27
|
+
"""
|
28
|
+
Truncate output based on token count and line count.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
output: The output string to truncate
|
32
|
+
max_tokens: Maximum number of tokens to keep
|
33
|
+
max_lines: Maximum number of lines to keep
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
Tuple containing:
|
37
|
+
- Truncated output
|
38
|
+
- Boolean indicating if truncation occurred
|
39
|
+
- Original token count
|
40
|
+
- Original line count
|
41
|
+
"""
|
42
|
+
# Count original lines
|
43
|
+
lines = output.splitlines()
|
44
|
+
original_line_count = len(lines)
|
45
|
+
|
46
|
+
# Approximate token count (rough estimate: 4 chars ≈ 1 token)
|
47
|
+
original_token_count = len(output) // 4
|
48
|
+
|
49
|
+
# Check if truncation is needed
|
50
|
+
if original_line_count <= max_lines and original_token_count <= max_tokens:
|
51
|
+
return output, False, original_token_count, original_line_count
|
52
|
+
|
53
|
+
# Truncate by lines first
|
54
|
+
if original_line_count > max_lines:
|
55
|
+
lines = lines[:max_lines] # Keep only the first max_lines
|
56
|
+
|
57
|
+
# Join lines back together
|
58
|
+
truncated = '\n'.join(lines)
|
59
|
+
|
60
|
+
# If still too many tokens, truncate further
|
61
|
+
if len(truncated) // 4 > max_tokens:
|
62
|
+
# Keep the first max_tokens*4 characters (approximate)
|
63
|
+
truncated = truncated[:max_tokens*4]
|
64
|
+
|
65
|
+
# Try to start at a newline to avoid partial lines
|
66
|
+
newline_pos = truncated.find('\n')
|
67
|
+
if newline_pos > 0:
|
68
|
+
truncated = truncated[newline_pos+1:]
|
69
|
+
|
70
|
+
return truncated, True, original_token_count, original_line_count
|
71
|
+
|
72
|
+
|
73
|
+
def load_truncation_template(template_type: str = "python_output") -> str:
|
74
|
+
"""
|
75
|
+
Load the truncation message template.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
template_type: Type of template to load ("python_output" or "bash_output")
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
Template string for the truncation message
|
82
|
+
"""
|
83
|
+
template_path = Path(__file__).parent.parent / "prompts" / "truncation.yaml"
|
84
|
+
|
85
|
+
try:
|
86
|
+
with open(template_path, 'r') as f:
|
87
|
+
templates = yaml.safe_load(f)
|
88
|
+
|
89
|
+
return templates.get("truncation_messages", {}).get(template_type, {}).get("message",
|
90
|
+
"--- Output truncated due to size limitations ---")
|
91
|
+
except Exception:
|
92
|
+
# Fallback template if file can't be loaded
|
93
|
+
return "--- Output truncated due to size limitations ---"
|
94
|
+
|
95
|
+
|
96
|
+
def format_truncation_message(output: str, is_truncated: bool, original_tokens: int,
|
97
|
+
original_lines: int, max_lines: int, template_type: str = "python_output") -> str:
|
98
|
+
"""
|
99
|
+
Format the truncated output with a truncation message if needed.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
output: The truncated output
|
103
|
+
is_truncated: Whether truncation occurred
|
104
|
+
original_tokens: Original token count
|
105
|
+
original_lines: Original line count
|
106
|
+
max_lines: Maximum line count used for truncation
|
107
|
+
template_type: Type of template to use
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Formatted output with truncation message if needed
|
111
|
+
"""
|
112
|
+
if not is_truncated:
|
113
|
+
return output
|
114
|
+
|
115
|
+
# Load the appropriate template
|
116
|
+
template = load_truncation_template(template_type)
|
117
|
+
|
118
|
+
# Determine size unit (tokens or KB)
|
119
|
+
if original_tokens > 1000:
|
120
|
+
size_value = original_tokens / 1000
|
121
|
+
size_unit = "K tokens"
|
122
|
+
else:
|
123
|
+
size_value = original_tokens
|
124
|
+
size_unit = "tokens"
|
125
|
+
|
126
|
+
# Format the message
|
127
|
+
message = template.format(
|
128
|
+
original_size=round(size_value, 1),
|
129
|
+
size_unit=size_unit,
|
130
|
+
original_lines=original_lines,
|
131
|
+
max_lines=max_lines
|
132
|
+
)
|
133
|
+
|
134
|
+
# Append the message to the output
|
135
|
+
return f"{output}\n\n{message}"
|
136
|
+
|
137
|
+
|
20
138
|
def make_session_blob(ns: dict) -> bytes:
|
21
139
|
"""
|
22
140
|
Create a serialized blob of the session namespace, excluding unserializable objects.
|
@@ -41,6 +159,146 @@ def make_session_blob(ns: dict) -> bytes:
|
|
41
159
|
return cloudpickle.dumps(clean)
|
42
160
|
|
43
161
|
|
162
|
+
def _run_shell(
|
163
|
+
command: List[str],
|
164
|
+
timeout: int = 10,
|
165
|
+
workdir: str = None
|
166
|
+
) -> Dict[str, Any]:
|
167
|
+
"""
|
168
|
+
Execute a shell command securely with proper timeout and error handling.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
command: List of command parts to execute
|
172
|
+
timeout: Maximum execution time in seconds
|
173
|
+
workdir: Working directory for command execution
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
Dictionary containing execution results with keys:
|
177
|
+
- stdout: stdout from the execution
|
178
|
+
- stderr: stderr from the execution
|
179
|
+
- exit_code: exit code from the command
|
180
|
+
"""
|
181
|
+
try:
|
182
|
+
# Set working directory if provided
|
183
|
+
cwd = os.path.expanduser(workdir) if workdir else None
|
184
|
+
|
185
|
+
# Check if this is a command that needs bash -c wrapping
|
186
|
+
if len(command) > 0:
|
187
|
+
# Special handling for bash login shells to avoid profile loading errors
|
188
|
+
if command[0] == "bash" and len(command) >= 3 and command[1] == "-lc":
|
189
|
+
# Create a clean environment that doesn't load user profile files
|
190
|
+
env = os.environ.copy()
|
191
|
+
env.update({
|
192
|
+
"BASH_ENV": "/dev/null",
|
193
|
+
"ENV": "/dev/null",
|
194
|
+
"BASH_PROFILE": "/dev/null",
|
195
|
+
"PROFILE": "/dev/null"
|
196
|
+
})
|
197
|
+
# Replace -lc with -c to avoid loading login profiles
|
198
|
+
modified_command = ["bash", "-c", command[2]]
|
199
|
+
process = subprocess.run(
|
200
|
+
modified_command,
|
201
|
+
shell=False,
|
202
|
+
capture_output=True,
|
203
|
+
text=True,
|
204
|
+
timeout=timeout,
|
205
|
+
cwd=cwd,
|
206
|
+
check=False,
|
207
|
+
env=env
|
208
|
+
)
|
209
|
+
# If the command already uses bash -c, use it directly
|
210
|
+
# This handles heredoc syntax and other complex shell constructs
|
211
|
+
elif command[0] == "bash" and len(command) >= 3 and command[1] == "-c":
|
212
|
+
process = subprocess.run(
|
213
|
+
command,
|
214
|
+
shell=False, # No need for shell=True as we're explicitly using bash -c
|
215
|
+
capture_output=True,
|
216
|
+
text=True,
|
217
|
+
timeout=timeout,
|
218
|
+
cwd=cwd,
|
219
|
+
check=False
|
220
|
+
)
|
221
|
+
# Special handling for interpreter commands with inline code execution flags
|
222
|
+
# This covers python -c, node -e, ruby -e, perl -e, etc.
|
223
|
+
elif len(command) >= 3 and command[0] in ["python", "node", "ruby", "perl", "php", "deno"] and command[1] in ["-c", "-e", "--eval", "--execute"]:
|
224
|
+
# Execute the interpreter command directly without shell wrapping
|
225
|
+
process = subprocess.run(
|
226
|
+
command,
|
227
|
+
shell=False,
|
228
|
+
capture_output=True,
|
229
|
+
text=True,
|
230
|
+
timeout=timeout,
|
231
|
+
cwd=cwd,
|
232
|
+
check=False
|
233
|
+
)
|
234
|
+
else:
|
235
|
+
# Check if the command contains heredoc syntax
|
236
|
+
command_str = " ".join(command)
|
237
|
+
if "<<" in command_str and any(f"<<'{token}'" in command_str or f'<<"{token}"' in command_str or f"<<{token}" in command_str for token in ["EOF", "EOL", "END", "HEREDOC", "PY", "JS", "RUBY", "PHP"]):
|
238
|
+
# For commands with heredoc, pass directly to bash -c without additional quoting
|
239
|
+
process = subprocess.run(
|
240
|
+
["bash", "-c", command_str],
|
241
|
+
shell=False,
|
242
|
+
capture_output=True,
|
243
|
+
text=True,
|
244
|
+
timeout=timeout,
|
245
|
+
cwd=cwd,
|
246
|
+
check=False
|
247
|
+
)
|
248
|
+
else:
|
249
|
+
# For all other commands, wrap in bash -c to handle shell operators
|
250
|
+
# and properly quote arguments that need quoting
|
251
|
+
|
252
|
+
# Shell operators that should not be quoted
|
253
|
+
shell_operators = ['|', '&&', '||', '>', '<', '>>', '<<', ';']
|
254
|
+
|
255
|
+
# Quote each part that needs quoting
|
256
|
+
quoted_parts = []
|
257
|
+
for part in command:
|
258
|
+
if part in shell_operators:
|
259
|
+
# Don't quote shell operators
|
260
|
+
quoted_parts.append(part)
|
261
|
+
else:
|
262
|
+
# Use shlex.quote to properly escape special characters
|
263
|
+
quoted_parts.append(shlex.quote(part))
|
264
|
+
|
265
|
+
shell_command = " ".join(quoted_parts)
|
266
|
+
process = subprocess.run(
|
267
|
+
["bash", "-c", shell_command],
|
268
|
+
shell=False, # Using explicit bash -c instead of shell=True
|
269
|
+
capture_output=True,
|
270
|
+
text=True,
|
271
|
+
timeout=timeout,
|
272
|
+
cwd=cwd,
|
273
|
+
check=False
|
274
|
+
)
|
275
|
+
else:
|
276
|
+
# Empty command
|
277
|
+
return {
|
278
|
+
"stdout": "",
|
279
|
+
"stderr": "Empty command",
|
280
|
+
"exit_code": 1
|
281
|
+
}
|
282
|
+
|
283
|
+
return {
|
284
|
+
"stdout": process.stdout,
|
285
|
+
"stderr": process.stderr,
|
286
|
+
"exit_code": process.returncode
|
287
|
+
}
|
288
|
+
except subprocess.TimeoutExpired:
|
289
|
+
return {
|
290
|
+
"stdout": "",
|
291
|
+
"stderr": f"Command timed out after {timeout} seconds",
|
292
|
+
"exit_code": 124 # Standard timeout exit code
|
293
|
+
}
|
294
|
+
except Exception as e:
|
295
|
+
return {
|
296
|
+
"stdout": "",
|
297
|
+
"stderr": f"Error executing command: {str(e)}",
|
298
|
+
"exit_code": 1
|
299
|
+
}
|
300
|
+
|
301
|
+
|
44
302
|
def _run_python(
|
45
303
|
code: str,
|
46
304
|
globals_dict: Dict[str, Any] | None = None,
|
@@ -48,6 +306,7 @@ def _run_python(
|
|
48
306
|
authorized_imports: List[str] | None = None,
|
49
307
|
authorized_functions: List[str] | None = None,
|
50
308
|
trusted_code: bool = False,
|
309
|
+
check_string_obfuscation: bool = True,
|
51
310
|
):
|
52
311
|
"""
|
53
312
|
Execute Python code in a controlled environment with proper error handling.
|
@@ -59,6 +318,7 @@ def _run_python(
|
|
59
318
|
authorized_imports: List of authorized imports that user code may access. Wildcards (e.g. "numpy.*") are supported. A value of None disables the allow-list and only blocks dangerous modules.
|
60
319
|
authorized_functions: List of authorized dangerous functions that user code may access. A value of None disables the allow-list and blocks all dangerous functions.
|
61
320
|
trusted_code: If True, skip security checks. Should only be used for framework code, tools, or default executed code.
|
321
|
+
check_string_obfuscation: If True (default), check for string obfuscation techniques. Set to False to allow legitimate use of base64 encoding and other string manipulations.
|
62
322
|
|
63
323
|
Returns:
|
64
324
|
Dictionary containing execution results
|
@@ -74,7 +334,8 @@ def _run_python(
|
|
74
334
|
# 1. Static safety analysis – refuse code containing dangerous imports or functions
|
75
335
|
# ------------------------------------------------------------------
|
76
336
|
validate_code_safety(code, authorized_imports=authorized_imports,
|
77
|
-
authorized_functions=authorized_functions, trusted_code=trusted_code
|
337
|
+
authorized_functions=authorized_functions, trusted_code=trusted_code,
|
338
|
+
check_string_obfuscation=check_string_obfuscation)
|
78
339
|
|
79
340
|
# Make copies to avoid mutating the original parameters
|
80
341
|
globals_dict = globals_dict or {}
|
tinyagent/hooks/__init__.py
CHANGED
@@ -2,4 +2,6 @@
|
|
2
2
|
from .rich_ui_callback import RichUICallback
|
3
3
|
from .rich_code_ui_callback import RichCodeUICallback
|
4
4
|
from .logging_manager import LoggingManager
|
5
|
-
|
5
|
+
from .token_tracker import TokenTracker, UsageStats, create_token_tracker
|
6
|
+
|
7
|
+
__all__ = ["RichUICallback", "RichCodeUICallback", "LoggingManager", "TokenTracker", "UsageStats", "create_token_tracker"]
|