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/spinner.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spinner animation for the tinyAgent CLI.
|
|
3
|
+
|
|
4
|
+
This module provides a spinner animation for the CLI to indicate ongoing processes.
|
|
5
|
+
The spinner is implemented as a context manager for ease of use.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import itertools
|
|
12
|
+
from typing import Optional, Iterator
|
|
13
|
+
|
|
14
|
+
from .colors import Colors
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Spinner:
|
|
18
|
+
"""
|
|
19
|
+
A simple spinner animation for the CLI.
|
|
20
|
+
|
|
21
|
+
This class provides a spinner animation that runs in a separate thread
|
|
22
|
+
and can be used to indicate that a process is running. It is implemented
|
|
23
|
+
as a context manager for ease of use with the 'with' statement.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
message: The message to display next to the spinner
|
|
27
|
+
running: Flag indicating if the spinner is running
|
|
28
|
+
spinner: Iterator of spinner characters
|
|
29
|
+
thread: Thread running the spinner animation
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str = "Processing"):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the spinner with a message.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
message: The message to display next to the spinner
|
|
38
|
+
"""
|
|
39
|
+
self.message = message
|
|
40
|
+
self.running = False
|
|
41
|
+
self.spinner: Iterator[str] = itertools.cycle(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'])
|
|
42
|
+
self.thread: Optional[threading.Thread] = None
|
|
43
|
+
|
|
44
|
+
def spin(self) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Run the spinner animation.
|
|
47
|
+
|
|
48
|
+
This method runs in a separate thread and updates the spinner character
|
|
49
|
+
at regular intervals. It continues until the running flag is set to False.
|
|
50
|
+
"""
|
|
51
|
+
while self.running:
|
|
52
|
+
sys.stdout.write(
|
|
53
|
+
f"\r{Colors.LIGHT_RED}{next(self.spinner)} "
|
|
54
|
+
f"{Colors.OFF_WHITE}{self.message}{Colors.RESET}"
|
|
55
|
+
)
|
|
56
|
+
sys.stdout.flush()
|
|
57
|
+
time.sleep(0.1)
|
|
58
|
+
|
|
59
|
+
# Clear the spinner when done
|
|
60
|
+
sys.stdout.write('\r' + ' ' * (len(self.message) + 2) + '\r')
|
|
61
|
+
sys.stdout.flush()
|
|
62
|
+
|
|
63
|
+
def __enter__(self) -> 'Spinner':
|
|
64
|
+
"""
|
|
65
|
+
Start the spinner when entering a context.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The Spinner instance
|
|
69
|
+
"""
|
|
70
|
+
self.running = True
|
|
71
|
+
self.thread = threading.Thread(target=self.spin)
|
|
72
|
+
self.thread.start()
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
76
|
+
"""Stop the spinner when exiting a context."""
|
|
77
|
+
self.running = False
|
|
78
|
+
if self.thread:
|
|
79
|
+
self.thread.join()
|
|
80
|
+
|
|
81
|
+
def update_message(self, message: str) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Update the spinner message while it's running.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
message: New message to display
|
|
87
|
+
"""
|
|
88
|
+
self.message = message
|
|
89
|
+
|
|
90
|
+
def stop(self) -> None:
|
|
91
|
+
"""Stop the spinner manually (if not using as a context manager)."""
|
|
92
|
+
self.running = False
|
|
93
|
+
if self.thread:
|
|
94
|
+
self.thread.join()
|
tinyagent/cli.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
tinyAgent - A simple yet powerful framework for building LLM-powered agents.
|
|
4
|
+
|
|
5
|
+
This is the main entry point for the tinyAgent framework. It provides access to
|
|
6
|
+
all the core components of the framework through a clean, simple API.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Load environment variables first
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
from tinyagent import (
|
|
14
|
+
# Core components
|
|
15
|
+
Agent, get_llm, Tool, ParamType, tool,
|
|
16
|
+
|
|
17
|
+
# Exception classes
|
|
18
|
+
TinyAgentError, ConfigurationError,
|
|
19
|
+
ToolError, ToolNotFoundError, ToolExecutionError,
|
|
20
|
+
RateLimitExceeded, ParsingError,
|
|
21
|
+
AgentRetryExceeded, OrchestratorError, AgentNotFoundError,
|
|
22
|
+
|
|
23
|
+
# Factory components
|
|
24
|
+
AgentFactory, DynamicAgentFactory, Orchestrator, TaskStatus,
|
|
25
|
+
|
|
26
|
+
# Utilities
|
|
27
|
+
configure_logging, get_logger, load_config, get_config_value,
|
|
28
|
+
Colors, Spinner, CLI, run_chat_mode,
|
|
29
|
+
|
|
30
|
+
# Version
|
|
31
|
+
__version__
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Built-in tools
|
|
35
|
+
from tinyagent.tools import (
|
|
36
|
+
anon_coder_tool,
|
|
37
|
+
llm_serializer_tool,
|
|
38
|
+
brave_web_search_tool,
|
|
39
|
+
ripgrep_tool,
|
|
40
|
+
aider_tool,
|
|
41
|
+
load_external_tools
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Run CLI if executed directly
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
# Call the core CLI implementation
|
|
47
|
+
CLI()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for the tinyAgent framework.
|
|
3
|
+
|
|
4
|
+
This package provides utilities for loading, validating, and accessing
|
|
5
|
+
configuration settings from different sources (files, environment variables, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .config import load_config, get_config_value, TinyAgentConfig
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'load_config',
|
|
12
|
+
'get_config_value',
|
|
13
|
+
'TinyAgentConfig',
|
|
14
|
+
]
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for the tinyAgent framework.
|
|
3
|
+
|
|
4
|
+
This module provides functions for loading, validating, and accessing configuration
|
|
5
|
+
settings from different sources (files, environment variables, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import yaml
|
|
10
|
+
from typing import Dict, Any, Optional, TypedDict, Union, cast, List
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
from ..exceptions import ConfigurationError
|
|
14
|
+
from ..logging import get_logger
|
|
15
|
+
|
|
16
|
+
# Set up logger
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ParsingConfig(TypedDict, total=False):
|
|
21
|
+
"""Configuration for response parsing."""
|
|
22
|
+
strict_json: bool
|
|
23
|
+
fallback_parsers: Dict[str, bool]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ModelConfig(TypedDict, total=False):
|
|
27
|
+
"""Configuration for models."""
|
|
28
|
+
default: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RetriesConfig(TypedDict, total=False):
|
|
32
|
+
"""Configuration for retry behavior."""
|
|
33
|
+
max_attempts: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RateLimitConfig(TypedDict, total=False):
|
|
37
|
+
"""Configuration for rate limiting."""
|
|
38
|
+
global_limit: int
|
|
39
|
+
tool_limits: Dict[str, int]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LoggingConfig(TypedDict, total=False):
|
|
43
|
+
"""Configuration for logging."""
|
|
44
|
+
level: str # DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
45
|
+
format: str # Python logging format string
|
|
46
|
+
handlers: List[str] # List of enabled handlers (console, file, etc)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AgentConfig(TypedDict, total=False):
|
|
50
|
+
"""Configuration for agent behavior."""
|
|
51
|
+
max_steps: int # Maximum number of steps per research phase
|
|
52
|
+
debug_level: int # Debug level (0-2)
|
|
53
|
+
default_model: str # Default model to use
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class APIConfig(TypedDict, total=False):
|
|
57
|
+
"""Configuration for API behavior."""
|
|
58
|
+
enable_docs: bool
|
|
59
|
+
cors_origins: List[str]
|
|
60
|
+
port: int
|
|
61
|
+
|
|
62
|
+
class TinyAgentConfig(TypedDict, total=False):
|
|
63
|
+
"""Top-level configuration structure."""
|
|
64
|
+
parsing: ParsingConfig
|
|
65
|
+
model: ModelConfig
|
|
66
|
+
retries: RetriesConfig
|
|
67
|
+
rate_limits: RateLimitConfig
|
|
68
|
+
logging: LoggingConfig
|
|
69
|
+
agent: AgentConfig
|
|
70
|
+
api: APIConfig
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Default configuration
|
|
74
|
+
DEFAULT_CONFIG: TinyAgentConfig = {
|
|
75
|
+
"api": {
|
|
76
|
+
"app_name": "tinyAgent API",
|
|
77
|
+
"enable_docs": True,
|
|
78
|
+
"cors_origins": ["*"],
|
|
79
|
+
"port": 9000,
|
|
80
|
+
"chat_tool": "default_chat"
|
|
81
|
+
},
|
|
82
|
+
"parsing": {
|
|
83
|
+
"strict_json": False,
|
|
84
|
+
"fallback_parsers": {
|
|
85
|
+
"template": True,
|
|
86
|
+
"regex": True
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"model": {
|
|
90
|
+
"default": "qwen/qwq-32B"
|
|
91
|
+
},
|
|
92
|
+
"retries": {
|
|
93
|
+
"max_attempts": 3
|
|
94
|
+
},
|
|
95
|
+
"rate_limits": {
|
|
96
|
+
"global_limit": 30,
|
|
97
|
+
"tool_limits": {}
|
|
98
|
+
},
|
|
99
|
+
"logging": {
|
|
100
|
+
"level": "INFO", # Default to INFO to see configuration details
|
|
101
|
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
102
|
+
"handlers": ["console"] # Default to console output
|
|
103
|
+
},
|
|
104
|
+
"agent": {
|
|
105
|
+
"max_steps": 2, # Default to 2 steps per phase
|
|
106
|
+
"debug_level": 0, # Default to no debug output
|
|
107
|
+
"default_model": "deepseek/deepseek-r1" # Default model
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_config(config_path: Optional[str] = None) -> TinyAgentConfig:
|
|
113
|
+
"""
|
|
114
|
+
Load configuration from YAML file, with fallback to default values.
|
|
115
|
+
|
|
116
|
+
This function attempts to load configuration from a YAML file. If the file
|
|
117
|
+
doesn't exist or there's an error loading it, it falls back to default values.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
config_path: Path to config.yml file. If None, uses config.yml from project root.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dict containing configuration values
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ConfigurationError: If there's an error with the configuration format
|
|
127
|
+
"""
|
|
128
|
+
# 1. Check environment variable override
|
|
129
|
+
env_config = os.getenv("TINYAGENT_CONFIG")
|
|
130
|
+
if env_config and os.path.isfile(env_config):
|
|
131
|
+
config_path = env_config
|
|
132
|
+
else:
|
|
133
|
+
# 2. If no explicit path, check current working directory
|
|
134
|
+
if config_path is None:
|
|
135
|
+
cwd_config = os.path.join(os.getcwd(), 'config.yml')
|
|
136
|
+
if os.path.isfile(cwd_config):
|
|
137
|
+
config_path = cwd_config
|
|
138
|
+
else:
|
|
139
|
+
# 3. Fallback to package root
|
|
140
|
+
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
141
|
+
config_path = os.path.join(project_root, 'config.yml')
|
|
142
|
+
elif not os.path.isabs(config_path):
|
|
143
|
+
# If relative path provided, make it relative to project root
|
|
144
|
+
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
145
|
+
config_path = os.path.join(project_root, config_path)
|
|
146
|
+
|
|
147
|
+
config = cast(TinyAgentConfig, DEFAULT_CONFIG.copy())
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
if os.path.exists(config_path):
|
|
151
|
+
with open(config_path, 'r', encoding='utf-8') as file:
|
|
152
|
+
yaml_config = yaml.safe_load(file)
|
|
153
|
+
|
|
154
|
+
if yaml_config:
|
|
155
|
+
# Validate the config
|
|
156
|
+
if not isinstance(yaml_config, dict):
|
|
157
|
+
raise ConfigurationError("Configuration must be a dictionary")
|
|
158
|
+
|
|
159
|
+
# Update config with values from YAML
|
|
160
|
+
_update_nested_dict(config, yaml_config)
|
|
161
|
+
logger.info(f"Configuration loaded from {config_path}")
|
|
162
|
+
else:
|
|
163
|
+
logger.warning(f"Empty configuration file: {config_path}")
|
|
164
|
+
else:
|
|
165
|
+
logger.info(f"Configuration file not found at {config_path}, using defaults")
|
|
166
|
+
except yaml.YAMLError as e:
|
|
167
|
+
logger.error(f"Error parsing YAML configuration: {str(e)}")
|
|
168
|
+
raise ConfigurationError(f"Invalid YAML in configuration file: {e}")
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Error loading configuration: {str(e)}")
|
|
171
|
+
logger.info("Using default configuration")
|
|
172
|
+
|
|
173
|
+
return config
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _update_nested_dict(base_dict: Dict[str, Any], update_dict: Dict[str, Any]) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Update a nested dictionary with values from another dictionary.
|
|
179
|
+
|
|
180
|
+
This function recursively updates a nested dictionary with values from another
|
|
181
|
+
dictionary, preserving the structure of the base dictionary.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
base_dict: The dictionary to update
|
|
185
|
+
update_dict: The dictionary with new values
|
|
186
|
+
"""
|
|
187
|
+
for key, value in update_dict.items():
|
|
188
|
+
if key in base_dict and isinstance(base_dict[key], dict) and isinstance(value, dict):
|
|
189
|
+
_update_nested_dict(base_dict[key], value)
|
|
190
|
+
else:
|
|
191
|
+
base_dict[key] = value
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_config_value(config: Dict[str, Any], key_path: str, default: Any = None) -> Any:
|
|
195
|
+
"""
|
|
196
|
+
Get a value from the config using a dot-notation path.
|
|
197
|
+
|
|
198
|
+
This function retrieves a value from a nested dictionary using a dot-notation
|
|
199
|
+
path. If the key doesn't exist, it returns the default value.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
config: The configuration dictionary
|
|
203
|
+
key_path: Dot-notation path to the desired value (e.g., 'parsing.strict_json')
|
|
204
|
+
default: Value to return if the key is not found
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
The configuration value or default if not found
|
|
208
|
+
|
|
209
|
+
Examples:
|
|
210
|
+
>>> config = {"parsing": {"strict_json": True}}
|
|
211
|
+
>>> get_config_value(config, "parsing.strict_json")
|
|
212
|
+
True
|
|
213
|
+
>>> get_config_value(config, "parsing.unknown", False)
|
|
214
|
+
False
|
|
215
|
+
"""
|
|
216
|
+
keys = key_path.split('.')
|
|
217
|
+
current = config
|
|
218
|
+
|
|
219
|
+
for key in keys:
|
|
220
|
+
if isinstance(current, dict) and key in current:
|
|
221
|
+
current = current[key]
|
|
222
|
+
else:
|
|
223
|
+
return default
|
|
224
|
+
|
|
225
|
+
return current
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def validate_config(config: TinyAgentConfig) -> None:
|
|
229
|
+
"""
|
|
230
|
+
Validate the configuration to ensure it matches the expected format.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
config: The configuration to validate
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
ConfigurationError: If the configuration is invalid
|
|
237
|
+
"""
|
|
238
|
+
# Validate model.default
|
|
239
|
+
if "model" in config and "default" in config["model"]:
|
|
240
|
+
if not isinstance(config["model"]["default"], str):
|
|
241
|
+
raise ConfigurationError("model.default must be a string")
|
|
242
|
+
|
|
243
|
+
# Validate retries.max_attempts
|
|
244
|
+
if "retries" in config and "max_attempts" in config["retries"]:
|
|
245
|
+
if not isinstance(config["retries"]["max_attempts"], int):
|
|
246
|
+
raise ConfigurationError("retries.max_attempts must be an integer")
|
|
247
|
+
if config["retries"]["max_attempts"] < 1:
|
|
248
|
+
raise ConfigurationError("retries.max_attempts must be at least 1")
|
|
249
|
+
|
|
250
|
+
# Validate parsing.strict_json
|
|
251
|
+
if "parsing" in config and "strict_json" in config["parsing"]:
|
|
252
|
+
if not isinstance(config["parsing"]["strict_json"], bool):
|
|
253
|
+
raise ConfigurationError("parsing.strict_json must be a boolean")
|
|
254
|
+
|
|
255
|
+
# Validate rate_limits.global_limit
|
|
256
|
+
if "rate_limits" in config and "global_limit" in config["rate_limits"]:
|
|
257
|
+
if not isinstance(config["rate_limits"]["global_limit"], int):
|
|
258
|
+
raise ConfigurationError("rate_limits.global_limit must be an integer")
|
tinyagent/decorators.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorator functions for the tinyAgent framework.
|
|
3
|
+
|
|
4
|
+
This module provides decorators used throughout the tinyAgent framework,
|
|
5
|
+
particularly the `tool` decorator for transforming functions into Tool instances.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
import inspect
|
|
10
|
+
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, cast, overload
|
|
11
|
+
|
|
12
|
+
from .tool import Tool, ParamType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Type variables for better type annotations
|
|
16
|
+
F = TypeVar('F', bound=Callable[..., Any]) # Function type
|
|
17
|
+
T = TypeVar('T') # Generic return type
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@overload
|
|
21
|
+
def tool(func: F) -> F:
|
|
22
|
+
"""Tool decorator usage without arguments."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@overload
|
|
27
|
+
def tool(
|
|
28
|
+
name: Optional[str] = None,
|
|
29
|
+
description: Optional[str] = None,
|
|
30
|
+
rate_limit: Optional[int] = None,
|
|
31
|
+
retry_limit: Optional[int] = None
|
|
32
|
+
) -> Callable[[F], F]:
|
|
33
|
+
"""Tool decorator usage with arguments."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def tool(
|
|
38
|
+
name=None,
|
|
39
|
+
description=None,
|
|
40
|
+
rate_limit=None,
|
|
41
|
+
retry_limit=None
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Decorator to transform a function into a tool with optional rate limiting.
|
|
45
|
+
|
|
46
|
+
This decorator provides a more intuitive and developer-friendly way to define tools.
|
|
47
|
+
Simply decorating a function transforms it into a registered tool.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
name: Optional name for the tool (defaults to function name)
|
|
51
|
+
description: Optional description for the tool (defaults to function docstring)
|
|
52
|
+
rate_limit: Optional rate limit for the tool (max number of calls allowed)
|
|
53
|
+
retry_limit: Optional retry limit for the tool (max retries on failure)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The decorated function wrapped as a tool with rate limiting
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
@tool
|
|
60
|
+
def calculate_sum(a: int, b: int) -> int:
|
|
61
|
+
'''Calculate the sum of two integers.'''
|
|
62
|
+
return a + b
|
|
63
|
+
|
|
64
|
+
@tool(rate_limit=5)
|
|
65
|
+
def rate_limited_api(query: str) -> str:
|
|
66
|
+
'''Make an API call with max 5 calls per session.'''
|
|
67
|
+
return make_api_call(query)
|
|
68
|
+
"""
|
|
69
|
+
# Handle case where decorator is used without parentheses
|
|
70
|
+
if callable(name):
|
|
71
|
+
func = name
|
|
72
|
+
name = None
|
|
73
|
+
return _create_tool_wrapper(func, None, None, None, None)
|
|
74
|
+
|
|
75
|
+
# Handle case where decorator is used with parameters
|
|
76
|
+
def decorator(func):
|
|
77
|
+
return _create_tool_wrapper(func, name, description, rate_limit, retry_limit)
|
|
78
|
+
|
|
79
|
+
return decorator
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _create_tool_wrapper(
|
|
83
|
+
func: F,
|
|
84
|
+
name: Optional[str],
|
|
85
|
+
description: Optional[str],
|
|
86
|
+
rate_limit: Optional[int],
|
|
87
|
+
retry_limit: Optional[int]
|
|
88
|
+
) -> F:
|
|
89
|
+
"""
|
|
90
|
+
Internal function to create a tool wrapper for a given function.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
func: The function to wrap as a tool
|
|
94
|
+
name: Optional name for the tool
|
|
95
|
+
description: Optional description for the tool
|
|
96
|
+
rate_limit: Optional rate limit for the tool
|
|
97
|
+
retry_limit: Optional retry limit for the tool
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The wrapped function
|
|
101
|
+
"""
|
|
102
|
+
# Get function signature and metadata
|
|
103
|
+
sig = inspect.signature(func)
|
|
104
|
+
|
|
105
|
+
# Set tool name and description
|
|
106
|
+
tool_name = name or func.__name__.lower()
|
|
107
|
+
tool_description = description or func.__doc__ or f"Tool for {func.__name__}"
|
|
108
|
+
|
|
109
|
+
# If rate limit specified, add it to description
|
|
110
|
+
if rate_limit is not None:
|
|
111
|
+
tool_description = f"{tool_description} (Limited to {rate_limit} calls per session)"
|
|
112
|
+
|
|
113
|
+
# Convert Python type hints to ParamType
|
|
114
|
+
parameters = {}
|
|
115
|
+
for param_name, param in sig.parameters.items():
|
|
116
|
+
if param_name in ('self', 'cls'):
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Map Python types to ParamType
|
|
120
|
+
if param.annotation == int:
|
|
121
|
+
param_type = ParamType.INTEGER
|
|
122
|
+
elif param.annotation == float:
|
|
123
|
+
param_type = ParamType.FLOAT
|
|
124
|
+
elif param.annotation == str:
|
|
125
|
+
param_type = ParamType.STRING
|
|
126
|
+
else:
|
|
127
|
+
param_type = ParamType.ANY
|
|
128
|
+
|
|
129
|
+
parameters[param_name] = param_type
|
|
130
|
+
|
|
131
|
+
# Create tool instance with rate limiting
|
|
132
|
+
tool_instance = Tool(
|
|
133
|
+
name=tool_name,
|
|
134
|
+
description=tool_description,
|
|
135
|
+
parameters=parameters,
|
|
136
|
+
func=func,
|
|
137
|
+
rate_limit=rate_limit
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Add retry limit if specified
|
|
141
|
+
if retry_limit is not None:
|
|
142
|
+
tool_instance.retry_limit = retry_limit
|
|
143
|
+
|
|
144
|
+
# Add the tool instance as an attribute to the function
|
|
145
|
+
func._tool = tool_instance
|
|
146
|
+
|
|
147
|
+
@functools.wraps(func)
|
|
148
|
+
def wrapper(*args, **kwargs):
|
|
149
|
+
# Convert positional arguments to keyword arguments if needed
|
|
150
|
+
if args and len(args) > 0:
|
|
151
|
+
sig = inspect.signature(func)
|
|
152
|
+
param_names = list(sig.parameters.keys())
|
|
153
|
+
# Skip self/cls if this is a method
|
|
154
|
+
if param_names and param_names[0] in ('self', 'cls') and len(args) > 0:
|
|
155
|
+
kwargs[param_names[1]] = args[0]
|
|
156
|
+
for i, arg in enumerate(args[1:], start=2):
|
|
157
|
+
if i < len(param_names):
|
|
158
|
+
kwargs[param_names[i]] = arg
|
|
159
|
+
else:
|
|
160
|
+
# Not a method
|
|
161
|
+
for i, arg in enumerate(args):
|
|
162
|
+
if i < len(param_names):
|
|
163
|
+
kwargs[param_names[i]] = arg
|
|
164
|
+
|
|
165
|
+
# Filter out parameters that are not part of the function signature
|
|
166
|
+
# This prevents errors with unwanted parameters being passed
|
|
167
|
+
sig = inspect.signature(func)
|
|
168
|
+
valid_params = set(sig.parameters.keys())
|
|
169
|
+
|
|
170
|
+
# Check if the function accepts **kwargs
|
|
171
|
+
accepts_kwargs = any(
|
|
172
|
+
param.kind == inspect.Parameter.VAR_KEYWORD
|
|
173
|
+
for param in sig.parameters.values()
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# If function doesn't accept **kwargs, filter out invalid parameters
|
|
177
|
+
if not accepts_kwargs:
|
|
178
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k in valid_params}
|
|
179
|
+
else:
|
|
180
|
+
filtered_kwargs = kwargs # Keep all parameters if the function accepts **kwargs
|
|
181
|
+
|
|
182
|
+
# Use the tool instance directly to ensure rate limiting is applied
|
|
183
|
+
return tool_instance(**filtered_kwargs)
|
|
184
|
+
|
|
185
|
+
wrapper._tool = tool_instance
|
|
186
|
+
|
|
187
|
+
return cast(F, wrapper)
|
tinyagent/exceptions.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exceptions for the tinyAgent framework.
|
|
3
|
+
|
|
4
|
+
This module contains custom exceptions used throughout the tinyAgent framework.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Dict, Any, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentRetryExceeded(Exception):
|
|
11
|
+
"""Exception raised when agent exceeds max retry attempts."""
|
|
12
|
+
def __init__(self, message, history=None):
|
|
13
|
+
self.message = message
|
|
14
|
+
self.history = history or []
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TinyAgentError(Exception):
|
|
19
|
+
"""Base class for all tinyAgent exceptions."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConfigurationError(TinyAgentError):
|
|
24
|
+
"""Exception raised for configuration-related errors."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ToolError(TinyAgentError):
|
|
29
|
+
"""Base class for tool-related errors."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ToolNotFoundError(ToolError):
|
|
34
|
+
"""Exception raised when a requested tool is not found."""
|
|
35
|
+
def __init__(self, tool_name: str, available_tools: Optional[List[str]] = None):
|
|
36
|
+
self.tool_name = tool_name
|
|
37
|
+
self.available_tools = available_tools or []
|
|
38
|
+
message = f"Tool '{tool_name}' not found"
|
|
39
|
+
if available_tools:
|
|
40
|
+
message += f". Available tools: {', '.join(available_tools)}"
|
|
41
|
+
super().__init__(message)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ToolExecutionError(ToolError):
|
|
45
|
+
"""Exception raised when a tool execution fails."""
|
|
46
|
+
def __init__(self, tool_name: str, args: Dict[str, Any], error_message: str):
|
|
47
|
+
self.tool_name = tool_name
|
|
48
|
+
self.args = args
|
|
49
|
+
self.error_message = error_message
|
|
50
|
+
super().__init__(f"Error executing tool {tool_name}: {error_message}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RateLimitExceeded(ToolError):
|
|
54
|
+
"""Exception raised when a tool's rate limit is exceeded."""
|
|
55
|
+
def __init__(self, tool_name: str, limit: int):
|
|
56
|
+
self.tool_name = tool_name
|
|
57
|
+
self.limit = limit
|
|
58
|
+
super().__init__(f"Rate limit exceeded for tool '{tool_name}'. Maximum: {limit} calls")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ParsingError(TinyAgentError):
|
|
62
|
+
"""Exception raised when parsing LLM responses fails."""
|
|
63
|
+
def __init__(self, content: str, details: str = None):
|
|
64
|
+
self.content = content
|
|
65
|
+
self.details = details
|
|
66
|
+
message = "Failed to parse LLM response"
|
|
67
|
+
if details:
|
|
68
|
+
message += f": {details}"
|
|
69
|
+
super().__init__(message)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class OrchestratorError(TinyAgentError):
|
|
73
|
+
"""Exception raised for orchestrator-related errors."""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AgentNotFoundError(OrchestratorError):
|
|
78
|
+
"""Exception raised when a requested agent is not found."""
|
|
79
|
+
def __init__(self, agent_id: str, available_agents: Optional[List[str]] = None):
|
|
80
|
+
self.agent_id = agent_id
|
|
81
|
+
self.available_agents = available_agents or []
|
|
82
|
+
message = f"Agent '{agent_id}' not found"
|
|
83
|
+
if available_agents:
|
|
84
|
+
message += f". Available agents: {', '.join(available_agents)}"
|
|
85
|
+
super().__init__(message)
|