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.
- tiny_agent_os-0.0.1.dist-info/METADATA +377 -0
- tiny_agent_os-0.0.1.dist-info/RECORD +64 -0
- tiny_agent_os-0.0.1.dist-info/WHEEL +5 -0
- tiny_agent_os-0.0.1.dist-info/entry_points.txt +2 -0
- tiny_agent_os-0.0.1.dist-info/licenses/LICENSE +53 -0
- tiny_agent_os-0.0.1.dist-info/top_level.txt +1 -0
- tinyagent/__init__.py +75 -0
- tinyagent/_version.py +21 -0
- tinyagent/agent.py +957 -0
- tinyagent/chat/__init__.py +12 -0
- tinyagent/chat/chat_mode.py +291 -0
- tinyagent/cli/__init__.py +16 -0
- tinyagent/cli/colors.py +104 -0
- tinyagent/cli/main.py +664 -0
- tinyagent/cli/spinner.py +94 -0
- tinyagent/cli.py +47 -0
- tinyagent/config/__init__.py +14 -0
- tinyagent/config/config.py +258 -0
- tinyagent/decorators.py +187 -0
- tinyagent/exceptions.py +85 -0
- tinyagent/factory/__init__.py +18 -0
- tinyagent/factory/agent_factory.py +439 -0
- tinyagent/factory/dynamic_agent_factory.py +561 -0
- tinyagent/factory/orchestrator.py +1514 -0
- tinyagent/factory/tiny_chain.py +552 -0
- tinyagent/logging.py +97 -0
- tinyagent/mcp/__init__.py +14 -0
- tinyagent/mcp/manager.py +321 -0
- tinyagent/prompts/README.md +133 -0
- tinyagent/prompts/default.md +14 -0
- tinyagent/prompts/prompt_manager.py +206 -0
- tinyagent/prompts/system/agent.md +50 -0
- tinyagent/prompts/system/retry.md +55 -0
- tinyagent/prompts/system/strict_json.md +54 -0
- tinyagent/prompts/system.md +10 -0
- tinyagent/prompts/tools/calculator.md +13 -0
- tinyagent/prompts/tools/weather.md +7 -0
- tinyagent/prompts/workflows/riv_reflect.md +62 -0
- tinyagent/prompts/workflows/riv_verify.md +47 -0
- tinyagent/prompts/workflows/triage.md +129 -0
- tinyagent/tool.py +185 -0
- tinyagent/tools/README.md +391 -0
- tinyagent/tools/__init__.py +39 -0
- tinyagent/tools/aider.py +122 -0
- tinyagent/tools/anon_coder.py +296 -0
- tinyagent/tools/boilerplate_tool.py +147 -0
- tinyagent/tools/brave_search.py +104 -0
- tinyagent/tools/codeagent_tool.py +217 -0
- tinyagent/tools/content_processor.py +285 -0
- tinyagent/tools/custom_text_browser.py +965 -0
- tinyagent/tools/duckduckgo_search.py +153 -0
- tinyagent/tools/external.py +303 -0
- tinyagent/tools/file_manipulator.py +274 -0
- tinyagent/tools/final_extractor_tool.py +249 -0
- tinyagent/tools/llm_serializer.py +124 -0
- tinyagent/tools/markdown_gen.py +300 -0
- tinyagent/tools/ripgrep.py +136 -0
- tinyagent/utils/__init__.py +13 -0
- tinyagent/utils/json_parser.py +231 -0
- tinyagent/utils/logging_utils.py +78 -0
- tinyagent/utils/openrouter_request.py +123 -0
- tinyagent/utils/serialization.py +185 -0
- tinyagent/utils/structured_outputs.py +131 -0
- 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. """
|