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,1373 @@
1
+ """
2
+ SQLite Graph Storage Backend
3
+
4
+ Provides file-based persistent graph storage using SQLite.
5
+
6
+ Multi-tenancy Support:
7
+ - SHARED_SCHEMA mode: Single database with tenant_id column filtering
8
+ - SEPARATE_SCHEMA mode: Table prefixes per tenant (tenant_xxx_entities, tenant_xxx_relations)
9
+ - Global namespace for tenant_id=NULL (backward compatible)
10
+ """
11
+
12
+ import json
13
+ import aiosqlite
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+ from pathlib import Path as PathLibPath
16
+ from contextlib import asynccontextmanager
17
+
18
+ from aiecs.domain.knowledge_graph.models.entity import Entity
19
+ from aiecs.domain.knowledge_graph.models.relation import Relation
20
+ from aiecs.domain.knowledge_graph.models.path import Path
21
+ from aiecs.infrastructure.graph_storage.base import GraphStore
22
+ from aiecs.infrastructure.graph_storage.tenant import (
23
+ TenantContext,
24
+ TenantIsolationMode,
25
+ CrossTenantRelationError,
26
+ )
27
+
28
+
29
+ # SQL Schema for SQLite graph storage with multi-tenancy support
30
+ SCHEMA_SQL = """
31
+ -- Entities table with tenant_id for multi-tenancy
32
+ CREATE TABLE IF NOT EXISTS entities (
33
+ id TEXT NOT NULL,
34
+ tenant_id TEXT, -- NULL for global namespace
35
+ entity_type TEXT NOT NULL,
36
+ properties TEXT NOT NULL, -- JSON
37
+ embedding BLOB, -- Vector embedding (serialized)
38
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
39
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
40
+ PRIMARY KEY (id, tenant_id)
41
+ );
42
+
43
+ -- Relations table with tenant_id for multi-tenancy
44
+ CREATE TABLE IF NOT EXISTS relations (
45
+ id TEXT NOT NULL,
46
+ tenant_id TEXT, -- NULL for global namespace
47
+ relation_type TEXT NOT NULL,
48
+ source_id TEXT NOT NULL,
49
+ target_id TEXT NOT NULL,
50
+ properties TEXT NOT NULL, -- JSON
51
+ weight REAL DEFAULT 1.0,
52
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
53
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
54
+ PRIMARY KEY (id, tenant_id)
55
+ );
56
+
57
+ -- Indexes for performance
58
+ CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(entity_type);
59
+ CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id);
60
+ CREATE INDEX IF NOT EXISTS idx_entities_tenant_type ON entities(tenant_id, entity_type);
61
+ CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type);
62
+ CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id);
63
+ CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_id);
64
+ CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_id);
65
+ CREATE INDEX IF NOT EXISTS idx_relations_tenant_source ON relations(tenant_id, source_id);
66
+ CREATE INDEX IF NOT EXISTS idx_relations_tenant_target ON relations(tenant_id, target_id);
67
+ """
68
+
69
+ # Migration SQL for adding tenant_id to existing databases
70
+ MIGRATION_ADD_TENANT_ID = """
71
+ -- Add tenant_id column to entities if not exists
72
+ ALTER TABLE entities ADD COLUMN tenant_id TEXT;
73
+
74
+ -- Add tenant_id column to relations if not exists
75
+ ALTER TABLE relations ADD COLUMN tenant_id TEXT;
76
+
77
+ -- Create tenant indexes
78
+ CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id);
79
+ CREATE INDEX IF NOT EXISTS idx_entities_tenant_type ON entities(tenant_id, entity_type);
80
+ CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id);
81
+ CREATE INDEX IF NOT EXISTS idx_relations_tenant_source ON relations(tenant_id, source_id);
82
+ CREATE INDEX IF NOT EXISTS idx_relations_tenant_target ON relations(tenant_id, target_id);
83
+ """
84
+
85
+
86
+ class SQLiteGraphStore(GraphStore):
87
+ """
88
+ SQLite-based graph storage implementation
89
+
90
+ Provides persistent file-based graph storage with:
91
+ - ACID transactions
92
+ - SQL-optimized queries
93
+ - Optional recursive CTEs for traversal
94
+ - Connection pooling
95
+
96
+ Features:
97
+ - File-based persistence (single .db file)
98
+ - Automatic schema initialization
99
+ - Efficient SQL queries for graph operations
100
+ - Optional Tier 2 optimizations
101
+
102
+ Multi-Tenancy Support:
103
+ - SHARED_SCHEMA mode: Single database with tenant_id column filtering
104
+ - SEPARATE_SCHEMA mode: Table prefixes per tenant
105
+ - Global namespace for tenant_id=NULL (backward compatible)
106
+
107
+ Example:
108
+ ```python
109
+ store = SQLiteGraphStore("knowledge_graph.db")
110
+ await store.initialize()
111
+
112
+ # Single-tenant usage (backward compatible)
113
+ entity = Entity(id="e1", entity_type="Person", properties={"name": "Alice"})
114
+ await store.add_entity(entity)
115
+
116
+ # Multi-tenant usage
117
+ from aiecs.infrastructure.graph_storage.tenant import TenantContext
118
+ context = TenantContext(tenant_id="acme-corp")
119
+ await store.add_entity(entity, context=context)
120
+
121
+ await store.close()
122
+ ```
123
+ """
124
+
125
+ def __init__(self, db_path: str = ":memory:", isolation_mode: TenantIsolationMode = TenantIsolationMode.SHARED_SCHEMA, **kwargs):
126
+ """
127
+ Initialize SQLite graph store
128
+
129
+ Args:
130
+ db_path: Path to SQLite database file (":memory:" for in-memory)
131
+ isolation_mode: Tenant isolation mode (SHARED_SCHEMA or SEPARATE_SCHEMA)
132
+ **kwargs: Additional SQLite connection parameters
133
+ """
134
+ super().__init__()
135
+ self.db_path = db_path
136
+ self.isolation_mode = isolation_mode
137
+ self.conn_kwargs = kwargs
138
+ self.conn: Optional[aiosqlite.Connection] = None
139
+ self._is_initialized = False
140
+ self._in_transaction = False
141
+ self._initialized_tenant_tables: set = set() # Track created tenant tables for SEPARATE_SCHEMA
142
+
143
+ async def initialize(self):
144
+ """Initialize SQLite database and create schema"""
145
+ # Create directory if needed
146
+ if self.db_path != ":memory:":
147
+ PathLibPath(self.db_path).parent.mkdir(parents=True, exist_ok=True)
148
+
149
+ # Connect to database
150
+ self.conn = await aiosqlite.connect(self.db_path, **self.conn_kwargs)
151
+
152
+ # Enable foreign keys
153
+ if self.conn is None:
154
+ raise RuntimeError("Failed to initialize database connection")
155
+ await self.conn.execute("PRAGMA foreign_keys = ON")
156
+
157
+ # Create schema (for SHARED_SCHEMA mode or base tables)
158
+ await self.conn.executescript(SCHEMA_SQL)
159
+ await self.conn.commit()
160
+
161
+ self._is_initialized = True
162
+ self._initialized_tenant_tables = set()
163
+
164
+ async def close(self):
165
+ """Close database connection"""
166
+ if self.conn:
167
+ await self.conn.close()
168
+ self.conn = None
169
+ self._is_initialized = False
170
+ self._initialized_tenant_tables = set()
171
+
172
+ # =========================================================================
173
+ # Multi-Tenancy Helpers
174
+ # =========================================================================
175
+
176
+ def _get_tenant_id(self, context: Optional[TenantContext]) -> Optional[str]:
177
+ """Extract tenant_id from context, returns None for global namespace."""
178
+ return context.tenant_id if context else None
179
+
180
+ def _get_table_name(self, base_table: str, tenant_id: Optional[str]) -> str:
181
+ """
182
+ Get table name based on isolation mode.
183
+
184
+ For SHARED_SCHEMA: Returns base table name (filtering done via WHERE clause)
185
+ For SEPARATE_SCHEMA: Returns prefixed table name (tenant_xxx_entities)
186
+ """
187
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
188
+ # Sanitize tenant_id for use in table name (replace - with _)
189
+ safe_tenant = tenant_id.replace("-", "_")
190
+ return f"tenant_{safe_tenant}_{base_table}"
191
+ return base_table
192
+
193
+ async def _ensure_tenant_tables(self, tenant_id: str) -> None:
194
+ """
195
+ Ensure tenant-specific tables exist for SEPARATE_SCHEMA mode.
196
+
197
+ Creates tables like tenant_xxx_entities and tenant_xxx_relations.
198
+ """
199
+ if self.isolation_mode != TenantIsolationMode.SEPARATE_SCHEMA:
200
+ return
201
+
202
+ if tenant_id in self._initialized_tenant_tables:
203
+ return
204
+
205
+ if self.conn is None:
206
+ raise RuntimeError("Database connection not initialized")
207
+
208
+ safe_tenant = tenant_id.replace("-", "_")
209
+ entities_table = f"tenant_{safe_tenant}_entities"
210
+ relations_table = f"tenant_{safe_tenant}_relations"
211
+
212
+ # Create tenant-specific tables
213
+ tenant_schema = f"""
214
+ CREATE TABLE IF NOT EXISTS {entities_table} (
215
+ id TEXT PRIMARY KEY,
216
+ entity_type TEXT NOT NULL,
217
+ properties TEXT NOT NULL,
218
+ embedding BLOB,
219
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
220
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
221
+ );
222
+
223
+ CREATE TABLE IF NOT EXISTS {relations_table} (
224
+ id TEXT PRIMARY KEY,
225
+ relation_type TEXT NOT NULL,
226
+ source_id TEXT NOT NULL,
227
+ target_id TEXT NOT NULL,
228
+ properties TEXT NOT NULL,
229
+ weight REAL DEFAULT 1.0,
230
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
231
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
232
+ );
233
+
234
+ CREATE INDEX IF NOT EXISTS idx_{safe_tenant}_entities_type ON {entities_table}(entity_type);
235
+ CREATE INDEX IF NOT EXISTS idx_{safe_tenant}_relations_type ON {relations_table}(relation_type);
236
+ CREATE INDEX IF NOT EXISTS idx_{safe_tenant}_relations_source ON {relations_table}(source_id);
237
+ CREATE INDEX IF NOT EXISTS idx_{safe_tenant}_relations_target ON {relations_table}(target_id);
238
+ """
239
+
240
+ await self.conn.executescript(tenant_schema)
241
+ await self.conn.commit()
242
+ self._initialized_tenant_tables.add(tenant_id)
243
+
244
+ def _build_tenant_filter(self, tenant_id: Optional[str], table_alias: str = "") -> Tuple[str, List]:
245
+ """
246
+ Build SQL WHERE clause for tenant filtering in SHARED_SCHEMA mode.
247
+
248
+ Returns:
249
+ Tuple of (WHERE clause fragment, parameters list)
250
+ """
251
+ prefix = f"{table_alias}." if table_alias else ""
252
+
253
+ if tenant_id is None:
254
+ return f"{prefix}tenant_id IS NULL", []
255
+ else:
256
+ return f"{prefix}tenant_id = ?", [tenant_id]
257
+
258
+ @asynccontextmanager
259
+ async def transaction(self):
260
+ """
261
+ Transaction context manager for atomic operations
262
+
263
+ Usage:
264
+ ```python
265
+ async with store.transaction():
266
+ await store.add_entity(entity1)
267
+ await store.add_entity(entity2)
268
+ # Both entities added atomically
269
+ ```
270
+
271
+ Note: SQLite uses connection-level transactions. Within a transaction,
272
+ commits are deferred until the context exits successfully.
273
+ """
274
+ if not self._is_initialized:
275
+ raise RuntimeError("GraphStore not initialized")
276
+
277
+ # Track transaction state to prevent auto-commits in operations
278
+ self._in_transaction = True
279
+ try:
280
+ # Begin transaction
281
+ await self.conn.execute("BEGIN")
282
+ yield
283
+ # Commit on success
284
+ await self.conn.commit()
285
+ except Exception:
286
+ # Rollback on error
287
+ await self.conn.rollback()
288
+ raise
289
+ finally:
290
+ self._in_transaction = False
291
+
292
+ # =========================================================================
293
+ # Tier 1: Basic Interface (SQL-optimized implementations)
294
+ # =========================================================================
295
+
296
+ async def add_entity(self, entity: Entity, context: Optional[TenantContext] = None) -> None:
297
+ """
298
+ Add entity to SQLite database
299
+
300
+ Args:
301
+ entity: Entity to add
302
+ context: Optional tenant context for multi-tenant isolation
303
+ """
304
+ if not self._is_initialized:
305
+ raise RuntimeError("GraphStore not initialized")
306
+ if self.conn is None:
307
+ raise RuntimeError("Database connection not initialized")
308
+
309
+ tenant_id = self._get_tenant_id(context)
310
+
311
+ # Ensure tenant tables exist for SEPARATE_SCHEMA mode
312
+ if tenant_id and self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA:
313
+ await self._ensure_tenant_tables(tenant_id)
314
+
315
+ table_name = self._get_table_name("entities", tenant_id)
316
+
317
+ # Check if entity already exists (within tenant scope)
318
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
319
+ cursor = await self.conn.execute(f"SELECT id FROM {table_name} WHERE id = ?", (entity.id,))
320
+ else:
321
+ tenant_filter, params = self._build_tenant_filter(tenant_id)
322
+ cursor = await self.conn.execute(
323
+ f"SELECT id FROM {table_name} WHERE id = ? AND {tenant_filter}",
324
+ [entity.id] + params
325
+ )
326
+
327
+ existing = await cursor.fetchone()
328
+ if existing:
329
+ raise ValueError(f"Entity with ID '{entity.id}' already exists")
330
+
331
+ # Set tenant_id on entity if context provided
332
+ if tenant_id is not None and entity.tenant_id is None:
333
+ entity.tenant_id = tenant_id
334
+
335
+ # Serialize data
336
+ properties_json = json.dumps(entity.properties)
337
+ embedding_blob = self._serialize_embedding(entity.embedding) if entity.embedding else None
338
+
339
+ # Insert entity
340
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
341
+ await self.conn.execute(
342
+ f"""
343
+ INSERT INTO {table_name} (id, entity_type, properties, embedding)
344
+ VALUES (?, ?, ?, ?)
345
+ """,
346
+ (entity.id, entity.entity_type, properties_json, embedding_blob),
347
+ )
348
+ else:
349
+ await self.conn.execute(
350
+ f"""
351
+ INSERT INTO {table_name} (id, tenant_id, entity_type, properties, embedding)
352
+ VALUES (?, ?, ?, ?, ?)
353
+ """,
354
+ (entity.id, tenant_id, entity.entity_type, properties_json, embedding_blob),
355
+ )
356
+
357
+ if not self._in_transaction:
358
+ await self.conn.commit()
359
+
360
+ async def get_entity(self, entity_id: str, context: Optional[TenantContext] = None) -> Optional[Entity]:
361
+ """
362
+ Get entity from SQLite database
363
+
364
+ Args:
365
+ entity_id: Entity ID to retrieve
366
+ context: Optional tenant context for multi-tenant isolation
367
+ """
368
+ if not self._is_initialized:
369
+ raise RuntimeError("GraphStore not initialized")
370
+ if self.conn is None:
371
+ raise RuntimeError("Database connection not initialized")
372
+
373
+ tenant_id = self._get_tenant_id(context)
374
+ table_name = self._get_table_name("entities", tenant_id)
375
+
376
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
377
+ # SEPARATE_SCHEMA: No tenant_id column in tenant-specific tables
378
+ cursor = await self.conn.execute(
379
+ f"""
380
+ SELECT id, entity_type, properties, embedding
381
+ FROM {table_name}
382
+ WHERE id = ?
383
+ """,
384
+ (entity_id,),
385
+ )
386
+ row = await cursor.fetchone()
387
+ if not row:
388
+ return None
389
+ return self._row_to_entity(tuple(row), tenant_id=tenant_id)
390
+ else:
391
+ # SHARED_SCHEMA: Filter by tenant_id column
392
+ tenant_filter, params = self._build_tenant_filter(tenant_id)
393
+ cursor = await self.conn.execute(
394
+ f"""
395
+ SELECT id, tenant_id, entity_type, properties, embedding
396
+ FROM {table_name}
397
+ WHERE id = ? AND {tenant_filter}
398
+ """,
399
+ [entity_id] + params,
400
+ )
401
+ row = await cursor.fetchone()
402
+ if not row:
403
+ return None
404
+ return self._row_to_entity_with_tenant(tuple(row))
405
+
406
+ async def update_entity(self, entity: Entity, context: Optional[TenantContext] = None) -> Entity:
407
+ """
408
+ Update entity in SQLite database
409
+
410
+ Args:
411
+ entity: Entity to update
412
+ context: Optional tenant context for multi-tenant isolation
413
+ """
414
+ if not self._is_initialized:
415
+ raise RuntimeError("GraphStore not initialized")
416
+ if self.conn is None:
417
+ raise RuntimeError("Database connection not initialized")
418
+
419
+ tenant_id = self._get_tenant_id(context)
420
+ table_name = self._get_table_name("entities", tenant_id)
421
+
422
+ # Check if entity exists
423
+ existing = await self.get_entity(entity.id, context=context)
424
+ if not existing:
425
+ raise ValueError(f"Entity with ID '{entity.id}' does not exist")
426
+
427
+ # Serialize data
428
+ properties_json = json.dumps(entity.properties)
429
+ embedding_blob = self._serialize_embedding(entity.embedding) if entity.embedding else None
430
+
431
+ # Update entity
432
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
433
+ await self.conn.execute(
434
+ f"""
435
+ UPDATE {table_name}
436
+ SET entity_type = ?, properties = ?, embedding = ?, updated_at = CURRENT_TIMESTAMP
437
+ WHERE id = ?
438
+ """,
439
+ (entity.entity_type, properties_json, embedding_blob, entity.id),
440
+ )
441
+ else:
442
+ tenant_filter, params = self._build_tenant_filter(tenant_id)
443
+ await self.conn.execute(
444
+ f"""
445
+ UPDATE {table_name}
446
+ SET entity_type = ?, properties = ?, embedding = ?, updated_at = CURRENT_TIMESTAMP
447
+ WHERE id = ? AND {tenant_filter}
448
+ """,
449
+ [entity.entity_type, properties_json, embedding_blob, entity.id] + params,
450
+ )
451
+
452
+ if not self._in_transaction:
453
+ await self.conn.commit()
454
+
455
+ return entity
456
+
457
+ async def delete_entity(self, entity_id: str, context: Optional[TenantContext] = None):
458
+ """
459
+ Delete entity and its relations from SQLite database
460
+
461
+ Args:
462
+ entity_id: Entity ID to delete
463
+ context: Optional tenant context for multi-tenant isolation
464
+ """
465
+ if not self._is_initialized:
466
+ raise RuntimeError("GraphStore not initialized")
467
+ if self.conn is None:
468
+ raise RuntimeError("Database connection not initialized")
469
+
470
+ tenant_id = self._get_tenant_id(context)
471
+ entities_table = self._get_table_name("entities", tenant_id)
472
+ relations_table = self._get_table_name("relations", tenant_id)
473
+
474
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
475
+ # Delete relations first (no foreign key in SEPARATE_SCHEMA)
476
+ await self.conn.execute(
477
+ f"DELETE FROM {relations_table} WHERE source_id = ? OR target_id = ?",
478
+ (entity_id, entity_id)
479
+ )
480
+ await self.conn.execute(f"DELETE FROM {entities_table} WHERE id = ?", (entity_id,))
481
+ else:
482
+ tenant_filter, params = self._build_tenant_filter(tenant_id)
483
+ # Delete relations first
484
+ await self.conn.execute(
485
+ f"DELETE FROM {relations_table} WHERE (source_id = ? OR target_id = ?) AND {tenant_filter}",
486
+ [entity_id, entity_id] + params
487
+ )
488
+ await self.conn.execute(
489
+ f"DELETE FROM {entities_table} WHERE id = ? AND {tenant_filter}",
490
+ [entity_id] + params
491
+ )
492
+
493
+ if not self._in_transaction:
494
+ await self.conn.commit()
495
+
496
+ async def add_relation(self, relation: Relation, context: Optional[TenantContext] = None) -> None:
497
+ """
498
+ Add relation to SQLite database
499
+
500
+ Args:
501
+ relation: Relation to add
502
+ context: Optional tenant context for multi-tenant isolation
503
+
504
+ Raises:
505
+ CrossTenantRelationError: If source and target entities belong to different tenants
506
+ """
507
+ if not self._is_initialized:
508
+ raise RuntimeError("GraphStore not initialized")
509
+ if self.conn is None:
510
+ raise RuntimeError("Database connection not initialized")
511
+
512
+ tenant_id = self._get_tenant_id(context)
513
+
514
+ # Ensure tenant tables exist for SEPARATE_SCHEMA mode
515
+ if tenant_id and self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA:
516
+ await self._ensure_tenant_tables(tenant_id)
517
+
518
+ table_name = self._get_table_name("relations", tenant_id)
519
+
520
+ # Check if relation already exists (within tenant scope)
521
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
522
+ cursor = await self.conn.execute(f"SELECT id FROM {table_name} WHERE id = ?", (relation.id,))
523
+ else:
524
+ tenant_filter, params = self._build_tenant_filter(tenant_id)
525
+ cursor = await self.conn.execute(
526
+ f"SELECT id FROM {table_name} WHERE id = ? AND {tenant_filter}",
527
+ [relation.id] + params
528
+ )
529
+
530
+ existing = await cursor.fetchone()
531
+ if existing:
532
+ raise ValueError(f"Relation with ID '{relation.id}' already exists")
533
+
534
+ # Check if entities exist within tenant scope
535
+ source_entity = await self.get_entity(relation.source_id, context=context)
536
+ target_entity = await self.get_entity(relation.target_id, context=context)
537
+
538
+ if not source_entity:
539
+ raise ValueError(f"Source entity '{relation.source_id}' does not exist")
540
+ if not target_entity:
541
+ raise ValueError(f"Target entity '{relation.target_id}' does not exist")
542
+
543
+ # Enforce same-tenant constraint
544
+ if tenant_id is not None:
545
+ source_tenant = source_entity.tenant_id
546
+ target_tenant = target_entity.tenant_id
547
+ if source_tenant != target_tenant:
548
+ raise CrossTenantRelationError(source_tenant, target_tenant)
549
+
550
+ # Set tenant_id on relation if context provided
551
+ if tenant_id is not None and relation.tenant_id is None:
552
+ relation.tenant_id = tenant_id
553
+
554
+ # Serialize data
555
+ properties_json = json.dumps(relation.properties)
556
+
557
+ # Insert relation
558
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
559
+ await self.conn.execute(
560
+ f"""
561
+ INSERT INTO {table_name} (id, relation_type, source_id, target_id, properties, weight)
562
+ VALUES (?, ?, ?, ?, ?, ?)
563
+ """,
564
+ (
565
+ relation.id,
566
+ relation.relation_type,
567
+ relation.source_id,
568
+ relation.target_id,
569
+ properties_json,
570
+ relation.weight,
571
+ ),
572
+ )
573
+ else:
574
+ await self.conn.execute(
575
+ f"""
576
+ INSERT INTO {table_name} (id, tenant_id, relation_type, source_id, target_id, properties, weight)
577
+ VALUES (?, ?, ?, ?, ?, ?, ?)
578
+ """,
579
+ (
580
+ relation.id,
581
+ tenant_id,
582
+ relation.relation_type,
583
+ relation.source_id,
584
+ relation.target_id,
585
+ properties_json,
586
+ relation.weight,
587
+ ),
588
+ )
589
+
590
+ if not self._in_transaction:
591
+ await self.conn.commit()
592
+
593
+ async def get_relation(self, relation_id: str, context: Optional[TenantContext] = None) -> Optional[Relation]:
594
+ """
595
+ Get relation from SQLite database
596
+
597
+ Args:
598
+ relation_id: Relation ID to retrieve
599
+ context: Optional tenant context for multi-tenant isolation
600
+ """
601
+ if not self._is_initialized:
602
+ raise RuntimeError("GraphStore not initialized")
603
+ if self.conn is None:
604
+ raise RuntimeError("Database connection not initialized")
605
+
606
+ tenant_id = self._get_tenant_id(context)
607
+ table_name = self._get_table_name("relations", tenant_id)
608
+
609
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
610
+ cursor = await self.conn.execute(
611
+ f"""
612
+ SELECT id, relation_type, source_id, target_id, properties, weight
613
+ FROM {table_name}
614
+ WHERE id = ?
615
+ """,
616
+ (relation_id,),
617
+ )
618
+ row = await cursor.fetchone()
619
+ if not row:
620
+ return None
621
+ return self._row_to_relation(tuple(row), tenant_id=tenant_id)
622
+ else:
623
+ tenant_filter, params = self._build_tenant_filter(tenant_id)
624
+ cursor = await self.conn.execute(
625
+ f"""
626
+ SELECT id, tenant_id, relation_type, source_id, target_id, properties, weight
627
+ FROM {table_name}
628
+ WHERE id = ? AND {tenant_filter}
629
+ """,
630
+ [relation_id] + params,
631
+ )
632
+ row = await cursor.fetchone()
633
+ if not row:
634
+ return None
635
+ return self._row_to_relation_with_tenant(tuple(row))
636
+
637
+ async def update_relation(self, relation: Relation, context: Optional[TenantContext] = None) -> Relation:
638
+ """
639
+ Update relation in SQLite database
640
+
641
+ Args:
642
+ relation: Relation to update
643
+ context: Optional tenant context for multi-tenant isolation
644
+ """
645
+ if not self._is_initialized:
646
+ raise RuntimeError("GraphStore not initialized")
647
+ if self.conn is None:
648
+ raise RuntimeError("Database connection not initialized")
649
+
650
+ tenant_id = self._get_tenant_id(context)
651
+ table_name = self._get_table_name("relations", tenant_id)
652
+
653
+ # Check if relation exists
654
+ existing = await self.get_relation(relation.id, context=context)
655
+ if not existing:
656
+ raise ValueError(f"Relation with ID '{relation.id}' does not exist")
657
+
658
+ # Serialize data
659
+ properties_json = json.dumps(relation.properties)
660
+
661
+ # Update relation
662
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
663
+ await self.conn.execute(
664
+ f"""
665
+ UPDATE {table_name}
666
+ SET relation_type = ?, source_id = ?, target_id = ?, properties = ?,
667
+ weight = ?, updated_at = CURRENT_TIMESTAMP
668
+ WHERE id = ?
669
+ """,
670
+ (
671
+ relation.relation_type,
672
+ relation.source_id,
673
+ relation.target_id,
674
+ properties_json,
675
+ relation.weight,
676
+ relation.id,
677
+ ),
678
+ )
679
+ else:
680
+ tenant_filter, params = self._build_tenant_filter(tenant_id)
681
+ await self.conn.execute(
682
+ f"""
683
+ UPDATE {table_name}
684
+ SET relation_type = ?, source_id = ?, target_id = ?, properties = ?,
685
+ weight = ?, updated_at = CURRENT_TIMESTAMP
686
+ WHERE id = ? AND {tenant_filter}
687
+ """,
688
+ [
689
+ relation.relation_type,
690
+ relation.source_id,
691
+ relation.target_id,
692
+ properties_json,
693
+ relation.weight,
694
+ relation.id,
695
+ ] + params,
696
+ )
697
+
698
+ if not self._in_transaction:
699
+ await self.conn.commit()
700
+
701
+ return relation
702
+
703
+ async def delete_relation(self, relation_id: str, context: Optional[TenantContext] = None):
704
+ """
705
+ Delete relation from SQLite database
706
+
707
+ Args:
708
+ relation_id: Relation ID to delete
709
+ context: Optional tenant context for multi-tenant isolation
710
+ """
711
+ if not self._is_initialized:
712
+ raise RuntimeError("GraphStore not initialized")
713
+ if self.conn is None:
714
+ raise RuntimeError("Database connection not initialized")
715
+
716
+ tenant_id = self._get_tenant_id(context)
717
+ table_name = self._get_table_name("relations", tenant_id)
718
+
719
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
720
+ await self.conn.execute(f"DELETE FROM {table_name} WHERE id = ?", (relation_id,))
721
+ else:
722
+ tenant_filter, params = self._build_tenant_filter(tenant_id)
723
+ await self.conn.execute(
724
+ f"DELETE FROM {table_name} WHERE id = ? AND {tenant_filter}",
725
+ [relation_id] + params
726
+ )
727
+
728
+ if not self._in_transaction:
729
+ await self.conn.commit()
730
+
731
+ async def get_neighbors(
732
+ self,
733
+ entity_id: str,
734
+ relation_type: Optional[str] = None,
735
+ direction: str = "outgoing",
736
+ context: Optional[TenantContext] = None,
737
+ ) -> List[Entity]:
738
+ """
739
+ Get neighboring entities connected by relations
740
+
741
+ Implements the base GraphStore interface.
742
+
743
+ Args:
744
+ entity_id: ID of entity to get neighbors for
745
+ relation_type: Optional filter by relation type
746
+ direction: "outgoing", "incoming", or "both"
747
+ context: Optional tenant context for multi-tenant isolation
748
+
749
+ Returns:
750
+ List of neighboring entities
751
+ """
752
+ if not self._is_initialized:
753
+ raise RuntimeError("GraphStore not initialized")
754
+ if self.conn is None:
755
+ raise RuntimeError("Database connection not initialized")
756
+
757
+ tenant_id = self._get_tenant_id(context)
758
+ entities_table = self._get_table_name("entities", tenant_id)
759
+ relations_table = self._get_table_name("relations", tenant_id)
760
+
761
+ neighbors = []
762
+
763
+ # Build WHERE clause for relation type
764
+ type_filter = ""
765
+ if relation_type:
766
+ type_filter = "AND r.relation_type = ?"
767
+
768
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
769
+ # SEPARATE_SCHEMA: No tenant filtering needed (table is tenant-specific)
770
+ params = [entity_id]
771
+ if relation_type:
772
+ params.append(relation_type)
773
+
774
+ # Outgoing relations
775
+ if direction in ["outgoing", "both"]:
776
+ query = f"""
777
+ SELECT e.id, e.entity_type, e.properties, e.embedding
778
+ FROM {relations_table} r
779
+ JOIN {entities_table} e ON r.target_id = e.id
780
+ WHERE r.source_id = ? {type_filter}
781
+ """
782
+
783
+ cursor = await self.conn.execute(query, params)
784
+ rows = await cursor.fetchall()
785
+
786
+ for row in rows:
787
+ entity = self._row_to_entity(tuple(row), tenant_id=tenant_id)
788
+ neighbors.append(entity)
789
+
790
+ # Incoming relations
791
+ if direction in ["incoming", "both"]:
792
+ params_incoming = [entity_id]
793
+ if relation_type:
794
+ params_incoming.append(relation_type)
795
+
796
+ query = f"""
797
+ SELECT e.id, e.entity_type, e.properties, e.embedding
798
+ FROM {relations_table} r
799
+ JOIN {entities_table} e ON r.source_id = e.id
800
+ WHERE r.target_id = ? {type_filter}
801
+ """
802
+
803
+ cursor = await self.conn.execute(query, params_incoming)
804
+ rows = await cursor.fetchall()
805
+
806
+ for row in rows:
807
+ entity = self._row_to_entity(tuple(row), tenant_id=tenant_id)
808
+ neighbors.append(entity)
809
+ else:
810
+ # SHARED_SCHEMA: Filter by tenant_id
811
+ tenant_filter_r, tenant_params = self._build_tenant_filter(tenant_id, "r")
812
+ tenant_filter_e, tenant_params_e = self._build_tenant_filter(tenant_id, "e")
813
+
814
+ # Outgoing relations
815
+ if direction in ["outgoing", "both"]:
816
+ # Parameter order must match query: JOIN condition (e.tenant), WHERE source_id, [type], r.tenant
817
+ params = tenant_params_e + [entity_id]
818
+ if relation_type:
819
+ params.append(relation_type)
820
+ params.extend(tenant_params)
821
+
822
+ query = f"""
823
+ SELECT e.id, e.tenant_id, e.entity_type, e.properties, e.embedding
824
+ FROM {relations_table} r
825
+ JOIN {entities_table} e ON r.target_id = e.id AND {tenant_filter_e}
826
+ WHERE r.source_id = ? {type_filter} AND {tenant_filter_r}
827
+ """
828
+
829
+ cursor = await self.conn.execute(query, params)
830
+ rows = await cursor.fetchall()
831
+
832
+ for row in rows:
833
+ entity = self._row_to_entity_with_tenant(tuple(row))
834
+ neighbors.append(entity)
835
+
836
+ # Incoming relations
837
+ if direction in ["incoming", "both"]:
838
+ # Parameter order must match query: JOIN condition (e.tenant), WHERE target_id, [type], r.tenant
839
+ params_incoming = tenant_params_e + [entity_id]
840
+ if relation_type:
841
+ params_incoming.append(relation_type)
842
+ params_incoming.extend(tenant_params)
843
+
844
+ query = f"""
845
+ SELECT e.id, e.tenant_id, e.entity_type, e.properties, e.embedding
846
+ FROM {relations_table} r
847
+ JOIN {entities_table} e ON r.source_id = e.id AND {tenant_filter_e}
848
+ WHERE r.target_id = ? {type_filter} AND {tenant_filter_r}
849
+ """
850
+
851
+ cursor = await self.conn.execute(query, params_incoming)
852
+ rows = await cursor.fetchall()
853
+
854
+ for row in rows:
855
+ entity = self._row_to_entity_with_tenant(tuple(row))
856
+ neighbors.append(entity)
857
+
858
+ return neighbors
859
+
860
+ # =========================================================================
861
+ # Tier 2: Advanced Interface (SQL-optimized overrides)
862
+ # =========================================================================
863
+
864
+ async def get_all_entities(
865
+ self,
866
+ entity_type: Optional[str] = None,
867
+ limit: Optional[int] = None,
868
+ offset: int = 0,
869
+ context: Optional[TenantContext] = None,
870
+ ) -> List[Entity]:
871
+ """
872
+ Get all entities in the graph store
873
+
874
+ SQL-optimized implementation that uses efficient queries with filtering
875
+ and pagination.
876
+
877
+ Args:
878
+ entity_type: Optional filter by entity type
879
+ limit: Optional maximum number of entities to return
880
+ offset: Number of entities to skip (for pagination)
881
+ context: Optional tenant context for multi-tenant isolation
882
+
883
+ Returns:
884
+ List of entities matching the criteria
885
+ """
886
+ if not self._is_initialized:
887
+ raise RuntimeError("GraphStore not initialized")
888
+ if self.conn is None:
889
+ raise RuntimeError("Database connection not initialized")
890
+
891
+ tenant_id = self._get_tenant_id(context)
892
+ table_name = self._get_table_name("entities", tenant_id)
893
+
894
+ # Build query with filters
895
+ conditions = []
896
+ params = []
897
+
898
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
899
+ # SEPARATE_SCHEMA: No tenant_id column, tenant filtering via table_name
900
+ if entity_type:
901
+ conditions.append("entity_type = ?")
902
+ params.append(entity_type)
903
+
904
+ where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
905
+
906
+ # Build LIMIT and OFFSET clauses
907
+ limit_clause = ""
908
+ if limit is not None and offset > 0:
909
+ limit_clause = f"LIMIT {limit} OFFSET {offset}"
910
+ elif limit is not None:
911
+ limit_clause = f"LIMIT {limit}"
912
+ elif offset > 0:
913
+ limit_clause = f"OFFSET {offset}"
914
+
915
+ # Execute query
916
+ query = f"""
917
+ SELECT id, entity_type, properties, embedding
918
+ FROM {table_name}
919
+ {where_clause}
920
+ {limit_clause}
921
+ """
922
+
923
+ cursor = await self.conn.execute(query, params)
924
+ rows = await cursor.fetchall()
925
+
926
+ # Convert rows to entities
927
+ entities = []
928
+ for row in rows:
929
+ entity = self._row_to_entity(tuple(row), tenant_id=tenant_id)
930
+ entities.append(entity)
931
+ else:
932
+ # SHARED_SCHEMA: Filter by tenant_id column
933
+ tenant_filter, tenant_params = self._build_tenant_filter(tenant_id)
934
+ if tenant_filter:
935
+ conditions.append(tenant_filter)
936
+ params.extend(tenant_params)
937
+
938
+ # Entity type filtering
939
+ if entity_type:
940
+ conditions.append("entity_type = ?")
941
+ params.append(entity_type)
942
+
943
+ # Build WHERE clause
944
+ where_clause = "WHERE " + " AND ".join(conditions) if conditions else ""
945
+
946
+ # Build LIMIT and OFFSET clauses
947
+ limit_clause = ""
948
+ if limit is not None and offset > 0:
949
+ limit_clause = f"LIMIT {limit} OFFSET {offset}"
950
+ elif limit is not None:
951
+ limit_clause = f"LIMIT {limit}"
952
+ elif offset > 0:
953
+ limit_clause = f"OFFSET {offset}"
954
+
955
+ # Execute query
956
+ query = f"""
957
+ SELECT id, tenant_id, entity_type, properties, embedding
958
+ FROM {table_name}
959
+ {where_clause}
960
+ {limit_clause}
961
+ """
962
+
963
+ cursor = await self.conn.execute(query, params)
964
+ rows = await cursor.fetchall()
965
+
966
+ # Convert rows to entities
967
+ entities = []
968
+ for row in rows:
969
+ entity = self._row_to_entity_with_tenant(tuple(row))
970
+ entities.append(entity)
971
+
972
+ return entities
973
+
974
+ async def vector_search(
975
+ self,
976
+ query_embedding: List[float],
977
+ entity_type: Optional[str] = None,
978
+ max_results: int = 10,
979
+ score_threshold: float = 0.0,
980
+ context: Optional[TenantContext] = None,
981
+ ) -> List[Tuple[Entity, float]]:
982
+ """
983
+ SQL-optimized vector similarity search
984
+
985
+ Performs cosine similarity search over entity embeddings stored in SQLite.
986
+ This implementation fetches all candidates and computes similarity in Python.
987
+
988
+ For production scale, consider:
989
+ - pgvector extension (PostgreSQL)
990
+ - Dedicated vector database (Qdrant, Milvus)
991
+ - Pre-computed ANN indexes
992
+
993
+ Args:
994
+ query_embedding: Query vector
995
+ entity_type: Optional filter by entity type
996
+ max_results: Maximum number of results to return
997
+ score_threshold: Minimum similarity score (0.0-1.0)
998
+ context: Optional tenant context for multi-tenant isolation
999
+
1000
+ Returns:
1001
+ List of (entity, similarity_score) tuples, sorted descending
1002
+ """
1003
+ if not self._is_initialized:
1004
+ raise RuntimeError("GraphStore not initialized")
1005
+ if self.conn is None:
1006
+ raise RuntimeError("Database connection not initialized")
1007
+
1008
+ if not query_embedding:
1009
+ raise ValueError("Query embedding cannot be empty")
1010
+
1011
+ tenant_id = self._get_tenant_id(context)
1012
+ table_name = self._get_table_name("entities", tenant_id)
1013
+
1014
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
1015
+ # SEPARATE_SCHEMA: No tenant filtering needed
1016
+ type_filter = "WHERE entity_type = ?" if entity_type else ""
1017
+ params = [entity_type] if entity_type else []
1018
+
1019
+ query = f"""
1020
+ SELECT id, entity_type, properties, embedding
1021
+ FROM {table_name}
1022
+ {type_filter}
1023
+ """
1024
+
1025
+ cursor = await self.conn.execute(query, params)
1026
+ rows = await cursor.fetchall()
1027
+
1028
+ # Compute similarities
1029
+ scored_entities = []
1030
+ for row in rows:
1031
+ entity = self._row_to_entity(tuple(row), tenant_id=tenant_id)
1032
+
1033
+ if not entity.embedding:
1034
+ continue
1035
+
1036
+ similarity = self._cosine_similarity(query_embedding, entity.embedding)
1037
+ if similarity >= score_threshold:
1038
+ scored_entities.append((entity, similarity))
1039
+ else:
1040
+ # SHARED_SCHEMA: Filter by tenant_id
1041
+ tenant_filter, tenant_params = self._build_tenant_filter(tenant_id)
1042
+
1043
+ if entity_type:
1044
+ where_clause = f"WHERE {tenant_filter} AND entity_type = ?"
1045
+ params = tenant_params + [entity_type]
1046
+ else:
1047
+ where_clause = f"WHERE {tenant_filter}"
1048
+ params = tenant_params
1049
+
1050
+ query = f"""
1051
+ SELECT id, tenant_id, entity_type, properties, embedding
1052
+ FROM {table_name}
1053
+ {where_clause}
1054
+ """
1055
+
1056
+ cursor = await self.conn.execute(query, params)
1057
+ rows = await cursor.fetchall()
1058
+
1059
+ # Compute similarities
1060
+ scored_entities = []
1061
+ for row in rows:
1062
+ entity = self._row_to_entity_with_tenant(tuple(row))
1063
+
1064
+ if not entity.embedding:
1065
+ continue
1066
+
1067
+ similarity = self._cosine_similarity(query_embedding, entity.embedding)
1068
+ if similarity >= score_threshold:
1069
+ scored_entities.append((entity, similarity))
1070
+
1071
+ # Sort by score descending and return top max_results
1072
+ scored_entities.sort(key=lambda x: x[1], reverse=True)
1073
+ return scored_entities[:max_results]
1074
+
1075
+ async def traverse(
1076
+ self,
1077
+ start_entity_id: str,
1078
+ relation_type: Optional[str] = None,
1079
+ max_depth: int = 3,
1080
+ max_results: int = 100,
1081
+ context: Optional[TenantContext] = None,
1082
+ ) -> List[Path]:
1083
+ """
1084
+ SQL-optimized traversal using recursive CTE
1085
+
1086
+ This overrides the default Tier 2 implementation for better performance.
1087
+ Uses recursive CTEs in SQLite for efficient graph traversal.
1088
+
1089
+ Args:
1090
+ start_entity_id: Starting entity ID
1091
+ relation_type: Optional filter by relation type
1092
+ max_depth: Maximum traversal depth
1093
+ max_results: Maximum number of paths to return
1094
+ context: Optional tenant context for multi-tenant isolation
1095
+ """
1096
+ if not self._is_initialized:
1097
+ raise RuntimeError("GraphStore not initialized")
1098
+
1099
+ # For SQLite, we'll use the default implementation from base class
1100
+ # which uses BFS with get_neighbors(). While recursive CTEs are powerful,
1101
+ # building full Path objects with them is complex. The default is sufficient.
1102
+ # Backends with native graph query languages (e.g., Neo4j with Cypher)
1103
+ # should override this for better performance.
1104
+ return await self._default_traverse_bfs(start_entity_id, relation_type, max_depth, max_results, context)
1105
+
1106
+ # =========================================================================
1107
+ # Helper Methods
1108
+ # =========================================================================
1109
+
1110
+ def _row_to_entity(self, row: tuple, tenant_id: Optional[str] = None) -> Entity:
1111
+ """Convert database row to Entity object (for SEPARATE_SCHEMA without tenant_id column)"""
1112
+ entity_id, entity_type, properties_json, embedding_blob = row
1113
+
1114
+ properties = json.loads(properties_json)
1115
+ embedding = self._deserialize_embedding(embedding_blob) if embedding_blob else None
1116
+
1117
+ return Entity(
1118
+ id=entity_id,
1119
+ entity_type=entity_type,
1120
+ properties=properties,
1121
+ embedding=embedding,
1122
+ tenant_id=tenant_id,
1123
+ )
1124
+
1125
+ def _row_to_entity_with_tenant(self, row: tuple) -> Entity:
1126
+ """Convert database row to Entity object (for SHARED_SCHEMA with tenant_id column)"""
1127
+ entity_id, tenant_id, entity_type, properties_json, embedding_blob = row
1128
+
1129
+ properties = json.loads(properties_json)
1130
+ embedding = self._deserialize_embedding(embedding_blob) if embedding_blob else None
1131
+
1132
+ return Entity(
1133
+ id=entity_id,
1134
+ entity_type=entity_type,
1135
+ properties=properties,
1136
+ embedding=embedding,
1137
+ tenant_id=tenant_id,
1138
+ )
1139
+
1140
+ def _row_to_relation(self, row: tuple, tenant_id: Optional[str] = None) -> Relation:
1141
+ """Convert database row to Relation object (for SEPARATE_SCHEMA without tenant_id column)"""
1142
+ rel_id, rel_type, source_id, target_id, properties_json, weight = row
1143
+
1144
+ properties = json.loads(properties_json)
1145
+
1146
+ return Relation(
1147
+ id=rel_id,
1148
+ relation_type=rel_type,
1149
+ source_id=source_id,
1150
+ target_id=target_id,
1151
+ properties=properties,
1152
+ weight=weight,
1153
+ tenant_id=tenant_id,
1154
+ )
1155
+
1156
+ def _row_to_relation_with_tenant(self, row: tuple) -> Relation:
1157
+ """Convert database row to Relation object (for SHARED_SCHEMA with tenant_id column)"""
1158
+ rel_id, tenant_id, rel_type, source_id, target_id, properties_json, weight = row
1159
+
1160
+ properties = json.loads(properties_json)
1161
+
1162
+ return Relation(
1163
+ id=rel_id,
1164
+ relation_type=rel_type,
1165
+ source_id=source_id,
1166
+ target_id=target_id,
1167
+ properties=properties,
1168
+ weight=weight,
1169
+ tenant_id=tenant_id,
1170
+ )
1171
+
1172
+ def _serialize_embedding(self, embedding: List[float]) -> bytes:
1173
+ """Serialize embedding vector to bytes"""
1174
+ import struct
1175
+
1176
+ return struct.pack(f"{len(embedding)}f", *embedding)
1177
+
1178
+ def _deserialize_embedding(self, blob: bytes) -> List[float]:
1179
+ """Deserialize embedding vector from bytes"""
1180
+ import struct
1181
+
1182
+ count = len(blob) // 4 # 4 bytes per float
1183
+ return list(struct.unpack(f"{count}f", blob))
1184
+
1185
+ def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
1186
+ """
1187
+ Compute cosine similarity between two vectors
1188
+
1189
+ Returns value between -1 and 1, where 1 means identical direction.
1190
+ Normalized to 0-1 range for consistency.
1191
+
1192
+ Args:
1193
+ vec1: First vector
1194
+ vec2: Second vector
1195
+
1196
+ Returns:
1197
+ Cosine similarity (0.0-1.0)
1198
+ """
1199
+ if len(vec1) != len(vec2):
1200
+ return 0.0
1201
+
1202
+ dot_product = sum(a * b for a, b in zip(vec1, vec2))
1203
+ magnitude1 = sum(a * a for a in vec1) ** 0.5
1204
+ magnitude2 = sum(b * b for b in vec2) ** 0.5
1205
+
1206
+ if magnitude1 == 0 or magnitude2 == 0:
1207
+ return 0.0
1208
+
1209
+ # Cosine similarity ranges from -1 to 1, normalize to 0 to 1
1210
+ similarity = dot_product / (magnitude1 * magnitude2)
1211
+ return (similarity + 1) / 2
1212
+
1213
+ async def get_stats(self, context: Optional[TenantContext] = None) -> Dict[str, Any]:
1214
+ """
1215
+ Get statistics about the SQLite graph store
1216
+
1217
+ Args:
1218
+ context: Optional tenant context for tenant-scoped stats
1219
+ """
1220
+ if not self._is_initialized:
1221
+ raise RuntimeError("GraphStore not initialized")
1222
+ if self.conn is None:
1223
+ raise RuntimeError("Database connection not initialized")
1224
+
1225
+ tenant_id = self._get_tenant_id(context)
1226
+ entities_table = self._get_table_name("entities", tenant_id)
1227
+ relations_table = self._get_table_name("relations", tenant_id)
1228
+
1229
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA and tenant_id:
1230
+ # Check if tenant tables exist
1231
+ cursor = await self.conn.execute(
1232
+ "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
1233
+ (entities_table,)
1234
+ )
1235
+ table_exists = await cursor.fetchone()
1236
+
1237
+ if not table_exists:
1238
+ entity_count = 0
1239
+ relation_count = 0
1240
+ else:
1241
+ cursor = await self.conn.execute(f"SELECT COUNT(*) FROM {entities_table}")
1242
+ entity_row = await cursor.fetchone()
1243
+ entity_count = entity_row[0] if entity_row else 0
1244
+
1245
+ cursor = await self.conn.execute(f"SELECT COUNT(*) FROM {relations_table}")
1246
+ relation_row = await cursor.fetchone()
1247
+ relation_count = relation_row[0] if relation_row else 0
1248
+ else:
1249
+ tenant_filter, params = self._build_tenant_filter(tenant_id)
1250
+
1251
+ cursor = await self.conn.execute(
1252
+ f"SELECT COUNT(*) FROM {entities_table} WHERE {tenant_filter}",
1253
+ params
1254
+ )
1255
+ entity_row = await cursor.fetchone()
1256
+ entity_count = entity_row[0] if entity_row else 0
1257
+
1258
+ cursor = await self.conn.execute(
1259
+ f"SELECT COUNT(*) FROM {relations_table} WHERE {tenant_filter}",
1260
+ params
1261
+ )
1262
+ relation_row = await cursor.fetchone()
1263
+ relation_count = relation_row[0] if relation_row else 0
1264
+
1265
+ # Database file size
1266
+ file_size = 0
1267
+ if self.db_path != ":memory:":
1268
+ try:
1269
+ file_size = PathLibPath(self.db_path).stat().st_size
1270
+ except (OSError, ValueError):
1271
+ pass
1272
+
1273
+ return {
1274
+ "entity_count": entity_count,
1275
+ "relation_count": relation_count,
1276
+ "storage_type": "sqlite",
1277
+ "db_path": self.db_path,
1278
+ "db_size_bytes": file_size,
1279
+ "is_initialized": self._is_initialized,
1280
+ "isolation_mode": self.isolation_mode.value,
1281
+ "tenant_id": tenant_id,
1282
+ }
1283
+
1284
+ async def clear(self, context: Optional[TenantContext] = None):
1285
+ """
1286
+ Clear data from SQLite database
1287
+
1288
+ Args:
1289
+ context: Optional tenant context for multi-tenant isolation.
1290
+ If provided, clears only data for the specified tenant.
1291
+ If None, clears all data.
1292
+ """
1293
+ if not self._is_initialized:
1294
+ raise RuntimeError("GraphStore not initialized")
1295
+ if self.conn is None:
1296
+ raise RuntimeError("Database connection not initialized")
1297
+
1298
+ tenant_id = self._get_tenant_id(context)
1299
+
1300
+ if tenant_id is None:
1301
+ # Clear all data (global and all tenants)
1302
+ await self.conn.execute("DELETE FROM relations")
1303
+ await self.conn.execute("DELETE FROM entities")
1304
+
1305
+ # Drop all tenant-specific tables for SEPARATE_SCHEMA
1306
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA:
1307
+ cursor = await self.conn.execute(
1308
+ "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'tenant_%'"
1309
+ )
1310
+ tables = await cursor.fetchall()
1311
+ for (table_name,) in tables:
1312
+ await self.conn.execute(f"DROP TABLE IF EXISTS {table_name}")
1313
+ self._initialized_tenant_tables.clear()
1314
+ else:
1315
+ # Clear tenant-specific data
1316
+ if self.isolation_mode == TenantIsolationMode.SEPARATE_SCHEMA:
1317
+ entities_table = self._get_table_name("entities", tenant_id)
1318
+ relations_table = self._get_table_name("relations", tenant_id)
1319
+
1320
+ # Drop tenant tables
1321
+ await self.conn.execute(f"DROP TABLE IF EXISTS {relations_table}")
1322
+ await self.conn.execute(f"DROP TABLE IF EXISTS {entities_table}")
1323
+ self._initialized_tenant_tables.discard(tenant_id)
1324
+ else:
1325
+ # Delete from shared tables with tenant filter
1326
+ tenant_filter, params = self._build_tenant_filter(tenant_id)
1327
+ await self.conn.execute(
1328
+ f"DELETE FROM relations WHERE {tenant_filter}",
1329
+ params
1330
+ )
1331
+ await self.conn.execute(
1332
+ f"DELETE FROM entities WHERE {tenant_filter}",
1333
+ params
1334
+ )
1335
+
1336
+ if not self._in_transaction:
1337
+ await self.conn.commit()
1338
+
1339
+ async def migrate_add_tenant_id(self):
1340
+ """
1341
+ Migration script to add tenant_id column to existing databases.
1342
+
1343
+ This should be run once when upgrading an existing database to support multi-tenancy.
1344
+ """
1345
+ if not self._is_initialized:
1346
+ raise RuntimeError("GraphStore not initialized")
1347
+ if self.conn is None:
1348
+ raise RuntimeError("Database connection not initialized")
1349
+
1350
+ # Check if tenant_id column already exists
1351
+ cursor = await self.conn.execute("PRAGMA table_info(entities)")
1352
+ columns = await cursor.fetchall()
1353
+ column_names = [col[1] for col in columns]
1354
+
1355
+ if "tenant_id" in column_names:
1356
+ return # Migration already applied
1357
+
1358
+ # Apply migration
1359
+ try:
1360
+ await self.conn.execute("ALTER TABLE entities ADD COLUMN tenant_id TEXT")
1361
+ await self.conn.execute("ALTER TABLE relations ADD COLUMN tenant_id TEXT")
1362
+
1363
+ # Create tenant indexes
1364
+ await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_entities_tenant ON entities(tenant_id)")
1365
+ await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_entities_tenant_type ON entities(tenant_id, entity_type)")
1366
+ await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_relations_tenant ON relations(tenant_id)")
1367
+ await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_relations_tenant_source ON relations(tenant_id, source_id)")
1368
+ await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_relations_tenant_target ON relations(tenant_id, target_id)")
1369
+
1370
+ await self.conn.commit()
1371
+ except Exception as e:
1372
+ await self.conn.rollback()
1373
+ raise RuntimeError(f"Migration failed: {e}")