tinyagent-py 0.0.7__py3-none-any.whl → 0.0.9__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.
@@ -0,0 +1,573 @@
1
+ import traceback
2
+ from textwrap import dedent
3
+ from typing import Optional, List, Dict, Any
4
+ from pathlib import Path
5
+ from tinyagent import TinyAgent, tool
6
+ from tinyagent.hooks.logging_manager import LoggingManager
7
+ from .providers.base import CodeExecutionProvider
8
+ from .providers.modal_provider import ModalProvider
9
+ from .helper import translate_tool_for_code_agent, load_template, render_system_prompt, prompt_code_example, prompt_qwen_helper
10
+
11
+
12
+ class TinyCodeAgent:
13
+ """
14
+ A TinyAgent specialized for code execution tasks.
15
+
16
+ This class provides a high-level interface for creating agents that can execute
17
+ Python code using various providers (Modal, Docker, local execution, etc.).
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ model: str = "gpt-4.1-mini",
23
+ api_key: Optional[str] = None,
24
+ log_manager: Optional[LoggingManager] = None,
25
+ provider: str = "modal",
26
+ tools: Optional[List[Any]] = None,
27
+ code_tools: Optional[List[Any]] = None,
28
+ authorized_imports: Optional[List[str]] = None,
29
+ system_prompt_template: Optional[str] = None,
30
+ provider_config: Optional[Dict[str, Any]] = None,
31
+ user_variables: Optional[Dict[str, Any]] = None,
32
+ pip_packages: Optional[List[str]] = None,
33
+ local_execution: bool = False,
34
+ **agent_kwargs
35
+ ):
36
+ """
37
+ Initialize TinyCodeAgent.
38
+
39
+ Args:
40
+ model: The language model to use
41
+ api_key: API key for the model
42
+ log_manager: Optional logging manager
43
+ provider: Code execution provider ("modal", "local", etc.)
44
+ tools: List of tools available to the LLM (regular tools)
45
+ code_tools: List of tools available in the Python execution environment
46
+ authorized_imports: List of authorized Python imports
47
+ system_prompt_template: Path to custom system prompt template
48
+ provider_config: Configuration for the code execution provider
49
+ user_variables: Dictionary of variables to make available in Python environment
50
+ pip_packages: List of additional Python packages to install in Modal environment
51
+ local_execution: If True, uses Modal's .local() method for local execution.
52
+ If False, uses Modal's .remote() method for cloud execution (default: False)
53
+ **agent_kwargs: Additional arguments passed to TinyAgent
54
+ """
55
+ self.model = model
56
+ self.api_key = api_key
57
+ self.log_manager = log_manager
58
+ self.tools = tools or [] # LLM tools
59
+ self.code_tools = code_tools or [] # Python environment tools
60
+ self.authorized_imports = authorized_imports or ["tinyagent", "gradio", "requests", "asyncio"]
61
+ self.provider_config = provider_config or {}
62
+ self.user_variables = user_variables or {}
63
+ self.pip_packages = pip_packages or []
64
+ self.local_execution = local_execution
65
+
66
+ # Create the code execution provider
67
+ self.code_provider = self._create_provider(provider, self.provider_config)
68
+
69
+ # Set user variables in the provider
70
+ if self.user_variables:
71
+ self.code_provider.set_user_variables(self.user_variables)
72
+
73
+ # Build system prompt
74
+ self.system_prompt = self._build_system_prompt(system_prompt_template)
75
+
76
+ # Create the underlying TinyAgent
77
+ self.agent = TinyAgent(
78
+ model=model,
79
+ api_key=api_key,
80
+ system_prompt=self.system_prompt,
81
+ logger=log_manager.get_logger('tinyagent.tiny_agent') if log_manager else None,
82
+ **agent_kwargs
83
+ )
84
+
85
+ # Add the code execution tool
86
+ self._setup_code_execution_tool()
87
+
88
+ # Add LLM tools (not code tools - those go to the provider)
89
+ if self.tools:
90
+ self.agent.add_tools(self.tools)
91
+
92
+ def _create_provider(self, provider_type: str, config: Dict[str, Any]) -> CodeExecutionProvider:
93
+ """Create a code execution provider based on the specified type."""
94
+ if provider_type.lower() == "modal":
95
+ # Merge pip_packages from both sources (direct parameter and provider_config)
96
+ config_pip_packages = config.get("pip_packages", [])
97
+ final_pip_packages = list(set(self.pip_packages + config_pip_packages))
98
+
99
+ final_config = config.copy()
100
+ final_config["pip_packages"] = final_pip_packages
101
+
102
+ return ModalProvider(
103
+ log_manager=self.log_manager,
104
+ code_tools=self.code_tools,
105
+ local_execution=self.local_execution,
106
+ **final_config
107
+ )
108
+ else:
109
+ raise ValueError(f"Unsupported provider type: {provider_type}")
110
+
111
+ def _build_system_prompt(self, template_path: Optional[str] = None) -> str:
112
+ """Build the system prompt for the code agent."""
113
+ # Use default template if none provided
114
+ if template_path is None:
115
+ template_path = str(Path(__file__).parent.parent / "prompts" / "code_agent.yaml")
116
+
117
+ # Translate code tools to code agent format
118
+ code_tools_metadata = {}
119
+ for tool in self.code_tools:
120
+ if hasattr(tool, '_tool_metadata'):
121
+ metadata = translate_tool_for_code_agent(tool)
122
+ code_tools_metadata[metadata["name"]] = metadata
123
+
124
+ # Load and render template
125
+ try:
126
+ template_str = load_template(template_path)
127
+ system_prompt = render_system_prompt(
128
+ template_str,
129
+ code_tools_metadata,
130
+ {},
131
+ self.authorized_imports
132
+ )
133
+ base_prompt = system_prompt + prompt_code_example + prompt_qwen_helper
134
+ except Exception as e:
135
+ # Fallback to a basic prompt if template loading fails
136
+ traceback.print_exc()
137
+ print(f"Failed to load template from {template_path}: {e}")
138
+ base_prompt = self._get_fallback_prompt()
139
+
140
+ # Add user variables information to the prompt
141
+ if self.user_variables:
142
+ variables_info = self._build_variables_prompt()
143
+ base_prompt += "\n\n" + variables_info
144
+
145
+ return base_prompt
146
+
147
+ def _get_fallback_prompt(self) -> str:
148
+ """Get a fallback system prompt if template loading fails."""
149
+ return dedent("""
150
+ You are a helpful AI assistant that can execute Python code to solve problems.
151
+
152
+ You have access to a run_python tool that can execute Python code in a sandboxed environment.
153
+ Use this tool to solve computational problems, analyze data, or perform any task that requires code execution.
154
+
155
+ When writing code:
156
+ - Always think step by step about the task
157
+ - Use print() statements to show intermediate results
158
+ - Handle errors gracefully
159
+ - Provide clear explanations of your approach
160
+
161
+ The user cannot see the direct output of run_python, so use final_answer to show results.
162
+ """)
163
+
164
+ def _build_variables_prompt(self) -> str:
165
+ """Build the variables section for the system prompt."""
166
+ if not self.user_variables:
167
+ return ""
168
+
169
+ variables_lines = ["## Available Variables", ""]
170
+ variables_lines.append("The following variables are pre-loaded and available in your Python environment:")
171
+ variables_lines.append("")
172
+
173
+ for var_name, var_value in self.user_variables.items():
174
+ var_type = type(var_value).__name__
175
+
176
+ # Try to get a brief description of the variable
177
+ if hasattr(var_value, 'shape') and hasattr(var_value, 'dtype'):
178
+ # Likely numpy array or pandas DataFrame
179
+ if hasattr(var_value, 'columns'):
180
+ # DataFrame
181
+ desc = f"DataFrame with shape {var_value.shape} and columns: {list(var_value.columns)}"
182
+ else:
183
+ # Array
184
+ desc = f"Array with shape {var_value.shape} and dtype {var_value.dtype}"
185
+ elif isinstance(var_value, (list, tuple)):
186
+ length = len(var_value)
187
+ if length > 0:
188
+ first_type = type(var_value[0]).__name__
189
+ desc = f"{var_type} with {length} items (first item type: {first_type})"
190
+ else:
191
+ desc = f"Empty {var_type}"
192
+ elif isinstance(var_value, dict):
193
+ keys_count = len(var_value)
194
+ if keys_count > 0:
195
+ sample_keys = list(var_value.keys())[:3]
196
+ desc = f"Dictionary with {keys_count} keys. Sample keys: {sample_keys}"
197
+ else:
198
+ desc = "Empty dictionary"
199
+ elif isinstance(var_value, str):
200
+ length = len(var_value)
201
+ preview = var_value[:50] + "..." if length > 50 else var_value
202
+ desc = f"String with {length} characters: '{preview}'"
203
+ else:
204
+ desc = f"{var_type}: {str(var_value)[:100]}"
205
+
206
+ variables_lines.append(f"- **{var_name}** ({var_type}): {desc}")
207
+
208
+ variables_lines.extend([
209
+ "",
210
+ "These variables are already loaded and ready to use in your code. You don't need to import or define them.",
211
+ "You can directly reference them by name in your Python code."
212
+ ])
213
+
214
+ return "\n".join(variables_lines)
215
+
216
+ def _build_code_tools_prompt(self) -> str:
217
+ """Build the code tools section for the system prompt."""
218
+ if not self.code_tools:
219
+ return ""
220
+
221
+ code_tools_lines = ["## Available Code Tools", ""]
222
+ code_tools_lines.append("The following code tools are available in your Python environment:")
223
+ code_tools_lines.append("")
224
+
225
+ for tool in self.code_tools:
226
+ if hasattr(tool, '_tool_metadata'):
227
+ metadata = translate_tool_for_code_agent(tool)
228
+ desc = f"- **{metadata['name']}** ({metadata['type']}): {metadata['description']}"
229
+ code_tools_lines.append(desc)
230
+
231
+ code_tools_lines.extend([
232
+ "",
233
+ "These tools are already loaded and ready to use in your code. You don't need to import or define them.",
234
+ "You can directly reference them by name in your Python code."
235
+ ])
236
+
237
+ return "\n".join(code_tools_lines)
238
+
239
+ def _setup_code_execution_tool(self):
240
+ """Set up the run_python tool using the code provider."""
241
+ @tool(name="run_python", description=dedent("""
242
+ This tool receives Python code and executes it in a sandboxed environment.
243
+ During each intermediate step, you can use 'print()' to save important information.
244
+ These print outputs will appear in the 'Observation:' field for the next step.
245
+
246
+ Args:
247
+ code_lines: list[str]: The Python code to execute as a list of strings.
248
+ Your code should include all necessary steps for successful execution,
249
+ cover edge cases, and include error handling.
250
+ Each line should be an independent line of code.
251
+
252
+ Returns:
253
+ Status of code execution or error message.
254
+ """))
255
+ async def run_python(code_lines: List[str], timeout: int = 120) -> str:
256
+ """Execute Python code using the configured provider."""
257
+ try:
258
+ result = await self.code_provider.execute_python(code_lines, timeout)
259
+ return str(result)
260
+ except Exception as e:
261
+ return f"Error executing code: {str(e)}"
262
+
263
+ self.agent.add_tool(run_python)
264
+
265
+ async def run(self, user_input: str, max_turns: int = 10) -> str:
266
+ """
267
+ Run the code agent with the given input.
268
+
269
+ Args:
270
+ user_input: The user's request or question
271
+ max_turns: Maximum number of conversation turns
272
+
273
+ Returns:
274
+ The agent's response
275
+ """
276
+ return await self.agent.run(user_input, max_turns)
277
+
278
+ async def connect_to_server(self, command: str, args: List[str], **kwargs):
279
+ """Connect to an MCP server."""
280
+ return await self.agent.connect_to_server(command, args, **kwargs)
281
+
282
+ def add_callback(self, callback):
283
+ """Add a callback to the agent."""
284
+ self.agent.add_callback(callback)
285
+
286
+ def add_tool(self, tool):
287
+ """Add a tool to the agent (LLM tool)."""
288
+ self.agent.add_tool(tool)
289
+
290
+ def add_tools(self, tools: List[Any]):
291
+ """Add multiple tools to the agent (LLM tools)."""
292
+ self.agent.add_tools(tools)
293
+
294
+ def add_code_tool(self, tool):
295
+ """
296
+ Add a code tool that will be available in the Python execution environment.
297
+
298
+ Args:
299
+ tool: The tool to add to the code execution environment
300
+ """
301
+ self.code_tools.append(tool)
302
+ # Update the provider with the new code tools
303
+ self.code_provider.set_code_tools(self.code_tools)
304
+ # Rebuild system prompt to include new code tools info
305
+ self.system_prompt = self._build_system_prompt()
306
+ # Update the agent's system prompt
307
+ self.agent.system_prompt = self.system_prompt
308
+
309
+ def add_code_tools(self, tools: List[Any]):
310
+ """
311
+ Add multiple code tools that will be available in the Python execution environment.
312
+
313
+ Args:
314
+ tools: List of tools to add to the code execution environment
315
+ """
316
+ self.code_tools.extend(tools)
317
+ # Update the provider with the new code tools
318
+ self.code_provider.set_code_tools(self.code_tools)
319
+ # Rebuild system prompt to include new code tools info
320
+ self.system_prompt = self._build_system_prompt()
321
+ # Update the agent's system prompt
322
+ self.agent.system_prompt = self.system_prompt
323
+
324
+ def remove_code_tool(self, tool_name: str):
325
+ """
326
+ Remove a code tool by name.
327
+
328
+ Args:
329
+ tool_name: Name of the tool to remove
330
+ """
331
+ self.code_tools = [tool for tool in self.code_tools
332
+ if not (hasattr(tool, '_tool_metadata') and
333
+ tool._tool_metadata.get('name') == tool_name)]
334
+ # Update the provider
335
+ self.code_provider.set_code_tools(self.code_tools)
336
+ # Rebuild system prompt
337
+ self.system_prompt = self._build_system_prompt()
338
+ # Update the agent's system prompt
339
+ self.agent.system_prompt = self.system_prompt
340
+
341
+ def get_code_tools(self) -> List[Any]:
342
+ """
343
+ Get a copy of current code tools.
344
+
345
+ Returns:
346
+ List of current code tools
347
+ """
348
+ return self.code_tools.copy()
349
+
350
+ def get_llm_tools(self) -> List[Any]:
351
+ """
352
+ Get a copy of current LLM tools.
353
+
354
+ Returns:
355
+ List of current LLM tools
356
+ """
357
+ return self.tools.copy()
358
+
359
+ def set_user_variables(self, variables: Dict[str, Any]):
360
+ """
361
+ Set user variables that will be available in the Python environment.
362
+
363
+ Args:
364
+ variables: Dictionary of variable name -> value pairs
365
+ """
366
+ self.user_variables = variables.copy()
367
+ self.code_provider.set_user_variables(self.user_variables)
368
+ # Rebuild system prompt to include new variables info
369
+ self.system_prompt = self._build_system_prompt()
370
+ # Update the agent's system prompt
371
+ self.agent.system_prompt = self.system_prompt
372
+
373
+ def add_user_variable(self, name: str, value: Any):
374
+ """
375
+ Add a single user variable.
376
+
377
+ Args:
378
+ name: Variable name
379
+ value: Variable value
380
+ """
381
+ self.user_variables[name] = value
382
+ self.code_provider.set_user_variables(self.user_variables)
383
+ # Rebuild system prompt to include new variables info
384
+ self.system_prompt = self._build_system_prompt()
385
+ # Update the agent's system prompt
386
+ self.agent.system_prompt = self.system_prompt
387
+
388
+ def remove_user_variable(self, name: str):
389
+ """
390
+ Remove a user variable.
391
+
392
+ Args:
393
+ name: Variable name to remove
394
+ """
395
+ if name in self.user_variables:
396
+ del self.user_variables[name]
397
+ self.code_provider.set_user_variables(self.user_variables)
398
+ # Rebuild system prompt
399
+ self.system_prompt = self._build_system_prompt()
400
+ # Update the agent's system prompt
401
+ self.agent.system_prompt = self.system_prompt
402
+
403
+ def get_user_variables(self) -> Dict[str, Any]:
404
+ """
405
+ Get a copy of current user variables.
406
+
407
+ Returns:
408
+ Dictionary of current user variables
409
+ """
410
+ return self.user_variables.copy()
411
+
412
+ def add_pip_packages(self, packages: List[str]):
413
+ """
414
+ Add additional pip packages to the Modal environment.
415
+ Note: This requires recreating the provider, so it's best to set packages during initialization.
416
+
417
+ Args:
418
+ packages: List of package names to install
419
+ """
420
+ self.pip_packages.extend(packages)
421
+ self.pip_packages = list(set(self.pip_packages)) # Remove duplicates
422
+
423
+ # Note: Adding packages after initialization requires recreating the provider
424
+ # This is expensive, so it's better to set packages during initialization
425
+ print("⚠️ Warning: Adding packages after initialization requires recreating the Modal environment.")
426
+ print(" For better performance, set pip_packages during TinyCodeAgent initialization.")
427
+
428
+ # Recreate the provider with new packages
429
+ self.code_provider = self._create_provider(self.provider, self.provider_config)
430
+
431
+ # Re-set user variables if they exist
432
+ if self.user_variables:
433
+ self.code_provider.set_user_variables(self.user_variables)
434
+
435
+ def get_pip_packages(self) -> List[str]:
436
+ """
437
+ Get a copy of current pip packages.
438
+
439
+ Returns:
440
+ List of pip packages that will be installed in Modal
441
+ """
442
+ return self.pip_packages.copy()
443
+
444
+ async def close(self):
445
+ """Clean up resources."""
446
+ await self.code_provider.cleanup()
447
+ await self.agent.close()
448
+
449
+ def clear_conversation(self):
450
+ """Clear the conversation history."""
451
+ self.agent.clear_conversation()
452
+
453
+ @property
454
+ def messages(self):
455
+ """Get the conversation messages."""
456
+ return self.agent.messages
457
+
458
+ @property
459
+ def session_id(self):
460
+ """Get the session ID."""
461
+ return self.agent.session_id
462
+
463
+
464
+ # Example usage demonstrating both LLM tools and code tools
465
+ async def run_example():
466
+ """
467
+ Example demonstrating TinyCodeAgent with both LLM tools and code tools.
468
+ Also shows how to use local vs remote execution.
469
+
470
+ LLM tools: Available to the LLM for direct calling
471
+ Code tools: Available in the Python execution environment
472
+ """
473
+ from tinyagent import tool
474
+
475
+ # Example LLM tool - available to the LLM for direct calling
476
+ @tool(name="search_web", description="Search the web for information")
477
+ async def search_web(query: str) -> str:
478
+ """Search the web for information."""
479
+ return f"Search results for: {query}"
480
+
481
+ # Example code tool - available in Python environment
482
+ @tool(name="data_processor", description="Process data arrays")
483
+ def data_processor(data: List[float]) -> Dict[str, Any]:
484
+ """Process a list of numbers and return statistics."""
485
+ return {
486
+ "mean": sum(data) / len(data),
487
+ "max": max(data),
488
+ "min": min(data),
489
+ "count": len(data)
490
+ }
491
+
492
+ print("🚀 Testing TinyCodeAgent with REMOTE execution (Modal)")
493
+ # Create TinyCodeAgent with remote execution (default)
494
+ agent_remote = TinyCodeAgent(
495
+ model="gpt-4.1-mini",
496
+ tools=[search_web], # LLM tools
497
+ code_tools=[data_processor], # Code tools
498
+ user_variables={
499
+ "sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
500
+ },
501
+ local_execution=False # Remote execution via Modal (default)
502
+ )
503
+
504
+ # Connect to MCP servers
505
+ await agent_remote.connect_to_server("npx", ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"])
506
+ await agent_remote.connect_to_server("npx", ["-y", "@modelcontextprotocol/server-sequential-thinking"])
507
+
508
+ # Test the remote agent
509
+ response_remote = await agent_remote.run("""
510
+ I have some sample data. Please use the data_processor tool in Python to analyze my sample_data
511
+ and show me the results.
512
+ """)
513
+
514
+ print("Remote Agent Response:")
515
+ print(response_remote)
516
+ print("\n" + "="*80 + "\n")
517
+
518
+ # Now test with local execution
519
+ print("🏠 Testing TinyCodeAgent with LOCAL execution")
520
+ agent_local = TinyCodeAgent(
521
+ model="gpt-4.1-mini",
522
+ tools=[search_web], # LLM tools
523
+ code_tools=[data_processor], # Code tools
524
+ user_variables={
525
+ "sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
526
+ },
527
+ local_execution=True # Local execution
528
+ )
529
+
530
+ # Connect to MCP servers
531
+ await agent_local.connect_to_server("npx", ["-y", "@openbnb/mcp-server-airbnb", "--ignore-robots-txt"])
532
+ await agent_local.connect_to_server("npx", ["-y", "@modelcontextprotocol/server-sequential-thinking"])
533
+
534
+ # Test the local agent
535
+ response_local = await agent_local.run("""
536
+ I have some sample data. Please use the data_processor tool in Python to analyze my sample_data
537
+ and show me the results.
538
+ """)
539
+
540
+ print("Local Agent Response:")
541
+ print(response_local)
542
+
543
+ # Demonstrate adding tools dynamically
544
+ @tool(name="validator", description="Validate processed results")
545
+ def validator(results: Dict[str, Any]) -> bool:
546
+ """Validate that results make sense."""
547
+ return all(key in results for key in ["mean", "max", "min", "count"])
548
+
549
+ # Add a new code tool to both agents
550
+ agent_remote.add_code_tool(validator)
551
+ agent_local.add_code_tool(validator)
552
+
553
+ print("\n" + "="*80)
554
+ print("🔧 Testing with dynamically added tools")
555
+
556
+ # Test both agents with the new tool
557
+ validation_prompt = "Now validate the previous analysis results using the validator tool."
558
+
559
+ response2_remote = await agent_remote.run(validation_prompt)
560
+ print("Remote Agent Validation Response:")
561
+ print(response2_remote)
562
+
563
+ response2_local = await agent_local.run(validation_prompt)
564
+ print("Local Agent Validation Response:")
565
+ print(response2_local)
566
+
567
+ await agent_remote.close()
568
+ await agent_local.close()
569
+
570
+
571
+ if __name__ == "__main__":
572
+ import asyncio
573
+ asyncio.run(run_example())
@@ -0,0 +1,3 @@
1
+ from .example_tools import get_weather, get_traffic
2
+
3
+ __all__ = ["get_weather", "get_traffic"]
@@ -0,0 +1,41 @@
1
+ from tinyagent import tool
2
+
3
+ # Global variables to track state across calls
4
+ weather_global = '-'
5
+ traffic_global = '-'
6
+
7
+
8
+ @tool(name="get_weather", description="Get the weather for a given city.")
9
+ def get_weather(city: str) -> str:
10
+ """Get the weather for a given city.
11
+ Args:
12
+ city: The city to get the weather for
13
+
14
+ Returns:
15
+ The weather for the given city
16
+ """
17
+ import random
18
+ global weather_global
19
+ output = f"Last time weather was checked was {weather_global}"
20
+ weather_global = random.choice(['sunny', 'cloudy', 'rainy', 'snowy'])
21
+ output += f"\n\nThe weather in {city} is now {weather_global}"
22
+
23
+ return output
24
+
25
+
26
+ @tool(name="get_traffic", description="Get the traffic for a given city.")
27
+ def get_traffic(city: str) -> str:
28
+ """Get the traffic for a given city.
29
+ Args:
30
+ city: The city to get the traffic for
31
+
32
+ Returns:
33
+ The traffic for the given city
34
+ """
35
+ import random
36
+ global traffic_global
37
+ output = f"Last time traffic was checked was {traffic_global}"
38
+ traffic_global = random.choice(['light', 'moderate', 'heavy', 'blocked'])
39
+ output += f"\n\nThe traffic in {city} is now {traffic_global}"
40
+
41
+ return output