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,684 @@
1
+ """
2
+ Base API Provider Interface
3
+
4
+ Abstract base class for all API data source providers in the API Source Tool.
5
+ Provides common functionality for rate limiting, caching, error handling, and metadata.
6
+
7
+ Enhanced with:
8
+ - Detailed metrics and health monitoring
9
+ - Smart error handling with retries
10
+ - Data quality assessment
11
+ - Comprehensive metadata with quality scores
12
+ - Operation exposure for AI agent visibility
13
+ """
14
+
15
+ import logging
16
+ import time
17
+ from abc import ABC, abstractmethod
18
+ from collections import deque
19
+ from datetime import datetime, timedelta
20
+ from threading import Lock
21
+ from typing import Any, Dict, List, Optional, Tuple
22
+
23
+ from aiecs.tools.apisource.monitoring.metrics import DetailedMetrics
24
+ from aiecs.tools.apisource.reliability.error_handler import SmartErrorHandler
25
+ from aiecs.tools.apisource.utils.validators import DataValidator
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def expose_operation(operation_name: str, description: str):
31
+ """
32
+ Decorator: Mark provider operations that should be exposed to AI agents.
33
+
34
+ This decorator allows provider operations to be automatically discovered by the
35
+ LangChain adapter and exposed as individual tools to AI agents, providing
36
+ fine-grained visibility into provider capabilities.
37
+
38
+ Args:
39
+ operation_name: The name of the operation (e.g., 'get_series_observations')
40
+ description: Human-readable description of what the operation does
41
+
42
+ Returns:
43
+ Decorated function with metadata for operation discovery
44
+
45
+ Example:
46
+ @expose_operation(
47
+ operation_name='get_series_observations',
48
+ description='Get FRED economic time series data'
49
+ )
50
+ def get_series_observations(self, series_id: str, ...):
51
+ pass
52
+ """
53
+ def decorator(func):
54
+ func._exposed_operation = True
55
+ func._operation_name = operation_name
56
+ func._operation_description = description
57
+ return func
58
+ return decorator
59
+
60
+
61
+ class RateLimiter:
62
+ """Token bucket rate limiter for API requests"""
63
+
64
+ def __init__(self, tokens_per_second: float = 1.0, max_tokens: int = 10):
65
+ """
66
+ Initialize rate limiter with token bucket algorithm.
67
+
68
+ Args:
69
+ tokens_per_second: Rate at which tokens are added to the bucket
70
+ max_tokens: Maximum number of tokens the bucket can hold
71
+ """
72
+ self.tokens_per_second = tokens_per_second
73
+ self.max_tokens = max_tokens
74
+ self.tokens = max_tokens
75
+ self.last_update = time.time()
76
+ self.lock = Lock()
77
+
78
+ def acquire(self, tokens: int = 1) -> bool:
79
+ """
80
+ Acquire tokens from the bucket.
81
+
82
+ Args:
83
+ tokens: Number of tokens to acquire
84
+
85
+ Returns:
86
+ True if tokens were acquired, False otherwise
87
+ """
88
+ with self.lock:
89
+ now = time.time()
90
+ elapsed = now - self.last_update
91
+
92
+ # Add new tokens based on elapsed time
93
+ self.tokens = min(
94
+ self.max_tokens,
95
+ self.tokens + elapsed * self.tokens_per_second
96
+ )
97
+ self.last_update = now
98
+
99
+ if self.tokens >= tokens:
100
+ self.tokens -= tokens
101
+ return True
102
+ return False
103
+
104
+ def wait(self, tokens: int = 1, timeout: float = 30.0) -> bool:
105
+ """
106
+ Wait until tokens are available.
107
+
108
+ Args:
109
+ tokens: Number of tokens to acquire
110
+ timeout: Maximum time to wait in seconds
111
+
112
+ Returns:
113
+ True if tokens were acquired, False if timeout
114
+ """
115
+ start_time = time.time()
116
+ while time.time() - start_time < timeout:
117
+ if self.acquire(tokens):
118
+ return True
119
+ time.sleep(0.1)
120
+ return False
121
+
122
+
123
+ class BaseAPIProvider(ABC):
124
+ """
125
+ Abstract base class for all API data source providers.
126
+
127
+ Provides:
128
+ - Rate limiting with token bucket algorithm
129
+ - Standardized error handling
130
+ - Metadata about provider capabilities
131
+ - Parameter validation
132
+ - Response formatting
133
+ """
134
+
135
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
136
+ """
137
+ Initialize the API provider.
138
+
139
+ Args:
140
+ config: Configuration dictionary with API keys, rate limits, etc.
141
+ """
142
+ self.config = config or {}
143
+ self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
144
+
145
+ # Initialize rate limiter
146
+ rate_limit = self.config.get('rate_limit', 10) # requests per second
147
+ max_burst = self.config.get('max_burst', 20)
148
+ self.rate_limiter = RateLimiter(
149
+ tokens_per_second=rate_limit,
150
+ max_tokens=max_burst
151
+ )
152
+
153
+ # Initialize detailed metrics
154
+ self.metrics = DetailedMetrics(max_response_times=100)
155
+
156
+ # Initialize smart error handler
157
+ self.error_handler = SmartErrorHandler(
158
+ max_retries=self.config.get('max_retries', 3),
159
+ backoff_factor=self.config.get('backoff_factor', 2.0),
160
+ initial_delay=self.config.get('initial_delay', 1.0),
161
+ max_delay=self.config.get('max_delay', 30.0)
162
+ )
163
+
164
+ # Initialize data validator
165
+ self.validator = DataValidator()
166
+
167
+ # Legacy stats for backwards compatibility
168
+ self.stats = {
169
+ 'total_requests': 0,
170
+ 'successful_requests': 0,
171
+ 'failed_requests': 0,
172
+ 'last_request_time': None
173
+ }
174
+ self.stats_lock = Lock()
175
+
176
+ @property
177
+ @abstractmethod
178
+ def name(self) -> str:
179
+ """Provider name (e.g., 'fred', 'worldbank')"""
180
+ pass
181
+
182
+ @property
183
+ @abstractmethod
184
+ def description(self) -> str:
185
+ """Human-readable description of the provider"""
186
+ pass
187
+
188
+ @property
189
+ @abstractmethod
190
+ def supported_operations(self) -> List[str]:
191
+ """List of supported operation names"""
192
+ pass
193
+
194
+ @abstractmethod
195
+ def validate_params(self, operation: str, params: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
196
+ """
197
+ Validate parameters for a specific operation.
198
+
199
+ Args:
200
+ operation: Operation name
201
+ params: Parameters to validate
202
+
203
+ Returns:
204
+ Tuple of (is_valid, error_message)
205
+ """
206
+ pass
207
+
208
+ @abstractmethod
209
+ def fetch(self, operation: str, params: Dict[str, Any]) -> Dict[str, Any]:
210
+ """
211
+ Fetch data from the API.
212
+
213
+ Args:
214
+ operation: Operation to perform
215
+ params: Operation parameters
216
+
217
+ Returns:
218
+ Response data in standardized format
219
+
220
+ Raises:
221
+ ValueError: If operation is not supported
222
+ Exception: If API request fails
223
+ """
224
+ pass
225
+
226
+ def get_metadata(self) -> Dict[str, Any]:
227
+ """
228
+ Get provider metadata including health status and detailed metrics.
229
+
230
+ Returns:
231
+ Dictionary with comprehensive provider information
232
+ """
233
+ return {
234
+ 'name': self.name,
235
+ 'description': self.description,
236
+ 'operations': self.supported_operations,
237
+ 'stats': self.metrics.get_summary(), # Use detailed metrics
238
+ 'health': {
239
+ 'score': self.metrics.get_health_score(),
240
+ 'status': 'healthy' if self.metrics.get_health_score() > 0.7 else 'degraded'
241
+ },
242
+ 'config': {
243
+ 'rate_limit': self.config.get('rate_limit', 10),
244
+ 'timeout': self.config.get('timeout', 30),
245
+ 'max_retries': self.config.get('max_retries', 3)
246
+ }
247
+ }
248
+
249
+ def get_operation_schema(self, operation: str) -> Optional[Dict[str, Any]]:
250
+ """
251
+ Get schema for a specific operation.
252
+
253
+ Args:
254
+ operation: Operation name
255
+
256
+ Returns:
257
+ Schema dictionary or None if not available
258
+ """
259
+ # Override in subclass to provide operation-specific schemas
260
+ return None
261
+
262
+ @classmethod
263
+ def get_exposed_operations(cls) -> List[Dict[str, Any]]:
264
+ """
265
+ Get all operations that are exposed to AI agents via the @expose_operation decorator.
266
+
267
+ This method discovers all methods decorated with @expose_operation and returns
268
+ their metadata along with their schemas. This enables the LangChain adapter to
269
+ automatically create individual tools for each provider operation.
270
+
271
+ Returns:
272
+ List of operation dictionaries, each containing:
273
+ - name: Operation name
274
+ - description: Operation description
275
+ - schema: Operation schema (parameters, types, descriptions)
276
+ - method_name: The actual method name on the class
277
+
278
+ Example:
279
+ >>> FREDProvider.get_exposed_operations()
280
+ [
281
+ {
282
+ 'name': 'get_series_observations',
283
+ 'description': 'Get FRED economic time series data',
284
+ 'schema': {...},
285
+ 'method_name': 'get_series_observations'
286
+ },
287
+ ...
288
+ ]
289
+ """
290
+ operations = []
291
+
292
+ # Create a temporary instance to access get_operation_schema
293
+ # We need this because get_operation_schema might be an instance method
294
+ try:
295
+ # Try to get schema without instantiation first
296
+ for attr_name in dir(cls):
297
+ # Skip private and special methods
298
+ if attr_name.startswith('_'):
299
+ continue
300
+
301
+ try:
302
+ attr = getattr(cls, attr_name)
303
+ except AttributeError:
304
+ continue
305
+
306
+ # Check if this is an exposed operation
307
+ if callable(attr) and hasattr(attr, '_exposed_operation'):
308
+ operation_name = attr._operation_name
309
+ operation_description = attr._operation_description
310
+
311
+ # Try to get schema - this might require instantiation
312
+ schema = None
313
+ if hasattr(cls, 'get_operation_schema'):
314
+ try:
315
+ # Try calling as class method first
316
+ schema = cls.get_operation_schema(cls, operation_name)
317
+ except (TypeError, AttributeError):
318
+ # If that fails, we'll need to handle it at runtime
319
+ logger.debug(f"Could not get schema for {operation_name} at class level")
320
+
321
+ operations.append({
322
+ 'name': operation_name,
323
+ 'description': operation_description,
324
+ 'schema': schema,
325
+ 'method_name': attr_name
326
+ })
327
+
328
+ logger.debug(f"Discovered exposed operation: {operation_name} from {cls.__name__}")
329
+
330
+ except Exception as e:
331
+ logger.warning(f"Error discovering exposed operations for {cls.__name__}: {e}")
332
+
333
+ return operations
334
+
335
+ def validate_and_clean_data(
336
+ self,
337
+ operation: str,
338
+ raw_data: Any
339
+ ) -> Dict[str, Any]:
340
+ """
341
+ Validate and clean data (optional, override in subclass).
342
+
343
+ Providers can implement custom validation logic for their specific data formats.
344
+
345
+ Args:
346
+ operation: Operation that produced the data
347
+ raw_data: Raw data from API
348
+
349
+ Returns:
350
+ Dictionary with:
351
+ - data: Cleaned data
352
+ - validation_warnings: List of warnings
353
+ - statistics: Data quality statistics
354
+ """
355
+ # Default implementation: no validation
356
+ return {
357
+ 'data': raw_data,
358
+ 'validation_warnings': [],
359
+ 'statistics': {}
360
+ }
361
+
362
+ def calculate_data_quality(
363
+ self,
364
+ operation: str,
365
+ data: Any,
366
+ response_time_ms: float
367
+ ) -> Dict[str, Any]:
368
+ """
369
+ Calculate quality metadata for the response.
370
+
371
+ Can be overridden by providers for custom quality assessment.
372
+
373
+ Args:
374
+ operation: Operation performed
375
+ data: Response data
376
+ response_time_ms: Response time in milliseconds
377
+
378
+ Returns:
379
+ Quality metadata dictionary
380
+ """
381
+ quality = {
382
+ 'score': 0.7, # Default quality score
383
+ 'completeness': 1.0, # Assume complete unless validated otherwise
384
+ 'freshness_hours': None, # Unknown freshness
385
+ 'confidence': 0.8, # Default confidence
386
+ 'authority_level': 'verified' # Provider is verified
387
+ }
388
+
389
+ # Adjust score based on response time
390
+ if response_time_ms < 500:
391
+ quality['score'] = min(quality['score'] + 0.1, 1.0)
392
+ elif response_time_ms > 5000:
393
+ quality['score'] = max(quality['score'] - 0.1, 0.0)
394
+
395
+ # Check if data is empty
396
+ if data is None:
397
+ quality['completeness'] = 0.0
398
+ quality['score'] = 0.0
399
+ elif isinstance(data, list) and len(data) == 0:
400
+ quality['completeness'] = 0.0
401
+ quality['score'] = max(quality['score'] - 0.3, 0.0)
402
+
403
+ return quality
404
+
405
+ def _update_stats(self, success: bool):
406
+ """Update request statistics"""
407
+ with self.stats_lock:
408
+ self.stats['total_requests'] += 1
409
+ if success:
410
+ self.stats['successful_requests'] += 1
411
+ else:
412
+ self.stats['failed_requests'] += 1
413
+ self.stats['last_request_time'] = datetime.utcnow().isoformat()
414
+
415
+ def _format_response(
416
+ self,
417
+ operation: str,
418
+ data: Any,
419
+ source: Optional[str] = None,
420
+ response_time_ms: Optional[float] = None,
421
+ validation_result: Optional[Dict[str, Any]] = None
422
+ ) -> Dict[str, Any]:
423
+ """
424
+ Format response in standardized format with enhanced metadata.
425
+
426
+ Args:
427
+ operation: Operation that was performed
428
+ data: Response data
429
+ source: Data source URL or identifier
430
+ response_time_ms: Response time in milliseconds
431
+ validation_result: Optional validation result from validate_and_clean_data
432
+
433
+ Returns:
434
+ Standardized response dictionary with comprehensive metadata
435
+ """
436
+ # Calculate quality metadata
437
+ quality = self.calculate_data_quality(operation, data, response_time_ms or 0)
438
+
439
+ # Calculate coverage information
440
+ coverage = self._calculate_coverage(data)
441
+
442
+ # Build metadata
443
+ metadata = {
444
+ 'timestamp': datetime.utcnow().isoformat(),
445
+ 'source': source or f'{self.name} API',
446
+ 'quality': quality,
447
+ 'coverage': coverage
448
+ }
449
+
450
+ # Add API info if response time provided
451
+ if response_time_ms is not None:
452
+ metadata['api_info'] = {
453
+ 'response_time_ms': round(response_time_ms, 2),
454
+ 'provider': self.name
455
+ }
456
+
457
+ # Add validation warnings if present
458
+ if validation_result and validation_result.get('validation_warnings'):
459
+ metadata['validation_warnings'] = validation_result['validation_warnings']
460
+
461
+ # Add statistics if present
462
+ if validation_result and validation_result.get('statistics'):
463
+ metadata['statistics'] = validation_result['statistics']
464
+
465
+ return {
466
+ 'provider': self.name,
467
+ 'operation': operation,
468
+ 'data': data,
469
+ 'metadata': metadata
470
+ }
471
+
472
+ def _calculate_coverage(self, data: Any) -> Dict[str, Any]:
473
+ """
474
+ Calculate data coverage information.
475
+
476
+ Args:
477
+ data: Response data
478
+
479
+ Returns:
480
+ Coverage information dictionary
481
+ """
482
+ coverage = {}
483
+
484
+ # Calculate record count
485
+ if isinstance(data, list):
486
+ coverage['total_records'] = len(data)
487
+
488
+ # Try to extract date range from time series data
489
+ if len(data) > 0 and isinstance(data[0], dict):
490
+ date_fields = ['date', 'observation_date', 'timestamp']
491
+ for date_field in date_fields:
492
+ if date_field in data[0]:
493
+ dates = [
494
+ item.get(date_field) for item in data
495
+ if date_field in item and item.get(date_field)
496
+ ]
497
+ if dates:
498
+ try:
499
+ # Sort to get earliest and latest
500
+ dates_sorted = sorted(dates)
501
+ coverage['start_date'] = dates_sorted[0]
502
+ coverage['end_date'] = dates_sorted[-1]
503
+
504
+ # Try to infer frequency
505
+ frequency = self.validator.infer_data_frequency(
506
+ data, date_field
507
+ )
508
+ if frequency:
509
+ coverage['frequency'] = frequency
510
+ except Exception:
511
+ pass
512
+ break
513
+ elif isinstance(data, dict):
514
+ # For dict responses
515
+ if 'articles' in data:
516
+ coverage['total_records'] = len(data['articles'])
517
+ elif 'total_results' in data:
518
+ coverage['total_results'] = data['total_results']
519
+ else:
520
+ coverage['total_records'] = 1
521
+ else:
522
+ coverage['total_records'] = 1 if data is not None else 0
523
+
524
+ return coverage
525
+
526
+ def _get_api_key(self, key_name: Optional[str] = None) -> Optional[str]:
527
+ """
528
+ Get API key from config or environment.
529
+
530
+ Args:
531
+ key_name: Specific key name to retrieve
532
+
533
+ Returns:
534
+ API key or None if not found
535
+ """
536
+ import os
537
+
538
+ # Try config first
539
+ if 'api_key' in self.config:
540
+ return self.config['api_key']
541
+
542
+ # Try environment variable
543
+ env_var = key_name or f'{self.name.upper()}_API_KEY'
544
+ return os.getenv(env_var)
545
+
546
+ def execute(self, operation: str, params: Dict[str, Any]) -> Dict[str, Any]:
547
+ """
548
+ Execute an operation with rate limiting, error handling, and metrics tracking.
549
+
550
+ Args:
551
+ operation: Operation to perform
552
+ params: Operation parameters
553
+
554
+ Returns:
555
+ Response data with enhanced metadata
556
+
557
+ Raises:
558
+ ValueError: If operation is invalid or parameters are invalid
559
+ Exception: If API request fails after all retries
560
+ """
561
+ # Validate operation
562
+ if operation not in self.supported_operations:
563
+ available_ops = ', '.join(self.supported_operations)
564
+ schema = self.get_operation_schema(operation)
565
+ error_msg = (
566
+ f"Operation '{operation}' not supported by {self.name}.\n"
567
+ f"Supported operations: {available_ops}"
568
+ )
569
+ if schema:
570
+ error_msg += f"\nSee get_operation_schema('{operation}') for details"
571
+ raise ValueError(error_msg)
572
+
573
+ # Validate parameters with enhanced error messages
574
+ is_valid, error_msg = self.validate_params(operation, params)
575
+ if not is_valid:
576
+ schema = self.get_operation_schema(operation)
577
+ enhanced_error = f"Invalid parameters for {self.name}.{operation}: {error_msg}"
578
+
579
+ if schema and 'parameters' in schema:
580
+ # Add helpful parameter information
581
+ required_params = [
582
+ name for name, info in schema['parameters'].items()
583
+ if info.get('required', False)
584
+ ]
585
+ if required_params:
586
+ enhanced_error += f"\nRequired parameters: {', '.join(required_params)}"
587
+
588
+ # Add examples if available
589
+ if 'examples' in schema and schema['examples']:
590
+ example = schema['examples'][0]
591
+ enhanced_error += f"\nExample: {example.get('params', {})}"
592
+
593
+ raise ValueError(enhanced_error)
594
+
595
+ # Apply rate limiting
596
+ wait_start = time.time()
597
+ if not self.rate_limiter.wait(tokens=1, timeout=30):
598
+ self.metrics.record_request(
599
+ success=False,
600
+ response_time_ms=0,
601
+ error_type='rate_limit'
602
+ )
603
+ raise Exception(
604
+ f"Rate limit exceeded for {self.name}. "
605
+ "Please try again later or increase rate limits in config."
606
+ )
607
+
608
+ # Track rate limit wait time
609
+ wait_time_ms = (time.time() - wait_start) * 1000
610
+ if wait_time_ms > 100: # Only record significant waits
611
+ self.metrics.record_rate_limit_wait(wait_time_ms)
612
+
613
+ # Execute with smart retry logic
614
+ def fetch_operation():
615
+ """Wrapper for fetch with timing"""
616
+ start_time = time.time()
617
+ result = self.fetch(operation, params)
618
+ response_time_ms = (time.time() - start_time) * 1000
619
+ return result, response_time_ms
620
+
621
+ # Use error handler for retries
622
+ execution_result = self.error_handler.execute_with_retry(
623
+ operation_func=fetch_operation,
624
+ operation_name=operation,
625
+ provider_name=self.name
626
+ )
627
+
628
+ if execution_result['success']:
629
+ result, response_time_ms = execution_result['data']
630
+
631
+ # Calculate data size for metrics
632
+ data = result.get('data') if isinstance(result, dict) else result
633
+ record_count = len(data) if isinstance(data, list) else (1 if data else 0)
634
+
635
+ # Record success metrics
636
+ self.metrics.record_request(
637
+ success=True,
638
+ response_time_ms=response_time_ms,
639
+ record_count=record_count,
640
+ cached=False
641
+ )
642
+
643
+ # Update legacy stats
644
+ self._update_stats(success=True)
645
+
646
+ self.logger.info(
647
+ f"Successfully executed {self.name}.{operation} "
648
+ f"in {response_time_ms:.0f}ms ({record_count} records)"
649
+ )
650
+
651
+ return result
652
+ else:
653
+ # All retries failed
654
+ error_info = execution_result['error']
655
+ retry_info = execution_result['retry_info']
656
+
657
+ # Record failure metrics
658
+ self.metrics.record_request(
659
+ success=False,
660
+ response_time_ms=0,
661
+ error_type=error_info.get('type', 'unknown'),
662
+ error_message=error_info.get('message')
663
+ )
664
+
665
+ # Update legacy stats
666
+ self._update_stats(success=False)
667
+
668
+ # Build comprehensive error message
669
+ error_msg = (
670
+ f"Failed to execute {self.name}.{operation} after "
671
+ f"{retry_info['attempts']} attempts.\n"
672
+ f"Error: {error_info['message']}"
673
+ )
674
+
675
+ # Add recovery suggestions
676
+ if retry_info.get('recovery_suggestions'):
677
+ error_msg += "\n\nSuggestions:"
678
+ for suggestion in retry_info['recovery_suggestions'][:3]:
679
+ error_msg += f"\n - {suggestion}"
680
+
681
+ self.logger.error(error_msg)
682
+
683
+ raise Exception(error_msg)
684
+