tiny-agent-os 0.0.1__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.
Files changed (64) hide show
  1. tiny_agent_os-0.0.1.dist-info/METADATA +377 -0
  2. tiny_agent_os-0.0.1.dist-info/RECORD +64 -0
  3. tiny_agent_os-0.0.1.dist-info/WHEEL +5 -0
  4. tiny_agent_os-0.0.1.dist-info/entry_points.txt +2 -0
  5. tiny_agent_os-0.0.1.dist-info/licenses/LICENSE +53 -0
  6. tiny_agent_os-0.0.1.dist-info/top_level.txt +1 -0
  7. tinyagent/__init__.py +75 -0
  8. tinyagent/_version.py +21 -0
  9. tinyagent/agent.py +957 -0
  10. tinyagent/chat/__init__.py +12 -0
  11. tinyagent/chat/chat_mode.py +291 -0
  12. tinyagent/cli/__init__.py +16 -0
  13. tinyagent/cli/colors.py +104 -0
  14. tinyagent/cli/main.py +664 -0
  15. tinyagent/cli/spinner.py +94 -0
  16. tinyagent/cli.py +47 -0
  17. tinyagent/config/__init__.py +14 -0
  18. tinyagent/config/config.py +258 -0
  19. tinyagent/decorators.py +187 -0
  20. tinyagent/exceptions.py +85 -0
  21. tinyagent/factory/__init__.py +18 -0
  22. tinyagent/factory/agent_factory.py +439 -0
  23. tinyagent/factory/dynamic_agent_factory.py +561 -0
  24. tinyagent/factory/orchestrator.py +1514 -0
  25. tinyagent/factory/tiny_chain.py +552 -0
  26. tinyagent/logging.py +97 -0
  27. tinyagent/mcp/__init__.py +14 -0
  28. tinyagent/mcp/manager.py +321 -0
  29. tinyagent/prompts/README.md +133 -0
  30. tinyagent/prompts/default.md +14 -0
  31. tinyagent/prompts/prompt_manager.py +206 -0
  32. tinyagent/prompts/system/agent.md +50 -0
  33. tinyagent/prompts/system/retry.md +55 -0
  34. tinyagent/prompts/system/strict_json.md +54 -0
  35. tinyagent/prompts/system.md +10 -0
  36. tinyagent/prompts/tools/calculator.md +13 -0
  37. tinyagent/prompts/tools/weather.md +7 -0
  38. tinyagent/prompts/workflows/riv_reflect.md +62 -0
  39. tinyagent/prompts/workflows/riv_verify.md +47 -0
  40. tinyagent/prompts/workflows/triage.md +129 -0
  41. tinyagent/tool.py +185 -0
  42. tinyagent/tools/README.md +391 -0
  43. tinyagent/tools/__init__.py +39 -0
  44. tinyagent/tools/aider.py +122 -0
  45. tinyagent/tools/anon_coder.py +296 -0
  46. tinyagent/tools/boilerplate_tool.py +147 -0
  47. tinyagent/tools/brave_search.py +104 -0
  48. tinyagent/tools/codeagent_tool.py +217 -0
  49. tinyagent/tools/content_processor.py +285 -0
  50. tinyagent/tools/custom_text_browser.py +965 -0
  51. tinyagent/tools/duckduckgo_search.py +153 -0
  52. tinyagent/tools/external.py +303 -0
  53. tinyagent/tools/file_manipulator.py +274 -0
  54. tinyagent/tools/final_extractor_tool.py +249 -0
  55. tinyagent/tools/llm_serializer.py +124 -0
  56. tinyagent/tools/markdown_gen.py +300 -0
  57. tinyagent/tools/ripgrep.py +136 -0
  58. tinyagent/utils/__init__.py +13 -0
  59. tinyagent/utils/json_parser.py +231 -0
  60. tinyagent/utils/logging_utils.py +78 -0
  61. tinyagent/utils/openrouter_request.py +123 -0
  62. tinyagent/utils/serialization.py +185 -0
  63. tinyagent/utils/structured_outputs.py +131 -0
  64. tinyagent/utils/type_converter.py +134 -0
