tinyagent-py 0.0.15__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/providers/__init__.py +14 -1
- tinyagent/code_agent/providers/base.py +29 -1
- tinyagent/code_agent/providers/modal_provider.py +9 -0
- tinyagent/code_agent/providers/seatbelt_provider.py +1065 -0
- tinyagent/code_agent/tiny_code_agent.py +674 -5
- tinyagent/code_agent/utils.py +187 -22
- tinyagent/prompts/truncation.yaml +13 -0
- tinyagent/tiny_agent.py +402 -49
- {tinyagent_py-0.0.15.dist-info → tinyagent_py-0.0.16.dist-info}/METADATA +25 -1
- {tinyagent_py-0.0.15.dist-info → tinyagent_py-0.0.16.dist-info}/RECORD +13 -11
- {tinyagent_py-0.0.15.dist-info → tinyagent_py-0.0.16.dist-info}/WHEEL +0 -0
- {tinyagent_py-0.0.15.dist-info → tinyagent_py-0.0.16.dist-info}/licenses/LICENSE +0 -0
- {tinyagent_py-0.0.15.dist-info → tinyagent_py-0.0.16.dist-info}/top_level.txt +0 -0
tinyagent/code_agent/utils.py
CHANGED
@@ -2,9 +2,12 @@ import sys
|
|
2
2
|
import cloudpickle
|
3
3
|
import subprocess
|
4
4
|
import os
|
5
|
-
from typing import Dict, Any, List
|
5
|
+
from typing import Dict, Any, List, Tuple
|
6
6
|
from .safety import validate_code_safety, function_safety_context
|
7
7
|
import shlex
|
8
|
+
import yaml
|
9
|
+
from pathlib import Path
|
10
|
+
import re
|
8
11
|
|
9
12
|
|
10
13
|
def clean_response(resp: Dict[str, Any]) -> Dict[str, Any]:
|
@@ -20,6 +23,118 @@ def clean_response(resp: Dict[str, Any]) -> Dict[str, Any]:
|
|
20
23
|
return {k: v for k, v in resp.items() if k in ['printed_output', 'return_value', 'stderr', 'error_traceback']}
|
21
24
|
|
22
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
|
+
|
23
138
|
def make_session_blob(ns: dict) -> bytes:
|
24
139
|
"""
|
25
140
|
Create a serialized blob of the session namespace, excluding unserializable objects.
|
@@ -69,8 +184,31 @@ def _run_shell(
|
|
69
184
|
|
70
185
|
# Check if this is a command that needs bash -c wrapping
|
71
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
|
+
)
|
72
209
|
# If the command already uses bash -c, use it directly
|
73
|
-
|
210
|
+
# This handles heredoc syntax and other complex shell constructs
|
211
|
+
elif command[0] == "bash" and len(command) >= 3 and command[1] == "-c":
|
74
212
|
process = subprocess.run(
|
75
213
|
command,
|
76
214
|
shell=False, # No need for shell=True as we're explicitly using bash -c
|
@@ -80,33 +218,60 @@ def _run_shell(
|
|
80
218
|
cwd=cwd,
|
81
219
|
check=False
|
82
220
|
)
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
# Shell operators that should not be quoted
|
88
|
-
shell_operators = ['|', '&&', '||', '>', '<', '>>', '<<', ';']
|
89
|
-
|
90
|
-
# Quote each part that needs quoting
|
91
|
-
quoted_parts = []
|
92
|
-
for part in command:
|
93
|
-
if part in shell_operators:
|
94
|
-
# Don't quote shell operators
|
95
|
-
quoted_parts.append(part)
|
96
|
-
else:
|
97
|
-
# Use shlex.quote to properly escape special characters
|
98
|
-
quoted_parts.append(shlex.quote(part))
|
99
|
-
|
100
|
-
shell_command = " ".join(quoted_parts)
|
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
|
101
225
|
process = subprocess.run(
|
102
|
-
|
103
|
-
shell=False,
|
226
|
+
command,
|
227
|
+
shell=False,
|
104
228
|
capture_output=True,
|
105
229
|
text=True,
|
106
230
|
timeout=timeout,
|
107
231
|
cwd=cwd,
|
108
232
|
check=False
|
109
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
|
+
)
|
110
275
|
else:
|
111
276
|
# Empty command
|
112
277
|
return {
|
@@ -0,0 +1,13 @@
|
|
1
|
+
truncation_messages:
|
2
|
+
python_output:
|
3
|
+
message: |-
|
4
|
+
---
|
5
|
+
**Output Truncated**: The original output was {original_size} {size_unit} ({original_lines} lines). Showing only the first {max_lines} lines.
|
6
|
+
To get more detailed output, please make your request more specific or adjust the output size.
|
7
|
+
---
|
8
|
+
bash_output:
|
9
|
+
message: |-
|
10
|
+
---
|
11
|
+
**Output Truncated**: The original output was {original_size} {size_unit} ({original_lines} lines). Showing only the first {max_lines} lines.
|
12
|
+
To get more detailed output, please use more specific commands or add filtering.
|
13
|
+
---
|