tinyagent-py 0.0.13__py3-none-any.whl → 0.0.15__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/base.py +153 -7
- tinyagent/code_agent/providers/modal_provider.py +141 -27
- tinyagent/code_agent/safety.py +6 -2
- tinyagent/code_agent/tiny_code_agent.py +303 -11
- tinyagent/code_agent/utils.py +97 -1
- 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/tiny_agent.py +426 -17
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.15.dist-info}/METADATA +1 -1
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.15.dist-info}/RECORD +17 -14
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.15.dist-info}/WHEEL +0 -0
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.15.dist-info}/licenses/LICENSE +0 -0
- {tinyagent_py-0.0.13.dist-info → tinyagent_py-0.0.15.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,26 @@
|
|
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
|
9
13
|
from .helper import translate_tool_for_code_agent, load_template, render_system_prompt, prompt_code_example, prompt_qwen_helper
|
10
14
|
|
11
15
|
|
16
|
+
DEFAULT_SUMMARY_SYSTEM_PROMPT = (
|
17
|
+
"You are an expert coding assistant. Your goal is to generate a concise, structured summary "
|
18
|
+
"of the conversation below that captures all essential information needed to continue "
|
19
|
+
"development after context replacement. Include tasks performed, code areas modified or "
|
20
|
+
"reviewed, key decisions or assumptions, test results or errors, and outstanding tasks or next steps."
|
21
|
+
|
22
|
+
)
|
23
|
+
|
12
24
|
class TinyCodeAgent:
|
13
25
|
"""
|
14
26
|
A TinyAgent specialized for code execution tasks.
|
@@ -27,10 +39,15 @@ class TinyCodeAgent:
|
|
27
39
|
code_tools: Optional[List[Any]] = None,
|
28
40
|
authorized_imports: Optional[List[str]] = None,
|
29
41
|
system_prompt_template: Optional[str] = None,
|
42
|
+
system_prompt: Optional[str] = None,
|
30
43
|
provider_config: Optional[Dict[str, Any]] = None,
|
31
44
|
user_variables: Optional[Dict[str, Any]] = None,
|
32
45
|
pip_packages: Optional[List[str]] = None,
|
33
46
|
local_execution: bool = False,
|
47
|
+
check_string_obfuscation: bool = True,
|
48
|
+
default_workdir: Optional[str] = None,
|
49
|
+
summary_config: Optional[Dict[str, Any]] = None,
|
50
|
+
ui: Optional[str] = None,
|
34
51
|
**agent_kwargs
|
35
52
|
):
|
36
53
|
"""
|
@@ -50,6 +67,11 @@ class TinyCodeAgent:
|
|
50
67
|
pip_packages: List of additional Python packages to install in Modal environment
|
51
68
|
local_execution: If True, uses Modal's .local() method for local execution.
|
52
69
|
If False, uses Modal's .remote() method for cloud execution (default: False)
|
70
|
+
check_string_obfuscation: If True (default), check for string obfuscation techniques. Set to False to allow
|
71
|
+
legitimate use of base64 encoding and other string manipulations.
|
72
|
+
default_workdir: Default working directory for shell commands. If None, the current working directory is used.
|
73
|
+
summary_config: Optional configuration for generating conversation summaries
|
74
|
+
ui: The user interface callback to use ('rich', 'jupyter', or None).
|
53
75
|
**agent_kwargs: Additional arguments passed to TinyAgent
|
54
76
|
"""
|
55
77
|
self.model = model
|
@@ -63,6 +85,8 @@ class TinyCodeAgent:
|
|
63
85
|
self.pip_packages = pip_packages or []
|
64
86
|
self.local_execution = local_execution
|
65
87
|
self.provider = provider # Store provider type for reuse
|
88
|
+
self.check_string_obfuscation = check_string_obfuscation
|
89
|
+
self.default_workdir = default_workdir or os.getcwd() # Default to current working directory if not specified
|
66
90
|
|
67
91
|
# Create the code execution provider
|
68
92
|
self.code_provider = self._create_provider(provider, self.provider_config)
|
@@ -72,23 +96,32 @@ class TinyCodeAgent:
|
|
72
96
|
self.code_provider.set_user_variables(self.user_variables)
|
73
97
|
|
74
98
|
# Build system prompt
|
75
|
-
self.
|
99
|
+
self.static_system_prompt= system_prompt
|
100
|
+
self.system_prompt = self._build_system_prompt(system_prompt_template)
|
101
|
+
|
76
102
|
|
77
|
-
|
103
|
+
self.summary_config = summary_config or {}
|
104
|
+
|
105
|
+
# Create the underlying TinyAgent with summary configuration
|
78
106
|
self.agent = TinyAgent(
|
79
107
|
model=model,
|
80
108
|
api_key=api_key,
|
81
109
|
system_prompt=self.system_prompt,
|
82
110
|
logger=log_manager.get_logger('tinyagent.tiny_agent') if log_manager else None,
|
111
|
+
summary_config=summary_config,
|
83
112
|
**agent_kwargs
|
84
113
|
)
|
85
114
|
|
86
|
-
# Add the code execution
|
87
|
-
self.
|
115
|
+
# Add the code execution tools
|
116
|
+
self._setup_code_execution_tools()
|
88
117
|
|
89
118
|
# Add LLM tools (not code tools - those go to the provider)
|
90
119
|
if self.tools:
|
91
120
|
self.agent.add_tools(self.tools)
|
121
|
+
|
122
|
+
# Add the selected UI callback
|
123
|
+
if ui:
|
124
|
+
self.add_ui_callback(ui)
|
92
125
|
|
93
126
|
def _create_provider(self, provider_type: str, config: Dict[str, Any]) -> CodeExecutionProvider:
|
94
127
|
"""Create a code execution provider based on the specified type."""
|
@@ -104,6 +137,7 @@ class TinyCodeAgent:
|
|
104
137
|
final_config = config.copy()
|
105
138
|
final_config["pip_packages"] = final_pip_packages
|
106
139
|
final_config["authorized_imports"] = final_authorized_imports
|
140
|
+
final_config["check_string_obfuscation"] = self.check_string_obfuscation
|
107
141
|
|
108
142
|
return ModalProvider(
|
109
143
|
log_manager=self.log_manager,
|
@@ -117,7 +151,9 @@ class TinyCodeAgent:
|
|
117
151
|
def _build_system_prompt(self, template_path: Optional[str] = None) -> str:
|
118
152
|
"""Build the system prompt for the code agent."""
|
119
153
|
# Use default template if none provided
|
120
|
-
if
|
154
|
+
if self.static_system_prompt is not None:
|
155
|
+
return self.static_system_prompt
|
156
|
+
elif template_path is None :
|
121
157
|
template_path = str(Path(__file__).parent.parent / "prompts" / "code_agent.yaml")
|
122
158
|
|
123
159
|
# Translate code tools to code agent format
|
@@ -242,8 +278,8 @@ class TinyCodeAgent:
|
|
242
278
|
|
243
279
|
return "\n".join(code_tools_lines)
|
244
280
|
|
245
|
-
def
|
246
|
-
"""Set up the
|
281
|
+
def _setup_code_execution_tools(self):
|
282
|
+
"""Set up the code execution tools using the code provider."""
|
247
283
|
@tool(name="run_python", description=dedent("""
|
248
284
|
This tool receives Python code and executes it in a sandboxed environment.
|
249
285
|
During each intermediate step, you can use 'print()' to save important information.
|
@@ -271,7 +307,7 @@ class TinyCodeAgent:
|
|
271
307
|
# This ensures they stay in sync
|
272
308
|
self.user_variables = self.code_provider.get_user_variables()
|
273
309
|
|
274
|
-
return
|
310
|
+
return json.dumps(result)
|
275
311
|
except Exception as e:
|
276
312
|
print("!"*100)
|
277
313
|
COLOR = {
|
@@ -286,9 +322,128 @@ class TinyCodeAgent:
|
|
286
322
|
# This ensures any variables that were successfully created/modified are preserved
|
287
323
|
self.user_variables = self.code_provider.get_user_variables()
|
288
324
|
|
289
|
-
return f"Error executing code: {str(e)}"
|
325
|
+
return json.dumps({"error": f"Error executing code: {str(e)}"})
|
326
|
+
|
327
|
+
@tool(name="bash", description=dedent("""
|
328
|
+
This tool executes shell commands securely in a sandboxed environment.
|
329
|
+
Only a limited set of safe commands are allowed for security reasons.
|
330
|
+
Before executing the command, please follow these steps:
|
331
|
+
|
332
|
+
1. Directory Verification:
|
333
|
+
- If the command will create new directories or files, first use ls to verify the parent directory exists and is the correct location
|
334
|
+
- For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
|
335
|
+
|
336
|
+
2. Command Execution:
|
337
|
+
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
|
338
|
+
- Examples of proper quoting:
|
339
|
+
- cd "/Users/name/My Documents" (correct)
|
340
|
+
- cd /Users/name/My Documents (incorrect - will fail)
|
341
|
+
- python "/path/with spaces/script.py" (correct)
|
342
|
+
- python /path/with spaces/script.py (incorrect - will fail)
|
343
|
+
- After ensuring proper quoting, execute the command.
|
344
|
+
- Capture the output of the command.
|
345
|
+
|
346
|
+
Usage notes:
|
347
|
+
- The command argument is required.
|
348
|
+
- 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.
|
349
|
+
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).
|
350
|
+
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
351
|
+
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
|
352
|
+
|
353
|
+
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` first, which all ${PRODUCT_NAME} users have pre-installed.
|
354
|
+
" - When issuing multiple commands, use the ; or && operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).\n" +
|
355
|
+
- 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.
|
356
|
+
<good-example>
|
357
|
+
pytest /foo/bar/tests
|
358
|
+
</good-example>
|
359
|
+
<bad-example>
|
360
|
+
cd /foo/bar && pytest tests
|
361
|
+
</bad-example>
|
362
|
+
|
363
|
+
Args:
|
364
|
+
command: list[str]: The shell command to execute as a list of strings. Example: ["ls", "-la"] or ["cat", "file.txt"]
|
365
|
+
|
366
|
+
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.
|
367
|
+
description: str: A clear, concise description of what this command does in 5-10 words.
|
368
|
+
timeout: int: Maximum execution time in seconds (default: 30).
|
369
|
+
Returns:
|
370
|
+
Dictionary with stdout, stderr, and exit_code from the command execution.
|
371
|
+
If the command is rejected for security reasons, stderr will contain the reason.
|
372
|
+
The stdout will include information about which working directory was used.
|
373
|
+
"""))
|
374
|
+
async def run_shell(command: List[str], absolute_workdir: str, description: str, timeout: int = 30) -> str:
|
375
|
+
"""Execute shell commands securely using the configured provider."""
|
376
|
+
try:
|
377
|
+
# Use the default working directory if none is specified
|
378
|
+
effective_workdir = absolute_workdir or self.default_workdir
|
379
|
+
print(f" {command} to {description}")
|
380
|
+
# Verify that the working directory exists
|
381
|
+
if effective_workdir and not os.path.exists(effective_workdir):
|
382
|
+
return json.dumps({
|
383
|
+
"stdout": "",
|
384
|
+
"stderr": f"Working directory does not exist: {effective_workdir}",
|
385
|
+
"exit_code": 1
|
386
|
+
})
|
387
|
+
|
388
|
+
if effective_workdir and not os.path.isdir(effective_workdir):
|
389
|
+
return json.dumps({
|
390
|
+
"stdout": "",
|
391
|
+
"stderr": f"Path is not a directory: {effective_workdir}",
|
392
|
+
"exit_code": 1
|
393
|
+
})
|
394
|
+
|
395
|
+
result = await self.code_provider.execute_shell(command, timeout, effective_workdir)
|
396
|
+
return json.dumps(result)
|
397
|
+
except Exception as e:
|
398
|
+
COLOR = {
|
399
|
+
"RED": "\033[91m",
|
400
|
+
"ENDC": "\033[0m",
|
401
|
+
}
|
402
|
+
print(f"{COLOR['RED']}{str(e)}{COLOR['ENDC']}")
|
403
|
+
print(f"{COLOR['RED']}{traceback.format_exc()}{COLOR['ENDC']}")
|
404
|
+
|
405
|
+
return json.dumps({"error": f"Error executing shell command: {str(e)}"})
|
290
406
|
|
291
407
|
self.agent.add_tool(run_python)
|
408
|
+
self.agent.add_tool(run_shell)
|
409
|
+
|
410
|
+
def set_default_workdir(self, workdir: str, create_if_not_exists: bool = False):
|
411
|
+
"""
|
412
|
+
Set the default working directory for shell commands.
|
413
|
+
|
414
|
+
Args:
|
415
|
+
workdir: The path to use as the default working directory
|
416
|
+
create_if_not_exists: If True, create the directory if it doesn't exist
|
417
|
+
|
418
|
+
Raises:
|
419
|
+
ValueError: If the directory doesn't exist and create_if_not_exists is False
|
420
|
+
OSError: If there's an error creating the directory
|
421
|
+
"""
|
422
|
+
workdir = os.path.expanduser(workdir) # Expand user directory if needed
|
423
|
+
|
424
|
+
if not os.path.exists(workdir):
|
425
|
+
if create_if_not_exists:
|
426
|
+
try:
|
427
|
+
os.makedirs(workdir, exist_ok=True)
|
428
|
+
print(f"Created directory: {workdir}")
|
429
|
+
except OSError as e:
|
430
|
+
raise OSError(f"Failed to create directory {workdir}: {str(e)}")
|
431
|
+
else:
|
432
|
+
raise ValueError(f"Directory does not exist: {workdir}")
|
433
|
+
|
434
|
+
if not os.path.isdir(workdir):
|
435
|
+
raise ValueError(f"Path is not a directory: {workdir}")
|
436
|
+
|
437
|
+
self.default_workdir = workdir
|
438
|
+
|
439
|
+
def get_default_workdir(self) -> str:
|
440
|
+
"""
|
441
|
+
Get the current default working directory for shell commands.
|
442
|
+
|
443
|
+
Returns:
|
444
|
+
The current default working directory path
|
445
|
+
"""
|
446
|
+
return self.default_workdir
|
292
447
|
|
293
448
|
async def run(self, user_input: str, max_turns: int = 10) -> str:
|
294
449
|
"""
|
@@ -303,6 +458,22 @@ class TinyCodeAgent:
|
|
303
458
|
"""
|
304
459
|
return await self.agent.run(user_input, max_turns)
|
305
460
|
|
461
|
+
async def resume(self, max_turns: int = 10) -> str:
|
462
|
+
"""
|
463
|
+
Resume the conversation without adding a new user message.
|
464
|
+
|
465
|
+
This method continues the conversation from the current state,
|
466
|
+
allowing the agent to process the existing conversation history
|
467
|
+
and potentially take additional actions.
|
468
|
+
|
469
|
+
Args:
|
470
|
+
max_turns: Maximum number of conversation turns
|
471
|
+
|
472
|
+
Returns:
|
473
|
+
The agent's response
|
474
|
+
"""
|
475
|
+
return await self.agent.resume(max_turns)
|
476
|
+
|
306
477
|
async def connect_to_server(self, command: str, args: List[str], **kwargs):
|
307
478
|
"""Connect to an MCP server."""
|
308
479
|
return await self.agent.connect_to_server(command, args, **kwargs)
|
@@ -551,6 +722,78 @@ class TinyCodeAgent:
|
|
551
722
|
"""Get the session ID."""
|
552
723
|
return self.agent.session_id
|
553
724
|
|
725
|
+
def set_check_string_obfuscation(self, enabled: bool):
|
726
|
+
"""
|
727
|
+
Enable or disable string obfuscation detection.
|
728
|
+
|
729
|
+
Args:
|
730
|
+
enabled: If True, check for string obfuscation techniques. If False, allow
|
731
|
+
legitimate use of base64 encoding and other string manipulations.
|
732
|
+
"""
|
733
|
+
self.check_string_obfuscation = enabled
|
734
|
+
|
735
|
+
# Update the provider with the new setting
|
736
|
+
if hasattr(self.code_provider, 'check_string_obfuscation'):
|
737
|
+
self.code_provider.check_string_obfuscation = enabled
|
738
|
+
|
739
|
+
async def summarize(self) -> str:
|
740
|
+
"""
|
741
|
+
Generate a summary of the current conversation history.
|
742
|
+
|
743
|
+
Args:
|
744
|
+
Returns:
|
745
|
+
A string containing the conversation summary
|
746
|
+
"""
|
747
|
+
# Use the underlying TinyAgent's summarize_conversation method
|
748
|
+
return await self.agent.summarize()
|
749
|
+
|
750
|
+
async def compact(self) -> bool:
|
751
|
+
"""
|
752
|
+
Compact the conversation history by replacing it with a summary.
|
753
|
+
|
754
|
+
This method delegates to the underlying TinyAgent's compact method,
|
755
|
+
which:
|
756
|
+
1. Generates a summary of the current conversation
|
757
|
+
2. If successful, replaces the conversation with just [system, user] messages
|
758
|
+
where the user message contains the summary
|
759
|
+
3. Returns True if compaction was successful, False otherwise
|
760
|
+
|
761
|
+
Returns:
|
762
|
+
Boolean indicating whether the compaction was successful
|
763
|
+
"""
|
764
|
+
return await self.agent.compact()
|
765
|
+
|
766
|
+
def add_ui_callback(self, ui_type: str, optimized: bool = True):
|
767
|
+
"""
|
768
|
+
Adds a UI callback to the agent based on the type.
|
769
|
+
|
770
|
+
Args:
|
771
|
+
ui_type: The type of UI callback ('rich' or 'jupyter')
|
772
|
+
optimized: Whether to use the optimized version (default: True for better performance)
|
773
|
+
"""
|
774
|
+
if ui_type == 'rich':
|
775
|
+
ui_callback = RichCodeUICallback(
|
776
|
+
logger=self.log_manager.get_logger('tinyagent.hooks.rich_code_ui_callback') if self.log_manager else None
|
777
|
+
)
|
778
|
+
self.add_callback(ui_callback)
|
779
|
+
elif ui_type == 'jupyter':
|
780
|
+
if optimized:
|
781
|
+
from tinyagent.hooks.jupyter_notebook_callback import OptimizedJupyterNotebookCallback
|
782
|
+
ui_callback = OptimizedJupyterNotebookCallback(
|
783
|
+
logger=self.log_manager.get_logger('tinyagent.hooks.jupyter_notebook_callback') if self.log_manager else None,
|
784
|
+
max_visible_turns=20, # Limit visible turns for performance
|
785
|
+
max_content_length=100000, # Limit total content
|
786
|
+
enable_markdown=True, # Keep markdown but optimized
|
787
|
+
show_raw_responses=False # Show formatted responses
|
788
|
+
)
|
789
|
+
else:
|
790
|
+
ui_callback = JupyterNotebookCallback(
|
791
|
+
logger=self.log_manager.get_logger('tinyagent.hooks.jupyter_notebook_callback') if self.log_manager else None
|
792
|
+
)
|
793
|
+
self.add_callback(ui_callback)
|
794
|
+
else:
|
795
|
+
self.log_manager.get_logger(__name__).warning(f"Unknown UI type: {ui_type}. No UI callback will be added.")
|
796
|
+
|
554
797
|
|
555
798
|
# Example usage demonstrating both LLM tools and code tools
|
556
799
|
async def run_example():
|
@@ -562,6 +805,7 @@ async def run_example():
|
|
562
805
|
Code tools: Available in the Python execution environment
|
563
806
|
"""
|
564
807
|
from tinyagent import tool
|
808
|
+
import os
|
565
809
|
|
566
810
|
# Example LLM tool - available to the LLM for direct calling
|
567
811
|
@tool(name="search_web", description="Search the web for information")
|
@@ -590,7 +834,9 @@ async def run_example():
|
|
590
834
|
"sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
|
591
835
|
},
|
592
836
|
authorized_imports=["tinyagent", "gradio", "requests", "numpy", "pandas"], # Explicitly specify authorized imports
|
593
|
-
local_execution=False # Remote execution via Modal (default)
|
837
|
+
local_execution=False, # Remote execution via Modal (default)
|
838
|
+
check_string_obfuscation=True,
|
839
|
+
default_workdir=os.path.join(os.getcwd(), "examples") # Set a default working directory for shell commands
|
594
840
|
)
|
595
841
|
|
596
842
|
# Connect to MCP servers
|
@@ -607,6 +853,13 @@ async def run_example():
|
|
607
853
|
print(response_remote)
|
608
854
|
print("\n" + "="*80 + "\n")
|
609
855
|
|
856
|
+
# Test the resume functionality
|
857
|
+
print("🔄 Testing resume functionality (continuing without new user input)")
|
858
|
+
resume_response = await agent_remote.resume(max_turns=3)
|
859
|
+
print("Resume Response:")
|
860
|
+
print(resume_response)
|
861
|
+
print("\n" + "="*80 + "\n")
|
862
|
+
|
610
863
|
# Now test with local execution
|
611
864
|
print("🏠 Testing TinyCodeAgent with LOCAL execution")
|
612
865
|
agent_local = TinyCodeAgent(
|
@@ -617,7 +870,8 @@ async def run_example():
|
|
617
870
|
"sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
|
618
871
|
},
|
619
872
|
authorized_imports=["tinyagent", "gradio", "requests"], # More restricted imports for local execution
|
620
|
-
local_execution=True # Local execution
|
873
|
+
local_execution=True, # Local execution
|
874
|
+
check_string_obfuscation=True
|
621
875
|
)
|
622
876
|
|
623
877
|
# Connect to MCP servers
|
@@ -669,6 +923,44 @@ async def run_example():
|
|
669
923
|
print("Local Agent Validation Response:")
|
670
924
|
print(response2_local)
|
671
925
|
|
926
|
+
# Test shell execution
|
927
|
+
print("\n" + "="*80)
|
928
|
+
print("🐚 Testing shell execution")
|
929
|
+
|
930
|
+
shell_prompt = "Run 'ls -la' to list files in the current directory."
|
931
|
+
|
932
|
+
response_shell = await agent_remote.run(shell_prompt)
|
933
|
+
print("Shell Execution Response:")
|
934
|
+
print(response_shell)
|
935
|
+
|
936
|
+
# Test default working directory functionality
|
937
|
+
print("\n" + "="*80)
|
938
|
+
print("🏠 Testing default working directory functionality")
|
939
|
+
|
940
|
+
# Set a custom default working directory
|
941
|
+
custom_dir = os.path.expanduser("~") # Use home directory as an example
|
942
|
+
agent_remote.set_default_workdir(custom_dir)
|
943
|
+
print(f"Set default working directory to: {custom_dir}")
|
944
|
+
|
945
|
+
# Create a new directory for testing
|
946
|
+
test_dir = os.path.join(os.getcwd(), "test_workdir")
|
947
|
+
print(f"Setting default working directory with auto-creation: {test_dir}")
|
948
|
+
agent_remote.set_default_workdir(test_dir, create_if_not_exists=True)
|
949
|
+
|
950
|
+
# Run shell command without specifying workdir - should use the default
|
951
|
+
shell_prompt_default_dir = "Run 'pwd' to show the current working directory."
|
952
|
+
|
953
|
+
response_shell_default = await agent_remote.run(shell_prompt_default_dir)
|
954
|
+
print("Shell Execution with Default Working Directory:")
|
955
|
+
print(response_shell_default)
|
956
|
+
|
957
|
+
# Run shell command with explicit workdir - should override the default
|
958
|
+
shell_prompt_explicit_dir = "Run 'pwd' in the /tmp directory."
|
959
|
+
|
960
|
+
response_shell_explicit = await agent_remote.run(shell_prompt_explicit_dir)
|
961
|
+
print("Shell Execution with Explicit Working Directory:")
|
962
|
+
print(response_shell_explicit)
|
963
|
+
|
672
964
|
await agent_remote.close()
|
673
965
|
await agent_local.close()
|
674
966
|
|
tinyagent/code_agent/utils.py
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
import sys
|
2
2
|
import cloudpickle
|
3
|
+
import subprocess
|
4
|
+
import os
|
3
5
|
from typing import Dict, Any, List
|
4
6
|
from .safety import validate_code_safety, function_safety_context
|
7
|
+
import shlex
|
5
8
|
|
6
9
|
|
7
10
|
def clean_response(resp: Dict[str, Any]) -> Dict[str, Any]:
|
@@ -41,6 +44,96 @@ def make_session_blob(ns: dict) -> bytes:
|
|
41
44
|
return cloudpickle.dumps(clean)
|
42
45
|
|
43
46
|
|
47
|
+
def _run_shell(
|
48
|
+
command: List[str],
|
49
|
+
timeout: int = 10,
|
50
|
+
workdir: str = None
|
51
|
+
) -> Dict[str, Any]:
|
52
|
+
"""
|
53
|
+
Execute a shell command securely with proper timeout and error handling.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
command: List of command parts to execute
|
57
|
+
timeout: Maximum execution time in seconds
|
58
|
+
workdir: Working directory for command execution
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Dictionary containing execution results with keys:
|
62
|
+
- stdout: stdout from the execution
|
63
|
+
- stderr: stderr from the execution
|
64
|
+
- exit_code: exit code from the command
|
65
|
+
"""
|
66
|
+
try:
|
67
|
+
# Set working directory if provided
|
68
|
+
cwd = os.path.expanduser(workdir) if workdir else None
|
69
|
+
|
70
|
+
# Check if this is a command that needs bash -c wrapping
|
71
|
+
if len(command) > 0:
|
72
|
+
# 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"]:
|
74
|
+
process = subprocess.run(
|
75
|
+
command,
|
76
|
+
shell=False, # No need for shell=True as we're explicitly using bash -c
|
77
|
+
capture_output=True,
|
78
|
+
text=True,
|
79
|
+
timeout=timeout,
|
80
|
+
cwd=cwd,
|
81
|
+
check=False
|
82
|
+
)
|
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)
|
101
|
+
process = subprocess.run(
|
102
|
+
["bash", "-c", shell_command],
|
103
|
+
shell=False, # Using explicit bash -c instead of shell=True
|
104
|
+
capture_output=True,
|
105
|
+
text=True,
|
106
|
+
timeout=timeout,
|
107
|
+
cwd=cwd,
|
108
|
+
check=False
|
109
|
+
)
|
110
|
+
else:
|
111
|
+
# Empty command
|
112
|
+
return {
|
113
|
+
"stdout": "",
|
114
|
+
"stderr": "Empty command",
|
115
|
+
"exit_code": 1
|
116
|
+
}
|
117
|
+
|
118
|
+
return {
|
119
|
+
"stdout": process.stdout,
|
120
|
+
"stderr": process.stderr,
|
121
|
+
"exit_code": process.returncode
|
122
|
+
}
|
123
|
+
except subprocess.TimeoutExpired:
|
124
|
+
return {
|
125
|
+
"stdout": "",
|
126
|
+
"stderr": f"Command timed out after {timeout} seconds",
|
127
|
+
"exit_code": 124 # Standard timeout exit code
|
128
|
+
}
|
129
|
+
except Exception as e:
|
130
|
+
return {
|
131
|
+
"stdout": "",
|
132
|
+
"stderr": f"Error executing command: {str(e)}",
|
133
|
+
"exit_code": 1
|
134
|
+
}
|
135
|
+
|
136
|
+
|
44
137
|
def _run_python(
|
45
138
|
code: str,
|
46
139
|
globals_dict: Dict[str, Any] | None = None,
|
@@ -48,6 +141,7 @@ def _run_python(
|
|
48
141
|
authorized_imports: List[str] | None = None,
|
49
142
|
authorized_functions: List[str] | None = None,
|
50
143
|
trusted_code: bool = False,
|
144
|
+
check_string_obfuscation: bool = True,
|
51
145
|
):
|
52
146
|
"""
|
53
147
|
Execute Python code in a controlled environment with proper error handling.
|
@@ -59,6 +153,7 @@ def _run_python(
|
|
59
153
|
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
154
|
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
155
|
trusted_code: If True, skip security checks. Should only be used for framework code, tools, or default executed code.
|
156
|
+
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
157
|
|
63
158
|
Returns:
|
64
159
|
Dictionary containing execution results
|
@@ -74,7 +169,8 @@ def _run_python(
|
|
74
169
|
# 1. Static safety analysis – refuse code containing dangerous imports or functions
|
75
170
|
# ------------------------------------------------------------------
|
76
171
|
validate_code_safety(code, authorized_imports=authorized_imports,
|
77
|
-
authorized_functions=authorized_functions, trusted_code=trusted_code
|
172
|
+
authorized_functions=authorized_functions, trusted_code=trusted_code,
|
173
|
+
check_string_obfuscation=check_string_obfuscation)
|
78
174
|
|
79
175
|
# Make copies to avoid mutating the original parameters
|
80
176
|
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"]
|