aiecs 1.0.1__py3-none-any.whl → 1.7.6__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 +13 -16
- aiecs/__main__.py +7 -7
- aiecs/aiecs_client.py +269 -75
- aiecs/application/executors/operation_executor.py +79 -54
- aiecs/application/knowledge_graph/__init__.py +7 -0
- aiecs/application/knowledge_graph/builder/__init__.py +37 -0
- aiecs/application/knowledge_graph/builder/data_quality.py +302 -0
- aiecs/application/knowledge_graph/builder/data_reshaping.py +293 -0
- aiecs/application/knowledge_graph/builder/document_builder.py +369 -0
- aiecs/application/knowledge_graph/builder/graph_builder.py +490 -0
- aiecs/application/knowledge_graph/builder/import_optimizer.py +396 -0
- aiecs/application/knowledge_graph/builder/schema_inference.py +462 -0
- aiecs/application/knowledge_graph/builder/schema_mapping.py +563 -0
- aiecs/application/knowledge_graph/builder/structured_pipeline.py +1384 -0
- aiecs/application/knowledge_graph/builder/text_chunker.py +317 -0
- aiecs/application/knowledge_graph/extractors/__init__.py +27 -0
- aiecs/application/knowledge_graph/extractors/base.py +98 -0
- aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +422 -0
- aiecs/application/knowledge_graph/extractors/llm_relation_extractor.py +347 -0
- aiecs/application/knowledge_graph/extractors/ner_entity_extractor.py +241 -0
- aiecs/application/knowledge_graph/fusion/__init__.py +78 -0
- aiecs/application/knowledge_graph/fusion/ab_testing.py +395 -0
- aiecs/application/knowledge_graph/fusion/abbreviation_expander.py +327 -0
- aiecs/application/knowledge_graph/fusion/alias_index.py +597 -0
- aiecs/application/knowledge_graph/fusion/alias_matcher.py +384 -0
- aiecs/application/knowledge_graph/fusion/cache_coordinator.py +343 -0
- aiecs/application/knowledge_graph/fusion/entity_deduplicator.py +433 -0
- aiecs/application/knowledge_graph/fusion/entity_linker.py +511 -0
- aiecs/application/knowledge_graph/fusion/evaluation_dataset.py +240 -0
- aiecs/application/knowledge_graph/fusion/knowledge_fusion.py +632 -0
- aiecs/application/knowledge_graph/fusion/matching_config.py +489 -0
- aiecs/application/knowledge_graph/fusion/name_normalizer.py +352 -0
- aiecs/application/knowledge_graph/fusion/relation_deduplicator.py +183 -0
- aiecs/application/knowledge_graph/fusion/semantic_name_matcher.py +464 -0
- aiecs/application/knowledge_graph/fusion/similarity_pipeline.py +534 -0
- aiecs/application/knowledge_graph/pattern_matching/__init__.py +21 -0
- aiecs/application/knowledge_graph/pattern_matching/pattern_matcher.py +342 -0
- aiecs/application/knowledge_graph/pattern_matching/query_executor.py +366 -0
- aiecs/application/knowledge_graph/profiling/__init__.py +12 -0
- aiecs/application/knowledge_graph/profiling/query_plan_visualizer.py +195 -0
- aiecs/application/knowledge_graph/profiling/query_profiler.py +223 -0
- aiecs/application/knowledge_graph/reasoning/__init__.py +27 -0
- aiecs/application/knowledge_graph/reasoning/evidence_synthesis.py +341 -0
- aiecs/application/knowledge_graph/reasoning/inference_engine.py +500 -0
- aiecs/application/knowledge_graph/reasoning/logic_form_parser.py +163 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/__init__.py +79 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_builder.py +513 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_nodes.py +913 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_validator.py +866 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/error_handler.py +475 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/parser.py +396 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/query_context.py +208 -0
- aiecs/application/knowledge_graph/reasoning/logic_query_integration.py +170 -0
- aiecs/application/knowledge_graph/reasoning/query_planner.py +855 -0
- aiecs/application/knowledge_graph/reasoning/reasoning_engine.py +518 -0
- aiecs/application/knowledge_graph/retrieval/__init__.py +27 -0
- aiecs/application/knowledge_graph/retrieval/query_intent_classifier.py +211 -0
- aiecs/application/knowledge_graph/retrieval/retrieval_strategies.py +592 -0
- aiecs/application/knowledge_graph/retrieval/strategy_types.py +23 -0
- aiecs/application/knowledge_graph/search/__init__.py +59 -0
- aiecs/application/knowledge_graph/search/hybrid_search.py +457 -0
- aiecs/application/knowledge_graph/search/reranker.py +293 -0
- aiecs/application/knowledge_graph/search/reranker_strategies.py +535 -0
- aiecs/application/knowledge_graph/search/text_similarity.py +392 -0
- aiecs/application/knowledge_graph/traversal/__init__.py +15 -0
- aiecs/application/knowledge_graph/traversal/enhanced_traversal.py +305 -0
- aiecs/application/knowledge_graph/traversal/path_scorer.py +271 -0
- aiecs/application/knowledge_graph/validators/__init__.py +13 -0
- aiecs/application/knowledge_graph/validators/relation_validator.py +239 -0
- aiecs/application/knowledge_graph/visualization/__init__.py +11 -0
- aiecs/application/knowledge_graph/visualization/graph_visualizer.py +313 -0
- aiecs/common/__init__.py +9 -0
- aiecs/common/knowledge_graph/__init__.py +17 -0
- aiecs/common/knowledge_graph/runnable.py +471 -0
- aiecs/config/__init__.py +20 -5
- aiecs/config/config.py +762 -31
- aiecs/config/graph_config.py +131 -0
- aiecs/config/tool_config.py +399 -0
- aiecs/core/__init__.py +29 -13
- aiecs/core/interface/__init__.py +2 -2
- aiecs/core/interface/execution_interface.py +22 -22
- aiecs/core/interface/storage_interface.py +37 -88
- aiecs/core/registry/__init__.py +31 -0
- aiecs/core/registry/service_registry.py +92 -0
- aiecs/domain/__init__.py +270 -1
- aiecs/domain/agent/__init__.py +191 -0
- aiecs/domain/agent/base_agent.py +3870 -0
- aiecs/domain/agent/exceptions.py +99 -0
- aiecs/domain/agent/graph_aware_mixin.py +569 -0
- aiecs/domain/agent/hybrid_agent.py +1435 -0
- aiecs/domain/agent/integration/__init__.py +29 -0
- aiecs/domain/agent/integration/context_compressor.py +216 -0
- aiecs/domain/agent/integration/context_engine_adapter.py +587 -0
- aiecs/domain/agent/integration/protocols.py +281 -0
- aiecs/domain/agent/integration/retry_policy.py +218 -0
- aiecs/domain/agent/integration/role_config.py +213 -0
- aiecs/domain/agent/knowledge_aware_agent.py +1892 -0
- aiecs/domain/agent/lifecycle.py +291 -0
- aiecs/domain/agent/llm_agent.py +692 -0
- aiecs/domain/agent/memory/__init__.py +12 -0
- aiecs/domain/agent/memory/conversation.py +1124 -0
- aiecs/domain/agent/migration/__init__.py +14 -0
- aiecs/domain/agent/migration/conversion.py +163 -0
- aiecs/domain/agent/migration/legacy_wrapper.py +86 -0
- aiecs/domain/agent/models.py +884 -0
- aiecs/domain/agent/observability.py +479 -0
- aiecs/domain/agent/persistence.py +449 -0
- aiecs/domain/agent/prompts/__init__.py +29 -0
- aiecs/domain/agent/prompts/builder.py +159 -0
- aiecs/domain/agent/prompts/formatters.py +187 -0
- aiecs/domain/agent/prompts/template.py +255 -0
- aiecs/domain/agent/registry.py +253 -0
- aiecs/domain/agent/tool_agent.py +444 -0
- aiecs/domain/agent/tools/__init__.py +15 -0
- aiecs/domain/agent/tools/schema_generator.py +364 -0
- aiecs/domain/community/__init__.py +155 -0
- aiecs/domain/community/agent_adapter.py +469 -0
- aiecs/domain/community/analytics.py +432 -0
- aiecs/domain/community/collaborative_workflow.py +648 -0
- aiecs/domain/community/communication_hub.py +634 -0
- aiecs/domain/community/community_builder.py +320 -0
- aiecs/domain/community/community_integration.py +796 -0
- aiecs/domain/community/community_manager.py +803 -0
- aiecs/domain/community/decision_engine.py +849 -0
- aiecs/domain/community/exceptions.py +231 -0
- aiecs/domain/community/models/__init__.py +33 -0
- aiecs/domain/community/models/community_models.py +234 -0
- aiecs/domain/community/resource_manager.py +461 -0
- aiecs/domain/community/shared_context_manager.py +589 -0
- aiecs/domain/context/__init__.py +40 -10
- aiecs/domain/context/context_engine.py +1910 -0
- aiecs/domain/context/conversation_models.py +87 -53
- aiecs/domain/context/graph_memory.py +582 -0
- aiecs/domain/execution/model.py +12 -4
- aiecs/domain/knowledge_graph/__init__.py +19 -0
- aiecs/domain/knowledge_graph/models/__init__.py +52 -0
- aiecs/domain/knowledge_graph/models/entity.py +148 -0
- aiecs/domain/knowledge_graph/models/evidence.py +178 -0
- aiecs/domain/knowledge_graph/models/inference_rule.py +184 -0
- aiecs/domain/knowledge_graph/models/path.py +171 -0
- aiecs/domain/knowledge_graph/models/path_pattern.py +171 -0
- aiecs/domain/knowledge_graph/models/query.py +261 -0
- aiecs/domain/knowledge_graph/models/query_plan.py +181 -0
- aiecs/domain/knowledge_graph/models/relation.py +202 -0
- aiecs/domain/knowledge_graph/schema/__init__.py +23 -0
- aiecs/domain/knowledge_graph/schema/entity_type.py +131 -0
- aiecs/domain/knowledge_graph/schema/graph_schema.py +253 -0
- aiecs/domain/knowledge_graph/schema/property_schema.py +143 -0
- aiecs/domain/knowledge_graph/schema/relation_type.py +163 -0
- aiecs/domain/knowledge_graph/schema/schema_manager.py +691 -0
- aiecs/domain/knowledge_graph/schema/type_enums.py +209 -0
- aiecs/domain/task/dsl_processor.py +172 -56
- aiecs/domain/task/model.py +20 -8
- aiecs/domain/task/task_context.py +27 -24
- aiecs/infrastructure/__init__.py +0 -2
- aiecs/infrastructure/graph_storage/__init__.py +11 -0
- aiecs/infrastructure/graph_storage/base.py +837 -0
- aiecs/infrastructure/graph_storage/batch_operations.py +458 -0
- aiecs/infrastructure/graph_storage/cache.py +424 -0
- aiecs/infrastructure/graph_storage/distributed.py +223 -0
- aiecs/infrastructure/graph_storage/error_handling.py +380 -0
- aiecs/infrastructure/graph_storage/graceful_degradation.py +294 -0
- aiecs/infrastructure/graph_storage/health_checks.py +378 -0
- aiecs/infrastructure/graph_storage/in_memory.py +1197 -0
- aiecs/infrastructure/graph_storage/index_optimization.py +446 -0
- aiecs/infrastructure/graph_storage/lazy_loading.py +431 -0
- aiecs/infrastructure/graph_storage/metrics.py +344 -0
- aiecs/infrastructure/graph_storage/migration.py +400 -0
- aiecs/infrastructure/graph_storage/pagination.py +483 -0
- aiecs/infrastructure/graph_storage/performance_monitoring.py +456 -0
- aiecs/infrastructure/graph_storage/postgres.py +1563 -0
- aiecs/infrastructure/graph_storage/property_storage.py +353 -0
- aiecs/infrastructure/graph_storage/protocols.py +76 -0
- aiecs/infrastructure/graph_storage/query_optimizer.py +642 -0
- aiecs/infrastructure/graph_storage/schema_cache.py +290 -0
- aiecs/infrastructure/graph_storage/sqlite.py +1373 -0
- aiecs/infrastructure/graph_storage/streaming.py +487 -0
- aiecs/infrastructure/graph_storage/tenant.py +412 -0
- aiecs/infrastructure/messaging/celery_task_manager.py +92 -54
- aiecs/infrastructure/messaging/websocket_manager.py +51 -35
- aiecs/infrastructure/monitoring/__init__.py +22 -0
- aiecs/infrastructure/monitoring/executor_metrics.py +45 -11
- aiecs/infrastructure/monitoring/global_metrics_manager.py +212 -0
- aiecs/infrastructure/monitoring/structured_logger.py +3 -7
- aiecs/infrastructure/monitoring/tracing_manager.py +63 -35
- aiecs/infrastructure/persistence/__init__.py +14 -1
- aiecs/infrastructure/persistence/context_engine_client.py +184 -0
- aiecs/infrastructure/persistence/database_manager.py +67 -43
- aiecs/infrastructure/persistence/file_storage.py +180 -103
- aiecs/infrastructure/persistence/redis_client.py +74 -21
- aiecs/llm/__init__.py +73 -25
- aiecs/llm/callbacks/__init__.py +11 -0
- aiecs/llm/{custom_callbacks.py → callbacks/custom_callbacks.py} +26 -19
- aiecs/llm/client_factory.py +224 -36
- aiecs/llm/client_resolver.py +155 -0
- aiecs/llm/clients/__init__.py +38 -0
- aiecs/llm/clients/base_client.py +324 -0
- aiecs/llm/clients/google_function_calling_mixin.py +457 -0
- aiecs/llm/clients/googleai_client.py +241 -0
- aiecs/llm/clients/openai_client.py +158 -0
- aiecs/llm/clients/openai_compatible_mixin.py +367 -0
- aiecs/llm/clients/vertex_client.py +897 -0
- aiecs/llm/clients/xai_client.py +201 -0
- aiecs/llm/config/__init__.py +51 -0
- aiecs/llm/config/config_loader.py +272 -0
- aiecs/llm/config/config_validator.py +206 -0
- aiecs/llm/config/model_config.py +143 -0
- aiecs/llm/protocols.py +149 -0
- aiecs/llm/utils/__init__.py +10 -0
- aiecs/llm/utils/validate_config.py +89 -0
- aiecs/main.py +140 -121
- aiecs/scripts/aid/VERSION_MANAGEMENT.md +138 -0
- aiecs/scripts/aid/__init__.py +19 -0
- aiecs/scripts/aid/module_checker.py +499 -0
- aiecs/scripts/aid/version_manager.py +235 -0
- aiecs/scripts/{DEPENDENCY_SYSTEM_SUMMARY.md → dependance_check/DEPENDENCY_SYSTEM_SUMMARY.md} +1 -0
- aiecs/scripts/{README_DEPENDENCY_CHECKER.md → dependance_check/README_DEPENDENCY_CHECKER.md} +1 -0
- aiecs/scripts/dependance_check/__init__.py +15 -0
- aiecs/scripts/dependance_check/dependency_checker.py +1835 -0
- aiecs/scripts/{dependency_fixer.py → dependance_check/dependency_fixer.py} +192 -90
- aiecs/scripts/{download_nlp_data.py → dependance_check/download_nlp_data.py} +203 -71
- aiecs/scripts/dependance_patch/__init__.py +7 -0
- aiecs/scripts/dependance_patch/fix_weasel/__init__.py +11 -0
- aiecs/scripts/{fix_weasel_validator.py → dependance_patch/fix_weasel/fix_weasel_validator.py} +21 -14
- aiecs/scripts/{patch_weasel_library.sh → dependance_patch/fix_weasel/patch_weasel_library.sh} +1 -1
- aiecs/scripts/knowledge_graph/__init__.py +3 -0
- aiecs/scripts/knowledge_graph/run_threshold_experiments.py +212 -0
- aiecs/scripts/migrations/multi_tenancy/README.md +142 -0
- aiecs/scripts/tools_develop/README.md +671 -0
- aiecs/scripts/tools_develop/README_CONFIG_CHECKER.md +273 -0
- aiecs/scripts/tools_develop/TOOLS_CONFIG_GUIDE.md +1287 -0
- aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
- aiecs/scripts/tools_develop/__init__.py +21 -0
- aiecs/scripts/tools_develop/check_all_tools_config.py +548 -0
- aiecs/scripts/tools_develop/check_type_annotations.py +257 -0
- aiecs/scripts/tools_develop/pre-commit-schema-coverage.sh +66 -0
- aiecs/scripts/tools_develop/schema_coverage.py +511 -0
- aiecs/scripts/tools_develop/validate_tool_schemas.py +475 -0
- aiecs/scripts/tools_develop/verify_executor_config_fix.py +98 -0
- aiecs/scripts/tools_develop/verify_tools.py +352 -0
- aiecs/tasks/__init__.py +0 -1
- aiecs/tasks/worker.py +115 -47
- aiecs/tools/__init__.py +194 -72
- aiecs/tools/apisource/__init__.py +99 -0
- aiecs/tools/apisource/intelligence/__init__.py +19 -0
- aiecs/tools/apisource/intelligence/data_fusion.py +632 -0
- aiecs/tools/apisource/intelligence/query_analyzer.py +417 -0
- aiecs/tools/apisource/intelligence/search_enhancer.py +385 -0
- aiecs/tools/apisource/monitoring/__init__.py +9 -0
- aiecs/tools/apisource/monitoring/metrics.py +330 -0
- aiecs/tools/apisource/providers/__init__.py +112 -0
- aiecs/tools/apisource/providers/base.py +671 -0
- aiecs/tools/apisource/providers/census.py +397 -0
- aiecs/tools/apisource/providers/fred.py +535 -0
- aiecs/tools/apisource/providers/newsapi.py +409 -0
- aiecs/tools/apisource/providers/worldbank.py +352 -0
- aiecs/tools/apisource/reliability/__init__.py +12 -0
- aiecs/tools/apisource/reliability/error_handler.py +363 -0
- aiecs/tools/apisource/reliability/fallback_strategy.py +376 -0
- aiecs/tools/apisource/tool.py +832 -0
- aiecs/tools/apisource/utils/__init__.py +9 -0
- aiecs/tools/apisource/utils/validators.py +334 -0
- aiecs/tools/base_tool.py +415 -21
- aiecs/tools/docs/__init__.py +121 -0
- aiecs/tools/docs/ai_document_orchestrator.py +607 -0
- aiecs/tools/docs/ai_document_writer_orchestrator.py +2350 -0
- aiecs/tools/docs/content_insertion_tool.py +1320 -0
- aiecs/tools/docs/document_creator_tool.py +1323 -0
- aiecs/tools/docs/document_layout_tool.py +1160 -0
- aiecs/tools/docs/document_parser_tool.py +1011 -0
- aiecs/tools/docs/document_writer_tool.py +1829 -0
- aiecs/tools/knowledge_graph/__init__.py +17 -0
- aiecs/tools/knowledge_graph/graph_reasoning_tool.py +807 -0
- aiecs/tools/knowledge_graph/graph_search_tool.py +944 -0
- aiecs/tools/knowledge_graph/kg_builder_tool.py +524 -0
- aiecs/tools/langchain_adapter.py +300 -138
- aiecs/tools/schema_generator.py +455 -0
- aiecs/tools/search_tool/__init__.py +100 -0
- aiecs/tools/search_tool/analyzers.py +581 -0
- aiecs/tools/search_tool/cache.py +264 -0
- aiecs/tools/search_tool/constants.py +128 -0
- aiecs/tools/search_tool/context.py +224 -0
- aiecs/tools/search_tool/core.py +778 -0
- aiecs/tools/search_tool/deduplicator.py +119 -0
- aiecs/tools/search_tool/error_handler.py +242 -0
- aiecs/tools/search_tool/metrics.py +343 -0
- aiecs/tools/search_tool/rate_limiter.py +172 -0
- aiecs/tools/search_tool/schemas.py +275 -0
- aiecs/tools/statistics/__init__.py +80 -0
- aiecs/tools/statistics/ai_data_analysis_orchestrator.py +646 -0
- aiecs/tools/statistics/ai_insight_generator_tool.py +508 -0
- aiecs/tools/statistics/ai_report_orchestrator_tool.py +684 -0
- aiecs/tools/statistics/data_loader_tool.py +555 -0
- aiecs/tools/statistics/data_profiler_tool.py +638 -0
- aiecs/tools/statistics/data_transformer_tool.py +580 -0
- aiecs/tools/statistics/data_visualizer_tool.py +498 -0
- aiecs/tools/statistics/model_trainer_tool.py +507 -0
- aiecs/tools/statistics/statistical_analyzer_tool.py +472 -0
- aiecs/tools/task_tools/__init__.py +49 -36
- aiecs/tools/task_tools/chart_tool.py +200 -184
- aiecs/tools/task_tools/classfire_tool.py +268 -267
- aiecs/tools/task_tools/image_tool.py +175 -131
- aiecs/tools/task_tools/office_tool.py +226 -146
- aiecs/tools/task_tools/pandas_tool.py +477 -121
- aiecs/tools/task_tools/report_tool.py +390 -142
- aiecs/tools/task_tools/research_tool.py +149 -79
- aiecs/tools/task_tools/scraper_tool.py +339 -145
- aiecs/tools/task_tools/stats_tool.py +448 -209
- aiecs/tools/temp_file_manager.py +26 -24
- aiecs/tools/tool_executor/__init__.py +18 -16
- aiecs/tools/tool_executor/tool_executor.py +364 -52
- aiecs/utils/LLM_output_structor.py +74 -48
- aiecs/utils/__init__.py +14 -3
- aiecs/utils/base_callback.py +0 -3
- aiecs/utils/cache_provider.py +696 -0
- aiecs/utils/execution_utils.py +50 -31
- aiecs/utils/prompt_loader.py +1 -0
- aiecs/utils/token_usage_repository.py +37 -11
- aiecs/ws/socket_server.py +14 -4
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/METADATA +52 -15
- aiecs-1.7.6.dist-info/RECORD +337 -0
- aiecs-1.7.6.dist-info/entry_points.txt +13 -0
- aiecs/config/registry.py +0 -19
- aiecs/domain/context/content_engine.py +0 -982
- aiecs/llm/base_client.py +0 -99
- aiecs/llm/openai_client.py +0 -125
- aiecs/llm/vertex_client.py +0 -186
- aiecs/llm/xai_client.py +0 -184
- aiecs/scripts/dependency_checker.py +0 -857
- aiecs/scripts/quick_dependency_check.py +0 -269
- aiecs/tools/task_tools/search_api.py +0 -7
- aiecs-1.0.1.dist-info/RECORD +0 -90
- aiecs-1.0.1.dist-info/entry_points.txt +0 -7
- /aiecs/scripts/{setup_nlp_data.sh → dependance_check/setup_nlp_data.sh} +0 -0
- /aiecs/scripts/{README_WEASEL_PATCH.md → dependance_patch/fix_weasel/README_WEASEL_PATCH.md} +0 -0
- /aiecs/scripts/{fix_weasel_validator.sh → dependance_patch/fix_weasel/fix_weasel_validator.sh} +0 -0
- /aiecs/scripts/{run_weasel_patch.sh → dependance_patch/fix_weasel/run_weasel_patch.sh} +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/WHEEL +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/licenses/LICENSE +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,671 @@
|
|
|
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 datetime import datetime
|
|
19
|
+
from threading import Lock
|
|
20
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
from aiecs.tools.apisource.monitoring.metrics import DetailedMetrics
|
|
23
|
+
from aiecs.tools.apisource.reliability.error_handler import SmartErrorHandler
|
|
24
|
+
from aiecs.tools.apisource.utils.validators import DataValidator
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def expose_operation(operation_name: str, description: str):
|
|
30
|
+
"""
|
|
31
|
+
Decorator: Mark provider operations that should be exposed to AI agents.
|
|
32
|
+
|
|
33
|
+
This decorator allows provider operations to be automatically discovered by the
|
|
34
|
+
LangChain adapter and exposed as individual tools to AI agents, providing
|
|
35
|
+
fine-grained visibility into provider capabilities.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
operation_name: The name of the operation (e.g., 'get_series_observations')
|
|
39
|
+
description: Human-readable description of what the operation does
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Decorated function with metadata for operation discovery
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
@expose_operation(
|
|
46
|
+
operation_name='get_series_observations',
|
|
47
|
+
description='Get FRED economic time series data'
|
|
48
|
+
)
|
|
49
|
+
def get_series_observations(self, series_id: str, ...):
|
|
50
|
+
pass
|
|
51
|
+
"""
|
|
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
|
+
|
|
59
|
+
return decorator
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RateLimiter:
|
|
63
|
+
"""Token bucket rate limiter for API requests"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, tokens_per_second: float = 1.0, max_tokens: int = 10):
|
|
66
|
+
"""
|
|
67
|
+
Initialize rate limiter with token bucket algorithm.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
tokens_per_second: Rate at which tokens are added to the bucket
|
|
71
|
+
max_tokens: Maximum number of tokens the bucket can hold
|
|
72
|
+
"""
|
|
73
|
+
self.tokens_per_second = tokens_per_second
|
|
74
|
+
self.max_tokens = max_tokens
|
|
75
|
+
self.tokens = max_tokens
|
|
76
|
+
self.last_update = time.time()
|
|
77
|
+
self.lock = Lock()
|
|
78
|
+
|
|
79
|
+
def acquire(self, tokens: int = 1) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Acquire tokens from the bucket.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
tokens: Number of tokens to acquire
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if tokens were acquired, False otherwise
|
|
88
|
+
"""
|
|
89
|
+
with self.lock:
|
|
90
|
+
now = time.time()
|
|
91
|
+
elapsed = now - self.last_update
|
|
92
|
+
|
|
93
|
+
# Add new tokens based on elapsed time
|
|
94
|
+
self.tokens = int(min(self.max_tokens, self.tokens + elapsed * self.tokens_per_second))
|
|
95
|
+
self.last_update = now
|
|
96
|
+
|
|
97
|
+
if self.tokens >= tokens:
|
|
98
|
+
self.tokens -= tokens
|
|
99
|
+
return True
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def wait(self, tokens: int = 1, timeout: float = 30.0) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Wait until tokens are available.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
tokens: Number of tokens to acquire
|
|
108
|
+
timeout: Maximum time to wait in seconds
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True if tokens were acquired, False if timeout
|
|
112
|
+
"""
|
|
113
|
+
start_time = time.time()
|
|
114
|
+
while time.time() - start_time < timeout:
|
|
115
|
+
if self.acquire(tokens):
|
|
116
|
+
return True
|
|
117
|
+
time.sleep(0.1)
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class BaseAPIProvider(ABC):
|
|
122
|
+
"""
|
|
123
|
+
Abstract base class for all API data source providers.
|
|
124
|
+
|
|
125
|
+
Provides:
|
|
126
|
+
- Rate limiting with token bucket algorithm
|
|
127
|
+
- Standardized error handling
|
|
128
|
+
- Metadata about provider capabilities
|
|
129
|
+
- Parameter validation
|
|
130
|
+
- Response formatting
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
|
134
|
+
"""
|
|
135
|
+
Initialize the API provider.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
config: Configuration dictionary with API keys, rate limits, etc.
|
|
139
|
+
"""
|
|
140
|
+
self.config = config or {}
|
|
141
|
+
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
|
142
|
+
|
|
143
|
+
# Initialize rate limiter
|
|
144
|
+
rate_limit = self.config.get("rate_limit", 10) # requests per second
|
|
145
|
+
max_burst = self.config.get("max_burst", 20)
|
|
146
|
+
self.rate_limiter = RateLimiter(tokens_per_second=rate_limit, max_tokens=max_burst)
|
|
147
|
+
|
|
148
|
+
# Initialize detailed metrics
|
|
149
|
+
self.metrics = DetailedMetrics(max_response_times=100)
|
|
150
|
+
|
|
151
|
+
# Initialize smart error handler
|
|
152
|
+
self.error_handler = SmartErrorHandler(
|
|
153
|
+
max_retries=self.config.get("max_retries", 3),
|
|
154
|
+
backoff_factor=self.config.get("backoff_factor", 2.0),
|
|
155
|
+
initial_delay=self.config.get("initial_delay", 1.0),
|
|
156
|
+
max_delay=self.config.get("max_delay", 30.0),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Initialize data validator
|
|
160
|
+
self.validator = DataValidator()
|
|
161
|
+
|
|
162
|
+
# Legacy stats for backwards compatibility
|
|
163
|
+
self.stats: Dict[str, Any] = {
|
|
164
|
+
"total_requests": 0,
|
|
165
|
+
"successful_requests": 0,
|
|
166
|
+
"failed_requests": 0,
|
|
167
|
+
"last_request_time": None,
|
|
168
|
+
}
|
|
169
|
+
self.stats_lock = Lock()
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
@abstractmethod
|
|
173
|
+
def name(self) -> str:
|
|
174
|
+
"""Provider name (e.g., 'fred', 'worldbank')"""
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
@abstractmethod
|
|
178
|
+
def description(self) -> str:
|
|
179
|
+
"""Human-readable description of the provider"""
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def supported_operations(self) -> List[str]:
|
|
184
|
+
"""List of supported operation names"""
|
|
185
|
+
|
|
186
|
+
@abstractmethod
|
|
187
|
+
def validate_params(self, operation: str, params: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
|
|
188
|
+
"""
|
|
189
|
+
Validate parameters for a specific operation.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
operation: Operation name
|
|
193
|
+
params: Parameters to validate
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Tuple of (is_valid, error_message)
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
@abstractmethod
|
|
200
|
+
def fetch(self, operation: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
201
|
+
"""
|
|
202
|
+
Fetch data from the API.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
operation: Operation to perform
|
|
206
|
+
params: Operation parameters
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Response data in standardized format
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
ValueError: If operation is not supported
|
|
213
|
+
Exception: If API request fails
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def get_metadata(self) -> Dict[str, Any]:
|
|
217
|
+
"""
|
|
218
|
+
Get provider metadata including health status and detailed metrics.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Dictionary with comprehensive provider information
|
|
222
|
+
"""
|
|
223
|
+
return {
|
|
224
|
+
"name": self.name,
|
|
225
|
+
"description": self.description,
|
|
226
|
+
"operations": self.supported_operations,
|
|
227
|
+
"stats": self.metrics.get_summary(), # Use detailed metrics
|
|
228
|
+
"health": {
|
|
229
|
+
"score": self.metrics.get_health_score(),
|
|
230
|
+
"status": ("healthy" if self.metrics.get_health_score() > 0.7 else "degraded"),
|
|
231
|
+
},
|
|
232
|
+
"config": {
|
|
233
|
+
"rate_limit": self.config.get("rate_limit", 10),
|
|
234
|
+
"timeout": self.config.get("timeout", 30),
|
|
235
|
+
"max_retries": self.config.get("max_retries", 3),
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def get_operation_schema(self, operation: str) -> Optional[Dict[str, Any]]:
|
|
240
|
+
"""
|
|
241
|
+
Get schema for a specific operation.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
operation: Operation name
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Schema dictionary or None if not available
|
|
248
|
+
"""
|
|
249
|
+
# Override in subclass to provide operation-specific schemas
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def get_exposed_operations(cls) -> List[Dict[str, Any]]:
|
|
254
|
+
"""
|
|
255
|
+
Get all operations that are exposed to AI agents via the @expose_operation decorator.
|
|
256
|
+
|
|
257
|
+
This method discovers all methods decorated with @expose_operation and returns
|
|
258
|
+
their metadata along with their schemas. This enables the LangChain adapter to
|
|
259
|
+
automatically create individual tools for each provider operation.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
List of operation dictionaries, each containing:
|
|
263
|
+
- name: Operation name
|
|
264
|
+
- description: Operation description
|
|
265
|
+
- schema: Operation schema (parameters, types, descriptions)
|
|
266
|
+
- method_name: The actual method name on the class
|
|
267
|
+
|
|
268
|
+
Example:
|
|
269
|
+
>>> FREDProvider.get_exposed_operations()
|
|
270
|
+
[
|
|
271
|
+
{
|
|
272
|
+
'name': 'get_series_observations',
|
|
273
|
+
'description': 'Get FRED economic time series data',
|
|
274
|
+
'schema': {...},
|
|
275
|
+
'method_name': 'get_series_observations'
|
|
276
|
+
},
|
|
277
|
+
...
|
|
278
|
+
]
|
|
279
|
+
"""
|
|
280
|
+
operations = []
|
|
281
|
+
|
|
282
|
+
# Create a temporary instance to access get_operation_schema
|
|
283
|
+
# We need this because get_operation_schema might be an instance method
|
|
284
|
+
try:
|
|
285
|
+
# Try to get schema without instantiation first
|
|
286
|
+
for attr_name in dir(cls):
|
|
287
|
+
# Skip private and special methods
|
|
288
|
+
if attr_name.startswith("_"):
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
attr = getattr(cls, attr_name)
|
|
293
|
+
except AttributeError:
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# Check if this is an exposed operation
|
|
297
|
+
if callable(attr) and hasattr(attr, "_exposed_operation"):
|
|
298
|
+
operation_name = attr._operation_name
|
|
299
|
+
operation_description = attr._operation_description
|
|
300
|
+
|
|
301
|
+
# Schema retrieval requires an instance, so skip at class level
|
|
302
|
+
# Schema will be available at runtime when provider instances are created
|
|
303
|
+
schema = None
|
|
304
|
+
|
|
305
|
+
operations.append(
|
|
306
|
+
{
|
|
307
|
+
"name": operation_name,
|
|
308
|
+
"description": operation_description,
|
|
309
|
+
"schema": schema,
|
|
310
|
+
"method_name": attr_name,
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
logger.debug(f"Discovered exposed operation: {operation_name} from {cls.__name__}")
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.warning(f"Error discovering exposed operations for {cls.__name__}: {e}")
|
|
318
|
+
|
|
319
|
+
return operations
|
|
320
|
+
|
|
321
|
+
def validate_and_clean_data(self, operation: str, raw_data: Any) -> Dict[str, Any]:
|
|
322
|
+
"""
|
|
323
|
+
Validate and clean data (optional, override in subclass).
|
|
324
|
+
|
|
325
|
+
Providers can implement custom validation logic for their specific data formats.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
operation: Operation that produced the data
|
|
329
|
+
raw_data: Raw data from API
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Dictionary with:
|
|
333
|
+
- data: Cleaned data
|
|
334
|
+
- validation_warnings: List of warnings
|
|
335
|
+
- statistics: Data quality statistics
|
|
336
|
+
"""
|
|
337
|
+
# Default implementation: no validation
|
|
338
|
+
return {"data": raw_data, "validation_warnings": [], "statistics": {}}
|
|
339
|
+
|
|
340
|
+
def calculate_data_quality(self, operation: str, data: Any, response_time_ms: float) -> Dict[str, Any]:
|
|
341
|
+
"""
|
|
342
|
+
Calculate quality metadata for the response.
|
|
343
|
+
|
|
344
|
+
Can be overridden by providers for custom quality assessment.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
operation: Operation performed
|
|
348
|
+
data: Response data
|
|
349
|
+
response_time_ms: Response time in milliseconds
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Quality metadata dictionary
|
|
353
|
+
"""
|
|
354
|
+
quality: Dict[str, Any] = {
|
|
355
|
+
"score": 0.7, # Default quality score
|
|
356
|
+
"completeness": 1.0, # Assume complete unless validated otherwise
|
|
357
|
+
"freshness_hours": None, # Unknown freshness
|
|
358
|
+
"confidence": 0.8, # Default confidence
|
|
359
|
+
"authority_level": "verified", # Provider is verified
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
# Adjust score based on response time
|
|
363
|
+
if response_time_ms < 500:
|
|
364
|
+
score = quality.get("score", 0.7)
|
|
365
|
+
if isinstance(score, (int, float)):
|
|
366
|
+
quality["score"] = min(score + 0.1, 1.0)
|
|
367
|
+
elif response_time_ms > 5000:
|
|
368
|
+
score = quality.get("score", 0.7)
|
|
369
|
+
if isinstance(score, (int, float)):
|
|
370
|
+
quality["score"] = max(score - 0.1, 0.0)
|
|
371
|
+
|
|
372
|
+
# Check if data is empty
|
|
373
|
+
if data is None:
|
|
374
|
+
quality["completeness"] = 0.0
|
|
375
|
+
quality["score"] = 0.0
|
|
376
|
+
elif isinstance(data, list) and len(data) == 0:
|
|
377
|
+
quality["completeness"] = 0.0
|
|
378
|
+
score = quality.get("score", 0.7)
|
|
379
|
+
if isinstance(score, (int, float)):
|
|
380
|
+
quality["score"] = max(score - 0.3, 0.0)
|
|
381
|
+
|
|
382
|
+
return quality
|
|
383
|
+
|
|
384
|
+
def _update_stats(self, success: bool):
|
|
385
|
+
"""Update request statistics"""
|
|
386
|
+
with self.stats_lock:
|
|
387
|
+
total = self.stats.get("total_requests", 0)
|
|
388
|
+
if isinstance(total, (int, float)):
|
|
389
|
+
self.stats["total_requests"] = total + 1
|
|
390
|
+
if success:
|
|
391
|
+
successful = self.stats.get("successful_requests", 0)
|
|
392
|
+
if isinstance(successful, (int, float)):
|
|
393
|
+
self.stats["successful_requests"] = successful + 1
|
|
394
|
+
else:
|
|
395
|
+
failed = self.stats.get("failed_requests", 0)
|
|
396
|
+
if isinstance(failed, (int, float)):
|
|
397
|
+
self.stats["failed_requests"] = failed + 1
|
|
398
|
+
self.stats["last_request_time"] = datetime.utcnow().isoformat()
|
|
399
|
+
|
|
400
|
+
def _format_response(
|
|
401
|
+
self,
|
|
402
|
+
operation: str,
|
|
403
|
+
data: Any,
|
|
404
|
+
source: Optional[str] = None,
|
|
405
|
+
response_time_ms: Optional[float] = None,
|
|
406
|
+
validation_result: Optional[Dict[str, Any]] = None,
|
|
407
|
+
) -> Dict[str, Any]:
|
|
408
|
+
"""
|
|
409
|
+
Format response in standardized format with enhanced metadata.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
operation: Operation that was performed
|
|
413
|
+
data: Response data
|
|
414
|
+
source: Data source URL or identifier
|
|
415
|
+
response_time_ms: Response time in milliseconds
|
|
416
|
+
validation_result: Optional validation result from validate_and_clean_data
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Standardized response dictionary with comprehensive metadata
|
|
420
|
+
"""
|
|
421
|
+
# Calculate quality metadata
|
|
422
|
+
quality = self.calculate_data_quality(operation, data, response_time_ms or 0)
|
|
423
|
+
|
|
424
|
+
# Calculate coverage information
|
|
425
|
+
coverage = self._calculate_coverage(data)
|
|
426
|
+
|
|
427
|
+
# Build metadata
|
|
428
|
+
metadata = {
|
|
429
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
430
|
+
"source": source or f"{self.name} API",
|
|
431
|
+
"quality": quality,
|
|
432
|
+
"coverage": coverage,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
# Add API info if response time provided
|
|
436
|
+
if response_time_ms is not None:
|
|
437
|
+
metadata["api_info"] = {
|
|
438
|
+
"response_time_ms": round(response_time_ms, 2),
|
|
439
|
+
"provider": self.name,
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
# Add validation warnings if present
|
|
443
|
+
if validation_result and validation_result.get("validation_warnings"):
|
|
444
|
+
metadata["validation_warnings"] = validation_result["validation_warnings"]
|
|
445
|
+
|
|
446
|
+
# Add statistics if present
|
|
447
|
+
if validation_result and validation_result.get("statistics"):
|
|
448
|
+
metadata["statistics"] = validation_result["statistics"]
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
"provider": self.name,
|
|
452
|
+
"operation": operation,
|
|
453
|
+
"data": data,
|
|
454
|
+
"metadata": metadata,
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
def _calculate_coverage(self, data: Any) -> Dict[str, Any]:
|
|
458
|
+
"""
|
|
459
|
+
Calculate data coverage information.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
data: Response data
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Coverage information dictionary
|
|
466
|
+
"""
|
|
467
|
+
coverage: Dict[str, Any] = {}
|
|
468
|
+
|
|
469
|
+
# Calculate record count
|
|
470
|
+
if isinstance(data, list):
|
|
471
|
+
coverage["total_records"] = len(data)
|
|
472
|
+
|
|
473
|
+
# Try to extract date range from time series data
|
|
474
|
+
if len(data) > 0 and isinstance(data[0], dict):
|
|
475
|
+
date_fields = ["date", "observation_date", "timestamp"]
|
|
476
|
+
for date_field in date_fields:
|
|
477
|
+
if date_field in data[0]:
|
|
478
|
+
dates = [item.get(date_field) for item in data if date_field in item and item.get(date_field)]
|
|
479
|
+
if dates:
|
|
480
|
+
try:
|
|
481
|
+
# Sort to get earliest and latest
|
|
482
|
+
dates_sorted = sorted(dates)
|
|
483
|
+
coverage["start_date"] = dates_sorted[0]
|
|
484
|
+
coverage["end_date"] = dates_sorted[-1]
|
|
485
|
+
|
|
486
|
+
# Try to infer frequency
|
|
487
|
+
frequency = self.validator.infer_data_frequency(data, date_field)
|
|
488
|
+
if frequency:
|
|
489
|
+
coverage["frequency"] = frequency
|
|
490
|
+
except Exception:
|
|
491
|
+
pass
|
|
492
|
+
break
|
|
493
|
+
elif isinstance(data, dict):
|
|
494
|
+
# For dict responses
|
|
495
|
+
if "articles" in data:
|
|
496
|
+
coverage["total_records"] = len(data["articles"])
|
|
497
|
+
elif "total_results" in data:
|
|
498
|
+
coverage["total_results"] = data["total_results"]
|
|
499
|
+
else:
|
|
500
|
+
coverage["total_records"] = 1
|
|
501
|
+
else:
|
|
502
|
+
coverage["total_records"] = 1 if data is not None else 0
|
|
503
|
+
|
|
504
|
+
return coverage
|
|
505
|
+
|
|
506
|
+
def _get_api_key(self, key_name: Optional[str] = None) -> Optional[str]:
|
|
507
|
+
"""
|
|
508
|
+
Get API key from config or environment.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
key_name: Specific key name to retrieve
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
API key or None if not found
|
|
515
|
+
|
|
516
|
+
Note: When used through APISourceTool, API keys are loaded from .env files
|
|
517
|
+
via BaseSettings and passed via config dict. This fallback is for backward
|
|
518
|
+
compatibility and independent provider usage.
|
|
519
|
+
"""
|
|
520
|
+
import os
|
|
521
|
+
|
|
522
|
+
# Try config first (primary path - API keys come from APISourceTool's BaseSettings)
|
|
523
|
+
if "api_key" in self.config:
|
|
524
|
+
return self.config["api_key"]
|
|
525
|
+
|
|
526
|
+
# Fallback: Try environment variable (ensures .env files are loaded)
|
|
527
|
+
# Use ToolConfigLoader to ensure .env files are loaded if not already
|
|
528
|
+
try:
|
|
529
|
+
from aiecs.config.tool_config import get_tool_config_loader
|
|
530
|
+
|
|
531
|
+
loader = get_tool_config_loader()
|
|
532
|
+
loader.load_env_config() # Ensures .env files are loaded
|
|
533
|
+
except Exception:
|
|
534
|
+
# If loader is unavailable, try direct dotenv load as fallback
|
|
535
|
+
try:
|
|
536
|
+
from dotenv import load_dotenv
|
|
537
|
+
from pathlib import Path
|
|
538
|
+
|
|
539
|
+
# Try to load .env files from common locations
|
|
540
|
+
for env_file in [".env", ".env.local"]:
|
|
541
|
+
env_path = Path(env_file)
|
|
542
|
+
if env_path.exists():
|
|
543
|
+
load_dotenv(env_path, override=False)
|
|
544
|
+
break
|
|
545
|
+
except Exception:
|
|
546
|
+
pass # If dotenv is unavailable, continue with os.environ
|
|
547
|
+
|
|
548
|
+
# Try environment variable (now includes values from .env files)
|
|
549
|
+
env_var = key_name or f"{self.name.upper()}_API_KEY"
|
|
550
|
+
return os.environ.get(env_var)
|
|
551
|
+
|
|
552
|
+
def execute(self, operation: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
553
|
+
"""
|
|
554
|
+
Execute an operation with rate limiting, error handling, and metrics tracking.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
operation: Operation to perform
|
|
558
|
+
params: Operation parameters
|
|
559
|
+
|
|
560
|
+
Returns:
|
|
561
|
+
Response data with enhanced metadata
|
|
562
|
+
|
|
563
|
+
Raises:
|
|
564
|
+
ValueError: If operation is invalid or parameters are invalid
|
|
565
|
+
Exception: If API request fails after all retries
|
|
566
|
+
"""
|
|
567
|
+
# Validate operation
|
|
568
|
+
if operation not in self.supported_operations:
|
|
569
|
+
available_ops = ", ".join(self.supported_operations)
|
|
570
|
+
schema = self.get_operation_schema(operation)
|
|
571
|
+
operation_error_msg = f"Operation '{operation}' not supported by {self.name}.\n" f"Supported operations: {available_ops}"
|
|
572
|
+
if schema:
|
|
573
|
+
operation_error_msg += f"\nSee get_operation_schema('{operation}') for details"
|
|
574
|
+
raise ValueError(operation_error_msg)
|
|
575
|
+
|
|
576
|
+
# Validate parameters with enhanced error messages
|
|
577
|
+
validation_result = self.validate_params(operation, params)
|
|
578
|
+
is_valid: bool = validation_result[0]
|
|
579
|
+
error_msg: Optional[str] = validation_result[1]
|
|
580
|
+
if not is_valid:
|
|
581
|
+
schema = self.get_operation_schema(operation)
|
|
582
|
+
enhanced_error = f"Invalid parameters for {self.name}.{operation}: {error_msg or 'Unknown error'}"
|
|
583
|
+
|
|
584
|
+
if schema and "parameters" in schema:
|
|
585
|
+
# Add helpful parameter information
|
|
586
|
+
required_params = [name for name, info in schema["parameters"].items() if info.get("required", False)]
|
|
587
|
+
if required_params:
|
|
588
|
+
enhanced_error += f"\nRequired parameters: {', '.join(required_params)}"
|
|
589
|
+
|
|
590
|
+
# Add examples if available
|
|
591
|
+
if "examples" in schema and schema["examples"]:
|
|
592
|
+
example = schema["examples"][0]
|
|
593
|
+
enhanced_error += f"\nExample: {example.get('params', {})}"
|
|
594
|
+
|
|
595
|
+
raise ValueError(enhanced_error)
|
|
596
|
+
|
|
597
|
+
# Apply rate limiting
|
|
598
|
+
wait_start = time.time()
|
|
599
|
+
if not self.rate_limiter.wait(tokens=1, timeout=30):
|
|
600
|
+
self.metrics.record_request(success=False, response_time_ms=0, error_type="rate_limit")
|
|
601
|
+
raise Exception(f"Rate limit exceeded for {self.name}. " "Please try again later or increase rate limits in config.")
|
|
602
|
+
|
|
603
|
+
# Track rate limit wait time
|
|
604
|
+
wait_time_ms = (time.time() - wait_start) * 1000
|
|
605
|
+
if wait_time_ms > 100: # Only record significant waits
|
|
606
|
+
self.metrics.record_rate_limit_wait(wait_time_ms)
|
|
607
|
+
|
|
608
|
+
# Execute with smart retry logic
|
|
609
|
+
def fetch_operation():
|
|
610
|
+
"""Wrapper for fetch with timing"""
|
|
611
|
+
start_time = time.time()
|
|
612
|
+
result = self.fetch(operation, params)
|
|
613
|
+
response_time_ms = (time.time() - start_time) * 1000
|
|
614
|
+
return result, response_time_ms
|
|
615
|
+
|
|
616
|
+
# Use error handler for retries
|
|
617
|
+
execution_result = self.error_handler.execute_with_retry(
|
|
618
|
+
operation_func=fetch_operation,
|
|
619
|
+
operation_name=operation,
|
|
620
|
+
provider_name=self.name,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
if execution_result["success"]:
|
|
624
|
+
result, response_time_ms = execution_result["data"]
|
|
625
|
+
|
|
626
|
+
# Calculate data size for metrics
|
|
627
|
+
data = result.get("data") if isinstance(result, dict) else result
|
|
628
|
+
record_count = len(data) if isinstance(data, list) else (1 if data else 0)
|
|
629
|
+
|
|
630
|
+
# Record success metrics
|
|
631
|
+
self.metrics.record_request(
|
|
632
|
+
success=True,
|
|
633
|
+
response_time_ms=response_time_ms,
|
|
634
|
+
record_count=record_count,
|
|
635
|
+
cached=False,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
# Update legacy stats
|
|
639
|
+
self._update_stats(success=True)
|
|
640
|
+
|
|
641
|
+
self.logger.info(f"Successfully executed {self.name}.{operation} " f"in {response_time_ms:.0f}ms ({record_count} records)")
|
|
642
|
+
|
|
643
|
+
return result
|
|
644
|
+
else:
|
|
645
|
+
# All retries failed
|
|
646
|
+
error_info = execution_result["error"]
|
|
647
|
+
retry_info = execution_result["retry_info"]
|
|
648
|
+
|
|
649
|
+
# Record failure metrics
|
|
650
|
+
self.metrics.record_request(
|
|
651
|
+
success=False,
|
|
652
|
+
response_time_ms=0,
|
|
653
|
+
error_type=error_info.get("type", "unknown"),
|
|
654
|
+
error_message=error_info.get("message"),
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
# Update legacy stats
|
|
658
|
+
self._update_stats(success=False)
|
|
659
|
+
|
|
660
|
+
# Build comprehensive error message
|
|
661
|
+
error_msg = f"Failed to execute {self.name}.{operation} after " f"{retry_info['attempts']} attempts.\n" f"Error: {error_info['message']}"
|
|
662
|
+
|
|
663
|
+
# Add recovery suggestions
|
|
664
|
+
if retry_info.get("recovery_suggestions"):
|
|
665
|
+
error_msg += "\n\nSuggestions:"
|
|
666
|
+
for suggestion in retry_info["recovery_suggestions"][:3]:
|
|
667
|
+
error_msg += f"\n - {suggestion}"
|
|
668
|
+
|
|
669
|
+
self.logger.error(error_msg)
|
|
670
|
+
|
|
671
|
+
raise Exception(error_msg)
|