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
tinyagent/cli/main.py ADDED
@@ -0,0 +1,664 @@
1
+ """
2
+ Main CLI functionality for the tinyAgent framework.
3
+
4
+ This module provides the main entry point for the tinyAgent CLI, handling
5
+ command-line arguments, tool execution, and interactive mode.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import argparse
11
+ import re
12
+ from typing import List, Dict
13
+
14
+ from ..logging import get_logger, configure_logging
15
+ from ..config import load_config, get_config_value
16
+ from ..agent import Agent
17
+ from ..tool import Tool
18
+ from ..mcp import ensure_mcp_server
19
+
20
+ #import all of the tools
21
+ from ..tools import *
22
+ from .colors import Colors
23
+ from .spinner import Spinner
24
+
25
+ # Set up logger
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ def print_banner() -> None:
30
+ """
31
+ Print a banner for the CLI.
32
+
33
+ This function prints a stylized ASCII art banner for the tinyAgent CLI,
34
+ setting the visual tone for the application.
35
+ """
36
+ banner = rf"""
37
+ __ .__ _____ __
38
+ _/ |_|__| ____ ___.__. / _ \ ____ ____ _____/ |_
39
+ \ __\ |/ < | |/ /_\ \ / ___\_/ __ \ / \ __\
40
+ | | | | | \___ / | \/ /_/ > ___/| | \ |
41
+ |__| |__|___| / ____\____|__ /\___ / \___ >___| /__|
42
+ \/\/ \//_____/ \/ \/
43
+ {Colors.BOLD}tinyAgent: AGI made simple{Colors.RESET}
44
+
45
+ {Colors.BOLD}Made by (x) @tunahorse21 | A product of alchemiststudios.ai{Colors.RESET}
46
+
47
+ {Colors.DARK_RED}IMPORTANT: tinyAgent is in EARLY BETA until V1. Use common sense.
48
+ NOT RESPONSIBLE FOR ANY ISSUES that may arise from its use.{Colors.RESET}
49
+ """
50
+
51
+ print(banner)
52
+
53
+
54
+ def print_tools_box(tools: Dict[str, Tool]) -> None:
55
+ """
56
+ Print available tools in a simple, clean format.
57
+
58
+ Args:
59
+ tools: Dictionary of tool names to Tool objects
60
+ """
61
+ if not tools:
62
+ return
63
+
64
+ tool_names = list(tools.keys())
65
+ if not tool_names:
66
+ return
67
+
68
+ # Calculate width
69
+ width = 50
70
+
71
+ print(f"\n{Colors.YELLOW}Available Tools:{Colors.RESET}")
72
+ print(f"{Colors.DARK_RED}╭{'─' * width}╮{Colors.RESET}")
73
+
74
+ # Simple header
75
+ print(f"{Colors.DARK_RED}│ {Colors.YELLOW}#{Colors.RESET} │ {Colors.YELLOW}Tool{Colors.RESET} │ {Colors.YELLOW}Description{Colors.RESET}{' ' * 35} │{Colors.RESET}")
76
+
77
+ # Divider
78
+ print(f"{Colors.DARK_RED}├{'─' * width}┤{Colors.RESET}")
79
+
80
+ # Print each tool with minimal formatting
81
+ for i, name in enumerate(sorted(tool_names)):
82
+ tool = tools[name]
83
+
84
+ # Just truncate description without trying to format
85
+ desc = tool.description
86
+ if len(desc) > 65:
87
+ desc = desc[:60] + "..."
88
+
89
+ # Color for the tool name
90
+ tool_color = Colors.LIGHT_RED if name == 'triage_agent' else Colors.CYAN
91
+
92
+ # Extra simple format
93
+ print(f"{Colors.DARK_RED}│{Colors.RESET} {i+1} │ {tool_color}{name}{Colors.RESET} │ {desc}")
94
+
95
+ print(f"{Colors.DARK_RED}╰{'─' * width}╯{Colors.RESET}")
96
+
97
+
98
+ def parse_arguments() -> argparse.Namespace:
99
+ """
100
+ Parse command-line arguments for the tinyAgent CLI.
101
+
102
+ This function parses command-line arguments using argparse, providing a
103
+ variety of options to control the behavior of the tinyAgent CLI.
104
+
105
+ Returns:
106
+ Parsed command-line arguments
107
+ """
108
+ parser = argparse.ArgumentParser(description="TinyAgent - A simple LLM-powered agent framework")
109
+ parser.add_argument("tool", nargs="?", help="Tool to execute directly or chain of tools separated by '|'")
110
+ parser.add_argument("args", nargs="?", help="Tool arguments (e.g., file paths)")
111
+ parser.add_argument("prompt", nargs="*", help="Prompt to pass to the tool or agent")
112
+ parser.add_argument("--model", "-m", default=None, help="Model to use for LLM")
113
+ parser.add_argument("--list-tools", "-l", action="store_true", help="List available tools")
114
+ parser.add_argument("--output-dir", "-o", help="Directory to save output for tools that support it")
115
+ parser.add_argument("--template", "-t", help="Path to prompt template file")
116
+ parser.add_argument("--vars", "-v", help="Variables to use in the template as JSON string")
117
+ parser.add_argument("--verbose", "-V", action="store_true", help="Enable verbose logging")
118
+ parser.add_argument("--quiet", "-q", action="store_true", help="Suppress non-error output")
119
+ parser.add_argument("--config", "-c", help="Path to configuration file")
120
+
121
+ return parser.parse_args()
122
+
123
+
124
+ def load_tools() -> List[Tool]:
125
+ """
126
+ Load all available tools.
127
+
128
+ This function loads tools from multiple sources, including built-in tools,
129
+ tools in the tools directory, and MCP tools.
130
+
131
+ Returns:
132
+ List of Tool objects
133
+ """
134
+ try:
135
+ # Load built-in tools that don't depend on MCP
136
+ tools = [
137
+ anon_coder_tool,
138
+ llm_serializer_tool,
139
+ ripgrep_tool,
140
+ aider_tool,
141
+ process_content,
142
+ file_manipulator_tool,
143
+ custom_text_browser_tool,
144
+ final_answer_extractor,
145
+ ]
146
+
147
+ # Try to ensure MCP server is running, but don't fail if it doesn't start
148
+ mcp_available = ensure_mcp_server()
149
+
150
+ # Only add MCP-dependent tools if MCP server is available
151
+ if mcp_available:
152
+ tools.append(brave_web_search_tool)
153
+ tools.append(duckduckgo_search_tool)
154
+ logger.info("MCP server is available, MCP-dependent tools loaded")
155
+ else:
156
+ logger.warning("MCP server is not available, MCP-dependent tools will not be loaded")
157
+
158
+ # Load external tools
159
+ external_tools = load_external_tools()
160
+ tools.extend(external_tools)
161
+
162
+ return tools
163
+ except ImportError as e:
164
+ logger.error(f"Error loading tools: {e}")
165
+ return []
166
+
167
+
168
+ def format_result(result):
169
+ """
170
+ Format results for CLI display with special handling for different result types.
171
+
172
+ Args:
173
+ result: The result to format
174
+
175
+ Returns:
176
+ Formatted string representation of the result
177
+ """
178
+ if not result:
179
+ return "No results available"
180
+
181
+ # Handle list of search results with title/description/url structure
182
+ if isinstance(result, list) and result and all(isinstance(item, dict) and 'title' in item for item in result):
183
+ output = ["📊 Search Results:", ""]
184
+ for idx, item in enumerate(result, 1):
185
+ title = item.get('title', 'No title')
186
+ description = item.get('description', 'No description')
187
+ url = item.get('url', 'No URL')
188
+
189
+ # Clean up HTML tags if present
190
+ description = re.sub(r'<[^>]+>', '', description)
191
+
192
+ output.extend([
193
+ f"{Colors.BOLD}{idx}. {Colors.YELLOW}{title}{Colors.RESET}",
194
+ f" {Colors.OFF_WHITE}{description}{Colors.RESET}",
195
+ f" {Colors.BLUE}🔗 {url}{Colors.RESET}",
196
+ ""
197
+ ])
198
+ return "\n".join(output)
199
+
200
+ # Handle dictionaries
201
+ elif isinstance(result, dict):
202
+ output = []
203
+ for key, value in result.items():
204
+ if isinstance(value, (list, dict)):
205
+ output.append(f"{Colors.BOLD}{key}:{Colors.RESET}")
206
+ output.append(format_result(value))
207
+ else:
208
+ output.append(f"{Colors.BOLD}{key}:{Colors.RESET} {value}")
209
+ return "\n".join(output)
210
+
211
+ # Handle generic lists
212
+ elif isinstance(result, list):
213
+ return "\n".join([f"{Colors.CYAN}•{Colors.RESET} {format_result(item)}" for item in result])
214
+
215
+ # Default for other types
216
+ else:
217
+ return str(result)
218
+
219
+
220
+ def main() -> None:
221
+ """
222
+ Main entry point for the tinyAgent CLI.
223
+
224
+ This function handles command-line arguments, initializes the agent,
225
+ and runs tools or enters interactive mode as appropriate.
226
+ """
227
+ args = parse_arguments()
228
+
229
+ # Configure logging based on verbosity
230
+ log_level = "DEBUG" if args.verbose else "INFO"
231
+ if args.quiet:
232
+ log_level = "ERROR"
233
+
234
+ # Load configuration
235
+ config = load_config(args.config)
236
+
237
+ # Configure logging
238
+ configure_logging(log_level, config)
239
+
240
+ # Load tools
241
+ tools = load_tools()
242
+
243
+ # Create a dictionary of tools by name
244
+ tools_dict = {tool.name: tool for tool in tools if hasattr(tool, 'name')}
245
+
246
+ # Handle --list-tools
247
+ if args.list_tools:
248
+ print("Available tools:")
249
+ for tool in tools:
250
+ if hasattr(tool, 'name') and hasattr(tool, 'description'):
251
+ print(f" {tool.name}: {tool.description}")
252
+ return
253
+
254
+ # Parse variables for template if provided
255
+ template_vars = None
256
+ if args.vars:
257
+ try:
258
+ import json
259
+ template_vars = json.loads(args.vars)
260
+ logger.debug(f"Template variables: {template_vars}")
261
+ except json.JSONDecodeError as e:
262
+ logger.error(f"Error parsing template variables: {e}")
263
+ print(f"{Colors.error('Invalid JSON format for template variables')}")
264
+
265
+ # Handle case where we have a prompt and template but no tool (direct agent execution)
266
+ if not args.tool and args.prompt and args.template:
267
+ logger.info(f"Executing agent directly with template: {args.template}")
268
+
269
+ # Get the singleton factory instance
270
+ from ..factory.agent_factory import AgentFactory
271
+ factory = AgentFactory.get_instance()
272
+
273
+ # Load and register tools
274
+ for tool in tools:
275
+ if hasattr(tool, 'name'):
276
+ factory.register_tool(tool)
277
+
278
+ # Create an agent without factory
279
+ agent = Agent(model=args.model)
280
+
281
+ # Register all tools directly with the agent
282
+ for tool in tools:
283
+ if hasattr(tool, 'name') and tool.name != "chat": # Skip chat tool since agent already adds it
284
+ agent.create_tool(
285
+ name=tool.name,
286
+ description=tool.description,
287
+ func=tool.func,
288
+ parameters=tool.parameters
289
+ )
290
+
291
+ # Process prompt
292
+ user_query = ' '.join(args.prompt)
293
+
294
+ try:
295
+ with Spinner(f"Processing with template {args.template}..."):
296
+ result = agent.run(user_query, template_path=args.template, variables=template_vars)
297
+
298
+ print(f"\n{Colors.OFF_WHITE}Query processed with template{Colors.RESET}")
299
+ print(f"{Colors.LIGHT_RED}╭─{Colors.RESET}")
300
+ print(f"{Colors.OFF_WHITE}{result}{Colors.RESET}")
301
+ print(f"{Colors.LIGHT_RED}╰─{Colors.RESET}")
302
+
303
+ except Exception as e:
304
+ logger.error(f"Error processing with template: {str(e)}")
305
+ print(f"{Colors.error(f'Error processing with template: {str(e)}')}")
306
+
307
+ return
308
+
309
+ # Print banner and tools box if no specific command
310
+ if not args.tool and not args.prompt:
311
+ print_banner()
312
+
313
+ # Add triage_agent for display purposes
314
+ if 'triage_agent' not in tools_dict:
315
+ tools_dict['triage_agent'] = Tool(
316
+ name="triage_agent",
317
+ description="Triages incoming queries, calls internal agents & agentfactory",
318
+ parameters={"message": "string", "allow_new_tools": "any"},
319
+ func=lambda message, allow_new_tools=False: message
320
+ )
321
+
322
+ print_tools_box(tools_dict)
323
+
324
+ print(f"""
325
+ {Colors.OFF_WHITE}+-----------+ --> +---------------+ --> +--------------+ --> +------------------+ --> +-----------+
326
+ | Triage | | AgentFactory | | Specialized | | Infinite | | Structured|
327
+ | Agent | | (Creates | | Agents | | Handoff | | Output |
328
+ | | | Agents on the | | (dynamically | | | | |
329
+ +-----------+ | fly via NLP) | | created) | +------------------+ +-----------+
330
+ +---------------+ +--------------+ |
331
+ |_______________________________________________________|{Colors.RESET}
332
+ """)
333
+ print(f"\n{Colors.OFF_WHITE}Enter in a task and tinyAgent will execute based on tools{Colors.RESET}")
334
+ print(f"\n{Colors.OFF_WHITE}Tool chaining supported with '|' (e.g. 'file_hunter | pm'){Colors.RESET}")
335
+ print(f"{Colors.OFF_WHITE}Special commands:{Colors.RESET}")
336
+ print(f"{Colors.OFF_WHITE} /chat - Enter direct chat mode with the LLM{Colors.RESET}")
337
+ print(f"{Colors.OFF_WHITE}Type 'exit' or 'quit' to exit{Colors.RESET}")
338
+
339
+ # Interactive mode
340
+ run_interactive_mode(args, tools_dict)
341
+ else:
342
+ # Direct tool execution
343
+ logger.info("Executing tool directly")
344
+
345
+ # Get the tool to execute
346
+ tool_name = args.tool
347
+
348
+ # Check if it's a chain of tools
349
+ if '|' in tool_name:
350
+ logger.info(f"Tool chain detected: {tool_name}")
351
+ # Handle tool chaining (execute tools in sequence)
352
+ tool_names = [t.strip() for t in tool_name.split('|')]
353
+
354
+ # Create a chain of tools
355
+ tools_to_execute = []
356
+ for name in tool_names:
357
+ if name in tools_dict:
358
+ tools_to_execute.append(tools_dict[name])
359
+ else:
360
+ logger.error(f"Tool not found: {name}")
361
+ print(f"Tool not found: {name}")
362
+ return
363
+
364
+ # Execute the tool chain
365
+ result = None
366
+ for tool in tools_to_execute:
367
+ try:
368
+ # Parse arguments for this tool
369
+ # For simplicity, we're using the same args for all tools in the chain
370
+ # In a real implementation, you'd want to parse arguments per tool
371
+ tool_args = {}
372
+ if args.args:
373
+ tool_args = {list(tool.parameters.keys())[0]: args.args}
374
+
375
+ # If we have a result from previous tool, use it as input
376
+ if result is not None:
377
+ # Use the first parameter
378
+ first_param = list(tool.parameters.keys())[0]
379
+ tool_args[first_param] = result
380
+
381
+ # Add prompt if provided
382
+ if args.prompt and not tool_args:
383
+ # Use the prompt as the first parameter
384
+ first_param = list(tool.parameters.keys())[0]
385
+ tool_args[first_param] = ' '.join(args.prompt)
386
+
387
+ logger.info(f"Executing tool: {tool.name} with args: {tool_args}")
388
+ result = tool(**tool_args)
389
+
390
+ except Exception as e:
391
+ logger.error(f"Error executing tool {tool.name}: {str(e)}")
392
+ print(f"Error executing tool {tool.name}: {str(e)}")
393
+ return
394
+
395
+ # Print the final result
396
+ print(result)
397
+
398
+ else:
399
+ # Single tool execution
400
+ if tool_name not in tools_dict:
401
+ logger.error(f"Tool not found: {tool_name}")
402
+ print(f"Tool not found: {tool_name}")
403
+ return
404
+
405
+ tool = tools_dict[tool_name]
406
+
407
+ try:
408
+ # Parse arguments
409
+ tool_args = {}
410
+
411
+ # Get the first parameter for text input
412
+ first_param = list(tool.parameters.keys())[0]
413
+ if args.args:
414
+ tool_args[first_param] = args.args
415
+ elif args.prompt:
416
+ tool_args[first_param] = ' '.join(args.prompt)
417
+
418
+ # Add default values for other parameters from manifest
419
+ if hasattr(tool, 'manifest'):
420
+ for param_name, param_info in tool.manifest.get('parameters', {}).items():
421
+ if isinstance(param_info, dict) and not param_info.get('required', True):
422
+ if param_name not in tool_args and 'default' in param_info:
423
+ tool_args[param_name] = param_info['default']
424
+
425
+ logger.info(f"Executing tool: {tool.name} with args: {tool_args}")
426
+ result = tool(**tool_args)
427
+ print(result)
428
+
429
+ except Exception as e:
430
+ logger.error(f"Error executing tool {tool.name}: {str(e)}")
431
+ print(f"Error executing tool {tool.name}: {str(e)}")
432
+
433
+
434
+ def run_interactive_mode(args: argparse.Namespace, tools_dict: Dict[str, Tool]) -> None:
435
+ """
436
+ Run the interactive mode of the CLI.
437
+
438
+ Args:
439
+ args: Parsed command-line arguments
440
+ tools_dict: Dictionary of tool names to Tool objects
441
+ """
442
+ # Ensure environment variables are loaded
443
+ from dotenv import load_dotenv
444
+ load_dotenv() # Load .env file from the current directory
445
+
446
+ model = args.model
447
+
448
+ # Get the singleton factory instance
449
+ from ..factory.agent_factory import AgentFactory
450
+ factory = AgentFactory.get_instance()
451
+
452
+ # Register all tools with the factory
453
+ for tool_name, tool in tools_dict.items():
454
+ # Check if tool is already registered (since has_tool method isn't available)
455
+ if tool_name not in factory._tools:
456
+ factory.register_tool(tool)
457
+
458
+ # Create an agent without the factory
459
+ agent = Agent(model=model)
460
+
461
+ # Register all tools directly with the agent
462
+ for tool_name, tool in tools_dict.items():
463
+ if tool_name not in ["chat"]: # Skip chat tool since agent already adds it
464
+ agent.create_tool(
465
+ name=tool.name,
466
+ description=tool.description,
467
+ func=tool.func,
468
+ parameters=tool.parameters
469
+ )
470
+
471
+ # Parse variables for template if provided
472
+ template_vars = None
473
+ if args.vars:
474
+ try:
475
+ import json
476
+ template_vars = json.loads(args.vars)
477
+ logger.debug(f"Template variables: {template_vars}")
478
+ except json.JSONDecodeError as e:
479
+ logger.error(f"Error parsing template variables: {e}")
480
+ print(f"{Colors.error('Invalid JSON format for template variables')}")
481
+
482
+ while True:
483
+ try:
484
+ user_input = input(f"\n{Colors.LIGHT_RED}❯{Colors.OFF_WHITE} ")
485
+ if user_input.lower() in ["exit", "quit"]:
486
+ print(f"\n{Colors.LIGHT_RED}Goodbye!{Colors.RESET}")
487
+ break
488
+ elif user_input.lower() == "/chat":
489
+ # Enter chat mode
490
+ from ..chat import run_chat_mode
491
+ print(f"\n{Colors.OFF_WHITE}Entering chat mode with {model or 'default model'}{Colors.RESET}")
492
+ run_chat_mode(model=model)
493
+ continue
494
+
495
+ # Check if this is a tool chain
496
+ if "|" in user_input:
497
+ tools_chain = [t.strip() for t in user_input.split("|")]
498
+ result = None
499
+ try:
500
+ for tool_cmd in tools_chain:
501
+ # Parse tool command
502
+ parts = tool_cmd.split(maxsplit=2) # Split into max 3 parts for multi-param tools
503
+ tool_name = parts[0]
504
+
505
+ if tool_name not in tools_dict:
506
+ raise ValueError(f"Tool not found: {tool_name}")
507
+
508
+ tool = tools_dict[tool_name]
509
+
510
+ # Handle tool-specific parameter requirements
511
+ tool_args = {}
512
+ if tool_name == "aider":
513
+ # Special handling for aider in chain
514
+ if len(parts) < 2:
515
+ raise ValueError("Missing required parameter: files")
516
+ if len(parts) < 3 and result is None:
517
+ raise ValueError("Missing required parameter: prompt")
518
+ tool_args["files"] = parts[1]
519
+ tool_args["prompt"] = parts[2] if len(parts) > 2 else result
520
+ else:
521
+ # Default handling for single-parameter tools
522
+ first_param = list(tool.parameters.keys())[0]
523
+ if len(parts) > 1:
524
+ tool_args[first_param] = parts[1]
525
+ elif result is not None:
526
+ tool_args[first_param] = result
527
+
528
+ logger.info(f"Executing tool in chain: {tool_name} with args: {tool_args}")
529
+ result = tool(**tool_args)
530
+
531
+ # Print final result
532
+ print(f"\n{Colors.OFF_WHITE}Chain execution completed{Colors.RESET}")
533
+ print(f"{Colors.LIGHT_RED}╭─{Colors.RESET}")
534
+ print(f"{Colors.OFF_WHITE}{result}{Colors.RESET}")
535
+ print(f"{Colors.LIGHT_RED}╰─{Colors.RESET}")
536
+
537
+ except Exception as e:
538
+ print(f"\n{Colors.DARK_RED}Chain execution failed: {str(e)}{Colors.RESET}")
539
+ continue
540
+
541
+ # Check if this is a direct tool call
542
+ first_word = user_input.split()[0]
543
+ if first_word in tools_dict:
544
+ try:
545
+ # Parse tool command
546
+ import shlex
547
+ parts = shlex.split(user_input)
548
+ tool_name = parts[0]
549
+ tool = tools_dict[tool_name]
550
+
551
+ # Handle tool-specific parameter requirements
552
+ tool_args = {}
553
+ if tool_name == "aider":
554
+ # Special handling for aider's two parameters
555
+ if len(parts) < 2:
556
+ raise ValueError("Missing required parameter: files")
557
+ if len(parts) < 3:
558
+ raise ValueError("Missing required parameter: prompt")
559
+ tool_args = {
560
+ "files": parts[1],
561
+ "prompt": parts[2]
562
+ }
563
+ elif tool_name == "brave_web_search":
564
+ # Ensure MCP server is running
565
+ from tinyagent.mcp import ensure_mcp_server
566
+ ensure_mcp_server()
567
+
568
+ # Special handling for brave_web_search parameters
569
+ if len(parts) < 2:
570
+ raise ValueError("Missing required parameter: query")
571
+ tool_args = {"query": parts[1]}
572
+ if len(parts) >= 3:
573
+ try:
574
+ tool_args["count"] = int(parts[2])
575
+ except ValueError:
576
+ raise ValueError("Count parameter must be a number")
577
+ else:
578
+ # Default handling for single-parameter tools
579
+ first_param = list(tool.parameters.keys())[0]
580
+ if len(parts) > 1:
581
+ tool_args[first_param] = parts[1]
582
+
583
+ logger.info(f"Executing tool directly: {tool_name} with args: {tool_args}")
584
+ result = tool(**tool_args)
585
+
586
+ # Print result
587
+ print(f"\n{Colors.OFF_WHITE}Tool execution completed{Colors.RESET}")
588
+ print(f"{Colors.LIGHT_RED}╭─{Colors.RESET}")
589
+ print(f"{Colors.OFF_WHITE}{result}{Colors.RESET}")
590
+ print(f"{Colors.LIGHT_RED}╰─{Colors.RESET}")
591
+
592
+ except Exception as e:
593
+ print(f"\n{Colors.DARK_RED}Tool execution failed: {str(e)}{Colors.RESET}")
594
+ continue
595
+
596
+ # Process the user input with agent using template if provided
597
+ if args.template:
598
+ with Spinner(f"Processing with template {args.template}..."):
599
+ try:
600
+ result = agent.run(user_input, template_path=args.template, variables=template_vars)
601
+ print(f"\n{Colors.OFF_WHITE}Query processed with template{Colors.RESET}")
602
+ print(f"{Colors.LIGHT_RED}╭─{Colors.RESET}")
603
+ print(f"{Colors.OFF_WHITE}{result}{Colors.RESET}")
604
+ print(f"{Colors.LIGHT_RED}╰─{Colors.RESET}")
605
+ except Exception as e:
606
+ print(f"\n{Colors.DARK_RED}Template processing failed: {str(e)}{Colors.RESET}")
607
+ continue
608
+
609
+ # Fallback to triage agent for other inputs
610
+ from ..factory.orchestrator import Orchestrator
611
+
612
+ with Spinner("Processing with Triage Agent..."):
613
+ orchestrator = Orchestrator.get_instance()
614
+ task_id = orchestrator.submit_task(user_input, need_permission=False)
615
+ status = orchestrator.get_task_status(task_id)
616
+
617
+ if status.status == "completed":
618
+ print(f"\n{Colors.OFF_WHITE}Task completed by Triage Agent{Colors.RESET}")
619
+ print(f"{Colors.OFF_WHITE}Result:{Colors.RESET}")
620
+ print(f"{Colors.LIGHT_RED}╭─{Colors.RESET}")
621
+ try:
622
+ # Try to parse json if possible
623
+ import json
624
+ if isinstance(status.result, str):
625
+ try:
626
+ parsed_result = json.loads(status.result)
627
+ formatted = format_result(parsed_result)
628
+ print(f"{formatted}")
629
+ except json.JSONDecodeError:
630
+ # Not JSON, print as is
631
+ print(f"{Colors.OFF_WHITE}{status.result}{Colors.RESET}")
632
+ else:
633
+ # Already a Python object
634
+ formatted = format_result(status.result)
635
+ print(f"{formatted}")
636
+ except Exception:
637
+ # Fallback to plain output
638
+ print(f"{Colors.OFF_WHITE}{status.result}{Colors.RESET}")
639
+ print(f"{Colors.LIGHT_RED}╰─{Colors.RESET}")
640
+ else:
641
+ print(f"\n{Colors.DARK_RED}Task failed: {status.error}{Colors.RESET}")
642
+
643
+ except KeyboardInterrupt:
644
+ print("\nExiting...")
645
+ break
646
+ except Exception as e:
647
+ logger.error(f"Error: {str(e)}")
648
+ print(f"{Colors.error(str(e))}")
649
+
650
+
651
+ if __name__ == "__main__":
652
+ main()
653
+
654
+ """ Summary of the Issue:
655
+ The core problem was that you couldn't reliably run the duckduckgo_search tool directly from the interactive CLI using commands like ❯ duckduckgo_search "query" --max_results 5. This manifested in two main ways:
656
+ Fallback to Triage Agent: Initially, the command wasn't recognized as a direct tool call at all. It was passed to the Triage Agent, which also failed because it either didn't know about duckduckgo_search or misinterpreted the command. This was likely due to the tool not being loaded correctly in the interactive context (potentially related to the MCP check).
657
+ Missing Parameter Error: After fixing the tool loading, the CLI did recognize duckduckgo_search for direct execution. However, it then failed with a Missing required parameter: max_results error. This happened because the argument parsing logic in the CLI for direct calls was too basic – it only grabbed the first argument (keywords) and ignored named arguments like --max_results. A validation step then failed because max_results was expected (based on the tool's definition) but wasn't provided by the faulty parser.
658
+ What We Learned:
659
+ CLI Parsing is Crucial: The way the interactive CLI (main.py) parses commands is critical for determining whether a tool is run directly or handed off to an agent. Simple parsing can easily break with prefixes (❯) or standard argument formats (--option value).
660
+ Conditional Tool Loading Matters: Tools might only be loaded if certain conditions are met (like the MCP server running). If a tool isn't loaded, it can't be called directly by name.
661
+ Framework Validation Exists: There's a validation step before a tool's specific function code runs. This validation checks the arguments provided by the caller against the parameters defined in the tool's Tool object.
662
+ Defaults Don't Always Save You: Even if a tool's function code defines default values for parameters (like duckduckgo_search does for max_results), an error can occur before that code runs if the framework's validation layer strictly requires the parameter based on its definition.
663
+ Hardcoded Logic is Brittle: The interactive CLI had specific, hardcoded argument handling for aider and brave_web_search, but a very basic default for everything else. This made it inflexible for tools with multiple or named arguments.
664
+ Debugging Requires Tracing: We had to follow the command from input, through parsing in main.py, tool loading checks, the direct execution attempt, the argument validation step, and the Triage Agent fallback path to understand the different failure points. """