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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +9 -1
- package/NOTICE +63 -0
- package/README.md +165 -480
- package/bin/slm +17 -449
- package/bin/slm-npm +2 -2
- package/bin/slm.bat +4 -2
- package/conftest.py +5 -0
- package/docs/api-reference.md +284 -0
- package/docs/architecture.md +149 -0
- package/docs/auto-memory.md +150 -0
- package/docs/cli-reference.md +276 -0
- package/docs/compliance.md +191 -0
- package/docs/configuration.md +182 -0
- package/docs/getting-started.md +102 -0
- package/docs/ide-setup.md +261 -0
- package/docs/mcp-tools.md +220 -0
- package/docs/migration-from-v2.md +170 -0
- package/docs/profiles.md +173 -0
- package/docs/troubleshooting.md +310 -0
- package/{configs → ide/configs}/antigravity-mcp.json +3 -3
- package/ide/configs/chatgpt-desktop-mcp.json +16 -0
- package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
- package/{configs → ide/configs}/codex-mcp.toml +4 -4
- package/{configs → ide/configs}/continue-mcp.yaml +4 -3
- package/{configs → ide/configs}/continue-skills.yaml +6 -6
- package/ide/configs/cursor-mcp.json +15 -0
- package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
- package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
- package/{configs → ide/configs}/opencode-mcp.json +2 -2
- package/{configs → ide/configs}/perplexity-mcp.json +2 -2
- package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
- package/{configs → ide/configs}/windsurf-mcp.json +3 -3
- package/{configs → ide/configs}/zed-mcp.json +2 -2
- package/{hooks → ide/hooks}/context-hook.js +9 -20
- package/ide/hooks/memory-list-skill.js +70 -0
- package/ide/hooks/memory-profile-skill.js +101 -0
- package/ide/hooks/memory-recall-skill.js +62 -0
- package/ide/hooks/memory-remember-skill.js +68 -0
- package/ide/hooks/memory-reset-skill.js +160 -0
- package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
- package/ide/integrations/langchain/README.md +106 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
- package/ide/integrations/langchain/pyproject.toml +38 -0
- package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
- package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
- package/ide/integrations/langchain/tests/test_security.py +117 -0
- package/ide/integrations/llamaindex/README.md +81 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
- package/ide/integrations/llamaindex/pyproject.toml +43 -0
- package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
- package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
- package/ide/integrations/llamaindex/tests/test_security.py +241 -0
- package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
- package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
- package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
- package/package.json +13 -22
- package/pyproject.toml +85 -0
- package/scripts/build-dmg.sh +417 -0
- package/scripts/install-skills.ps1 +334 -0
- package/{install.ps1 → scripts/install.ps1} +36 -4
- package/{install.sh → scripts/install.sh} +14 -13
- package/scripts/postinstall.js +2 -2
- package/scripts/start-dashboard.ps1 +52 -0
- package/scripts/start-dashboard.sh +41 -0
- package/scripts/sync-wiki.ps1 +127 -0
- package/scripts/sync-wiki.sh +82 -0
- package/scripts/test-dmg.sh +161 -0
- package/scripts/test-npm-package.ps1 +252 -0
- package/scripts/test-npm-package.sh +207 -0
- package/scripts/verify-install.ps1 +294 -0
- package/scripts/verify-install.sh +266 -0
- package/src/superlocalmemory/__init__.py +0 -0
- package/src/superlocalmemory/attribution/__init__.py +9 -0
- package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
- package/src/superlocalmemory/attribution/signer.py +153 -0
- package/src/superlocalmemory/attribution/watermark.py +189 -0
- package/src/superlocalmemory/cli/__init__.py +5 -0
- package/src/superlocalmemory/cli/commands.py +245 -0
- package/src/superlocalmemory/cli/main.py +89 -0
- package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
- package/src/superlocalmemory/cli/post_install.py +99 -0
- package/src/superlocalmemory/cli/setup_wizard.py +129 -0
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +204 -0
- package/src/superlocalmemory/compliance/audit.py +314 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
- package/src/superlocalmemory/compliance/gdpr.py +294 -0
- package/src/superlocalmemory/compliance/lifecycle.py +158 -0
- package/src/superlocalmemory/compliance/retention.py +232 -0
- package/src/superlocalmemory/compliance/scheduler.py +148 -0
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/config.py +391 -0
- package/src/superlocalmemory/core/embeddings.py +293 -0
- package/src/superlocalmemory/core/engine.py +701 -0
- package/src/superlocalmemory/core/hooks.py +65 -0
- package/src/superlocalmemory/core/maintenance.py +172 -0
- package/src/superlocalmemory/core/modes.py +140 -0
- package/src/superlocalmemory/core/profiles.py +234 -0
- package/src/superlocalmemory/core/registry.py +117 -0
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/consolidator.py +485 -0
- package/src/superlocalmemory/encoding/emotional.py +125 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
- package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
- package/src/superlocalmemory/encoding/foresight.py +91 -0
- package/src/superlocalmemory/encoding/graph_builder.py +302 -0
- package/src/superlocalmemory/encoding/observation_builder.py +160 -0
- package/src/superlocalmemory/encoding/scene_builder.py +183 -0
- package/src/superlocalmemory/encoding/signal_inference.py +90 -0
- package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
- package/src/superlocalmemory/encoding/type_router.py +235 -0
- package/src/superlocalmemory/hooks/__init__.py +3 -0
- package/src/superlocalmemory/hooks/auto_capture.py +111 -0
- package/src/superlocalmemory/hooks/auto_recall.py +93 -0
- package/src/superlocalmemory/hooks/ide_connector.py +204 -0
- package/src/superlocalmemory/hooks/rules_engine.py +99 -0
- package/src/superlocalmemory/infra/__init__.py +3 -0
- package/src/superlocalmemory/infra/auth_middleware.py +82 -0
- package/src/superlocalmemory/infra/backup.py +317 -0
- package/src/superlocalmemory/infra/cache_manager.py +267 -0
- package/src/superlocalmemory/infra/event_bus.py +381 -0
- package/src/superlocalmemory/infra/rate_limiter.py +135 -0
- package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +172 -0
- package/src/superlocalmemory/learning/behavioral.py +490 -0
- package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
- package/src/superlocalmemory/learning/bootstrap.py +298 -0
- package/src/superlocalmemory/learning/cross_project.py +399 -0
- package/src/superlocalmemory/learning/database.py +376 -0
- package/src/superlocalmemory/learning/engagement.py +323 -0
- package/src/superlocalmemory/learning/features.py +138 -0
- package/src/superlocalmemory/learning/feedback.py +316 -0
- package/src/superlocalmemory/learning/outcomes.py +255 -0
- package/src/superlocalmemory/learning/project_context.py +366 -0
- package/src/superlocalmemory/learning/ranker.py +155 -0
- package/src/superlocalmemory/learning/source_quality.py +303 -0
- package/src/superlocalmemory/learning/workflows.py +309 -0
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +316 -0
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/fisher.py +356 -0
- package/src/superlocalmemory/math/langevin.py +398 -0
- package/src/superlocalmemory/math/sheaf.py +257 -0
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/resources.py +245 -0
- package/src/superlocalmemory/mcp/server.py +61 -0
- package/src/superlocalmemory/mcp/tools.py +18 -0
- package/src/superlocalmemory/mcp/tools_core.py +305 -0
- package/src/superlocalmemory/mcp/tools_v28.py +223 -0
- package/src/superlocalmemory/mcp/tools_v3.py +286 -0
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +295 -0
- package/src/superlocalmemory/retrieval/ann_index.py +223 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
- package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
- package/src/superlocalmemory/retrieval/engine.py +390 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
- package/src/superlocalmemory/retrieval/fusion.py +78 -0
- package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
- package/src/superlocalmemory/retrieval/reranker.py +154 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
- package/src/superlocalmemory/retrieval/strategy.py +96 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
- package/src/superlocalmemory/server/__init__.py +1 -0
- package/src/superlocalmemory/server/api.py +248 -0
- package/src/superlocalmemory/server/routes/__init__.py +4 -0
- package/src/superlocalmemory/server/routes/agents.py +107 -0
- package/src/superlocalmemory/server/routes/backup.py +91 -0
- package/src/superlocalmemory/server/routes/behavioral.py +127 -0
- package/src/superlocalmemory/server/routes/compliance.py +160 -0
- package/src/superlocalmemory/server/routes/data_io.py +188 -0
- package/src/superlocalmemory/server/routes/events.py +183 -0
- package/src/superlocalmemory/server/routes/helpers.py +85 -0
- package/src/superlocalmemory/server/routes/learning.py +273 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
- package/src/superlocalmemory/server/routes/memories.py +399 -0
- package/src/superlocalmemory/server/routes/profiles.py +219 -0
- package/src/superlocalmemory/server/routes/stats.py +346 -0
- package/src/superlocalmemory/server/routes/v3_api.py +365 -0
- package/src/superlocalmemory/server/routes/ws.py +82 -0
- package/src/superlocalmemory/server/security_middleware.py +57 -0
- package/src/superlocalmemory/server/ui.py +245 -0
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/access_control.py +182 -0
- package/src/superlocalmemory/storage/database.py +594 -0
- package/src/superlocalmemory/storage/migrations.py +303 -0
- package/src/superlocalmemory/storage/models.py +406 -0
- package/src/superlocalmemory/storage/schema.py +726 -0
- package/src/superlocalmemory/storage/v2_migrator.py +317 -0
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +130 -0
- package/src/superlocalmemory/trust/provenance.py +124 -0
- package/src/superlocalmemory/trust/scorer.py +347 -0
- package/src/superlocalmemory/trust/signals.py +153 -0
- package/ui/index.html +278 -5
- package/ui/js/auto-settings.js +70 -0
- package/ui/js/dashboard.js +90 -0
- package/ui/js/fact-detail.js +92 -0
- package/ui/js/feedback.js +2 -2
- package/ui/js/ide-status.js +102 -0
- package/ui/js/math-health.js +98 -0
- package/ui/js/recall-lab.js +127 -0
- package/ui/js/settings.js +2 -2
- package/ui/js/trust-dashboard.js +73 -0
- package/api_server.py +0 -724
- package/bin/aider-smart +0 -72
- package/bin/superlocalmemoryv2-learning +0 -4
- package/bin/superlocalmemoryv2-list +0 -3
- package/bin/superlocalmemoryv2-patterns +0 -4
- package/bin/superlocalmemoryv2-profile +0 -3
- package/bin/superlocalmemoryv2-recall +0 -3
- package/bin/superlocalmemoryv2-remember +0 -3
- package/bin/superlocalmemoryv2-reset +0 -3
- package/bin/superlocalmemoryv2-status +0 -3
- package/configs/chatgpt-desktop-mcp.json +0 -16
- package/configs/cursor-mcp.json +0 -15
- package/docs/SECURITY-QUICK-REFERENCE.md +0 -214
- package/hooks/memory-list-skill.js +0 -139
- package/hooks/memory-profile-skill.js +0 -273
- package/hooks/memory-recall-skill.js +0 -114
- package/hooks/memory-remember-skill.js +0 -127
- package/hooks/memory-reset-skill.js +0 -274
- package/mcp_server.py +0 -1800
- package/requirements-core.txt +0 -22
- package/requirements-learning.txt +0 -12
- package/requirements.txt +0 -12
- package/src/agent_registry.py +0 -411
- package/src/auth_middleware.py +0 -61
- package/src/auto_backup.py +0 -459
- package/src/behavioral/__init__.py +0 -49
- package/src/behavioral/behavioral_listener.py +0 -203
- package/src/behavioral/behavioral_patterns.py +0 -275
- package/src/behavioral/cross_project_transfer.py +0 -206
- package/src/behavioral/outcome_inference.py +0 -194
- package/src/behavioral/outcome_tracker.py +0 -193
- package/src/behavioral/tests/__init__.py +0 -4
- package/src/behavioral/tests/test_behavioral_integration.py +0 -108
- package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
- package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
- package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
- package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
- package/src/behavioral/tests/test_outcome_inference.py +0 -107
- package/src/behavioral/tests/test_outcome_tracker.py +0 -96
- package/src/cache_manager.py +0 -518
- package/src/compliance/__init__.py +0 -48
- package/src/compliance/abac_engine.py +0 -149
- package/src/compliance/abac_middleware.py +0 -116
- package/src/compliance/audit_db.py +0 -215
- package/src/compliance/audit_logger.py +0 -148
- package/src/compliance/retention_manager.py +0 -289
- package/src/compliance/retention_scheduler.py +0 -186
- package/src/compliance/tests/__init__.py +0 -4
- package/src/compliance/tests/test_abac_enforcement.py +0 -95
- package/src/compliance/tests/test_abac_engine.py +0 -124
- package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
- package/src/compliance/tests/test_audit_db.py +0 -123
- package/src/compliance/tests/test_audit_logger.py +0 -98
- package/src/compliance/tests/test_mcp_audit.py +0 -128
- package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
- package/src/compliance/tests/test_retention_manager.py +0 -131
- package/src/compliance/tests/test_retention_scheduler.py +0 -99
- package/src/compression/__init__.py +0 -25
- package/src/compression/cli.py +0 -150
- package/src/compression/cold_storage.py +0 -217
- package/src/compression/config.py +0 -72
- package/src/compression/orchestrator.py +0 -133
- package/src/compression/tier2_compressor.py +0 -228
- package/src/compression/tier3_compressor.py +0 -153
- package/src/compression/tier_classifier.py +0 -148
- package/src/db_connection_manager.py +0 -536
- package/src/embedding_engine.py +0 -63
- package/src/embeddings/__init__.py +0 -47
- package/src/embeddings/cache.py +0 -70
- package/src/embeddings/cli.py +0 -113
- package/src/embeddings/constants.py +0 -47
- package/src/embeddings/database.py +0 -91
- package/src/embeddings/engine.py +0 -247
- package/src/embeddings/model_loader.py +0 -145
- package/src/event_bus.py +0 -562
- package/src/graph/__init__.py +0 -36
- package/src/graph/build_helpers.py +0 -74
- package/src/graph/cli.py +0 -87
- package/src/graph/cluster_builder.py +0 -188
- package/src/graph/cluster_summary.py +0 -148
- package/src/graph/constants.py +0 -47
- package/src/graph/edge_builder.py +0 -162
- package/src/graph/entity_extractor.py +0 -95
- package/src/graph/graph_core.py +0 -226
- package/src/graph/graph_search.py +0 -231
- package/src/graph/hierarchical.py +0 -207
- package/src/graph/schema.py +0 -99
- package/src/graph_engine.py +0 -52
- package/src/hnsw_index.py +0 -628
- package/src/hybrid_search.py +0 -46
- package/src/learning/__init__.py +0 -217
- package/src/learning/adaptive_ranker.py +0 -682
- package/src/learning/bootstrap/__init__.py +0 -69
- package/src/learning/bootstrap/constants.py +0 -93
- package/src/learning/bootstrap/db_queries.py +0 -316
- package/src/learning/bootstrap/sampling.py +0 -82
- package/src/learning/bootstrap/text_utils.py +0 -71
- package/src/learning/cross_project_aggregator.py +0 -857
- package/src/learning/db/__init__.py +0 -40
- package/src/learning/db/constants.py +0 -44
- package/src/learning/db/schema.py +0 -279
- package/src/learning/engagement_tracker.py +0 -628
- package/src/learning/feature_extractor.py +0 -708
- package/src/learning/feedback_collector.py +0 -806
- package/src/learning/learning_db.py +0 -915
- package/src/learning/project_context_manager.py +0 -572
- package/src/learning/ranking/__init__.py +0 -33
- package/src/learning/ranking/constants.py +0 -84
- package/src/learning/ranking/helpers.py +0 -278
- package/src/learning/source_quality_scorer.py +0 -676
- package/src/learning/synthetic_bootstrap.py +0 -755
- package/src/learning/tests/test_adaptive_ranker.py +0 -325
- package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
- package/src/learning/tests/test_aggregator.py +0 -306
- package/src/learning/tests/test_auto_retrain_v28.py +0 -35
- package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
- package/src/learning/tests/test_feature_extractor_v28.py +0 -93
- package/src/learning/tests/test_feedback_collector.py +0 -294
- package/src/learning/tests/test_learning_db.py +0 -602
- package/src/learning/tests/test_learning_db_v28.py +0 -110
- package/src/learning/tests/test_learning_init_v28.py +0 -48
- package/src/learning/tests/test_outcome_signals.py +0 -48
- package/src/learning/tests/test_project_context.py +0 -292
- package/src/learning/tests/test_schema_migration.py +0 -319
- package/src/learning/tests/test_signal_inference.py +0 -397
- package/src/learning/tests/test_source_quality.py +0 -351
- package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
- package/src/learning/tests/test_workflow_miner.py +0 -318
- package/src/learning/workflow_pattern_miner.py +0 -655
- package/src/lifecycle/__init__.py +0 -54
- package/src/lifecycle/bounded_growth.py +0 -239
- package/src/lifecycle/compaction_engine.py +0 -226
- package/src/lifecycle/lifecycle_engine.py +0 -355
- package/src/lifecycle/lifecycle_evaluator.py +0 -257
- package/src/lifecycle/lifecycle_scheduler.py +0 -130
- package/src/lifecycle/retention_policy.py +0 -285
- package/src/lifecycle/tests/test_bounded_growth.py +0 -193
- package/src/lifecycle/tests/test_compaction.py +0 -179
- package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
- package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
- package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
- package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
- package/src/lifecycle/tests/test_mcp_compact.py +0 -149
- package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
- package/src/lifecycle/tests/test_retention_policy.py +0 -162
- package/src/mcp_tools_v28.py +0 -281
- package/src/memory/__init__.py +0 -36
- package/src/memory/cli.py +0 -205
- package/src/memory/constants.py +0 -39
- package/src/memory/helpers.py +0 -28
- package/src/memory/schema.py +0 -166
- package/src/memory-profiles.py +0 -595
- package/src/memory-reset.py +0 -491
- package/src/memory_compression.py +0 -989
- package/src/memory_store_v2.py +0 -1155
- package/src/migrate_v1_to_v2.py +0 -629
- package/src/pattern_learner.py +0 -34
- package/src/patterns/__init__.py +0 -24
- package/src/patterns/analyzers.py +0 -251
- package/src/patterns/learner.py +0 -271
- package/src/patterns/scoring.py +0 -171
- package/src/patterns/store.py +0 -225
- package/src/patterns/terminology.py +0 -140
- package/src/provenance_tracker.py +0 -312
- package/src/qualixar_attribution.py +0 -139
- package/src/qualixar_watermark.py +0 -78
- package/src/query_optimizer.py +0 -511
- package/src/rate_limiter.py +0 -83
- package/src/search/__init__.py +0 -20
- package/src/search/cli.py +0 -77
- package/src/search/constants.py +0 -26
- package/src/search/engine.py +0 -241
- package/src/search/fusion.py +0 -122
- package/src/search/index_loader.py +0 -114
- package/src/search/methods.py +0 -162
- package/src/search_engine_v2.py +0 -401
- package/src/setup_validator.py +0 -482
- package/src/subscription_manager.py +0 -391
- package/src/tree/__init__.py +0 -59
- package/src/tree/builder.py +0 -185
- package/src/tree/nodes.py +0 -202
- package/src/tree/queries.py +0 -257
- package/src/tree/schema.py +0 -80
- package/src/tree_manager.py +0 -19
- package/src/trust/__init__.py +0 -45
- package/src/trust/constants.py +0 -66
- package/src/trust/queries.py +0 -157
- package/src/trust/schema.py +0 -95
- package/src/trust/scorer.py +0 -299
- package/src/trust/signals.py +0 -95
- package/src/trust_scorer.py +0 -44
- package/ui/app.js +0 -1588
- package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
- package/ui/js/graph-cytoscape.js +0 -1168
- package/ui/js/graph-d3-backup.js +0 -32
- package/ui/js/graph.js +0 -32
- package/ui_server.py +0 -266
- /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
- /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
- /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
- /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
- /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
- /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
- /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
- /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
- /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
- /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
- /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
- /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
- /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
- /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
- /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
- /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
- /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
- /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
- /package/{completions → ide/completions}/slm.bash +0 -0
- /package/{completions → ide/completions}/slm.zsh +0 -0
- /package/{configs → ide/configs}/cody-commands.json +0 -0
- /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
"""V3-native feature extractor for adaptive ranking.
|
|
6
|
+
|
|
7
|
+
Extracts features from retrieval results for LightGBM training.
|
|
8
|
+
Features are grouped by source: channel scores, fusion, reranker,
|
|
9
|
+
math layers, query analysis, memory metadata, user history.
|
|
10
|
+
|
|
11
|
+
Each feature vector has a fixed dimension (FEATURE_DIM).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Feature dimension — total number of features extracted
|
|
23
|
+
FEATURE_DIM = 20
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class FeatureVector:
|
|
28
|
+
"""Immutable feature vector for one retrieval result."""
|
|
29
|
+
fact_id: str
|
|
30
|
+
query_id: str
|
|
31
|
+
features: dict[str, float]
|
|
32
|
+
label: float | None = None # None = unlabeled (inference), float = labeled (training)
|
|
33
|
+
|
|
34
|
+
def to_list(self) -> list[float]:
|
|
35
|
+
"""Convert to ordered list for LightGBM input."""
|
|
36
|
+
return [self.features.get(name, 0.0) for name in FEATURE_NAMES]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Ordered feature names — must match FEATURE_DIM
|
|
40
|
+
FEATURE_NAMES: list[str] = [
|
|
41
|
+
# Channel scores (4)
|
|
42
|
+
"semantic_score",
|
|
43
|
+
"bm25_score",
|
|
44
|
+
"entity_score",
|
|
45
|
+
"temporal_score",
|
|
46
|
+
# Fusion (2)
|
|
47
|
+
"rrf_rank",
|
|
48
|
+
"rrf_score",
|
|
49
|
+
# Reranker (1)
|
|
50
|
+
"cross_encoder_score",
|
|
51
|
+
# Math layers (3)
|
|
52
|
+
"fisher_distance",
|
|
53
|
+
"fisher_confidence",
|
|
54
|
+
"sheaf_consistent",
|
|
55
|
+
# Query features (4)
|
|
56
|
+
"query_type_sh", # one-hot: single-hop
|
|
57
|
+
"query_type_mh", # one-hot: multi-hop
|
|
58
|
+
"query_type_temp", # one-hot: temporal
|
|
59
|
+
"query_type_od", # one-hot: open-domain
|
|
60
|
+
# Memory metadata (4)
|
|
61
|
+
"fact_age_days",
|
|
62
|
+
"access_count",
|
|
63
|
+
"fact_trust_score",
|
|
64
|
+
"fact_confidence",
|
|
65
|
+
# User/profile (2)
|
|
66
|
+
"profile_recall_count",
|
|
67
|
+
"topic_affinity",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
assert len(FEATURE_NAMES) == FEATURE_DIM
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FeatureExtractor:
|
|
74
|
+
"""Extract features from V3 retrieval results for LightGBM ranking."""
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def extract(result: dict[str, Any], query_context: dict[str, Any]) -> FeatureVector:
|
|
78
|
+
"""Extract features from a single retrieval result.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
result: dict with keys from RetrievalResult (score, channel_scores,
|
|
82
|
+
fact metadata, etc.)
|
|
83
|
+
query_context: dict with query_type, query_length, profile stats, etc.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
FeatureVector with FEATURE_DIM features.
|
|
87
|
+
"""
|
|
88
|
+
channel = result.get("channel_scores", {})
|
|
89
|
+
fact = result.get("fact", {})
|
|
90
|
+
|
|
91
|
+
features = {
|
|
92
|
+
# Channel scores
|
|
93
|
+
"semantic_score": channel.get("semantic", 0.0),
|
|
94
|
+
"bm25_score": channel.get("bm25", 0.0),
|
|
95
|
+
"entity_score": channel.get("entity_graph", 0.0),
|
|
96
|
+
"temporal_score": channel.get("temporal", 0.0),
|
|
97
|
+
# Fusion
|
|
98
|
+
"rrf_rank": result.get("rrf_rank", 0.0),
|
|
99
|
+
"rrf_score": result.get("rrf_score", 0.0),
|
|
100
|
+
# Reranker
|
|
101
|
+
"cross_encoder_score": result.get("cross_encoder_score", 0.0),
|
|
102
|
+
# Math
|
|
103
|
+
"fisher_distance": result.get("fisher_distance", 0.0),
|
|
104
|
+
"fisher_confidence": result.get("fisher_confidence", 0.0),
|
|
105
|
+
"sheaf_consistent": 1.0 if result.get("sheaf_consistent", True) else 0.0,
|
|
106
|
+
# Query (one-hot)
|
|
107
|
+
"query_type_sh": 1.0 if query_context.get("query_type") == "single_hop" else 0.0,
|
|
108
|
+
"query_type_mh": 1.0 if query_context.get("query_type") == "multi_hop" else 0.0,
|
|
109
|
+
"query_type_temp": 1.0 if query_context.get("query_type") == "temporal" else 0.0,
|
|
110
|
+
"query_type_od": 1.0 if query_context.get("query_type") == "open_domain" else 0.0,
|
|
111
|
+
# Memory metadata
|
|
112
|
+
"fact_age_days": _safe_float(fact.get("age_days", 0)),
|
|
113
|
+
"access_count": _safe_float(fact.get("access_count", 0)),
|
|
114
|
+
"fact_trust_score": _safe_float(result.get("trust_score", 0.5)),
|
|
115
|
+
"fact_confidence": _safe_float(fact.get("confidence", 0.7)),
|
|
116
|
+
# User/profile
|
|
117
|
+
"profile_recall_count": _safe_float(query_context.get("profile_recall_count", 0)),
|
|
118
|
+
"topic_affinity": _safe_float(query_context.get("topic_affinity", 0.0)),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return FeatureVector(
|
|
122
|
+
fact_id=result.get("fact_id", ""),
|
|
123
|
+
query_id=query_context.get("query_id", ""),
|
|
124
|
+
features=features,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def extract_batch(results: list[dict], query_context: dict) -> list[FeatureVector]:
|
|
129
|
+
"""Extract features from a batch of retrieval results."""
|
|
130
|
+
return [FeatureExtractor.extract(r, query_context) for r in results]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _safe_float(value: Any) -> float:
|
|
134
|
+
"""Convert to float safely, defaulting to 0.0."""
|
|
135
|
+
try:
|
|
136
|
+
return float(value)
|
|
137
|
+
except (TypeError, ValueError):
|
|
138
|
+
return 0.0
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2026 Qualixar / SuperLocalMemory (superlocalmemory.com)
|
|
4
|
+
# Part of Qualixar | Author: Varun Pratap Bhardwaj (qualixar.com | varunpratap.com)
|
|
5
|
+
"""
|
|
6
|
+
FeedbackCollector -- Multi-signal feedback collection for V3 learning.
|
|
7
|
+
|
|
8
|
+
Collects implicit and explicit relevance signals:
|
|
9
|
+
- Implicit: recall_hit (fact returned), recall_miss (fact not in results)
|
|
10
|
+
- Explicit: user_positive, user_negative, user_correction
|
|
11
|
+
- Derived: access_pattern (frequent recall = positive signal)
|
|
12
|
+
|
|
13
|
+
Privacy:
|
|
14
|
+
- Full query text is NEVER stored.
|
|
15
|
+
- Queries are hashed to SHA-256[:16] for grouping.
|
|
16
|
+
|
|
17
|
+
Storage:
|
|
18
|
+
Uses direct sqlite3 with a self-contained ``learning_feedback`` table.
|
|
19
|
+
NOT coupled to V3 DatabaseManager -- this is a standalone data collector.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import hashlib
|
|
25
|
+
import logging
|
|
26
|
+
import sqlite3
|
|
27
|
+
import threading
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Dict, List, Optional
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("superlocalmemory.learning.feedback")
|
|
33
|
+
|
|
34
|
+
# Signal type -> numeric value for downstream consumers
|
|
35
|
+
SIGNAL_VALUES: Dict[str, float] = {
|
|
36
|
+
"recall_hit": 0.7,
|
|
37
|
+
"recall_miss": 0.0,
|
|
38
|
+
"user_positive": 1.0,
|
|
39
|
+
"user_negative": 0.0,
|
|
40
|
+
"user_correction": 0.2,
|
|
41
|
+
"access_pattern": 0.6,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_CREATE_TABLE = """
|
|
45
|
+
CREATE TABLE IF NOT EXISTS learning_feedback (
|
|
46
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
|
+
profile_id TEXT NOT NULL,
|
|
48
|
+
fact_id TEXT NOT NULL,
|
|
49
|
+
signal_type TEXT NOT NULL,
|
|
50
|
+
signal_value REAL NOT NULL,
|
|
51
|
+
query_hash TEXT,
|
|
52
|
+
created_at TEXT NOT NULL,
|
|
53
|
+
metadata TEXT
|
|
54
|
+
)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_CREATE_INDEX = """
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_profile
|
|
59
|
+
ON learning_feedback (profile_id, created_at DESC)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _utcnow_iso() -> str:
|
|
64
|
+
"""Return current UTC time as ISO-8601 string."""
|
|
65
|
+
return datetime.now(timezone.utc).isoformat()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _hash_query(query: str) -> str:
|
|
69
|
+
"""Privacy-preserving SHA-256[:16] query hash."""
|
|
70
|
+
return hashlib.sha256(query.encode("utf-8")).hexdigest()[:16]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class FeedbackCollector:
|
|
74
|
+
"""
|
|
75
|
+
Collects multi-signal relevance feedback for the V3 learning system.
|
|
76
|
+
|
|
77
|
+
Each instance owns a sqlite3 database at *db_path*. All writes are
|
|
78
|
+
serialised through a threading lock for safety.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
db_path: Path to the sqlite3 database file.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, db_path: Path) -> None:
|
|
85
|
+
self._db_path = Path(db_path)
|
|
86
|
+
self._lock = threading.Lock()
|
|
87
|
+
self._ensure_schema()
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
# Schema
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def _ensure_schema(self) -> None:
|
|
94
|
+
"""Create tables/indexes if they do not exist."""
|
|
95
|
+
conn = self._connect()
|
|
96
|
+
try:
|
|
97
|
+
conn.execute(_CREATE_TABLE)
|
|
98
|
+
conn.execute(_CREATE_INDEX)
|
|
99
|
+
conn.commit()
|
|
100
|
+
finally:
|
|
101
|
+
conn.close()
|
|
102
|
+
|
|
103
|
+
def _connect(self) -> sqlite3.Connection:
|
|
104
|
+
"""Open a connection with WAL mode and busy timeout."""
|
|
105
|
+
conn = sqlite3.connect(str(self._db_path), timeout=10)
|
|
106
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
107
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
108
|
+
conn.row_factory = sqlite3.Row
|
|
109
|
+
return conn
|
|
110
|
+
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
# Public API: record implicit feedback
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def record_implicit(
|
|
116
|
+
self,
|
|
117
|
+
profile_id: str,
|
|
118
|
+
query: str,
|
|
119
|
+
fact_ids_returned: List[str],
|
|
120
|
+
fact_ids_available: List[str],
|
|
121
|
+
) -> int:
|
|
122
|
+
"""
|
|
123
|
+
Record implicit feedback from a recall operation.
|
|
124
|
+
|
|
125
|
+
Facts in *fact_ids_returned* get a ``recall_hit`` signal.
|
|
126
|
+
Facts in *fact_ids_available* but NOT in *fact_ids_returned* get
|
|
127
|
+
a ``recall_miss`` signal.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
profile_id: Profile that performed the recall.
|
|
131
|
+
query: The recall query (hashed, never stored raw).
|
|
132
|
+
fact_ids_returned: Fact IDs that appeared in results.
|
|
133
|
+
fact_ids_available: All candidate fact IDs for this query.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Number of feedback records created.
|
|
137
|
+
"""
|
|
138
|
+
if not profile_id or not query:
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
qhash = _hash_query(query)
|
|
142
|
+
returned_set = set(fact_ids_returned)
|
|
143
|
+
now = _utcnow_iso()
|
|
144
|
+
records: list[tuple] = []
|
|
145
|
+
|
|
146
|
+
for fid in returned_set:
|
|
147
|
+
records.append((
|
|
148
|
+
profile_id, fid, "recall_hit",
|
|
149
|
+
SIGNAL_VALUES["recall_hit"], qhash, now, None,
|
|
150
|
+
))
|
|
151
|
+
|
|
152
|
+
for fid in fact_ids_available:
|
|
153
|
+
if fid not in returned_set:
|
|
154
|
+
records.append((
|
|
155
|
+
profile_id, fid, "recall_miss",
|
|
156
|
+
SIGNAL_VALUES["recall_miss"], qhash, now, None,
|
|
157
|
+
))
|
|
158
|
+
|
|
159
|
+
if not records:
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
with self._lock:
|
|
163
|
+
conn = self._connect()
|
|
164
|
+
try:
|
|
165
|
+
conn.executemany(
|
|
166
|
+
"INSERT INTO learning_feedback "
|
|
167
|
+
"(profile_id, fact_id, signal_type, signal_value, "
|
|
168
|
+
"query_hash, created_at, metadata) "
|
|
169
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
170
|
+
records,
|
|
171
|
+
)
|
|
172
|
+
conn.commit()
|
|
173
|
+
return len(records)
|
|
174
|
+
finally:
|
|
175
|
+
conn.close()
|
|
176
|
+
|
|
177
|
+
# ------------------------------------------------------------------
|
|
178
|
+
# Public API: record explicit feedback
|
|
179
|
+
# ------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def record_explicit(
|
|
182
|
+
self,
|
|
183
|
+
profile_id: str,
|
|
184
|
+
fact_id: str,
|
|
185
|
+
signal_type: str,
|
|
186
|
+
value: float,
|
|
187
|
+
) -> Optional[int]:
|
|
188
|
+
"""
|
|
189
|
+
Record explicit user feedback on a specific fact.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
profile_id: Profile providing feedback.
|
|
193
|
+
fact_id: The fact being rated.
|
|
194
|
+
signal_type: One of ``user_positive``, ``user_negative``,
|
|
195
|
+
``user_correction``, or any custom type.
|
|
196
|
+
value: Numeric signal value (0.0 to 1.0).
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Row ID of the inserted record, or None on error.
|
|
200
|
+
"""
|
|
201
|
+
if not profile_id or not fact_id:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
clamped = max(0.0, min(1.0, float(value)))
|
|
205
|
+
now = _utcnow_iso()
|
|
206
|
+
|
|
207
|
+
with self._lock:
|
|
208
|
+
conn = self._connect()
|
|
209
|
+
try:
|
|
210
|
+
cursor = conn.execute(
|
|
211
|
+
"INSERT INTO learning_feedback "
|
|
212
|
+
"(profile_id, fact_id, signal_type, signal_value, "
|
|
213
|
+
"query_hash, created_at, metadata) "
|
|
214
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
215
|
+
(profile_id, fact_id, signal_type, clamped, None, now, None),
|
|
216
|
+
)
|
|
217
|
+
conn.commit()
|
|
218
|
+
return cursor.lastrowid
|
|
219
|
+
finally:
|
|
220
|
+
conn.close()
|
|
221
|
+
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
# Public API: read feedback
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
def get_feedback(
|
|
227
|
+
self,
|
|
228
|
+
profile_id: str,
|
|
229
|
+
limit: int = 100,
|
|
230
|
+
) -> List[Dict[str, Any]]:
|
|
231
|
+
"""
|
|
232
|
+
Retrieve recent feedback records for a profile.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
profile_id: Profile to query.
|
|
236
|
+
limit: Maximum records to return.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
List of dicts with keys: id, fact_id, signal_type,
|
|
240
|
+
signal_value, query_hash, created_at.
|
|
241
|
+
"""
|
|
242
|
+
conn = self._connect()
|
|
243
|
+
try:
|
|
244
|
+
rows = conn.execute(
|
|
245
|
+
"SELECT id, fact_id, signal_type, signal_value, "
|
|
246
|
+
"query_hash, created_at "
|
|
247
|
+
"FROM learning_feedback "
|
|
248
|
+
"WHERE profile_id = ? "
|
|
249
|
+
"ORDER BY created_at DESC LIMIT ?",
|
|
250
|
+
(profile_id, limit),
|
|
251
|
+
).fetchall()
|
|
252
|
+
return [dict(r) for r in rows]
|
|
253
|
+
finally:
|
|
254
|
+
conn.close()
|
|
255
|
+
|
|
256
|
+
def get_feedback_count(self, profile_id: str) -> int:
|
|
257
|
+
"""
|
|
258
|
+
Return the total number of feedback records for a profile.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
profile_id: Profile to query.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Integer count of feedback records.
|
|
265
|
+
"""
|
|
266
|
+
conn = self._connect()
|
|
267
|
+
try:
|
|
268
|
+
row = conn.execute(
|
|
269
|
+
"SELECT COUNT(*) FROM learning_feedback WHERE profile_id = ?",
|
|
270
|
+
(profile_id,),
|
|
271
|
+
).fetchone()
|
|
272
|
+
return row[0] if row else 0
|
|
273
|
+
finally:
|
|
274
|
+
conn.close()
|
|
275
|
+
|
|
276
|
+
# ------------------------------------------------------------------
|
|
277
|
+
# Public API: summary
|
|
278
|
+
# ------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
def get_summary(self, profile_id: str) -> Dict[str, Any]:
|
|
281
|
+
"""
|
|
282
|
+
Return summary statistics for a profile's feedback.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Dict with total, by_type counts, and latest timestamp.
|
|
286
|
+
"""
|
|
287
|
+
conn = self._connect()
|
|
288
|
+
try:
|
|
289
|
+
total_row = conn.execute(
|
|
290
|
+
"SELECT COUNT(*) FROM learning_feedback WHERE profile_id = ?",
|
|
291
|
+
(profile_id,),
|
|
292
|
+
).fetchone()
|
|
293
|
+
total = total_row[0] if total_row else 0
|
|
294
|
+
|
|
295
|
+
type_rows = conn.execute(
|
|
296
|
+
"SELECT signal_type, COUNT(*) AS cnt "
|
|
297
|
+
"FROM learning_feedback WHERE profile_id = ? "
|
|
298
|
+
"GROUP BY signal_type",
|
|
299
|
+
(profile_id,),
|
|
300
|
+
).fetchall()
|
|
301
|
+
by_type = {r["signal_type"]: r["cnt"] for r in type_rows}
|
|
302
|
+
|
|
303
|
+
latest_row = conn.execute(
|
|
304
|
+
"SELECT MAX(created_at) FROM learning_feedback "
|
|
305
|
+
"WHERE profile_id = ?",
|
|
306
|
+
(profile_id,),
|
|
307
|
+
).fetchone()
|
|
308
|
+
latest = latest_row[0] if latest_row else None
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
"total": total,
|
|
312
|
+
"by_type": by_type,
|
|
313
|
+
"latest": latest,
|
|
314
|
+
}
|
|
315
|
+
finally:
|
|
316
|
+
conn.close()
|
|
@@ -0,0 +1,255 @@
|
|
|
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 — Outcome Tracking & Inference.
|
|
6
|
+
|
|
7
|
+
Records what happens AFTER memories are recalled: success, failure,
|
|
8
|
+
or partial outcomes. Also provides signal-based outcome inference
|
|
9
|
+
for implicit feedback loops.
|
|
10
|
+
|
|
11
|
+
Uses the ``action_outcomes`` table from the V3 schema and returns
|
|
12
|
+
``ActionOutcome`` dataclass instances for type safety.
|
|
13
|
+
|
|
14
|
+
The feedback loop:
|
|
15
|
+
recall() -> user action -> record_outcome() -> learning engine
|
|
16
|
+
|
|
17
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from superlocalmemory.storage.models import ActionOutcome
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Valid outcome labels
|
|
31
|
+
VALID_OUTCOMES = frozenset({"success", "failure", "partial"})
|
|
32
|
+
|
|
33
|
+
# Inference signal weights
|
|
34
|
+
_SIGNAL_WEIGHTS: dict[str, tuple[str, float]] = {
|
|
35
|
+
"used_immediately": ("success", 0.9),
|
|
36
|
+
"mcp_used_high": ("success", 0.8),
|
|
37
|
+
"cross_tool_access": ("success", 0.7),
|
|
38
|
+
"no_requery_10m": ("success", 0.6),
|
|
39
|
+
"partial_use": ("partial", 0.5),
|
|
40
|
+
"requery_different_terms": ("failure", 0.3),
|
|
41
|
+
"rapid_fire_queries": ("failure", 0.2),
|
|
42
|
+
"deleted_after_recall": ("failure", 0.1),
|
|
43
|
+
"ignored": ("failure", 0.4),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OutcomeTracker:
|
|
48
|
+
"""Track retrieval outcomes and feed into learning.
|
|
49
|
+
|
|
50
|
+
Accepts a ``DatabaseManager`` and operates on the ``action_outcomes``
|
|
51
|
+
table created by the V3 schema. Returns ``ActionOutcome`` dataclasses.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, db) -> None:
|
|
55
|
+
self._db = db
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# Public API — Recording
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def record_outcome(
|
|
62
|
+
self,
|
|
63
|
+
query: str,
|
|
64
|
+
fact_ids: list[str],
|
|
65
|
+
outcome: str,
|
|
66
|
+
profile_id: str,
|
|
67
|
+
context: dict[str, Any] | None = None,
|
|
68
|
+
) -> ActionOutcome:
|
|
69
|
+
"""Record an outcome against one or more facts.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
query: The recall query that produced these facts.
|
|
73
|
+
fact_ids: List of fact IDs involved in the outcome.
|
|
74
|
+
outcome: One of "success", "failure", "partial".
|
|
75
|
+
profile_id: Profile scope.
|
|
76
|
+
context: Arbitrary metadata dict.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The persisted ActionOutcome.
|
|
80
|
+
"""
|
|
81
|
+
if outcome not in VALID_OUTCOMES:
|
|
82
|
+
logger.warning(
|
|
83
|
+
"Invalid outcome '%s'. Must be one of %s", outcome, VALID_OUTCOMES
|
|
84
|
+
)
|
|
85
|
+
outcome = "partial"
|
|
86
|
+
|
|
87
|
+
ao = ActionOutcome(
|
|
88
|
+
profile_id=profile_id,
|
|
89
|
+
query=query,
|
|
90
|
+
fact_ids=list(fact_ids),
|
|
91
|
+
outcome=outcome,
|
|
92
|
+
context=dict(context) if context else {},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self._db.execute(
|
|
96
|
+
"INSERT OR REPLACE INTO action_outcomes "
|
|
97
|
+
"(outcome_id, profile_id, query, fact_ids_json, outcome, "
|
|
98
|
+
" context_json, timestamp) "
|
|
99
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
100
|
+
(
|
|
101
|
+
ao.outcome_id,
|
|
102
|
+
ao.profile_id,
|
|
103
|
+
ao.query,
|
|
104
|
+
json.dumps(ao.fact_ids),
|
|
105
|
+
ao.outcome,
|
|
106
|
+
json.dumps(ao.context),
|
|
107
|
+
ao.timestamp,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
return ao
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
# Public API — Querying
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def get_outcomes(
|
|
117
|
+
self,
|
|
118
|
+
profile_id: str,
|
|
119
|
+
limit: int = 50,
|
|
120
|
+
outcome_filter: str | None = None,
|
|
121
|
+
) -> list[ActionOutcome]:
|
|
122
|
+
"""Get recent outcomes for a profile.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of ``ActionOutcome`` objects, newest first.
|
|
126
|
+
"""
|
|
127
|
+
sql = "SELECT * FROM action_outcomes WHERE profile_id = ?"
|
|
128
|
+
params: list[Any] = [profile_id]
|
|
129
|
+
|
|
130
|
+
if outcome_filter and outcome_filter in VALID_OUTCOMES:
|
|
131
|
+
sql += " AND outcome = ?"
|
|
132
|
+
params.append(outcome_filter)
|
|
133
|
+
|
|
134
|
+
sql += " ORDER BY timestamp DESC LIMIT ?"
|
|
135
|
+
params.append(limit)
|
|
136
|
+
|
|
137
|
+
rows = self._db.execute(sql, tuple(params))
|
|
138
|
+
return [self._row_to_outcome(r) for r in rows]
|
|
139
|
+
|
|
140
|
+
def get_success_rate(self, profile_id: str) -> float:
|
|
141
|
+
"""Overall success rate. ``success`` = 1.0, ``partial`` = 0.5."""
|
|
142
|
+
rows = self._db.execute(
|
|
143
|
+
"SELECT outcome, COUNT(*) AS cnt FROM action_outcomes "
|
|
144
|
+
"WHERE profile_id = ? GROUP BY outcome",
|
|
145
|
+
(profile_id,),
|
|
146
|
+
)
|
|
147
|
+
if not rows:
|
|
148
|
+
return 0.0
|
|
149
|
+
|
|
150
|
+
counts = {dict(r)["outcome"]: dict(r)["cnt"] for r in rows}
|
|
151
|
+
total = sum(counts.values())
|
|
152
|
+
if total == 0:
|
|
153
|
+
return 0.0
|
|
154
|
+
|
|
155
|
+
success = counts.get("success", 0)
|
|
156
|
+
partial = counts.get("partial", 0) * 0.5
|
|
157
|
+
return round((success + partial) / total, 4)
|
|
158
|
+
|
|
159
|
+
def get_fact_success_rate(self, fact_id: str, profile_id: str) -> float:
|
|
160
|
+
"""How often a specific fact led to successful outcomes.
|
|
161
|
+
|
|
162
|
+
Returns 0.5 (neutral) if no relevant data exists.
|
|
163
|
+
"""
|
|
164
|
+
outcomes = self.get_outcomes(profile_id, limit=500)
|
|
165
|
+
relevant = [o for o in outcomes if fact_id in o.fact_ids]
|
|
166
|
+
|
|
167
|
+
if not relevant:
|
|
168
|
+
return 0.5
|
|
169
|
+
|
|
170
|
+
successes = sum(1 for o in relevant if o.outcome == "success")
|
|
171
|
+
return round(successes / len(relevant), 4)
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
# Public API — Inference
|
|
175
|
+
# ------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def infer_outcome(
|
|
178
|
+
self,
|
|
179
|
+
profile_id: str,
|
|
180
|
+
fact_ids: list[str],
|
|
181
|
+
signals: dict[str, Any],
|
|
182
|
+
) -> str:
|
|
183
|
+
"""Infer outcome from behavioral signals and auto-record it."""
|
|
184
|
+
success_score = 0.0
|
|
185
|
+
failure_score = 0.0
|
|
186
|
+
matched_signals: list[str] = []
|
|
187
|
+
|
|
188
|
+
for signal_name, value in signals.items():
|
|
189
|
+
if not value:
|
|
190
|
+
continue
|
|
191
|
+
if signal_name in _SIGNAL_WEIGHTS:
|
|
192
|
+
outcome_label, weight = _SIGNAL_WEIGHTS[signal_name]
|
|
193
|
+
matched_signals.append(signal_name)
|
|
194
|
+
if outcome_label == "success":
|
|
195
|
+
success_score += weight
|
|
196
|
+
elif outcome_label == "failure":
|
|
197
|
+
failure_score += weight
|
|
198
|
+
else:
|
|
199
|
+
success_score += weight * 0.5
|
|
200
|
+
failure_score += weight * 0.5
|
|
201
|
+
|
|
202
|
+
if success_score > failure_score:
|
|
203
|
+
inferred = "success"
|
|
204
|
+
elif failure_score > success_score:
|
|
205
|
+
inferred = "failure"
|
|
206
|
+
else:
|
|
207
|
+
inferred = "partial"
|
|
208
|
+
|
|
209
|
+
self.record_outcome(
|
|
210
|
+
query="[inferred]",
|
|
211
|
+
fact_ids=fact_ids,
|
|
212
|
+
outcome=inferred,
|
|
213
|
+
profile_id=profile_id,
|
|
214
|
+
context={
|
|
215
|
+
"signals": matched_signals,
|
|
216
|
+
"success_score": round(success_score, 3),
|
|
217
|
+
"failure_score": round(failure_score, 3),
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
return inferred
|
|
221
|
+
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
# Public API — Maintenance
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
def delete_outcomes(self, profile_id: str) -> int:
|
|
227
|
+
"""Delete all outcomes for a profile. Returns count deleted."""
|
|
228
|
+
rows = self._db.execute(
|
|
229
|
+
"SELECT COUNT(*) AS c FROM action_outcomes WHERE profile_id = ?",
|
|
230
|
+
(profile_id,),
|
|
231
|
+
)
|
|
232
|
+
count = int(dict(rows[0])["c"]) if rows else 0
|
|
233
|
+
self._db.execute(
|
|
234
|
+
"DELETE FROM action_outcomes WHERE profile_id = ?",
|
|
235
|
+
(profile_id,),
|
|
236
|
+
)
|
|
237
|
+
return count
|
|
238
|
+
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
# Internal helpers
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _row_to_outcome(row) -> ActionOutcome:
|
|
245
|
+
"""Convert a DB row to ActionOutcome."""
|
|
246
|
+
d = dict(row)
|
|
247
|
+
return ActionOutcome(
|
|
248
|
+
outcome_id=d["outcome_id"],
|
|
249
|
+
profile_id=d["profile_id"],
|
|
250
|
+
query=d.get("query", ""),
|
|
251
|
+
fact_ids=json.loads(d.get("fact_ids_json", "[]")),
|
|
252
|
+
outcome=d.get("outcome", ""),
|
|
253
|
+
context=json.loads(d.get("context_json", "{}")),
|
|
254
|
+
timestamp=d.get("timestamp", ""),
|
|
255
|
+
)
|