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.
@@ -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.system_prompt = self._build_system_prompt(system_prompt_template)
99
+ self.static_system_prompt= system_prompt
100
+ self.system_prompt = self._build_system_prompt(system_prompt_template)
101
+
76
102
 
77
- # Create the underlying TinyAgent
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 tool
87
- self._setup_code_execution_tool()
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 template_path is None:
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 _setup_code_execution_tool(self):
246
- """Set up the run_python tool using the code provider."""
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 str(result)
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
 
@@ -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 {}
@@ -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
- __all__ = ["RichUICallback", "RichCodeUICallback", "LoggingManager"]
5
+ from .token_tracker import TokenTracker, UsageStats, create_token_tracker
6
+
7
+ __all__ = ["RichUICallback", "RichCodeUICallback", "LoggingManager", "TokenTracker", "UsageStats", "create_token_tracker"]