superlocalmemory 2.8.5 → 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 (434) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +9 -1
  3. package/NOTICE +63 -0
  4. package/README.md +165 -480
  5. package/bin/slm +17 -449
  6. package/bin/slm-npm +2 -2
  7. package/bin/slm.bat +4 -2
  8. package/conftest.py +5 -0
  9. package/docs/api-reference.md +284 -0
  10. package/docs/architecture.md +149 -0
  11. package/docs/auto-memory.md +150 -0
  12. package/docs/cli-reference.md +276 -0
  13. package/docs/compliance.md +191 -0
  14. package/docs/configuration.md +182 -0
  15. package/docs/getting-started.md +102 -0
  16. package/docs/ide-setup.md +261 -0
  17. package/docs/mcp-tools.md +220 -0
  18. package/docs/migration-from-v2.md +170 -0
  19. package/docs/profiles.md +173 -0
  20. package/docs/troubleshooting.md +310 -0
  21. package/{configs → ide/configs}/antigravity-mcp.json +3 -3
  22. package/ide/configs/chatgpt-desktop-mcp.json +16 -0
  23. package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
  24. package/{configs → ide/configs}/codex-mcp.toml +4 -4
  25. package/{configs → ide/configs}/continue-mcp.yaml +4 -3
  26. package/{configs → ide/configs}/continue-skills.yaml +6 -6
  27. package/ide/configs/cursor-mcp.json +15 -0
  28. package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
  29. package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
  30. package/{configs → ide/configs}/opencode-mcp.json +2 -2
  31. package/{configs → ide/configs}/perplexity-mcp.json +2 -2
  32. package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
  33. package/{configs → ide/configs}/windsurf-mcp.json +3 -3
  34. package/{configs → ide/configs}/zed-mcp.json +2 -2
  35. package/{hooks → ide/hooks}/context-hook.js +9 -20
  36. package/ide/hooks/memory-list-skill.js +70 -0
  37. package/ide/hooks/memory-profile-skill.js +101 -0
  38. package/ide/hooks/memory-recall-skill.js +62 -0
  39. package/ide/hooks/memory-remember-skill.js +68 -0
  40. package/ide/hooks/memory-reset-skill.js +160 -0
  41. package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
  42. package/ide/integrations/langchain/README.md +106 -0
  43. package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
  44. package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
  45. package/ide/integrations/langchain/pyproject.toml +38 -0
  46. package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
  47. package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
  48. package/ide/integrations/langchain/tests/test_security.py +117 -0
  49. package/ide/integrations/llamaindex/README.md +81 -0
  50. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
  51. package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
  52. package/ide/integrations/llamaindex/pyproject.toml +43 -0
  53. package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
  54. package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
  55. package/ide/integrations/llamaindex/tests/test_security.py +241 -0
  56. package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
  57. package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
  58. package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
  59. package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
  60. package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
  61. package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
  62. package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
  63. package/package.json +13 -22
  64. package/pyproject.toml +85 -0
  65. package/scripts/build-dmg.sh +417 -0
  66. package/scripts/install-skills.ps1 +334 -0
  67. package/{install.ps1 → scripts/install.ps1} +36 -4
  68. package/{install.sh → scripts/install.sh} +14 -13
  69. package/scripts/postinstall.js +2 -2
  70. package/scripts/start-dashboard.ps1 +52 -0
  71. package/scripts/start-dashboard.sh +41 -0
  72. package/scripts/sync-wiki.ps1 +127 -0
  73. package/scripts/sync-wiki.sh +82 -0
  74. package/scripts/test-dmg.sh +161 -0
  75. package/scripts/test-npm-package.ps1 +252 -0
  76. package/scripts/test-npm-package.sh +207 -0
  77. package/scripts/verify-install.ps1 +294 -0
  78. package/scripts/verify-install.sh +266 -0
  79. package/src/superlocalmemory/__init__.py +0 -0
  80. package/src/superlocalmemory/attribution/__init__.py +9 -0
  81. package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
  82. package/src/superlocalmemory/attribution/signer.py +153 -0
  83. package/src/superlocalmemory/attribution/watermark.py +189 -0
  84. package/src/superlocalmemory/cli/__init__.py +5 -0
  85. package/src/superlocalmemory/cli/commands.py +245 -0
  86. package/src/superlocalmemory/cli/main.py +89 -0
  87. package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
  88. package/src/superlocalmemory/cli/post_install.py +99 -0
  89. package/src/superlocalmemory/cli/setup_wizard.py +129 -0
  90. package/src/superlocalmemory/compliance/__init__.py +0 -0
  91. package/src/superlocalmemory/compliance/abac.py +204 -0
  92. package/src/superlocalmemory/compliance/audit.py +314 -0
  93. package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
  94. package/src/superlocalmemory/compliance/gdpr.py +294 -0
  95. package/src/superlocalmemory/compliance/lifecycle.py +158 -0
  96. package/src/superlocalmemory/compliance/retention.py +232 -0
  97. package/src/superlocalmemory/compliance/scheduler.py +148 -0
  98. package/src/superlocalmemory/core/__init__.py +0 -0
  99. package/src/superlocalmemory/core/config.py +391 -0
  100. package/src/superlocalmemory/core/embeddings.py +293 -0
  101. package/src/superlocalmemory/core/engine.py +701 -0
  102. package/src/superlocalmemory/core/hooks.py +65 -0
  103. package/src/superlocalmemory/core/maintenance.py +172 -0
  104. package/src/superlocalmemory/core/modes.py +140 -0
  105. package/src/superlocalmemory/core/profiles.py +234 -0
  106. package/src/superlocalmemory/core/registry.py +117 -0
  107. package/src/superlocalmemory/dynamics/__init__.py +0 -0
  108. package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
  109. package/src/superlocalmemory/encoding/__init__.py +0 -0
  110. package/src/superlocalmemory/encoding/consolidator.py +485 -0
  111. package/src/superlocalmemory/encoding/emotional.py +125 -0
  112. package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
  113. package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
  114. package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
  115. package/src/superlocalmemory/encoding/foresight.py +91 -0
  116. package/src/superlocalmemory/encoding/graph_builder.py +302 -0
  117. package/src/superlocalmemory/encoding/observation_builder.py +160 -0
  118. package/src/superlocalmemory/encoding/scene_builder.py +183 -0
  119. package/src/superlocalmemory/encoding/signal_inference.py +90 -0
  120. package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
  121. package/src/superlocalmemory/encoding/type_router.py +235 -0
  122. package/src/superlocalmemory/hooks/__init__.py +3 -0
  123. package/src/superlocalmemory/hooks/auto_capture.py +111 -0
  124. package/src/superlocalmemory/hooks/auto_recall.py +93 -0
  125. package/src/superlocalmemory/hooks/ide_connector.py +204 -0
  126. package/src/superlocalmemory/hooks/rules_engine.py +99 -0
  127. package/src/superlocalmemory/infra/__init__.py +3 -0
  128. package/src/superlocalmemory/infra/auth_middleware.py +82 -0
  129. package/src/superlocalmemory/infra/backup.py +317 -0
  130. package/src/superlocalmemory/infra/cache_manager.py +267 -0
  131. package/src/superlocalmemory/infra/event_bus.py +381 -0
  132. package/src/superlocalmemory/infra/rate_limiter.py +135 -0
  133. package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
  134. package/src/superlocalmemory/learning/__init__.py +0 -0
  135. package/src/superlocalmemory/learning/adaptive.py +172 -0
  136. package/src/superlocalmemory/learning/behavioral.py +490 -0
  137. package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
  138. package/src/superlocalmemory/learning/bootstrap.py +298 -0
  139. package/src/superlocalmemory/learning/cross_project.py +399 -0
  140. package/src/superlocalmemory/learning/database.py +376 -0
  141. package/src/superlocalmemory/learning/engagement.py +323 -0
  142. package/src/superlocalmemory/learning/features.py +138 -0
  143. package/src/superlocalmemory/learning/feedback.py +316 -0
  144. package/src/superlocalmemory/learning/outcomes.py +255 -0
  145. package/src/superlocalmemory/learning/project_context.py +366 -0
  146. package/src/superlocalmemory/learning/ranker.py +155 -0
  147. package/src/superlocalmemory/learning/source_quality.py +303 -0
  148. package/src/superlocalmemory/learning/workflows.py +309 -0
  149. package/src/superlocalmemory/llm/__init__.py +0 -0
  150. package/src/superlocalmemory/llm/backbone.py +316 -0
  151. package/src/superlocalmemory/math/__init__.py +0 -0
  152. package/src/superlocalmemory/math/fisher.py +356 -0
  153. package/src/superlocalmemory/math/langevin.py +398 -0
  154. package/src/superlocalmemory/math/sheaf.py +257 -0
  155. package/src/superlocalmemory/mcp/__init__.py +0 -0
  156. package/src/superlocalmemory/mcp/resources.py +245 -0
  157. package/src/superlocalmemory/mcp/server.py +61 -0
  158. package/src/superlocalmemory/mcp/tools.py +18 -0
  159. package/src/superlocalmemory/mcp/tools_core.py +305 -0
  160. package/src/superlocalmemory/mcp/tools_v28.py +223 -0
  161. package/src/superlocalmemory/mcp/tools_v3.py +286 -0
  162. package/src/superlocalmemory/retrieval/__init__.py +0 -0
  163. package/src/superlocalmemory/retrieval/agentic.py +295 -0
  164. package/src/superlocalmemory/retrieval/ann_index.py +223 -0
  165. package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
  166. package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
  167. package/src/superlocalmemory/retrieval/engine.py +390 -0
  168. package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
  169. package/src/superlocalmemory/retrieval/fusion.py +78 -0
  170. package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
  171. package/src/superlocalmemory/retrieval/reranker.py +154 -0
  172. package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
  173. package/src/superlocalmemory/retrieval/strategy.py +96 -0
  174. package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
  175. package/src/superlocalmemory/server/__init__.py +1 -0
  176. package/src/superlocalmemory/server/api.py +248 -0
  177. package/src/superlocalmemory/server/routes/__init__.py +4 -0
  178. package/src/superlocalmemory/server/routes/agents.py +107 -0
  179. package/src/superlocalmemory/server/routes/backup.py +91 -0
  180. package/src/superlocalmemory/server/routes/behavioral.py +127 -0
  181. package/src/superlocalmemory/server/routes/compliance.py +160 -0
  182. package/src/superlocalmemory/server/routes/data_io.py +188 -0
  183. package/src/superlocalmemory/server/routes/events.py +183 -0
  184. package/src/superlocalmemory/server/routes/helpers.py +85 -0
  185. package/src/superlocalmemory/server/routes/learning.py +273 -0
  186. package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
  187. package/src/superlocalmemory/server/routes/memories.py +399 -0
  188. package/src/superlocalmemory/server/routes/profiles.py +219 -0
  189. package/src/superlocalmemory/server/routes/stats.py +346 -0
  190. package/src/superlocalmemory/server/routes/v3_api.py +365 -0
  191. package/src/superlocalmemory/server/routes/ws.py +82 -0
  192. package/src/superlocalmemory/server/security_middleware.py +57 -0
  193. package/src/superlocalmemory/server/ui.py +245 -0
  194. package/src/superlocalmemory/storage/__init__.py +0 -0
  195. package/src/superlocalmemory/storage/access_control.py +182 -0
  196. package/src/superlocalmemory/storage/database.py +594 -0
  197. package/src/superlocalmemory/storage/migrations.py +303 -0
  198. package/src/superlocalmemory/storage/models.py +406 -0
  199. package/src/superlocalmemory/storage/schema.py +726 -0
  200. package/src/superlocalmemory/storage/v2_migrator.py +317 -0
  201. package/src/superlocalmemory/trust/__init__.py +0 -0
  202. package/src/superlocalmemory/trust/gate.py +130 -0
  203. package/src/superlocalmemory/trust/provenance.py +124 -0
  204. package/src/superlocalmemory/trust/scorer.py +347 -0
  205. package/src/superlocalmemory/trust/signals.py +153 -0
  206. package/ui/index.html +278 -5
  207. package/ui/js/auto-settings.js +70 -0
  208. package/ui/js/dashboard.js +90 -0
  209. package/ui/js/fact-detail.js +92 -0
  210. package/ui/js/feedback.js +2 -2
  211. package/ui/js/ide-status.js +102 -0
  212. package/ui/js/math-health.js +98 -0
  213. package/ui/js/recall-lab.js +127 -0
  214. package/ui/js/settings.js +2 -2
  215. package/ui/js/trust-dashboard.js +73 -0
  216. package/api_server.py +0 -724
  217. package/bin/aider-smart +0 -72
  218. package/bin/superlocalmemoryv2-learning +0 -4
  219. package/bin/superlocalmemoryv2-list +0 -3
  220. package/bin/superlocalmemoryv2-patterns +0 -4
  221. package/bin/superlocalmemoryv2-profile +0 -3
  222. package/bin/superlocalmemoryv2-recall +0 -3
  223. package/bin/superlocalmemoryv2-remember +0 -3
  224. package/bin/superlocalmemoryv2-reset +0 -3
  225. package/bin/superlocalmemoryv2-status +0 -3
  226. package/configs/chatgpt-desktop-mcp.json +0 -16
  227. package/configs/cursor-mcp.json +0 -15
  228. package/docs/SECURITY-QUICK-REFERENCE.md +0 -214
  229. package/hooks/memory-list-skill.js +0 -139
  230. package/hooks/memory-profile-skill.js +0 -273
  231. package/hooks/memory-recall-skill.js +0 -114
  232. package/hooks/memory-remember-skill.js +0 -127
  233. package/hooks/memory-reset-skill.js +0 -274
  234. package/mcp_server.py +0 -1800
  235. package/requirements-core.txt +0 -22
  236. package/requirements-learning.txt +0 -12
  237. package/requirements.txt +0 -12
  238. package/src/agent_registry.py +0 -411
  239. package/src/auth_middleware.py +0 -61
  240. package/src/auto_backup.py +0 -459
  241. package/src/behavioral/__init__.py +0 -49
  242. package/src/behavioral/behavioral_listener.py +0 -203
  243. package/src/behavioral/behavioral_patterns.py +0 -275
  244. package/src/behavioral/cross_project_transfer.py +0 -206
  245. package/src/behavioral/outcome_inference.py +0 -194
  246. package/src/behavioral/outcome_tracker.py +0 -193
  247. package/src/behavioral/tests/__init__.py +0 -4
  248. package/src/behavioral/tests/test_behavioral_integration.py +0 -108
  249. package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
  250. package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
  251. package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
  252. package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
  253. package/src/behavioral/tests/test_outcome_inference.py +0 -107
  254. package/src/behavioral/tests/test_outcome_tracker.py +0 -96
  255. package/src/cache_manager.py +0 -518
  256. package/src/compliance/__init__.py +0 -48
  257. package/src/compliance/abac_engine.py +0 -149
  258. package/src/compliance/abac_middleware.py +0 -116
  259. package/src/compliance/audit_db.py +0 -215
  260. package/src/compliance/audit_logger.py +0 -148
  261. package/src/compliance/retention_manager.py +0 -289
  262. package/src/compliance/retention_scheduler.py +0 -186
  263. package/src/compliance/tests/__init__.py +0 -4
  264. package/src/compliance/tests/test_abac_enforcement.py +0 -95
  265. package/src/compliance/tests/test_abac_engine.py +0 -124
  266. package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
  267. package/src/compliance/tests/test_audit_db.py +0 -123
  268. package/src/compliance/tests/test_audit_logger.py +0 -98
  269. package/src/compliance/tests/test_mcp_audit.py +0 -128
  270. package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
  271. package/src/compliance/tests/test_retention_manager.py +0 -131
  272. package/src/compliance/tests/test_retention_scheduler.py +0 -99
  273. package/src/compression/__init__.py +0 -25
  274. package/src/compression/cli.py +0 -150
  275. package/src/compression/cold_storage.py +0 -217
  276. package/src/compression/config.py +0 -72
  277. package/src/compression/orchestrator.py +0 -133
  278. package/src/compression/tier2_compressor.py +0 -228
  279. package/src/compression/tier3_compressor.py +0 -153
  280. package/src/compression/tier_classifier.py +0 -148
  281. package/src/db_connection_manager.py +0 -536
  282. package/src/embedding_engine.py +0 -63
  283. package/src/embeddings/__init__.py +0 -47
  284. package/src/embeddings/cache.py +0 -70
  285. package/src/embeddings/cli.py +0 -113
  286. package/src/embeddings/constants.py +0 -47
  287. package/src/embeddings/database.py +0 -91
  288. package/src/embeddings/engine.py +0 -247
  289. package/src/embeddings/model_loader.py +0 -145
  290. package/src/event_bus.py +0 -562
  291. package/src/graph/__init__.py +0 -36
  292. package/src/graph/build_helpers.py +0 -74
  293. package/src/graph/cli.py +0 -87
  294. package/src/graph/cluster_builder.py +0 -188
  295. package/src/graph/cluster_summary.py +0 -148
  296. package/src/graph/constants.py +0 -47
  297. package/src/graph/edge_builder.py +0 -162
  298. package/src/graph/entity_extractor.py +0 -95
  299. package/src/graph/graph_core.py +0 -226
  300. package/src/graph/graph_search.py +0 -231
  301. package/src/graph/hierarchical.py +0 -207
  302. package/src/graph/schema.py +0 -99
  303. package/src/graph_engine.py +0 -52
  304. package/src/hnsw_index.py +0 -628
  305. package/src/hybrid_search.py +0 -46
  306. package/src/learning/__init__.py +0 -217
  307. package/src/learning/adaptive_ranker.py +0 -682
  308. package/src/learning/bootstrap/__init__.py +0 -69
  309. package/src/learning/bootstrap/constants.py +0 -93
  310. package/src/learning/bootstrap/db_queries.py +0 -316
  311. package/src/learning/bootstrap/sampling.py +0 -82
  312. package/src/learning/bootstrap/text_utils.py +0 -71
  313. package/src/learning/cross_project_aggregator.py +0 -857
  314. package/src/learning/db/__init__.py +0 -40
  315. package/src/learning/db/constants.py +0 -44
  316. package/src/learning/db/schema.py +0 -279
  317. package/src/learning/engagement_tracker.py +0 -628
  318. package/src/learning/feature_extractor.py +0 -708
  319. package/src/learning/feedback_collector.py +0 -806
  320. package/src/learning/learning_db.py +0 -915
  321. package/src/learning/project_context_manager.py +0 -572
  322. package/src/learning/ranking/__init__.py +0 -33
  323. package/src/learning/ranking/constants.py +0 -84
  324. package/src/learning/ranking/helpers.py +0 -278
  325. package/src/learning/source_quality_scorer.py +0 -676
  326. package/src/learning/synthetic_bootstrap.py +0 -755
  327. package/src/learning/tests/test_adaptive_ranker.py +0 -325
  328. package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
  329. package/src/learning/tests/test_aggregator.py +0 -306
  330. package/src/learning/tests/test_auto_retrain_v28.py +0 -35
  331. package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
  332. package/src/learning/tests/test_feature_extractor_v28.py +0 -93
  333. package/src/learning/tests/test_feedback_collector.py +0 -294
  334. package/src/learning/tests/test_learning_db.py +0 -602
  335. package/src/learning/tests/test_learning_db_v28.py +0 -110
  336. package/src/learning/tests/test_learning_init_v28.py +0 -48
  337. package/src/learning/tests/test_outcome_signals.py +0 -48
  338. package/src/learning/tests/test_project_context.py +0 -292
  339. package/src/learning/tests/test_schema_migration.py +0 -319
  340. package/src/learning/tests/test_signal_inference.py +0 -397
  341. package/src/learning/tests/test_source_quality.py +0 -351
  342. package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
  343. package/src/learning/tests/test_workflow_miner.py +0 -318
  344. package/src/learning/workflow_pattern_miner.py +0 -655
  345. package/src/lifecycle/__init__.py +0 -54
  346. package/src/lifecycle/bounded_growth.py +0 -239
  347. package/src/lifecycle/compaction_engine.py +0 -226
  348. package/src/lifecycle/lifecycle_engine.py +0 -355
  349. package/src/lifecycle/lifecycle_evaluator.py +0 -257
  350. package/src/lifecycle/lifecycle_scheduler.py +0 -130
  351. package/src/lifecycle/retention_policy.py +0 -285
  352. package/src/lifecycle/tests/test_bounded_growth.py +0 -193
  353. package/src/lifecycle/tests/test_compaction.py +0 -179
  354. package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
  355. package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
  356. package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
  357. package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
  358. package/src/lifecycle/tests/test_mcp_compact.py +0 -149
  359. package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
  360. package/src/lifecycle/tests/test_retention_policy.py +0 -162
  361. package/src/mcp_tools_v28.py +0 -281
  362. package/src/memory/__init__.py +0 -36
  363. package/src/memory/cli.py +0 -205
  364. package/src/memory/constants.py +0 -39
  365. package/src/memory/helpers.py +0 -28
  366. package/src/memory/schema.py +0 -166
  367. package/src/memory-profiles.py +0 -595
  368. package/src/memory-reset.py +0 -491
  369. package/src/memory_compression.py +0 -989
  370. package/src/memory_store_v2.py +0 -1155
  371. package/src/migrate_v1_to_v2.py +0 -629
  372. package/src/pattern_learner.py +0 -34
  373. package/src/patterns/__init__.py +0 -24
  374. package/src/patterns/analyzers.py +0 -251
  375. package/src/patterns/learner.py +0 -271
  376. package/src/patterns/scoring.py +0 -171
  377. package/src/patterns/store.py +0 -225
  378. package/src/patterns/terminology.py +0 -140
  379. package/src/provenance_tracker.py +0 -312
  380. package/src/qualixar_attribution.py +0 -139
  381. package/src/qualixar_watermark.py +0 -78
  382. package/src/query_optimizer.py +0 -511
  383. package/src/rate_limiter.py +0 -83
  384. package/src/search/__init__.py +0 -20
  385. package/src/search/cli.py +0 -77
  386. package/src/search/constants.py +0 -26
  387. package/src/search/engine.py +0 -241
  388. package/src/search/fusion.py +0 -122
  389. package/src/search/index_loader.py +0 -114
  390. package/src/search/methods.py +0 -162
  391. package/src/search_engine_v2.py +0 -401
  392. package/src/setup_validator.py +0 -482
  393. package/src/subscription_manager.py +0 -391
  394. package/src/tree/__init__.py +0 -59
  395. package/src/tree/builder.py +0 -185
  396. package/src/tree/nodes.py +0 -202
  397. package/src/tree/queries.py +0 -257
  398. package/src/tree/schema.py +0 -80
  399. package/src/tree_manager.py +0 -19
  400. package/src/trust/__init__.py +0 -45
  401. package/src/trust/constants.py +0 -66
  402. package/src/trust/queries.py +0 -157
  403. package/src/trust/schema.py +0 -95
  404. package/src/trust/scorer.py +0 -299
  405. package/src/trust/signals.py +0 -95
  406. package/src/trust_scorer.py +0 -44
  407. package/ui/app.js +0 -1588
  408. package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
  409. package/ui/js/graph-cytoscape.js +0 -1168
  410. package/ui/js/graph-d3-backup.js +0 -32
  411. package/ui/js/graph.js +0 -32
  412. package/ui_server.py +0 -266
  413. /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
  414. /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
  415. /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
  416. /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
  417. /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
  418. /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
  419. /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
  420. /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
  421. /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
  422. /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
  423. /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
  424. /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
  425. /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
  426. /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
  427. /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
  428. /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
  429. /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
  430. /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
  431. /package/{completions → ide/completions}/slm.bash +0 -0
  432. /package/{completions → ide/completions}/slm.zsh +0 -0
  433. /package/{configs → ide/configs}/cody-commands.json +0 -0
  434. /package/{install-skills.sh → scripts/install-skills.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