superlocalmemory 2.8.6 → 3.0.0

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.
Files changed (431) hide show
  1. package/LICENSE +9 -1
  2. package/NOTICE +63 -0
  3. package/README.md +165 -480
  4. package/bin/slm +17 -449
  5. package/bin/slm-npm +1 -1
  6. package/conftest.py +5 -0
  7. package/docs/api-reference.md +284 -0
  8. package/docs/architecture.md +149 -0
  9. package/docs/auto-memory.md +150 -0
  10. package/docs/cli-reference.md +276 -0
  11. package/docs/compliance.md +191 -0
  12. package/docs/configuration.md +182 -0
  13. package/docs/getting-started.md +102 -0
  14. package/docs/ide-setup.md +261 -0
  15. package/docs/mcp-tools.md +220 -0
  16. package/docs/migration-from-v2.md +170 -0
  17. package/docs/profiles.md +173 -0
  18. package/docs/troubleshooting.md +310 -0
  19. package/{configs → ide/configs}/antigravity-mcp.json +3 -3
  20. package/ide/configs/chatgpt-desktop-mcp.json +16 -0
  21. package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
  22. package/{configs → ide/configs}/codex-mcp.toml +4 -4
  23. package/{configs → ide/configs}/continue-mcp.yaml +4 -3
  24. package/{configs → ide/configs}/continue-skills.yaml +6 -6
  25. package/ide/configs/cursor-mcp.json +15 -0
  26. package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
  27. package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
  28. package/{configs → ide/configs}/opencode-mcp.json +2 -2
  29. package/{configs → ide/configs}/perplexity-mcp.json +2 -2
  30. package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
  31. package/{configs → ide/configs}/windsurf-mcp.json +3 -3
  32. package/{configs → ide/configs}/zed-mcp.json +2 -2
  33. package/{hooks → ide/hooks}/context-hook.js +9 -20
  34. package/ide/hooks/memory-list-skill.js +70 -0
  35. package/ide/hooks/memory-profile-skill.js +101 -0
  36. package/ide/hooks/memory-recall-skill.js +62 -0
  37. package/ide/hooks/memory-remember-skill.js +68 -0
  38. package/ide/hooks/memory-reset-skill.js +160 -0
  39. package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
  40. package/ide/integrations/langchain/README.md +106 -0
  41. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
  42. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
  43. package/ide/integrations/langchain/pyproject.toml +38 -0
  44. package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
  45. package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
  46. package/ide/integrations/langchain/tests/test_security.py +117 -0
  47. package/ide/integrations/llamaindex/README.md +81 -0
  48. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
  49. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
  50. package/ide/integrations/llamaindex/pyproject.toml +43 -0
  51. package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
  52. package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
  53. package/ide/integrations/llamaindex/tests/test_security.py +241 -0
  54. package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
  55. package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
  56. package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
  57. package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
  58. package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
  59. package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
  60. package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
  61. package/package.json +13 -22
  62. package/pyproject.toml +85 -0
  63. package/scripts/build-dmg.sh +417 -0
  64. package/scripts/install-skills.ps1 +334 -0
  65. package/scripts/postinstall.js +2 -2
  66. package/scripts/start-dashboard.ps1 +52 -0
  67. package/scripts/start-dashboard.sh +41 -0
  68. package/scripts/sync-wiki.ps1 +127 -0
  69. package/scripts/sync-wiki.sh +82 -0
  70. package/scripts/test-dmg.sh +161 -0
  71. package/scripts/test-npm-package.ps1 +252 -0
  72. package/scripts/test-npm-package.sh +207 -0
  73. package/scripts/verify-install.ps1 +294 -0
  74. package/scripts/verify-install.sh +266 -0
  75. package/src/superlocalmemory/__init__.py +0 -0
  76. package/src/superlocalmemory/attribution/__init__.py +9 -0
  77. package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
  78. package/src/superlocalmemory/attribution/signer.py +153 -0
  79. package/src/superlocalmemory/attribution/watermark.py +189 -0
  80. package/src/superlocalmemory/cli/__init__.py +5 -0
  81. package/src/superlocalmemory/cli/commands.py +245 -0
  82. package/src/superlocalmemory/cli/main.py +89 -0
  83. package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
  84. package/src/superlocalmemory/cli/post_install.py +99 -0
  85. package/src/superlocalmemory/cli/setup_wizard.py +129 -0
  86. package/src/superlocalmemory/compliance/__init__.py +0 -0
  87. package/src/superlocalmemory/compliance/abac.py +204 -0
  88. package/src/superlocalmemory/compliance/audit.py +314 -0
  89. package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
  90. package/src/superlocalmemory/compliance/gdpr.py +294 -0
  91. package/src/superlocalmemory/compliance/lifecycle.py +158 -0
  92. package/src/superlocalmemory/compliance/retention.py +232 -0
  93. package/src/superlocalmemory/compliance/scheduler.py +148 -0
  94. package/src/superlocalmemory/core/__init__.py +0 -0
  95. package/src/superlocalmemory/core/config.py +391 -0
  96. package/src/superlocalmemory/core/embeddings.py +293 -0
  97. package/src/superlocalmemory/core/engine.py +701 -0
  98. package/src/superlocalmemory/core/hooks.py +65 -0
  99. package/src/superlocalmemory/core/maintenance.py +172 -0
  100. package/src/superlocalmemory/core/modes.py +140 -0
  101. package/src/superlocalmemory/core/profiles.py +234 -0
  102. package/src/superlocalmemory/core/registry.py +117 -0
  103. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  104. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
  105. package/src/superlocalmemory/encoding/__init__.py +0 -0
  106. package/src/superlocalmemory/encoding/consolidator.py +485 -0
  107. package/src/superlocalmemory/encoding/emotional.py +125 -0
  108. package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
  109. package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
  110. package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
  111. package/src/superlocalmemory/encoding/foresight.py +91 -0
  112. package/src/superlocalmemory/encoding/graph_builder.py +302 -0
  113. package/src/superlocalmemory/encoding/observation_builder.py +160 -0
  114. package/src/superlocalmemory/encoding/scene_builder.py +183 -0
  115. package/src/superlocalmemory/encoding/signal_inference.py +90 -0
  116. package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
  117. package/src/superlocalmemory/encoding/type_router.py +235 -0
  118. package/src/superlocalmemory/hooks/__init__.py +3 -0
  119. package/src/superlocalmemory/hooks/auto_capture.py +111 -0
  120. package/src/superlocalmemory/hooks/auto_recall.py +93 -0
  121. package/src/superlocalmemory/hooks/ide_connector.py +204 -0
  122. package/src/superlocalmemory/hooks/rules_engine.py +99 -0
  123. package/src/superlocalmemory/infra/__init__.py +3 -0
  124. package/src/superlocalmemory/infra/auth_middleware.py +82 -0
  125. package/src/superlocalmemory/infra/backup.py +317 -0
  126. package/src/superlocalmemory/infra/cache_manager.py +267 -0
  127. package/src/superlocalmemory/infra/event_bus.py +381 -0
  128. package/src/superlocalmemory/infra/rate_limiter.py +135 -0
  129. package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
  130. package/src/superlocalmemory/learning/__init__.py +0 -0
  131. package/src/superlocalmemory/learning/adaptive.py +172 -0
  132. package/src/superlocalmemory/learning/behavioral.py +490 -0
  133. package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
  134. package/src/superlocalmemory/learning/bootstrap.py +298 -0
  135. package/src/superlocalmemory/learning/cross_project.py +399 -0
  136. package/src/superlocalmemory/learning/database.py +376 -0
  137. package/src/superlocalmemory/learning/engagement.py +323 -0
  138. package/src/superlocalmemory/learning/features.py +138 -0
  139. package/src/superlocalmemory/learning/feedback.py +316 -0
  140. package/src/superlocalmemory/learning/outcomes.py +255 -0
  141. package/src/superlocalmemory/learning/project_context.py +366 -0
  142. package/src/superlocalmemory/learning/ranker.py +155 -0
  143. package/src/superlocalmemory/learning/source_quality.py +303 -0
  144. package/src/superlocalmemory/learning/workflows.py +309 -0
  145. package/src/superlocalmemory/llm/__init__.py +0 -0
  146. package/src/superlocalmemory/llm/backbone.py +316 -0
  147. package/src/superlocalmemory/math/__init__.py +0 -0
  148. package/src/superlocalmemory/math/fisher.py +356 -0
  149. package/src/superlocalmemory/math/langevin.py +398 -0
  150. package/src/superlocalmemory/math/sheaf.py +257 -0
  151. package/src/superlocalmemory/mcp/__init__.py +0 -0
  152. package/src/superlocalmemory/mcp/resources.py +245 -0
  153. package/src/superlocalmemory/mcp/server.py +61 -0
  154. package/src/superlocalmemory/mcp/tools.py +18 -0
  155. package/src/superlocalmemory/mcp/tools_core.py +305 -0
  156. package/src/superlocalmemory/mcp/tools_v28.py +223 -0
  157. package/src/superlocalmemory/mcp/tools_v3.py +286 -0
  158. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  159. package/src/superlocalmemory/retrieval/agentic.py +295 -0
  160. package/src/superlocalmemory/retrieval/ann_index.py +223 -0
  161. package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
  162. package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
  163. package/src/superlocalmemory/retrieval/engine.py +390 -0
  164. package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
  165. package/src/superlocalmemory/retrieval/fusion.py +78 -0
  166. package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
  167. package/src/superlocalmemory/retrieval/reranker.py +154 -0
  168. package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
  169. package/src/superlocalmemory/retrieval/strategy.py +96 -0
  170. package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
  171. package/src/superlocalmemory/server/__init__.py +1 -0
  172. package/src/superlocalmemory/server/api.py +248 -0
  173. package/src/superlocalmemory/server/routes/__init__.py +4 -0
  174. package/src/superlocalmemory/server/routes/agents.py +107 -0
  175. package/src/superlocalmemory/server/routes/backup.py +91 -0
  176. package/src/superlocalmemory/server/routes/behavioral.py +127 -0
  177. package/src/superlocalmemory/server/routes/compliance.py +160 -0
  178. package/src/superlocalmemory/server/routes/data_io.py +188 -0
  179. package/src/superlocalmemory/server/routes/events.py +183 -0
  180. package/src/superlocalmemory/server/routes/helpers.py +85 -0
  181. package/src/superlocalmemory/server/routes/learning.py +273 -0
  182. package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
  183. package/src/superlocalmemory/server/routes/memories.py +399 -0
  184. package/src/superlocalmemory/server/routes/profiles.py +219 -0
  185. package/src/superlocalmemory/server/routes/stats.py +346 -0
  186. package/src/superlocalmemory/server/routes/v3_api.py +365 -0
  187. package/src/superlocalmemory/server/routes/ws.py +82 -0
  188. package/src/superlocalmemory/server/security_middleware.py +57 -0
  189. package/src/superlocalmemory/server/ui.py +245 -0
  190. package/src/superlocalmemory/storage/__init__.py +0 -0
  191. package/src/superlocalmemory/storage/access_control.py +182 -0
  192. package/src/superlocalmemory/storage/database.py +594 -0
  193. package/src/superlocalmemory/storage/migrations.py +303 -0
  194. package/src/superlocalmemory/storage/models.py +406 -0
  195. package/src/superlocalmemory/storage/schema.py +726 -0
  196. package/src/superlocalmemory/storage/v2_migrator.py +317 -0
  197. package/src/superlocalmemory/trust/__init__.py +0 -0
  198. package/src/superlocalmemory/trust/gate.py +130 -0
  199. package/src/superlocalmemory/trust/provenance.py +124 -0
  200. package/src/superlocalmemory/trust/scorer.py +347 -0
  201. package/src/superlocalmemory/trust/signals.py +153 -0
  202. package/ui/index.html +278 -5
  203. package/ui/js/auto-settings.js +70 -0
  204. package/ui/js/dashboard.js +90 -0
  205. package/ui/js/fact-detail.js +92 -0
  206. package/ui/js/feedback.js +2 -2
  207. package/ui/js/ide-status.js +102 -0
  208. package/ui/js/math-health.js +98 -0
  209. package/ui/js/recall-lab.js +127 -0
  210. package/ui/js/settings.js +2 -2
  211. package/ui/js/trust-dashboard.js +73 -0
  212. package/api_server.py +0 -724
  213. package/bin/aider-smart +0 -72
  214. package/bin/superlocalmemoryv2-learning +0 -4
  215. package/bin/superlocalmemoryv2-list +0 -3
  216. package/bin/superlocalmemoryv2-patterns +0 -4
  217. package/bin/superlocalmemoryv2-profile +0 -3
  218. package/bin/superlocalmemoryv2-recall +0 -3
  219. package/bin/superlocalmemoryv2-remember +0 -3
  220. package/bin/superlocalmemoryv2-reset +0 -3
  221. package/bin/superlocalmemoryv2-status +0 -3
  222. package/configs/chatgpt-desktop-mcp.json +0 -16
  223. package/configs/cursor-mcp.json +0 -15
  224. package/hooks/memory-list-skill.js +0 -139
  225. package/hooks/memory-profile-skill.js +0 -273
  226. package/hooks/memory-recall-skill.js +0 -114
  227. package/hooks/memory-remember-skill.js +0 -127
  228. package/hooks/memory-reset-skill.js +0 -274
  229. package/mcp_server.py +0 -1808
  230. package/requirements-core.txt +0 -22
  231. package/requirements-learning.txt +0 -12
  232. package/requirements.txt +0 -12
  233. package/src/agent_registry.py +0 -411
  234. package/src/auth_middleware.py +0 -61
  235. package/src/auto_backup.py +0 -459
  236. package/src/behavioral/__init__.py +0 -49
  237. package/src/behavioral/behavioral_listener.py +0 -203
  238. package/src/behavioral/behavioral_patterns.py +0 -275
  239. package/src/behavioral/cross_project_transfer.py +0 -206
  240. package/src/behavioral/outcome_inference.py +0 -194
  241. package/src/behavioral/outcome_tracker.py +0 -193
  242. package/src/behavioral/tests/__init__.py +0 -4
  243. package/src/behavioral/tests/test_behavioral_integration.py +0 -108
  244. package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
  245. package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
  246. package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
  247. package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
  248. package/src/behavioral/tests/test_outcome_inference.py +0 -107
  249. package/src/behavioral/tests/test_outcome_tracker.py +0 -96
  250. package/src/cache_manager.py +0 -518
  251. package/src/compliance/__init__.py +0 -48
  252. package/src/compliance/abac_engine.py +0 -149
  253. package/src/compliance/abac_middleware.py +0 -116
  254. package/src/compliance/audit_db.py +0 -215
  255. package/src/compliance/audit_logger.py +0 -148
  256. package/src/compliance/retention_manager.py +0 -289
  257. package/src/compliance/retention_scheduler.py +0 -186
  258. package/src/compliance/tests/__init__.py +0 -4
  259. package/src/compliance/tests/test_abac_enforcement.py +0 -95
  260. package/src/compliance/tests/test_abac_engine.py +0 -124
  261. package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
  262. package/src/compliance/tests/test_audit_db.py +0 -123
  263. package/src/compliance/tests/test_audit_logger.py +0 -98
  264. package/src/compliance/tests/test_mcp_audit.py +0 -128
  265. package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
  266. package/src/compliance/tests/test_retention_manager.py +0 -131
  267. package/src/compliance/tests/test_retention_scheduler.py +0 -99
  268. package/src/compression/__init__.py +0 -25
  269. package/src/compression/cli.py +0 -150
  270. package/src/compression/cold_storage.py +0 -217
  271. package/src/compression/config.py +0 -72
  272. package/src/compression/orchestrator.py +0 -133
  273. package/src/compression/tier2_compressor.py +0 -228
  274. package/src/compression/tier3_compressor.py +0 -153
  275. package/src/compression/tier_classifier.py +0 -148
  276. package/src/db_connection_manager.py +0 -536
  277. package/src/embedding_engine.py +0 -63
  278. package/src/embeddings/__init__.py +0 -47
  279. package/src/embeddings/cache.py +0 -70
  280. package/src/embeddings/cli.py +0 -113
  281. package/src/embeddings/constants.py +0 -47
  282. package/src/embeddings/database.py +0 -91
  283. package/src/embeddings/engine.py +0 -247
  284. package/src/embeddings/model_loader.py +0 -145
  285. package/src/event_bus.py +0 -562
  286. package/src/graph/__init__.py +0 -36
  287. package/src/graph/build_helpers.py +0 -74
  288. package/src/graph/cli.py +0 -87
  289. package/src/graph/cluster_builder.py +0 -188
  290. package/src/graph/cluster_summary.py +0 -148
  291. package/src/graph/constants.py +0 -47
  292. package/src/graph/edge_builder.py +0 -162
  293. package/src/graph/entity_extractor.py +0 -95
  294. package/src/graph/graph_core.py +0 -226
  295. package/src/graph/graph_search.py +0 -231
  296. package/src/graph/hierarchical.py +0 -207
  297. package/src/graph/schema.py +0 -99
  298. package/src/graph_engine.py +0 -52
  299. package/src/hnsw_index.py +0 -628
  300. package/src/hybrid_search.py +0 -46
  301. package/src/learning/__init__.py +0 -217
  302. package/src/learning/adaptive_ranker.py +0 -682
  303. package/src/learning/bootstrap/__init__.py +0 -69
  304. package/src/learning/bootstrap/constants.py +0 -93
  305. package/src/learning/bootstrap/db_queries.py +0 -316
  306. package/src/learning/bootstrap/sampling.py +0 -82
  307. package/src/learning/bootstrap/text_utils.py +0 -71
  308. package/src/learning/cross_project_aggregator.py +0 -857
  309. package/src/learning/db/__init__.py +0 -40
  310. package/src/learning/db/constants.py +0 -44
  311. package/src/learning/db/schema.py +0 -279
  312. package/src/learning/engagement_tracker.py +0 -628
  313. package/src/learning/feature_extractor.py +0 -708
  314. package/src/learning/feedback_collector.py +0 -806
  315. package/src/learning/learning_db.py +0 -915
  316. package/src/learning/project_context_manager.py +0 -572
  317. package/src/learning/ranking/__init__.py +0 -33
  318. package/src/learning/ranking/constants.py +0 -84
  319. package/src/learning/ranking/helpers.py +0 -278
  320. package/src/learning/source_quality_scorer.py +0 -676
  321. package/src/learning/synthetic_bootstrap.py +0 -755
  322. package/src/learning/tests/test_adaptive_ranker.py +0 -325
  323. package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
  324. package/src/learning/tests/test_aggregator.py +0 -306
  325. package/src/learning/tests/test_auto_retrain_v28.py +0 -35
  326. package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
  327. package/src/learning/tests/test_feature_extractor_v28.py +0 -93
  328. package/src/learning/tests/test_feedback_collector.py +0 -294
  329. package/src/learning/tests/test_learning_db.py +0 -602
  330. package/src/learning/tests/test_learning_db_v28.py +0 -110
  331. package/src/learning/tests/test_learning_init_v28.py +0 -48
  332. package/src/learning/tests/test_outcome_signals.py +0 -48
  333. package/src/learning/tests/test_project_context.py +0 -292
  334. package/src/learning/tests/test_schema_migration.py +0 -319
  335. package/src/learning/tests/test_signal_inference.py +0 -397
  336. package/src/learning/tests/test_source_quality.py +0 -351
  337. package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
  338. package/src/learning/tests/test_workflow_miner.py +0 -318
  339. package/src/learning/workflow_pattern_miner.py +0 -655
  340. package/src/lifecycle/__init__.py +0 -54
  341. package/src/lifecycle/bounded_growth.py +0 -239
  342. package/src/lifecycle/compaction_engine.py +0 -226
  343. package/src/lifecycle/lifecycle_engine.py +0 -355
  344. package/src/lifecycle/lifecycle_evaluator.py +0 -257
  345. package/src/lifecycle/lifecycle_scheduler.py +0 -130
  346. package/src/lifecycle/retention_policy.py +0 -285
  347. package/src/lifecycle/tests/test_bounded_growth.py +0 -193
  348. package/src/lifecycle/tests/test_compaction.py +0 -179
  349. package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
  350. package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
  351. package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
  352. package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
  353. package/src/lifecycle/tests/test_mcp_compact.py +0 -149
  354. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
  355. package/src/lifecycle/tests/test_retention_policy.py +0 -162
  356. package/src/mcp_tools_v28.py +0 -281
  357. package/src/memory/__init__.py +0 -36
  358. package/src/memory/cli.py +0 -205
  359. package/src/memory/constants.py +0 -39
  360. package/src/memory/helpers.py +0 -28
  361. package/src/memory/schema.py +0 -166
  362. package/src/memory-profiles.py +0 -595
  363. package/src/memory-reset.py +0 -491
  364. package/src/memory_compression.py +0 -989
  365. package/src/memory_store_v2.py +0 -1155
  366. package/src/migrate_v1_to_v2.py +0 -629
  367. package/src/pattern_learner.py +0 -34
  368. package/src/patterns/__init__.py +0 -24
  369. package/src/patterns/analyzers.py +0 -251
  370. package/src/patterns/learner.py +0 -271
  371. package/src/patterns/scoring.py +0 -171
  372. package/src/patterns/store.py +0 -225
  373. package/src/patterns/terminology.py +0 -140
  374. package/src/provenance_tracker.py +0 -312
  375. package/src/qualixar_attribution.py +0 -139
  376. package/src/qualixar_watermark.py +0 -78
  377. package/src/query_optimizer.py +0 -511
  378. package/src/rate_limiter.py +0 -83
  379. package/src/search/__init__.py +0 -20
  380. package/src/search/cli.py +0 -77
  381. package/src/search/constants.py +0 -26
  382. package/src/search/engine.py +0 -241
  383. package/src/search/fusion.py +0 -122
  384. package/src/search/index_loader.py +0 -114
  385. package/src/search/methods.py +0 -162
  386. package/src/search_engine_v2.py +0 -401
  387. package/src/setup_validator.py +0 -482
  388. package/src/subscription_manager.py +0 -391
  389. package/src/tree/__init__.py +0 -59
  390. package/src/tree/builder.py +0 -185
  391. package/src/tree/nodes.py +0 -202
  392. package/src/tree/queries.py +0 -257
  393. package/src/tree/schema.py +0 -80
  394. package/src/tree_manager.py +0 -19
  395. package/src/trust/__init__.py +0 -45
  396. package/src/trust/constants.py +0 -66
  397. package/src/trust/queries.py +0 -157
  398. package/src/trust/schema.py +0 -95
  399. package/src/trust/scorer.py +0 -299
  400. package/src/trust/signals.py +0 -95
  401. package/src/trust_scorer.py +0 -44
  402. package/ui/app.js +0 -1588
  403. package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
  404. package/ui/js/graph-cytoscape.js +0 -1168
  405. package/ui/js/graph-d3-backup.js +0 -32
  406. package/ui/js/graph.js +0 -32
  407. package/ui_server.py +0 -286
  408. /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
  409. /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
  410. /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
  411. /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
  412. /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
  413. /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
  414. /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
  415. /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
  416. /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
  417. /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
  418. /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
  419. /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
  420. /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
  421. /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
  422. /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
  423. /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
  424. /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
  425. /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
  426. /package/{completions → ide/completions}/slm.bash +0 -0
  427. /package/{completions → ide/completions}/slm.zsh +0 -0
  428. /package/{configs → ide/configs}/cody-commands.json +0 -0
  429. /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
  430. /package/{install.ps1 → scripts/install.ps1} +0 -0
  431. /package/{install.sh → scripts/install.sh} +0 -0
