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
@@ -13,6 +13,7 @@ from contextlib import contextmanager
13
13
 
14
14
  from cachetools import LRUCache
15
15
  from aiecs.utils.execution_utils import ExecutionUtils
16
+ from aiecs.utils.cache_provider import ICacheProvider, LRUCacheProvider
16
17
  import re
17
18
  from pydantic import BaseModel, ValidationError, ConfigDict
18
19
 
@@ -58,6 +59,10 @@ class ExecutorConfig(BaseModel):
58
59
  retry_attempts (int): Number of retry attempts for transient errors.
59
60
  retry_backoff (float): Backoff factor for retries.
60
61
  timeout (int): Timeout for operations in seconds.
62
+ enable_dual_cache (bool): Enable dual-layer caching (L1: LRU + L2: Redis).
63
+ enable_redis_cache (bool): Enable Redis as L2 cache (requires enable_dual_cache=True).
64
+ redis_cache_ttl (int): Redis cache TTL in seconds (for L2 cache).
65
+ l1_cache_ttl (int): L1 cache TTL in seconds (for dual-layer cache).
61
66
  """
62
67
  enable_cache: bool = True
63
68
  cache_size: int = 100
@@ -73,6 +78,12 @@ class ExecutorConfig(BaseModel):
73
78
  retry_backoff: float = 1.0
74
79
  timeout: int = 30
75
80
 
81
+ # Dual-layer cache configuration
82
+ enable_dual_cache: bool = False
83
+ enable_redis_cache: bool = False
84
+ redis_cache_ttl: int = 86400 # 1 day
85
+ l1_cache_ttl: int = 300 # 5 minutes
86
+
76
87
  model_config = ConfigDict(env_prefix="TOOL_EXECUTOR_")
77
88
 
78
89
  # Metrics counter
@@ -157,6 +168,93 @@ def cache_result(ttl: Optional[int] = None) -> Callable:
157
168
  return wrapper
158
169
  return decorator
159
170
 
171
+
172
+ def cache_result_with_strategy(ttl_strategy: Optional[Union[int, Callable]] = None) -> Callable:
173
+ """
174
+ Decorator to cache function results with flexible TTL strategy.
175
+
176
+ Supports multiple TTL strategy types:
177
+ 1. Fixed TTL (int): Static TTL in seconds
178
+ 2. Callable strategy: Function that calculates TTL based on result and context
179
+ 3. None: Use default TTL from executor config
180
+
181
+ Args:
182
+ ttl_strategy: TTL strategy, can be:
183
+ - int: Fixed TTL in seconds
184
+ - Callable[[Any, tuple, dict], int]: Function(result, args, kwargs) -> ttl_seconds
185
+ - None: Use default TTL
186
+
187
+ Returns:
188
+ Callable: Decorated function with intelligent caching.
189
+
190
+ Example:
191
+ # Fixed TTL
192
+ @cache_result_with_strategy(ttl_strategy=3600)
193
+ def simple_operation(self, data):
194
+ return process(data)
195
+
196
+ # Dynamic TTL based on result
197
+ def calculate_ttl(result, args, kwargs):
198
+ if result.get('type') == 'static':
199
+ return 86400 # 1 day
200
+ return 3600 # 1 hour
201
+
202
+ @cache_result_with_strategy(ttl_strategy=calculate_ttl)
203
+ def smart_operation(self, query):
204
+ return search(query)
205
+ """
206
+ def decorator(func: Callable) -> Callable:
207
+ @functools.wraps(func)
208
+ def wrapper(self, *args, **kwargs):
209
+ if not hasattr(self, '_executor') or not self._executor.config.enable_cache:
210
+ return func(self, *args, **kwargs)
211
+
212
+ # Generate cache key
213
+ cache_key = self._executor._get_cache_key(func.__name__, args, kwargs)
214
+
215
+ # Check cache
216
+ cached = self._executor._get_from_cache(cache_key)
217
+ if cached is not None:
218
+ logger.debug(f"Cache hit for {func.__name__}")
219
+ self._executor._metrics.record_cache_hit()
220
+ return cached
221
+
222
+ # Execute function
223
+ result = func(self, *args, **kwargs)
224
+
225
+ # Calculate TTL based on strategy
226
+ # Support both regular callables and lambdas that need self
227
+ if callable(ttl_strategy):
228
+ try:
229
+ # Try calling with self first (for lambda self, result, args, kwargs)
230
+ import inspect
231
+ sig = inspect.signature(ttl_strategy)
232
+ if len(sig.parameters) == 4: # self, result, args, kwargs
233
+ ttl = ttl_strategy(self, result, args, kwargs)
234
+ else: # result, args, kwargs
235
+ ttl = ttl_strategy(result, args, kwargs)
236
+
237
+ if not isinstance(ttl, int) or ttl < 0:
238
+ logger.warning(
239
+ f"TTL strategy returned invalid value: {ttl}. "
240
+ f"Expected positive integer. Using default TTL."
241
+ )
242
+ ttl = None
243
+ except Exception as e:
244
+ logger.error(f"Error calculating TTL from strategy: {e}. Using default TTL.")
245
+ ttl = None
246
+ else:
247
+ ttl = self._executor._calculate_ttl_from_strategy(
248
+ ttl_strategy, result, args, kwargs
249
+ )
250
+
251
+ # Cache with calculated TTL
252
+ self._executor._add_to_cache(cache_key, result, ttl)
253
+ return result
254
+
255
+ return wrapper
256
+ return decorator
257
+
160
258
  def run_in_executor(func: Callable) -> Callable:
161
259
  """
