aiecs 1.2.1__py3-none-any.whl → 1.3.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.

Potentially problematic release.


This version of aiecs might be problematic. Click here for more details.

Files changed (56) hide show
  1. aiecs/__init__.py +1 -1
  2. aiecs/config/config.py +2 -1
  3. aiecs/llm/clients/vertex_client.py +5 -0
  4. aiecs/main.py +2 -2
  5. aiecs/scripts/tools_develop/README.md +111 -2
  6. aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
  7. aiecs/scripts/tools_develop/validate_tool_schemas.py +80 -21
  8. aiecs/scripts/tools_develop/verify_tools.py +347 -0
  9. aiecs/tools/__init__.py +94 -30
  10. aiecs/tools/apisource/__init__.py +106 -0
  11. aiecs/tools/apisource/intelligence/__init__.py +20 -0
  12. aiecs/tools/apisource/intelligence/data_fusion.py +378 -0
  13. aiecs/tools/apisource/intelligence/query_analyzer.py +387 -0
  14. aiecs/tools/apisource/intelligence/search_enhancer.py +384 -0
  15. aiecs/tools/apisource/monitoring/__init__.py +12 -0
  16. aiecs/tools/apisource/monitoring/metrics.py +308 -0
  17. aiecs/tools/apisource/providers/__init__.py +114 -0
  18. aiecs/tools/apisource/providers/base.py +684 -0
  19. aiecs/tools/apisource/providers/census.py +412 -0
  20. aiecs/tools/apisource/providers/fred.py +575 -0
  21. aiecs/tools/apisource/providers/newsapi.py +402 -0
  22. aiecs/tools/apisource/providers/worldbank.py +346 -0
  23. aiecs/tools/apisource/reliability/__init__.py +14 -0
  24. aiecs/tools/apisource/reliability/error_handler.py +362 -0
  25. aiecs/tools/apisource/reliability/fallback_strategy.py +420 -0
  26. aiecs/tools/apisource/tool.py +814 -0
  27. aiecs/tools/apisource/utils/__init__.py +12 -0
  28. aiecs/tools/apisource/utils/validators.py +343 -0
  29. aiecs/tools/langchain_adapter.py +95 -17
  30. aiecs/tools/search_tool/__init__.py +102 -0
  31. aiecs/tools/search_tool/analyzers.py +583 -0
  32. aiecs/tools/search_tool/cache.py +280 -0
  33. aiecs/tools/search_tool/constants.py +127 -0
  34. aiecs/tools/search_tool/context.py +219 -0
  35. aiecs/tools/search_tool/core.py +773 -0
  36. aiecs/tools/search_tool/deduplicator.py +123 -0
  37. aiecs/tools/search_tool/error_handler.py +257 -0
  38. aiecs/tools/search_tool/metrics.py +375 -0
  39. aiecs/tools/search_tool/rate_limiter.py +177 -0
  40. aiecs/tools/search_tool/schemas.py +297 -0
  41. aiecs/tools/statistics/data_loader_tool.py +2 -2
  42. aiecs/tools/statistics/data_transformer_tool.py +1 -1
  43. aiecs/tools/task_tools/__init__.py +8 -8
  44. aiecs/tools/task_tools/report_tool.py +1 -1
  45. aiecs/tools/tool_executor/__init__.py +2 -0
  46. aiecs/tools/tool_executor/tool_executor.py +284 -14
  47. aiecs/utils/__init__.py +11 -0
  48. aiecs/utils/cache_provider.py +698 -0
  49. aiecs/utils/execution_utils.py +5 -5
  50. {aiecs-1.2.1.dist-info → aiecs-1.3.1.dist-info}/METADATA +1 -1
  51. {aiecs-1.2.1.dist-info → aiecs-1.3.1.dist-info}/RECORD +55 -23
  52. aiecs/tools/task_tools/search_tool.py +0 -1123
  53. {aiecs-1.2.1.dist-info → aiecs-1.3.1.dist-info}/WHEEL +0 -0
  54. {aiecs-1.2.1.dist-info → aiecs-1.3.1.dist-info}/entry_points.txt +0 -0
  55. {aiecs-1.2.1.dist-info → aiecs-1.3.1.dist-info}/licenses/LICENSE +0 -0
  56. {aiecs-1.2.1.dist-info → aiecs-1.3.1.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
+