@@ -0,0 +1,135 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+ """Sliding-window rate limiter for per-agent request throttling.
5
+
6
+ Pure stdlib -- no external dependencies. Thread-safe.
7
+
8
+ Defaults (configurable via env vars):
9
+ SLM_RATE_LIMIT_WRITE = 100 req / window
10
+ SLM_RATE_LIMIT_READ = 300 req / window
11
+ SLM_RATE_LIMIT_WINDOW = 60 seconds
12
+ """
13
+
14
+ import logging
15
+ import os
16
+ import threading
17
+ import time
18
+ from collections import defaultdict
19
+ from typing import Dict, List, Tuple
20
+
21
+ logger = logging.getLogger("superlocalmemory.ratelimit")
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Module-level defaults (overridable via environment)
25
+ # ---------------------------------------------------------------------------
26
+ WRITE_LIMIT = int(os.environ.get("SLM_RATE_LIMIT_WRITE", "100"))
27
+ READ_LIMIT = int(os.environ.get("SLM_RATE_LIMIT_READ", "300"))
28
+ WINDOW_SECONDS = int(os.environ.get("SLM_RATE_LIMIT_WINDOW", "60"))
29
+
30
+
31
+ class RateLimiter:
32
+ """Thread-safe sliding-window rate limiter.
33
+
34
+ Each *client_id* (agent name, IP, etc.) gets its own independent
35
+ request window. Expired timestamps are pruned lazily on every call
36
+ to ``allow()`` or ``is_allowed()``.
37
+
38
+ Args:
39
+ max_requests: Maximum requests allowed per window.
40
+ window_seconds: Length of the sliding window in seconds.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ max_requests: int = 100,
46
+ window_seconds: int = 60,
47
+ ) -> None:
48
+ self.max_requests = max_requests
49
+ self.window = window_seconds
50
+ self._requests: Dict[str, List[float]] = defaultdict(list)
51
+ self._lock = threading.Lock()
52
+
53
+ # ----- public API -----
54
+
55
+ def allow(self, client_id: str) -> bool:
56
+ """Check **and record** a request for *client_id*.
57
+
58
+ Returns ``True`` when the request is allowed, ``False`` when the
59
+ client has exceeded its limit for the current window.
60
+ """
61
+ allowed, _ = self.is_allowed(client_id)
62
+ return allowed
63
+
64
+ def is_allowed(self, client_id: str) -> Tuple[bool, int]:
65
+ """Check and record a request.
66
+
67
+ Returns:
68
+ ``(allowed, remaining)`` -- whether the request is permitted
69
+ and how many requests remain in the current window.
70
+ """
71
+ now = time.time()
72
+ cutoff = now - self.window
73
+
74
+ with self._lock:
75
+ # Prune expired timestamps
76
+ self._requests[client_id] = [
77
+ t for t in self._requests[client_id] if t > cutoff
78
+ ]
79
+
80
+ current = len(self._requests[client_id])
81
+
82
+ if current >= self.max_requests:
83
+ return False, 0
84
+
85
+ self._requests[client_id].append(now)
86
+ return True, self.max_requests - current - 1
87
+
88
+ def remaining(self, client_id: str) -> int:
89
+ """Return how many requests *client_id* has left without recording one."""
90
+ now = time.time()
91
+ cutoff = now - self.window
92
+
93
+ with self._lock:
94
+ active = [t for t in self._requests.get(client_id, []) if t > cutoff]
95
+ return max(0, self.max_requests - len(active))
96
+
97
+ def reset(self, client_id: str) -> None:
98
+ """Clear all recorded requests for *client_id*."""
99
+ with self._lock:
100
+ self._requests.pop(client_id, None)
101
+
102
+ def cleanup(self) -> int:
103
+ """Remove stale entries for clients with no recent requests.
104
+
105
+ Returns:
106
+ Number of client entries removed.
107
+ """
108
+ now = time.time()
109
+ cutoff = now - self.window * 2 # keep 2 windows of data
110
+
111
+ with self._lock:
112
+ stale = [
113
+ k
114
+ for k, v in self._requests.items()
115
+ if not v or max(v) < cutoff
116
+ ]
117
+ for k in stale:
118
+ del self._requests[k]
119
+ return len(stale)
120
+
121
+ def get_stats(self) -> dict:
122
+ """Return a snapshot of limiter state."""
123
+ with self._lock:
124
+ return {
125
+ "max_requests": self.max_requests,
126
+ "window_seconds": self.window,
127
+ "tracked_clients": len(self._requests),
128
+ }
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # Module-level convenience singletons
133
+ # ---------------------------------------------------------------------------
134
+ write_limiter = RateLimiter(max_requests=WRITE_LIMIT, window_seconds=WINDOW_SECONDS)
135
+ read_limiter = RateLimiter(max_requests=READ_LIMIT, window_seconds=WINDOW_SECONDS)
@@ -1,17 +1,16 @@
1
- #!/usr/bin/env python3
2
- # SPDX-License-Identifier: MIT
3
- # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
- """
5
- WebhookDispatcher — Delivers events via HTTP POST to configured webhook URLs.
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+ """WebhookDispatcher -- background HTTP POST delivery for memory events.
6
5
 
7
- Runs on a background thread so webhook delivery never blocks the main event flow.
8
- Retries failed deliveries with exponential backoff (3 attempts).
6
+ Runs on a daemon thread so webhook delivery never blocks the main event
7
+ flow. Failed deliveries are retried with exponential back-off (up to
8
+ ``MAX_RETRIES`` attempts).
9
9
 
10
10
  Security:
11
- - Only allows http:// and https:// URLs
12
- - Validates URL format before dispatch
13
- - 10-second timeout per request
14
- - No private/internal IP blocking in v2.5 (added in v2.6 with trust enforcement)
11
+ * Only ``http://`` and ``https://`` URLs are accepted.
12
+ * Private / loopback IPs are rejected.
13
+ * 10-second timeout per outgoing request.
15
14
  """