@@ -0,0 +1,561 @@
1
+ """
2
+ Dynamic agent factory for creating specialized agents on-the-fly.
3
+
4
+ This module provides an enhanced factory class that extends AgentFactory with
5
+ capabilities for dynamically creating specialized agents and tools based on
6
+ natural language requirements.
7
+ """
8
+
9
+ import json
10
+ import re
11
+ from typing import Dict, List, Any, Optional, Callable, Union, TypeVar, Type, cast
12
+
13
+ from ..logging import get_logger
14
+ from ..config import load_config, get_config_value
15
+ from ..tool import Tool, ParamType
16
+ from ..agent import Agent, get_llm
17
+ from ..exceptions import ConfigurationError
18
+ from .agent_factory import AgentFactory
19
+
20
+ # Set up logger
21
+ logger = get_logger(__name__)
22
+
23
+ # Define a generic type variable for better type hints
24
+ T = TypeVar('T')
25
+
26
+
27
+ class DynamicAgentFactory(AgentFactory):
28
+ """
29
+ Enhanced factory for creating agents dynamically based on NLP analysis.
30
+
31
+ This class extends AgentFactory with capabilities for dynamically creating
32
+ specialized agents and tools based on natural language requirements.
33
+
34
+ Attributes:
35
+ _dynamic_agents: Dictionary of dynamically created agents
36
+ """
37
+
38
+ _instance = None
39
+
40
+ @classmethod
41
+ def get_instance(cls: Type[T], config: Optional[Dict[str, Any]] = None) -> T:
42
+ """
43
+ Get or create the singleton factory instance.
44
+
45
+ Args:
46
+ config: Optional configuration dictionary
47
+
48
+ Returns:
49
+ The singleton DynamicAgentFactory instance
50
+ """
51
+ if cls._instance is None:
52
+ cls._instance = cls(config)
53
+ return cls._instance
54
+
55
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
56
+ """
57
+ Initialize with parent class and add dynamic capabilities.
58
+
59
+ Args:
60
+ config: Optional configuration dictionary
61
+ """
62
+ super().__init__(config)
63
+ self._dynamic_agents: Dict[str, Agent] = {}
64
+
65
+ # Get dynamic agent configuration
66
+ if self.config:
67
+ max_agents = get_config_value(self.config, 'dynamic_agents.max_agents', 10)
68
+ allow_new_tools = get_config_value(self.config, 'dynamic_agents.allow_new_tools_by_default', False)
69
+ model = get_config_value(self.config, 'dynamic_agents.model', None)
70
+
71
+ logger.debug(f"Dynamic agent config: max_agents={max_agents}, allow_new_tools={allow_new_tools}")
72
+
73
+ self._max_agents = max_agents
74
+ self._allow_new_tools = allow_new_tools
75
+ self._model = model
76
+ else:
77
+ self._max_agents = 10
78
+ self._allow_new_tools = False
79
+ self._model = None
80
+
81
+ def create_dynamic_agent(self, task_description: str, model: Optional[str] = None) -> Agent:
82
+ """
83
+ Create a specialized agent based on task description using NLP analysis.
84
+
85
+ This method analyzes the task description to determine which tools are
86
+ needed, then creates a specialized agent with those tools.
87
+
88
+ Args:
89
+ task_description: Description of the task to be performed
90
+ model: Optional model to use for the agent
91
+
92
+ Returns:
93
+ A specialized agent with appropriate tools
94
+ """
95
+ # Use specified model or default from config
96
+ model = model or self._model
97
+
98
+ # Get LLM to analyze the task and determine required tools
99
+ llm = get_llm(model)
100
+
101
+ # Prompt to analyze the task
102
+ analysis_prompt = f"""
103
+ Analyze the following task description and determine which tools are needed:
104
+
105
+ Task: {task_description}
106
+
107
+ Available tools: {', '.join(self._tools.keys())}
108
+
109
+ Return a JSON object with the required tools and explanation:
110
+ {{
111
+ "required_tools": ["tool1", "tool2", ...],
112
+ "explanation": "Why these tools are needed"
113
+ }}
114
+
115
+ Keep your response minimal and focused only on the tools needed.
116
+ """
117
+
118
+ # Get analysis result
119
+ analysis_result = llm(analysis_prompt)
120
+
121
+ try:
122
+ # Find JSON using regex
123
+ json_match = re.search(r'({[\s\S]*})', analysis_result)
124
+ if json_match:
125
+ analysis = json.loads(json_match.group(1))
126
+ required_tools = analysis.get("required_tools", [])
127
+ else:
128
+ required_tools = []
129
+ logger.warning("Could not parse analysis result, using all tools")
130
+ # If parsing fails, use all tools
131
+ required_tools = list(self._tools.keys())
132
+ except Exception as e:
133
+ logger.error(f"Error parsing analysis result: {str(e)}")
134
+ # If parsing fails, use all tools
135
+ required_tools = list(self._tools.keys())
136
+
137
+ # Get selected tools
138
+ selected_tools = []
139
+ for name in required_tools:
140
+ if name in self._tools:
141
+ selected_tools.append(self._tools[name])
142
+
143
+ # Create the agent
144
+ agent = self.create_agent(
145
+ tools=selected_tools,
146
+ model=model
147
+ )
148
+
149
+ # Add metadata for tracking
150
+ agent_id = f"agent_{len(self._dynamic_agents) + 1}"
151
+ agent.name = f"Specialized Agent {agent_id}"
152
+ agent.description = f"Agent specialized for: {task_description[:100]}"
153
+
154
+ # Manage dynamic agents (remove oldest if exceeding limit)
155
+ if len(self._dynamic_agents) >= self._max_agents:
156
+ # Remove oldest agent (first item)
157
+ if self._dynamic_agents:
158
+ oldest_key = next(iter(self._dynamic_agents))
159
+ del self._dynamic_agents[oldest_key]
160
+ logger.info(f"Removed oldest dynamic agent {oldest_key} to make room for new one")
161
+
162
+ # Register the agent
163
+ self._dynamic_agents[agent_id] = agent
164
+
165
+ logger.info(f"Created dynamic agent {agent_id} with {len(selected_tools)} tools")
166
+
167
+ return agent
168
+
169
+ def can_handle_with_existing_tools(self, requirement: str, model: Optional[str] = None) -> Dict[str, Any]:
170
+ """
171
+ Analyze if a requirement can be handled with existing tools.
172
+
173
+ Args:
174
+ requirement: Natural language description of requirement
175
+ model: Optional model to use for analysis
176
+
177
+ Returns:
178
+ Dict with analysis results
179
+ """
180
+ # Use specified model or default from config
181
+ model = model or self._model
182
+
183
+ # Get LLM to analyze
184
+ llm = get_llm(model)
185
+
186
+ # Simple prompt to analyze if existing tools are sufficient
187
+ tool_prompt = f"""
188
+ Analyze the following requirement and determine if it can be handled
189
+ with our existing tools:
190
+
191
+ Requirement: {requirement}
192
+
193
+ Available tools: {', '.join(self._tools.keys())}
194
+
195
+ Return your analysis as JSON:
196
+ {{
197
+ "can_handle": true/false,
198
+ "required_tools": ["tool1", "tool2"],
199
+ "missing_capabilities": ["capability1", "capability2"],
200
+ "reasoning": "Your reasoning about whether existing tools can handle this"
201
+ }}
202
+ """
203
+
204
+ try:
205
+ # Get analysis from LLM
206
+ analysis_result = llm(tool_prompt)
207
+
208
+ # Try to find JSON object using regex
209
+ json_match = re.search(r'({[\s\S]*})', analysis_result)
210
+ if json_match:
211
+ try:
212
+ analysis = json.loads(json_match.group(1))
213
+
214
+ # Validate the analysis has required fields
215
+ if "can_handle" not in analysis:
216
+ analysis["can_handle"] = False
217
+
218
+ if "required_tools" not in analysis:
219
+ analysis["required_tools"] = []
220
+
221
+ if "missing_capabilities" not in analysis:
222
+ analysis["missing_capabilities"] = []
223
+
224
+ if "reasoning" not in analysis:
225
+ analysis["reasoning"] = "No reasoning provided"
226
+
227
+ # Validate that required tools actually exist
228
+ valid_tools = [t for t in analysis.get("required_tools", []) if t in self._tools]
229
+ analysis["required_tools"] = valid_tools
230
+
231
+ return {"success": True, "analysis": analysis}
232
+ except json.JSONDecodeError:
233
+ logger.error(f"Failed to parse JSON from regex match: {json_match.group(1)[:100]}...")
234
+
235
+ # Try parsing the whole response
236
+ try:
237
+ analysis = json.loads(analysis_result)
238
+
239
+ # Validate the analysis has required fields
240
+ if "can_handle" not in analysis:
241
+ analysis["can_handle"] = False
242
+
243
+ if "required_tools" not in analysis:
244
+ analysis["required_tools"] = []
245
+
246
+ if "missing_capabilities" not in analysis:
247
+ analysis["missing_capabilities"] = []
248
+
249
+ if "reasoning" not in analysis:
250
+ analysis["reasoning"] = "No reasoning provided"
251
+
252
+ # Validate that required tools actually exist
253
+ valid_tools = [t for t in analysis.get("required_tools", []) if t in self._tools]
254
+ analysis["required_tools"] = valid_tools
255
+
256
+ return {"success": True, "analysis": analysis}
257
+ except json.JSONDecodeError:
258
+ logger.error(f"Failed to parse full response as JSON: {analysis_result[:100]}...")
259
+
260
+ # If we get here, both parsing attempts failed
261
+ return {
262
+ "success": False,
263
+ "error": "Failed to parse response",
264
+ "raw_response": analysis_result[:500] + "..." if len(analysis_result) > 500 else analysis_result
265
+ }
266
+
267
+ except Exception as e:
268
+ logger.error(f"Error analyzing requirement: {str(e)}")
269
+ return {"success": False, "error": str(e)}
270
+
271
+ def create_agent_from_requirement(self, requirement: str, model: Optional[str] = None,
272
+ ask_permission: bool = True) -> Dict[str, Any]:
273
+ """
274
+ Create a new agent based on a natural language requirement.
275
+
276
+ This method analyzes the requirement to determine which tools are needed,
277
+ potentially creating new tools if they don't exist.
278
+
279
+ Args:
280
+ requirement: Natural language description of agent requirements
281
+ model: Optional model to use for the agent
282
+ ask_permission: Whether to ask permission before creating new tools
283
+
284
+ Returns:
285
+ Dict with new agent and metadata
286
+ """
287
+ # Use specified model or default from config
288
+ model = model or self._model
289
+
290
+ # First check if we can handle with existing tools
291
+ existing_tools_check = self.can_handle_with_existing_tools(requirement, model)
292
+
293
+ if existing_tools_check.get("success", False):
294
+ analysis = existing_tools_check["analysis"]
295
+ if analysis.get("can_handle", False):
296
+ # We can handle with existing tools, create an agent with just these tools
297
+ required_tool_names = analysis.get("required_tools", [])
298
+ selected_tools = []
299
+
300
+ for name in required_tool_names:
301
+ if name in self._tools:
302
+ selected_tools.append(self._tools[name])
303
+
304
+ # Add chat tool by default
305
+ if "chat" in self._tools and "chat" not in required_tool_names:
306
+ selected_tools.append(self._tools["chat"])
307
+
308
+ # Create agent with selected tools
309
+ agent = self.create_agent(tools=selected_tools, model=model)
310
+ agent.name = "Specialized Agent (Existing Tools)"
311
+ agent.description = f"Specialized agent for: {requirement}"
312
+
313
+ # Register the dynamic agent
314
+ agent_id = f"agent_{len(self._dynamic_agents) + 1}"
315
+
316
+ # Manage dynamic agents (remove oldest if exceeding limit)
317
+ if len(self._dynamic_agents) >= self._max_agents:
318
+ # Remove oldest agent (first item)
319
+ if self._dynamic_agents:
320
+ oldest_key = next(iter(self._dynamic_agents))
321
+ del self._dynamic_agents[oldest_key]
322
+ logger.info(f"Removed oldest dynamic agent {oldest_key} to make room for new one")
323
+
324
+ self._dynamic_agents[agent_id] = agent
325
+
326
+ return {
327
+ "success": True,
328
+ "agent": agent,
329
+ "used_existing_tools": True,
330
+ "tools": [t.name for t in selected_tools],
331
+ "agent_id": agent_id
332
+ }
333
+
334
+ # If we reach here, existing tools aren't sufficient, proceed with potential new tool creation
335
+ # Get LLM to analyze requirements
336
+ llm = get_llm(model)
337
+
338
+ # Analyze what tools are needed
339
+ tool_analysis_prompt = f"""
340
+ Analyze the following requirement and determine what tools are needed:
341
+
342
+ Requirement: {requirement}
343
+
344
+ Available tools: {', '.join(self._tools.keys())}
345
+
346
+ Return your analysis as JSON:
347
+ {{
348
+ "existing_tools_needed": ["tool1", "tool2"],
349
+ "new_tools_needed": [
350
+ {{
351
+ "name": "tool_name",
352
+ "description": "What the tool does",
353
+ "parameters": {{"param1": "string", "param2": "integer"}},
354
+ "implementation_details": "How this tool should be implemented"
355
+ }}
356
+ ],
357
+ "agent_name": "descriptive_name_for_agent",
358
+ "agent_description": "What this agent does"
359
+ }}
360
+ """
361
+
362
+ analysis_result = llm(tool_analysis_prompt)
363
+
364
+ # Parse response
365
+ try:
366
+ # Try to find JSON object using regex
367
+ json_match = re.search(r'({[\s\S]*})', analysis_result)
368
+ if json_match:
369
+ try:
370
+ analysis = json.loads(json_match.group(1))
371
+ except json.JSONDecodeError:
372
+ # Try the whole string if regex failed
373
+ analysis = json.loads(analysis_result)
374
+ else:
375
+ # Try the whole string
376
+ analysis = json.loads(analysis_result)
377
+ except json.JSONDecodeError:
378
+ logger.error(f"Could not parse LLM response as JSON: {analysis_result[:200]}...")
379
+ return {
380
+ "success": False,
381
+ "error": "Could not parse LLM response",
382
+ "raw_response": analysis_result[:500] if len(analysis_result) > 500 else analysis_result,
383
+ "agent": self.create_agent(tools=list(self._tools.values()), model=model)
384
+ }
385
+
386
+ # Check if we need to create new tools
387
+ new_tools_needed = analysis.get("new_tools_needed", [])
388
+ if new_tools_needed and ask_permission and not self._allow_new_tools:
389
+ # Return the analysis for the caller to handle permission
390
+ return {
391
+ "success": True,
392
+ "requires_permission": True,
393
+ "analysis": analysis,
394
+ "model": model
395
+ }
396
+
397
+ # Get existing tools
398
+ existing_tools = []
399
+ for tool_name in analysis.get("existing_tools_needed", []):
400
+ if tool_name in self._tools:
401
+ existing_tools.append(self._tools[tool_name])
402
+
403
+ # Always include chat tool
404
+ if "chat" in self._tools and "chat" not in analysis.get("existing_tools_needed", []):
405
+ existing_tools.append(self._tools["chat"])
406
+
407
+ # If we're allowed to create new tools
408
+ created_tools = []
409
+ if new_tools_needed and (not ask_permission or ask_permission is False or self._allow_new_tools):
410
+ for tool_spec in new_tools_needed:
411
+ # Create dynamic tool implementation
412
+ tool_impl = self._create_dynamic_tool_implementation(
413
+ tool_spec["name"],
414
+ tool_spec["description"],
415
+ tool_spec["parameters"],
416
+ tool_spec.get("implementation_details", ""),
417
+ model
418
+ )
419
+
420
+ # Convert parameter types
421
+ params = {}
422
+ for param_name, param_type in tool_spec["parameters"].items():
423
+ if isinstance(param_type, str):
424
+ if param_type.lower() == "string":
425
+ params[param_name] = ParamType.STRING
426
+ elif param_type.lower() == "integer":
427
+ params[param_name] = ParamType.INTEGER
428
+ elif param_type.lower() == "float":
429
+ params[param_name] = ParamType.FLOAT
430
+ else:
431
+ params[param_name] = ParamType.ANY
432
+ else:
433
+ params[param_name] = ParamType.ANY
434
+
435
+ # Create the tool
436
+ new_tool = Tool(
437
+ name=tool_spec["name"],
438
+ description=tool_spec["description"],
439
+ parameters=params,
440
+ func=tool_impl
441
+ )
442
+
443
+ # Register the tool
444
+ self.register_tool(new_tool)
445
+ created_tools.append(new_tool)
446
+ logger.info(f"Created new dynamic tool: {new_tool.name}")
447
+
448
+ # Combine existing and created tools
449
+ all_tools = existing_tools + created_tools
450
+
451
+ # Create the agent
452
+ agent = self.create_agent(tools=all_tools, model=model)
453
+
454
+ # Store the agent's name and description
455
+ agent.name = analysis.get("agent_name", "Specialized Agent")
456
+ agent.description = analysis.get("agent_description", "A specialized agent")
457
+
458
+ # Register the dynamic agent
459
+ agent_id = f"agent_{len(self._dynamic_agents) + 1}"
460
+
461
+ # Manage dynamic agents (remove oldest if exceeding limit)
462
+ if len(self._dynamic_agents) >= self._max_agents:
463
+ # Remove oldest agent (first item)
464
+ if self._dynamic_agents:
465
+ oldest_key = next(iter(self._dynamic_agents))
466
+ del self._dynamic_agents[oldest_key]
467
+ logger.info(f"Removed oldest dynamic agent {oldest_key} to make room for new one")
468
+
469
+ self._dynamic_agents[agent_id] = agent
470
+
471
+ return {
472
+ "success": True,
473
+ "agent": agent,
474
+ "created_tools": created_tools,
475
+ "existing_tools": existing_tools,
476
+ "agent_id": agent_id
477
+ }
478
+
479
+ def _create_dynamic_tool_implementation(
480
+ self,
481
+ name: str,
482
+ description: str,
483
+ parameters: Dict[str, str],
484
+ implementation_details: str,
485
+ model: Optional[str] = None
486
+ ) -> Callable:
487
+ """
488
+ Create a dynamic implementation for a tool using the LLM.
489
+
490
+ This method creates a function that uses an LLM to implement the tool's
491
+ functionality based on a natural language description.
492
+
493
+ Args:
494
+ name: Tool name
495
+ description: Tool description
496
+ parameters: Dictionary of parameter names to types
497
+ implementation_details: Natural language description of how to implement
498
+ model: Model to use
499
+
500
+ Returns:
501
+ A function that implements the tool
502
+ """
503
+ # Create LLM client
504
+ llm = get_llm(model)
505
+
506
+ def dynamic_tool_implementation(**kwargs):
507
+ """Dynamically implements a tool using LLM."""
508
+ # Format the kwargs as string for LLM prompt
509
+ kwargs_str = ", ".join([f"{k}={repr(v)}" for k, v in kwargs.items()])
510
+
511
+ # Prompt the LLM to implement the tool
512
+ prompt = f"""
513
+ You are implementing the '{name}' tool with the following description:
514
+ {description}
515
+
516
+ Implementation details:
517
+ {implementation_details}
518
+
519
+ This tool has been called with these parameters:
520
+ {kwargs_str}
521
+
522
+ Implement the tool's functionality and return the result. Be direct and return only what is needed.
523
+ """
524
+
525
+ # Get the implementation from LLM
526
+ result = llm(prompt)
527
+ return result
528
+
529
+ # Set the metadata
530
+ dynamic_tool_implementation.__name__ = name
531
+ dynamic_tool_implementation.__doc__ = description
532
+
533
+ return dynamic_tool_implementation
534
+
535
+ def get_dynamic_agent(self, agent_id: str) -> Optional[Agent]:
536
+ """
537
+ Get a dynamically created agent by ID.
538
+
539
+ Args:
540
+ agent_id: ID of the agent to retrieve
541
+
542
+ Returns:
543
+ The Agent instance or None if not found
544
+ """
545
+ return self._dynamic_agents.get(agent_id)
546
+
547
+ def list_dynamic_agents(self) -> Dict[str, Dict[str, Any]]:
548
+ """
549
+ List all dynamically created agents.
550
+
551
+ Returns:
552
+ Dictionary of agent IDs to metadata
553
+ """
554
+ return {
555
+ agent_id: {
556
+ "name": agent.name,
557
+ "description": agent.description,
558
+ "tools": [t.name for t in agent.tools.values()] if hasattr(agent, 'tools') else []
559
+ }
560
+ for agent_id, agent in self._dynamic_agents.items()
561
+ }