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
@@ -1,20 +1,44 @@
|
|
1
1
|
import traceback
|
2
|
+
import os
|
3
|
+
import json
|
2
4
|
from textwrap import dedent
|
3
5
|
from typing import Optional, List, Dict, Any
|
4
6
|
from pathlib import Path
|
5
7
|
from tinyagent import TinyAgent, tool
|
6
8
|
from tinyagent.hooks.logging_manager import LoggingManager
|
9
|
+
from tinyagent.hooks.rich_code_ui_callback import RichCodeUICallback
|
10
|
+
from tinyagent.hooks.jupyter_notebook_callback import JupyterNotebookCallback
|
7
11
|
from .providers.base import CodeExecutionProvider
|
8
12
|
from .providers.modal_provider import ModalProvider
|
13
|
+
from .providers.seatbelt_provider import SeatbeltProvider
|
9
14
|
from .helper import translate_tool_for_code_agent, load_template, render_system_prompt, prompt_code_example, prompt_qwen_helper
|
15
|
+
from .utils import truncate_output, format_truncation_message
|
16
|
+
import datetime
|
10
17
|
|
11
18
|
|
19
|
+
DEFAULT_SUMMARY_SYSTEM_PROMPT = (
|
20
|
+
"You are an expert coding assistant. Your goal is to generate a concise, structured summary "
|
21
|
+
"of the conversation below that captures all essential information needed to continue "
|
22
|
+
"development after context replacement. Include tasks performed, code areas modified or "
|
23
|
+
"reviewed, key decisions or assumptions, test results or errors, and outstanding tasks or next steps."
|
24
|
+
|
25
|
+
)
|
26
|
+
|
12
27
|
class TinyCodeAgent:
|
13
28
|
"""
|
14
29
|
A TinyAgent specialized for code execution tasks.
|
15
30
|
|
16
31
|
This class provides a high-level interface for creating agents that can execute
|
17
|
-
Python code using various providers (Modal,
|
32
|
+
Python code using various providers (Modal, SeatbeltProvider for macOS sandboxing, etc.).
|
33
|
+
|
34
|
+
Features include:
|
35
|
+
- Code execution in sandboxed environments
|
36
|
+
- Shell command execution with safety checks
|
37
|
+
- Environment variable management (SeatbeltProvider)
|
38
|
+
- File system access controls
|
39
|
+
- Memory management and conversation summarization
|
40
|
+
- Git checkpoint automation
|
41
|
+
- Output truncation controls
|
18
42
|
"""
|
19
43
|
|
20
44
|
def __init__(
|
@@ -27,10 +51,17 @@ class TinyCodeAgent:
|
|
27
51
|
code_tools: Optional[List[Any]] = None,
|
28
52
|
authorized_imports: Optional[List[str]] = None,
|
29
53
|
system_prompt_template: Optional[str] = None,
|
54
|
+
system_prompt: Optional[str] = None,
|
30
55
|
provider_config: Optional[Dict[str, Any]] = None,
|
31
56
|
user_variables: Optional[Dict[str, Any]] = None,
|
32
57
|
pip_packages: Optional[List[str]] = None,
|
33
58
|
local_execution: bool = False,
|
59
|
+
check_string_obfuscation: bool = True,
|
60
|
+
default_workdir: Optional[str] = None,
|
61
|
+
summary_config: Optional[Dict[str, Any]] = None,
|
62
|
+
ui: Optional[str] = None,
|
63
|
+
truncation_config: Optional[Dict[str, Any]] = None,
|
64
|
+
auto_git_checkpoint: bool = False,
|
34
65
|
**agent_kwargs
|
35
66
|
):
|
36
67
|
"""
|
@@ -50,7 +81,38 @@ class TinyCodeAgent:
|
|
50
81
|
pip_packages: List of additional Python packages to install in Modal environment
|
51
82
|
local_execution: If True, uses Modal's .local() method for local execution.
|
52
83
|
If False, uses Modal's .remote() method for cloud execution (default: False)
|
84
|
+
check_string_obfuscation: If True (default), check for string obfuscation techniques. Set to False to allow
|
85
|
+
legitimate use of base64 encoding and other string manipulations.
|
86
|
+
default_workdir: Default working directory for shell commands. If None, the current working directory is used.
|
87
|
+
summary_config: Optional configuration for generating conversation summaries
|
88
|
+
ui: The user interface callback to use ('rich', 'jupyter', or None).
|
89
|
+
truncation_config: Configuration for output truncation (max_tokens, max_lines)
|
90
|
+
auto_git_checkpoint: If True, automatically create git checkpoints after each successful shell command
|
53
91
|
**agent_kwargs: Additional arguments passed to TinyAgent
|
92
|
+
|
93
|
+
Provider Config Options:
|
94
|
+
For SeatbeltProvider:
|
95
|
+
- seatbelt_profile: String containing seatbelt profile rules
|
96
|
+
- seatbelt_profile_path: Path to a file containing seatbelt profile rules
|
97
|
+
- python_env_path: Path to the Python environment to use
|
98
|
+
- bypass_shell_safety: If True, bypass shell command safety checks (default: True for seatbelt)
|
99
|
+
- additional_safe_shell_commands: Additional shell commands to consider safe
|
100
|
+
- additional_safe_control_operators: Additional shell control operators to consider safe
|
101
|
+
- additional_read_dirs: List of additional directories to allow read access to
|
102
|
+
- additional_write_dirs: List of additional directories to allow write access to
|
103
|
+
- environment_variables: Dictionary of environment variables to make available in the sandbox
|
104
|
+
|
105
|
+
For ModalProvider:
|
106
|
+
- pip_packages: List of additional Python packages to install
|
107
|
+
- authorized_imports: List of authorized Python imports
|
108
|
+
- bypass_shell_safety: If True, bypass shell command safety checks (default: False for modal)
|
109
|
+
- additional_safe_shell_commands: Additional shell commands to consider safe
|
110
|
+
- additional_safe_control_operators: Additional shell control operators to consider safe
|
111
|
+
|
112
|
+
Truncation Config Options:
|
113
|
+
- max_tokens: Maximum number of tokens to keep in output (default: 3000)
|
114
|
+
- max_lines: Maximum number of lines to keep in output (default: 250)
|
115
|
+
- enabled: Whether truncation is enabled (default: True)
|
54
116
|
"""
|
55
117
|
self.model = model
|
56
118
|
self.api_key = api_key
|
@@ -63,6 +125,17 @@ class TinyCodeAgent:
|
|
63
125
|
self.pip_packages = pip_packages or []
|
64
126
|
self.local_execution = local_execution
|
65
127
|
self.provider = provider # Store provider type for reuse
|
128
|
+
self.check_string_obfuscation = check_string_obfuscation
|
129
|
+
self.default_workdir = default_workdir or os.getcwd() # Default to current working directory if not specified
|
130
|
+
self.auto_git_checkpoint = auto_git_checkpoint # Enable/disable automatic git checkpoints
|
131
|
+
|
132
|
+
# Set up truncation configuration with defaults
|
133
|
+
default_truncation = {
|
134
|
+
"max_tokens": 3000,
|
135
|
+
"max_lines": 250,
|
136
|
+
"enabled": True
|
137
|
+
}
|
138
|
+
self.truncation_config = {**default_truncation, **(truncation_config or {})}
|
66
139
|
|
67
140
|
# Create the code execution provider
|
68
141
|
self.code_provider = self._create_provider(provider, self.provider_config)
|
@@ -72,23 +145,32 @@ class TinyCodeAgent:
|
|
72
145
|
self.code_provider.set_user_variables(self.user_variables)
|
73
146
|
|
74
147
|
# Build system prompt
|
75
|
-
self.
|
148
|
+
self.static_system_prompt= system_prompt
|
149
|
+
self.system_prompt = self._build_system_prompt(system_prompt_template)
|
150
|
+
|
76
151
|
|
77
|
-
|
152
|
+
self.summary_config = summary_config or {}
|
153
|
+
|
154
|
+
# Create the underlying TinyAgent with summary configuration
|
78
155
|
self.agent = TinyAgent(
|
79
156
|
model=model,
|
80
157
|
api_key=api_key,
|
81
158
|
system_prompt=self.system_prompt,
|
82
159
|
logger=log_manager.get_logger('tinyagent.tiny_agent') if log_manager else None,
|
160
|
+
summary_config=summary_config,
|
83
161
|
**agent_kwargs
|
84
162
|
)
|
85
163
|
|
86
|
-
# Add the code execution
|
87
|
-
self.
|
164
|
+
# Add the code execution tools
|
165
|
+
self._setup_code_execution_tools()
|
88
166
|
|
89
167
|
# Add LLM tools (not code tools - those go to the provider)
|
90
168
|
if self.tools:
|
91
169
|
self.agent.add_tools(self.tools)
|
170
|
+
|
171
|
+
# Add the selected UI callback
|
172
|
+
if ui:
|
173
|
+
self.add_ui_callback(ui)
|
92
174
|
|
93
175
|
def _create_provider(self, provider_type: str, config: Dict[str, Any]) -> CodeExecutionProvider:
|
94
176
|
"""Create a code execution provider based on the specified type."""
|
@@ -104,20 +186,81 @@ class TinyCodeAgent:
|
|
104
186
|
final_config = config.copy()
|
105
187
|
final_config["pip_packages"] = final_pip_packages
|
106
188
|
final_config["authorized_imports"] = final_authorized_imports
|
189
|
+
final_config["check_string_obfuscation"] = self.check_string_obfuscation
|
190
|
+
|
191
|
+
# Shell safety configuration (default to False for Modal)
|
192
|
+
bypass_shell_safety = config.get("bypass_shell_safety", False)
|
193
|
+
additional_safe_shell_commands = config.get("additional_safe_shell_commands", None)
|
194
|
+
additional_safe_control_operators = config.get("additional_safe_control_operators", None)
|
107
195
|
|
108
196
|
return ModalProvider(
|
109
197
|
log_manager=self.log_manager,
|
110
198
|
code_tools=self.code_tools,
|
111
199
|
local_execution=self.local_execution,
|
200
|
+
bypass_shell_safety=bypass_shell_safety,
|
201
|
+
additional_safe_shell_commands=additional_safe_shell_commands,
|
202
|
+
additional_safe_control_operators=additional_safe_control_operators,
|
112
203
|
**final_config
|
113
204
|
)
|
205
|
+
elif provider_type.lower() == "seatbelt":
|
206
|
+
# Check if seatbelt is supported on this system
|
207
|
+
if not SeatbeltProvider.is_supported():
|
208
|
+
raise ValueError("Seatbelt provider is not supported on this system. It requires macOS with sandbox-exec.")
|
209
|
+
|
210
|
+
# Seatbelt only works with local execution
|
211
|
+
if not self.local_execution:
|
212
|
+
raise ValueError("Seatbelt provider requires local execution mode. Please set local_execution=True.")
|
213
|
+
|
214
|
+
# Create a copy of the config without the parameters we'll pass directly
|
215
|
+
filtered_config = config.copy()
|
216
|
+
for key in ['seatbelt_profile', 'seatbelt_profile_path', 'python_env_path',
|
217
|
+
'bypass_shell_safety', 'additional_safe_shell_commands',
|
218
|
+
'additional_safe_control_operators', 'additional_read_dirs',
|
219
|
+
'additional_write_dirs', 'environment_variables']:
|
220
|
+
if key in filtered_config:
|
221
|
+
filtered_config.pop(key)
|
222
|
+
|
223
|
+
# Get seatbelt profile configuration
|
224
|
+
seatbelt_profile = config.get("seatbelt_profile", None)
|
225
|
+
seatbelt_profile_path = config.get("seatbelt_profile_path", None)
|
226
|
+
python_env_path = config.get("python_env_path", None)
|
227
|
+
|
228
|
+
# Shell safety configuration (default to True for Seatbelt)
|
229
|
+
bypass_shell_safety = config.get("bypass_shell_safety", True)
|
230
|
+
additional_safe_shell_commands = config.get("additional_safe_shell_commands", None)
|
231
|
+
additional_safe_control_operators = config.get("additional_safe_control_operators", None)
|
232
|
+
|
233
|
+
# Additional directory access configuration
|
234
|
+
additional_read_dirs = config.get("additional_read_dirs", None)
|
235
|
+
additional_write_dirs = config.get("additional_write_dirs", None)
|
236
|
+
|
237
|
+
# Environment variables to make available in the sandbox
|
238
|
+
environment_variables = config.get("environment_variables", {})
|
239
|
+
|
240
|
+
# Create the seatbelt provider
|
241
|
+
return SeatbeltProvider(
|
242
|
+
log_manager=self.log_manager,
|
243
|
+
code_tools=self.code_tools,
|
244
|
+
seatbelt_profile=seatbelt_profile,
|
245
|
+
seatbelt_profile_path=seatbelt_profile_path,
|
246
|
+
python_env_path=python_env_path,
|
247
|
+
bypass_shell_safety=bypass_shell_safety,
|
248
|
+
additional_safe_shell_commands=additional_safe_shell_commands,
|
249
|
+
additional_safe_control_operators=additional_safe_control_operators,
|
250
|
+
additional_read_dirs=additional_read_dirs,
|
251
|
+
additional_write_dirs=additional_write_dirs,
|
252
|
+
environment_variables=environment_variables,
|
253
|
+
**filtered_config
|
254
|
+
)
|
114
255
|
else:
|
115
256
|
raise ValueError(f"Unsupported provider type: {provider_type}")
|
116
257
|
|
117
258
|
def _build_system_prompt(self, template_path: Optional[str] = None) -> str:
|
118
259
|
"""Build the system prompt for the code agent."""
|
119
260
|
# Use default template if none provided
|
120
|
-
if
|
261
|
+
if self.static_system_prompt is not None:
|
262
|
+
return self.static_system_prompt
|
263
|
+
elif template_path is None :
|
121
264
|
template_path = str(Path(__file__).parent.parent / "prompts" / "code_agent.yaml")
|
122
265
|
|
123
266
|
# Translate code tools to code agent format
|
@@ -242,8 +385,8 @@ class TinyCodeAgent:
|
|
242
385
|
|
243
386
|
return "\n".join(code_tools_lines)
|
244
387
|
|
245
|
-
def
|
246
|
-
"""Set up the
|
388
|
+
def _setup_code_execution_tools(self):
|
389
|
+
"""Set up the code execution tools using the code provider."""
|
247
390
|
@tool(name="run_python", description=dedent("""
|
248
391
|
This tool receives Python code and executes it in a sandboxed environment.
|
249
392
|
During each intermediate step, you can use 'print()' to save important information.
|
@@ -271,7 +414,25 @@ class TinyCodeAgent:
|
|
271
414
|
# This ensures they stay in sync
|
272
415
|
self.user_variables = self.code_provider.get_user_variables()
|
273
416
|
|
274
|
-
|
417
|
+
# Apply truncation if enabled
|
418
|
+
if self.truncation_config["enabled"] and "printed_output" in result:
|
419
|
+
truncated_output, is_truncated, original_tokens, original_lines = truncate_output(
|
420
|
+
result["printed_output"],
|
421
|
+
max_tokens=self.truncation_config["max_tokens"],
|
422
|
+
max_lines=self.truncation_config["max_lines"]
|
423
|
+
)
|
424
|
+
|
425
|
+
if is_truncated:
|
426
|
+
result["printed_output"] = format_truncation_message(
|
427
|
+
truncated_output,
|
428
|
+
is_truncated,
|
429
|
+
original_tokens,
|
430
|
+
original_lines,
|
431
|
+
self.truncation_config["max_lines"],
|
432
|
+
"python_output"
|
433
|
+
)
|
434
|
+
|
435
|
+
return json.dumps(result)
|
275
436
|
except Exception as e:
|
276
437
|
print("!"*100)
|
277
438
|
COLOR = {
|
@@ -286,9 +447,257 @@ class TinyCodeAgent:
|
|
286
447
|
# This ensures any variables that were successfully created/modified are preserved
|
287
448
|
self.user_variables = self.code_provider.get_user_variables()
|
288
449
|
|
289
|
-
return f"Error executing code: {str(e)}"
|
450
|
+
return json.dumps({"error": f"Error executing code: {str(e)}"})
|
451
|
+
|
452
|
+
@tool(name="bash", description=dedent("""
|
453
|
+
This tool executes shell commands securely in a sandboxed environment.
|
454
|
+
Only a limited set of safe commands are allowed for security reasons.
|
455
|
+
Before executing the command, please follow these steps:
|
456
|
+
|
457
|
+
1. Directory Verification:
|
458
|
+
- If the command will create new directories or files, first use ls to verify the parent directory exists and is the correct location
|
459
|
+
- For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
|
460
|
+
|
461
|
+
2. Command Execution:
|
462
|
+
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
|
463
|
+
- Examples of proper quoting:
|
464
|
+
- cd "/Users/name/My Documents" (correct)
|
465
|
+
- cd /Users/name/My Documents (incorrect - will fail)
|
466
|
+
- python "/path/with spaces/script.py" (correct)
|
467
|
+
- python /path/with spaces/script.py (incorrect - will fail)
|
468
|
+
- After ensuring proper quoting, execute the command.
|
469
|
+
- Capture the output of the command.
|
470
|
+
|
471
|
+
Usage notes:
|
472
|
+
- The command argument is required.
|
473
|
+
- You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance.
|
474
|
+
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).
|
475
|
+
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
476
|
+
- If the output is too large, it will be truncated before being returned to you.
|
477
|
+
|
478
|
+
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` first, which all ${PRODUCT_NAME} users have pre-installed.
|
479
|
+
" - When issuing multiple commands, use the ; or && operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).\n" +
|
480
|
+
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
|
481
|
+
<good-example>
|
482
|
+
pytest /foo/bar/tests
|
483
|
+
</good-example>
|
484
|
+
<bad-example>
|
485
|
+
cd /foo/bar && pytest tests
|
486
|
+
</bad-example>
|
487
|
+
|
488
|
+
## IMPORTANT: Bash Tool Usage
|
489
|
+
|
490
|
+
When using the bash tool, you MUST provide all required parameters:
|
491
|
+
|
492
|
+
**Correct Usage:**
|
493
|
+
```
|
494
|
+
bash(
|
495
|
+
command=["ls", "-la"],
|
496
|
+
absolute_workdir="/path/to/directory",
|
497
|
+
description="List files in directory"
|
498
|
+
)
|
499
|
+
```
|
500
|
+
|
501
|
+
**For creating files with content, use these safe patterns:**
|
502
|
+
|
503
|
+
1. **Simple file creation:**
|
504
|
+
```
|
505
|
+
bash(
|
506
|
+
command=["touch", "filename.txt"],
|
507
|
+
absolute_workdir="/working/directory",
|
508
|
+
description="Create empty file"
|
509
|
+
)
|
510
|
+
```
|
511
|
+
|
512
|
+
2. **Write content using cat and heredoc:**
|
513
|
+
```
|
514
|
+
bash(
|
515
|
+
command=["sh", "-c", "cat > filename.txt << 'EOF'\nYour content here\nEOF"],
|
516
|
+
absolute_workdir="/working/directory",
|
517
|
+
description="Create file with content"
|
518
|
+
)
|
519
|
+
```
|
520
|
+
|
521
|
+
3. **Write content using echo:**
|
522
|
+
```
|
523
|
+
bash(
|
524
|
+
command=["sh", "-c", "echo 'Your content' > filename.txt"],
|
525
|
+
absolute_workdir="/working/directory",
|
526
|
+
description="Write content to file"
|
527
|
+
)
|
528
|
+
```
|
529
|
+
|
530
|
+
**Never:**
|
531
|
+
- Call bash() without all required parameters
|
532
|
+
- Use complex nested quotes without testing
|
533
|
+
- Try to create large files in a single command (break into parts)
|
534
|
+
|
535
|
+
Args:
|
536
|
+
command: list[str]: The shell command to execute as a list of strings. Example: ["ls", "-la"] or ["cat", "file.txt"]
|
537
|
+
|
538
|
+
absolute_workdir: str: could be presented workdir in the system prompt or one of the subdirectories of the workdir. This is the only allowed path, and accessing else will result in an error.
|
539
|
+
description: str: A clear, concise description of what this command does in 5-10 words.
|
540
|
+
timeout: int: Maximum execution time in seconds (default: 60).
|
541
|
+
Returns:
|
542
|
+
Dictionary with stdout, stderr, and exit_code from the command execution.
|
543
|
+
If the command is rejected for security reasons, stderr will contain the reason.
|
544
|
+
The stdout will include information about which working directory was used.
|
545
|
+
"""))
|
546
|
+
async def run_shell(command: List[str], absolute_workdir: str, description: str, timeout: int = 60) -> str:
|
547
|
+
"""Execute shell commands securely using the configured provider."""
|
548
|
+
try:
|
549
|
+
# Use the default working directory if none is specified
|
550
|
+
effective_workdir = absolute_workdir or self.default_workdir
|
551
|
+
print(f" {command} to {description}")
|
552
|
+
# Verify that the working directory exists
|
553
|
+
if effective_workdir and not os.path.exists(effective_workdir):
|
554
|
+
return json.dumps({
|
555
|
+
"stdout": "",
|
556
|
+
"stderr": f"Working directory does not exist: {effective_workdir}",
|
557
|
+
"exit_code": 1
|
558
|
+
})
|
559
|
+
|
560
|
+
if effective_workdir and not os.path.isdir(effective_workdir):
|
561
|
+
return json.dumps({
|
562
|
+
"stdout": "",
|
563
|
+
"stderr": f"Path is not a directory: {effective_workdir}",
|
564
|
+
"exit_code": 1
|
565
|
+
})
|
566
|
+
|
567
|
+
result = await self.code_provider.execute_shell(command, timeout, effective_workdir)
|
568
|
+
|
569
|
+
# Apply truncation if enabled
|
570
|
+
if self.truncation_config["enabled"] and "stdout" in result and result["stdout"]:
|
571
|
+
truncated_output, is_truncated, original_tokens, original_lines = truncate_output(
|
572
|
+
result["stdout"],
|
573
|
+
max_tokens=self.truncation_config["max_tokens"],
|
574
|
+
max_lines=self.truncation_config["max_lines"]
|
575
|
+
)
|
576
|
+
|
577
|
+
if is_truncated:
|
578
|
+
result["stdout"] = format_truncation_message(
|
579
|
+
truncated_output,
|
580
|
+
is_truncated,
|
581
|
+
original_tokens,
|
582
|
+
original_lines,
|
583
|
+
self.truncation_config["max_lines"],
|
584
|
+
"bash_output"
|
585
|
+
)
|
586
|
+
|
587
|
+
# Create a git checkpoint if auto_git_checkpoint is enabled
|
588
|
+
if self.auto_git_checkpoint and result.get("exit_code", 1) == 0:
|
589
|
+
checkpoint_result = await self._create_git_checkpoint(command, description, effective_workdir)
|
590
|
+
self.log_manager.get_logger(__name__).info(f"Git checkpoint {effective_workdir} result: {checkpoint_result}")
|
591
|
+
|
592
|
+
return json.dumps(result)
|
593
|
+
except Exception as e:
|
594
|
+
COLOR = {
|
595
|
+
"RED": "\033[91m",
|
596
|
+
"ENDC": "\033[0m",
|
597
|
+
}
|
598
|
+
print(f"{COLOR['RED']}{str(e)}{COLOR['ENDC']}")
|
599
|
+
print(f"{COLOR['RED']}{traceback.format_exc()}{COLOR['ENDC']}")
|
600
|
+
|
601
|
+
return json.dumps({"error": f"Error executing shell command: {str(e)}"})
|
290
602
|
|
291
603
|
self.agent.add_tool(run_python)
|
604
|
+
self.agent.add_tool(run_shell)
|
605
|
+
|
606
|
+
async def _create_git_checkpoint(self, command: List[str], description: str, workdir: str) -> Dict[str, Any]:
|
607
|
+
"""
|
608
|
+
Create a git checkpoint after command execution.
|
609
|
+
|
610
|
+
Args:
|
611
|
+
command: The command that was executed
|
612
|
+
description: Description of the command
|
613
|
+
workdir: Working directory where the command was executed
|
614
|
+
|
615
|
+
Returns:
|
616
|
+
Dictionary with stdout and stderr from the git operations
|
617
|
+
"""
|
618
|
+
try:
|
619
|
+
# Format the command for the commit message
|
620
|
+
cmd_str = " ".join(command)
|
621
|
+
|
622
|
+
# Check if there are changes to commit
|
623
|
+
git_check_cmd = ["bash", "-c", "if ! git diff-index --quiet HEAD --; then echo 'changes_exist'; else echo 'no_changes'; fi"]
|
624
|
+
check_result = await self.code_provider.execute_shell(git_check_cmd, 10, workdir)
|
625
|
+
|
626
|
+
# If no changes or check failed, return early
|
627
|
+
if check_result.get("exit_code", 1) != 0 or "no_changes" in check_result.get("stdout", ""):
|
628
|
+
return {"stdout": "No changes detected, skipping git checkpoint", "stderr": ""}
|
629
|
+
|
630
|
+
# Stage all changes
|
631
|
+
git_add_cmd = ["git", "add", "-A"]
|
632
|
+
add_result = await self.code_provider.execute_shell(git_add_cmd, 30, workdir)
|
633
|
+
|
634
|
+
if add_result.get("exit_code", 1) != 0:
|
635
|
+
return {
|
636
|
+
"stdout": "",
|
637
|
+
"stderr": f"Failed to stage changes: {add_result.get('stderr', '')}"
|
638
|
+
}
|
639
|
+
|
640
|
+
# Create commit with command description and timestamp
|
641
|
+
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
642
|
+
commit_msg = f"Checkpoint: {description} @ {timestamp}\n\nCommand: {cmd_str}"
|
643
|
+
git_commit_cmd = ["git", "commit", "-m", commit_msg, "--no-gpg-sign"]
|
644
|
+
commit_result = await self.code_provider.execute_shell(git_commit_cmd, 30, workdir)
|
645
|
+
|
646
|
+
if commit_result.get("exit_code", 1) != 0:
|
647
|
+
return {
|
648
|
+
"stdout": "",
|
649
|
+
"stderr": f"Failed to create commit: {commit_result.get('stderr', '')}"
|
650
|
+
}
|
651
|
+
|
652
|
+
# Get the first line of the commit message without using split with \n in f-string
|
653
|
+
first_line = commit_msg.split("\n")[0]
|
654
|
+
return {
|
655
|
+
"stdout": f"✓ Git checkpoint created: {first_line}",
|
656
|
+
"stderr": ""
|
657
|
+
}
|
658
|
+
except Exception as e:
|
659
|
+
return {
|
660
|
+
"stdout": "",
|
661
|
+
"stderr": f"Error creating git checkpoint: {str(e)}"
|
662
|
+
}
|
663
|
+
|
664
|
+
def set_default_workdir(self, workdir: str, create_if_not_exists: bool = False):
|
665
|
+
"""
|
666
|
+
Set the default working directory for shell commands.
|
667
|
+
|
668
|
+
Args:
|
669
|
+
workdir: The path to use as the default working directory
|
670
|
+
create_if_not_exists: If True, create the directory if it doesn't exist
|
671
|
+
|
672
|
+
Raises:
|
673
|
+
ValueError: If the directory doesn't exist and create_if_not_exists is False
|
674
|
+
OSError: If there's an error creating the directory
|
675
|
+
"""
|
676
|
+
workdir = os.path.expanduser(workdir) # Expand user directory if needed
|
677
|
+
|
678
|
+
if not os.path.exists(workdir):
|
679
|
+
if create_if_not_exists:
|
680
|
+
try:
|
681
|
+
os.makedirs(workdir, exist_ok=True)
|
682
|
+
print(f"Created directory: {workdir}")
|
683
|
+
except OSError as e:
|
684
|
+
raise OSError(f"Failed to create directory {workdir}: {str(e)}")
|
685
|
+
else:
|
686
|
+
raise ValueError(f"Directory does not exist: {workdir}")
|
687
|
+
|
688
|
+
if not os.path.isdir(workdir):
|
689
|
+
raise ValueError(f"Path is not a directory: {workdir}")
|
690
|
+
|
691
|
+
self.default_workdir = workdir
|
692
|
+
|
693
|
+
def get_default_workdir(self) -> str:
|
694
|
+
"""
|
695
|
+
Get the current default working directory for shell commands.
|
696
|
+
|
697
|
+
Returns:
|
698
|
+
The current default working directory path
|
699
|
+
"""
|
700
|
+
return self.default_workdir
|
292
701
|
|
293
702
|
async def run(self, user_input: str, max_turns: int = 10) -> str:
|
294
703
|
"""
|
@@ -303,6 +712,22 @@ class TinyCodeAgent:
|
|
303
712
|
"""
|
304
713
|
return await self.agent.run(user_input, max_turns)
|
305
714
|
|
715
|
+
async def resume(self, max_turns: int = 10) -> str:
|
716
|
+
"""
|
717
|
+
Resume the conversation without adding a new user message.
|
718
|
+
|
719
|
+
This method continues the conversation from the current state,
|
720
|
+
allowing the agent to process the existing conversation history
|
721
|
+
and potentially take additional actions.
|
722
|
+
|
723
|
+
Args:
|
724
|
+
max_turns: Maximum number of conversation turns
|
725
|
+
|
726
|
+
Returns:
|
727
|
+
The agent's response
|
728
|
+
"""
|
729
|
+
return await self.agent.resume(max_turns)
|
730
|
+
|
306
731
|
async def connect_to_server(self, command: str, args: List[str], **kwargs):
|
307
732
|
"""Connect to an MCP server."""
|
308
733
|
return await self.agent.connect_to_server(command, args, **kwargs)
|
@@ -505,6 +930,17 @@ class TinyCodeAgent:
|
|
505
930
|
"""
|
506
931
|
return self.authorized_imports.copy()
|
507
932
|
|
933
|
+
@classmethod
|
934
|
+
def is_seatbelt_supported(cls) -> bool:
|
935
|
+
"""
|
936
|
+
Check if the seatbelt provider is supported on this system.
|
937
|
+
|
938
|
+
Returns:
|
939
|
+
True if seatbelt is supported (macOS with sandbox-exec), False otherwise
|
940
|
+
"""
|
941
|
+
from .providers.seatbelt_provider import SeatbeltProvider
|
942
|
+
return SeatbeltProvider.is_supported()
|
943
|
+
|
508
944
|
def remove_authorized_import(self, import_name: str):
|
509
945
|
"""
|
510
946
|
Remove an authorized import.
|
@@ -551,6 +987,191 @@ class TinyCodeAgent:
|
|
551
987
|
"""Get the session ID."""
|
552
988
|
return self.agent.session_id
|
553
989
|
|
990
|
+
def set_check_string_obfuscation(self, enabled: bool):
|
991
|
+
"""
|
992
|
+
Enable or disable string obfuscation detection.
|
993
|
+
|
994
|
+
Args:
|
995
|
+
enabled: If True, check for string obfuscation techniques. If False, allow
|
996
|
+
legitimate use of base64 encoding and other string manipulations.
|
997
|
+
"""
|
998
|
+
self.check_string_obfuscation = enabled
|
999
|
+
|
1000
|
+
# Update the provider with the new setting
|
1001
|
+
if hasattr(self.code_provider, 'check_string_obfuscation'):
|
1002
|
+
self.code_provider.check_string_obfuscation = enabled
|
1003
|
+
|
1004
|
+
async def summarize(self) -> str:
|
1005
|
+
"""
|
1006
|
+
Generate a summary of the current conversation history.
|
1007
|
+
|
1008
|
+
Args:
|
1009
|
+
Returns:
|
1010
|
+
A string containing the conversation summary
|
1011
|
+
"""
|
1012
|
+
# Use the underlying TinyAgent's summarize_conversation method
|
1013
|
+
return await self.agent.summarize()
|
1014
|
+
|
1015
|
+
async def compact(self) -> bool:
|
1016
|
+
"""
|
1017
|
+
Compact the conversation history by replacing it with a summary.
|
1018
|
+
|
1019
|
+
This method delegates to the underlying TinyAgent's compact method,
|
1020
|
+
which:
|
1021
|
+
1. Generates a summary of the current conversation
|
1022
|
+
2. If successful, replaces the conversation with just [system, user] messages
|
1023
|
+
where the user message contains the summary
|
1024
|
+
3. Returns True if compaction was successful, False otherwise
|
1025
|
+
|
1026
|
+
Returns:
|
1027
|
+
Boolean indicating whether the compaction was successful
|
1028
|
+
"""
|
1029
|
+
return await self.agent.compact()
|
1030
|
+
|
1031
|
+
def add_ui_callback(self, ui_type: str, optimized: bool = True):
|
1032
|
+
"""
|
1033
|
+
Adds a UI callback to the agent based on the type.
|
1034
|
+
|
1035
|
+
Args:
|
1036
|
+
ui_type: The type of UI callback ('rich' or 'jupyter')
|
1037
|
+
optimized: Whether to use the optimized version (default: True for better performance)
|
1038
|
+
"""
|
1039
|
+
if ui_type == 'rich':
|
1040
|
+
ui_callback = RichCodeUICallback(
|
1041
|
+
logger=self.log_manager.get_logger('tinyagent.hooks.rich_code_ui_callback') if self.log_manager else None
|
1042
|
+
)
|
1043
|
+
self.add_callback(ui_callback)
|
1044
|
+
elif ui_type == 'jupyter':
|
1045
|
+
if optimized:
|
1046
|
+
from tinyagent.hooks.jupyter_notebook_callback import OptimizedJupyterNotebookCallback
|
1047
|
+
ui_callback = OptimizedJupyterNotebookCallback(
|
1048
|
+
logger=self.log_manager.get_logger('tinyagent.hooks.jupyter_notebook_callback') if self.log_manager else None,
|
1049
|
+
max_visible_turns=20, # Limit visible turns for performance
|
1050
|
+
max_content_length=100000, # Limit total content
|
1051
|
+
enable_markdown=True, # Keep markdown but optimized
|
1052
|
+
show_raw_responses=False # Show formatted responses
|
1053
|
+
)
|
1054
|
+
else:
|
1055
|
+
ui_callback = JupyterNotebookCallback(
|
1056
|
+
logger=self.log_manager.get_logger('tinyagent.hooks.jupyter_notebook_callback') if self.log_manager else None
|
1057
|
+
)
|
1058
|
+
self.add_callback(ui_callback)
|
1059
|
+
else:
|
1060
|
+
self.log_manager.get_logger(__name__).warning(f"Unknown UI type: {ui_type}. No UI callback will be added.")
|
1061
|
+
|
1062
|
+
def set_truncation_config(self, config: Dict[str, Any]):
|
1063
|
+
"""
|
1064
|
+
Set the truncation configuration.
|
1065
|
+
|
1066
|
+
Args:
|
1067
|
+
config: Dictionary containing truncation configuration options:
|
1068
|
+
- max_tokens: Maximum number of tokens to keep in output
|
1069
|
+
- max_lines: Maximum number of lines to keep in output
|
1070
|
+
- enabled: Whether truncation is enabled
|
1071
|
+
"""
|
1072
|
+
self.truncation_config.update(config)
|
1073
|
+
|
1074
|
+
def get_truncation_config(self) -> Dict[str, Any]:
|
1075
|
+
"""
|
1076
|
+
Get the current truncation configuration.
|
1077
|
+
|
1078
|
+
Returns:
|
1079
|
+
Dictionary containing truncation configuration
|
1080
|
+
"""
|
1081
|
+
return self.truncation_config.copy()
|
1082
|
+
|
1083
|
+
def enable_truncation(self, enabled: bool = True):
|
1084
|
+
"""
|
1085
|
+
Enable or disable output truncation.
|
1086
|
+
|
1087
|
+
Args:
|
1088
|
+
enabled: Whether to enable truncation
|
1089
|
+
"""
|
1090
|
+
self.truncation_config["enabled"] = enabled
|
1091
|
+
|
1092
|
+
def enable_auto_git_checkpoint(self, enabled: bool = True):
|
1093
|
+
"""
|
1094
|
+
Enable or disable automatic git checkpoint creation after successful shell commands.
|
1095
|
+
|
1096
|
+
Args:
|
1097
|
+
enabled: If True, automatically create git checkpoints. If False, do not create them.
|
1098
|
+
"""
|
1099
|
+
self.auto_git_checkpoint = enabled
|
1100
|
+
|
1101
|
+
def get_auto_git_checkpoint_status(self) -> bool:
|
1102
|
+
"""
|
1103
|
+
Get the current status of auto_git_checkpoint.
|
1104
|
+
|
1105
|
+
Returns:
|
1106
|
+
True if auto_git_checkpoint is enabled, False otherwise.
|
1107
|
+
"""
|
1108
|
+
return self.auto_git_checkpoint
|
1109
|
+
|
1110
|
+
def set_environment_variables(self, env_vars: Dict[str, str]):
|
1111
|
+
"""
|
1112
|
+
Set environment variables for the code execution provider.
|
1113
|
+
Currently only supported for SeatbeltProvider.
|
1114
|
+
|
1115
|
+
Args:
|
1116
|
+
env_vars: Dictionary of environment variable name -> value pairs
|
1117
|
+
|
1118
|
+
Raises:
|
1119
|
+
AttributeError: If the provider doesn't support environment variables
|
1120
|
+
"""
|
1121
|
+
if hasattr(self.code_provider, 'set_environment_variables'):
|
1122
|
+
self.code_provider.set_environment_variables(env_vars)
|
1123
|
+
else:
|
1124
|
+
raise AttributeError(f"Provider {self.provider} does not support environment variables")
|
1125
|
+
|
1126
|
+
def add_environment_variable(self, name: str, value: str):
|
1127
|
+
"""
|
1128
|
+
Add a single environment variable for the code execution provider.
|
1129
|
+
Currently only supported for SeatbeltProvider.
|
1130
|
+
|
1131
|
+
Args:
|
1132
|
+
name: Environment variable name
|
1133
|
+
value: Environment variable value
|
1134
|
+
|
1135
|
+
Raises:
|
1136
|
+
AttributeError: If the provider doesn't support environment variables
|
1137
|
+
"""
|
1138
|
+
if hasattr(self.code_provider, 'add_environment_variable'):
|
1139
|
+
self.code_provider.add_environment_variable(name, value)
|
1140
|
+
else:
|
1141
|
+
raise AttributeError(f"Provider {self.provider} does not support environment variables")
|
1142
|
+
|
1143
|
+
def remove_environment_variable(self, name: str):
|
1144
|
+
"""
|
1145
|
+
Remove an environment variable from the code execution provider.
|
1146
|
+
Currently only supported for SeatbeltProvider.
|
1147
|
+
|
1148
|
+
Args:
|
1149
|
+
name: Environment variable name to remove
|
1150
|
+
|
1151
|
+
Raises:
|
1152
|
+
AttributeError: If the provider doesn't support environment variables
|
1153
|
+
"""
|
1154
|
+
if hasattr(self.code_provider, 'remove_environment_variable'):
|
1155
|
+
self.code_provider.remove_environment_variable(name)
|
1156
|
+
else:
|
1157
|
+
raise AttributeError(f"Provider {self.provider} does not support environment variables")
|
1158
|
+
|
1159
|
+
def get_environment_variables(self) -> Dict[str, str]:
|
1160
|
+
"""
|
1161
|
+
Get a copy of current environment variables from the code execution provider.
|
1162
|
+
Currently only supported for SeatbeltProvider.
|
1163
|
+
|
1164
|
+
Returns:
|
1165
|
+
Dictionary of current environment variables
|
1166
|
+
|
1167
|
+
Raises:
|
1168
|
+
AttributeError: If the provider doesn't support environment variables
|
1169
|
+
"""
|
1170
|
+
if hasattr(self.code_provider, 'get_environment_variables'):
|
1171
|
+
return self.code_provider.get_environment_variables()
|
1172
|
+
else:
|
1173
|
+
raise AttributeError(f"Provider {self.provider} does not support environment variables")
|
1174
|
+
|
554
1175
|
|
555
1176
|
# Example usage demonstrating both LLM tools and code tools
|
556
1177
|
async def run_example():
|
@@ -562,6 +1183,7 @@ async def run_example():
|
|
562
1183
|
Code tools: Available in the Python execution environment
|
563
1184
|
"""
|
564
1185
|
from tinyagent import tool
|
1186
|
+
import os
|
565
1187
|
|
566
1188
|
# Example LLM tool - available to the LLM for direct calling
|
567
1189
|
@tool(name="search_web", description="Search the web for information")
|
@@ -590,7 +1212,14 @@ async def run_example():
|
|
590
1212
|
"sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
|
591
1213
|
},
|
592
1214
|
authorized_imports=["tinyagent", "gradio", "requests", "numpy", "pandas"], # Explicitly specify authorized imports
|
593
|
-
local_execution=False # Remote execution via Modal (default)
|
1215
|
+
local_execution=False, # Remote execution via Modal (default)
|
1216
|
+
check_string_obfuscation=True,
|
1217
|
+
default_workdir=os.path.join(os.getcwd(), "examples"), # Set a default working directory for shell commands
|
1218
|
+
truncation_config={
|
1219
|
+
"max_tokens": 3000,
|
1220
|
+
"max_lines": 250,
|
1221
|
+
"enabled": True
|
1222
|
+
}
|
594
1223
|
)
|
595
1224
|
|
596
1225
|
# Connect to MCP servers
|
@@ -607,6 +1236,13 @@ async def run_example():
|
|
607
1236
|
print(response_remote)
|
608
1237
|
print("\n" + "="*80 + "\n")
|
609
1238
|
|
1239
|
+
# Test the resume functionality
|
1240
|
+
print("🔄 Testing resume functionality (continuing without new user input)")
|
1241
|
+
resume_response = await agent_remote.resume(max_turns=3)
|
1242
|
+
print("Resume Response:")
|
1243
|
+
print(resume_response)
|
1244
|
+
print("\n" + "="*80 + "\n")
|
1245
|
+
|
610
1246
|
# Now test with local execution
|
611
1247
|
print("🏠 Testing TinyCodeAgent with LOCAL execution")
|
612
1248
|
agent_local = TinyCodeAgent(
|
@@ -617,7 +1253,8 @@ async def run_example():
|
|
617
1253
|
"sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
|
618
1254
|
},
|
619
1255
|
authorized_imports=["tinyagent", "gradio", "requests"], # More restricted imports for local execution
|
620
|
-
local_execution=True # Local execution
|
1256
|
+
local_execution=True, # Local execution
|
1257
|
+
check_string_obfuscation=True
|
621
1258
|
)
|
622
1259
|
|
623
1260
|
# Connect to MCP servers
|
@@ -669,6 +1306,330 @@ async def run_example():
|
|
669
1306
|
print("Local Agent Validation Response:")
|
670
1307
|
print(response2_local)
|
671
1308
|
|
1309
|
+
# Test shell execution
|
1310
|
+
print("\n" + "="*80)
|
1311
|
+
print("🐚 Testing shell execution")
|
1312
|
+
|
1313
|
+
shell_prompt = "Run 'ls -la' to list files in the current directory."
|
1314
|
+
|
1315
|
+
response_shell = await agent_remote.run(shell_prompt)
|
1316
|
+
print("Shell Execution Response:")
|
1317
|
+
print(response_shell)
|
1318
|
+
|
1319
|
+
# Test default working directory functionality
|
1320
|
+
print("\n" + "="*80)
|
1321
|
+
print("🏠 Testing default working directory functionality")
|
1322
|
+
|
1323
|
+
# Set a custom default working directory
|
1324
|
+
custom_dir = os.path.expanduser("~") # Use home directory as an example
|
1325
|
+
agent_remote.set_default_workdir(custom_dir)
|
1326
|
+
print(f"Set default working directory to: {custom_dir}")
|
1327
|
+
|
1328
|
+
# Create a new directory for testing
|
1329
|
+
test_dir = os.path.join(os.getcwd(), "test_workdir")
|
1330
|
+
print(f"Setting default working directory with auto-creation: {test_dir}")
|
1331
|
+
agent_remote.set_default_workdir(test_dir, create_if_not_exists=True)
|
1332
|
+
|
1333
|
+
# Run shell command without specifying workdir - should use the default
|
1334
|
+
shell_prompt_default_dir = "Run 'pwd' to show the current working directory."
|
1335
|
+
|
1336
|
+
response_shell_default = await agent_remote.run(shell_prompt_default_dir)
|
1337
|
+
print("Shell Execution with Default Working Directory:")
|
1338
|
+
print(response_shell_default)
|
1339
|
+
|
1340
|
+
# Run shell command with explicit workdir - should override the default
|
1341
|
+
shell_prompt_explicit_dir = "Run 'pwd' in the /tmp directory."
|
1342
|
+
|
1343
|
+
response_shell_explicit = await agent_remote.run(shell_prompt_explicit_dir)
|
1344
|
+
print("Shell Execution with Explicit Working Directory:")
|
1345
|
+
print(response_shell_explicit)
|
1346
|
+
|
1347
|
+
# Test truncation functionality
|
1348
|
+
print("\n" + "="*80)
|
1349
|
+
print("✂️ Testing output truncation")
|
1350
|
+
|
1351
|
+
# Configure truncation with smaller limits for testing
|
1352
|
+
agent_remote.set_truncation_config({
|
1353
|
+
"max_tokens": 100, # Very small limit for testing
|
1354
|
+
"max_lines": 5 # Very small limit for testing
|
1355
|
+
})
|
1356
|
+
|
1357
|
+
# Generate a large output to test truncation
|
1358
|
+
large_output_prompt = """
|
1359
|
+
Generate a large output by printing a lot of text. Create a Python script that:
|
1360
|
+
1. Prints numbers from 1 to 1000
|
1361
|
+
2. For each number, also print its square and cube
|
1362
|
+
3. Add random text for each line to make it longer
|
1363
|
+
"""
|
1364
|
+
|
1365
|
+
response_truncated = await agent_remote.run(large_output_prompt)
|
1366
|
+
print("Truncated Output Response:")
|
1367
|
+
print(response_truncated)
|
1368
|
+
|
1369
|
+
# Test disabling truncation
|
1370
|
+
print("\n" + "="*80)
|
1371
|
+
print("🔄 Testing with truncation disabled")
|
1372
|
+
|
1373
|
+
agent_remote.enable_truncation(False)
|
1374
|
+
response_untruncated = await agent_remote.run("Run the same script again but limit to 20 numbers")
|
1375
|
+
print("Untruncated Output Response:")
|
1376
|
+
print(response_untruncated)
|
1377
|
+
|
1378
|
+
# Test git checkpoint functionality
|
1379
|
+
print("\n" + "="*80)
|
1380
|
+
print("🔄 Testing git checkpoint functionality")
|
1381
|
+
|
1382
|
+
# Enable git checkpoints
|
1383
|
+
agent_remote.enable_auto_git_checkpoint(True)
|
1384
|
+
print(f"Auto Git Checkpoint enabled: {agent_remote.get_auto_git_checkpoint_status()}")
|
1385
|
+
|
1386
|
+
# Create a test file to demonstrate git checkpoint
|
1387
|
+
git_test_prompt = """
|
1388
|
+
Create a new file called test_file.txt with some content, then modify it, and observe
|
1389
|
+
that git checkpoints are created automatically after each change.
|
1390
|
+
"""
|
1391
|
+
|
1392
|
+
git_response = await agent_remote.run(git_test_prompt)
|
1393
|
+
print("Git Checkpoint Response:")
|
1394
|
+
print(git_response)
|
1395
|
+
|
1396
|
+
# Disable git checkpoints
|
1397
|
+
agent_remote.enable_auto_git_checkpoint(False)
|
1398
|
+
print(f"Auto Git Checkpoint disabled: {agent_remote.get_auto_git_checkpoint_status()}")
|
1399
|
+
|
1400
|
+
# Test seatbelt provider if supported
|
1401
|
+
if TinyCodeAgent.is_seatbelt_supported():
|
1402
|
+
print("\n" + "="*80)
|
1403
|
+
print("🔒 Testing TinyCodeAgent with SEATBELT provider (sandboxed execution)")
|
1404
|
+
|
1405
|
+
# Create a test directory for read/write access
|
1406
|
+
test_read_dir = os.path.join(os.getcwd(), "test_read_dir")
|
1407
|
+
test_write_dir = os.path.join(os.getcwd(), "test_write_dir")
|
1408
|
+
|
1409
|
+
# Create directories if they don't exist
|
1410
|
+
os.makedirs(test_read_dir, exist_ok=True)
|
1411
|
+
os.makedirs(test_write_dir, exist_ok=True)
|
1412
|
+
|
1413
|
+
# Create a test file in the read directory
|
1414
|
+
with open(os.path.join(test_read_dir, "test.txt"), "w") as f:
|
1415
|
+
f.write("This is a test file for reading")
|
1416
|
+
|
1417
|
+
# Create a simple seatbelt profile
|
1418
|
+
seatbelt_profile = """(version 1)
|
1419
|
+
|
1420
|
+
; Default to deny everything
|
1421
|
+
(deny default)
|
1422
|
+
|
1423
|
+
; Allow network connections with proper DNS resolution
|
1424
|
+
(allow network*)
|
1425
|
+
(allow network-outbound)
|
1426
|
+
(allow mach-lookup)
|
1427
|
+
|
1428
|
+
; Allow process execution
|
1429
|
+
(allow process-exec)
|
1430
|
+
(allow process-fork)
|
1431
|
+
(allow signal (target self))
|
1432
|
+
|
1433
|
+
; Restrict file read to current path and system files
|
1434
|
+
(deny file-read* (subpath "/Users"))
|
1435
|
+
(allow file-read*
|
1436
|
+
(subpath "{os.getcwd()}")
|
1437
|
+
(subpath "/usr")
|
1438
|
+
(subpath "/System")
|
1439
|
+
(subpath "/Library")
|
1440
|
+
(subpath "/bin")
|
1441
|
+
(subpath "/sbin")
|
1442
|
+
(subpath "/opt")
|
1443
|
+
(subpath "/private/tmp")
|
1444
|
+
(subpath "/private/var/tmp")
|
1445
|
+
(subpath "/dev")
|
1446
|
+
(subpath "/etc")
|
1447
|
+
(literal "/")
|
1448
|
+
(literal "/."))
|
1449
|
+
|
1450
|
+
; Allow write access to specified folder and temp directories
|
1451
|
+
(deny file-write* (subpath "/"))
|
1452
|
+
(allow file-write*
|
1453
|
+
(subpath "{os.getcwd()}")
|
1454
|
+
(subpath "/private/tmp")
|
1455
|
+
(subpath "/private/var/tmp")
|
1456
|
+
(subpath "/dev"))
|
1457
|
+
|
1458
|
+
; Allow standard device operations
|
1459
|
+
(allow file-write-data
|
1460
|
+
(literal "/dev/null")
|
1461
|
+
(literal "/dev/dtracehelper")
|
1462
|
+
(literal "/dev/tty")
|
1463
|
+
(literal "/dev/stdout")
|
1464
|
+
(literal "/dev/stderr"))
|
1465
|
+
|
1466
|
+
; Allow iokit operations needed for system functions
|
1467
|
+
(allow iokit-open)
|
1468
|
+
|
1469
|
+
; Allow shared memory operations
|
1470
|
+
(allow ipc-posix-shm)
|
1471
|
+
|
1472
|
+
; Allow basic system operations
|
1473
|
+
(allow file-read-metadata)
|
1474
|
+
(allow process-info-pidinfo)
|
1475
|
+
(allow process-info-setcontrol)
|
1476
|
+
"""
|
1477
|
+
|
1478
|
+
# Create TinyCodeAgent with seatbelt provider
|
1479
|
+
agent_seatbelt = TinyCodeAgent(
|
1480
|
+
model="gpt-4.1-mini",
|
1481
|
+
tools=[search_web], # LLM tools
|
1482
|
+
code_tools=[data_processor], # Code tools
|
1483
|
+
user_variables={
|
1484
|
+
"sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
|
1485
|
+
},
|
1486
|
+
provider="seatbelt", # Use seatbelt provider
|
1487
|
+
provider_config={
|
1488
|
+
"seatbelt_profile": seatbelt_profile,
|
1489
|
+
# Alternatively, you can specify a path to a seatbelt profile file:
|
1490
|
+
# "seatbelt_profile_path": "/path/to/seatbelt.sb",
|
1491
|
+
# "python_env_path": "/path/to/python/env", # Optional path to Python environment
|
1492
|
+
|
1493
|
+
# Specify additional directories for read/write access
|
1494
|
+
"additional_read_dirs": [test_read_dir],
|
1495
|
+
"additional_write_dirs": [test_write_dir],
|
1496
|
+
|
1497
|
+
# Allow git commands
|
1498
|
+
"bypass_shell_safety": True,
|
1499
|
+
"additional_safe_shell_commands": ["git"],
|
1500
|
+
|
1501
|
+
# Environment variables to make available in the sandbox
|
1502
|
+
"environment_variables": {
|
1503
|
+
"TEST_READ_DIR": test_read_dir,
|
1504
|
+
"TEST_WRITE_DIR": test_write_dir,
|
1505
|
+
"PROJECT_NAME": "TinyAgent Seatbelt Demo",
|
1506
|
+
"BUILD_VERSION": "1.0.0"
|
1507
|
+
}
|
1508
|
+
},
|
1509
|
+
local_execution=True, # Required for seatbelt
|
1510
|
+
check_string_obfuscation=True,
|
1511
|
+
truncation_config={
|
1512
|
+
"max_tokens": 500,
|
1513
|
+
"max_lines": 20,
|
1514
|
+
"enabled": True
|
1515
|
+
}
|
1516
|
+
)
|
1517
|
+
|
1518
|
+
# Connect to MCP servers
|
1519
|
+
await agent_seatbelt.connect_to_server("npx", ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"])
|
1520
|
+
await agent_seatbelt.connect_to_server("npx", ["-y", "@modelcontextprotocol/server-sequential-thinking"])
|
1521
|
+
|
1522
|
+
# Test the seatbelt agent
|
1523
|
+
response_seatbelt = await agent_seatbelt.run("""
|
1524
|
+
I have some sample data. Please use the data_processor tool in Python to analyze my sample_data
|
1525
|
+
and show me the results.
|
1526
|
+
""")
|
1527
|
+
|
1528
|
+
print("Seatbelt Agent Response:")
|
1529
|
+
print(response_seatbelt)
|
1530
|
+
|
1531
|
+
# Test shell execution in sandbox
|
1532
|
+
shell_prompt_sandbox = "Run 'ls -la' to list files in the current directory."
|
1533
|
+
|
1534
|
+
response_shell_sandbox = await agent_seatbelt.run(shell_prompt_sandbox)
|
1535
|
+
print("Shell Execution in Sandbox:")
|
1536
|
+
print(response_shell_sandbox)
|
1537
|
+
|
1538
|
+
# Test reading from the additional read directory
|
1539
|
+
read_prompt = f"Read the contents of the file in the test_read_dir directory."
|
1540
|
+
|
1541
|
+
response_read = await agent_seatbelt.run(read_prompt)
|
1542
|
+
print("Reading from Additional Read Directory:")
|
1543
|
+
print(response_read)
|
1544
|
+
|
1545
|
+
# Test writing to the additional write directory
|
1546
|
+
write_prompt = f"Write a file called 'output.txt' with the text 'Hello from sandbox!' in the test_write_dir directory."
|
1547
|
+
|
1548
|
+
response_write = await agent_seatbelt.run(write_prompt)
|
1549
|
+
print("Writing to Additional Write Directory:")
|
1550
|
+
print(response_write)
|
1551
|
+
|
1552
|
+
# Test environment variables
|
1553
|
+
print("\n" + "="*80)
|
1554
|
+
print("🔧 Testing environment variables functionality")
|
1555
|
+
|
1556
|
+
# Add additional environment variables dynamically
|
1557
|
+
agent_seatbelt.add_environment_variable("CUSTOM_VAR", "custom_value")
|
1558
|
+
agent_seatbelt.add_environment_variable("DEBUG_MODE", "true")
|
1559
|
+
|
1560
|
+
# Get and display current environment variables
|
1561
|
+
current_env_vars = agent_seatbelt.get_environment_variables()
|
1562
|
+
print(f"Current environment variables: {list(current_env_vars.keys())}")
|
1563
|
+
|
1564
|
+
# Test accessing environment variables in Python and shell
|
1565
|
+
env_test_prompt = """
|
1566
|
+
Test the environment variables we set:
|
1567
|
+
1. In Python, use os.environ to check for CUSTOM_VAR and DEBUG_MODE
|
1568
|
+
2. In a shell command, use 'echo $CUSTOM_VAR' and 'echo $DEBUG_MODE'
|
1569
|
+
3. Also check the TEST_READ_DIR and TEST_WRITE_DIR variables that were set during initialization
|
1570
|
+
4. Show all environment variables that start with 'TEST_' or 'CUSTOM_' or 'DEBUG_'
|
1571
|
+
"""
|
1572
|
+
|
1573
|
+
response_env_test = await agent_seatbelt.run(env_test_prompt)
|
1574
|
+
print("Environment Variables Test:")
|
1575
|
+
print(response_env_test)
|
1576
|
+
|
1577
|
+
# Update environment variables
|
1578
|
+
agent_seatbelt.set_environment_variables({
|
1579
|
+
"CUSTOM_VAR": "updated_value",
|
1580
|
+
"NEW_VAR": "new_value",
|
1581
|
+
"API_KEY": "test_api_key_123"
|
1582
|
+
})
|
1583
|
+
|
1584
|
+
# Test updated environment variables
|
1585
|
+
updated_env_test_prompt = """
|
1586
|
+
Test the updated environment variables:
|
1587
|
+
1. Check that CUSTOM_VAR now has the value 'updated_value'
|
1588
|
+
2. Check that NEW_VAR is available with value 'new_value'
|
1589
|
+
3. Check that API_KEY is available with value 'test_api_key_123'
|
1590
|
+
4. Verify that DEBUG_MODE is no longer available (should have been removed by set operation)
|
1591
|
+
"""
|
1592
|
+
|
1593
|
+
response_updated_env = await agent_seatbelt.run(updated_env_test_prompt)
|
1594
|
+
print("Updated Environment Variables Test:")
|
1595
|
+
print(response_updated_env)
|
1596
|
+
|
1597
|
+
# Remove a specific environment variable
|
1598
|
+
agent_seatbelt.remove_environment_variable("API_KEY")
|
1599
|
+
|
1600
|
+
# Test that the removed variable is no longer available
|
1601
|
+
removed_env_test_prompt = """
|
1602
|
+
Test that API_KEY environment variable has been removed:
|
1603
|
+
1. Try to access API_KEY in Python - it should not be available
|
1604
|
+
2. Use shell command 'echo $API_KEY' - it should be empty
|
1605
|
+
3. List all current environment variables that start with 'CUSTOM_' or 'NEW_'
|
1606
|
+
"""
|
1607
|
+
|
1608
|
+
response_removed_env = await agent_seatbelt.run(removed_env_test_prompt)
|
1609
|
+
print("Removed Environment Variable Test:")
|
1610
|
+
print(response_removed_env)
|
1611
|
+
|
1612
|
+
# Test git commands with the custom configuration
|
1613
|
+
git_prompt = "Run 'git status' to show the current git status."
|
1614
|
+
|
1615
|
+
response_git = await agent_seatbelt.run(git_prompt)
|
1616
|
+
print("Git Command Execution:")
|
1617
|
+
print(response_git)
|
1618
|
+
|
1619
|
+
# Clean up test directories
|
1620
|
+
import shutil
|
1621
|
+
try:
|
1622
|
+
shutil.rmtree(test_read_dir)
|
1623
|
+
shutil.rmtree(test_write_dir)
|
1624
|
+
print("Cleaned up test directories")
|
1625
|
+
except Exception as e:
|
1626
|
+
print(f"Error cleaning up test directories: {str(e)}")
|
1627
|
+
|
1628
|
+
await agent_seatbelt.close()
|
1629
|
+
else:
|
1630
|
+
print("\n" + "="*80)
|
1631
|
+
print("⚠️ Seatbelt provider is not supported on this system. Skipping seatbelt tests.")
|
1632
|
+
|
672
1633
|
await agent_remote.close()
|
673
1634
|
await agent_local.close()
|
674
1635
|
|