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,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
|
+
|