aiecs 1.2.2__py3-none-any.whl → 1.3.3__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.
Potentially problematic release.
This version of aiecs might be problematic. Click here for more details.
- aiecs/__init__.py +1 -1
- aiecs/llm/clients/vertex_client.py +22 -2
- aiecs/main.py +2 -2
- aiecs/scripts/tools_develop/README.md +111 -2
- aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
- aiecs/scripts/tools_develop/validate_tool_schemas.py +80 -21
- aiecs/scripts/tools_develop/verify_tools.py +347 -0
- aiecs/tools/__init__.py +94 -30
- aiecs/tools/apisource/__init__.py +106 -0
- aiecs/tools/apisource/intelligence/__init__.py +20 -0
- aiecs/tools/apisource/intelligence/data_fusion.py +378 -0
- aiecs/tools/apisource/intelligence/query_analyzer.py +387 -0
- aiecs/tools/apisource/intelligence/search_enhancer.py +384 -0
- aiecs/tools/apisource/monitoring/__init__.py +12 -0
- aiecs/tools/apisource/monitoring/metrics.py +308 -0
- aiecs/tools/apisource/providers/__init__.py +114 -0
- aiecs/tools/apisource/providers/base.py +684 -0
- aiecs/tools/apisource/providers/census.py +412 -0
- aiecs/tools/apisource/providers/fred.py +575 -0
- aiecs/tools/apisource/providers/newsapi.py +402 -0
- aiecs/tools/apisource/providers/worldbank.py +346 -0
- aiecs/tools/apisource/reliability/__init__.py +14 -0
- aiecs/tools/apisource/reliability/error_handler.py +362 -0
- aiecs/tools/apisource/reliability/fallback_strategy.py +420 -0
- aiecs/tools/apisource/tool.py +814 -0
- aiecs/tools/apisource/utils/__init__.py +12 -0
- aiecs/tools/apisource/utils/validators.py +343 -0
- aiecs/tools/langchain_adapter.py +95 -17
- aiecs/tools/search_tool/__init__.py +102 -0
- aiecs/tools/search_tool/analyzers.py +583 -0
- aiecs/tools/search_tool/cache.py +280 -0
- aiecs/tools/search_tool/constants.py +127 -0
- aiecs/tools/search_tool/context.py +219 -0
- aiecs/tools/search_tool/core.py +773 -0
- aiecs/tools/search_tool/deduplicator.py +123 -0
- aiecs/tools/search_tool/error_handler.py +257 -0
- aiecs/tools/search_tool/metrics.py +375 -0
- aiecs/tools/search_tool/rate_limiter.py +177 -0
- aiecs/tools/search_tool/schemas.py +297 -0
- aiecs/tools/statistics/data_loader_tool.py +2 -2
- aiecs/tools/statistics/data_transformer_tool.py +1 -1
- aiecs/tools/task_tools/__init__.py +8 -8
- aiecs/tools/task_tools/report_tool.py +1 -1
- aiecs/tools/tool_executor/__init__.py +2 -0
- aiecs/tools/tool_executor/tool_executor.py +284 -14
- aiecs/utils/__init__.py +11 -0
- aiecs/utils/cache_provider.py +698 -0
- aiecs/utils/execution_utils.py +5 -5
- {aiecs-1.2.2.dist-info → aiecs-1.3.3.dist-info}/METADATA +1 -1
- {aiecs-1.2.2.dist-info → aiecs-1.3.3.dist-info}/RECORD +54 -22
- aiecs/tools/task_tools/search_tool.py +0 -1123
- {aiecs-1.2.2.dist-info → aiecs-1.3.3.dist-info}/WHEEL +0 -0
- {aiecs-1.2.2.dist-info → aiecs-1.3.3.dist-info}/entry_points.txt +0 -0
- {aiecs-1.2.2.dist-info → aiecs-1.3.3.dist-info}/licenses/LICENSE +0 -0
- {aiecs-1.2.2.dist-info → aiecs-1.3.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smart Error Handler for API Providers
|
|
3
|
+
|
|
4
|
+
Provides intelligent error handling with:
|
|
5
|
+
- Classification of retryable vs non-retryable errors
|
|
6
|
+
- Exponential backoff retry strategy
|
|
7
|
+
- Agent-friendly error messages with recovery suggestions
|
|
8
|
+
- Detailed error context and history
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SmartErrorHandler:
|
|
20
|
+
"""
|
|
21
|
+
Intelligent error handler with retry logic and recovery suggestions.
|
|
22
|
+
|
|
23
|
+
Automatically classifies errors, applies appropriate retry strategies,
|
|
24
|
+
and provides actionable recovery suggestions for AI agents.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Error types that can be retried
|
|
28
|
+
RETRYABLE_ERRORS = [
|
|
29
|
+
'timeout',
|
|
30
|
+
'connection',
|
|
31
|
+
'rate limit',
|
|
32
|
+
'rate_limit',
|
|
33
|
+
'429', # Too Many Requests
|
|
34
|
+
'500', # Internal Server Error
|
|
35
|
+
'502', # Bad Gateway
|
|
36
|
+
'503', # Service Unavailable
|
|
37
|
+
'504', # Gateway Timeout
|
|
38
|
+
'temporary',
|
|
39
|
+
'transient'
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Error types that should not be retried
|
|
43
|
+
NON_RETRYABLE_ERRORS = [
|
|
44
|
+
'authentication',
|
|
45
|
+
'auth',
|
|
46
|
+
'api key',
|
|
47
|
+
'unauthorized',
|
|
48
|
+
'401', # Unauthorized
|
|
49
|
+
'403', # Forbidden
|
|
50
|
+
'invalid',
|
|
51
|
+
'not found',
|
|
52
|
+
'404', # Not Found
|
|
53
|
+
'bad request',
|
|
54
|
+
'400' # Bad Request
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
max_retries: int = 3,
|
|
60
|
+
backoff_factor: float = 2.0,
|
|
61
|
+
initial_delay: float = 1.0,
|
|
62
|
+
max_delay: float = 30.0
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
Initialize error handler.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
max_retries: Maximum number of retry attempts
|
|
69
|
+
backoff_factor: Multiplier for exponential backoff
|
|
70
|
+
initial_delay: Initial delay in seconds before first retry
|
|
71
|
+
max_delay: Maximum delay between retries in seconds
|
|
72
|
+
"""
|
|
73
|
+
self.max_retries = max_retries
|
|
74
|
+
self.backoff_factor = backoff_factor
|
|
75
|
+
self.initial_delay = initial_delay
|
|
76
|
+
self.max_delay = max_delay
|
|
77
|
+
|
|
78
|
+
def execute_with_retry(
|
|
79
|
+
self,
|
|
80
|
+
operation_func: Callable[[], Any],
|
|
81
|
+
operation_name: str = "operation",
|
|
82
|
+
provider_name: str = "provider"
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Execute an operation with intelligent retry logic.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
operation_func: Function to execute (should take no arguments)
|
|
89
|
+
operation_name: Name of the operation for logging
|
|
90
|
+
provider_name: Name of the provider for logging
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Dictionary with:
|
|
94
|
+
- success: bool
|
|
95
|
+
- data: result data if successful
|
|
96
|
+
- error: error details if failed
|
|
97
|
+
- retry_info: retry attempt information
|
|
98
|
+
"""
|
|
99
|
+
last_error = None
|
|
100
|
+
retry_info = {
|
|
101
|
+
'attempts': 0,
|
|
102
|
+
'errors': [],
|
|
103
|
+
'recovery_suggestions': []
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for attempt in range(self.max_retries):
|
|
107
|
+
retry_info['attempts'] = attempt + 1
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
# Execute the operation
|
|
111
|
+
result = operation_func()
|
|
112
|
+
|
|
113
|
+
# Success!
|
|
114
|
+
if attempt > 0:
|
|
115
|
+
logger.info(
|
|
116
|
+
f"{provider_name}.{operation_name} succeeded after "
|
|
117
|
+
f"{attempt + 1} attempts"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
'success': True,
|
|
122
|
+
'data': result,
|
|
123
|
+
'retry_info': retry_info
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
last_error = e
|
|
128
|
+
error_msg = str(e).lower()
|
|
129
|
+
error_type = self._classify_error(e, error_msg)
|
|
130
|
+
|
|
131
|
+
# Record this error
|
|
132
|
+
retry_info['errors'].append({
|
|
133
|
+
'attempt': attempt + 1,
|
|
134
|
+
'error': str(e),
|
|
135
|
+
'error_type': error_type,
|
|
136
|
+
'timestamp': datetime.utcnow().isoformat()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
# Determine if we should retry
|
|
140
|
+
is_retryable = self._is_retryable(error_msg, error_type)
|
|
141
|
+
is_last_attempt = (attempt == self.max_retries - 1)
|
|
142
|
+
|
|
143
|
+
if not is_retryable:
|
|
144
|
+
logger.warning(
|
|
145
|
+
f"{provider_name}.{operation_name} failed with non-retryable "
|
|
146
|
+
f"error: {error_type}"
|
|
147
|
+
)
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
if is_last_attempt:
|
|
151
|
+
logger.error(
|
|
152
|
+
f"{provider_name}.{operation_name} failed after "
|
|
153
|
+
f"{self.max_retries} attempts"
|
|
154
|
+
)
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
# Calculate wait time with exponential backoff
|
|
158
|
+
wait_time = min(
|
|
159
|
+
self.initial_delay * (self.backoff_factor ** attempt),
|
|
160
|
+
self.max_delay
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
logger.info(
|
|
164
|
+
f"{provider_name}.{operation_name} attempt {attempt + 1} failed "
|
|
165
|
+
f"({error_type}), retrying in {wait_time:.1f}s..."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
time.sleep(wait_time)
|
|
169
|
+
|
|
170
|
+
# All retries exhausted or non-retryable error
|
|
171
|
+
retry_info['recovery_suggestions'] = self._generate_recovery_suggestions(
|
|
172
|
+
last_error,
|
|
173
|
+
operation_name,
|
|
174
|
+
provider_name
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
'success': False,
|
|
179
|
+
'error': {
|
|
180
|
+
'type': type(last_error).__name__,
|
|
181
|
+
'message': str(last_error),
|
|
182
|
+
'details': self._parse_error_details(last_error),
|
|
183
|
+
'is_retryable': self._is_retryable(str(last_error).lower(),
|
|
184
|
+
self._classify_error(last_error, str(last_error).lower()))
|
|
185
|
+
},
|
|
186
|
+
'retry_info': retry_info
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def _classify_error(self, error: Exception, error_msg: str) -> str:
|
|
190
|
+
"""
|
|
191
|
+
Classify error type.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
error: The exception object
|
|
195
|
+
error_msg: Error message in lowercase
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Error classification string
|
|
199
|
+
"""
|
|
200
|
+
# Check exception type
|
|
201
|
+
error_class = type(error).__name__.lower()
|
|
202
|
+
|
|
203
|
+
if 'timeout' in error_class or 'timeout' in error_msg:
|
|
204
|
+
return 'timeout'
|
|
205
|
+
elif 'connection' in error_class or 'connection' in error_msg:
|
|
206
|
+
return 'connection'
|
|
207
|
+
elif 'rate' in error_msg and 'limit' in error_msg:
|
|
208
|
+
return 'rate_limit'
|
|
209
|
+
elif '429' in error_msg:
|
|
210
|
+
return 'rate_limit'
|
|
211
|
+
elif 'auth' in error_msg or '401' in error_msg or '403' in error_msg:
|
|
212
|
+
return 'authentication'
|
|
213
|
+
elif 'not found' in error_msg or '404' in error_msg:
|
|
214
|
+
return 'not_found'
|
|
215
|
+
elif 'bad request' in error_msg or '400' in error_msg:
|
|
216
|
+
return 'invalid_parameter'
|
|
217
|
+
elif '500' in error_msg or '502' in error_msg or '503' in error_msg or '504' in error_msg:
|
|
218
|
+
return 'server_error'
|
|
219
|
+
elif 'invalid' in error_msg:
|
|
220
|
+
return 'invalid_parameter'
|
|
221
|
+
else:
|
|
222
|
+
return 'unknown'
|
|
223
|
+
|
|
224
|
+
def _is_retryable(self, error_msg: str, error_type: str) -> bool:
|
|
225
|
+
"""
|
|
226
|
+
Determine if an error is retryable.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
error_msg: Error message in lowercase
|
|
230
|
+
error_type: Classified error type
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
True if error is retryable
|
|
234
|
+
"""
|
|
235
|
+
# Check non-retryable first (higher priority)
|
|
236
|
+
for non_retryable in self.NON_RETRYABLE_ERRORS:
|
|
237
|
+
if non_retryable in error_msg or non_retryable in error_type:
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
# Check retryable
|
|
241
|
+
for retryable in self.RETRYABLE_ERRORS:
|
|
242
|
+
if retryable in error_msg or retryable in error_type:
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
# Default: retry server errors and unknown errors
|
|
246
|
+
return error_type in ['server_error', 'timeout', 'connection', 'rate_limit']
|
|
247
|
+
|
|
248
|
+
def _parse_error_details(self, error: Exception) -> Dict[str, Any]:
|
|
249
|
+
"""
|
|
250
|
+
Extract detailed information from error.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
error: The exception object
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Dictionary with error details
|
|
257
|
+
"""
|
|
258
|
+
details = {
|
|
259
|
+
'class': type(error).__name__,
|
|
260
|
+
'message': str(error)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Extract HTTP status code if available
|
|
264
|
+
if hasattr(error, 'response') and hasattr(error.response, 'status_code'):
|
|
265
|
+
details['status_code'] = error.response.status_code
|
|
266
|
+
if hasattr(error.response, 'text'):
|
|
267
|
+
details['response_body'] = error.response.text[:500] # Limit size
|
|
268
|
+
|
|
269
|
+
# Extract additional context
|
|
270
|
+
if hasattr(error, '__cause__') and error.__cause__:
|
|
271
|
+
details['cause'] = str(error.__cause__)
|
|
272
|
+
|
|
273
|
+
return details
|
|
274
|
+
|
|
275
|
+
def _generate_recovery_suggestions(
|
|
276
|
+
self,
|
|
277
|
+
error: Exception,
|
|
278
|
+
operation_name: str,
|
|
279
|
+
provider_name: str
|
|
280
|
+
) -> List[str]:
|
|
281
|
+
"""
|
|
282
|
+
Generate actionable recovery suggestions for the error.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
error: The exception object
|
|
286
|
+
operation_name: Name of the failed operation
|
|
287
|
+
provider_name: Name of the provider
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List of recovery suggestion strings
|
|
291
|
+
"""
|
|
292
|
+
suggestions = []
|
|
293
|
+
error_msg = str(error).lower()
|
|
294
|
+
error_type = self._classify_error(error, error_msg)
|
|
295
|
+
|
|
296
|
+
if error_type == 'authentication':
|
|
297
|
+
suggestions.extend([
|
|
298
|
+
f"Check that your {provider_name.upper()} API key is valid and properly configured",
|
|
299
|
+
f"Verify the API key environment variable {provider_name.upper()}_API_KEY is set",
|
|
300
|
+
"Confirm the API key has not expired or been revoked",
|
|
301
|
+
f"Ensure you have the necessary permissions for the {operation_name} operation"
|
|
302
|
+
])
|
|
303
|
+
|
|
304
|
+
elif error_type == 'rate_limit':
|
|
305
|
+
suggestions.extend([
|
|
306
|
+
"Wait before making more requests (rate limit exceeded)",
|
|
307
|
+
f"Consider using a different provider if available",
|
|
308
|
+
"Reduce the frequency of requests or implement request batching",
|
|
309
|
+
"Check if you can upgrade your API plan for higher rate limits",
|
|
310
|
+
"Enable caching to reduce API calls"
|
|
311
|
+
])
|
|
312
|
+
|
|
313
|
+
elif error_type == 'not_found':
|
|
314
|
+
suggestions.extend([
|
|
315
|
+
f"Verify the resource ID or parameter values are correct for {provider_name}",
|
|
316
|
+
f"Use the search or list operations to find valid resource IDs",
|
|
317
|
+
"Check that the resource exists and is accessible with your credentials",
|
|
318
|
+
f"Review the {provider_name} API documentation for correct resource identifiers"
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
elif error_type == 'invalid_parameter':
|
|
322
|
+
suggestions.extend([
|
|
323
|
+
f"Check the operation schema for {operation_name} to see required parameters",
|
|
324
|
+
"Review parameter types and value formats",
|
|
325
|
+
f"Use get_operation_schema('{operation_name}') to see valid parameters and examples",
|
|
326
|
+
"Verify parameter names are spelled correctly"
|
|
327
|
+
])
|
|
328
|
+
|
|
329
|
+
elif error_type == 'timeout':
|
|
330
|
+
suggestions.extend([
|
|
331
|
+
"Try again with a smaller date range or result limit",
|
|
332
|
+
"Increase the timeout setting in the provider configuration",
|
|
333
|
+
"Check your network connection",
|
|
334
|
+
"The API service may be experiencing high load, try again later"
|
|
335
|
+
])
|
|
336
|
+
|
|
337
|
+
elif error_type == 'connection':
|
|
338
|
+
suggestions.extend([
|
|
339
|
+
"Check your internet connection",
|
|
340
|
+
"Verify the API endpoint is accessible",
|
|
341
|
+
"Check if there are any firewall or proxy issues",
|
|
342
|
+
"The API service may be temporarily unavailable"
|
|
343
|
+
])
|
|
344
|
+
|
|
345
|
+
elif error_type == 'server_error':
|
|
346
|
+
suggestions.extend([
|
|
347
|
+
"The API service is experiencing issues, try again later",
|
|
348
|
+
"Check the API status page for known outages",
|
|
349
|
+
"Try a different provider if available",
|
|
350
|
+
"Reduce the complexity of your request"
|
|
351
|
+
])
|
|
352
|
+
|
|
353
|
+
else:
|
|
354
|
+
suggestions.extend([
|
|
355
|
+
f"Review the error message for specific details",
|
|
356
|
+
f"Check the {provider_name} API documentation for {operation_name}",
|
|
357
|
+
"Verify all required parameters are provided",
|
|
358
|
+
"Try a simpler query to isolate the issue"
|
|
359
|
+
])
|
|
360
|
+
|
|
361
|
+
return suggestions
|
|
362
|
+
|