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,1892 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Knowledge-Aware Agent
|
|
3
|
+
|
|
4
|
+
Enhanced hybrid agent with knowledge graph integration.
|
|
5
|
+
Extends the standard HybridAgent with graph reasoning capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Dict, List, Any, Optional, Union, TYPE_CHECKING, Callable, Awaitable, AsyncIterator
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from aiecs.llm import BaseLLMClient
|
|
14
|
+
from aiecs.infrastructure.graph_storage.base import GraphStore
|
|
15
|
+
from aiecs.infrastructure.graph_storage.error_handling import (
|
|
16
|
+
RetryHandler,
|
|
17
|
+
GraphStoreConnectionError,
|
|
18
|
+
GraphStoreQueryError,
|
|
19
|
+
GraphStoreTimeoutError,
|
|
20
|
+
)
|
|
21
|
+
from aiecs.tools.knowledge_graph import GraphReasoningTool
|
|
22
|
+
from aiecs.domain.knowledge_graph.models.entity import Entity
|
|
23
|
+
from aiecs.tools.base_tool import BaseTool
|
|
24
|
+
|
|
25
|
+
from .hybrid_agent import HybridAgent
|
|
26
|
+
from .models import AgentConfiguration, GraphMetrics
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from aiecs.llm.protocols import LLMClientProtocol
|
|
30
|
+
from aiecs.domain.agent.integration.protocols import (
|
|
31
|
+
ConfigManagerProtocol,
|
|
32
|
+
CheckpointerProtocol,
|
|
33
|
+
)
|
|
34
|
+
from aiecs.application.knowledge_graph.search.hybrid_search import SearchMode
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class KnowledgeAwareAgent(HybridAgent):
|
|
40
|
+
"""
|
|
41
|
+
Knowledge-Aware Agent with integrated knowledge graph reasoning.
|
|
42
|
+
|
|
43
|
+
Extends HybridAgent with:
|
|
44
|
+
- Knowledge graph consultation during reasoning
|
|
45
|
+
- Graph-aware tool selection
|
|
46
|
+
- Knowledge-augmented prompt construction
|
|
47
|
+
- Automatic access to graph reasoning capabilities
|
|
48
|
+
|
|
49
|
+
Example with tool names (backward compatible):
|
|
50
|
+
```python
|
|
51
|
+
from aiecs.domain.agent import KnowledgeAwareAgent
|
|
52
|
+
from aiecs.infrastructure.graph_storage import InMemoryGraphStore
|
|
53
|
+
|
|
54
|
+
# Initialize with knowledge graph
|
|
55
|
+
graph_store = InMemoryGraphStore()
|
|
56
|
+
await graph_store.initialize()
|
|
57
|
+
|
|
58
|
+
agent = KnowledgeAwareAgent(
|
|
59
|
+
agent_id="kg_agent_001",
|
|
60
|
+
name="Knowledge Assistant",
|
|
61
|
+
llm_client=llm_client,
|
|
62
|
+
tools=["web_search", "calculator"],
|
|
63
|
+
config=config,
|
|
64
|
+
graph_store=graph_store
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
await agent.initialize()
|
|
68
|
+
result = await agent.execute_task("How is Alice connected to Company X?")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Example with tool instances (new flexibility):
|
|
72
|
+
```python
|
|
73
|
+
# Pre-configured tools with state
|
|
74
|
+
agent = KnowledgeAwareAgent(
|
|
75
|
+
agent_id="kg_agent_001",
|
|
76
|
+
name="Knowledge Assistant",
|
|
77
|
+
llm_client=llm_client,
|
|
78
|
+
tools={
|
|
79
|
+
"web_search": WebSearchTool(api_key="..."),
|
|
80
|
+
"calculator": CalculatorTool()
|
|
81
|
+
},
|
|
82
|
+
config=config,
|
|
83
|
+
graph_store=graph_store
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
agent_id: str,
|
|
91
|
+
name: str,
|
|
92
|
+
llm_client: Union[BaseLLMClient, "LLMClientProtocol"],
|
|
93
|
+
tools: Union[List[str], Dict[str, BaseTool]],
|
|
94
|
+
config: AgentConfiguration,
|
|
95
|
+
graph_store: Optional[GraphStore] = None,
|
|
96
|
+
description: Optional[str] = None,
|
|
97
|
+
version: str = "1.0.0",
|
|
98
|
+
max_iterations: int = 10,
|
|
99
|
+
enable_graph_reasoning: bool = True,
|
|
100
|
+
config_manager: Optional["ConfigManagerProtocol"] = None,
|
|
101
|
+
checkpointer: Optional["CheckpointerProtocol"] = None,
|
|
102
|
+
context_engine: Optional[Any] = None,
|
|
103
|
+
collaboration_enabled: bool = False,
|
|
104
|
+
agent_registry: Optional[Dict[str, Any]] = None,
|
|
105
|
+
learning_enabled: bool = False,
|
|
106
|
+
resource_limits: Optional[Any] = None,
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Initialize Knowledge-Aware agent.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
agent_id: Unique agent identifier
|
|
113
|
+
name: Agent name
|
|
114
|
+
llm_client: LLM client for reasoning (BaseLLMClient or any LLMClientProtocol)
|
|
115
|
+
tools: Tools - either list of tool names or dict of tool instances
|
|
116
|
+
(graph_reasoning auto-added if graph_store provided and tools is a list)
|
|
117
|
+
config: Agent configuration
|
|
118
|
+
graph_store: Optional knowledge graph store
|
|
119
|
+
description: Optional description
|
|
120
|
+
version: Agent version
|
|
121
|
+
max_iterations: Maximum ReAct iterations
|
|
122
|
+
enable_graph_reasoning: Whether to enable graph reasoning capabilities
|
|
123
|
+
config_manager: Optional configuration manager for dynamic config
|
|
124
|
+
checkpointer: Optional checkpointer for state persistence
|
|
125
|
+
context_engine: Optional context engine for persistent storage
|
|
126
|
+
collaboration_enabled: Enable collaboration features
|
|
127
|
+
agent_registry: Registry of other agents for collaboration
|
|
128
|
+
learning_enabled: Enable learning features
|
|
129
|
+
resource_limits: Optional resource limits configuration
|
|
130
|
+
|
|
131
|
+
Note:
|
|
132
|
+
When using tool instances (Dict[str, BaseTool]), graph_reasoning tool
|
|
133
|
+
is NOT auto-added. You must include it manually if needed:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
tools = {
|
|
137
|
+
"web_search": WebSearchTool(),
|
|
138
|
+
"graph_reasoning": GraphReasoningTool(graph_store)
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
"""
|
|
142
|
+
# Auto-add graph_reasoning tool if graph_store is provided and tools is a list
|
|
143
|
+
if graph_store is not None and enable_graph_reasoning and isinstance(tools, list):
|
|
144
|
+
if "graph_reasoning" not in tools:
|
|
145
|
+
tools = tools + ["graph_reasoning"]
|
|
146
|
+
|
|
147
|
+
super().__init__(
|
|
148
|
+
agent_id=agent_id,
|
|
149
|
+
name=name,
|
|
150
|
+
llm_client=llm_client,
|
|
151
|
+
tools=tools,
|
|
152
|
+
config=config,
|
|
153
|
+
description=description or "Knowledge-aware agent with integrated graph reasoning",
|
|
154
|
+
version=version,
|
|
155
|
+
max_iterations=max_iterations,
|
|
156
|
+
config_manager=config_manager,
|
|
157
|
+
checkpointer=checkpointer,
|
|
158
|
+
context_engine=context_engine,
|
|
159
|
+
collaboration_enabled=collaboration_enabled,
|
|
160
|
+
agent_registry=agent_registry,
|
|
161
|
+
learning_enabled=learning_enabled,
|
|
162
|
+
resource_limits=resource_limits,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
self.graph_store = graph_store
|
|
166
|
+
self.enable_graph_reasoning = enable_graph_reasoning
|
|
167
|
+
self._graph_reasoning_tool: Optional[GraphReasoningTool] = None
|
|
168
|
+
self._knowledge_context: Dict[str, Any] = {}
|
|
169
|
+
self._query_intent_classifier: Optional[Any] = None # Initialized in _initialize()
|
|
170
|
+
self._hybrid_search: Optional[Any] = None # Initialized in _initialize()
|
|
171
|
+
self._entity_extractor: Optional[Any] = None # Initialized in _initialize()
|
|
172
|
+
self._entity_extraction_cache: Dict[str, List[Any]] = {} # Cache for entity extraction results
|
|
173
|
+
self._graph_cache: Optional[Any] = None # Initialized in _initialize()
|
|
174
|
+
|
|
175
|
+
# Cache metrics
|
|
176
|
+
self._cache_hits: int = 0
|
|
177
|
+
self._cache_misses: int = 0
|
|
178
|
+
|
|
179
|
+
# Graph metrics
|
|
180
|
+
self._graph_metrics: GraphMetrics = GraphMetrics(
|
|
181
|
+
min_graph_query_time=None,
|
|
182
|
+
max_graph_query_time=None,
|
|
183
|
+
last_reset_at=None
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Prometheus metrics (initialized lazily)
|
|
187
|
+
self._prometheus_metrics: Optional[Dict[str, Any]] = None
|
|
188
|
+
self._prometheus_enabled: bool = False
|
|
189
|
+
|
|
190
|
+
# Context management configuration
|
|
191
|
+
self._max_context_size: int = 50
|
|
192
|
+
self._relevance_threshold: float = 0.3
|
|
193
|
+
self._relevance_weight: float = 0.6
|
|
194
|
+
self._recency_weight: float = 0.4
|
|
195
|
+
|
|
196
|
+
# Retry handler for knowledge retrieval operations
|
|
197
|
+
self._retry_handler: RetryHandler = RetryHandler(
|
|
198
|
+
max_retries=3,
|
|
199
|
+
base_delay=1.0,
|
|
200
|
+
max_delay=10.0,
|
|
201
|
+
exponential_base=2.0,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Circuit breaker state
|
|
205
|
+
self._circuit_breaker_failures: int = 0
|
|
206
|
+
self._circuit_breaker_threshold: int = 5
|
|
207
|
+
self._circuit_breaker_open: bool = False
|
|
208
|
+
|
|
209
|
+
logger.info(f"KnowledgeAwareAgent initialized: {agent_id} " f"with graph_store={'enabled' if graph_store else 'disabled'}")
|
|
210
|
+
|
|
211
|
+
async def _initialize(self) -> None:
|
|
212
|
+
"""Initialize Knowledge-Aware agent - setup graph tools and augmented prompts."""
|
|
213
|
+
# Call parent initialization
|
|
214
|
+
await super()._initialize()
|
|
215
|
+
|
|
216
|
+
# Initialize graph reasoning tool if graph store is available
|
|
217
|
+
if self.graph_store is not None and self.enable_graph_reasoning:
|
|
218
|
+
try:
|
|
219
|
+
self._graph_reasoning_tool = GraphReasoningTool(self.graph_store)
|
|
220
|
+
logger.info(f"KnowledgeAwareAgent {self.agent_id} initialized graph reasoning")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.warning(f"Failed to initialize graph reasoning tool: {e}")
|
|
223
|
+
|
|
224
|
+
# Initialize HybridSearchStrategy if graph store is available
|
|
225
|
+
if self.graph_store is not None and self.enable_graph_reasoning:
|
|
226
|
+
try:
|
|
227
|
+
from aiecs.application.knowledge_graph.search.hybrid_search import HybridSearchStrategy
|
|
228
|
+
|
|
229
|
+
self._hybrid_search = HybridSearchStrategy(self.graph_store)
|
|
230
|
+
logger.info(f"KnowledgeAwareAgent {self.agent_id} initialized hybrid search strategy")
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.warning(f"Failed to initialize hybrid search strategy: {e}")
|
|
233
|
+
|
|
234
|
+
# Initialize query intent classifier if configured
|
|
235
|
+
if self.graph_store is not None and self.enable_graph_reasoning:
|
|
236
|
+
try:
|
|
237
|
+
self._query_intent_classifier = self._create_query_intent_classifier()
|
|
238
|
+
if self._query_intent_classifier is not None:
|
|
239
|
+
logger.info(f"KnowledgeAwareAgent {self.agent_id} initialized query intent classifier")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.warning(f"Failed to initialize query intent classifier: {e}")
|
|
242
|
+
|
|
243
|
+
# Initialize LLMEntityExtractor if graph store is available
|
|
244
|
+
if self.graph_store is not None and self.enable_graph_reasoning:
|
|
245
|
+
try:
|
|
246
|
+
from aiecs.application.knowledge_graph.extractors.llm_entity_extractor import LLMEntityExtractor
|
|
247
|
+
|
|
248
|
+
# Use the agent's LLM client for entity extraction
|
|
249
|
+
# Cast to LLMClientProtocol since BaseLLMClient implements the protocol
|
|
250
|
+
from typing import cast
|
|
251
|
+
from aiecs.llm.protocols import LLMClientProtocol
|
|
252
|
+
llm_client_protocol = cast(LLMClientProtocol, self.llm_client)
|
|
253
|
+
self._entity_extractor = LLMEntityExtractor(
|
|
254
|
+
schema=None, # No schema constraint for now
|
|
255
|
+
llm_client=llm_client_protocol,
|
|
256
|
+
temperature=0.1, # Low temperature for deterministic extraction
|
|
257
|
+
max_tokens=1000,
|
|
258
|
+
)
|
|
259
|
+
logger.info(f"KnowledgeAwareAgent {self.agent_id} initialized entity extractor")
|
|
260
|
+
except Exception as e:
|
|
261
|
+
logger.warning(f"Failed to initialize entity extractor: {e}")
|
|
262
|
+
|
|
263
|
+
# Initialize GraphStoreCache if graph store is available and caching is enabled
|
|
264
|
+
if self.graph_store is not None and self.enable_graph_reasoning:
|
|
265
|
+
try:
|
|
266
|
+
from aiecs.infrastructure.graph_storage.cache import GraphStoreCache, GraphStoreCacheConfig
|
|
267
|
+
|
|
268
|
+
# Check if caching is enabled in config
|
|
269
|
+
enable_caching = getattr(self._config, "enable_knowledge_caching", True)
|
|
270
|
+
cache_ttl = getattr(self._config, "cache_ttl", 300) # Default 5 minutes
|
|
271
|
+
|
|
272
|
+
if enable_caching:
|
|
273
|
+
cache_config = GraphStoreCacheConfig(
|
|
274
|
+
enabled=True,
|
|
275
|
+
ttl=cache_ttl,
|
|
276
|
+
max_cache_size_mb=100,
|
|
277
|
+
redis_url=None, # Use in-memory cache by default
|
|
278
|
+
key_prefix="knowledge:",
|
|
279
|
+
)
|
|
280
|
+
self._graph_cache = GraphStoreCache(cache_config)
|
|
281
|
+
await self._graph_cache.initialize()
|
|
282
|
+
logger.info(f"KnowledgeAwareAgent {self.agent_id} initialized graph cache (TTL: {cache_ttl}s)")
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.warning(f"Failed to initialize graph cache: {e}")
|
|
285
|
+
|
|
286
|
+
# Rebuild system prompt with knowledge graph capabilities
|
|
287
|
+
if self.graph_store is not None:
|
|
288
|
+
self._system_prompt = self._build_kg_augmented_system_prompt()
|
|
289
|
+
|
|
290
|
+
logger.info(f"KnowledgeAwareAgent {self.agent_id} initialized with enhanced capabilities")
|
|
291
|
+
|
|
292
|
+
async def _shutdown(self) -> None:
|
|
293
|
+
"""Shutdown Knowledge-Aware agent."""
|
|
294
|
+
# Clear knowledge context
|
|
295
|
+
self._knowledge_context.clear()
|
|
296
|
+
|
|
297
|
+
# Shutdown graph store if needed
|
|
298
|
+
if self.graph_store is not None:
|
|
299
|
+
try:
|
|
300
|
+
await self.graph_store.close()
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.warning(f"Error closing graph store: {e}")
|
|
303
|
+
|
|
304
|
+
# Call parent shutdown
|
|
305
|
+
await super()._shutdown()
|
|
306
|
+
|
|
307
|
+
logger.info(f"KnowledgeAwareAgent {self.agent_id} shut down")
|
|
308
|
+
|
|
309
|
+
def _build_kg_augmented_system_prompt(self) -> str:
|
|
310
|
+
"""
|
|
311
|
+
Build knowledge graph-augmented system prompt.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Enhanced system prompt with KG capabilities
|
|
315
|
+
"""
|
|
316
|
+
base_prompt = super()._build_system_prompt()
|
|
317
|
+
|
|
318
|
+
# Add knowledge graph capabilities section
|
|
319
|
+
kg_section = """
|
|
320
|
+
|
|
321
|
+
KNOWLEDGE GRAPH CAPABILITIES:
|
|
322
|
+
You have access to an integrated knowledge graph that can help answer complex questions.
|
|
323
|
+
|
|
324
|
+
REASONING WITH KNOWLEDGE:
|
|
325
|
+
Your reasoning process now includes an automatic RETRIEVE phase:
|
|
326
|
+
1. RETRIEVE: Relevant knowledge is automatically fetched from the graph before each reasoning step
|
|
327
|
+
2. THOUGHT: You analyze the task considering retrieved knowledge
|
|
328
|
+
3. ACTION: Use tools or provide final answer
|
|
329
|
+
4. OBSERVATION: Review results and continue
|
|
330
|
+
|
|
331
|
+
Retrieved knowledge will be provided as:
|
|
332
|
+
RETRIEVED KNOWLEDGE:
|
|
333
|
+
- Entity: id (properties)
|
|
334
|
+
- Entity: id (properties)
|
|
335
|
+
...
|
|
336
|
+
|
|
337
|
+
When to use the 'graph_reasoning' tool:
|
|
338
|
+
- Multi-hop questions (e.g., "How is X connected to Y?")
|
|
339
|
+
- Relationship discovery (e.g., "Who knows people at Company Z?")
|
|
340
|
+
- Knowledge completion (e.g., "What do we know about Person A?")
|
|
341
|
+
- Evidence-based reasoning (multiple sources needed)
|
|
342
|
+
|
|
343
|
+
The 'graph_reasoning' tool supports these modes:
|
|
344
|
+
- query_plan: Plan complex query execution
|
|
345
|
+
- multi_hop: Find connections between entities
|
|
346
|
+
- inference: Apply logical inference rules
|
|
347
|
+
- full_reasoning: Complete reasoning pipeline with evidence synthesis
|
|
348
|
+
|
|
349
|
+
Use graph reasoning proactively when questions involve:
|
|
350
|
+
- Connections, relationships, or paths
|
|
351
|
+
- Multiple entities or complex queries
|
|
352
|
+
- Need for evidence from multiple sources
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
return base_prompt + kg_section
|
|
356
|
+
|
|
357
|
+
def _create_query_intent_classifier(self) -> Optional[Any]:
|
|
358
|
+
"""
|
|
359
|
+
Create query intent classifier from configuration.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
QueryIntentClassifier instance or None if not configured
|
|
363
|
+
"""
|
|
364
|
+
from aiecs.application.knowledge_graph.retrieval import QueryIntentClassifier
|
|
365
|
+
from aiecs.llm import LLMClientFactory
|
|
366
|
+
|
|
367
|
+
# Check if strategy selection LLM is configured
|
|
368
|
+
config = self.get_config()
|
|
369
|
+
if (
|
|
370
|
+
config.strategy_selection_llm_provider is not None
|
|
371
|
+
and config.strategy_selection_llm_provider.strip()
|
|
372
|
+
):
|
|
373
|
+
try:
|
|
374
|
+
# Resolve LLM client from provider name
|
|
375
|
+
client = LLMClientFactory.get_client(
|
|
376
|
+
config.strategy_selection_llm_provider
|
|
377
|
+
)
|
|
378
|
+
# Cast to LLMClientProtocol since BaseLLMClient implements the protocol
|
|
379
|
+
from typing import cast
|
|
380
|
+
from aiecs.llm.protocols import LLMClientProtocol
|
|
381
|
+
llm_client = cast(LLMClientProtocol, client) if client else None
|
|
382
|
+
|
|
383
|
+
# Create classifier with custom client
|
|
384
|
+
classifier = QueryIntentClassifier(
|
|
385
|
+
llm_client=llm_client,
|
|
386
|
+
enable_caching=True,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
logger.info(
|
|
390
|
+
f"Created QueryIntentClassifier with provider: "
|
|
391
|
+
f"{config.strategy_selection_llm_provider}"
|
|
392
|
+
)
|
|
393
|
+
return classifier
|
|
394
|
+
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logger.warning(
|
|
397
|
+
f"Failed to create QueryIntentClassifier with custom LLM: {e}, "
|
|
398
|
+
f"falling back to rule-based classification"
|
|
399
|
+
)
|
|
400
|
+
# Fall back to rule-based classifier (no LLM client)
|
|
401
|
+
return QueryIntentClassifier(llm_client=None, enable_caching=True)
|
|
402
|
+
else:
|
|
403
|
+
# No custom LLM configured, use rule-based classifier
|
|
404
|
+
logger.debug("No strategy selection LLM configured, using rule-based classification")
|
|
405
|
+
return QueryIntentClassifier(llm_client=None, enable_caching=True)
|
|
406
|
+
|
|
407
|
+
async def _reason_with_graph(self, query: str, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
408
|
+
"""
|
|
409
|
+
Consult knowledge graph during reasoning.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
query: Query to reason about
|
|
413
|
+
context: Optional context for reasoning
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Reasoning results from knowledge graph
|
|
417
|
+
"""
|
|
418
|
+
if self._graph_reasoning_tool is None:
|
|
419
|
+
logger.warning("Graph reasoning tool not available")
|
|
420
|
+
return {"error": "Graph reasoning not available"}
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
# Use multi_hop mode by default for general queries
|
|
424
|
+
from aiecs.tools.knowledge_graph.graph_reasoning_tool import (
|
|
425
|
+
GraphReasoningInput,
|
|
426
|
+
ReasoningModeEnum,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Extract entity IDs from context if available
|
|
430
|
+
start_entity_id = None
|
|
431
|
+
target_entity_id = None
|
|
432
|
+
if context:
|
|
433
|
+
start_entity_id = context.get("start_entity_id")
|
|
434
|
+
target_entity_id = context.get("target_entity_id")
|
|
435
|
+
|
|
436
|
+
input_data = GraphReasoningInput( # type: ignore[call-arg]
|
|
437
|
+
mode=ReasoningModeEnum.MULTI_HOP,
|
|
438
|
+
query=query,
|
|
439
|
+
start_entity_id=start_entity_id,
|
|
440
|
+
target_entity_id=target_entity_id,
|
|
441
|
+
max_hops=3,
|
|
442
|
+
synthesize_evidence=True,
|
|
443
|
+
confidence_threshold=0.6,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
result = await self._graph_reasoning_tool._execute(input_data)
|
|
447
|
+
|
|
448
|
+
# Store knowledge context for later use
|
|
449
|
+
self._knowledge_context[query] = {
|
|
450
|
+
"answer": result.get("answer"),
|
|
451
|
+
"confidence": result.get("confidence"),
|
|
452
|
+
"evidence_count": result.get("evidence_count"),
|
|
453
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return result
|
|
457
|
+
|
|
458
|
+
except Exception as e:
|
|
459
|
+
logger.error(f"Error in graph reasoning: {e}")
|
|
460
|
+
return {"error": str(e)}
|
|
461
|
+
|
|
462
|
+
async def _select_tools_with_graph_awareness(self, task: str, available_tools: List[str]) -> List[str]:
|
|
463
|
+
"""
|
|
464
|
+
Select tools with graph awareness.
|
|
465
|
+
|
|
466
|
+
Prioritizes graph reasoning tool for knowledge-related queries.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
task: Task description
|
|
470
|
+
available_tools: Available tool names
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
Selected tool names
|
|
474
|
+
"""
|
|
475
|
+
# Keywords that suggest graph reasoning might be useful
|
|
476
|
+
graph_keywords = [
|
|
477
|
+
"connected",
|
|
478
|
+
"connection",
|
|
479
|
+
"relationship",
|
|
480
|
+
"related",
|
|
481
|
+
"knows",
|
|
482
|
+
"works",
|
|
483
|
+
"friend",
|
|
484
|
+
"colleague",
|
|
485
|
+
"partner",
|
|
486
|
+
"how",
|
|
487
|
+
"why",
|
|
488
|
+
"who",
|
|
489
|
+
"what",
|
|
490
|
+
"which",
|
|
491
|
+
"find",
|
|
492
|
+
"discover",
|
|
493
|
+
"explore",
|
|
494
|
+
"trace",
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
task_lower = task.lower()
|
|
498
|
+
|
|
499
|
+
# Check if task involves knowledge graph queries
|
|
500
|
+
uses_graph_keywords = any(keyword in task_lower for keyword in graph_keywords)
|
|
501
|
+
|
|
502
|
+
# If graph reasoning is available and task seems graph-related,
|
|
503
|
+
# prioritize it
|
|
504
|
+
if uses_graph_keywords and "graph_reasoning" in available_tools:
|
|
505
|
+
# Put graph_reasoning first
|
|
506
|
+
selected = ["graph_reasoning"]
|
|
507
|
+
# Add other tools
|
|
508
|
+
selected.extend([t for t in available_tools if t != "graph_reasoning"])
|
|
509
|
+
return selected
|
|
510
|
+
|
|
511
|
+
return available_tools
|
|
512
|
+
|
|
513
|
+
async def _augment_prompt_with_knowledge(self, task: str, context: Optional[Dict[str, Any]] = None) -> str:
|
|
514
|
+
"""
|
|
515
|
+
Augment prompt with relevant knowledge from graph.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
task: Original task
|
|
519
|
+
context: Optional context
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Augmented task with knowledge context
|
|
523
|
+
"""
|
|
524
|
+
if self.graph_store is None or not self.enable_graph_reasoning:
|
|
525
|
+
return task
|
|
526
|
+
|
|
527
|
+
# Check if we have cached knowledge for similar queries
|
|
528
|
+
relevant_knowledge = []
|
|
529
|
+
for query, kg_context in self._knowledge_context.items():
|
|
530
|
+
# Simple keyword matching (could be enhanced with embeddings)
|
|
531
|
+
if any(word in task.lower() for word in query.lower().split()):
|
|
532
|
+
confidence = kg_context.get("confidence", 0.0)
|
|
533
|
+
timestamp = kg_context.get("timestamp")
|
|
534
|
+
relevant_knowledge.append({
|
|
535
|
+
"query": query,
|
|
536
|
+
"answer": kg_context['answer'],
|
|
537
|
+
"confidence": confidence,
|
|
538
|
+
"timestamp": timestamp,
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
if relevant_knowledge:
|
|
542
|
+
# Prioritize knowledge by confidence (relevance) and recency
|
|
543
|
+
# Convert to (item, score) tuples for prioritization
|
|
544
|
+
knowledge_items = []
|
|
545
|
+
for item in relevant_knowledge:
|
|
546
|
+
# Create a simple object with the required attributes
|
|
547
|
+
class KnowledgeItem:
|
|
548
|
+
def __init__(self, data):
|
|
549
|
+
self.data = data
|
|
550
|
+
self.created_at = None
|
|
551
|
+
if data.get("timestamp"):
|
|
552
|
+
try:
|
|
553
|
+
from dateutil import parser # type: ignore[import-untyped]
|
|
554
|
+
self.created_at = parser.parse(data["timestamp"])
|
|
555
|
+
except:
|
|
556
|
+
pass
|
|
557
|
+
|
|
558
|
+
knowledge_items.append((KnowledgeItem(item), item["confidence"]))
|
|
559
|
+
|
|
560
|
+
# Prioritize using our prioritization method
|
|
561
|
+
prioritized = self._prioritize_knowledge_context(
|
|
562
|
+
knowledge_items,
|
|
563
|
+
relevance_weight=0.7, # Favor relevance over recency for knowledge context
|
|
564
|
+
recency_weight=0.3,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
# Format top 3 prioritized items
|
|
568
|
+
formatted_knowledge = []
|
|
569
|
+
for kg_item, priority_score in prioritized[:3]:
|
|
570
|
+
data = kg_item.data
|
|
571
|
+
formatted_knowledge.append(
|
|
572
|
+
f"- {data['query']}: {data['answer']} (confidence: {data['confidence']:.2f})"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
knowledge_section = "\n\nRELEVANT KNOWLEDGE FROM GRAPH:\n" + "\n".join(formatted_knowledge)
|
|
576
|
+
return task + knowledge_section
|
|
577
|
+
|
|
578
|
+
return task
|
|
579
|
+
|
|
580
|
+
async def execute_task(self, task: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
581
|
+
"""
|
|
582
|
+
Execute task with knowledge graph augmentation.
|
|
583
|
+
|
|
584
|
+
Uses knowledge-augmented ReAct loop that includes a RETRIEVE phase.
|
|
585
|
+
|
|
586
|
+
Args:
|
|
587
|
+
task: Task specification with 'description' or 'prompt'
|
|
588
|
+
context: Execution context
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Task execution result
|
|
592
|
+
"""
|
|
593
|
+
# Extract task description
|
|
594
|
+
task_description = task.get("description") or task.get("prompt") or task.get("task")
|
|
595
|
+
if not task_description:
|
|
596
|
+
return await super().execute_task(task, context)
|
|
597
|
+
|
|
598
|
+
# Augment task with knowledge if available
|
|
599
|
+
augmented_task_desc = await self._augment_prompt_with_knowledge(task_description, context)
|
|
600
|
+
|
|
601
|
+
# If task seems graph-related, consult graph first
|
|
602
|
+
if self.graph_store is not None and self.enable_graph_reasoning:
|
|
603
|
+
# Check if this is a direct graph query
|
|
604
|
+
graph_keywords = [
|
|
605
|
+
"connected",
|
|
606
|
+
"connection",
|
|
607
|
+
"relationship",
|
|
608
|
+
"knows",
|
|
609
|
+
"works at",
|
|
610
|
+
]
|
|
611
|
+
if any(keyword in task_description.lower() for keyword in graph_keywords):
|
|
612
|
+
logger.info(f"Consulting knowledge graph for task: {task_description}")
|
|
613
|
+
|
|
614
|
+
# Try graph reasoning
|
|
615
|
+
graph_result = await self._reason_with_graph(augmented_task_desc, context)
|
|
616
|
+
|
|
617
|
+
# If we got a good answer from the graph, use it
|
|
618
|
+
if "answer" in graph_result and graph_result.get("confidence", 0) > 0.7:
|
|
619
|
+
return {
|
|
620
|
+
"success": True,
|
|
621
|
+
"output": graph_result["answer"],
|
|
622
|
+
"confidence": graph_result["confidence"],
|
|
623
|
+
"source": "knowledge_graph",
|
|
624
|
+
"evidence_count": graph_result.get("evidence_count", 0),
|
|
625
|
+
"reasoning_trace": graph_result.get("reasoning_trace", []),
|
|
626
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
# Fall back to standard hybrid agent execution
|
|
630
|
+
# This will use the overridden _react_loop with knowledge retrieval
|
|
631
|
+
# Create modified task dict with augmented description
|
|
632
|
+
augmented_task = task.copy()
|
|
633
|
+
if "description" in task:
|
|
634
|
+
augmented_task["description"] = augmented_task_desc
|
|
635
|
+
elif "prompt" in task:
|
|
636
|
+
augmented_task["prompt"] = augmented_task_desc
|
|
637
|
+
elif "task" in task:
|
|
638
|
+
augmented_task["task"] = augmented_task_desc
|
|
639
|
+
|
|
640
|
+
return await super().execute_task(augmented_task, context)
|
|
641
|
+
|
|
642
|
+
async def execute_task_streaming(self, task: Dict[str, Any], context: Dict[str, Any]) -> AsyncIterator[Dict[str, Any]]:
|
|
643
|
+
"""
|
|
644
|
+
Execute task with streaming knowledge graph events.
|
|
645
|
+
|
|
646
|
+
Extends HybridAgent's streaming to include knowledge retrieval events.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
task: Task specification with 'description' or 'prompt'
|
|
650
|
+
context: Execution context
|
|
651
|
+
|
|
652
|
+
Yields:
|
|
653
|
+
Dict[str, Any]: Event dictionaries including knowledge events
|
|
654
|
+
|
|
655
|
+
Event types:
|
|
656
|
+
- 'knowledge_retrieval_started': Knowledge retrieval initiated
|
|
657
|
+
- 'entity_extraction_completed': Entity extraction finished
|
|
658
|
+
- 'knowledge_cache_hit': Cache hit occurred
|
|
659
|
+
- 'knowledge_retrieval_completed': Knowledge retrieval finished
|
|
660
|
+
- Plus all standard HybridAgent events (status, token, tool_call, etc.)
|
|
661
|
+
"""
|
|
662
|
+
# Store event callback in context for _retrieve_relevant_knowledge to use
|
|
663
|
+
events_queue = []
|
|
664
|
+
|
|
665
|
+
async def event_callback(event: Dict[str, Any]):
|
|
666
|
+
"""Callback to collect knowledge events."""
|
|
667
|
+
events_queue.append(event)
|
|
668
|
+
|
|
669
|
+
# Add callback to context
|
|
670
|
+
context_with_callback = context.copy()
|
|
671
|
+
context_with_callback["_knowledge_event_callback"] = event_callback
|
|
672
|
+
|
|
673
|
+
# Stream from parent class
|
|
674
|
+
async for event in super().execute_task_streaming(task, context_with_callback):
|
|
675
|
+
# Yield any queued knowledge events first
|
|
676
|
+
while events_queue:
|
|
677
|
+
yield events_queue.pop(0)
|
|
678
|
+
|
|
679
|
+
# Then yield the main event
|
|
680
|
+
yield event
|
|
681
|
+
|
|
682
|
+
async def _react_loop(self, task: str, context: Dict[str, Any]) -> Dict[str, Any]:
|
|
683
|
+
"""
|
|
684
|
+
Execute knowledge-augmented ReAct loop: Retrieve → Reason → Act → Observe.
|
|
685
|
+
|
|
686
|
+
Extends the standard ReAct loop with a RETRIEVE phase that fetches
|
|
687
|
+
relevant knowledge from the graph before each reasoning step.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
task: Task description
|
|
691
|
+
context: Context dictionary
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
Result dictionary with 'final_answer', 'steps', 'iterations'
|
|
695
|
+
"""
|
|
696
|
+
steps = []
|
|
697
|
+
tool_calls_count = 0
|
|
698
|
+
total_tokens = 0
|
|
699
|
+
knowledge_retrievals = 0
|
|
700
|
+
|
|
701
|
+
# Build initial messages
|
|
702
|
+
from aiecs.llm import LLMMessage
|
|
703
|
+
|
|
704
|
+
messages = self._build_initial_messages(task, context)
|
|
705
|
+
|
|
706
|
+
for iteration in range(self._max_iterations):
|
|
707
|
+
logger.debug(f"KnowledgeAwareAgent {self.agent_id} - ReAct iteration {iteration + 1}")
|
|
708
|
+
|
|
709
|
+
# RETRIEVE: Get relevant knowledge from graph (if enabled)
|
|
710
|
+
retrieved_knowledge = []
|
|
711
|
+
if self.graph_store is not None and self.enable_graph_reasoning:
|
|
712
|
+
try:
|
|
713
|
+
# Get event callback from context if available
|
|
714
|
+
event_callback = context.get("_knowledge_event_callback")
|
|
715
|
+
retrieved_knowledge = await self._retrieve_relevant_knowledge(
|
|
716
|
+
task, context, iteration, event_callback
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
if retrieved_knowledge:
|
|
720
|
+
knowledge_retrievals += 1
|
|
721
|
+
knowledge_str = self._format_retrieved_knowledge(retrieved_knowledge)
|
|
722
|
+
|
|
723
|
+
steps.append(
|
|
724
|
+
{
|
|
725
|
+
"type": "retrieve",
|
|
726
|
+
"knowledge_count": len(retrieved_knowledge),
|
|
727
|
+
"content": (knowledge_str[:200] + "..." if len(knowledge_str) > 200 else knowledge_str),
|
|
728
|
+
"iteration": iteration + 1,
|
|
729
|
+
}
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# Add knowledge to messages
|
|
733
|
+
messages.append(
|
|
734
|
+
LLMMessage(
|
|
735
|
+
role="system",
|
|
736
|
+
content=f"RETRIEVED KNOWLEDGE:\n{knowledge_str}",
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
except Exception as e:
|
|
740
|
+
logger.warning(f"Knowledge retrieval failed: {e}")
|
|
741
|
+
|
|
742
|
+
# THINK: LLM reasons about next action
|
|
743
|
+
response = await self.llm_client.generate_text(
|
|
744
|
+
messages=messages,
|
|
745
|
+
model=self._config.llm_model,
|
|
746
|
+
temperature=self._config.temperature,
|
|
747
|
+
max_tokens=self._config.max_tokens,
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
thought = response.content
|
|
751
|
+
total_tokens += getattr(response, "total_tokens", 0)
|
|
752
|
+
|
|
753
|
+
steps.append(
|
|
754
|
+
{
|
|
755
|
+
"type": "thought",
|
|
756
|
+
"content": thought,
|
|
757
|
+
"iteration": iteration + 1,
|
|
758
|
+
}
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
# Check if final answer
|
|
762
|
+
if "FINAL ANSWER:" in thought:
|
|
763
|
+
final_answer = self._extract_final_answer(thought)
|
|
764
|
+
return {
|
|
765
|
+
"final_answer": final_answer,
|
|
766
|
+
"steps": steps,
|
|
767
|
+
"iterations": iteration + 1,
|
|
768
|
+
"tool_calls_count": tool_calls_count,
|
|
769
|
+
"knowledge_retrievals": knowledge_retrievals,
|
|
770
|
+
"total_tokens": total_tokens,
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
# Check if tool call
|
|
774
|
+
if "TOOL:" in thought:
|
|
775
|
+
# ACT: Execute tool
|
|
776
|
+
try:
|
|
777
|
+
tool_info = self._parse_tool_call(thought)
|
|
778
|
+
tool_result = await self._execute_tool(
|
|
779
|
+
tool_info["tool"],
|
|
780
|
+
tool_info.get("operation"),
|
|
781
|
+
tool_info.get("parameters", {}),
|
|
782
|
+
)
|
|
783
|
+
tool_calls_count += 1
|
|
784
|
+
|
|
785
|
+
steps.append(
|
|
786
|
+
{
|
|
787
|
+
"type": "action",
|
|
788
|
+
"tool": tool_info["tool"],
|
|
789
|
+
"operation": tool_info.get("operation"),
|
|
790
|
+
"parameters": tool_info.get("parameters"),
|
|
791
|
+
"iteration": iteration + 1,
|
|
792
|
+
}
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
# OBSERVE: Add tool result to conversation
|
|
796
|
+
observation = f"OBSERVATION: Tool '{tool_info['tool']}' returned: {tool_result}"
|
|
797
|
+
steps.append(
|
|
798
|
+
{
|
|
799
|
+
"type": "observation",
|
|
800
|
+
"content": observation,
|
|
801
|
+
"iteration": iteration + 1,
|
|
802
|
+
}
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
# Add to messages for next iteration
|
|
806
|
+
messages.append(LLMMessage(role="assistant", content=thought))
|
|
807
|
+
messages.append(LLMMessage(role="user", content=observation))
|
|
808
|
+
|
|
809
|
+
except Exception as e:
|
|
810
|
+
error_msg = f"OBSERVATION: Tool execution failed: {str(e)}"
|
|
811
|
+
steps.append(
|
|
812
|
+
{
|
|
813
|
+
"type": "observation",
|
|
814
|
+
"content": error_msg,
|
|
815
|
+
"iteration": iteration + 1,
|
|
816
|
+
"error": True,
|
|
817
|
+
}
|
|
818
|
+
)
|
|
819
|
+
messages.append(LLMMessage(role="assistant", content=thought))
|
|
820
|
+
messages.append(LLMMessage(role="user", content=error_msg))
|
|
821
|
+
|
|
822
|
+
else:
|
|
823
|
+
# LLM didn't provide clear action - treat as final answer
|
|
824
|
+
return {
|
|
825
|
+
"final_answer": thought,
|
|
826
|
+
"steps": steps,
|
|
827
|
+
"iterations": iteration + 1,
|
|
828
|
+
"tool_calls_count": tool_calls_count,
|
|
829
|
+
"knowledge_retrievals": knowledge_retrievals,
|
|
830
|
+
"total_tokens": total_tokens,
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
# Max iterations reached
|
|
834
|
+
logger.warning(f"KnowledgeAwareAgent {self.agent_id} reached max iterations")
|
|
835
|
+
return {
|
|
836
|
+
"final_answer": "Max iterations reached. Unable to complete task fully.",
|
|
837
|
+
"steps": steps,
|
|
838
|
+
"iterations": self._max_iterations,
|
|
839
|
+
"tool_calls_count": tool_calls_count,
|
|
840
|
+
"knowledge_retrievals": knowledge_retrievals,
|
|
841
|
+
"total_tokens": total_tokens,
|
|
842
|
+
"max_iterations_reached": True,
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async def _retrieve_relevant_knowledge(
|
|
846
|
+
self,
|
|
847
|
+
task: str,
|
|
848
|
+
context: Dict[str, Any],
|
|
849
|
+
iteration: int,
|
|
850
|
+
event_callback: Optional[Callable[[Dict[str, Any]], Awaitable[None]]] = None
|
|
851
|
+
) -> List[Entity]:
|
|
852
|
+
"""
|
|
853
|
+
Retrieve relevant knowledge for the current reasoning step.
|
|
854
|
+
|
|
855
|
+
Uses HybridSearchStrategy to retrieve relevant entities from the knowledge graph
|
|
856
|
+
based on semantic similarity and graph structure.
|
|
857
|
+
|
|
858
|
+
Implements retry logic with exponential backoff and circuit breaker pattern
|
|
859
|
+
for resilience against transient failures.
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
task: Task description
|
|
863
|
+
context: Context dictionary
|
|
864
|
+
iteration: Current iteration number
|
|
865
|
+
event_callback: Optional async callback for streaming events
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
List of relevant entities
|
|
869
|
+
"""
|
|
870
|
+
# Return empty if hybrid search not available
|
|
871
|
+
if self._hybrid_search is None or self.graph_store is None:
|
|
872
|
+
return []
|
|
873
|
+
|
|
874
|
+
# Circuit breaker: if open, return empty results immediately
|
|
875
|
+
if self._circuit_breaker_open:
|
|
876
|
+
logger.warning(
|
|
877
|
+
f"Circuit breaker is OPEN - skipping knowledge retrieval "
|
|
878
|
+
f"(failures: {self._circuit_breaker_failures}/{self._circuit_breaker_threshold})"
|
|
879
|
+
)
|
|
880
|
+
return []
|
|
881
|
+
|
|
882
|
+
# Start timing
|
|
883
|
+
start_time = time.time()
|
|
884
|
+
|
|
885
|
+
# Emit knowledge_retrieval_started event
|
|
886
|
+
if event_callback:
|
|
887
|
+
await event_callback({
|
|
888
|
+
"type": "knowledge_retrieval_started",
|
|
889
|
+
"query": task,
|
|
890
|
+
"iteration": iteration,
|
|
891
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
892
|
+
})
|
|
893
|
+
|
|
894
|
+
try:
|
|
895
|
+
# Step 1: Extract entities from task description (with caching)
|
|
896
|
+
# Check if seed entities are provided in context first
|
|
897
|
+
seed_entity_ids = context.get("seed_entity_ids")
|
|
898
|
+
if not seed_entity_ids:
|
|
899
|
+
seed_entity_ids = await self._extract_seed_entities(task)
|
|
900
|
+
|
|
901
|
+
# Emit entity_extraction_completed event
|
|
902
|
+
if event_callback:
|
|
903
|
+
await event_callback({
|
|
904
|
+
"type": "entity_extraction_completed",
|
|
905
|
+
"entity_ids": seed_entity_ids if seed_entity_ids else [],
|
|
906
|
+
"entity_count": len(seed_entity_ids) if seed_entity_ids else 0,
|
|
907
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
# Step 2: Determine retrieval strategy
|
|
911
|
+
strategy = getattr(self._config, "retrieval_strategy", "hybrid")
|
|
912
|
+
if "retrieval_strategy" in context:
|
|
913
|
+
strategy = context["retrieval_strategy"]
|
|
914
|
+
|
|
915
|
+
# Step 3: Check cache for this query
|
|
916
|
+
cache_key = self._generate_cache_key("knowledge_retrieval", {"task": task, "strategy": strategy})
|
|
917
|
+
cached_entities = await self._get_cached_knowledge(cache_key)
|
|
918
|
+
if cached_entities is not None:
|
|
919
|
+
logger.debug(f"Cache hit for knowledge retrieval (key: {cache_key})")
|
|
920
|
+
self._cache_hits += 1
|
|
921
|
+
|
|
922
|
+
# Emit knowledge_cache_hit event
|
|
923
|
+
if event_callback:
|
|
924
|
+
await event_callback({
|
|
925
|
+
"type": "knowledge_cache_hit",
|
|
926
|
+
"cache_key": cache_key,
|
|
927
|
+
"entity_count": len(cached_entities),
|
|
928
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
929
|
+
})
|
|
930
|
+
|
|
931
|
+
# Update metrics
|
|
932
|
+
self._update_graph_metrics(
|
|
933
|
+
query_time=time.time() - start_time,
|
|
934
|
+
entities_count=len(cached_entities),
|
|
935
|
+
strategy=strategy,
|
|
936
|
+
cache_hit=True,
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
# Emit knowledge_retrieval_completed event
|
|
940
|
+
if event_callback:
|
|
941
|
+
await event_callback({
|
|
942
|
+
"type": "knowledge_retrieval_completed",
|
|
943
|
+
"entity_count": len(cached_entities),
|
|
944
|
+
"retrieval_time_ms": (time.time() - start_time) * 1000,
|
|
945
|
+
"cache_hit": True,
|
|
946
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
return cached_entities
|
|
950
|
+
|
|
951
|
+
# Cache miss - proceed with retrieval
|
|
952
|
+
logger.debug(f"Cache miss for knowledge retrieval (key: {cache_key})")
|
|
953
|
+
self._cache_misses += 1
|
|
954
|
+
|
|
955
|
+
# Step 4: Configure search mode based on agent config
|
|
956
|
+
from aiecs.application.knowledge_graph.search.hybrid_search import (
|
|
957
|
+
HybridSearchConfig,
|
|
958
|
+
SearchMode,
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
# Convert strategy to search mode
|
|
962
|
+
search_mode = self._select_search_mode(strategy, task)
|
|
963
|
+
|
|
964
|
+
# Step 5: Generate embedding for task description (not required for graph-only if we have seeds)
|
|
965
|
+
query_embedding = None
|
|
966
|
+
if search_mode != SearchMode.GRAPH_ONLY or not seed_entity_ids:
|
|
967
|
+
# Need embedding for vector search or hybrid search, or if no seed entities for graph search
|
|
968
|
+
query_embedding = await self._get_query_embedding(task)
|
|
969
|
+
if not query_embedding and search_mode != SearchMode.GRAPH_ONLY:
|
|
970
|
+
logger.warning("Failed to generate query embedding, returning empty results")
|
|
971
|
+
return []
|
|
972
|
+
elif not query_embedding and search_mode == SearchMode.GRAPH_ONLY and not seed_entity_ids:
|
|
973
|
+
logger.warning("Failed to generate query embedding and no seed entities available for graph search")
|
|
974
|
+
return []
|
|
975
|
+
|
|
976
|
+
# Step 6: Create search configuration
|
|
977
|
+
max_results = getattr(self._config, "max_context_size", 10)
|
|
978
|
+
config = HybridSearchConfig(
|
|
979
|
+
mode=search_mode,
|
|
980
|
+
vector_weight=0.6,
|
|
981
|
+
graph_weight=0.4,
|
|
982
|
+
max_results=max_results,
|
|
983
|
+
vector_threshold=0.0,
|
|
984
|
+
max_graph_depth=2,
|
|
985
|
+
expand_results=True,
|
|
986
|
+
min_combined_score=0.0,
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
# Step 7: Execute hybrid search with retry logic
|
|
990
|
+
async def _execute_search():
|
|
991
|
+
"""Execute search with retry support"""
|
|
992
|
+
return await self._hybrid_search.search(
|
|
993
|
+
query_embedding=query_embedding,
|
|
994
|
+
config=config,
|
|
995
|
+
seed_entity_ids=seed_entity_ids if seed_entity_ids else None,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
# Retry on connection, query, and timeout errors
|
|
999
|
+
results = await self._retry_handler.execute(
|
|
1000
|
+
_execute_search,
|
|
1001
|
+
retry_on=[
|
|
1002
|
+
GraphStoreConnectionError,
|
|
1003
|
+
GraphStoreQueryError,
|
|
1004
|
+
GraphStoreTimeoutError,
|
|
1005
|
+
],
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
# Step 8: Extract entities from results
|
|
1009
|
+
entities = [entity for entity, score in results]
|
|
1010
|
+
|
|
1011
|
+
logger.debug(
|
|
1012
|
+
f"Retrieved {len(entities)} entities using {search_mode.value} search "
|
|
1013
|
+
f"(iteration {iteration})"
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
# Reset circuit breaker on successful retrieval
|
|
1017
|
+
if self._circuit_breaker_failures > 0:
|
|
1018
|
+
logger.info(
|
|
1019
|
+
f"Knowledge retrieval succeeded - resetting circuit breaker "
|
|
1020
|
+
f"(was at {self._circuit_breaker_failures} failures)"
|
|
1021
|
+
)
|
|
1022
|
+
self._circuit_breaker_failures = 0
|
|
1023
|
+
|
|
1024
|
+
# Step 9: Apply context prioritization and pruning
|
|
1025
|
+
# First prioritize by relevance + recency
|
|
1026
|
+
prioritized_entities = self._prioritize_knowledge_context(
|
|
1027
|
+
entities,
|
|
1028
|
+
relevance_weight=self._relevance_weight,
|
|
1029
|
+
recency_weight=self._recency_weight,
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
# Then prune to keep only the most relevant
|
|
1033
|
+
pruned_entities_with_scores = self._prune_knowledge_context(
|
|
1034
|
+
prioritized_entities,
|
|
1035
|
+
max_context_size=self._max_context_size,
|
|
1036
|
+
relevance_threshold=self._relevance_threshold,
|
|
1037
|
+
max_age_seconds=None, # No age limit by default
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
# Extract entities from (Entity, score) tuples for caching
|
|
1041
|
+
pruned_entities = [
|
|
1042
|
+
entity if isinstance(entity, Entity) else entity[0]
|
|
1043
|
+
for entity in pruned_entities_with_scores
|
|
1044
|
+
]
|
|
1045
|
+
|
|
1046
|
+
logger.debug(
|
|
1047
|
+
f"Context management: {len(entities)} → {len(prioritized_entities)} prioritized → "
|
|
1048
|
+
f"{len(pruned_entities)} pruned"
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
# Step 10: Cache the pruned results
|
|
1052
|
+
await self._cache_knowledge(cache_key, pruned_entities)
|
|
1053
|
+
|
|
1054
|
+
# Step 11: Update metrics
|
|
1055
|
+
query_time = time.time() - start_time
|
|
1056
|
+
self._update_graph_metrics(
|
|
1057
|
+
query_time=query_time,
|
|
1058
|
+
entities_count=len(pruned_entities),
|
|
1059
|
+
strategy=strategy,
|
|
1060
|
+
cache_hit=False,
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
# Emit knowledge_retrieval_completed event
|
|
1064
|
+
if event_callback:
|
|
1065
|
+
# Calculate average relevance score
|
|
1066
|
+
avg_score = 0.0
|
|
1067
|
+
if results:
|
|
1068
|
+
avg_score = sum(score for _, score in results) / len(results)
|
|
1069
|
+
|
|
1070
|
+
await event_callback({
|
|
1071
|
+
"type": "knowledge_retrieval_completed",
|
|
1072
|
+
"entity_count": len(pruned_entities),
|
|
1073
|
+
"retrieval_time_ms": query_time * 1000,
|
|
1074
|
+
"cache_hit": False,
|
|
1075
|
+
"average_relevance_score": avg_score,
|
|
1076
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
return pruned_entities
|
|
1080
|
+
|
|
1081
|
+
except Exception as e:
|
|
1082
|
+
# Increment circuit breaker failure count
|
|
1083
|
+
self._circuit_breaker_failures += 1
|
|
1084
|
+
|
|
1085
|
+
logger.error(
|
|
1086
|
+
f"Error retrieving knowledge (failure {self._circuit_breaker_failures}/"
|
|
1087
|
+
f"{self._circuit_breaker_threshold}): {e}",
|
|
1088
|
+
exc_info=True
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
# Open circuit breaker if threshold reached
|
|
1092
|
+
if self._circuit_breaker_failures >= self._circuit_breaker_threshold:
|
|
1093
|
+
self._circuit_breaker_open = True
|
|
1094
|
+
logger.error(
|
|
1095
|
+
f"Circuit breaker OPENED after {self._circuit_breaker_failures} consecutive failures. "
|
|
1096
|
+
f"Knowledge retrieval will be disabled until manual reset."
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
# Fallback to empty results
|
|
1100
|
+
return []
|
|
1101
|
+
|
|
1102
|
+
async def _get_query_embedding(self, query: str) -> Optional[List[float]]:
|
|
1103
|
+
"""
|
|
1104
|
+
Generate embedding for query text.
|
|
1105
|
+
|
|
1106
|
+
Args:
|
|
1107
|
+
query: Query text
|
|
1108
|
+
|
|
1109
|
+
Returns:
|
|
1110
|
+
Embedding vector or None if generation fails
|
|
1111
|
+
"""
|
|
1112
|
+
try:
|
|
1113
|
+
# Use LLM client to generate embeddings
|
|
1114
|
+
# Check if client supports embeddings (check both method existence and callability)
|
|
1115
|
+
if not hasattr(self.llm_client, "get_embeddings"):
|
|
1116
|
+
logger.warning(
|
|
1117
|
+
f"LLM client ({type(self.llm_client).__name__}) does not support embeddings. "
|
|
1118
|
+
f"Available methods: {[m for m in dir(self.llm_client) if not m.startswith('_')]}"
|
|
1119
|
+
)
|
|
1120
|
+
return None
|
|
1121
|
+
|
|
1122
|
+
# Verify the method is callable
|
|
1123
|
+
get_embeddings_method = getattr(self.llm_client, "get_embeddings", None)
|
|
1124
|
+
if not callable(get_embeddings_method):
|
|
1125
|
+
logger.warning(
|
|
1126
|
+
f"LLM client ({type(self.llm_client).__name__}) has 'get_embeddings' attribute but it's not callable"
|
|
1127
|
+
)
|
|
1128
|
+
return None
|
|
1129
|
+
|
|
1130
|
+
embeddings = await self.llm_client.get_embeddings(
|
|
1131
|
+
texts=[query],
|
|
1132
|
+
model=None, # Use default embedding model
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
if embeddings and len(embeddings) > 0:
|
|
1136
|
+
return embeddings[0]
|
|
1137
|
+
|
|
1138
|
+
return None
|
|
1139
|
+
|
|
1140
|
+
except Exception as e:
|
|
1141
|
+
logger.error(f"Failed to generate query embedding: {e}", exc_info=True)
|
|
1142
|
+
return None
|
|
1143
|
+
|
|
1144
|
+
async def _extract_seed_entities(self, task: str) -> List[str]:
|
|
1145
|
+
"""
|
|
1146
|
+
Extract entities from task description to use as seed entities for graph traversal.
|
|
1147
|
+
|
|
1148
|
+
Uses caching to avoid redundant LLM calls for the same task.
|
|
1149
|
+
|
|
1150
|
+
Args:
|
|
1151
|
+
task: Task description
|
|
1152
|
+
|
|
1153
|
+
Returns:
|
|
1154
|
+
List of entity IDs to use as seed entities
|
|
1155
|
+
"""
|
|
1156
|
+
# Check cache first
|
|
1157
|
+
if task in self._entity_extraction_cache:
|
|
1158
|
+
cached_entities = self._entity_extraction_cache[task]
|
|
1159
|
+
logger.debug(f"Using cached entity extraction for task (found {len(cached_entities)} entities)")
|
|
1160
|
+
return [e.id for e in cached_entities]
|
|
1161
|
+
|
|
1162
|
+
# Return empty if entity extractor not available
|
|
1163
|
+
if self._entity_extractor is None:
|
|
1164
|
+
logger.debug("Entity extractor not available, skipping entity extraction")
|
|
1165
|
+
return []
|
|
1166
|
+
|
|
1167
|
+
try:
|
|
1168
|
+
# Extract entities from task description with timing
|
|
1169
|
+
extraction_start = time.time()
|
|
1170
|
+
entities = await self._entity_extractor.extract_entities(task)
|
|
1171
|
+
extraction_time = time.time() - extraction_start
|
|
1172
|
+
|
|
1173
|
+
# Update extraction metrics
|
|
1174
|
+
self._graph_metrics.entity_extraction_count += 1
|
|
1175
|
+
self._graph_metrics.total_extraction_time += extraction_time
|
|
1176
|
+
self._graph_metrics.average_extraction_time = (
|
|
1177
|
+
self._graph_metrics.total_extraction_time / self._graph_metrics.entity_extraction_count
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
# Record to Prometheus if enabled
|
|
1181
|
+
if self._prometheus_enabled and self._prometheus_metrics is not None:
|
|
1182
|
+
try:
|
|
1183
|
+
self._prometheus_metrics["entity_extraction_total"].labels(
|
|
1184
|
+
agent_id=self.agent_id,
|
|
1185
|
+
).inc()
|
|
1186
|
+
self._prometheus_metrics["entity_extraction_duration"].labels(
|
|
1187
|
+
agent_id=self.agent_id,
|
|
1188
|
+
).observe(extraction_time)
|
|
1189
|
+
except Exception as e:
|
|
1190
|
+
logger.warning(f"Failed to record entity extraction Prometheus metrics: {e}")
|
|
1191
|
+
|
|
1192
|
+
# Cache the results
|
|
1193
|
+
self._entity_extraction_cache[task] = entities
|
|
1194
|
+
|
|
1195
|
+
# Convert to entity IDs
|
|
1196
|
+
entity_ids = [e.id for e in entities]
|
|
1197
|
+
|
|
1198
|
+
if entity_ids:
|
|
1199
|
+
logger.debug(f"Extracted {len(entity_ids)} seed entities from task: {entity_ids[:5]} (took {extraction_time:.3f}s)")
|
|
1200
|
+
else:
|
|
1201
|
+
logger.debug("No entities extracted from task")
|
|
1202
|
+
|
|
1203
|
+
return entity_ids
|
|
1204
|
+
|
|
1205
|
+
except Exception as e:
|
|
1206
|
+
logger.warning(f"Failed to extract entities from task: {e}")
|
|
1207
|
+
return []
|
|
1208
|
+
|
|
1209
|
+
def _select_search_mode(self, strategy: str, task: str) -> "SearchMode":
|
|
1210
|
+
"""
|
|
1211
|
+
Select search mode based on retrieval strategy and task analysis.
|
|
1212
|
+
|
|
1213
|
+
Supports automatic strategy selection based on query keywords when strategy is "auto".
|
|
1214
|
+
|
|
1215
|
+
Args:
|
|
1216
|
+
strategy: Retrieval strategy ("vector", "graph", "hybrid", or "auto")
|
|
1217
|
+
task: Task description for auto-selection analysis
|
|
1218
|
+
|
|
1219
|
+
Returns:
|
|
1220
|
+
SearchMode enum value
|
|
1221
|
+
"""
|
|
1222
|
+
from aiecs.application.knowledge_graph.search.hybrid_search import SearchMode
|
|
1223
|
+
|
|
1224
|
+
# Handle explicit strategies
|
|
1225
|
+
if strategy == "vector":
|
|
1226
|
+
return SearchMode.VECTOR_ONLY
|
|
1227
|
+
elif strategy == "graph":
|
|
1228
|
+
return SearchMode.GRAPH_ONLY
|
|
1229
|
+
elif strategy == "hybrid":
|
|
1230
|
+
return SearchMode.HYBRID
|
|
1231
|
+
elif strategy == "auto":
|
|
1232
|
+
# Auto-select based on task analysis
|
|
1233
|
+
return self._auto_select_search_mode(task)
|
|
1234
|
+
else:
|
|
1235
|
+
# Default to hybrid for unknown strategies
|
|
1236
|
+
logger.warning(f"Unknown retrieval strategy '{strategy}', defaulting to hybrid")
|
|
1237
|
+
return SearchMode.HYBRID
|
|
1238
|
+
|
|
1239
|
+
def _auto_select_search_mode(self, task: str) -> "SearchMode":
|
|
1240
|
+
"""
|
|
1241
|
+
Automatically select search mode based on task analysis.
|
|
1242
|
+
|
|
1243
|
+
Uses keyword matching to determine the most appropriate search mode:
|
|
1244
|
+
- Relationship/connection keywords → GRAPH mode
|
|
1245
|
+
- Semantic/conceptual keywords → VECTOR mode
|
|
1246
|
+
- Default → HYBRID mode
|
|
1247
|
+
|
|
1248
|
+
Args:
|
|
1249
|
+
task: Task description
|
|
1250
|
+
|
|
1251
|
+
Returns:
|
|
1252
|
+
SearchMode enum value
|
|
1253
|
+
"""
|
|
1254
|
+
from aiecs.application.knowledge_graph.search.hybrid_search import SearchMode
|
|
1255
|
+
|
|
1256
|
+
task_lower = task.lower()
|
|
1257
|
+
|
|
1258
|
+
# Keywords indicating graph traversal is preferred
|
|
1259
|
+
graph_keywords = [
|
|
1260
|
+
"related", "connected", "relationship", "link", "path", "neighbor",
|
|
1261
|
+
"upstream", "downstream", "dependency", "depends on", "used by",
|
|
1262
|
+
"parent", "child", "ancestor", "descendant", "connected to"
|
|
1263
|
+
]
|
|
1264
|
+
|
|
1265
|
+
# Keywords indicating semantic search is preferred
|
|
1266
|
+
vector_keywords = [
|
|
1267
|
+
"similar", "like", "about", "concept", "topic", "meaning",
|
|
1268
|
+
"semantic", "understand", "explain", "describe", "what is"
|
|
1269
|
+
]
|
|
1270
|
+
|
|
1271
|
+
# Check for graph keywords
|
|
1272
|
+
if any(keyword in task_lower for keyword in graph_keywords):
|
|
1273
|
+
logger.debug(f"Auto-selected GRAPH mode based on task keywords")
|
|
1274
|
+
return SearchMode.GRAPH_ONLY
|
|
1275
|
+
|
|
1276
|
+
# Check for vector keywords
|
|
1277
|
+
if any(keyword in task_lower for keyword in vector_keywords):
|
|
1278
|
+
logger.debug(f"Auto-selected VECTOR mode based on task keywords")
|
|
1279
|
+
return SearchMode.VECTOR_ONLY
|
|
1280
|
+
|
|
1281
|
+
# Default to hybrid mode
|
|
1282
|
+
logger.debug(f"Auto-selected HYBRID mode (default)")
|
|
1283
|
+
return SearchMode.HYBRID
|
|
1284
|
+
|
|
1285
|
+
def _generate_cache_key(self, tool_name: str, parameters: Dict[str, Any]) -> str:
|
|
1286
|
+
"""
|
|
1287
|
+
Generate cache key for knowledge retrieval.
|
|
1288
|
+
|
|
1289
|
+
Overrides base class method to handle knowledge retrieval cache keys.
|
|
1290
|
+
Expects parameters dict with 'task' and 'strategy' keys.
|
|
1291
|
+
|
|
1292
|
+
Args:
|
|
1293
|
+
tool_name: Name of the tool (unused for knowledge retrieval)
|
|
1294
|
+
parameters: Tool parameters dict containing 'task' and 'strategy'
|
|
1295
|
+
|
|
1296
|
+
Returns:
|
|
1297
|
+
Cache key string
|
|
1298
|
+
"""
|
|
1299
|
+
import hashlib
|
|
1300
|
+
|
|
1301
|
+
# Extract task and strategy from parameters
|
|
1302
|
+
task = parameters.get("task", "")
|
|
1303
|
+
strategy = parameters.get("strategy", "")
|
|
1304
|
+
|
|
1305
|
+
# Create hash of task description
|
|
1306
|
+
task_hash = hashlib.md5(task.encode()).hexdigest()[:16]
|
|
1307
|
+
|
|
1308
|
+
# Combine with strategy
|
|
1309
|
+
cache_key = f"knowledge:{task_hash}:{strategy}"
|
|
1310
|
+
|
|
1311
|
+
return cache_key
|
|
1312
|
+
|
|
1313
|
+
async def _get_cached_knowledge(self, cache_key: str) -> Optional[List[Entity]]:
|
|
1314
|
+
"""
|
|
1315
|
+
Get cached knowledge retrieval results.
|
|
1316
|
+
|
|
1317
|
+
Args:
|
|
1318
|
+
cache_key: Cache key
|
|
1319
|
+
|
|
1320
|
+
Returns:
|
|
1321
|
+
List of cached entities or None if not cached
|
|
1322
|
+
"""
|
|
1323
|
+
if self._graph_cache is None or not self._graph_cache._initialized:
|
|
1324
|
+
return None
|
|
1325
|
+
|
|
1326
|
+
try:
|
|
1327
|
+
# Get from cache
|
|
1328
|
+
cached_data = await self._graph_cache.backend.get(cache_key)
|
|
1329
|
+
if cached_data is None:
|
|
1330
|
+
return None
|
|
1331
|
+
|
|
1332
|
+
# Deserialize entities
|
|
1333
|
+
import json
|
|
1334
|
+
entity_dicts = json.loads(cached_data)
|
|
1335
|
+
|
|
1336
|
+
# Convert back to Entity objects
|
|
1337
|
+
entities = []
|
|
1338
|
+
for entity_dict in entity_dicts:
|
|
1339
|
+
entity = Entity(
|
|
1340
|
+
id=entity_dict["id"],
|
|
1341
|
+
entity_type=entity_dict["entity_type"],
|
|
1342
|
+
properties=entity_dict.get("properties", {}),
|
|
1343
|
+
embedding=entity_dict.get("embedding"),
|
|
1344
|
+
)
|
|
1345
|
+
entities.append(entity)
|
|
1346
|
+
|
|
1347
|
+
return entities
|
|
1348
|
+
|
|
1349
|
+
except Exception as e:
|
|
1350
|
+
logger.warning(f"Failed to get cached knowledge: {e}")
|
|
1351
|
+
return None
|
|
1352
|
+
|
|
1353
|
+
async def _cache_knowledge(self, cache_key: str, entities: List[Entity]) -> None:
|
|
1354
|
+
"""
|
|
1355
|
+
Cache knowledge retrieval results.
|
|
1356
|
+
|
|
1357
|
+
Args:
|
|
1358
|
+
cache_key: Cache key
|
|
1359
|
+
entities: Entities to cache
|
|
1360
|
+
"""
|
|
1361
|
+
if self._graph_cache is None or not self._graph_cache._initialized:
|
|
1362
|
+
return
|
|
1363
|
+
|
|
1364
|
+
try:
|
|
1365
|
+
# Serialize entities to JSON
|
|
1366
|
+
import json
|
|
1367
|
+
entity_dicts = []
|
|
1368
|
+
for entity in entities:
|
|
1369
|
+
entity_dict = {
|
|
1370
|
+
"id": entity.id,
|
|
1371
|
+
"entity_type": entity.entity_type,
|
|
1372
|
+
"properties": entity.properties,
|
|
1373
|
+
"embedding": entity.embedding,
|
|
1374
|
+
}
|
|
1375
|
+
entity_dicts.append(entity_dict)
|
|
1376
|
+
|
|
1377
|
+
cached_data = json.dumps(entity_dicts)
|
|
1378
|
+
|
|
1379
|
+
# Store in cache with TTL
|
|
1380
|
+
ttl = getattr(self._config, "cache_ttl", 300)
|
|
1381
|
+
await self._graph_cache.backend.set(cache_key, cached_data, ttl)
|
|
1382
|
+
|
|
1383
|
+
logger.debug(f"Cached {len(entities)} entities (key: {cache_key}, TTL: {ttl}s)")
|
|
1384
|
+
|
|
1385
|
+
except Exception as e:
|
|
1386
|
+
logger.warning(f"Failed to cache knowledge: {e}")
|
|
1387
|
+
|
|
1388
|
+
def _format_retrieved_knowledge(self, entities: List[Entity]) -> str:
|
|
1389
|
+
"""
|
|
1390
|
+
Format retrieved knowledge entities for inclusion in prompt.
|
|
1391
|
+
|
|
1392
|
+
Args:
|
|
1393
|
+
entities: List of entities retrieved from graph
|
|
1394
|
+
|
|
1395
|
+
Returns:
|
|
1396
|
+
Formatted knowledge string
|
|
1397
|
+
"""
|
|
1398
|
+
if not entities:
|
|
1399
|
+
return ""
|
|
1400
|
+
|
|
1401
|
+
lines = []
|
|
1402
|
+
for entity in entities:
|
|
1403
|
+
entity_str = f"- {entity.entity_type}: {entity.id}"
|
|
1404
|
+
if entity.properties:
|
|
1405
|
+
props_str = ", ".join(f"{k}={v}" for k, v in entity.properties.items())
|
|
1406
|
+
entity_str += f" ({props_str})"
|
|
1407
|
+
lines.append(entity_str)
|
|
1408
|
+
|
|
1409
|
+
return "\n".join(lines)
|
|
1410
|
+
|
|
1411
|
+
def _update_graph_metrics(
|
|
1412
|
+
self,
|
|
1413
|
+
query_time: float,
|
|
1414
|
+
entities_count: int,
|
|
1415
|
+
strategy: str,
|
|
1416
|
+
cache_hit: bool,
|
|
1417
|
+
relationships_count: int = 0,
|
|
1418
|
+
) -> None:
|
|
1419
|
+
"""
|
|
1420
|
+
Update graph metrics after a retrieval operation.
|
|
1421
|
+
|
|
1422
|
+
Args:
|
|
1423
|
+
query_time: Time taken for the query in seconds
|
|
1424
|
+
entities_count: Number of entities retrieved
|
|
1425
|
+
strategy: Retrieval strategy used
|
|
1426
|
+
cache_hit: Whether this was a cache hit
|
|
1427
|
+
relationships_count: Number of relationships traversed
|
|
1428
|
+
"""
|
|
1429
|
+
# Update query counts
|
|
1430
|
+
self._graph_metrics.total_graph_queries += 1
|
|
1431
|
+
self._graph_metrics.total_entities_retrieved += entities_count
|
|
1432
|
+
self._graph_metrics.total_relationships_traversed += relationships_count
|
|
1433
|
+
|
|
1434
|
+
# Update timing metrics
|
|
1435
|
+
self._graph_metrics.total_graph_query_time += query_time
|
|
1436
|
+
self._graph_metrics.average_graph_query_time = (
|
|
1437
|
+
self._graph_metrics.total_graph_query_time / self._graph_metrics.total_graph_queries
|
|
1438
|
+
)
|
|
1439
|
+
|
|
1440
|
+
# Update min/max query times
|
|
1441
|
+
if self._graph_metrics.min_graph_query_time is None or query_time < self._graph_metrics.min_graph_query_time:
|
|
1442
|
+
self._graph_metrics.min_graph_query_time = query_time
|
|
1443
|
+
if self._graph_metrics.max_graph_query_time is None or query_time > self._graph_metrics.max_graph_query_time:
|
|
1444
|
+
self._graph_metrics.max_graph_query_time = query_time
|
|
1445
|
+
|
|
1446
|
+
# Update cache metrics
|
|
1447
|
+
if cache_hit:
|
|
1448
|
+
self._graph_metrics.cache_hits += 1
|
|
1449
|
+
else:
|
|
1450
|
+
self._graph_metrics.cache_misses += 1
|
|
1451
|
+
|
|
1452
|
+
total_cache_requests = self._graph_metrics.cache_hits + self._graph_metrics.cache_misses
|
|
1453
|
+
if total_cache_requests > 0:
|
|
1454
|
+
self._graph_metrics.cache_hit_rate = self._graph_metrics.cache_hits / total_cache_requests
|
|
1455
|
+
|
|
1456
|
+
# Update strategy counts
|
|
1457
|
+
strategy_lower = strategy.lower()
|
|
1458
|
+
if "vector" in strategy_lower:
|
|
1459
|
+
self._graph_metrics.vector_search_count += 1
|
|
1460
|
+
elif "graph" in strategy_lower:
|
|
1461
|
+
self._graph_metrics.graph_search_count += 1
|
|
1462
|
+
elif "hybrid" in strategy_lower:
|
|
1463
|
+
self._graph_metrics.hybrid_search_count += 1
|
|
1464
|
+
|
|
1465
|
+
# Update timestamp
|
|
1466
|
+
self._graph_metrics.updated_at = datetime.utcnow()
|
|
1467
|
+
|
|
1468
|
+
# Record to Prometheus if enabled
|
|
1469
|
+
self._record_prometheus_metrics(
|
|
1470
|
+
query_time=query_time,
|
|
1471
|
+
entities_count=entities_count,
|
|
1472
|
+
strategy=strategy,
|
|
1473
|
+
cache_hit=cache_hit,
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
def get_knowledge_context(self) -> Dict[str, Any]:
|
|
1477
|
+
"""
|
|
1478
|
+
Get accumulated knowledge context.
|
|
1479
|
+
|
|
1480
|
+
Returns:
|
|
1481
|
+
Dictionary of accumulated knowledge
|
|
1482
|
+
"""
|
|
1483
|
+
return self._knowledge_context.copy()
|
|
1484
|
+
|
|
1485
|
+
def clear_knowledge_context(self) -> None:
|
|
1486
|
+
"""Clear accumulated knowledge context."""
|
|
1487
|
+
self._knowledge_context.clear()
|
|
1488
|
+
logger.debug(f"Cleared knowledge context for agent {self.agent_id}")
|
|
1489
|
+
|
|
1490
|
+
def _prune_knowledge_context(
|
|
1491
|
+
self,
|
|
1492
|
+
entities: List[Any],
|
|
1493
|
+
max_context_size: int = 50,
|
|
1494
|
+
relevance_threshold: float = 0.3,
|
|
1495
|
+
max_age_seconds: Optional[int] = None,
|
|
1496
|
+
) -> List[Any]:
|
|
1497
|
+
"""
|
|
1498
|
+
Prune knowledge context based on relevance and recency.
|
|
1499
|
+
|
|
1500
|
+
This method filters entities to keep only the most relevant and recent ones,
|
|
1501
|
+
preventing context overflow and improving retrieval quality.
|
|
1502
|
+
|
|
1503
|
+
Args:
|
|
1504
|
+
entities: List of (Entity, score) tuples from retrieval
|
|
1505
|
+
max_context_size: Maximum number of entities to keep
|
|
1506
|
+
relevance_threshold: Minimum relevance score (0.0-1.0)
|
|
1507
|
+
max_age_seconds: Maximum age in seconds (None = no age limit)
|
|
1508
|
+
|
|
1509
|
+
Returns:
|
|
1510
|
+
Pruned list of (Entity, score) tuples
|
|
1511
|
+
"""
|
|
1512
|
+
if not entities:
|
|
1513
|
+
return []
|
|
1514
|
+
|
|
1515
|
+
pruned = []
|
|
1516
|
+
current_time = datetime.utcnow()
|
|
1517
|
+
|
|
1518
|
+
for item in entities:
|
|
1519
|
+
# Handle both (Entity, score) tuples and Entity objects
|
|
1520
|
+
if isinstance(item, tuple):
|
|
1521
|
+
entity, score = item
|
|
1522
|
+
else:
|
|
1523
|
+
entity = item
|
|
1524
|
+
score = 1.0 # Default score if not provided
|
|
1525
|
+
|
|
1526
|
+
# Filter by relevance score
|
|
1527
|
+
if score < relevance_threshold:
|
|
1528
|
+
continue
|
|
1529
|
+
|
|
1530
|
+
# Filter by age if specified
|
|
1531
|
+
if max_age_seconds is not None:
|
|
1532
|
+
entity_age = None
|
|
1533
|
+
|
|
1534
|
+
# Try to get timestamp from entity
|
|
1535
|
+
if hasattr(entity, 'updated_at') and entity.updated_at:
|
|
1536
|
+
entity_age = (current_time - entity.updated_at).total_seconds()
|
|
1537
|
+
elif hasattr(entity, 'created_at') and entity.created_at:
|
|
1538
|
+
entity_age = (current_time - entity.created_at).total_seconds()
|
|
1539
|
+
|
|
1540
|
+
# Skip if too old
|
|
1541
|
+
if entity_age is not None and entity_age > max_age_seconds:
|
|
1542
|
+
continue
|
|
1543
|
+
|
|
1544
|
+
pruned.append((entity, score))
|
|
1545
|
+
|
|
1546
|
+
# Sort by score descending and limit to max_context_size
|
|
1547
|
+
pruned.sort(key=lambda x: x[1], reverse=True)
|
|
1548
|
+
pruned = pruned[:max_context_size]
|
|
1549
|
+
|
|
1550
|
+
logger.debug(
|
|
1551
|
+
f"Pruned knowledge context: {len(entities)} → {len(pruned)} entities "
|
|
1552
|
+
f"(threshold={relevance_threshold}, max_size={max_context_size})"
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
return pruned
|
|
1556
|
+
|
|
1557
|
+
def _prioritize_knowledge_context(
|
|
1558
|
+
self,
|
|
1559
|
+
entities: List[Any],
|
|
1560
|
+
relevance_weight: float = 0.6,
|
|
1561
|
+
recency_weight: float = 0.4,
|
|
1562
|
+
) -> List[Any]:
|
|
1563
|
+
"""
|
|
1564
|
+
Prioritize knowledge context using hybrid scoring.
|
|
1565
|
+
|
|
1566
|
+
Combines relevance scores with recency to determine the most important
|
|
1567
|
+
entities for the current context. More recent entities get a boost.
|
|
1568
|
+
|
|
1569
|
+
Args:
|
|
1570
|
+
entities: List of (Entity, score) tuples from retrieval
|
|
1571
|
+
relevance_weight: Weight for relevance score (0.0-1.0)
|
|
1572
|
+
recency_weight: Weight for recency score (0.0-1.0)
|
|
1573
|
+
|
|
1574
|
+
Returns:
|
|
1575
|
+
Prioritized list of (Entity, priority_score) tuples sorted by priority
|
|
1576
|
+
"""
|
|
1577
|
+
if not entities:
|
|
1578
|
+
return []
|
|
1579
|
+
|
|
1580
|
+
# Normalize weights
|
|
1581
|
+
total_weight = relevance_weight + recency_weight
|
|
1582
|
+
if total_weight == 0:
|
|
1583
|
+
total_weight = 1.0
|
|
1584
|
+
|
|
1585
|
+
norm_relevance_weight = relevance_weight / total_weight
|
|
1586
|
+
norm_recency_weight = recency_weight / total_weight
|
|
1587
|
+
|
|
1588
|
+
current_time = datetime.utcnow()
|
|
1589
|
+
prioritized = []
|
|
1590
|
+
|
|
1591
|
+
# Find oldest and newest timestamps for normalization
|
|
1592
|
+
timestamps = []
|
|
1593
|
+
for item in entities:
|
|
1594
|
+
entity = item[0] if isinstance(item, tuple) else item
|
|
1595
|
+
|
|
1596
|
+
if hasattr(entity, 'updated_at') and entity.updated_at:
|
|
1597
|
+
timestamps.append(entity.updated_at)
|
|
1598
|
+
elif hasattr(entity, 'created_at') and entity.created_at:
|
|
1599
|
+
timestamps.append(entity.created_at)
|
|
1600
|
+
|
|
1601
|
+
# Calculate recency scores
|
|
1602
|
+
if timestamps:
|
|
1603
|
+
oldest_time = min(timestamps)
|
|
1604
|
+
newest_time = max(timestamps)
|
|
1605
|
+
time_range = (newest_time - oldest_time).total_seconds()
|
|
1606
|
+
|
|
1607
|
+
# Avoid division by zero
|
|
1608
|
+
if time_range == 0:
|
|
1609
|
+
time_range = 1.0
|
|
1610
|
+
else:
|
|
1611
|
+
time_range = 1.0
|
|
1612
|
+
oldest_time = current_time
|
|
1613
|
+
|
|
1614
|
+
for item in entities:
|
|
1615
|
+
# Handle both (Entity, score) tuples and Entity objects
|
|
1616
|
+
if isinstance(item, tuple):
|
|
1617
|
+
entity, relevance_score = item
|
|
1618
|
+
else:
|
|
1619
|
+
entity = item
|
|
1620
|
+
relevance_score = 1.0
|
|
1621
|
+
|
|
1622
|
+
# Calculate recency score (0.0 = oldest, 1.0 = newest)
|
|
1623
|
+
recency_score = 0.5 # Default middle value
|
|
1624
|
+
|
|
1625
|
+
if hasattr(entity, 'updated_at') and entity.updated_at:
|
|
1626
|
+
age_seconds = (newest_time - entity.updated_at).total_seconds()
|
|
1627
|
+
recency_score = 1.0 - (age_seconds / time_range) if time_range > 0 else 1.0
|
|
1628
|
+
elif hasattr(entity, 'created_at') and entity.created_at:
|
|
1629
|
+
age_seconds = (newest_time - entity.created_at).total_seconds()
|
|
1630
|
+
recency_score = 1.0 - (age_seconds / time_range) if time_range > 0 else 1.0
|
|
1631
|
+
|
|
1632
|
+
# Combine scores with weights
|
|
1633
|
+
priority_score = (
|
|
1634
|
+
relevance_score * norm_relevance_weight +
|
|
1635
|
+
recency_score * norm_recency_weight
|
|
1636
|
+
)
|
|
1637
|
+
|
|
1638
|
+
prioritized.append((entity, priority_score))
|
|
1639
|
+
|
|
1640
|
+
# Sort by priority score descending
|
|
1641
|
+
prioritized.sort(key=lambda x: x[1], reverse=True)
|
|
1642
|
+
|
|
1643
|
+
logger.debug(
|
|
1644
|
+
f"Prioritized {len(prioritized)} entities "
|
|
1645
|
+
f"(relevance_weight={norm_relevance_weight:.2f}, recency_weight={norm_recency_weight:.2f})"
|
|
1646
|
+
)
|
|
1647
|
+
|
|
1648
|
+
return prioritized
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
def get_cache_metrics(self) -> Dict[str, Any]:
|
|
1652
|
+
"""
|
|
1653
|
+
Get knowledge cache metrics.
|
|
1654
|
+
|
|
1655
|
+
Returns:
|
|
1656
|
+
Dictionary with cache statistics including:
|
|
1657
|
+
- cache_hits: Number of cache hits
|
|
1658
|
+
- cache_misses: Number of cache misses
|
|
1659
|
+
- total_requests: Total cache requests
|
|
1660
|
+
- hit_rate: Cache hit rate (0.0 to 1.0)
|
|
1661
|
+
"""
|
|
1662
|
+
total_requests = self._cache_hits + self._cache_misses
|
|
1663
|
+
hit_rate = self._cache_hits / total_requests if total_requests > 0 else 0.0
|
|
1664
|
+
|
|
1665
|
+
return {
|
|
1666
|
+
"cache_hits": self._cache_hits,
|
|
1667
|
+
"cache_misses": self._cache_misses,
|
|
1668
|
+
"total_requests": total_requests,
|
|
1669
|
+
"hit_rate": hit_rate,
|
|
1670
|
+
"hit_rate_percentage": hit_rate * 100,
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
def reset_cache_metrics(self) -> None:
|
|
1674
|
+
"""Reset cache metrics counters."""
|
|
1675
|
+
self._cache_hits = 0
|
|
1676
|
+
self._cache_misses = 0
|
|
1677
|
+
logger.debug(f"Reset cache metrics for agent {self.agent_id}")
|
|
1678
|
+
|
|
1679
|
+
def get_graph_metrics(self) -> Dict[str, Any]:
|
|
1680
|
+
"""
|
|
1681
|
+
Get knowledge graph retrieval metrics.
|
|
1682
|
+
|
|
1683
|
+
Returns:
|
|
1684
|
+
Dictionary with graph metrics including:
|
|
1685
|
+
- Query counts and entity statistics
|
|
1686
|
+
- Performance metrics (timing)
|
|
1687
|
+
- Cache metrics
|
|
1688
|
+
- Strategy usage counts
|
|
1689
|
+
- Entity extraction metrics
|
|
1690
|
+
"""
|
|
1691
|
+
return self._graph_metrics.model_dump()
|
|
1692
|
+
|
|
1693
|
+
def reset_graph_metrics(self) -> None:
|
|
1694
|
+
"""Reset graph metrics to initial state."""
|
|
1695
|
+
self._graph_metrics = GraphMetrics(
|
|
1696
|
+
min_graph_query_time=None,
|
|
1697
|
+
max_graph_query_time=None,
|
|
1698
|
+
last_reset_at=None
|
|
1699
|
+
)
|
|
1700
|
+
logger.debug(f"Reset graph metrics for agent {self.agent_id}")
|
|
1701
|
+
|
|
1702
|
+
def get_comprehensive_status(self) -> Dict[str, Any]:
|
|
1703
|
+
"""
|
|
1704
|
+
Get comprehensive agent status including cache metrics.
|
|
1705
|
+
|
|
1706
|
+
Extends base agent status with knowledge graph cache metrics.
|
|
1707
|
+
|
|
1708
|
+
Returns:
|
|
1709
|
+
Dictionary with comprehensive status information
|
|
1710
|
+
"""
|
|
1711
|
+
# Get base status from parent
|
|
1712
|
+
status = super().get_comprehensive_status()
|
|
1713
|
+
|
|
1714
|
+
# Add cache metrics
|
|
1715
|
+
status["cache_metrics"] = self.get_cache_metrics()
|
|
1716
|
+
|
|
1717
|
+
# Add graph metrics
|
|
1718
|
+
status["graph_metrics"] = self.get_graph_metrics()
|
|
1719
|
+
|
|
1720
|
+
# Add graph store status
|
|
1721
|
+
status["graph_store_enabled"] = self.graph_store is not None
|
|
1722
|
+
status["graph_reasoning_enabled"] = self.enable_graph_reasoning
|
|
1723
|
+
|
|
1724
|
+
return status
|
|
1725
|
+
|
|
1726
|
+
def initialize_prometheus_metrics(self) -> None:
|
|
1727
|
+
"""
|
|
1728
|
+
Initialize Prometheus metrics for knowledge graph operations.
|
|
1729
|
+
|
|
1730
|
+
Defines counters, histograms, and gauges for tracking graph queries,
|
|
1731
|
+
entity extraction, and cache performance.
|
|
1732
|
+
|
|
1733
|
+
Note: This should be called after the global Prometheus registry is set up.
|
|
1734
|
+
"""
|
|
1735
|
+
try:
|
|
1736
|
+
from prometheus_client import Counter, Histogram, Gauge
|
|
1737
|
+
|
|
1738
|
+
self._prometheus_metrics = {
|
|
1739
|
+
# Graph query counters
|
|
1740
|
+
"knowledge_retrieval_total": Counter(
|
|
1741
|
+
"knowledge_retrieval_total",
|
|
1742
|
+
"Total number of knowledge graph queries",
|
|
1743
|
+
["agent_id", "strategy"],
|
|
1744
|
+
),
|
|
1745
|
+
"knowledge_entities_retrieved": Counter(
|
|
1746
|
+
"knowledge_entities_retrieved_total",
|
|
1747
|
+
"Total number of entities retrieved from knowledge graph",
|
|
1748
|
+
["agent_id"],
|
|
1749
|
+
),
|
|
1750
|
+
# Query latency histogram
|
|
1751
|
+
"knowledge_retrieval_duration": Histogram(
|
|
1752
|
+
"knowledge_retrieval_duration_seconds",
|
|
1753
|
+
"Knowledge graph query duration in seconds",
|
|
1754
|
+
["agent_id", "strategy"],
|
|
1755
|
+
buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
|
|
1756
|
+
),
|
|
1757
|
+
# Cache metrics
|
|
1758
|
+
"knowledge_cache_hit_rate": Gauge(
|
|
1759
|
+
"knowledge_cache_hit_rate",
|
|
1760
|
+
"Knowledge graph cache hit rate",
|
|
1761
|
+
["agent_id"],
|
|
1762
|
+
),
|
|
1763
|
+
"knowledge_cache_hits": Counter(
|
|
1764
|
+
"knowledge_cache_hits_total",
|
|
1765
|
+
"Total number of cache hits",
|
|
1766
|
+
["agent_id"],
|
|
1767
|
+
),
|
|
1768
|
+
"knowledge_cache_misses": Counter(
|
|
1769
|
+
"knowledge_cache_misses_total",
|
|
1770
|
+
"Total number of cache misses",
|
|
1771
|
+
["agent_id"],
|
|
1772
|
+
),
|
|
1773
|
+
# Entity extraction metrics
|
|
1774
|
+
"entity_extraction_total": Counter(
|
|
1775
|
+
"entity_extraction_total",
|
|
1776
|
+
"Total number of entity extractions",
|
|
1777
|
+
["agent_id"],
|
|
1778
|
+
),
|
|
1779
|
+
"entity_extraction_duration": Histogram(
|
|
1780
|
+
"entity_extraction_duration_seconds",
|
|
1781
|
+
"Entity extraction duration in seconds",
|
|
1782
|
+
["agent_id"],
|
|
1783
|
+
buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
|
|
1784
|
+
),
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
self._prometheus_enabled = True
|
|
1788
|
+
logger.info(f"Prometheus metrics initialized for agent {self.agent_id}")
|
|
1789
|
+
|
|
1790
|
+
except ImportError:
|
|
1791
|
+
logger.warning("prometheus_client not available, Prometheus metrics disabled")
|
|
1792
|
+
self._prometheus_enabled = False
|
|
1793
|
+
except Exception as e:
|
|
1794
|
+
logger.warning(f"Failed to initialize Prometheus metrics: {e}")
|
|
1795
|
+
self._prometheus_enabled = False
|
|
1796
|
+
|
|
1797
|
+
def _record_prometheus_metrics(
|
|
1798
|
+
self,
|
|
1799
|
+
query_time: float,
|
|
1800
|
+
entities_count: int,
|
|
1801
|
+
strategy: str,
|
|
1802
|
+
cache_hit: bool,
|
|
1803
|
+
extraction_time: Optional[float] = None,
|
|
1804
|
+
) -> None:
|
|
1805
|
+
"""
|
|
1806
|
+
Record metrics to Prometheus.
|
|
1807
|
+
|
|
1808
|
+
Args:
|
|
1809
|
+
query_time: Query execution time in seconds
|
|
1810
|
+
entities_count: Number of entities retrieved
|
|
1811
|
+
strategy: Retrieval strategy used
|
|
1812
|
+
cache_hit: Whether this was a cache hit
|
|
1813
|
+
extraction_time: Entity extraction time (if applicable)
|
|
1814
|
+
"""
|
|
1815
|
+
if not self._prometheus_enabled or self._prometheus_metrics is None:
|
|
1816
|
+
return
|
|
1817
|
+
|
|
1818
|
+
try:
|
|
1819
|
+
# Record query
|
|
1820
|
+
self._prometheus_metrics["knowledge_retrieval_total"].labels(
|
|
1821
|
+
agent_id=self.agent_id,
|
|
1822
|
+
strategy=strategy,
|
|
1823
|
+
).inc()
|
|
1824
|
+
|
|
1825
|
+
# Record entities retrieved
|
|
1826
|
+
self._prometheus_metrics["knowledge_entities_retrieved"].labels(
|
|
1827
|
+
agent_id=self.agent_id,
|
|
1828
|
+
).inc(entities_count)
|
|
1829
|
+
|
|
1830
|
+
# Record query duration
|
|
1831
|
+
self._prometheus_metrics["knowledge_retrieval_duration"].labels(
|
|
1832
|
+
agent_id=self.agent_id,
|
|
1833
|
+
strategy=strategy,
|
|
1834
|
+
).observe(query_time)
|
|
1835
|
+
|
|
1836
|
+
# Record cache metrics
|
|
1837
|
+
if cache_hit:
|
|
1838
|
+
self._prometheus_metrics["knowledge_cache_hits"].labels(
|
|
1839
|
+
agent_id=self.agent_id,
|
|
1840
|
+
).inc()
|
|
1841
|
+
else:
|
|
1842
|
+
self._prometheus_metrics["knowledge_cache_misses"].labels(
|
|
1843
|
+
agent_id=self.agent_id,
|
|
1844
|
+
).inc()
|
|
1845
|
+
|
|
1846
|
+
# Update cache hit rate gauge
|
|
1847
|
+
total_requests = self._graph_metrics.cache_hits + self._graph_metrics.cache_misses
|
|
1848
|
+
if total_requests > 0:
|
|
1849
|
+
hit_rate = self._graph_metrics.cache_hits / total_requests
|
|
1850
|
+
self._prometheus_metrics["knowledge_cache_hit_rate"].labels(
|
|
1851
|
+
agent_id=self.agent_id,
|
|
1852
|
+
).set(hit_rate)
|
|
1853
|
+
|
|
1854
|
+
# Record entity extraction if applicable
|
|
1855
|
+
if extraction_time is not None:
|
|
1856
|
+
self._prometheus_metrics["entity_extraction_total"].labels(
|
|
1857
|
+
agent_id=self.agent_id,
|
|
1858
|
+
).inc()
|
|
1859
|
+
self._prometheus_metrics["entity_extraction_duration"].labels(
|
|
1860
|
+
agent_id=self.agent_id,
|
|
1861
|
+
).observe(extraction_time)
|
|
1862
|
+
|
|
1863
|
+
except Exception as e:
|
|
1864
|
+
logger.warning(f"Failed to record Prometheus metrics: {e}")
|
|
1865
|
+
|
|
1866
|
+
def reset_circuit_breaker(self) -> None:
|
|
1867
|
+
"""
|
|
1868
|
+
Manually reset the circuit breaker for knowledge retrieval.
|
|
1869
|
+
|
|
1870
|
+
This allows knowledge retrieval to resume after persistent failures
|
|
1871
|
+
have been resolved.
|
|
1872
|
+
"""
|
|
1873
|
+
if self._circuit_breaker_open:
|
|
1874
|
+
logger.info(
|
|
1875
|
+
f"Resetting circuit breaker (was at {self._circuit_breaker_failures} failures)"
|
|
1876
|
+
)
|
|
1877
|
+
self._circuit_breaker_open = False
|
|
1878
|
+
self._circuit_breaker_failures = 0
|
|
1879
|
+
|
|
1880
|
+
def get_circuit_breaker_status(self) -> Dict[str, Any]:
|
|
1881
|
+
"""
|
|
1882
|
+
Get the current status of the circuit breaker.
|
|
1883
|
+
|
|
1884
|
+
Returns:
|
|
1885
|
+
Dictionary with circuit breaker status information
|
|
1886
|
+
"""
|
|
1887
|
+
return {
|
|
1888
|
+
"open": self._circuit_breaker_open,
|
|
1889
|
+
"failures": self._circuit_breaker_failures,
|
|
1890
|
+
"threshold": self._circuit_breaker_threshold,
|
|
1891
|
+
"status": "OPEN" if self._circuit_breaker_open else "CLOSED",
|
|
1892
|
+
}
|