162
260
  Decorator to run a synchronous function in the thread pool executor.
@@ -240,12 +338,13 @@ class ToolExecutor:
240
338
  executor = ToolExecutor(config={'max_workers': 8})
241
339
  result = executor.execute(tool_instance, 'operation_name', param1='value')
242
340
  """
243
- def __init__(self, config: Optional[Dict[str, Any]] = None):
341
+ def __init__(self, config: Optional[Dict[str, Any]] = None, cache_provider: Optional[ICacheProvider] = None):
244
342
  """
245
343
  Initialize the executor with optional configuration.
246
344
 
247
345
  Args:
248
346
  config (Dict[str, Any], optional): Configuration overrides for ExecutorConfig.
347
+ cache_provider (ICacheProvider, optional): Custom cache provider. If None, uses default based on config.
249
348
 
250
349
  Raises:
251
350
  ValueError: If config is invalid.
@@ -265,6 +364,66 @@ class ToolExecutor:
265
364
  retry_backoff=self.config.retry_backoff
266
365
  )
267
366
 
367
+ # Support pluggable cache provider
368
+ if cache_provider is not None:
369
+ # User provided custom cache provider
370
+ self.cache_provider = cache_provider
371
+ logger.info(f"Using custom cache provider: {cache_provider.__class__.__name__}")
372
+ elif self.config.enable_dual_cache and self.config.enable_redis_cache:
373
+ # Enable dual-layer cache (L1: LRU + L2: Redis)
374
+ self.cache_provider = self._initialize_dual_cache()
375
+ else:
376
+ # Default: use LRUCacheProvider wrapping ExecutionUtils
377
+ self.cache_provider = LRUCacheProvider(self.execution_utils)
378
+ logger.debug("Using default LRUCacheProvider")
379
+
380
+ def _initialize_dual_cache(self) -> ICacheProvider:
381
+ """
382
+ Initialize dual-layer cache (L1: LRU + L2: Redis).
383
+
384
+ Returns:
385
+ DualLayerCacheProvider instance or fallback to LRUCacheProvider
386
+ """
387
+ try:
388
+ from aiecs.utils.cache_provider import DualLayerCacheProvider, RedisCacheProvider
389
+
390
+ # Create L1 cache (LRU)
391
+ l1_cache = LRUCacheProvider(self.execution_utils)
392
+
393
+ # Create L2 cache (Redis) - this requires async initialization
394
+ # We'll use a lazy initialization approach
395
+ try:
396
+ # Try to get global Redis client synchronously
397
+ # Note: This assumes Redis client is already initialized
398
+ from aiecs.infrastructure.persistence import redis_client
399
+
400
+ if redis_client is not None:
401
+ l2_cache = RedisCacheProvider(
402
+ redis_client,
403
+ prefix="tool_executor:",
404
+ default_ttl=self.config.redis_cache_ttl
405
+ )
406
+
407
+ dual_cache = DualLayerCacheProvider(
408
+ l1_provider=l1_cache,
409
+ l2_provider=l2_cache,
410
+ l1_ttl=self.config.l1_cache_ttl
411
+ )
412
+
413
+ logger.info("Dual-layer cache enabled (L1: LRU + L2: Redis)")
414
+ return dual_cache
415
+ else:
416
+ logger.warning("Redis client not initialized, falling back to LRU cache")
417
+ return l1_cache
418
+
419
+ except ImportError:
420
+ logger.warning("Redis client not available, falling back to LRU cache")
421
+ return l1_cache
422
+
423
+ except Exception as e:
424
+ logger.warning(f"Failed to initialize dual-layer cache: {e}, falling back to LRU")
425
+ return LRUCacheProvider(self.execution_utils)
426
+
268
427
  def _get_cache_key(self, func_name: str, args: tuple, kwargs: Dict[str, Any]) -> str:
