tinyagent-py 0.0.15__py3-none-any.whl → 0.0.16rc0__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.
@@ -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
- if command[0] == "bash" and len(command) >= 3 and command[1] in ["-c", "-lc"]:
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
- else:
84
- # For all other commands, wrap in bash -c to handle shell operators
85
- # and properly quote arguments that need quoting
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
- ["bash", "-c", shell_command],
103
- shell=False, # Using explicit bash -c instead of shell=True
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
+ ---