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.

Files changed (340) hide show
  1. aiecs/__init__.py +13 -16
  2. aiecs/__main__.py +7 -7
  3. aiecs/aiecs_client.py +269 -75
  4. aiecs/application/executors/operation_executor.py +79 -54
  5. aiecs/application/knowledge_graph/__init__.py +7 -0
  6. aiecs/application/knowledge_graph/builder/__init__.py +37 -0
  7. aiecs/application/knowledge_graph/builder/data_quality.py +302 -0
  8. aiecs/application/knowledge_graph/builder/data_reshaping.py +293 -0
  9. aiecs/application/knowledge_graph/builder/document_builder.py +369 -0
  10. aiecs/application/knowledge_graph/builder/graph_builder.py +490 -0
  11. aiecs/application/knowledge_graph/builder/import_optimizer.py +396 -0
  12. aiecs/application/knowledge_graph/builder/schema_inference.py +462 -0
  13. aiecs/application/knowledge_graph/builder/schema_mapping.py +563 -0
  14. aiecs/application/knowledge_graph/builder/structured_pipeline.py +1384 -0
  15. aiecs/application/knowledge_graph/builder/text_chunker.py +317 -0
  16. aiecs/application/knowledge_graph/extractors/__init__.py +27 -0
  17. aiecs/application/knowledge_graph/extractors/base.py +98 -0
  18. aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +422 -0
  19. aiecs/application/knowledge_graph/extractors/llm_relation_extractor.py +347 -0
  20. aiecs/application/knowledge_graph/extractors/ner_entity_extractor.py +241 -0
  21. aiecs/application/knowledge_graph/fusion/__init__.py +78 -0
  22. aiecs/application/knowledge_graph/fusion/ab_testing.py +395 -0
  23. aiecs/application/knowledge_graph/fusion/abbreviation_expander.py +327 -0
  24. aiecs/application/knowledge_graph/fusion/alias_index.py +597 -0
  25. aiecs/application/knowledge_graph/fusion/alias_matcher.py +384 -0
  26. aiecs/application/knowledge_graph/fusion/cache_coordinator.py +343 -0
  27. aiecs/application/knowledge_graph/fusion/entity_deduplicator.py +433 -0
  28. aiecs/application/knowledge_graph/fusion/entity_linker.py +511 -0
  29. aiecs/application/knowledge_graph/fusion/evaluation_dataset.py +240 -0
  30. aiecs/application/knowledge_graph/fusion/knowledge_fusion.py +632 -0
  31. aiecs/application/knowledge_graph/fusion/matching_config.py +489 -0
  32. aiecs/application/knowledge_graph/fusion/name_normalizer.py +352 -0
  33. aiecs/application/knowledge_graph/fusion/relation_deduplicator.py +183 -0
  34. aiecs/application/knowledge_graph/fusion/semantic_name_matcher.py +464 -0
  35. aiecs/application/knowledge_graph/fusion/similarity_pipeline.py +534 -0
  36. aiecs/application/knowledge_graph/pattern_matching/__init__.py +21 -0
  37. aiecs/application/knowledge_graph/pattern_matching/pattern_matcher.py +342 -0
  38. aiecs/application/knowledge_graph/pattern_matching/query_executor.py +366 -0
  39. aiecs/application/knowledge_graph/profiling/__init__.py +12 -0
  40. aiecs/application/knowledge_graph/profiling/query_plan_visualizer.py +195 -0
  41. aiecs/application/knowledge_graph/profiling/query_profiler.py +223 -0
  42. aiecs/application/knowledge_graph/reasoning/__init__.py +27 -0
  43. aiecs/application/knowledge_graph/reasoning/evidence_synthesis.py +341 -0
  44. aiecs/application/knowledge_graph/reasoning/inference_engine.py +500 -0
  45. aiecs/application/knowledge_graph/reasoning/logic_form_parser.py +163 -0
  46. aiecs/application/knowledge_graph/reasoning/logic_parser/__init__.py +79 -0
  47. aiecs/application/knowledge_graph/reasoning/logic_parser/ast_builder.py +513 -0
  48. aiecs/application/knowledge_graph/reasoning/logic_parser/ast_nodes.py +913 -0
  49. aiecs/application/knowledge_graph/reasoning/logic_parser/ast_validator.py +866 -0
  50. aiecs/application/knowledge_graph/reasoning/logic_parser/error_handler.py +475 -0
  51. aiecs/application/knowledge_graph/reasoning/logic_parser/parser.py +396 -0
  52. aiecs/application/knowledge_graph/reasoning/logic_parser/query_context.py +208 -0
  53. aiecs/application/knowledge_graph/reasoning/logic_query_integration.py +170 -0
  54. aiecs/application/knowledge_graph/reasoning/query_planner.py +855 -0
  55. aiecs/application/knowledge_graph/reasoning/reasoning_engine.py +518 -0
  56. aiecs/application/knowledge_graph/retrieval/__init__.py +27 -0
  57. aiecs/application/knowledge_graph/retrieval/query_intent_classifier.py +211 -0
  58. aiecs/application/knowledge_graph/retrieval/retrieval_strategies.py +592 -0
  59. aiecs/application/knowledge_graph/retrieval/strategy_types.py +23 -0
  60. aiecs/application/knowledge_graph/search/__init__.py +59 -0
  61. aiecs/application/knowledge_graph/search/hybrid_search.py +457 -0
  62. aiecs/application/knowledge_graph/search/reranker.py +293 -0
  63. aiecs/application/knowledge_graph/search/reranker_strategies.py +535 -0
  64. aiecs/application/knowledge_graph/search/text_similarity.py +392 -0
  65. aiecs/application/knowledge_graph/traversal/__init__.py +15 -0
  66. aiecs/application/knowledge_graph/traversal/enhanced_traversal.py +305 -0
  67. aiecs/application/knowledge_graph/traversal/path_scorer.py +271 -0
  68. aiecs/application/knowledge_graph/validators/__init__.py +13 -0
  69. aiecs/application/knowledge_graph/validators/relation_validator.py +239 -0
  70. aiecs/application/knowledge_graph/visualization/__init__.py +11 -0
  71. aiecs/application/knowledge_graph/visualization/graph_visualizer.py +313 -0
  72. aiecs/common/__init__.py +9 -0
  73. aiecs/common/knowledge_graph/__init__.py +17 -0
  74. aiecs/common/knowledge_graph/runnable.py +471 -0
  75. aiecs/config/__init__.py +20 -5
  76. aiecs/config/config.py +762 -31
  77. aiecs/config/graph_config.py +131 -0
  78. aiecs/config/tool_config.py +399 -0
  79. aiecs/core/__init__.py +29 -13
  80. aiecs/core/interface/__init__.py +2 -2
  81. aiecs/core/interface/execution_interface.py +22 -22
  82. aiecs/core/interface/storage_interface.py +37 -88
  83. aiecs/core/registry/__init__.py +31 -0
  84. aiecs/core/registry/service_registry.py +92 -0
  85. aiecs/domain/__init__.py +270 -1
  86. aiecs/domain/agent/__init__.py +191 -0
  87. aiecs/domain/agent/base_agent.py +3870 -0
  88. aiecs/domain/agent/exceptions.py +99 -0
  89. aiecs/domain/agent/graph_aware_mixin.py +569 -0
  90. aiecs/domain/agent/hybrid_agent.py +1435 -0
  91. aiecs/domain/agent/integration/__init__.py +29 -0
  92. aiecs/domain/agent/integration/context_compressor.py +216 -0
  93. aiecs/domain/agent/integration/context_engine_adapter.py +587 -0
  94. aiecs/domain/agent/integration/protocols.py +281 -0
  95. aiecs/domain/agent/integration/retry_policy.py +218 -0
  96. aiecs/domain/agent/integration/role_config.py +213 -0
  97. aiecs/domain/agent/knowledge_aware_agent.py +1892 -0
  98. aiecs/domain/agent/lifecycle.py +291 -0
  99. aiecs/domain/agent/llm_agent.py +692 -0
  100. aiecs/domain/agent/memory/__init__.py +12 -0
  101. aiecs/domain/agent/memory/conversation.py +1124 -0
  102. aiecs/domain/agent/migration/__init__.py +14 -0
  103. aiecs/domain/agent/migration/conversion.py +163 -0
  104. aiecs/domain/agent/migration/legacy_wrapper.py +86 -0
  105. aiecs/domain/agent/models.py +884 -0
  106. aiecs/domain/agent/observability.py +479 -0
  107. aiecs/domain/agent/persistence.py +449 -0
  108. aiecs/domain/agent/prompts/__init__.py +29 -0
  109. aiecs/domain/agent/prompts/builder.py +159 -0
  110. aiecs/domain/agent/prompts/formatters.py +187 -0
  111. aiecs/domain/agent/prompts/template.py +255 -0
  112. aiecs/domain/agent/registry.py +253 -0
  113. aiecs/domain/agent/tool_agent.py +444 -0
  114. aiecs/domain/agent/tools/__init__.py +15 -0
  115. aiecs/domain/agent/tools/schema_generator.py +364 -0
  116. aiecs/domain/community/__init__.py +155 -0
  117. aiecs/domain/community/agent_adapter.py +469 -0
  118. aiecs/domain/community/analytics.py +432 -0
  119. aiecs/domain/community/collaborative_workflow.py +648 -0
  120. aiecs/domain/community/communication_hub.py +634 -0
  121. aiecs/domain/community/community_builder.py +320 -0
  122. aiecs/domain/community/community_integration.py +796 -0
  123. aiecs/domain/community/community_manager.py +803 -0
  124. aiecs/domain/community/decision_engine.py +849 -0
  125. aiecs/domain/community/exceptions.py +231 -0
  126. aiecs/domain/community/models/__init__.py +33 -0
  127. aiecs/domain/community/models/community_models.py +234 -0
  128. aiecs/domain/community/resource_manager.py +461 -0
  129. aiecs/domain/community/shared_context_manager.py +589 -0
  130. aiecs/domain/context/__init__.py +40 -10
  131. aiecs/domain/context/context_engine.py +1910 -0
  132. aiecs/domain/context/conversation_models.py +87 -53
  133. aiecs/domain/context/graph_memory.py +582 -0
  134. aiecs/domain/execution/model.py +12 -4
  135. aiecs/domain/knowledge_graph/__init__.py +19 -0
  136. aiecs/domain/knowledge_graph/models/__init__.py +52 -0
  137. aiecs/domain/knowledge_graph/models/entity.py +148 -0
  138. aiecs/domain/knowledge_graph/models/evidence.py +178 -0
  139. aiecs/domain/knowledge_graph/models/inference_rule.py +184 -0
  140. aiecs/domain/knowledge_graph/models/path.py +171 -0
  141. aiecs/domain/knowledge_graph/models/path_pattern.py +171 -0
  142. aiecs/domain/knowledge_graph/models/query.py +261 -0
  143. aiecs/domain/knowledge_graph/models/query_plan.py +181 -0
  144. aiecs/domain/knowledge_graph/models/relation.py +202 -0
  145. aiecs/domain/knowledge_graph/schema/__init__.py +23 -0
  146. aiecs/domain/knowledge_graph/schema/entity_type.py +131 -0
  147. aiecs/domain/knowledge_graph/schema/graph_schema.py +253 -0
  148. aiecs/domain/knowledge_graph/schema/property_schema.py +143 -0
  149. aiecs/domain/knowledge_graph/schema/relation_type.py +163 -0
  150. aiecs/domain/knowledge_graph/schema/schema_manager.py +691 -0
  151. aiecs/domain/knowledge_graph/schema/type_enums.py +209 -0
  152. aiecs/domain/task/dsl_processor.py +172 -56
  153. aiecs/domain/task/model.py +20 -8
  154. aiecs/domain/task/task_context.py +27 -24
  155. aiecs/infrastructure/__init__.py +0 -2
  156. aiecs/infrastructure/graph_storage/__init__.py +11 -0
  157. aiecs/infrastructure/graph_storage/base.py +837 -0
  158. aiecs/infrastructure/graph_storage/batch_operations.py +458 -0
  159. aiecs/infrastructure/graph_storage/cache.py +424 -0
  160. aiecs/infrastructure/graph_storage/distributed.py +223 -0
  161. aiecs/infrastructure/graph_storage/error_handling.py +380 -0
  162. aiecs/infrastructure/graph_storage/graceful_degradation.py +294 -0
  163. aiecs/infrastructure/graph_storage/health_checks.py +378 -0
  164. aiecs/infrastructure/graph_storage/in_memory.py +1197 -0
  165. aiecs/infrastructure/graph_storage/index_optimization.py +446 -0
  166. aiecs/infrastructure/graph_storage/lazy_loading.py +431 -0
  167. aiecs/infrastructure/graph_storage/metrics.py +344 -0
  168. aiecs/infrastructure/graph_storage/migration.py +400 -0
  169. aiecs/infrastructure/graph_storage/pagination.py +483 -0
  170. aiecs/infrastructure/graph_storage/performance_monitoring.py +456 -0
  171. aiecs/infrastructure/graph_storage/postgres.py +1563 -0
  172. aiecs/infrastructure/graph_storage/property_storage.py +353 -0
  173. aiecs/infrastructure/graph_storage/protocols.py +76 -0
  174. aiecs/infrastructure/graph_storage/query_optimizer.py +642 -0
  175. aiecs/infrastructure/graph_storage/schema_cache.py +290 -0
  176. aiecs/infrastructure/graph_storage/sqlite.py +1373 -0
  177. aiecs/infrastructure/graph_storage/streaming.py +487 -0
  178. aiecs/infrastructure/graph_storage/tenant.py +412 -0
  179. aiecs/infrastructure/messaging/celery_task_manager.py +92 -54
  180. aiecs/infrastructure/messaging/websocket_manager.py +51 -35
  181. aiecs/infrastructure/monitoring/__init__.py +22 -0
  182. aiecs/infrastructure/monitoring/executor_metrics.py +45 -11
  183. aiecs/infrastructure/monitoring/global_metrics_manager.py +212 -0
  184. aiecs/infrastructure/monitoring/structured_logger.py +3 -7
  185. aiecs/infrastructure/monitoring/tracing_manager.py +63 -35
  186. aiecs/infrastructure/persistence/__init__.py +14 -1
  187. aiecs/infrastructure/persistence/context_engine_client.py +184 -0
  188. aiecs/infrastructure/persistence/database_manager.py +67 -43
  189. aiecs/infrastructure/persistence/file_storage.py +180 -103
  190. aiecs/infrastructure/persistence/redis_client.py +74 -21
  191. aiecs/llm/__init__.py +73 -25
  192. aiecs/llm/callbacks/__init__.py +11 -0
  193. aiecs/llm/{custom_callbacks.py → callbacks/custom_callbacks.py} +26 -19
  194. aiecs/llm/client_factory.py +224 -36
  195. aiecs/llm/client_resolver.py +155 -0
  196. aiecs/llm/clients/__init__.py +38 -0
  197. aiecs/llm/clients/base_client.py +324 -0
  198. aiecs/llm/clients/google_function_calling_mixin.py +457 -0
  199. aiecs/llm/clients/googleai_client.py +241 -0
  200. aiecs/llm/clients/openai_client.py +158 -0
  201. aiecs/llm/clients/openai_compatible_mixin.py +367 -0
  202. aiecs/llm/clients/vertex_client.py +897 -0
  203. aiecs/llm/clients/xai_client.py +201 -0
  204. aiecs/llm/config/__init__.py +51 -0
  205. aiecs/llm/config/config_loader.py +272 -0
  206. aiecs/llm/config/config_validator.py +206 -0
  207. aiecs/llm/config/model_config.py +143 -0
  208. aiecs/llm/protocols.py +149 -0
  209. aiecs/llm/utils/__init__.py +10 -0
  210. aiecs/llm/utils/validate_config.py +89 -0
  211. aiecs/main.py +140 -121
  212. aiecs/scripts/aid/VERSION_MANAGEMENT.md +138 -0
  213. aiecs/scripts/aid/__init__.py +19 -0
  214. aiecs/scripts/aid/module_checker.py +499 -0
  215. aiecs/scripts/aid/version_manager.py +235 -0
  216. aiecs/scripts/{DEPENDENCY_SYSTEM_SUMMARY.md → dependance_check/DEPENDENCY_SYSTEM_SUMMARY.md} +1 -0
  217. aiecs/scripts/{README_DEPENDENCY_CHECKER.md → dependance_check/README_DEPENDENCY_CHECKER.md} +1 -0
  218. aiecs/scripts/dependance_check/__init__.py +15 -0
  219. aiecs/scripts/dependance_check/dependency_checker.py +1835 -0
  220. aiecs/scripts/{dependency_fixer.py → dependance_check/dependency_fixer.py} +192 -90
  221. aiecs/scripts/{download_nlp_data.py → dependance_check/download_nlp_data.py} +203 -71
  222. aiecs/scripts/dependance_patch/__init__.py +7 -0
  223. aiecs/scripts/dependance_patch/fix_weasel/__init__.py +11 -0
  224. aiecs/scripts/{fix_weasel_validator.py → dependance_patch/fix_weasel/fix_weasel_validator.py} +21 -14
  225. aiecs/scripts/{patch_weasel_library.sh → dependance_patch/fix_weasel/patch_weasel_library.sh} +1 -1
  226. aiecs/scripts/knowledge_graph/__init__.py +3 -0
  227. aiecs/scripts/knowledge_graph/run_threshold_experiments.py +212 -0
  228. aiecs/scripts/migrations/multi_tenancy/README.md +142 -0
  229. aiecs/scripts/tools_develop/README.md +671 -0
  230. aiecs/scripts/tools_develop/README_CONFIG_CHECKER.md +273 -0
  231. aiecs/scripts/tools_develop/TOOLS_CONFIG_GUIDE.md +1287 -0
  232. aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
  233. aiecs/scripts/tools_develop/__init__.py +21 -0
  234. aiecs/scripts/tools_develop/check_all_tools_config.py +548 -0
  235. aiecs/scripts/tools_develop/check_type_annotations.py +257 -0
  236. aiecs/scripts/tools_develop/pre-commit-schema-coverage.sh +66 -0
  237. aiecs/scripts/tools_develop/schema_coverage.py +511 -0
  238. aiecs/scripts/tools_develop/validate_tool_schemas.py +475 -0
  239. aiecs/scripts/tools_develop/verify_executor_config_fix.py +98 -0
  240. aiecs/scripts/tools_develop/verify_tools.py +352 -0
  241. aiecs/tasks/__init__.py +0 -1
  242. aiecs/tasks/worker.py +115 -47
  243. aiecs/tools/__init__.py +194 -72
  244. aiecs/tools/apisource/__init__.py +99 -0
  245. aiecs/tools/apisource/intelligence/__init__.py +19 -0
  246. aiecs/tools/apisource/intelligence/data_fusion.py +632 -0
  247. aiecs/tools/apisource/intelligence/query_analyzer.py +417 -0
  248. aiecs/tools/apisource/intelligence/search_enhancer.py +385 -0
  249. aiecs/tools/apisource/monitoring/__init__.py +9 -0
  250. aiecs/tools/apisource/monitoring/metrics.py +330 -0
  251. aiecs/tools/apisource/providers/__init__.py +112 -0
  252. aiecs/tools/apisource/providers/base.py +671 -0
  253. aiecs/tools/apisource/providers/census.py +397 -0
  254. aiecs/tools/apisource/providers/fred.py +535 -0
  255. aiecs/tools/apisource/providers/newsapi.py +409 -0
  256. aiecs/tools/apisource/providers/worldbank.py +352 -0
  257. aiecs/tools/apisource/reliability/__init__.py +12 -0
  258. aiecs/tools/apisource/reliability/error_handler.py +363 -0
  259. aiecs/tools/apisource/reliability/fallback_strategy.py +376 -0
  260. aiecs/tools/apisource/tool.py +832 -0
  261. aiecs/tools/apisource/utils/__init__.py +9 -0
  262. aiecs/tools/apisource/utils/validators.py +334 -0
  263. aiecs/tools/base_tool.py +415 -21
  264. aiecs/tools/docs/__init__.py +121 -0
  265. aiecs/tools/docs/ai_document_orchestrator.py +607 -0
  266. aiecs/tools/docs/ai_document_writer_orchestrator.py +2350 -0
  267. aiecs/tools/docs/content_insertion_tool.py +1320 -0
  268. aiecs/tools/docs/document_creator_tool.py +1323 -0
  269. aiecs/tools/docs/document_layout_tool.py +1160 -0
  270. aiecs/tools/docs/document_parser_tool.py +1011 -0
  271. aiecs/tools/docs/document_writer_tool.py +1829 -0
  272. aiecs/tools/knowledge_graph/__init__.py +17 -0
  273. aiecs/tools/knowledge_graph/graph_reasoning_tool.py +807 -0
  274. aiecs/tools/knowledge_graph/graph_search_tool.py +944 -0
  275. aiecs/tools/knowledge_graph/kg_builder_tool.py +524 -0
  276. aiecs/tools/langchain_adapter.py +300 -138
  277. aiecs/tools/schema_generator.py +455 -0
  278. aiecs/tools/search_tool/__init__.py +100 -0
  279. aiecs/tools/search_tool/analyzers.py +581 -0
  280. aiecs/tools/search_tool/cache.py +264 -0
  281. aiecs/tools/search_tool/constants.py +128 -0
  282. aiecs/tools/search_tool/context.py +224 -0
  283. aiecs/tools/search_tool/core.py +778 -0
  284. aiecs/tools/search_tool/deduplicator.py +119 -0
  285. aiecs/tools/search_tool/error_handler.py +242 -0
  286. aiecs/tools/search_tool/metrics.py +343 -0
  287. aiecs/tools/search_tool/rate_limiter.py +172 -0
  288. aiecs/tools/search_tool/schemas.py +275 -0
  289. aiecs/tools/statistics/__init__.py +80 -0
  290. aiecs/tools/statistics/ai_data_analysis_orchestrator.py +646 -0
  291. aiecs/tools/statistics/ai_insight_generator_tool.py +508 -0
  292. aiecs/tools/statistics/ai_report_orchestrator_tool.py +684 -0
  293. aiecs/tools/statistics/data_loader_tool.py +555 -0
  294. aiecs/tools/statistics/data_profiler_tool.py +638 -0
  295. aiecs/tools/statistics/data_transformer_tool.py +580 -0
  296. aiecs/tools/statistics/data_visualizer_tool.py +498 -0
  297. aiecs/tools/statistics/model_trainer_tool.py +507 -0
  298. aiecs/tools/statistics/statistical_analyzer_tool.py +472 -0
  299. aiecs/tools/task_tools/__init__.py +49 -36
  300. aiecs/tools/task_tools/chart_tool.py +200 -184
  301. aiecs/tools/task_tools/classfire_tool.py +268 -267
  302. aiecs/tools/task_tools/image_tool.py +175 -131
  303. aiecs/tools/task_tools/office_tool.py +226 -146
  304. aiecs/tools/task_tools/pandas_tool.py +477 -121
  305. aiecs/tools/task_tools/report_tool.py +390 -142
  306. aiecs/tools/task_tools/research_tool.py +149 -79
  307. aiecs/tools/task_tools/scraper_tool.py +339 -145
  308. aiecs/tools/task_tools/stats_tool.py +448 -209
  309. aiecs/tools/temp_file_manager.py +26 -24
  310. aiecs/tools/tool_executor/__init__.py +18 -16
  311. aiecs/tools/tool_executor/tool_executor.py +364 -52
  312. aiecs/utils/LLM_output_structor.py +74 -48
  313. aiecs/utils/__init__.py +14 -3
  314. aiecs/utils/base_callback.py +0 -3
  315. aiecs/utils/cache_provider.py +696 -0
  316. aiecs/utils/execution_utils.py +50 -31
  317. aiecs/utils/prompt_loader.py +1 -0
  318. aiecs/utils/token_usage_repository.py +37 -11
  319. aiecs/ws/socket_server.py +14 -4
  320. {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/METADATA +52 -15
  321. aiecs-1.7.6.dist-info/RECORD +337 -0
  322. aiecs-1.7.6.dist-info/entry_points.txt +13 -0
  323. aiecs/config/registry.py +0 -19
  324. aiecs/domain/context/content_engine.py +0 -982
  325. aiecs/llm/base_client.py +0 -99
  326. aiecs/llm/openai_client.py +0 -125
  327. aiecs/llm/vertex_client.py +0 -186
  328. aiecs/llm/xai_client.py +0 -184
  329. aiecs/scripts/dependency_checker.py +0 -857
  330. aiecs/scripts/quick_dependency_check.py +0 -269
  331. aiecs/tools/task_tools/search_api.py +0 -7
  332. aiecs-1.0.1.dist-info/RECORD +0 -90
  333. aiecs-1.0.1.dist-info/entry_points.txt +0 -7
  334. /aiecs/scripts/{setup_nlp_data.sh → dependance_check/setup_nlp_data.sh} +0 -0
  335. /aiecs/scripts/{README_WEASEL_PATCH.md → dependance_patch/fix_weasel/README_WEASEL_PATCH.md} +0 -0
  336. /aiecs/scripts/{fix_weasel_validator.sh → dependance_patch/fix_weasel/fix_weasel_validator.sh} +0 -0
  337. /aiecs/scripts/{run_weasel_patch.sh → dependance_patch/fix_weasel/run_weasel_patch.sh} +0 -0
  338. {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/WHEEL +0 -0
  339. {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/licenses/LICENSE +0 -0
  340. {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
+ }