269
428
  """
270
429
  Generate a context-aware cache key from function name, user ID, task ID, and arguments.
@@ -281,9 +440,70 @@ class ToolExecutor:
281
440
  task_id = kwargs.get("task_id", "none")
282
441
  return self.execution_utils.generate_cache_key(func_name, user_id, task_id, args, kwargs)
283
442
 
443
+ def _calculate_ttl_from_strategy(
444
+ self,
445
+ ttl_strategy: Optional[Union[int, Callable]],
446
+ result: Any,
447
+ args: tuple,
448
+ kwargs: Dict[str, Any]
449
+ ) -> Optional[int]:
450
+ """
451
+ Calculate TTL based on the provided strategy.
452
+
453
+ Supports multiple strategy types:
454
+ 1. None: Use default TTL from config
455
+ 2. int: Fixed TTL in seconds
456
+ 3. Callable: Dynamic TTL calculation function
457
+
458
+ Args:
459
+ ttl_strategy: TTL strategy (None, int, or Callable)
460
+ result: Function execution result
461
+ args: Function positional arguments
462
+ kwargs: Function keyword arguments
463
+
464
+ Returns:
465
+ Optional[int]: Calculated TTL in seconds, or None for default
466
+
467
+ Example:
468
+ # Strategy function signature
469
+ def my_ttl_strategy(result: Any, args: tuple, kwargs: dict) -> int:
470
+ if result.get('type') == 'permanent':
471
+ return 86400 * 30 # 30 days
472
+ return 3600 # 1 hour
473
+ """
474
+ # Case 1: No strategy - use default
475
+ if ttl_strategy is None:
476
+ return None
477
+
478
+ # Case 2: Fixed TTL (integer)
479
+ if isinstance(ttl_strategy, int):
480
+ return ttl_strategy
481
+
482
+ # Case 3: Callable strategy - dynamic calculation
483
+ if callable(ttl_strategy):
484
+ try:
485
+ calculated_ttl = ttl_strategy(result, args, kwargs)
486
+ if not isinstance(calculated_ttl, int) or calculated_ttl < 0:
487
+ logger.warning(
488
+ f"TTL strategy returned invalid value: {calculated_ttl}. "
489
+ f"Expected positive integer. Using default TTL."
490
+ )
491
+ return None
492
+ return calculated_ttl
493
+ except Exception as e:
494
+ logger.error(f"Error calculating TTL from strategy: {e}. Using default TTL.")
495
+ return None
496
+
497
+ # Invalid strategy type
498
+ logger.warning(
499
+ f"Invalid TTL strategy type: {type(ttl_strategy)}. "
500
+ f"Expected None, int, or Callable. Using default TTL."
501
+ )
502
+ return None
503
+
284
504
  def _get_from_cache(self, cache_key: str) -> Optional[Any]:
285
505
  """
286
- Get a result from cache if it exists and is not expired.
506
+ Get a result from cache if it exists and is not expired (synchronous).
287
507
 
288
508
  Args:
289
509
  cache_key (str): Cache key.
@@ -293,11 +513,44 @@ class ToolExecutor:
293
513
  """
294
514
  if not self.config.enable_cache:
295
515
  return None
296
- return self.execution_utils.get_from_cache(cache_key)
516
+ return self.cache_provider.get(cache_key)
297
517
 
298
518
  def _add_to_cache(self, cache_key: str, result: Any, ttl: Optional[int] = None) -> None:
299
519
  """
300
- Add a result to the cache with optional TTL.
520
+ Add a result to the cache with optional TTL (synchronous).
521
+
522
+ Args:
523
+ cache_key (str): Cache key.
524
+ result (Any): Result to cache.
525
+ ttl (Optional[int]): Time-to-live in seconds.
526
+ """
527
+ if not self.config.enable_cache:
528
+ return
529
+ self.cache_provider.set(cache_key, result, ttl)
530
+
531
+ async def _get_from_cache_async(self, cache_key: str) -> Optional[Any]:
532
+ """
533
+ Get a result from cache if it exists and is not expired (asynchronous).
534
+
535
+ Args:
536
+ cache_key (str): Cache key.
537
+
538
+ Returns:
539
+ Optional[Any]: Cached result or None.
540
+ """
541
+ if not self.config.enable_cache:
542
+ return None
543
+
544
+ # Use async interface if available
545
+ if hasattr(self.cache_provider, 'get_async'):
546
+ return await self.cache_provider.get_async(cache_key)
547
+ else:
548
+ # Fallback to sync interface
549
+ return self.cache_provider.get(cache_key)
550
+
551
+ async def _add_to_cache_async(self, cache_key: str, result: Any, ttl: Optional[int] = None) -> None:
552
+ """
553
+ Add a result to the cache with optional TTL (asynchronous).
301
554
 
