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,1910 @@
1
+ """
2
+ ContextEngine: Advanced Context and Session Management Engine
3
+
4
+ This engine extends TaskContext capabilities to provide comprehensive
5
+ session management, conversation tracking, and persistent storage for BaseAIService.
6
+
7
+ Key Features:
8
+ 1. Multi-session management (extends TaskContext from single task to multiple sessions)
9
+ 2. Redis backend storage for persistence and scalability
10
+ 3. Conversation history management with optimization
11
+ 4. Performance metrics and analytics
12
+ 5. Resource and lifecycle management
13
+ 6. Integration with BaseServiceCheckpointer
14
+ """
15
+
16
+ from aiecs.core.interface.storage_interface import (
17
+ IStorageBackend,
18
+ ICheckpointerBackend,
19
+ )
20
+ from aiecs.domain.task.task_context import TaskContext, ContextUpdate
21
+ import json
22
+ import logging
23
+ import uuid
24
+ from datetime import datetime, timedelta
25
+ from typing import Dict, Any, List, Optional
26
+ from dataclasses import dataclass, asdict, is_dataclass
27
+
28
+
29
+ class DateTimeEncoder(json.JSONEncoder):
30
+ """Custom JSON encoder to handle datetime objects."""
31
+
32
+ def default(self, obj):
33
+ if isinstance(obj, datetime):
34
+ return obj.isoformat()
35
+ return super().default(obj)
36
+
37
+
38
+ # Import TaskContext for base functionality
39
+
40
+ # Import core storage interfaces
41
+
42
+ # Redis client import - use existing infrastructure
43
+ try:
44
+ import redis.asyncio as redis
45
+ from aiecs.infrastructure.persistence.redis_client import get_redis_client
46
+
47
+ REDIS_AVAILABLE = True
48
+ except ImportError:
49
+ redis = None # type: ignore[assignment]
50
+ get_redis_client = None # type: ignore[assignment]
51
+ REDIS_AVAILABLE = False
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+
56
+ @dataclass
57
+ class SessionMetrics:
58
+ """Session-level performance metrics."""
59
+
60
+ session_id: str
61
+ user_id: str
62
+ created_at: datetime
63
+ last_activity: datetime
64
+ request_count: int = 0
65
+ error_count: int = 0
66
+ total_processing_time: float = 0.0
67
+ status: str = "active" # active, completed, failed, expired
68
+
69
+ def to_dict(self) -> Dict[str, Any]:
70
+ return {
71
+ **asdict(self),
72
+ "created_at": self.created_at.isoformat(),
73
+ "last_activity": self.last_activity.isoformat(),
74
+ }
75
+
76
+ @classmethod
77
+ def from_dict(cls, data: Dict[str, Any]) -> "SessionMetrics":
78
+ data = data.copy()
79
+ data["created_at"] = datetime.fromisoformat(data["created_at"])
80
+ data["last_activity"] = datetime.fromisoformat(data["last_activity"])
81
+ return cls(**data)
82
+
83
+
84
+ @dataclass
85
+ class ConversationMessage:
86
+ """Structured conversation message."""
87
+
88
+ role: str # user, assistant, system
89
+ content: str
90
+ timestamp: datetime
91
+ metadata: Optional[Dict[str, Any]] = None
92
+
93
+ def to_dict(self) -> Dict[str, Any]:
94
+ return {
95
+ "role": self.role,
96
+ "content": self.content,
97
+ "timestamp": self.timestamp.isoformat(),
98
+ "metadata": self.metadata or {},
99
+ }
100
+
101
+ @classmethod
102
+ def from_dict(cls, data: Dict[str, Any]) -> "ConversationMessage":
103
+ data = data.copy()
104
+ data["timestamp"] = datetime.fromisoformat(data["timestamp"])
105
+ return cls(**data)
106
+
107
+
108
+ @dataclass
109
+ class CompressionConfig:
110
+ """
111
+ Configuration for conversation compression.
112
+
113
+ Provides flexible control over compression behavior with multiple strategies
114
+ to manage conversation history size and reduce token usage.
115
+
116
+ **Compression Strategies:**
117
+ - truncate: Fast truncation, keeps most recent N messages (no LLM required)
118
+ - summarize: LLM-based summarization of older messages
119
+ - semantic: Embedding-based deduplication of similar messages
120
+ - hybrid: Combination of multiple strategies applied sequentially
121
+
122
+ **Key Features:**
123
+ - Automatic compression triggers based on message count
124
+ - Custom prompt templates for summarization
125
+ - Configurable similarity thresholds for semantic deduplication
126
+ - Performance timeouts to prevent long-running operations
127
+
128
+ Attributes:
129
+ strategy: Compression strategy to use. One of: "truncate", "summarize", "semantic", "hybrid"
130
+ max_messages: Maximum messages to keep (for truncation strategy)
131
+ keep_recent: Always keep N most recent messages (applies to all strategies)
132
+ summary_prompt_template: Custom prompt template for summarization (uses {messages} placeholder)
133
+ summary_max_tokens: Maximum tokens for summary output
134
+ include_summary_in_history: Whether to add summary as system message in history
135
+ similarity_threshold: Similarity threshold for semantic deduplication (0.0-1.0)
136
+ embedding_model: Embedding model name for semantic deduplication
137
+ hybrid_strategies: List of strategies to combine for hybrid mode (default: ["truncate", "summarize"])
138
+ auto_compress_enabled: Enable automatic compression when threshold exceeded
139
+ auto_compress_threshold: Message count threshold to trigger auto-compression
140
+ auto_compress_target: Target message count after auto-compression
141
+ compression_timeout: Maximum time for compression operation in seconds
142
+
143
+ Examples:
144
+ # Example 1: Basic truncation configuration
145
+ config = CompressionConfig(
146
+ strategy="truncate",
147
+ max_messages=50,
148
+ keep_recent=10
149
+ )
150
+
151
+ # Example 2: LLM-based summarization
152
+ config = CompressionConfig(
153
+ strategy="summarize",
154
+ keep_recent=10,
155
+ summary_max_tokens=500,
156
+ include_summary_in_history=True
157
+ )
158
+
159
+ # Example 3: Semantic deduplication
160
+ config = CompressionConfig(
161
+ strategy="semantic",
162
+ keep_recent=10,
163
+ similarity_threshold=0.95,
164
+ embedding_model="text-embedding-ada-002"
165
+ )
166
+
167
+ # Example 4: Hybrid strategy (truncate then summarize)
168
+ config = CompressionConfig(
169
+ strategy="hybrid",
170
+ hybrid_strategies=["truncate", "summarize"],
171
+ keep_recent=10,
172
+ summary_max_tokens=500
173
+ )
174
+
175
+ # Example 5: Auto-compression enabled
176
+ config = CompressionConfig(
177
+ auto_compress_enabled=True,
178
+ auto_compress_threshold=100,
179
+ auto_compress_target=50,
180
+ strategy="summarize",
181
+ keep_recent=10
182
+ )
183
+
184
+ # Example 6: Custom summarization prompt
185
+ config = CompressionConfig(
186
+ strategy="summarize",
187
+ summary_prompt_template=(
188
+ "Summarize the following conversation focusing on "
189
+ "key decisions and action items:\n\n{messages}"
190
+ ),
191
+ summary_max_tokens=300
192
+ )
193
+ """
194
+
195
+ # Strategy selection
196
+ strategy: str = "truncate" # truncate, summarize, semantic, hybrid
197
+
198
+ # Truncation settings
199
+ max_messages: int = 50 # Maximum messages to keep
200
+ keep_recent: int = 10 # Always keep N most recent messages
201
+
202
+ # Summarization settings (LLM-based)
203
+ summary_prompt_template: Optional[str] = None # Custom prompt template
204
+ summary_max_tokens: int = 500 # Max tokens for summary
205
+ include_summary_in_history: bool = True # Add summary as system message
206
+
207
+ # Semantic deduplication settings (embedding-based)
208
+ similarity_threshold: float = 0.95 # Messages above this similarity are duplicates
209
+ embedding_model: str = "text-embedding-ada-002" # Embedding model to use
210
+
211
+ # Hybrid strategy settings
212
+ hybrid_strategies: Optional[List[str]] = None # Strategies to combine (default: ["truncate", "summarize"])
213
+
214
+ # Auto-compression triggers
215
+ auto_compress_enabled: bool = False # Enable automatic compression
216
+ auto_compress_threshold: int = 100 # Trigger when message count exceeds this
217
+ auto_compress_target: int = 50 # Target message count after compression
218
+
219
+ # Performance settings
220
+ compression_timeout: float = 30.0 # Max time for compression operation (seconds)
221
+
222
+ def __post_init__(self):
223
+ """Validate and set defaults."""
224
+ if self.hybrid_strategies is None:
225
+ self.hybrid_strategies = ["truncate", "summarize"]
226
+
227
+ # Validate strategy
228
+ valid_strategies = ["truncate", "summarize", "semantic", "hybrid"]
229
+ if self.strategy not in valid_strategies:
230
+ raise ValueError(f"Invalid strategy '{self.strategy}'. " f"Must be one of: {', '.join(valid_strategies)}")
231
+
232
+
233
+ class ContextEngine(IStorageBackend, ICheckpointerBackend):
234
+ """
235
+ Advanced Context and Session Management Engine.
236
+
237
+ Implements core storage interfaces to provide comprehensive session management
238
+ with Redis backend storage for BaseAIService and BaseServiceCheckpointer.
239
+
240
+ This implementation follows the middleware's core interface pattern,
241
+ enabling dependency inversion and clean architecture.
242
+
243
+ **Key Features:**
244
+ - Multi-session management with Redis backend
245
+ - Conversation history management with compression
246
+ - Performance metrics and analytics
247
+ - Resource and lifecycle management
248
+ - Integration with BaseServiceCheckpointer
249
+
250
+ **Compression Strategies:**
251
+ - truncate: Fast truncation (no LLM required)
252
+ - summarize: LLM-based summarization
253
+ - semantic: Embedding-based deduplication
254
+ - hybrid: Combination of multiple strategies
255
+
256
+ Examples:
257
+ # Example 1: Basic ContextEngine initialization
258
+ engine = ContextEngine()
259
+ await engine.initialize()
260
+
261
+ # Create session
262
+ session = await engine.create_session(
263
+ session_id="session-123",
264
+ user_id="user-456"
265
+ )
266
+
267
+ # Add conversation messages
268
+ await engine.add_conversation_message(
269
+ session_id="session-123",
270
+ role="user",
271
+ content="Hello, I need help"
272
+ )
273
+
274
+ # Example 2: ContextEngine with compression (truncation strategy)
275
+ from aiecs.domain.context.context_engine import CompressionConfig
276
+
277
+ compression_config = CompressionConfig(
278
+ strategy="truncate",
279
+ max_messages=50,
280
+ keep_recent=10 # Always keep 10 most recent messages
281
+ )
282
+
283
+ engine = ContextEngine(compression_config=compression_config)
284
+ await engine.initialize()
285
+
286
+ # Add many messages
287
+ for i in range(100):
288
+ await engine.add_conversation_message(
289
+ session_id="session-123",
290
+ role="user" if i % 2 == 0 else "assistant",
291
+ content=f"Message {i}"
292
+ )
293
+
294
+ # Compress conversation (truncates to 10 most recent)
295
+ result = await engine.compress_conversation("session-123")
296
+ print(f"Compressed from {result['original_count']} to {result['compressed_count']} messages")
297
+
298
+ # Example 3: ContextEngine with LLM-based summarization
299
+ from aiecs.llm import OpenAIClient
300
+
301
+ llm_client = OpenAIClient()
302
+
303
+ compression_config = CompressionConfig(
304
+ strategy="summarize",
305
+ keep_recent=10, # Keep 10 most recent messages
306
+ summary_max_tokens=500,
307
+ include_summary_in_history=True
308
+ )
309
+
310
+ engine = ContextEngine(
311
+ compression_config=compression_config,
312
+ llm_client=llm_client # Required for summarization
313
+ )
314
+ await engine.initialize()
315
+
316
+ # Add conversation
317
+ for i in range(50):
318
+ await engine.add_conversation_message(
319
+ session_id="session-123",
320
+ role="user" if i % 2 == 0 else "assistant",
321
+ content=f"Message {i}: Important information about topic {i % 5}"
322
+ )
323
+
324
+ # Compress using summarization
325
+ result = await engine.compress_conversation("session-123", strategy="summarize")
326
+ print(f"Compressed: {result['original_count']} -> {result['compressed_count']} messages")
327
+ print(f"Compression ratio: {result['compression_ratio']:.1%}")
328
+
329
+ # Example 4: ContextEngine with semantic deduplication
330
+ compression_config = CompressionConfig(
331
+ strategy="semantic",
332
+ keep_recent=10,
333
+ similarity_threshold=0.95, # Remove messages >95% similar
334
+ embedding_model="text-embedding-ada-002"
335
+ )
336
+
337
+ engine = ContextEngine(
338
+ compression_config=compression_config,
339
+ llm_client=llm_client # Required for embeddings
340
+ )
341
+ await engine.initialize()
342
+
343
+ # Add conversation with similar messages
344
+ messages = [
345
+ "What's the weather?",
346
+ "What's the weather today?",
347
+ "Tell me about the weather",
348
+ "What's the temperature?"
349
+ ]
350
+ for msg in messages:
351
+ await engine.add_conversation_message(
352
+ session_id="session-123",
353
+ role="user",
354
+ content=msg
355
+ )
356
+
357
+ # Compress using semantic deduplication
358
+ result = await engine.compress_conversation("session-123", strategy="semantic")
359
+ print(f"Removed {result['original_count'] - result['compressed_count']} similar messages")
360
+
361
+ # Example 5: ContextEngine with hybrid compression
362
+ compression_config = CompressionConfig(
363
+ strategy="hybrid",
364
+ hybrid_strategies=["truncate", "summarize"], # Apply truncate then summarize
365
+ keep_recent=10,
366
+ summary_max_tokens=500
367
+ )
368
+
369
+ engine = ContextEngine(
370
+ compression_config=compression_config,
371
+ llm_client=llm_client
372
+ )
373
+ await engine.initialize()
374
+
375
+ # Compress using hybrid strategy
376
+ result = await engine.compress_conversation("session-123", strategy="hybrid")
377
+
378
+ # Example 6: Auto-compression on message limit
379
+ compression_config = CompressionConfig(
380
+ auto_compress_enabled=True,
381
+ auto_compress_threshold=100, # Trigger at 100 messages
382
+ auto_compress_target=50, # Compress to 50 messages
383
+ strategy="summarize",
384
+ keep_recent=10
385
+ )
386
+
387
+ engine = ContextEngine(
388
+ compression_config=compression_config,
389
+ llm_client=llm_client
390
+ )
391
+ await engine.initialize()
392
+
393
+ # Add messages - auto-compression triggers at 100
394
+ for i in range(105):
395
+ await engine.add_conversation_message(
396
+ session_id="session-123",
397
+ role="user" if i % 2 == 0 else "assistant",
398
+ content=f"Message {i}"
399
+ )
400
+
401
+ # Check if auto-compression was triggered
402
+ result = await engine.auto_compress_on_limit("session-123")
403
+ if result:
404
+ print(f"Auto-compressed: {result['original_count']} -> {result['compressed_count']}")
405
+
406
+ # Example 7: Custom compression prompt template
407
+ compression_config = CompressionConfig(
408
+ strategy="summarize",
409
+ summary_prompt_template=(
410
+ "Summarize the following conversation focusing on key decisions, "
411
+ "action items, and important facts. Keep it concise:\n\n{messages}"
412
+ ),
413
+ summary_max_tokens=300
414
+ )
415
+
416
+ engine = ContextEngine(
417
+ compression_config=compression_config,
418
+ llm_client=llm_client
419
+ )
420
+ await engine.initialize()
421
+
422
+ # Compress with custom prompt
423
+ result = await engine.compress_conversation("session-123")
424
+
425
+ # Example 8: Get compressed context in different formats
426
+ engine = ContextEngine(compression_config=compression_config, llm_client=llm_client)
427
+ await engine.initialize()
428
+
429
+ # Get as formatted string
430
+ context_string = await engine.get_compressed_context(
431
+ session_id="session-123",
432
+ format="string",
433
+ compress_first=True # Compress before returning
434
+ )
435
+ print(context_string)
436
+
437
+ # Get as messages list
438
+ messages = await engine.get_compressed_context(
439
+ session_id="session-123",
440
+ format="messages",
441
+ compress_first=False # Use existing compressed version
442
+ )
443
+
444
+ # Get as dictionary
445
+ context_dict = await engine.get_compressed_context(
446
+ session_id="session-123",
447
+ format="dict"
448
+ )
449
+
450
+ # Example 9: Runtime compression config override
451
+ engine = ContextEngine(
452
+ compression_config=CompressionConfig(strategy="truncate"),
453
+ llm_client=llm_client
454
+ )
455
+ await engine.initialize()
456
+
457
+ # Override compression config for specific operation
458
+ custom_config = CompressionConfig(
459
+ strategy="summarize",
460
+ summary_max_tokens=1000
461
+ )
462
+
463
+ result = await engine.compress_conversation(
464
+ session_id="session-123",
465
+ config_override=custom_config
466
+ )
467
+
468
+ # Example 10: Compression with custom LLM client
469
+ class CustomLLMClient:
470
+ provider_name = "custom"
471
+
472
+ async def generate_text(self, messages, **kwargs):
473
+ # Custom summarization logic
474
+ return LLMResponse(content="Custom summary...")
475
+
476
+ async def get_embeddings(self, texts, model):
477
+ # Custom embedding logic
478
+ return [[0.1] * 1536 for _ in texts]
479
+
480
+ custom_llm = CustomLLMClient()
481
+
482
+ compression_config = CompressionConfig(strategy="semantic")
483
+ engine = ContextEngine(
484
+ compression_config=compression_config,
485
+ llm_client=custom_llm # Custom LLM client for compression
486
+ )
487
+ await engine.initialize()
488
+
489
+ # Compress using custom LLM client
490
+ result = await engine.compress_conversation("session-123", strategy="semantic")
491
+ """
492
+
493
+ def __init__(
494
+ self,
495
+ use_existing_redis: bool = True,
496
+ compression_config: Optional[CompressionConfig] = None,
497
+ llm_client: Optional[Any] = None,
498
+ ):
499
+ """
500
+ Initialize ContextEngine.
501
+
502
+ Args:
503
+ use_existing_redis: Whether to use the existing Redis client from infrastructure
504
+ (已弃用: 现在总是创建独立的 RedisClient 实例以避免事件循环冲突)
505
+ compression_config: Optional compression configuration for conversation compression
506
+ llm_client: Optional LLM client for summarization and embeddings (must implement LLMClientProtocol)
507
+ """
508
+ self.use_existing_redis = use_existing_redis
509
+ self.redis_client: Optional[redis.Redis] = None
510
+ self._redis_client_wrapper: Optional[Any] = None # RedisClient 包装器实例
511
+
512
+ # Fallback to memory storage if Redis not available
513
+ self._memory_sessions: Dict[str, SessionMetrics] = {}
514
+ self._memory_conversations: Dict[str, List[ConversationMessage]] = {}
515
+ self._memory_contexts: Dict[str, TaskContext] = {}
516
+ self._memory_checkpoints: Dict[str, Dict[str, Any]] = {}
517
+
518
+ # Configuration
519
+ self.session_ttl = 3600 * 24 # 24 hours default TTL
520
+ self.conversation_limit = 1000 # Max messages per conversation
521
+ self.checkpoint_ttl = 3600 * 24 * 7 # 7 days for checkpoints
522
+
523
+ # Compression configuration (Phase 6)
524
+ self.compression_config = compression_config or CompressionConfig()
525
+ self.llm_client = llm_client
526
+
527
+ # Metrics
528
+ self._global_metrics = {
529
+ "total_sessions": 0,
530
+ "active_sessions": 0,
531
+ "total_messages": 0,
532
+ "total_checkpoints": 0,
533
+ }
534
+
535
+ logger.info(f"ContextEngine initialized with compression strategy: {self.compression_config.strategy}")
536
+
537
+ async def initialize(self) -> bool:
538
+ """Initialize Redis connection and validate setup."""
539
+ if not REDIS_AVAILABLE:
540
+ logger.warning("Redis not available, using memory storage")
541
+ return True
542
+
543
+ try:
544
+ # ✅ 修复方案:在当前事件循环中创建新的 RedisClient 实例
545
+ #
546
+ # 问题根源:
547
+ # - 全局 RedisClient 单例在应用启动的事件循环A中创建
548
+ # - ContextEngine 可能在不同的事件循环B中被初始化(例如在请求处理中)
549
+ # - redis.asyncio 的连接池绑定到创建时的事件循环
550
+ # - 跨事件循环使用会导致 "Task got Future attached to a different loop" 错误
551
+ #
552
+ # 解决方案:
553
+ # - 为每个 ContextEngine 实例创建独立的 RedisClient
554
+ # - 使用 RedisClient 包装器保持架构一致性
555
+ # - 在当前事件循环中初始化,确保事件循环匹配
556
+
557
+ from aiecs.infrastructure.persistence.redis_client import (
558
+ RedisClient,
559
+ )
560
+
561
+ # 创建专属的 RedisClient 实例(在当前事件循环中)
562
+ self._redis_client_wrapper = RedisClient()
563
+ await self._redis_client_wrapper.initialize()
564
+
565
+ # 获取底层 redis.Redis 客户端用于现有代码
566
+ self.redis_client = await self._redis_client_wrapper.get_client()
567
+
568
+ # Test connection
569
+ await self.redis_client.ping()
570
+ logger.info("ContextEngine connected to Redis successfully using RedisClient wrapper in current event loop")
571
+ return True
572
+
573
+ except Exception as e:
574
+ logger.error(f"Failed to connect to Redis: {e}")
575
+ logger.warning("Falling back to memory storage")
576
+ self.redis_client = None
577
+ self._redis_client_wrapper = None
578
+ return False
579
+
580
+ async def close(self):
581
+ """Close Redis connection."""
582
+ if hasattr(self, "_redis_client_wrapper") and self._redis_client_wrapper:
583
+ # 使用 RedisClient 包装器的 close 方法
584
+ await self._redis_client_wrapper.close()
585
+ self._redis_client_wrapper = None
586
+ self.redis_client = None
587
+ elif self.redis_client:
588
+ # 兼容性处理:直接关闭 redis 客户端
589
+ await self.redis_client.close()
590
+ self.redis_client = None
591
+
592
+ # ==================== Session Management ====================
593
+
594
+ async def create_session(self, session_id: str, user_id: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
595
+ """Create a new session."""
596
+ now = datetime.utcnow()
597
+ session = SessionMetrics(
598
+ session_id=session_id,
599
+ user_id=user_id,
600
+ created_at=now,
601
+ last_activity=now,
602
+ )
603
+
604
+ # Store session
605
+ await self._store_session(session)
606
+
607
+ # Create associated TaskContext
608
+ task_context = TaskContext(
609
+ {
610
+ "user_id": user_id,
611
+ "chat_id": session_id,
612
+ "metadata": metadata or {},
613
+ }
614
+ )
615
+ await self._store_task_context(session_id, task_context)
616
+
617
+ # Update metrics
618
+ self._global_metrics["total_sessions"] += 1
619
+ self._global_metrics["active_sessions"] += 1
620
+
621
+ logger.info(f"Created session {session_id} for user {user_id}")
622
+ return session.to_dict()
623
+
624
+ async def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
625
+ """Get session by ID."""
626
+ if self.redis_client:
627
+ try:
628
+ data = await self.redis_client.hget("sessions", session_id) # type: ignore[misc]
629
+ if data:
630
+ session = SessionMetrics.from_dict(json.loads(data))
631
+ return session.to_dict()
632
+ except Exception as e:
633
+ logger.error(f"Failed to get session from Redis: {e}")
634
+
635
+ # Fallback to memory
636
+ memory_session: Optional[SessionMetrics] = self._memory_sessions.get(session_id)
637
+ return memory_session.to_dict() if memory_session else None
638
+
639
+ async def update_session(
640
+ self,
641
+ session_id: str,
642
+ updates: Optional[Dict[str, Any]] = None,
643
+ increment_requests: bool = False,
644
+ add_processing_time: float = 0.0,
645
+ mark_error: bool = False,
646
+ ) -> bool:
647
+ """Update session with activity and metrics."""
648
+ session_data = await self.get_session(session_id)
649
+ if not session_data:
650
+ return False
651
+
652
+ # Convert dict to SessionMetrics if needed
653
+ session: SessionMetrics
654
+ if isinstance(session_data, dict):
655
+ session = SessionMetrics.from_dict(session_data)
656
+ else:
657
+ session = session_data
658
+
659
+ # Update activity
660
+ session.last_activity = datetime.utcnow()
661
+
662
+ # Update metrics
663
+ if increment_requests:
664
+ session.request_count += 1
665
+ if add_processing_time > 0:
666
+ session.total_processing_time += add_processing_time
667
+ if mark_error:
668
+ session.error_count += 1
669
+
670
+ # Apply custom updates
671
+ if updates:
672
+ for key, value in updates.items():
673
+ if hasattr(session, key):
674
+ setattr(session, key, value)
675
+
676
+ # Store updated session
677
+ await self._store_session(session)
678
+ return True
679
+
680
+ async def end_session(self, session_id: str, status: str = "completed") -> bool:
681
+ """End a session and update metrics."""
682
+ session_data = await self.get_session(session_id)
683
+ if not session_data:
684
+ return False
685
+
686
+ # Convert dict to SessionMetrics if needed
687
+ session = SessionMetrics.from_dict(session_data) if isinstance(session_data, dict) else session_data
688
+ session.status = status
689
+ session.last_activity = datetime.utcnow()
690
+
691
+ # Store final state
692
+ await self._store_session(session)
693
+
694
+ # Update global metrics
695
+ self._global_metrics["active_sessions"] = max(0, self._global_metrics["active_sessions"] - 1)
696
+
697
+ logger.info(f"Ended session {session_id} with status: {status}")
698
+ return True
699
+
700
+ async def _store_session(self, session: SessionMetrics):
701
+ """Store session to Redis or memory."""
702
+ if self.redis_client:
703
+ try:
704
+ await self.redis_client.hset( # type: ignore[misc]
705
+ "sessions",
706
+ session.session_id,
707
+ json.dumps(session.to_dict(), cls=DateTimeEncoder),
708
+ )
709
+ await self.redis_client.expire("sessions", self.session_ttl) # type: ignore[misc]
710
+ return
711
+ except Exception as e:
712
+ logger.error(f"Failed to store session to Redis: {e}")
713
+
714
+ # Fallback to memory
715
+ self._memory_sessions[session.session_id] = session
716
+
717
+ # ==================== Conversation Management ====================
718
+
719
+ async def add_conversation_message(
720
+ self,
721
+ session_id: str,
722
+ role: str,
723
+ content: str,
724
+ metadata: Optional[Dict[str, Any]] = None,
725
+ ) -> bool:
726
+ """Add message to conversation history."""
727
+ message = ConversationMessage(
728
+ role=role,
729
+ content=content,
730
+ timestamp=datetime.utcnow(),
731
+ metadata=metadata,
732
+ )
733
+
734
+ # Store message
735
+ await self._store_conversation_message(session_id, message)
736
+
737
+ # Update session activity
738
+ await self.update_session(session_id)
739
+
740
+ # Update global metrics
741
+ self._global_metrics["total_messages"] += 1
742
+
743
+ return True
744
+
745
+ async def get_conversation_history(self, session_id: str, limit: int = 50) -> List[Dict[str, Any]]:
746
+ """Get conversation history for a session."""
747
+ if self.redis_client:
748
+ try:
749
+ messages_data = await self.redis_client.lrange(f"conversation:{session_id}", -limit, -1) # type: ignore[misc]
750
+ # Since lpush adds to the beginning, we need to reverse to get
751
+ # chronological order
752
+ messages = [ConversationMessage.from_dict(json.loads(msg)) for msg in reversed(messages_data)]
753
+ return [msg.to_dict() for msg in messages]
754
+ except Exception as e:
755
+ logger.error(f"Failed to get conversation from Redis: {e}")
756
+
757
+ # Fallback to memory
758
+ messages = self._memory_conversations.get(session_id, [])
759
+ message_list = messages[-limit:] if limit > 0 else messages
760
+ return [msg.to_dict() for msg in message_list]
761
+
762
+ async def _store_conversation_message(self, session_id: str, message: ConversationMessage):
763
+ """Store conversation message to Redis or memory."""
764
+ if self.redis_client:
765
+ try:
766
+ # Add to list
767
+ await self.redis_client.lpush( # type: ignore[misc]
768
+ f"conversation:{session_id}",
769
+ json.dumps(message.to_dict(), cls=DateTimeEncoder),
770
+ )
771
+ # Trim to limit
772
+ await self.redis_client.ltrim(f"conversation:{session_id}", -self.conversation_limit, -1) # type: ignore[misc]
773
+ # Set TTL
774
+ await self.redis_client.expire(f"conversation:{session_id}", self.session_ttl)
775
+ return
776
+ except Exception as e:
777
+ logger.error(f"Failed to store message to Redis: {e}")
778
+
779
+ # Fallback to memory
780
+ if session_id not in self._memory_conversations:
781
+ self._memory_conversations[session_id] = []
782
+
783
+ self._memory_conversations[session_id].append(message)
784
+
785
+ # Trim to limit
786
+ if len(self._memory_conversations[session_id]) > self.conversation_limit:
787
+ self._memory_conversations[session_id] = self._memory_conversations[session_id][-self.conversation_limit :]
788
+
789
+ # ==================== TaskContext Integration ====================
790
+
791
+ async def get_task_context(self, session_id: str) -> Optional[TaskContext]:
792
+ """Get TaskContext for a session."""
793
+ if self.redis_client:
794
+ try:
795
+ data = await self.redis_client.hget("task_contexts", session_id) # type: ignore[misc]
796
+ if data:
797
+ context_data = json.loads(data)
798
+ # Reconstruct TaskContext from stored data
799
+ return self._reconstruct_task_context(context_data)
800
+ except Exception as e:
801
+ logger.error(f"Failed to get TaskContext from Redis: {e}")
802
+
803
+ # Fallback to memory
804
+ return self._memory_contexts.get(session_id)
805
+
806
+ def _sanitize_dataclasses(self, obj: Any) -> Any:
807
+ """
808
+ Recursively convert dataclasses to dictionaries for JSON serialization.
809
+
810
+ This method handles:
811
+ - Dataclass instances -> dict (via asdict)
812
+ - Nested dataclasses in dictionaries
813
+ - Nested dataclasses in lists
814
+ - Other types -> pass through
815
+
816
+ Args:
817
+ obj: Object to sanitize
818
+
819
+ Returns:
820
+ Sanitized object (JSON-serializable)
821
+ """
822
+ # Handle dataclass instances
823
+ if is_dataclass(obj) and not isinstance(obj, type):
824
+ logger.debug(f"Converting dataclass {type(obj).__name__} to dict for serialization")
825
+ # Convert dataclass to dict and recursively sanitize
826
+ return self._sanitize_dataclasses(asdict(obj))
827
+
828
+ # Handle dictionaries
829
+ if isinstance(obj, dict):
830
+ return {key: self._sanitize_dataclasses(value) for key, value in obj.items()}
831
+
832
+ # Handle lists and tuples
833
+ if isinstance(obj, (list, tuple)):
834
+ sanitized_list = [self._sanitize_dataclasses(item) for item in obj]
835
+ return sanitized_list if isinstance(obj, list) else tuple(sanitized_list)
836
+
837
+ # Handle sets
838
+ if isinstance(obj, set):
839
+ return [self._sanitize_dataclasses(item) for item in obj]
840
+
841
+ # All other types pass through
842
+ return obj
843
+
844
+ async def _store_task_context(self, session_id: str, context: TaskContext):
845
+ """
846
+ Store TaskContext to Redis or memory.
847
+
848
+ Automatically converts dataclasses to dictionaries to ensure
849
+ JSON serialization compatibility.
850
+ """
851
+ if self.redis_client:
852
+ try:
853
+ # Get context dict and sanitize dataclasses
854
+ context_dict = context.to_dict()
855
+ sanitized_dict = self._sanitize_dataclasses(context_dict)
856
+
857
+ await self.redis_client.hset( # type: ignore[misc]
858
+ "task_contexts",
859
+ session_id,
860
+ json.dumps(sanitized_dict, cls=DateTimeEncoder),
861
+ )
862
+ await self.redis_client.expire("task_contexts", self.session_ttl) # type: ignore[misc]
863
+ return
864
+ except Exception as e:
865
+ logger.error(f"Failed to store TaskContext to Redis: {e}")
866
+
867
+ # Fallback to memory
868
+ self._memory_contexts[session_id] = context
869
+
870
+ def _reconstruct_task_context(self, data: Dict[str, Any]) -> TaskContext:
871
+ """Reconstruct TaskContext from stored data."""
872
+ # Create new TaskContext with stored data
873
+ context = TaskContext(data)
874
+
875
+ # Restore context history
876
+ if "context_history" in data:
877
+ context.context_history = [
878
+ ContextUpdate(
879
+ timestamp=entry["timestamp"],
880
+ update_type=entry["update_type"],
881
+ data=entry["data"],
882
+ metadata=entry["metadata"],
883
+ )
884
+ for entry in data["context_history"]
885
+ ]
886
+
887
+ return context
888
+
889
+ # ==================== Checkpoint Management (for BaseServiceCheckpointer)
890
+
891
+ async def store_checkpoint(
892
+ self,
893
+ thread_id: str,
894
+ checkpoint_id: str,
895
+ checkpoint_data: Dict[str, Any],
896
+ metadata: Optional[Dict[str, Any]] = None,
897
+ ) -> bool:
898
+ """
899
+ Store checkpoint data for LangGraph workflows.
900
+
901
+ Automatically converts dataclasses to dictionaries to ensure
902
+ JSON serialization compatibility.
903
+ """
904
+ # Sanitize checkpoint data to handle dataclasses
905
+ sanitized_data = self._sanitize_dataclasses(checkpoint_data)
906
+ sanitized_metadata = self._sanitize_dataclasses(metadata or {})
907
+
908
+ checkpoint = {
909
+ "checkpoint_id": checkpoint_id,
910
+ "thread_id": thread_id,
911
+ "data": sanitized_data,
912
+ "metadata": sanitized_metadata,
913
+ "created_at": datetime.utcnow().isoformat(),
914
+ }
915
+
916
+ if self.redis_client:
917
+ try:
918
+ # Store checkpoint
919
+ await self.redis_client.hset( # type: ignore[misc]
920
+ f"checkpoints:{thread_id}",
921
+ checkpoint_id,
922
+ json.dumps(checkpoint, cls=DateTimeEncoder),
923
+ )
924
+ # Set TTL
925
+ await self.redis_client.expire(f"checkpoints:{thread_id}", self.checkpoint_ttl) # type: ignore[misc]
926
+
927
+ # Update global metrics
928
+ self._global_metrics["total_checkpoints"] += 1
929
+ return True
930
+
931
+ except Exception as e:
932
+ logger.error(f"Failed to store checkpoint to Redis: {e}")
933
+
934
+ # Fallback to memory
935
+ key = f"{thread_id}:{checkpoint_id}"
936
+ self._memory_checkpoints[key] = checkpoint
937
+ return True
938
+
939
+ async def get_checkpoint(self, thread_id: str, checkpoint_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
940
+ """Get checkpoint data. If checkpoint_id is None, get the latest."""
941
+ if self.redis_client:
942
+ try:
943
+ if checkpoint_id:
944
+ # Get specific checkpoint
945
+ data = await self.redis_client.hget(f"checkpoints:{thread_id}", checkpoint_id) # type: ignore[misc]
946
+ if data:
947
+ return json.loads(data)
948
+ else:
949
+ # Get latest checkpoint
950
+ checkpoints = await self.redis_client.hgetall(f"checkpoints:{thread_id}") # type: ignore[misc]
951
+ if checkpoints:
952
+ # Sort by creation time and get latest
953
+ latest = max(
954
+ checkpoints.values(),
955
+ key=lambda x: json.loads(x)["created_at"],
956
+ )
957
+ return json.loads(latest)
958
+ except Exception as e:
959
+ logger.error(f"Failed to get checkpoint from Redis: {e}")
960
+
961
+ # Fallback to memory
962
+ if checkpoint_id:
963
+ key = f"{thread_id}:{checkpoint_id}"
964
+ return self._memory_checkpoints.get(key)
965
+ else:
966
+ # Get latest from memory
967
+ thread_checkpoints = {k: v for k, v in self._memory_checkpoints.items() if k.startswith(f"{thread_id}:")}
968
+ if thread_checkpoints:
969
+ latest_key = max(
970
+ thread_checkpoints.keys(),
971
+ key=lambda k: thread_checkpoints[k]["created_at"],
972
+ )
973
+ return thread_checkpoints[latest_key]
974
+
975
+ return None
976
+
977
+ async def list_checkpoints(self, thread_id: str, limit: int = 10) -> List[Dict[str, Any]]:
978
+ """List checkpoints for a thread, ordered by creation time (newest first)."""
979
+ if self.redis_client:
980
+ try:
981
+ checkpoints_data = await self.redis_client.hgetall(f"checkpoints:{thread_id}") # type: ignore[misc]
982
+ checkpoints = [json.loads(data) for data in checkpoints_data.values()]
983
+ # Sort by creation time (newest first)
984
+ checkpoints.sort(key=lambda x: x["created_at"], reverse=True)
985
+ return checkpoints[:limit]
986
+ except Exception as e:
987
+ logger.error(f"Failed to list checkpoints from Redis: {e}")
988
+
989
+ # Fallback to memory
990
+ thread_checkpoints = [v for k, v in self._memory_checkpoints.items() if k.startswith(f"{thread_id}:")]
991
+ thread_checkpoints.sort(key=lambda x: x["created_at"], reverse=True)
992
+ return thread_checkpoints[:limit]
993
+
994
+ # ==================== Cleanup and Maintenance ====================
995
+
996
+ async def cleanup_expired_sessions(self, max_idle_hours: int = 24) -> int:
997
+ """Clean up expired sessions and associated data."""
998
+ cutoff_time = datetime.utcnow() - timedelta(hours=max_idle_hours)
999
+ cleaned_count = 0
1000
+
1001
+ if self.redis_client:
1002
+ try:
1003
+ # Get all sessions
1004
+ sessions_data = await self.redis_client.hgetall("sessions") # type: ignore[misc]
1005
+ expired_sessions = []
1006
+
1007
+ for session_id, data in sessions_data.items():
1008
+ session = SessionMetrics.from_dict(json.loads(data))
1009
+ if session.last_activity < cutoff_time:
1010
+ expired_sessions.append(session_id)
1011
+
1012
+ # Clean up expired sessions
1013
+ for session_id in expired_sessions:
1014
+ await self._cleanup_session_data(session_id)
1015
+ cleaned_count += 1
1016
+
1017
+ except Exception as e:
1018
+ logger.error(f"Failed to cleanup expired sessions from Redis: {e}")
1019
+ else:
1020
+ # Memory cleanup
1021
+ expired_sessions = [session_id for session_id, session in self._memory_sessions.items() if session.last_activity < cutoff_time]
1022
+
1023
+ for session_id in expired_sessions:
1024
+ await self._cleanup_session_data(session_id)
1025
+ cleaned_count += 1
1026
+
1027
+ if cleaned_count > 0:
1028
+ logger.info(f"Cleaned up {cleaned_count} expired sessions")
1029
+
1030
+ return cleaned_count
1031
+
1032
+ async def _cleanup_session_data(self, session_id: str):
1033
+ """Clean up all data associated with a session."""
1034
+ if self.redis_client:
1035
+ try:
1036
+ # Remove session
1037
+ await self.redis_client.hdel("sessions", session_id) # type: ignore[misc]
1038
+ # Remove conversation
1039
+ await self.redis_client.delete(f"conversation:{session_id}") # type: ignore[misc]
1040
+ # Remove task context
1041
+ await self.redis_client.hdel("task_contexts", session_id) # type: ignore[misc]
1042
+ # Remove checkpoints
1043
+ await self.redis_client.delete(f"checkpoints:{session_id}") # type: ignore[misc]
1044
+ except Exception as e:
1045
+ logger.error(f"Failed to cleanup session data from Redis: {e}")
1046
+ else:
1047
+ # Memory cleanup
1048
+ self._memory_sessions.pop(session_id, None)
1049
+ self._memory_conversations.pop(session_id, None)
1050
+ self._memory_contexts.pop(session_id, None)
1051
+
1052
+ # Remove checkpoints
1053
+ checkpoint_keys = [k for k in self._memory_checkpoints.keys() if k.startswith(f"{session_id}:")]
1054
+ for key in checkpoint_keys:
1055
+ self._memory_checkpoints.pop(key, None)
1056
+
1057
+ # ==================== Metrics and Health ====================
1058
+
1059
+ async def get_metrics(self) -> Dict[str, Any]:
1060
+ """Get comprehensive metrics."""
1061
+ active_sessions_count = 0
1062
+
1063
+ if self.redis_client:
1064
+ try:
1065
+ sessions_data = await self.redis_client.hgetall("sessions") # type: ignore[misc]
1066
+ active_sessions_count = len([s for s in sessions_data.values() if json.loads(s)["status"] == "active"])
1067
+ except Exception as e:
1068
+ logger.error(f"Failed to get metrics from Redis: {e}")
1069
+ else:
1070
+ active_sessions_count = len([s for s in self._memory_sessions.values() if s.status == "active"])
1071
+
1072
+ return {
1073
+ **self._global_metrics,
1074
+ "active_sessions": active_sessions_count,
1075
+ "storage_backend": "redis" if self.redis_client else "memory",
1076
+ "redis_connected": self.redis_client is not None,
1077
+ "timestamp": datetime.utcnow().isoformat(),
1078
+ }
1079
+
1080
+ async def health_check(self) -> Dict[str, Any]:
1081
+ """Perform health check."""
1082
+ health: Dict[str, Any] = {
1083
+ "status": "healthy",
1084
+ "storage_backend": "redis" if self.redis_client else "memory",
1085
+ "redis_connected": False,
1086
+ "issues": [],
1087
+ }
1088
+ issues: List[str] = health["issues"] # Type narrowing
1089
+
1090
+ # Check Redis connection
1091
+ if self.redis_client:
1092
+ try:
1093
+ await self.redis_client.ping()
1094
+ health["redis_connected"] = True
1095
+ except Exception as e:
1096
+ issues.append(f"Redis connection failed: {e}")
1097
+ health["status"] = "degraded"
1098
+
1099
+ # Check memory usage (basic check)
1100
+ if not self.redis_client:
1101
+ total_memory_items = len(self._memory_sessions) + len(self._memory_conversations) + len(self._memory_contexts) + len(self._memory_checkpoints)
1102
+ if total_memory_items > 10000: # Arbitrary threshold
1103
+ issues.append(f"High memory usage: {total_memory_items} items")
1104
+ health["status"] = "warning"
1105
+
1106
+ health["issues"] = issues # Update health dict
1107
+
1108
+ return health
1109
+
1110
+ # ==================== ICheckpointerBackend Implementation ===============
1111
+
1112
+ async def put_checkpoint(
1113
+ self,
1114
+ thread_id: str,
1115
+ checkpoint_id: str,
1116
+ checkpoint_data: Dict[str, Any],
1117
+ metadata: Optional[Dict[str, Any]] = None,
1118
+ ) -> bool:
1119
+ """Store a checkpoint for LangGraph workflows (ICheckpointerBackend interface)."""
1120
+ return await self.store_checkpoint(thread_id, checkpoint_id, checkpoint_data, metadata)
1121
+
1122
+ async def put_writes(
1123
+ self,
1124
+ thread_id: str,
1125
+ checkpoint_id: str,
1126
+ task_id: str,
1127
+ writes_data: List[tuple],
1128
+ ) -> bool:
1129
+ """Store intermediate writes for a checkpoint (ICheckpointerBackend interface)."""
1130
+ writes_key = f"writes:{thread_id}:{checkpoint_id}:{task_id}"
1131
+ writes_payload = {
1132
+ "thread_id": thread_id,
1133
+ "checkpoint_id": checkpoint_id,
1134
+ "task_id": task_id,
1135
+ "writes": writes_data,
1136
+ "created_at": datetime.utcnow().isoformat(),
1137
+ }
1138
+
1139
+ if self.redis_client:
1140
+ try:
1141
+ await self.redis_client.hset( # type: ignore[misc]
1142
+ f"checkpoint_writes:{thread_id}",
1143
+ f"{checkpoint_id}:{task_id}",
1144
+ json.dumps(writes_payload, cls=DateTimeEncoder),
1145
+ )
1146
+ await self.redis_client.expire(f"checkpoint_writes:{thread_id}", self.checkpoint_ttl)
1147
+ return True
1148
+ except Exception as e:
1149
+ logger.error(f"Failed to store writes to Redis: {e}")
1150
+
1151
+ # Fallback to memory
1152
+ self._memory_checkpoints[writes_key] = writes_payload
1153
+ return True
1154
+
1155
+ async def get_writes(self, thread_id: str, checkpoint_id: str) -> List[tuple]:
1156
+ """Get intermediate writes for a checkpoint (ICheckpointerBackend interface)."""
1157
+ if self.redis_client:
1158
+ try:
1159
+ writes_data = await self.redis_client.hgetall(f"checkpoint_writes:{thread_id}") # type: ignore[misc]
1160
+ writes = []
1161
+ for key, data in writes_data.items():
1162
+ if key.startswith(f"{checkpoint_id}:"):
1163
+ payload = json.loads(data)
1164
+ writes.extend(payload.get("writes", []))
1165
+ return writes
1166
+ except Exception as e:
1167
+ logger.error(f"Failed to get writes from Redis: {e}")
1168
+
1169
+ # Fallback to memory
1170
+ writes = []
1171
+ writes_prefix = f"writes:{thread_id}:{checkpoint_id}:"
1172
+ for key, payload in self._memory_checkpoints.items():
1173
+ if key.startswith(writes_prefix):
1174
+ writes.extend(payload.get("writes", []))
1175
+ return writes
1176
+
1177
+ # ==================== ITaskContextStorage Implementation ================
1178
+
1179
+ async def store_task_context(self, session_id: str, context: Any) -> bool:
1180
+ """Store TaskContext for a session (ITaskContextStorage interface)."""
1181
+ return await self._store_task_context(session_id, context)
1182
+
1183
+ # ==================== Agent Communication and Conversation Isolation ====
1184
+
1185
+ async def create_conversation_session(
1186
+ self,
1187
+ session_id: str,
1188
+ participants: List[Dict[str, Any]],
1189
+ session_type: str,
1190
+ metadata: Optional[Dict[str, Any]] = None,
1191
+ ) -> str:
1192
+ """
1193
+ Create an isolated conversation session between participants.
1194
+
1195
+ Args:
1196
+ session_id: Base session ID
1197
+ participants: List of participant dictionaries with id, type, role
1198
+ session_type: Type of conversation ('user_to_mc', 'mc_to_agent', 'agent_to_agent', 'user_to_agent')
1199
+ metadata: Additional session metadata
1200
+
1201
+ Returns:
1202
+ Generated session key for conversation isolation
1203
+ """
1204
+ from .conversation_models import (
1205
+ ConversationSession,
1206
+ ConversationParticipant,
1207
+ )
1208
+
1209
+ # Create participant objects
1210
+ participant_objects = [
1211
+ ConversationParticipant(
1212
+ participant_id=p.get("id") or "",
1213
+ participant_type=p.get("type") or "",
1214
+ participant_role=p.get("role"),
1215
+ metadata=p.get("metadata", {}),
1216
+ )
1217
+ for p in participants
1218
+ ]
1219
+
1220
+ # Create conversation session
1221
+ conversation_session = ConversationSession(
1222
+ session_id=session_id,
1223
+ participants=participant_objects,
1224
+ session_type=session_type,
1225
+ created_at=datetime.utcnow(),
1226
+ last_activity=datetime.utcnow(),
1227
+ metadata=metadata or {},
1228
+ )
1229
+
1230
+ # Generate unique session key
1231
+ session_key = conversation_session.generate_session_key()
1232
+
1233
+ # Store conversation session metadata
1234
+ await self._store_conversation_session(session_key, conversation_session)
1235
+
1236
+ logger.info(f"Created conversation session: {session_key} (type: {session_type})")
1237
+ return session_key
1238
+
1239
+ async def add_agent_communication_message(
1240
+ self,
1241
+ session_key: str,
1242
+ sender_id: str,
1243
+ sender_type: str,
1244
+ sender_role: Optional[str],
1245
+ recipient_id: str,
1246
+ recipient_type: str,
1247
+ recipient_role: Optional[str],
1248
+ content: str,
1249
+ message_type: str = "communication",
1250
+ metadata: Optional[Dict[str, Any]] = None,
1251
+ ) -> bool:
1252
+ """
1253
+ Add a message to an agent communication session.
1254
+
1255
+ Args:
1256
+ session_key: Isolated session key
1257
+ sender_id: ID of the sender
1258
+ sender_type: Type of sender ('master_controller', 'agent', 'user')
1259
+ sender_role: Role of sender (for agents)
1260
+ recipient_id: ID of the recipient
1261
+ recipient_type: Type of recipient
1262
+ recipient_role: Role of recipient (for agents)
1263
+ content: Message content
1264
+ message_type: Type of message
1265
+ metadata: Additional message metadata
1266
+
1267
+ Returns:
1268
+ Success status
1269
+ """
1270
+ from .conversation_models import AgentCommunicationMessage
1271
+
1272
+ # Create agent communication message
1273
+ message = AgentCommunicationMessage(
1274
+ message_id=str(uuid.uuid4()),
1275
+ session_key=session_key,
1276
+ sender_id=sender_id,
1277
+ sender_type=sender_type,
1278
+ sender_role=sender_role,
1279
+ recipient_id=recipient_id,
1280
+ recipient_type=recipient_type,
1281
+ recipient_role=recipient_role,
1282
+ content=content,
1283
+ message_type=message_type,
1284
+ timestamp=datetime.utcnow(),
1285
+ metadata=metadata or {},
1286
+ )
1287
+
1288
+ # Convert to conversation message format and store
1289
+ conv_message_dict = message.to_conversation_message_dict()
1290
+
1291
+ # Store using existing conversation message infrastructure
1292
+ await self.add_conversation_message(
1293
+ session_id=session_key,
1294
+ role=conv_message_dict["role"],
1295
+ content=conv_message_dict["content"],
1296
+ metadata=conv_message_dict["metadata"],
1297
+ )
1298
+
1299
+ # Update session activity
1300
+ await self._update_conversation_session_activity(session_key)
1301
+
1302
+ logger.debug(f"Added agent communication message to session {session_key}")
1303
+ return True
1304
+
1305
+ async def get_agent_conversation_history(
1306
+ self,
1307
+ session_key: str,
1308
+ limit: int = 50,
1309
+ message_types: Optional[List[str]] = None,
1310
+ ) -> List[Dict[str, Any]]:
1311
+ """
1312
+ Get conversation history for an agent communication session.
1313
+
1314
+ Args:
1315
+ session_key: Isolated session key
1316
+ limit: Maximum number of messages to retrieve
1317
+ message_types: Filter by message types
1318
+
1319
+ Returns:
1320
+ List of conversation messages
1321
+ """
1322
+ # Get conversation history using existing infrastructure
1323
+ messages = await self.get_conversation_history(session_key, limit)
1324
+
1325
+ # Filter by message types if specified
1326
+ if message_types:
1327
+ filtered_messages = []
1328
+ for msg in messages:
1329
+ if hasattr(msg, "to_dict"):
1330
+ msg_dict = msg.to_dict()
1331
+ else:
1332
+ msg_dict = msg # type: ignore[assignment]
1333
+
1334
+ msg_metadata = msg_dict.get("metadata", {})
1335
+ msg_type = msg_metadata.get("message_type", "communication")
1336
+
1337
+ if msg_type in message_types:
1338
+ filtered_messages.append(msg_dict)
1339
+
1340
+ return filtered_messages
1341
+
1342
+ # Convert messages to dict format
1343
+ return [msg.to_dict() if hasattr(msg, "to_dict") else msg for msg in messages]
1344
+
1345
+ async def _store_conversation_session(self, session_key: str, conversation_session) -> None:
1346
+ """Store conversation session metadata."""
1347
+ session_data = {
1348
+ "session_id": conversation_session.session_id,
1349
+ "participants": [
1350
+ {
1351
+ "participant_id": p.participant_id,
1352
+ "participant_type": p.participant_type,
1353
+ "participant_role": p.participant_role,
1354
+ "metadata": p.metadata,
1355
+ }
1356
+ for p in conversation_session.participants
1357
+ ],
1358
+ "session_type": conversation_session.session_type,
1359
+ "created_at": conversation_session.created_at.isoformat(),
1360
+ "last_activity": conversation_session.last_activity.isoformat(),
1361
+ "metadata": conversation_session.metadata,
1362
+ }
1363
+
1364
+ if self.redis_client:
1365
+ try:
1366
+ await self.redis_client.hset( # type: ignore[misc]
1367
+ "conversation_sessions",
1368
+ session_key,
1369
+ json.dumps(session_data, cls=DateTimeEncoder),
1370
+ )
1371
+ await self.redis_client.expire("conversation_sessions", self.session_ttl) # type: ignore[misc]
1372
+ return
1373
+ except Exception as e:
1374
+ logger.error(f"Failed to store conversation session to Redis: {e}")
1375
+
1376
+ # Fallback to memory (extend memory storage)
1377
+ if not hasattr(self, "_memory_conversation_sessions"):
1378
+ self._memory_conversation_sessions = {}
1379
+ self._memory_conversation_sessions[session_key] = session_data
1380
+
1381
+ async def _update_conversation_session_activity(self, session_key: str) -> None:
1382
+ """Update last activity timestamp for a conversation session."""
1383
+ if self.redis_client:
1384
+ try:
1385
+ session_data = await self.redis_client.hget("conversation_sessions", session_key) # type: ignore[misc]
1386
+ if session_data:
1387
+ session_dict = json.loads(session_data)
1388
+ session_dict["last_activity"] = datetime.utcnow().isoformat()
1389
+ await self.redis_client.hset( # type: ignore[misc]
1390
+ "conversation_sessions",
1391
+ session_key,
1392
+ json.dumps(session_dict, cls=DateTimeEncoder),
1393
+ )
1394
+ return
1395
+ except Exception as e:
1396
+ logger.error(f"Failed to update conversation session activity in Redis: {e}")
1397
+
1398
+ # Fallback to memory
1399
+ if hasattr(self, "_memory_conversation_sessions") and session_key in self._memory_conversation_sessions:
1400
+ self._memory_conversation_sessions[session_key]["last_activity"] = datetime.utcnow().isoformat()
1401
+
1402
+ # ==================== Compression Methods (Phase 6) ====================
1403
+
1404
+ async def compress_conversation(
1405
+ self,
1406
+ session_id: str,
1407
+ strategy: Optional[str] = None,
1408
+ config_override: Optional[CompressionConfig] = None,
1409
+ ) -> Dict[str, Any]:
1410
+ """
1411
+ Compress conversation history using specified strategy.
1412
+
1413
+ Args:
1414
+ session_id: Session ID to compress
1415
+ strategy: Compression strategy (overrides config if provided)
1416
+ config_override: Override compression config for this operation
1417
+
1418
+ Returns:
1419
+ Dictionary with compression results:
1420
+ {
1421
+ "success": bool,
1422
+ "strategy": str,
1423
+ "original_count": int,
1424
+ "compressed_count": int,
1425
+ "compression_ratio": float,
1426
+ "tokens_saved": int (if applicable),
1427
+ "time_taken": float
1428
+ }
1429
+
1430
+ Example:
1431
+ result = await engine.compress_conversation(
1432
+ session_id="session-123",
1433
+ strategy="summarize"
1434
+ )
1435
+ print(f"Compressed from {result['original_count']} to {result['compressed_count']} messages")
1436
+ """
1437
+ import time
1438
+
1439
+ start_time = time.time()
1440
+
1441
+ # Use config override or default
1442
+ config = config_override or self.compression_config
1443
+ selected_strategy = strategy or config.strategy
1444
+
1445
+ logger.info(f"Compressing conversation {session_id} using strategy: {selected_strategy}")
1446
+
1447
+ try:
1448
+ # Get current conversation
1449
+ messages_dict = await self.get_conversation_history(session_id)
1450
+ # Convert dict list to ConversationMessage list
1451
+ messages = [ConversationMessage.from_dict(msg) for msg in messages_dict]
1452
+ original_count = len(messages)
1453
+
1454
+ if original_count == 0:
1455
+ return {
1456
+ "success": False,
1457
+ "error": "No messages to compress",
1458
+ "original_count": 0,
1459
+ "compressed_count": 0,
1460
+ }
1461
+
1462
+ # Select compression strategy
1463
+ if selected_strategy == "truncate":
1464
+ compressed_messages = await self._compress_with_truncation(messages, config)
1465
+ elif selected_strategy == "summarize":
1466
+ compressed_messages = await self._compress_with_summarization(messages, config)
1467
+ elif selected_strategy == "semantic":
1468
+ compressed_messages = await self._compress_with_semantic_dedup(messages, config)
1469
+ elif selected_strategy == "hybrid":
1470
+ compressed_messages = await self._compress_with_hybrid(messages, config)
1471
+ else:
1472
+ raise ValueError(f"Unknown compression strategy: {selected_strategy}")
1473
+
1474
+ compressed_count = len(compressed_messages)
1475
+ compression_ratio = 1.0 - (compressed_count / original_count) if original_count > 0 else 0.0
1476
+
1477
+ # Replace conversation history
1478
+ await self._replace_conversation_history(session_id, compressed_messages)
1479
+
1480
+ time_taken = time.time() - start_time
1481
+
1482
+ result = {
1483
+ "success": True,
1484
+ "strategy": selected_strategy,
1485
+ "original_count": original_count,
1486
+ "compressed_count": compressed_count,
1487
+ "compression_ratio": compression_ratio,
1488
+ "time_taken": time_taken,
1489
+ }
1490
+
1491
+ logger.info(f"Compression complete: {original_count} -> {compressed_count} messages " f"({compression_ratio:.1%} reduction) in {time_taken:.2f}s")
1492
+
1493
+ return result
1494
+
1495
+ except Exception as e:
1496
+ logger.error(f"Compression failed for session {session_id}: {e}")
1497
+ return {
1498
+ "success": False,
1499
+ "error": str(e),
1500
+ "strategy": selected_strategy,
1501
+ "time_taken": time.time() - start_time,
1502
+ }
1503
+
1504
+ async def _compress_with_truncation(self, messages: List[ConversationMessage], config: CompressionConfig) -> List[ConversationMessage]:
1505
+ """
1506
+ Compress by truncating old messages (fast, no LLM required).
1507
+
1508
+ Keeps the most recent N messages based on config.keep_recent.
1509
+
1510
+ Args:
1511
+ messages: List of conversation messages
1512
+ config: Compression configuration
1513
+
1514
+ Returns:
1515
+ Truncated list of messages
1516
+ """
1517
+ if len(messages) <= config.keep_recent:
1518
+ return messages
1519
+
1520
+ # Keep most recent messages
1521
+ truncated = messages[-config.keep_recent :]
1522
+
1523
+ logger.debug(f"Truncation: kept {len(truncated)} most recent messages " f"(removed {len(messages) - len(truncated)})")
1524
+
1525
+ return truncated
1526
+
1527
+ async def _compress_with_summarization(self, messages: List[ConversationMessage], config: CompressionConfig) -> List[ConversationMessage]:
1528
+ """
1529
+ Compress using LLM-based summarization.
1530
+
1531
+ Creates a summary of older messages and keeps recent messages intact.
1532
+
1533
+ Args:
1534
+ messages: List of conversation messages
1535
+ config: Compression configuration
1536
+
1537
+ Returns:
1538
+ List with summary message + recent messages
1539
+
1540
+ Raises:
1541
+ ValueError: If no LLM client configured
1542
+ """
1543
+ if not self.llm_client:
1544
+ raise ValueError("LLM client required for summarization compression. " "Provide llm_client parameter to ContextEngine.")
1545
+
1546
+ if len(messages) <= config.keep_recent:
1547
+ return messages
1548
+
1549
+ # Split into messages to summarize and messages to keep
1550
+ messages_to_summarize = messages[: -config.keep_recent]
1551
+ messages_to_keep = messages[-config.keep_recent :]
1552
+
1553
+ # Build summary prompt
1554
+ summary_prompt = self._build_summary_prompt(messages_to_summarize, config)
1555
+
1556
+ # Generate summary using LLM
1557
+ from aiecs.llm.clients.base_client import LLMMessage
1558
+
1559
+ llm_messages = [LLMMessage(role="user", content=summary_prompt)]
1560
+
1561
+ response = await self.llm_client.generate_text(messages=llm_messages, max_tokens=config.summary_max_tokens)
1562
+
1563
+ summary_text = response.content
1564
+
1565
+ # Create summary message
1566
+ summary_message = ConversationMessage(
1567
+ role="system",
1568
+ content=f"[Summary of {len(messages_to_summarize)} previous messages]\n\n{summary_text}",
1569
+ timestamp=datetime.utcnow(),
1570
+ metadata={"type": "summary", "summarized_count": len(messages_to_summarize)},
1571
+ )
1572
+
1573
+ # Combine summary + recent messages
1574
+ if config.include_summary_in_history:
1575
+ compressed = [summary_message] + messages_to_keep
1576
+ else:
1577
+ compressed = messages_to_keep
1578
+
1579
+ logger.debug(f"Summarization: {len(messages_to_summarize)} messages -> 1 summary, " f"kept {len(messages_to_keep)} recent messages")
1580
+
1581
+ return compressed
1582
+
1583
+ def _build_summary_prompt(self, messages: List[ConversationMessage], config: CompressionConfig) -> str:
1584
+ """
1585
+ Build prompt for summarization.
1586
+
1587
+ Args:
1588
+ messages: Messages to summarize
1589
+ config: Compression configuration
1590
+
1591
+ Returns:
1592
+ Prompt string for LLM
1593
+ """
1594
+ # Use custom template if provided
1595
+ if config.summary_prompt_template:
1596
+ # Format template with messages
1597
+ messages_text = "\n\n".join([f"{msg.role}: {msg.content}" for msg in messages])
1598
+ return config.summary_prompt_template.format(messages=messages_text)
1599
+
1600
+ # Default template
1601
+ messages_text = "\n\n".join([f"{msg.role}: {msg.content}" for msg in messages])
1602
+
1603
+ prompt = f"""Please provide a concise summary of the following conversation.
1604
+ Focus on key points, decisions, and important information.
1605
+ Keep the summary under {config.summary_max_tokens} tokens.
1606
+
1607
+ Conversation:
1608
+ {messages_text}
1609
+
1610
+ Summary:"""
1611
+
1612
+ return prompt
1613
+
1614
+ async def _compress_with_semantic_dedup(self, messages: List[ConversationMessage], config: CompressionConfig) -> List[ConversationMessage]:
1615
+ """
1616
+ Compress using semantic deduplication (embedding-based).
1617
+
1618
+ Removes messages that are semantically similar to keep diverse content.
1619
+
1620
+ Args:
1621
+ messages: List of conversation messages
1622
+ config: Compression configuration
1623
+
1624
+ Returns:
1625
+ List of semantically diverse messages
1626
+
1627
+ Raises:
1628
+ ValueError: If no LLM client configured
1629
+ """
1630
+ if not self.llm_client:
1631
+ raise ValueError("LLM client required for semantic deduplication. " "Provide llm_client parameter to ContextEngine.")
1632
+
1633
+ if len(messages) <= config.keep_recent:
1634
+ return messages
1635
+
1636
+ # Get embeddings for all messages
1637
+ texts = [msg.content for msg in messages]
1638
+
1639
+ try:
1640
+ embeddings = await self.llm_client.get_embeddings(texts=texts, model=config.embedding_model)
1641
+ except NotImplementedError:
1642
+ logger.warning("LLM client does not support embeddings. Falling back to truncation.")
1643
+ return await self._compress_with_truncation(messages, config)
1644
+
1645
+ # Find diverse messages using embeddings
1646
+ diverse_indices = self._find_diverse_messages(embeddings, config.similarity_threshold, config.keep_recent)
1647
+
1648
+ # Keep messages at diverse indices
1649
+ compressed = [messages[i] for i in sorted(diverse_indices)]
1650
+
1651
+ logger.debug(f"Semantic dedup: kept {len(compressed)} diverse messages " f"(removed {len(messages) - len(compressed)} similar messages)")
1652
+
1653
+ return compressed
1654
+
1655
+ def _find_diverse_messages(self, embeddings: List[List[float]], similarity_threshold: float, target_count: int) -> List[int]:
1656
+ """
1657
+ Find diverse messages using embeddings.
1658
+
1659
+ Uses greedy selection to find messages that are semantically diverse.
1660
+
1661
+ Args:
1662
+ embeddings: List of embedding vectors
1663
+ similarity_threshold: Similarity threshold for deduplication
1664
+ target_count: Target number of messages to keep
1665
+
1666
+ Returns:
1667
+ List of indices of diverse messages
1668
+ """
1669
+ import numpy as np
1670
+
1671
+ if len(embeddings) <= target_count:
1672
+ return list(range(len(embeddings)))
1673
+
1674
+ # Convert to numpy array
1675
+ emb_array = np.array(embeddings)
1676
+
1677
+ # Normalize embeddings for cosine similarity
1678
+ norms = np.linalg.norm(emb_array, axis=1, keepdims=True)
1679
+ emb_normalized = emb_array / (norms + 1e-8)
1680
+
1681
+ # Greedy selection: always keep most recent messages
1682
+ selected_indices = list(range(len(embeddings) - target_count, len(embeddings)))
1683
+
1684
+ # For older messages, select diverse ones
1685
+ remaining_indices = list(range(len(embeddings) - target_count))
1686
+
1687
+ while remaining_indices and len(selected_indices) < target_count:
1688
+ # Find message most different from selected ones
1689
+ max_min_distance = -1
1690
+ best_idx = None
1691
+
1692
+ for idx in remaining_indices:
1693
+ # Calculate similarity to all selected messages
1694
+ similarities = np.dot(emb_normalized[idx], emb_normalized[selected_indices].T)
1695
+ min_similarity = np.min(similarities) if len(similarities) > 0 else 0
1696
+
1697
+ # We want maximum minimum distance (most diverse)
1698
+ if min_similarity > max_min_distance:
1699
+ max_min_distance = min_similarity
1700
+ best_idx = idx
1701
+
1702
+ if best_idx is not None and max_min_distance < similarity_threshold:
1703
+ selected_indices.append(best_idx)
1704
+ remaining_indices.remove(best_idx)
1705
+ else:
1706
+ break
1707
+
1708
+ return selected_indices
1709
+
1710
+ async def _replace_conversation_history(self, session_id: str, messages: List[ConversationMessage]) -> None:
1711
+ """
1712
+ Replace conversation history with compressed messages.
1713
+
1714
+ Args:
1715
+ session_id: Session ID
1716
+ messages: New list of messages
1717
+ """
1718
+ if self.redis_client:
1719
+ try:
1720
+ # Clear existing messages
1721
+ await self.redis_client.delete(f"conversation:{session_id}")
1722
+
1723
+ # Store new messages
1724
+ for msg in messages:
1725
+ await self.redis_client.rpush( # type: ignore[misc]
1726
+ f"conversation:{session_id}",
1727
+ json.dumps(msg.to_dict(), cls=DateTimeEncoder),
1728
+ )
1729
+
1730
+ # Set TTL
1731
+ await self.redis_client.expire(f"conversation:{session_id}", self.session_ttl)
1732
+
1733
+ logger.debug(f"Replaced conversation history for {session_id} with {len(messages)} messages")
1734
+ return
1735
+ except Exception as e:
1736
+ logger.error(f"Failed to replace conversation history in Redis: {e}")
1737
+
1738
+ # Fallback to memory
1739
+ self._memory_conversations[session_id] = messages
1740
+ logger.debug(f"Replaced conversation history (memory) for {session_id} with {len(messages)} messages")
1741
+
1742
+ async def _compress_with_hybrid(self, messages: List[ConversationMessage], config: CompressionConfig) -> List[ConversationMessage]:
1743
+ """
1744
+ Compress using hybrid strategy (combination of multiple strategies).
1745
+
1746
+ Applies multiple compression strategies in sequence based on config.hybrid_strategies.
1747
+
1748
+ Args:
1749
+ messages: List of conversation messages
1750
+ config: Compression configuration
1751
+
1752
+ Returns:
1753
+ Compressed list of messages
1754
+
1755
+ Example:
1756
+ # Default hybrid: truncate then summarize
1757
+ config = CompressionConfig(
1758
+ strategy="hybrid",
1759
+ hybrid_strategies=["truncate", "summarize"]
1760
+ )
1761
+ """
1762
+ compressed = messages
1763
+
1764
+ # Type narrowing: ensure hybrid_strategies is a list
1765
+ if config.hybrid_strategies is None:
1766
+ config.hybrid_strategies = ["truncate", "summarize"]
1767
+
1768
+ for strategy in config.hybrid_strategies:
1769
+ if strategy == "truncate":
1770
+ compressed = await self._compress_with_truncation(compressed, config)
1771
+ elif strategy == "summarize":
1772
+ compressed = await self._compress_with_summarization(compressed, config)
1773
+ elif strategy == "semantic":
1774
+ compressed = await self._compress_with_semantic_dedup(compressed, config)
1775
+ else:
1776
+ logger.warning(f"Unknown hybrid strategy: {strategy}, skipping")
1777
+
1778
+ logger.debug(f"Hybrid compression: {len(messages)} -> {len(compressed)} messages " f"using strategies: {', '.join(config.hybrid_strategies)}")
1779
+
1780
+ return compressed
1781
+
1782
+ async def auto_compress_on_limit(self, session_id: str) -> Optional[Dict[str, Any]]:
1783
+ """
1784
+ Automatically compress conversation if it exceeds threshold.
1785
+
1786
+ Checks if conversation exceeds auto_compress_threshold and compresses
1787
+ to auto_compress_target if needed.
1788
+
1789
+ Args:
1790
+ session_id: Session ID to check
1791
+
1792
+ Returns:
1793
+ Compression result dict if compression was triggered, None otherwise
1794
+
1795
+ Example:
1796
+ # Configure auto-compression
1797
+ config = CompressionConfig(
1798
+ auto_compress_enabled=True,
1799
+ auto_compress_threshold=100,
1800
+ auto_compress_target=50
1801
+ )
1802
+ engine = ContextEngine(compression_config=config)
1803
+
1804
+ # Check and auto-compress if needed
1805
+ result = await engine.auto_compress_on_limit(session_id)
1806
+ if result:
1807
+ print(f"Auto-compressed: {result['original_count']} -> {result['compressed_count']}")
1808
+ """
1809
+ if not self.compression_config.auto_compress_enabled:
1810
+ return None
1811
+
1812
+ # Get current message count
1813
+ messages = await self.get_conversation_history(session_id)
1814
+ message_count = len(messages)
1815
+
1816
+ # Check if threshold exceeded
1817
+ if message_count <= self.compression_config.auto_compress_threshold:
1818
+ return None
1819
+
1820
+ logger.info(f"Auto-compression triggered for {session_id}: " f"{message_count} messages exceeds threshold of " f"{self.compression_config.auto_compress_threshold}")
1821
+
1822
+ # Compress conversation
1823
+ result = await self.compress_conversation(session_id)
1824
+
1825
+ if result.get("success"):
1826
+ logger.info(f"Auto-compression complete for {session_id}: " f"{result['original_count']} -> {result['compressed_count']} messages")
1827
+
1828
+ return result
1829
+
1830
+ async def get_compressed_context(
1831
+ self,
1832
+ session_id: str,
1833
+ format: str = "messages",
1834
+ compress_first: bool = False,
1835
+ ) -> Any:
1836
+ """
1837
+ Get conversation context in compressed format.
1838
+
1839
+ Args:
1840
+ session_id: Session ID
1841
+ format: Output format - "messages", "string", or "dict"
1842
+ compress_first: Whether to compress before returning
1843
+
1844
+ Returns:
1845
+ Conversation in requested format:
1846
+ - "messages": List[ConversationMessage]
1847
+ - "string": Formatted string
1848
+ - "dict": List[Dict[str, Any]]
1849
+
1850
+ Example:
1851
+ # Get as formatted string
1852
+ context = await engine.get_compressed_context(
1853
+ session_id="session-123",
1854
+ format="string"
1855
+ )
1856
+ print(context)
1857
+
1858
+ # Get as messages, compress first
1859
+ messages = await engine.get_compressed_context(
1860
+ session_id="session-456",
1861
+ format="messages",
1862
+ compress_first=True
1863
+ )
1864
+ """
1865
+ # Compress first if requested
1866
+ if compress_first:
1867
+ await self.compress_conversation(session_id)
1868
+
1869
+ # Get conversation history
1870
+ messages = await self.get_conversation_history(session_id)
1871
+
1872
+ # Return in requested format
1873
+ if format == "messages":
1874
+ return messages
1875
+
1876
+ elif format == "string":
1877
+ # Format as string
1878
+ lines = []
1879
+ for msg in messages:
1880
+ # messages is List[Dict[str, Any]] from get_conversation_history
1881
+ timestamp = msg.get("timestamp", "").strftime("%Y-%m-%d %H:%M:%S") if isinstance(msg.get("timestamp"), datetime) else str(msg.get("timestamp", ""))
1882
+ role = msg.get("role", "")
1883
+ content = msg.get("content", "")
1884
+ lines.append(f"[{timestamp}] {role}: {content}")
1885
+ return "\n\n".join(lines)
1886
+
1887
+ elif format == "dict":
1888
+ # Return as list of dicts (already dicts from get_conversation_history)
1889
+ return [self._sanitize_for_json(msg) for msg in messages]
1890
+
1891
+ else:
1892
+ raise ValueError(f"Invalid format '{format}'. Must be 'messages', 'string', or 'dict'")
1893
+
1894
+ def _sanitize_for_json(self, obj: Any) -> Any:
1895
+ """
1896
+ Sanitize object for JSON serialization.
1897
+
1898
+ Handles common non-serializable types like datetime, dataclasses, etc.
1899
+
1900
+ Args:
1901
+ obj: Object to sanitize
1902
+
1903
+ Returns:
1904
+ JSON-serializable version of object
1905
+
1906
+ Note:
1907
+ This is similar to _sanitize_dataclasses but more general purpose.
1908
+ """
1909
+ # Use existing sanitization logic
1910
+ return self._sanitize_dataclasses(obj)