16
15
 
17
16
  import ipaddress
@@ -21,44 +20,41 @@ import socket
21
20
  import threading
22
21
  import time
23
22
  import urllib.parse
24
- from queue import Queue, Empty
25
- from typing import Optional, Dict
26
23
  from datetime import datetime
24
+ from queue import Empty, Queue
25
+ from typing import Dict, Optional
27
26
 
28
27
  logger = logging.getLogger("superlocalmemory.webhooks")
29
28
 
30
- # Configuration
29
+ # ---------------------------------------------------------------------------
30
+ # Configuration constants
31
+ # ---------------------------------------------------------------------------
31
32
  MAX_RETRIES = 3
32
- RETRY_BACKOFF_BASE = 2 # seconds: 2, 4, 8
33
- REQUEST_TIMEOUT = 10 # seconds
33
+ RETRY_BACKOFF_BASE = 2 # seconds: 2, 4, 8
34
+ REQUEST_TIMEOUT = 10 # seconds
34
35
  MAX_QUEUE_SIZE = 1000
36
+ VERSION = "3.0.0"
35
37
 
36
- # Optional: urllib3/requests for HTTP POST
37
- try:
38
- from urllib.request import Request, urlopen
39
- from urllib.error import URLError, HTTPError
40
- HTTP_AVAILABLE = True
41
- except ImportError:
42
- HTTP_AVAILABLE = False
38
+ # stdlib HTTP -- always available
39
+ from urllib.request import Request, urlopen # noqa: E402
40
+ from urllib.error import HTTPError, URLError # noqa: E402
43
41
 
