tinyagent-py 0.0.12__tar.gz → 0.0.15__tar.gz

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.
Files changed (42) hide show
  1. {tinyagent_py-0.0.12/tinyagent_py.egg-info → tinyagent_py-0.0.15}/PKG-INFO +11 -1
  2. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/README.md +10 -0
  3. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/pyproject.toml +1 -1
  4. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/helper.py +2 -2
  5. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/modal_sandbox.py +1 -1
  6. tinyagent_py-0.0.15/tinyagent/code_agent/providers/base.py +353 -0
  7. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/providers/modal_provider.py +157 -32
  8. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/safety.py +6 -2
  9. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/tiny_code_agent.py +317 -11
  10. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/utils.py +129 -9
  11. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/hooks/__init__.py +3 -1
  12. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/hooks/gradio_callback.py +3 -2
  13. tinyagent_py-0.0.15/tinyagent/hooks/jupyter_notebook_callback.py +1464 -0
  14. tinyagent_py-0.0.15/tinyagent/hooks/token_tracker.py +564 -0
  15. tinyagent_py-0.0.15/tinyagent/prompts/summarize.yaml +96 -0
  16. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/tiny_agent.py +426 -17
  17. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15/tinyagent_py.egg-info}/PKG-INFO +11 -1
  18. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent_py.egg-info/SOURCES.txt +3 -0
  19. tinyagent_py-0.0.12/tinyagent/code_agent/providers/base.py +0 -152
  20. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/LICENSE +0 -0
  21. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/setup.cfg +0 -0
  22. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/__init__.py +0 -0
  23. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/__init__.py +0 -0
  24. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/example.py +0 -0
  25. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/providers/__init__.py +0 -0
  26. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/tools/__init__.py +0 -0
  27. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/code_agent/tools/example_tools.py +0 -0
  28. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/hooks/logging_manager.py +0 -0
  29. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/hooks/rich_code_ui_callback.py +0 -0
  30. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/hooks/rich_ui_callback.py +0 -0
  31. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/mcp_client.py +0 -0
  32. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/memory_manager.py +0 -0
  33. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/prompts/code_agent.yaml +0 -0
  34. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/storage/__init__.py +0 -0
  35. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/storage/base.py +0 -0
  36. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/storage/json_file_storage.py +0 -0
  37. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/storage/postgres_storage.py +0 -0
  38. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/storage/redis_storage.py +0 -0
  39. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent/storage/sqlite_storage.py +0 -0
  40. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent_py.egg-info/dependency_links.txt +0 -0
  41. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent_py.egg-info/requires.txt +0 -0
  42. {tinyagent_py-0.0.12 → tinyagent_py-0.0.15}/tinyagent_py.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinyagent-py
3
- Version: 0.0.12
3
+ Version: 0.0.15
4
4
  Summary: TinyAgent with MCP Client, Code Agent (Thinking, Planning, and Executing in Python), and Extendable Hooks, Tiny but powerful
5
5
  Author-email: Mahdi Golchin <golchin@askdev.ai>
6
6
  Project-URL: Homepage, https://github.com/askbudi/tinyagent
@@ -60,6 +60,16 @@ Inspired by:
60
60
  ## Quick Links