302
555
  Args:
303
556
  cache_key (str): Cache key.
@@ -306,7 +559,13 @@ class ToolExecutor:
306
559
  """
307
560
  if not self.config.enable_cache:
308
561
  return
309
- self.execution_utils.add_to_cache(cache_key, result, ttl)
562
+
563
+ # Use async interface if available
564
+ if hasattr(self.cache_provider, 'set_async'):
565
+ await self.cache_provider.set_async(cache_key, result, ttl)
566
+ else:
567
+ # Fallback to sync interface
568
+ self.cache_provider.set(cache_key, result, ttl)
310
569
 
311
570
  def get_lock(self, resource_id: str) -> threading.Lock:
312
571
  """
@@ -443,13 +702,13 @@ class ToolExecutor:
443
702
  for k, v in kwargs.items():
444
703
  if isinstance(v, str) and re.search(r'(\bSELECT\b|\bINSERT\b|--|;|/\*)', v, re.IGNORECASE):
445
704
  raise SecurityError(f"Input parameter '{k}' contains potentially malicious content")
446
- # Use cache if enabled
705
+ # Use cache if enabled (async)
447
706
  if self.config.enable_cache:
448
707
  cache_key = self._get_cache_key(operation, (), kwargs)
449
- cached_result = self._get_from_cache(cache_key)
708
+ cached_result = await self._get_from_cache_async(cache_key)
450
709
  if cached_result is not None:
451
710
  self._metrics.record_cache_hit()
452
- logger.debug(f"Cache hit for {operation}")
711
+ logger.debug(f"Cache hit for {operation} (async)")
453
712
  return cached_result
454
713
 
455
714
  async def _execute():
@@ -462,9 +721,9 @@ class ToolExecutor:
462
721
  if self.config.log_execution_time:
463
722
  logger.info(f"{tool_instance.__class__.__name__}.{operation} executed in {time.time() - start_time:.4f} seconds")
464
723
 
465
- # Cache result if enabled
724
+ # Cache result if enabled (async)
466
725
  if self.config.enable_cache:
467
- self._add_to_cache(cache_key, result)
726
+ await self._add_to_cache_async(cache_key, result)
468
727
  return result
469
728
  except Exception as e:
470
729
  self._metrics.record_failure()
@@ -499,20 +758,31 @@ class ToolExecutor:
499
758
  logger.error(f"Batch operation {operations[i]['op']} failed: {result}")
500
759
  return results
501
760
 
502
- # Singleton executor instance
761
+ # Singleton executor instance (for backward compatibility)
503
762
  _default_executor = None
504
763
 
505
764
  def get_executor(config: Optional[Dict[str, Any]] = None) -> ToolExecutor:
506
765
  """
507
- Get or create the default executor instance.
766
+ Get or create executor instance.
767
+
768
+ If config is provided, creates a new executor with that config.
769
+ If config is None, returns the default singleton executor.
508
770
 
509
771
  Args:
510
772
  config (Dict[str, Any], optional): Configuration overrides.
773
+ If provided, creates a new executor instance.
774
+ If None, returns the default singleton.
511
775
 
512
776
  Returns:
513
- ToolExecutor: Singleton executor instance.
777
+ ToolExecutor: Executor instance.
514
778
  """
515
779
  global _default_executor
780
+
781
+ # If config is provided, create a new executor with that config
782
+ if config is not None:
783
+ return ToolExecutor(config)
784
+
785
+ # Otherwise, return the default singleton
516
786
  if _default_executor is None:
517
- _default_executor = ToolExecutor(config)
787
+ _default_executor = ToolExecutor()
518
788
  return _default_executor
aiecs/utils/__init__.py CHANGED
@@ -5,16 +5,27 @@ This module provides utility functions including:
5
5
  - Prompt loading functionality
6
6
  - Token usage tracking
7
7
  - Execution utilities
8
+ - Cache provider interfaces and implementations
8
9
  """
9
10
 
10
11
  from .prompt_loader import get_prompt
11
12
  from .token_usage_repository import TokenUsageRepository
12
13
  from .execution_utils import ExecutionUtils
14
+ from .cache_provider import (
15
+ ICacheProvider,
16
+ LRUCacheProvider,
17
+ DualLayerCacheProvider,
18
+ RedisCacheProvider
19
+ )
13
20
 
14
21
  __all__ = [
15
22
  'get_prompt',
16
23
  'TokenUsageRepository',
17
24
  'ExecutionUtils',
25
+ 'ICacheProvider',
26
+ 'LRUCacheProvider',
27
+ 'DualLayerCacheProvider',
28
+ 'RedisCacheProvider',
18
29
  ]
19
30
 
20
31
  # Version information