44
42
 
45
43
  def _is_private_ip(hostname: str) -> bool:
46
- """Check if hostname resolves to a private/internal IP address."""
44
+ """Return ``True`` if *hostname* resolves to a private / loopback IP."""
47
45
  try:
48
46
  ip_str = socket.gethostbyname(hostname)
49
47
  ip = ipaddress.ip_address(ip_str)
50
48
  return ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved
51
49
  except (socket.gaierror, ValueError):
52
- return False # DNS resolution failed — allow (might be valid external hostname)
50
+ return False
53
51
 
54
52
 
55
53
  class WebhookDispatcher:
56
- """
57
- Background webhook delivery with retry logic.
54
+ """Background webhook delivery with retry logic.
58
55
 
59
- Thread-safe. Enqueues webhook deliveries and processes them on a
60
- dedicated background thread. Failed deliveries are retried with
61
- exponential backoff.
56
+ Thread-safe. Enqueues deliveries and processes them on a dedicated
57
+ daemon thread.
62
58
  """
63
59
 
64
60
  _instances: Dict[str, "WebhookDispatcher"] = {}
@@ -66,7 +62,7 @@ class WebhookDispatcher:
66
62
 
67
63
  @classmethod
68
64
  def get_instance(cls, name: str = "default") -> "WebhookDispatcher":
69
- """Get or create a singleton WebhookDispatcher."""
65
+ """Get or create a named singleton."""
70
66
  with cls._instances_lock:
71
67
  if name not in cls._instances:
72
68
  cls._instances[name] = cls()
@@ -74,7 +70,7 @@ class WebhookDispatcher:
74
70
 
75
71
  @classmethod
76
72
  def reset_instance(cls, name: Optional[str] = None) -> None:
77
- """Remove singleton(s). Used for testing."""
73
+ """Remove singleton(s). Primarily for testing."""
78
74
  with cls._instances_lock:
79
75
  if name is None:
80
76
  for inst in cls._instances.values():
@@ -84,7 +80,7 @@ class WebhookDispatcher:
84
80
  cls._instances[name].close()
85
81
  del cls._instances[name]
86
82
 
87
- def __init__(self):
83
+ def __init__(self) -> None:
88
84
  self._queue: Queue = Queue(maxsize=MAX_QUEUE_SIZE)
89
85
  self._closed = False
90
86
  self._stats = {
@@ -95,7 +91,6 @@ class WebhookDispatcher:
95
91
  }
96
92
  self._stats_lock = threading.Lock()
97
93
 
98
- # Background worker thread
99
94
  self._worker = threading.Thread(
100
95
  target=self._worker_loop,
101
96
  name="slm-webhook-worker",
@@ -104,82 +99,106 @@ class WebhookDispatcher:
104
99
  self._worker.start()
105
100
  logger.info("WebhookDispatcher started")
106
101
 
107
- def dispatch(self, event: dict, webhook_url: str) -> None:
108
- """
109
- Enqueue a webhook delivery.
102
+ # ----- public API -----
110
103
 
