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,420 @@
1
+ """
2
+ Intelligent Fallback Strategy for API Providers
3
+
4
+ Provides automatic provider failover:
5
+ - Define fallback chains between providers
6
+ - Map equivalent operations across providers
7
+ - Convert parameters between provider formats
8
+ - Track fallback attempts and success rates
9
+ """
10
+
11
+ import logging
12
+ from typing import Any, Callable, Dict, List, Optional, Tuple
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class FallbackStrategy:
18
+ """
19
+ Manages fallback logic when primary providers fail.
20
+
21
+ Automatically retries with alternative providers when a request fails,
22
+ handling operation mapping and parameter conversion.
23
+ """
24
+
25
+ # Provider fallback chains: primary -> [fallbacks]
26
+ FALLBACK_MAP = {
27
+ 'fred': ['worldbank'], # FRED -> World Bank for economic data
28
+ 'worldbank': [], # World Bank has no fallback
29
+ 'newsapi': [], # News API has no fallback
30
+ 'census': ['worldbank'] # Census -> World Bank for demographic data
31
+ }
32
+
33
+ # Operation mappings: (provider, operation) -> [(fallback_provider, fallback_operation)]
34
+ OPERATION_MAP = {
35
+ ('fred', 'get_series'): [
36
+ ('worldbank', 'get_indicator')
37
+ ],
38
+ ('fred', 'get_series_observations'): [
39
+ ('worldbank', 'get_indicator')
40
+ ],
41
+ ('fred', 'search_series'): [
42
+ ('worldbank', 'search_indicators')
43
+ ],
44
+ ('census', 'get_population'): [
45
+ ('worldbank', 'get_indicator')
46
+ ],
47
+ ('census', 'get_acs_data'): [
48
+ ('worldbank', 'get_indicator')
49
+ ]
50
+ }
51
+
52
+ # Parameter conversion rules
53
+ PARAMETER_CONVERSIONS = {
54
+ ('fred', 'worldbank'): {
55
+ 'series_id': 'indicator_code',
56
+ 'observation_start': 'date', # Note: will need special handling
57
+ 'observation_end': 'date',
58
+ 'limit': 'per_page'
59
+ },
60
+ ('census', 'worldbank'): {
61
+ 'variables': 'indicator_code',
62
+ 'geography': 'country_code' # Note: needs conversion (e.g., 'state:*' -> 'US')
63
+ }
64
+ }
65
+
66
+ def __init__(self):
67
+ """Initialize fallback strategy"""
68
+ self.fallback_stats = {} # Track fallback success rates
69
+
70
+ def execute_with_fallback(
71
+ self,
72
+ primary_provider: str,
73
+ operation: str,
74
+ params: Dict[str, Any],
75
+ provider_executor: Callable[[str, str, Dict[str, Any]], Dict[str, Any]],
76
+ providers_available: List[str]
77
+ ) -> Dict[str, Any]:
78
+ """
79
+ Execute operation with automatic fallback to alternative providers.
80
+
81
+ Args:
82
+ primary_provider: Primary provider name
83
+ operation: Operation name
84
+ params: Operation parameters
85
+ provider_executor: Function to execute provider operation:
86
+ (provider, operation, params) -> result
87
+ providers_available: List of available provider names
88
+
89
+ Returns:
90
+ Result dictionary with:
91
+ - success: bool
92
+ - data: result data if successful
93
+ - attempts: list of attempt information
94
+ - fallback_used: bool
95
+ """
96
+ result = {
97
+ 'success': False,
98
+ 'data': None,
99
+ 'attempts': [],
100
+ 'fallback_used': False
101
+ }
102
+
103
+ # Try primary provider
104
+ try:
105
+ logger.info(
106
+ f"Attempting primary provider: {primary_provider}.{operation}"
107
+ )
108
+ data = provider_executor(primary_provider, operation, params)
109
+
110
+ result['success'] = True
111
+ result['data'] = data
112
+ result['attempts'].append({
113
+ 'provider': primary_provider,
114
+ 'operation': operation,
115
+ 'status': 'success'
116
+ })
117
+
118
+ return result
119
+
120
+ except Exception as primary_error:
121
+ logger.warning(
122
+ f"Primary provider {primary_provider}.{operation} failed: {primary_error}"
123
+ )
124
+ result['attempts'].append({
125
+ 'provider': primary_provider,
126
+ 'operation': operation,
127
+ 'status': 'failed',
128
+ 'error': str(primary_error)
129
+ })
130
+
131
+ # Get fallback providers
132
+ fallback_providers = self.FALLBACK_MAP.get(primary_provider, [])
133
+
134
+ if not fallback_providers:
135
+ logger.info(f"No fallback providers configured for {primary_provider}")
136
+ return result
137
+
138
+ # Try each fallback provider
139
+ for fallback_provider in fallback_providers:
140
+ if fallback_provider not in providers_available:
141
+ logger.debug(f"Fallback provider {fallback_provider} not available")
142
+ continue
143
+
144
+ # Find equivalent operation
145
+ fallback_operations = self._get_fallback_operations(
146
+ primary_provider,
147
+ operation,
148
+ fallback_provider
149
+ )
150
+
151
+ if not fallback_operations:
152
+ logger.debug(
153
+ f"No operation mapping from {primary_provider}.{operation} "
154
+ f"to {fallback_provider}"
155
+ )
156
+ continue
157
+
158
+ # Try each mapped operation
159
+ for fallback_op in fallback_operations:
160
+ try:
161
+ logger.info(
162
+ f"Attempting fallback: {fallback_provider}.{fallback_op}"
163
+ )
164
+
165
+ # Convert parameters
166
+ converted_params = self._convert_parameters(
167
+ primary_provider,
168
+ fallback_provider,
169
+ params
170
+ )
171
+
172
+ # Execute fallback
173
+ data = provider_executor(
174
+ fallback_provider,
175
+ fallback_op,
176
+ converted_params
177
+ )
178
+
179
+ # Success!
180
+ result['success'] = True
181
+ result['data'] = data
182
+ result['fallback_used'] = True
183
+ result['attempts'].append({
184
+ 'provider': fallback_provider,
185
+ 'operation': fallback_op,
186
+ 'status': 'success'
187
+ })
188
+
189
+ # Add fallback warning to metadata
190
+ if isinstance(data, dict) and 'metadata' in data:
191
+ data['metadata']['fallback_warning'] = (
192
+ f"Primary provider {primary_provider} failed, "
193
+ f"using fallback {fallback_provider}"
194
+ )
195
+ data['metadata']['original_provider'] = primary_provider
196
+ data['metadata']['original_operation'] = operation
197
+
198
+ # Update success stats
199
+ self._update_stats(fallback_provider, fallback_op, success=True)
200
+
201
+ logger.info(
202
+ f"Fallback successful: {fallback_provider}.{fallback_op}"
203
+ )
204
+
205
+ return result
206
+
207
+ except Exception as fallback_error:
208
+ logger.warning(
209
+ f"Fallback {fallback_provider}.{fallback_op} failed: "
210
+ f"{fallback_error}"
211
+ )
212
+ result['attempts'].append({
213
+ 'provider': fallback_provider,
214
+ 'operation': fallback_op,
215
+ 'status': 'failed',
216
+ 'error': str(fallback_error)
217
+ })
218
+
219
+ # Update failure stats
220
+ self._update_stats(fallback_provider, fallback_op, success=False)
221
+
222
+ # All attempts failed
223
+ logger.error(
224
+ f"All providers failed for operation {operation}. "
225
+ f"Attempts: {len(result['attempts'])}"
226
+ )
227
+
228
+ return result
229
+
230
+ def _get_fallback_operations(
231
+ self,
232
+ primary_provider: str,
233
+ operation: str,
234
+ fallback_provider: str
235
+ ) -> List[str]:
236
+ """
237
+ Get equivalent operations in fallback provider.
238
+
239
+ Args:
240
+ primary_provider: Primary provider name
241
+ operation: Operation name
242
+ fallback_provider: Fallback provider name
243
+
244
+ Returns:
245
+ List of equivalent operation names
246
+ """
247
+ key = (primary_provider, operation)
248
+ mappings = self.OPERATION_MAP.get(key, [])
249
+
250
+ # Filter for specific fallback provider
251
+ fallback_ops = [
252
+ op for provider, op in mappings
253
+ if provider == fallback_provider
254
+ ]
255
+
256
+ return fallback_ops
257
+
258
+ def _convert_parameters(
259
+ self,
260
+ source_provider: str,
261
+ target_provider: str,
262
+ params: Dict[str, Any]
263
+ ) -> Dict[str, Any]:
264
+ """
265
+ Convert parameters from source to target provider format.
266
+
267
+ Args:
268
+ source_provider: Source provider name
269
+ target_provider: Target provider name
270
+ params: Original parameters
271
+
272
+ Returns:
273
+ Converted parameters
274
+ """
275
+ conversion_key = (source_provider, target_provider)
276
+ conversion_rules = self.PARAMETER_CONVERSIONS.get(conversion_key, {})
277
+
278
+ converted = {}
279
+
280
+ for source_param, value in params.items():
281
+ # Check if there's a conversion rule
282
+ if source_param in conversion_rules:
283
+ target_param = conversion_rules[source_param]
284
+
285
+ # Apply special conversions
286
+ converted_value = self._convert_parameter_value(
287
+ source_provider,
288
+ target_provider,
289
+ source_param,
290
+ target_param,
291
+ value
292
+ )
293
+
294
+ converted[target_param] = converted_value
295
+ else:
296
+ # Keep parameter as-is
297
+ converted[source_param] = value
298
+
299
+ # Add default parameters for target provider
300
+ converted = self._add_default_parameters(target_provider, converted)
301
+
302
+ return converted
303
+
304
+ def _convert_parameter_value(
305
+ self,
306
+ source_provider: str,
307
+ target_provider: str,
308
+ source_param: str,
309
+ target_param: str,
310
+ value: Any
311
+ ) -> Any:
312
+ """
313
+ Convert parameter value between provider formats.
314
+
315
+ Args:
316
+ source_provider: Source provider
317
+ target_provider: Target provider
318
+ source_param: Source parameter name
319
+ target_param: Target parameter name
320
+ value: Original value
321
+
322
+ Returns:
323
+ Converted value
324
+ """
325
+ # FRED -> World Bank conversions
326
+ if source_provider == 'fred' and target_provider == 'worldbank':
327
+ if source_param == 'series_id':
328
+ # Try to map common FRED series to World Bank indicators
329
+ series_map = {
330
+ 'GDP': 'NY.GDP.MKTP.CD',
331
+ 'GDPC1': 'NY.GDP.MKTP.KD',
332
+ 'UNRATE': 'SL.UEM.TOTL.NE.ZS',
333
+ 'CPIAUCSL': 'FP.CPI.TOTL',
334
+ 'CPILFESL': 'FP.CPI.TOTL'
335
+ }
336
+ return series_map.get(value, value)
337
+
338
+ elif source_param in ['observation_start', 'observation_end']:
339
+ # Convert to World Bank date format (year or year:year)
340
+ # Extract year from date string
341
+ if isinstance(value, str) and len(value) >= 4:
342
+ return value[:4]
343
+
344
+ # Census -> World Bank conversions
345
+ elif source_provider == 'census' and target_provider == 'worldbank':
346
+ if source_param == 'geography':
347
+ # Convert census geography to country code
348
+ if 'state' in str(value).lower():
349
+ return 'US' # US country code
350
+
351
+ return value
352
+
353
+ def _add_default_parameters(
354
+ self,
355
+ provider: str,
356
+ params: Dict[str, Any]
357
+ ) -> Dict[str, Any]:
358
+ """
359
+ Add default parameters required by provider.
360
+
361
+ Args:
362
+ provider: Provider name
363
+ params: Current parameters
364
+
365
+ Returns:
366
+ Parameters with defaults added
367
+ """
368
+ if provider == 'worldbank':
369
+ # World Bank needs country code
370
+ if 'country_code' not in params:
371
+ params['country_code'] = 'US' # Default to US
372
+
373
+ return params
374
+
375
+ def _update_stats(self, provider: str, operation: str, success: bool):
376
+ """
377
+ Update fallback success statistics.
378
+
379
+ Args:
380
+ provider: Provider name
381
+ operation: Operation name
382
+ success: Whether attempt was successful
383
+ """
384
+ key = f"{provider}.{operation}"
385
+
386
+ if key not in self.fallback_stats:
387
+ self.fallback_stats[key] = {
388
+ 'attempts': 0,
389
+ 'successes': 0,
390
+ 'failures': 0
391
+ }
392
+
393
+ self.fallback_stats[key]['attempts'] += 1
394
+ if success:
395
+ self.fallback_stats[key]['successes'] += 1
396
+ else:
397
+ self.fallback_stats[key]['failures'] += 1
398
+
399
+ def get_fallback_stats(self) -> Dict[str, Any]:
400
+ """
401
+ Get fallback statistics.
402
+
403
+ Returns:
404
+ Dictionary with fallback statistics
405
+ """
406
+ stats = {}
407
+ for key, data in self.fallback_stats.items():
408
+ success_rate = (
409
+ data['successes'] / data['attempts']
410
+ if data['attempts'] > 0 else 0.0
411
+ )
412
+ stats[key] = {
413
+ 'attempts': data['attempts'],
414
+ 'successes': data['successes'],
415
+ 'failures': data['failures'],
416
+ 'success_rate': round(success_rate, 3)
417
+ }
418
+
419
+ return stats
420
+