superlocalmemory 2.8.6 → 3.0.1

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 +62 -48
  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
@@ -1,806 +0,0 @@
1
- #!/usr/bin/env python3
2
- # SPDX-License-Identifier: MIT
3
- # Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
4
- """
5
- FeedbackCollector -- Multi-channel feedback collection for the LightGBM re-ranker.
6
-
7
- Collects implicit and explicit relevance signals from three channels:
8
-
9
- 1. MCP -- ``memory_used`` tool with usefulness level (high/medium/low).
10
- 2. CLI -- ``slm useful <id> [<id>...]`` marks memories as helpful.
11
- 3. Dashboard -- click events with optional dwell-time tracking.
12
-
13
- Additionally tracks *passive decay*: memories that are repeatedly returned
14
- by recall but never receive a positive signal are assigned a 0.0 (negative)
15
- feedback entry, teaching the re-ranker to demote them.
16
-
17
- Privacy:
18
- - Full query text is NEVER stored.
19
- - Queries are hashed to SHA-256[:16] for grouping.
20
- - Top 3 keywords are extracted for loose thematic grouping only.
21
-
22
- All data is written to the ``ranking_feedback`` table in learning.db via
23
- the shared LearningDB instance.
24
-
25
- Research backing:
26
- - ADPMF (IPM 2024): privacy-preserving feedback for recommendation.
27
- - FCS LREC 2024: cold-start feedback bootstrapping.
28
- """
29
-
30
- import hashlib
31
- import logging
32
- import re
33
- import threading
34
- from collections import Counter
35
- from datetime import datetime
36
- from typing import Dict, List, Optional, Any
37
-
38
- logger = logging.getLogger("superlocalmemory.learning.feedback")
39
-
40
- # ---------------------------------------------------------------------------
41
- # Stopwords for keyword extraction (small, curated list -- no NLTK needed)
42
- # ---------------------------------------------------------------------------
43
- _STOPWORDS = frozenset({
44
- "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
45
- "have", "has", "had", "do", "does", "did", "will", "would", "shall",
46
- "should", "may", "might", "must", "can", "could",
47
- "i", "me", "my", "we", "our", "you", "your", "he", "she", "it",
48
- "they", "them", "their", "its", "this", "that", "these", "those",
49
- "what", "which", "who", "whom", "how", "when", "where", "why",
50
- "not", "no", "nor", "but", "or", "and", "if", "then", "so",
51
- "of", "in", "on", "at", "to", "for", "with", "from", "by",
52
- "about", "into", "through", "during", "before", "after",
53
- "above", "below", "between", "out", "off", "up", "down",
54
- "all", "each", "every", "both", "few", "more", "most", "some", "any",
55
- "such", "only", "same", "than", "too", "very",
56
- "just", "also", "now", "here", "there",
57
- })
58
-
59
- # Regex to split on non-alphanumeric (keeps words and numbers)
60
- _WORD_SPLIT = re.compile(r"[^a-zA-Z0-9]+")
61
-
62
-
63
- class FeedbackCollector:
64
- """
65
- Collects multi-channel relevance feedback for the adaptive re-ranker.
66
-
67
- Each signal maps to a numeric value used as a training label:
68
-
69
- mcp_used_high = 1.0 (strong positive)
70
- mcp_used_medium = 0.7
71
- mcp_used_low = 0.4
72
- cli_useful = 0.9
73
- dashboard_click = 0.8
74
- passive_decay = 0.0 (negative signal)
75
-
76
- Usage:
77
- from learning.learning_db import LearningDB
78
- collector = FeedbackCollector(learning_db=LearningDB())
79
-
80
- # MCP channel
81
- collector.record_memory_used(42, "how to deploy FastAPI", usefulness="high")
82
-
83
- # CLI channel
84
- collector.record_cli_useful([42, 87], "deploy fastapi")
85
-
86
- # Dashboard channel
87
- collector.record_dashboard_click(42, "deploy fastapi", dwell_time=12.5)
88
-
89
- # Passive decay (call periodically)
90
- collector.record_recall_results("deploy fastapi", [42, 87, 91])
91
- collector.compute_passive_decay()
92
- """
93
-
94
- # Signal type -> numeric label for re-ranker training
95
- SIGNAL_VALUES: Dict[str, float] = {
96
- "mcp_used_high": 1.0,
97
- "mcp_used_medium": 0.7,
98
- "mcp_used_low": 0.4,
99
- "cli_useful": 0.9,
100
- "dashboard_click": 0.8,
101
- "dashboard_thumbs_up": 1.0,
102
- "dashboard_thumbs_down": 0.0,
103
- "dashboard_pin": 1.0,
104
- "dashboard_dwell_positive": 0.7,
105
- "dashboard_dwell_negative": 0.1,
106
- "implicit_positive_timegap": 0.6,
107
- "implicit_negative_requick": 0.1,
108
- "implicit_positive_reaccess": 0.7,
109
- "implicit_positive_post_update": 0.8,
110
- "implicit_negative_post_delete": 0.0,
111
- "implicit_positive_cross_tool": 0.8,
112
- "passive_decay": 0.0,
113
- # v2.8.0: Outcome-based signals from behavioral learning
114
- "outcome_success": 1.0,
115
- "outcome_partial": 0.5,
116
- "outcome_failure": 0.0,
117
- "outcome_retry": 0.2,
118
- }
119
-
120
- # Usefulness string -> signal type mapping
121
- _USEFULNESS_MAP: Dict[str, str] = {
122
- "high": "mcp_used_high",
123
- "medium": "mcp_used_medium",
124
- "low": "mcp_used_low",
125
- }
126
-
127
- def __init__(self, learning_db: Optional[Any] = None):
128
- """
129
- Args:
130
- learning_db: LearningDB instance for persisting feedback.
131
- If None, auto-creates a LearningDB instance.
132
- """
133
- if learning_db is None:
134
- try:
135
- from .learning_db import LearningDB
136
- self.learning_db = LearningDB()
137
- except Exception:
138
- self.learning_db = None
139
- else:
140
- self.learning_db = learning_db
141
-
142
- # In-memory buffer for passive decay tracking.
143
- # Structure: {query_hash: {memory_id: times_returned_count}}
144
- # Protected by a lock since MCP/CLI/API may call concurrently.
145
- self._recall_buffer: Dict[str, Dict[int, int]] = {}
146
- self._recall_buffer_lock = threading.Lock()
147
-
148
- # Counter: total recall operations tracked (for decay threshold)
149
- self._recall_count: int = 0
150
-
151
- # ======================================================================
152
- # Channel 1: MCP -- memory_used tool
153
- # ======================================================================
154
-
155
- def record_memory_used(
156
- self,
157
- memory_id: int,
158
- query: str,
159
- usefulness: str = "high",
160
- source_tool: Optional[str] = None,
161
- rank_position: Optional[int] = None,
162
- ) -> Optional[int]:
163
- """
164
- Record that a memory was explicitly used after an MCP recall.
165
-
166
- Called by the ``memory_used`` MCP tool. This is the highest-quality
167
- feedback signal because the AI agent explicitly indicates it found
168
- the memory useful.
169
-
170
- Args:
171
- memory_id: ID of the used memory in memory.db.
172
- query: The original recall query (hashed, not stored raw).
173
- usefulness: "high", "medium", or "low".
174
- source_tool: Which tool originated the query (e.g. 'claude-desktop').
175
- rank_position: Position of the memory in the recall results (1-based).
176
-
177
- Returns:
178
- Row ID of the feedback record, or None on error.
179
- """
180
- if not query:
181
- logger.warning("record_memory_used called with empty query")
182
- return None
183
-
184
- # Validate usefulness level
185
- usefulness = usefulness.lower().strip()
186
- if usefulness not in self._USEFULNESS_MAP:
187
- logger.warning(
188
- "Invalid usefulness level '%s', defaulting to 'high'",
189
- usefulness,
190
- )
191
- usefulness = "high"
192
-
193
- signal_type = self._USEFULNESS_MAP[usefulness]
194
- signal_value = self.SIGNAL_VALUES[signal_type]
195
- query_hash = self._hash_query(query)
196
- keywords = self._extract_keywords(query)
197
-
198
- return self._store_feedback(
199
- query_hash=query_hash,
200
- query_keywords=keywords,
201
- memory_id=memory_id,
202
- signal_type=signal_type,
203
- signal_value=signal_value,
204
- channel="mcp",
205
- source_tool=source_tool,
206
- rank_position=rank_position,
207
- )
208
-
209
- # ======================================================================
210
- # Channel 2: CLI -- slm useful <id> [<id>...]
211
- # ======================================================================
212
-
213
- def record_cli_useful(
214
- self,
215
- memory_ids: List[int],
216
- query: str,
217
- ) -> List[Optional[int]]:
218
- """
219
- Record positive feedback from the CLI ``slm useful`` command.
220
-
221
- Stores a positive signal for each memory_id. The CLI typically
222
- captures the most recent recall query, so all IDs share the same
223
- query hash.
224
-
225
- Args:
226
- memory_ids: List of memory IDs the user marked as useful.
227
- query: The recall query that surfaced these memories.
228
-
229
- Returns:
230
- List of row IDs (one per memory_id), or None entries on error.
231
- """
232
- if not query:
233
- logger.warning("record_cli_useful called with empty query")
234
- return [None] * len(memory_ids)
235
-
236
- query_hash = self._hash_query(query)
237
- keywords = self._extract_keywords(query)
238
- signal_value = self.SIGNAL_VALUES["cli_useful"]
239
- row_ids: List[Optional[int]] = []
240
-
241
- for mid in memory_ids:
242
- row_id = self._store_feedback(
243
- query_hash=query_hash,
244
- query_keywords=keywords,
245
- memory_id=mid,
246
- signal_type="cli_useful",
247
- signal_value=signal_value,
248
- channel="cli",
249
- )
250
- row_ids.append(row_id)
251
-
252
- logger.info(
253
- "CLI useful: %d memories marked for query_hash=%s",
254
- len(memory_ids),
255
- query_hash,
256
- )
257
- return row_ids
258
-
259
- # ======================================================================
260
- # Channel 3: Dashboard -- click events
261
- # ======================================================================
262
-
263
- def record_dashboard_click(
264
- self,
265
- memory_id: int,
266
- query: str,
267
- dwell_time: Optional[float] = None,
268
- ) -> Optional[int]:
269
- """
270
- Record a dashboard click on a memory card in search results.
271
-
272
- Optionally includes dwell time (seconds the user spent viewing
273
- the expanded memory). Longer dwell times indicate higher relevance
274
- but this is captured as metadata, not reflected in signal_value
275
- (the re-ranker can learn from dwell_time as a feature).
276
-
277
- Args:
278
- memory_id: ID of the clicked memory.
279
- query: The search query active when the click happened.
280
- dwell_time: Seconds spent viewing the memory (optional).
281
-
282
- Returns:
283
- Row ID of the feedback record, or None on error.
284
- """
285
- if not query:
286
- logger.warning("record_dashboard_click called with empty query")
287
- return None
288
-
289
- query_hash = self._hash_query(query)
290
- keywords = self._extract_keywords(query)
291
- signal_value = self.SIGNAL_VALUES["dashboard_click"]
292
-
293
- return self._store_feedback(
294
- query_hash=query_hash,
295
- query_keywords=keywords,
296
- memory_id=memory_id,
297
- signal_type="dashboard_click",
298
- signal_value=signal_value,
299
- channel="dashboard",
300
- dwell_time=dwell_time,
301
- )
302
-
303
- # ======================================================================
304
- # Channel 4: Implicit Signals (v2.7.4 — auto-collected, zero user effort)
305
- # ======================================================================
306
-
307
- def record_implicit_signal(
308
- self,
309
- memory_id: int,
310
- query: str,
311
- signal_type: str,
312
- source_tool: Optional[str] = None,
313
- rank_position: Optional[int] = None,
314
- ) -> Optional[int]:
315
- """
316
- Record an implicit feedback signal inferred from user behavior.
317
-
318
- Called by the signal inference engine in mcp_server.py when it
319
- detects behavioral patterns (time gaps, re-queries, re-access, etc.).
320
-
321
- Args:
322
- memory_id: ID of the memory.
323
- query: The recall query (hashed, not stored raw).
324
- signal_type: One of the implicit_* signal types.
325
- source_tool: Which tool originated the query.
326
- rank_position: Where the memory appeared in results.
327
-
328
- Returns:
329
- Row ID of the feedback record, or None on error.
330
- """
331
- if not query or signal_type not in self.SIGNAL_VALUES:
332
- logger.warning(
333
- "record_implicit_signal: invalid query or signal_type=%s",
334
- signal_type,
335
- )
336
- return None
337
-
338
- signal_value = self.SIGNAL_VALUES[signal_type]
339
- query_hash = self._hash_query(query)
340
- keywords = self._extract_keywords(query)
341
-
342
- return self._store_feedback(
343
- query_hash=query_hash,
344
- query_keywords=keywords,
345
- memory_id=memory_id,
346
- signal_type=signal_type,
347
- signal_value=signal_value,
348
- channel="implicit",
349
- source_tool=source_tool,
350
- rank_position=rank_position,
351
- )
352
-
353
- def record_dashboard_feedback(
354
- self,
355
- memory_id: int,
356
- query: str,
357
- feedback_type: str,
358
- dwell_time: Optional[float] = None,
359
- ) -> Optional[int]:
360
- """
361
- Record explicit dashboard feedback (thumbs up/down, pin, dwell).
362
-
363
- Args:
364
- memory_id: ID of the memory.
365
- query: The search query active when feedback given.
366
- feedback_type: One of 'thumbs_up', 'thumbs_down', 'pin',
367
- 'dwell_positive', 'dwell_negative'.
368
- dwell_time: Seconds spent viewing (for dwell signals).
369
-
370
- Returns:
371
- Row ID of the feedback record, or None on error.
372
- """
373
- type_map = {
374
- "thumbs_up": "dashboard_thumbs_up",
375
- "thumbs_down": "dashboard_thumbs_down",
376
- "pin": "dashboard_pin",
377
- "dwell_positive": "dashboard_dwell_positive",
378
- "dwell_negative": "dashboard_dwell_negative",
379
- }
380
-
381
- signal_type = type_map.get(feedback_type)
382
- if not signal_type or signal_type not in self.SIGNAL_VALUES:
383
- logger.warning(
384
- "record_dashboard_feedback: invalid feedback_type=%s",
385
- feedback_type,
386
- )
387
- return None
388
-
389
- if not query:
390
- query = f"__dashboard__:{memory_id}"
391
-
392
- signal_value = self.SIGNAL_VALUES[signal_type]
393
- query_hash = self._hash_query(query)
394
- keywords = self._extract_keywords(query)
395
-
396
- return self._store_feedback(
397
- query_hash=query_hash,
398
- query_keywords=keywords,
399
- memory_id=memory_id,
400
- signal_type=signal_type,
401
- signal_value=signal_value,
402
- channel="dashboard",
403
- dwell_time=dwell_time,
404
- )
405
-
406
- # ======================================================================
407
- # Passive Decay Tracking
408
- # ======================================================================
409
-
410
- def record_recall_results(
411
- self,
412
- query: str,
413
- returned_ids: List[int],
414
- ) -> None:
415
- """
416
- Track which memories were returned in a recall operation.
417
-
418
- This does NOT create feedback records immediately. Instead, it
419
- populates an in-memory buffer. When ``compute_passive_decay()``
420
- is called (periodically), memories that were returned in 5+
421
- distinct queries but never received a positive signal get a
422
- passive_decay (0.0) feedback entry.
423
-
424
- Args:
425
- query: The recall query (hashed for grouping).
426
- returned_ids: Memory IDs returned by the recall.
427
- """
428
- if not query or not returned_ids:
429
- return
430
-
431
- query_hash = self._hash_query(query)
432
-
433
- with self._recall_buffer_lock:
434
- if query_hash not in self._recall_buffer:
435
- self._recall_buffer[query_hash] = {}
436
-
437
- for mid in returned_ids:
438
- self._recall_buffer[query_hash][mid] = (
439
- self._recall_buffer[query_hash].get(mid, 0) + 1
440
- )
441
-
442
- self._recall_count += 1
443
-
444
- def compute_passive_decay(self, threshold: int = 10) -> int:
445
- """
446
- Emit passive decay signals for memories that appear in results
447
- but are never explicitly marked as useful.
448
-
449
- Algorithm:
450
- 1. Only runs after *threshold* recall operations are tracked.
451
- 2. For each memory that appeared in 5+ distinct query hashes:
452
- a. Check if it has ANY positive feedback in ranking_feedback.
453
- b. If not, insert a passive_decay signal (value=0.0).
454
- 3. Clear the recall buffer after processing.
455
-
456
- This teaches the re-ranker: "this memory keeps showing up but
457
- nobody ever finds it useful -- demote it."
458
-
459
- Args:
460
- threshold: Minimum number of tracked recalls before running.
461
-
462
- Returns:
463
- Number of passive decay signals emitted.
464
- """
465
- with self._recall_buffer_lock:
466
- if self._recall_count < threshold:
467
- logger.debug(
468
- "Passive decay skipped: %d/%d recalls tracked",
469
- self._recall_count,
470
- threshold,
471
- )
472
- return 0
473
-
474
- # Build a map: memory_id -> set of distinct query_hashes it appeared in
475
- memory_query_counts: Dict[int, int] = {}
476
- for query_hash, mem_counts in self._recall_buffer.items():
477
- for mid in mem_counts:
478
- memory_query_counts[mid] = memory_query_counts.get(mid, 0) + 1
479
-
480
- # Find candidates: appeared in 5+ distinct queries
481
- candidates = [
482
- mid for mid, qcount in memory_query_counts.items()
483
- if qcount >= 5
484
- ]
485
-
486
- # Snapshot and clear buffer
487
- buffer_snapshot = dict(self._recall_buffer)
488
- self._recall_buffer.clear()
489
- self._recall_count = 0
490
-
491
- if not candidates:
492
- logger.debug("No passive decay candidates found")
493
- return 0
494
-
495
- # Check which candidates have positive feedback already
496
- decay_count = 0
497
- for mid in candidates:
498
- if self._has_positive_feedback(mid):
499
- continue
500
-
501
- # Emit passive decay signal. Use a synthetic query hash
502
- # derived from the memory_id to group decay signals.
503
- decay_hash = self._hash_query(f"__passive_decay__:{mid}")
504
- self._store_feedback(
505
- query_hash=decay_hash,
506
- query_keywords=None,
507
- memory_id=mid,
508
- signal_type="passive_decay",
509
- signal_value=self.SIGNAL_VALUES["passive_decay"],
510
- channel="system",
511
- )
512
- decay_count += 1
513
-
514
- if decay_count > 0:
515
- logger.info(
516
- "Passive decay: emitted %d signals for %d candidates",
517
- decay_count,
518
- len(candidates),
519
- )
520
-
521
- return decay_count
522
-
523
- # ======================================================================
524
- # Summary & Diagnostics
525
- # ======================================================================
526
-
527
- def get_feedback_summary(self) -> dict:
528
- """
529
- Return summary statistics for display in CLI or dashboard.
530
-
531
- Returns:
532
- {
533
- 'total_signals': 142,
534
- 'unique_queries': 38,
535
- 'by_channel': {'mcp': 80, 'cli': 35, 'dashboard': 20, 'system': 7},
536
- 'by_type': {'mcp_used_high': 50, 'cli_useful': 35, ...},
537
- 'passive_decay_pending': 12,
538
- 'recall_buffer_size': 45,
539
- }
540
- """
541
- summary: Dict[str, Any] = {
542
- "total_signals": 0,
543
- "unique_queries": 0,
544
- "by_channel": {},
545
- "by_type": {},
546
- "passive_decay_pending": 0,
547
- "recall_buffer_size": 0,
548
- }
549
-
550
- # Buffer stats (always available, even without DB)
551
- with self._recall_buffer_lock:
552
- summary["recall_buffer_size"] = self._recall_count
553
- # Count memories that would be decay candidates
554
- memory_query_counts: Dict[int, int] = {}
555
- for _qh, mem_counts in self._recall_buffer.items():
556
- for mid in mem_counts:
557
- memory_query_counts[mid] = memory_query_counts.get(mid, 0) + 1
558
- summary["passive_decay_pending"] = sum(
559
- 1 for qcount in memory_query_counts.values() if qcount >= 5
560
- )
561
-
562
- if self.learning_db is None:
563
- summary["error"] = "No learning database connected"
564
- return summary
565
-
566
- try:
567
- conn = self.learning_db._get_connection()
568
- try:
569
- cursor = conn.cursor()
570
-
571
- # Total signals
572
- cursor.execute("SELECT COUNT(*) FROM ranking_feedback")
573
- summary["total_signals"] = cursor.fetchone()[0]
574
-
575
- # Unique queries
576
- cursor.execute(
577
- "SELECT COUNT(DISTINCT query_hash) FROM ranking_feedback"
578
- )
579
- summary["unique_queries"] = cursor.fetchone()[0]
580
-
581
- # By channel
582
- cursor.execute(
583
- "SELECT channel, COUNT(*) as cnt "
584
- "FROM ranking_feedback GROUP BY channel"
585
- )
586
- summary["by_channel"] = {
587
- row["channel"]: row["cnt"] for row in cursor.fetchall()
588
- }
589
-
590
- # By signal type
591
- cursor.execute(
592
- "SELECT signal_type, COUNT(*) as cnt "
593
- "FROM ranking_feedback GROUP BY signal_type"
594
- )
595
- summary["by_type"] = {
596
- row["signal_type"]: row["cnt"] for row in cursor.fetchall()
597
- }
598
-
599
- finally:
600
- conn.close()
601
-
602
- except Exception as e:
603
- logger.error("Failed to get feedback summary: %s", e)
604
- summary["error"] = str(e)
605
-
606
- return summary
607
-
608
- # ======================================================================
609
- # Internal helpers
610
- # ======================================================================
611
-
612
- def _hash_query(self, query: str) -> str:
613
- """
614
- Privacy-preserving query hash.
615
-
616
- Returns the first 16 hex characters of the SHA-256 digest.
617
- This is sufficient for grouping without being reversible.
618
- """
619
- return hashlib.sha256(query.encode("utf-8")).hexdigest()[:16]
620
-
621
- def _extract_keywords(self, query: str, top_n: int = 3) -> str:
622
- """
623
- Extract the top N meaningful words from a query string.
624
-
625
- Removes stopwords and short tokens (< 2 chars), then returns
626
- the most frequent remaining words as a comma-separated string.
627
-
628
- Args:
629
- query: Raw query text.
630
- top_n: Number of keywords to extract.
631
-
632
- Returns:
633
- Comma-separated keyword string (e.g. "deploy,fastapi,docker").
634
- Empty string if no keywords extracted.
635
- """
636
- if not query:
637
- return ""
638
-
639
- words = _WORD_SPLIT.split(query.lower())
640
- # Filter stopwords and short tokens
641
- meaningful = [w for w in words if w and len(w) >= 2 and w not in _STOPWORDS]
642
-
643
- if not meaningful:
644
- return ""
645
-
646
- # Most common N words (preserves order of first occurrence for ties)
647
- counts = Counter(meaningful)
648
- top_words = [word for word, _count in counts.most_common(top_n)]
649
- return ",".join(top_words)
650
-
651
- def _store_feedback(
652
- self,
653
- query_hash: str,
654
- query_keywords: Optional[str],
655
- memory_id: int,
656
- signal_type: str,
657
- signal_value: float,
658
- channel: str,
659
- source_tool: Optional[str] = None,
660
- rank_position: Optional[int] = None,
661
- dwell_time: Optional[float] = None,
662
- ) -> Optional[int]:
663
- """
664
- Persist a single feedback record via LearningDB.
665
-
666
- Also updates the daily engagement metric counter.
667
-
668
- Returns:
669
- Row ID on success, None on failure or if no DB is available.
670
- """
671
- if self.learning_db is None:
672
- logger.debug(
673
- "Feedback not stored (no DB): memory=%d, type=%s",
674
- memory_id,
675
- signal_type,
676
- )
677
- return None
678
-
679
- try:
680
- row_id = self.learning_db.store_feedback(
681
- query_hash=query_hash,
682
- memory_id=memory_id,
683
- signal_type=signal_type,
684
- signal_value=signal_value,
685
- channel=channel,
686
- query_keywords=query_keywords,
687
- rank_position=rank_position,
688
- source_tool=source_tool,
689
- dwell_time=dwell_time,
690
- )
691
-
692
- # Update daily engagement counter (best-effort)
693
- try:
694
- self.learning_db.increment_engagement(
695
- "feedback_signals",
696
- count=1,
697
- source=source_tool,
698
- )
699
- except Exception:
700
- pass
701
-
702
- return row_id
703
-
704
- except Exception as e:
705
- logger.error(
706
- "Failed to store feedback for memory %d: %s",
707
- memory_id,
708
- e,
709
- )
710
- return None
711
-
712
- def _has_positive_feedback(self, memory_id: int) -> bool:
713
- """
714
- Check if a memory has ANY positive feedback in learning.db.
715
-
716
- Positive = signal_value > 0.0 (anything above passive_decay).
717
- Used by passive decay to avoid penalising memories that were
718
- actually found useful at some point.
719
-
720
- Args:
721
- memory_id: Memory ID to check.
722
-
723
- Returns:
724
- True if at least one positive feedback record exists.
725
- """
726
- if self.learning_db is None:
727
- return False
728
-
729
- try:
730
- conn = self.learning_db._get_connection()
731
- try:
732
- cursor = conn.cursor()
733
- cursor.execute(
734
- """
735
- SELECT COUNT(*) FROM ranking_feedback
736
- WHERE memory_id = ? AND signal_value > 0.0
737
- """,
738
- (memory_id,),
739
- )
740
- count = cursor.fetchone()[0]
741
- return count > 0
742
- finally:
743
- conn.close()
744
- except Exception as e:
745
- logger.error(
746
- "Failed to check positive feedback for memory %d: %s",
747
- memory_id,
748
- e,
749
- )
750
- # If we can't check, assume positive to avoid false penalisation
751
- return True
752
-
753
-
754
- # ======================================================================
755
- # Standalone execution (for diagnostics: python3 feedback_collector.py)
756
- # ======================================================================
757
-
758
- def main():
759
- """Print feedback summary from CLI."""
760
- import sys
761
-
762
- logging.basicConfig(
763
- level=logging.INFO,
764
- format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
765
- )
766
-
767
- # Try to get LearningDB
768
- learning_db = None
769
- try:
770
- sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent))
771
- from learning_db import LearningDB
772
- learning_db = LearningDB()
773
- except ImportError:
774
- logger.warning("LearningDB not available")
775
-
776
- collector = FeedbackCollector(learning_db=learning_db)
777
- summary = collector.get_feedback_summary()
778
-
779
- print(f"\n{'='*60}")
780
- print(f" Feedback Summary")
781
- print(f"{'='*60}")
782
- print(f" Total signals: {summary.get('total_signals', 0)}")
783
- print(f" Unique queries: {summary.get('unique_queries', 0)}")
784
- print(f" Recall buffer size: {summary.get('recall_buffer_size', 0)}")
785
- print(f" Decay pending: {summary.get('passive_decay_pending', 0)}")
786
-
787
- by_channel = summary.get("by_channel", {})
788
- if by_channel:
789
- print(f"\n By Channel:")
790
- for ch, cnt in sorted(by_channel.items()):
791
- print(f" {ch:>12s}: {cnt}")
792
-
793
- by_type = summary.get("by_type", {})
794
- if by_type:
795
- print(f"\n By Signal Type:")
796
- for st, cnt in sorted(by_type.items()):
797
- print(f" {st:>18s}: {cnt}")
798
-
799
- if "error" in summary:
800
- print(f"\n Error: {summary['error']}")
801
-
802
- print()
803
-
804
-
805
- if __name__ == "__main__":
806
- main()