111
- Args:
112
- event: Event dict to send as JSON POST body
113
- webhook_url: URL to POST to
104
+ def dispatch(self, event: dict, webhook_url: str) -> None:
105
+ """Enqueue a webhook delivery.
114
106
 
115
107
  Raises:
116
- ValueError: If webhook_url is invalid
117
- RuntimeError: If dispatcher is closed
108
+ ValueError: If *webhook_url* is invalid or private.
109
+ RuntimeError: If the dispatcher is closed.
118
110
  """
119
111
  if self._closed:
120
112
  raise RuntimeError("WebhookDispatcher is closed")
121
113
 
122
- if not webhook_url or not (webhook_url.startswith("http://") or webhook_url.startswith("https://")):
114
+ if not webhook_url or not (
115
+ webhook_url.startswith("http://") or webhook_url.startswith("https://")
116
+ ):
123
117
  raise ValueError(f"Invalid webhook URL: {webhook_url}")
124
118
 
125
119
  parsed = urllib.parse.urlparse(webhook_url)
126
120
  if parsed.hostname and _is_private_ip(parsed.hostname):
127
- raise ValueError(f"Webhook URL points to private/internal network: {webhook_url}")
121
+ raise ValueError(
122
+ f"Webhook URL points to private/internal network: {webhook_url}"
123
+ )
128
124
 