61
61
  - [Build your own Tiny Agent](https://askdev.ai/github/askbudi/tinyagent)
62
62
 
63
+
64
+ ## Live Projects using TinyAgent (🔥)
65
+ - [AskDev.AI](https://askdev.ai) - Understand, chat, and summarize codebase of any project on GitHub.
66
+ - [HackBuddy AI](https://huggingface.co/spaces/ask-dev/HackBuddyAI) - A Hackathon Assistant Agent, built with TinyCodeAgent and Gradio. Match invdividuals to teams based on their skills, interests and organizer preferences.
67
+
68
+ - [TinyCodeAgent Demo](https://huggingface.co/spaces/ask-dev/TinyCodeAgent) - A playground for TinyCodeAgent, built with tinyagent, Gradio and Modal.com
69
+
70
+ ** Building something with TinyAgent? Let us know and I'll add it here!**
71
+
72
+
63
73
  ## Overview
64
74
  This is a tiny agent framework that uses MCP and LiteLLM to interact with language models. You have full control over the agent, you can add any tools you like from MCP and extend the agent using its event system.
65
75
 
@@ -18,6 +18,16 @@ Inspired by:
18
18
  ## Quick Links
19
19
  - [Build your own Tiny Agent](https://askdev.ai/github/askbudi/tinyagent)
20
20
 
21
+
22
+ ## Live Projects using TinyAgent (🔥)
23
+ - [AskDev.AI](https://askdev.ai) - Understand, chat, and summarize codebase of any project on GitHub.
24
+ - [HackBuddy AI](https://huggingface.co/spaces/ask-dev/HackBuddyAI) - A Hackathon Assistant Agent, built with TinyCodeAgent and Gradio. Match invdividuals to teams based on their skills, interests and organizer preferences.
25
+
26
+ - [TinyCodeAgent Demo](https://huggingface.co/spaces/ask-dev/TinyCodeAgent) - A playground for TinyCodeAgent, built with tinyagent, Gradio and Modal.com
27
+
28
+ ** Building something with TinyAgent? Let us know and I'll add it here!**
29
+
30
+
21
31
  ## Overview
22
32
  This is a tiny agent framework that uses MCP and LiteLLM to interact with language models. You have full control over the agent, you can add any tools you like from MCP and extend the agent using its event system.
23
33
 
@@ -12,7 +12,7 @@ tinyagent = ["prompts/*.yaml"]
12
12
 
13
13
  [project]
14
14
  name = "tinyagent-py"
15
- version = "0.0.12"
15
+ version = "0.0.15"
16
16
  description = "TinyAgent with MCP Client, Code Agent (Thinking, Planning, and Executing in Python), and Extendable Hooks, Tiny but powerful"
17
17
  readme = "README.md"
18
18
  authors = [
@@ -47,13 +47,13 @@ You are an Agent, You need to solve the task, not suggesting user about how to s
47
47
 
48
48
  """)
49
49
 
50
- def load_template(path: str) -> str:
50
+ def load_template(path: str,key:str="system_prompt") -> str:
51
51
  """
52
52
  Load the YAML file and extract its 'system_prompt' field.
53
53
  """
54
54
  with open(path, "r") as f:
55
55
  data = yaml.safe_load(f)
56
- return data["system_prompt"]
56
+ return data[key]
57
57
 
58
58
  def render_system_prompt(template_str: str,
59
59
  tools: dict,
@@ -78,7 +78,7 @@ def create_sandbox(
78
78
 
79
79
  if apt_packages is None:
80
80
  # Always install the basics required for most workflows
81
- apt_packages = ("git", "curl", "nodejs", "npm")
81
+ apt_packages = ("git", "curl", "nodejs", "npm","ripgrep","tree")
82
82
 
83
83
  if default_packages is None:
84
84
  default_packages = (
@@ -0,0 +1,353 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, List, Any, Optional, Set
3
+ from tinyagent.hooks.logging_manager import LoggingManager
4
+ import cloudpickle
5
+
6
+
7
+ class CodeExecutionProvider(ABC):
8
+ """
9
+ Abstract base class for code execution providers.
10
+
11
+ This class defines the interface that all code execution providers must implement.
12
+ It allows for easy extension to support different execution environments
13
+ (Modal, Docker, local execution, cloud functions, etc.) with minimal code changes.
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ log_manager: LoggingManager,
19
+ default_python_codes: Optional[List[str]] = None,
20
+ code_tools: List[Dict[str, Any]] = None,
21
+ pip_packages: List[str] = None,
22
+ secrets: Dict[str, Any] = None,
23
+ lazy_init: bool = True,
24
+ **kwargs
25
+ ):
26
+ self.log_manager = log_manager
27
+ self.default_python_codes = default_python_codes or []
28
+ self.code_tools = code_tools or []
29
+ self.pip_packages = pip_packages or []
30
+ self.secrets = secrets or {}
31
+ self.lazy_init = lazy_init
32
+ self.kwargs = kwargs
33
+ self.executed_default_codes = False
34
+ self._globals_dict = kwargs.get("globals_dict", {})
35
+ self._locals_dict = kwargs.get("locals_dict", {})
36
+ self._user_variables = {}
37
+ self.code_tools_definitions = []
38
+ # Safe shell commands that don't modify the system or access sensitive data
39
+ self.safe_shell_commands: Set[str] = {
40
+ "ls", "cat", "grep", "find", "echo", "pwd", "whoami", "date",
41
+ "head", "tail", "wc", "sort", "uniq", "tr", "cut", "sed", "awk",
42
+ "ps", "df", "du", "uname", "which", "type", "file", "stat","rg","if",
43
+ "tree"
44
+ }
45
+ # Safe control operators for shell commands
46
+ self.safe_control_operators: Set[str] = {"&&", "||", ";", "|"}
47
+
48
+ @abstractmethod
49
+ async def execute_python(
50
+ self,
51
+ code_lines: List[str],
52
+ timeout: int = 120
53
+ ) -> Dict[str, Any]:
54
+ """
55
+ Execute Python code and return the result.
56
+
57
+ Args:
58
+ code_lines: List of Python code lines to execute
59
+ timeout: Maximum execution time in seconds
60
+
61
+ Returns:
62
+ Dictionary containing execution results with keys:
63
+ - printed_output: stdout from the execution
64
+ - return_value: the return value if any
65
+ - stderr: stderr from the execution
66
+ - error_traceback: exception traceback if any error occurred
67
+ """
68
+ pass
69
+
70
+ @abstractmethod
71
+ async def execute_shell(
72
+ self,
73
+ command: List[str],
74
+ timeout: int = 10,
75
+ workdir: Optional[str] = None
76
+ ) -> Dict[str, Any]:
77
+ """
78
+ Execute a shell command securely and return the result.
79
+
80
+ Args:
81
+ command: List of command parts to execute
82
+ timeout: Maximum execution time in seconds
83
+ workdir: Working directory for command execution
84
+
85
+ Returns:
86
+ Dictionary containing execution results with keys:
87
+ - stdout: stdout from the execution
88
+ - stderr: stderr from the execution
89
+ - exit_code: exit code from the command
90
+ """
91
+ pass
92
+
93
+ def is_safe_command(self, command: List[str]) -> Dict[str, Any]:
94
+ """
95
+ Check if a shell command is safe to execute.
96
+
97
+ Args:
98
+ command: List of command parts to check
99
+
100
+ Returns:
101
+ Dictionary with:
102
+ - safe: Boolean indicating if command is safe
103
+ - reason: Reason why command is not safe (if applicable)
104
+ """
105
+ if type(command) == str:
106
+ command = command.split(" ")
107
+ if not command or not isinstance(command, list) or len(command) == 0:
108
+ return {"safe": False, "reason": "Empty or invalid command"}
109
+
110
+ # Special handling for bash -c or bash -lc commands
111
+ if len(command) >= 3 and command[0] == "bash" and command[1] in ["-c", "-lc"]:
112
+ # For bash -c or bash -lc, we need to parse the command string that follows
113
+ # We'll extract commands from the bash command string and check them
114
+ bash_cmd_str = command[2]
115
+
116
+ # Simple parsing of the bash command to extract command names
117
+ # This is a basic implementation and might not cover all edge cases
118
+ import shlex
119
+ import re
120
+
121
+ try:
122
+ # Shell script keywords that should be allowed
123
+ shell_keywords = {
124
+ "if", "then", "else", "elif", "fi", "for", "do", "done",
125
+ "while", "until", "case", "esac", "in", "function", "select",
126
+ "time", "coproc", "true", "false"
127
+ }
128
+
129
+ # Split the command by common shell operators
130
+ cmd_parts = re.split(r'(\||;|&&|\|\||>|>>|<|<<)', bash_cmd_str)
131
+ commands_to_check = []
132
+
133
+ for part in cmd_parts:
134
+ part = part.strip()
135
+ if part and part not in ['|', ';', '&&', '||', '>', '>>', '<', '<<']:
136
+ # Get the first word which is typically the command
137
+ try:
138
+ words = shlex.split(part)
139
+ if words:
140
+ cmd_name = words[0].split('/')[-1] # Extract binary name
141
+
142
+ # Skip shell keywords
143
+ if cmd_name in shell_keywords:
144
+ continue
145
+
146
+ # Skip variable assignments (e.g., VAR=value)
147
+ if re.match(r'^[A-Za-z_][A-Za-z0-9_]*=', cmd_name):
148
+ continue
149
+
150
+ if cmd_name not in self.safe_shell_commands and '*' not in cmd_name and '?' not in cmd_name:
151
+ return {"safe": False, "reason": f"Unsafe command in bash script: {cmd_name}"}
152
+ except Exception:
153
+ # If parsing fails, be cautious and reject
154
+ return {"safe": False, "reason": "Could not parse bash command safely"}
155
+
156
+ # All commands in the bash script are safe
157
+ return {"safe": True}
158
+ except Exception as e:
159
+ return {"safe": False, "reason": f"Error parsing bash command: {str(e)}"}
160
+
161
+ # Normal command processing for non-bash -c commands
162
+ # Shell operators that might be passed as separate arguments
163
+ shell_operators = ['|', '>', '<', '>>', '<<', '&&', '||', ';']
164
+
165
+ # Extract actual commands from the command list, ignoring shell operators
166
+ commands_to_check = []
167
+ i = 0
168
+ while i < len(command):
169
+ if command[i] in shell_operators:
170
+ i += 1
171
+ continue
172
+
173
+ # Extract the binary name
174
+ bin_name = command[i].split("/")[-1]
175
+ commands_to_check.append(bin_name)
176
+
177
+ # Skip to next command after an operator
178
+ i += 1
179
+ while i < len(command) and command[i] not in shell_operators:
180
+ i += 1
181
+
182
+ # Check if all commands are in the safe list
183
+ for cmd in commands_to_check:
184
+ # Handle wildcards in command names (e.g., *.py)
185
+ if '*' in cmd or '?' in cmd:
186
+ continue
187
+
188
+ if cmd not in self.safe_shell_commands:
189
+ return {"safe": False, "reason": f"Unsafe command: {cmd}"}
190
+
191
+ return {"safe": True}
192
+
193
+ @abstractmethod
194
+ async def cleanup(self):
195
+ """Clean up any resources used by the provider."""
196
+ pass
197
+
198
+ def add_tools(self, tools: List[Any]) -> None:
199
+ """
200
+ Add tools to the execution environment.
201
+
202
+ Args:
203
+ tools: List of tool objects to add
204
+ """
205
+ tools_str_list = ["import cloudpickle"]
206
+ tools_str_list.append("###########<tools>###########\n")
207
+ for tool in tools:
208
+ tools_str_list.append(
209
+ f"globals()['{tool._tool_metadata['name']}'] = cloudpickle.loads({cloudpickle.dumps(tool)})"
210
+ )
211
+ tools_str_list.append("\n\n")
212
+ tools_str_list.append("###########</tools>###########\n")
213
+ tools_str_list.append("\n\n")
214
+ self.code_tools_definitions.extend(tools_str_list)
215
+
216
+ def set_code_tools(self, tools: List[Any]) -> None:
217
+ """
218
+ Set the code tools available in the execution environment.
219
+ Replaces any existing tools with the new list.
220
+
221
+ Args:
222
+ tools: List of tool objects to set
223
+ """
224
+ # Clear existing tools
225
+ self.code_tools = tools.copy()
226
+ self.code_tools_definitions = []
227
+
228
+ # Add the new tools
229
+ if tools:
230
+ self.add_tools(tools)
231
+
232
+ def set_user_variables(self, variables: Dict[str, Any]) -> None:
233
+ """
234
+ Set user variables that will be available in the Python environment.
235
+
236
+ Args:
237
+ variables: Dictionary of variable name -> value pairs
238
+ """
239
+ self._user_variables = variables.copy()
240
+
241
+ # Add variables to the execution environment by serializing them
242
+ # This ensures they are available when code is executed
243
+ variables_str_list = ["import cloudpickle"]
244
+ variables_str_list.append("###########<user_variables>###########\n")
245
+
246
+ for var_name, var_value in variables.items():
247
+ # Serialize the variable and add it to globals
248
+ serialized_var = cloudpickle.dumps(var_value)
249
+ variables_str_list.append(
250
+ f"globals()['{var_name}'] = cloudpickle.loads({serialized_var})"
251
+ )
252
+
253
+ variables_str_list.append("\n###########</user_variables>###########\n")
254
+ variables_str_list.append("\n")
255
+
256
+ # Remove any existing user variables from default codes
257
+ self._remove_existing_user_variables()
258
+
259
+ # Add new variables to default codes at the beginning (after tools if any)
260
+ # This ensures variables are available from the start
261
+ if variables_str_list:
262
+ # Find where to insert (after tools section if it exists)
263
+ insert_index = 0
264
+ for i, code in enumerate(self.code_tools_definitions):
265
+ if "###########</tools>###########" in code:
266
+ insert_index = i + 1
267
+ break
268
+
269
+ # Insert the variables code
270
+ for j, var_code in enumerate(variables_str_list):
271
+ self.code_tools_definitions.insert(insert_index + j, var_code)
272
+
273
+ def _remove_existing_user_variables(self) -> None:
274
+ """Remove existing user variables from default python codes."""
275
+ # Find and remove the user variables section
276
+ start_index = None
277
+ end_index = None
278
+
279
+ for i, code in enumerate(self.code_tools_definitions):
280
+ if "###########<user_variables>###########" in code:
281
+ start_index = i - 1 if i > 0 and "import cloudpickle" in self.code_tools_definitions[i-1] else i
282
+ elif "###########</user_variables>###########" in code:
283
+ end_index = i + 2 # Include the newline after
284
+ break
285
+
286
+ if start_index is not None and end_index is not None:
287
+ # Remove the old variables section
288
+ del self.code_tools_definitions[start_index:end_index]
289
+
290
+ def get_user_variables(self) -> Dict[str, Any]:
291
+ """
292
+ Get a copy of current user variables.
293
+
294
+ Returns:
295
+ Dictionary of current user variables
296
+ """
297
+ return self._user_variables.copy()
298
+
299
+ def update_user_variables_from_globals(self, globals_dict: Dict[str, Any]) -> None:
300
+ """
301
+ Extract and update user variables from the globals dictionary after code execution.
302
+ This ensures that any modifications to user variables during code execution are preserved.
303
+
304
+ Args:
305
+ globals_dict: The globals dictionary after code execution
306
+ """
307
+ if not globals_dict or not self._user_variables:
308
+ return
309
+
310
+ # Update user variables with values from globals
311
+ for var_name in list(self._user_variables.keys()):
312
+ if var_name in globals_dict:
313
+ try:
314
+ # Try to serialize the value to ensure it's valid
315
+ cloudpickle.dumps(globals_dict[var_name])
316
+ # Update the user variable with the new value
317
+ self._user_variables[var_name] = globals_dict[var_name]
318
+ except Exception:
319
+ # If serialization fails, keep the old value
320
+ pass
321
+
322
+ # Check for new variables that might have been created
323
+ # This handles cases where LLM creates new variables that should be preserved
324
+ for var_name, var_value in globals_dict.items():
325
+ # Skip special variables, modules, and functions
326
+ if (var_name.startswith('__') or
327
+ var_name in ['builtins', 'cloudpickle'] or
328
+ callable(var_value) or
329
+ var_name in self._user_variables):
330
+ continue
331
+
332
+ try:
333
+ # Try to serialize the value to ensure it's valid
334
+ cloudpickle.dumps(var_value)
335
+ # Add the new variable to user variables
336
+ self._user_variables[var_name] = var_value
337
+ except Exception:
338
+ # If serialization fails, skip this variable
339
+ pass
340
+
341
+ def shell_response_to_llm_understandable(self, response: Dict[str, Any]) -> str:
342
+ """
343
+ Convert a shell command response to a format that is understandable by the LLM.
344
+ """
345
+ if response.get('stderr',None) not in [None,""]:
346
+ error_message = "Bash Error: " + response['stderr']
347
+ if "No such file or directory" in response['stderr']:
348
+ error_message.replace("No such file or directory", "No such file or directory, Have you provided the correct absolute path? If you are unsure use ls first to make sure the path exists")
349
+ if "Command timed out after" in response['stderr']:
350
+ error_message += ", Make sure your command is specific enough. And only if it is the most specific and optimized command then try to increase the timeout parameter if you need to more time for this command."
351
+ return error_message
352
+ else:
353
+ return response['stdout']