129
125
  try:
130
- self._queue.put_nowait({
131
- "event": event,
132
- "url": webhook_url,
133
- "attempt": 0,
134
- "enqueued_at": datetime.now().isoformat(),
135
- })
126
+ self._queue.put_nowait(
127
+ {
128
+ "event": event,
129
+ "url": webhook_url,
130
+ "attempt": 0,
131
+ "enqueued_at": datetime.now().isoformat(),
132
+ }
133
+ )
136
134
  with self._stats_lock:
137
135
  self._stats["dispatched"] += 1
138
136
  except Exception:
139
137
  logger.warning("Webhook queue full, dropping event for %s", webhook_url)
140
138
 
141
- def _worker_loop(self):
142
- """Background worker: processes webhook deliveries sequentially."""
139
+ def get_stats(self) -> dict:
140
+ """Return delivery statistics snapshot."""
141
+ with self._stats_lock:
142
+ return dict(self._stats)
143
+
144
+ def close(self) -> None:
145
+ """Shut down the dispatcher, draining remaining items."""
146
+ if self._closed:
147
+ return
148
+ self._closed = True
149
+ self._queue.put(None) # sentinel
150
+ if self._worker.is_alive():
151
+ self._worker.join(timeout=5)
152
+ logger.info("WebhookDispatcher closed: stats=%s", self._stats)
153
+
154
+ @property
155
+ def is_closed(self) -> bool:
156
+ return self._closed
157
+
158
+ @property
159
+ def queue_size(self) -> int:
160
+ return self._queue.qsize()
161
+
162
+ # ----- internal -----
163
+
164
+ def _worker_loop(self) -> None:
165
+ """Background loop: dequeue and deliver."""
143
166
  while not self._closed:
144
167
  try:
145
168
  item = self._queue.get(timeout=1.0)
146
169
  except Empty:
147
170
  continue
148
171
 
149
- if item is None: # Shutdown sentinel
172
+ if item is None: # shutdown sentinel
150
173
  self._queue.task_done()
151
174
  break
152
175
 
153
176
  self._deliver(item)
154
177
  self._queue.task_done()
155
178
 
156
- def _deliver(self, item: dict):
157
- """Attempt to deliver a webhook. Retry on failure."""
179
+ def _deliver(self, item: dict) -> None:
180
+ """Attempt delivery with exponential-backoff retry."""
158
181
  event = item["event"]
159
182
  url = item["url"]
160
183
  attempt = item["attempt"]
161
184
 
162
- if not HTTP_AVAILABLE:
163
- logger.error("HTTP library not available, cannot deliver webhook to %s", url)
164
- with self._stats_lock:
165
- self._stats["failed"] += 1
166
- return
167
-
168
185
  try:
169
- payload = json.dumps({
170
- "event": event,
171
- "delivered_at": datetime.now().isoformat(),
172
- "attempt": attempt + 1,
173
- "source": "superlocalmemory",
174
- "version": "2.5.0",
175
- }).encode("utf-8")
186
+ payload = json.dumps(
187
+ {
188
+ "event": event,
189
+ "delivered_at": datetime.now().isoformat(),
190
+ "attempt": attempt + 1,
191
+ "source": "superlocalmemory",
192
+ "version": VERSION,
193
+ }
194
+ ).encode("utf-8")
176
195
 
177
196
  req = Request(
178
197
  url,
179
198
  data=payload,
180
199
  headers={
181
200
  "Content-Type": "application/json",
182
- "User-Agent": "SuperLocalMemory/2.5.0",
201
+ "User-Agent": f"SuperLocalMemory/{VERSION}",
183
202
  "X-SLM-Event-Type": event.get("event_type", "unknown"),
184
203
  },
185
204
  method="POST",
@@ -190,47 +209,31 @@ class WebhookDispatcher:
190
209
  if 200 <= status < 300:
191
210
  with self._stats_lock:
192
211
  self._stats["succeeded"] += 1
193
- logger.debug("Webhook delivered: url=%s, status=%d", url, status)
212
+ logger.debug("Webhook delivered: url=%s status=%d", url, status)
194
213
  return
195
- else:
196
- raise HTTPError(url, status, f"HTTP {status}", {}, None)
214
+ raise HTTPError(url, status, f"HTTP {status}", {}, None)
197
215
 
198
- except Exception as e:
199
- logger.warning("Webhook delivery failed (attempt %d/%d): url=%s, error=%s",
200
- attempt + 1, MAX_RETRIES, url, e)
216
+ except Exception as exc:
217
+ logger.warning(
218
+ "Webhook delivery failed (attempt %d/%d): url=%s error=%s",
219
+ attempt + 1,
220
+ MAX_RETRIES,
221
+ url,
222
+ exc,
223
+ )
201
224
 
202
225
  if attempt + 1 < MAX_RETRIES:
203
- # Retry with exponential backoff
204
226
  backoff = RETRY_BACKOFF_BASE ** (attempt + 1)
205
227
  time.sleep(backoff)
206
228
  with self._stats_lock:
207
229
  self._stats["retries"] += 1
208
230
  item["attempt"] = attempt + 1
209
- self._deliver(item) # Recursive retry
231
+ self._deliver(item)
210
232
  else:
211
233
  with self._stats_lock:
212
234
  self._stats["failed"] += 1
213
- logger.error("Webhook permanently failed after %d attempts: url=%s", MAX_RETRIES, url)
214
-
215
- def get_stats(self) -> dict:
216
- """Get webhook delivery statistics."""
217
- with self._stats_lock:
218
- return dict(self._stats)
219
-
220
- def close(self) -> None:
221
- """Shut down the dispatcher. Drains remaining items."""
222
- if self._closed:
223
- return
224
- self._closed = True
225
- self._queue.put(None) # Shutdown sentinel
226
- if self._worker.is_alive():
227
- self._worker.join(timeout=5)
228
- logger.info("WebhookDispatcher closed: stats=%s", self._stats)
229
-
230
- @property
231
- def is_closed(self) -> bool:
232
- return self._closed
233
-
234
- @property
235
- def queue_size(self) -> int:
236
- return self._queue.qsize()
235
+ logger.error(
236
+ "Webhook permanently failed after %d attempts: url=%s",
237
+ MAX_RETRIES,
238
+ url,
239
+ )
File without changes
@@ -0,0 +1,172 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """SuperLocalMemory V3 — Adaptive Learning (3-Phase).
6
+
7
+ Learns optimal retrieval weights from user feedback.
8
+ Ported from V2.8 LightGBM-based learning system.
9
+ Profile-scoped: each profile learns independently.
10
+
11
+ Phase 1: Collect feedback (ranking_feedback)
12
+ Phase 2: Train model on feedback patterns
13
+ Phase 3: Apply learned weights to retrieval
14
+
15
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import logging
22
+ from collections import defaultdict
23
+ from datetime import UTC, datetime
24
+
25
+ from superlocalmemory.storage.models import FeedbackRecord
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Minimum feedback records before training
30
+ _MIN_FEEDBACK_FOR_TRAINING = 20
31
+
32
+ # Default channel weights (before learning)
33
+ _DEFAULT_WEIGHTS = {
34
+ "semantic": 1.2,
35
+ "bm25": 1.0,
36
+ "entity_graph": 1.0,
37
+ "temporal": 0.8,
38
+ }
39
+
40
+
41
+ class AdaptiveLearner:
42
+ """3-phase adaptive learning for retrieval weight optimization.
43
+
44
+ Learns from user feedback which channels produce the best results
45
+ for different query types. Profile-scoped.
46
+ """
47
+
48
+ def __init__(self, db) -> None:
49
+ self._db = db
50
+ self._learned_weights: dict[str, dict[str, float]] = {}
51
+
52
+ # -- Phase 1: Collect feedback -----------------------------------------
53
+
54
+ def record_feedback(
55
+ self,
56
+ query: str,
57
+ fact_id: str,
58
+ feedback_type: str,
59
+ profile_id: str,
60
+ dwell_time_ms: int = 0,
61
+ ) -> FeedbackRecord:
62
+ """Record user feedback on a retrieval result.
63
+
64
+ feedback_type: "relevant", "irrelevant", "partial"
65
+ """
66
+ record = FeedbackRecord(
67
+ profile_id=profile_id,
68
+ query=query,
69
+ fact_id=fact_id,
70
+ feedback_type=feedback_type,
71
+ dwell_time_ms=dwell_time_ms,
72
+ timestamp=datetime.now(UTC).isoformat(),
73
+ )
74
+ self._db.execute(
75
+ "INSERT INTO feedback_records "
76
+ "(feedback_id, profile_id, query, fact_id, feedback_type, "
77
+ "dwell_time_ms, timestamp) VALUES (?,?,?,?,?,?,?)",
78
+ (record.feedback_id, record.profile_id, record.query,
79
+ record.fact_id, record.feedback_type, record.dwell_time_ms,
80
+ record.timestamp),
81
+ )
82
+ return record
83
+
84
+ def get_feedback_count(self, profile_id: str) -> int:
85
+ """Count feedback records for a profile."""
86
+ rows = self._db.execute(
87
+ "SELECT COUNT(*) AS c FROM feedback_records WHERE profile_id = ?",
88
+ (profile_id,),
89
+ )
90
+ return int(dict(rows[0])["c"]) if rows else 0
91
+
92
+ # -- Phase 2: Learn patterns -------------------------------------------
93
+
94
+ def train(self, profile_id: str) -> dict[str, dict[str, float]]:
95
+ """Learn optimal weights from feedback patterns.
96
+
97
+ Simple heuristic approach (LightGBM port deferred to production):
98
+ - Analyze which channels produced "relevant" results
99
+ - Boost channels that correlate with positive feedback
100
+ - Reduce channels that correlate with negative feedback
101
+ """
102
+ count = self.get_feedback_count(profile_id)
103
+ if count < _MIN_FEEDBACK_FOR_TRAINING:
104
+ logger.info(
105
+ "Only %d feedback records (need %d). Using defaults.",
106
+ count, _MIN_FEEDBACK_FOR_TRAINING,
107
+ )
108
+ return {}
109
+
110
+ rows = self._db.execute(
111
+ "SELECT query, fact_id, feedback_type FROM feedback_records "
112
+ "WHERE profile_id = ? ORDER BY timestamp DESC LIMIT 500",
113
+ (profile_id,),
114
+ )
115
+
116
+ # Count positive/negative per query pattern
117
+ positive_count = 0
118
+ negative_count = 0
119
+ for row in rows:
120
+ d = dict(row)
121
+ if d["feedback_type"] == "relevant":
122
+ positive_count += 1
123
+ elif d["feedback_type"] == "irrelevant":
124
+ negative_count += 1
125
+
126
+ # Simple relevance ratio → weight adjustment
127
+ if positive_count + negative_count == 0:
128
+ return {}
129
+
130
+ relevance_ratio = positive_count / (positive_count + negative_count)
131
+
132
+ # If retrieval is generally good (>70% relevant), trust current weights
133
+ # If poor (<50%), boost BM25 and entity (more precise channels)
134
+ if relevance_ratio < 0.5:
135
+ learned = {
136
+ "general": {
137
+ "semantic": 1.0,
138
+ "bm25": 1.5, # Boost precision
139
+ "entity_graph": 1.3, # Boost entity matching
140
+ "temporal": 0.8,
141
+ },
142
+ }
143
+ else:
144
+ learned = {
145
+ "general": dict(_DEFAULT_WEIGHTS),
146
+ }
147
+
148
+ self._learned_weights = learned
149
+ logger.info("Learned weights (ratio=%.2f): %s", relevance_ratio, learned)
150
+ return learned
151
+
152
+ # -- Phase 3: Apply weights --------------------------------------------
153
+
154
+ def get_weights(
155
+ self, query_type: str, profile_id: str
156
+ ) -> dict[str, float]:
157
+ """Get learned weights for a query type.
158
+
159
+ Falls back to defaults if no learned weights available.
160
+ """
161
+ if not self._learned_weights:
162
+ self.train(profile_id)
163
+
164
+ if query_type in self._learned_weights:
165
+ return self._learned_weights[query_type]
166
+ if "general" in self._learned_weights:
167
+ return self._learned_weights["general"]
168
+ return dict(_DEFAULT_WEIGHTS)
169
+
170
+ def is_trained(self, profile_id: str) -> bool:
171
+ """Check if the learner has enough data to provide learned weights."""
172
+ return self.get_feedback_count(profile_id) >= _MIN_FEEDBACK_FOR_TRAINING