superlocalmemory 2.8.6 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -1
- package/NOTICE +63 -0
- package/README.md +165 -480
- package/bin/slm +17 -449
- package/bin/slm-npm +1 -1
- 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/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/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 -1808
- 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 -286
- /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
- /package/{install.ps1 → scripts/install.ps1} +0 -0
- /package/{install.sh → scripts/install.sh} +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
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 — Bridge Discovery + Spreading Activation.
|
|
6
|
+
|
|
7
|
+
Connects disconnected retrieval results via intermediate facts.
|
|
8
|
+
Combines AriadneMem's 5-step bridging with Hindsight's TEMPR activation.
|
|
9
|
+
|
|
10
|
+
Algorithm:
|
|
11
|
+
1. Sort seed results chronologically
|
|
12
|
+
2. For consecutive pairs, check entity overlap + temporal proximity
|
|
13
|
+
3. If disconnected: try entity/keyword/proper-noun bridge strategies
|
|
14
|
+
4. Add bridge facts with inferred edges
|
|
15
|
+
5. Spreading activation from seeds through graph
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
- max_depth=3 hops
|
|
19
|
+
- node_budget=8-25
|
|
20
|
+
- time_window=1-168 hours
|
|
21
|
+
- decay=0.7 per hop
|
|
22
|
+
- typed mu: entity=1.2, causal=1.3, semantic=0.8, temporal=0.9
|
|
23
|
+
|
|
24
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
25
|
+
License: MIT
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
import re
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
# Spreading activation parameters (Hindsight TEMPR)
|
|
39
|
+
_DECAY: float = 0.7
|
|
40
|
+
_TYPED_MU: dict[str, float] = {
|
|
41
|
+
"entity": 1.2,
|
|
42
|
+
"causal": 1.3,
|
|
43
|
+
"semantic": 0.8,
|
|
44
|
+
"temporal": 0.9,
|
|
45
|
+
"supersedes": 0.0,
|
|
46
|
+
}
|
|
47
|
+
_MAX_DEPTH: int = 4
|
|
48
|
+
_NODE_BUDGET: int = 50
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BridgeDiscovery:
|
|
52
|
+
"""Connect disconnected retrieval results via graph paths.
|
|
53
|
+
|
|
54
|
+
Usage::
|
|
55
|
+
bridge = BridgeDiscovery(db)
|
|
56
|
+
expanded = bridge.discover(seed_fact_ids, profile_id)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, db: DatabaseManager) -> None:
|
|
60
|
+
self._db = db
|
|
61
|
+
|
|
62
|
+
def discover(
|
|
63
|
+
self,
|
|
64
|
+
seed_ids: list[str],
|
|
65
|
+
profile_id: str,
|
|
66
|
+
max_bridges: int = 10,
|
|
67
|
+
) -> list[tuple[str, float]]:
|
|
68
|
+
"""Find bridge facts connecting seed results.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
seed_ids: Fact IDs from initial retrieval.
|
|
72
|
+
profile_id: Scope to this profile.
|
|
73
|
+
max_bridges: Maximum bridge facts to return.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
List of (fact_id, bridge_score) for discovered bridges.
|
|
77
|
+
"""
|
|
78
|
+
if len(seed_ids) < 2:
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
bridges: list[tuple[str, float]] = []
|
|
82
|
+
seen = set(seed_ids)
|
|
83
|
+
|
|
84
|
+
# Check consecutive pairs for entity overlap
|
|
85
|
+
for i in range(len(seed_ids) - 1):
|
|
86
|
+
fact_a = self._db.get_fact(seed_ids[i])
|
|
87
|
+
fact_b = self._db.get_fact(seed_ids[i + 1])
|
|
88
|
+
if not fact_a or not fact_b:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
entities_a = set(fact_a.canonical_entities)
|
|
92
|
+
entities_b = set(fact_b.canonical_entities)
|
|
93
|
+
|
|
94
|
+
# If they share entities, no bridge needed
|
|
95
|
+
if entities_a & entities_b:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
# Strategy 1: Entity bridge (union minus intersection)
|
|
99
|
+
bridge_entities = (entities_a | entities_b) - (entities_a & entities_b)
|
|
100
|
+
for eid in bridge_entities:
|
|
101
|
+
entity_facts = self._db.get_facts_by_entity(eid, profile_id)
|
|
102
|
+
for f in entity_facts[:5]:
|
|
103
|
+
if f.fact_id not in seen:
|
|
104
|
+
seen.add(f.fact_id)
|
|
105
|
+
bridges.append((f.fact_id, 0.7))
|
|
106
|
+
|
|
107
|
+
if len(bridges) >= max_bridges:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
bridges.sort(key=lambda x: x[1], reverse=True)
|
|
111
|
+
return bridges[:max_bridges]
|
|
112
|
+
|
|
113
|
+
def spreading_activation(
|
|
114
|
+
self,
|
|
115
|
+
seed_ids: list[str],
|
|
116
|
+
profile_id: str,
|
|
117
|
+
max_depth: int = _MAX_DEPTH,
|
|
118
|
+
budget: int = _NODE_BUDGET,
|
|
119
|
+
) -> list[tuple[str, float]]:
|
|
120
|
+
"""Spreading activation from seed facts through the graph.
|
|
121
|
+
|
|
122
|
+
At each hop, activation decays by _DECAY and is modulated by
|
|
123
|
+
edge type via _TYPED_MU.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
seed_ids: Starting fact IDs.
|
|
127
|
+
profile_id: Scope.
|
|
128
|
+
max_depth: Maximum hops.
|
|
129
|
+
budget: Maximum nodes to return.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of (fact_id, activation_score) for activated facts.
|
|
133
|
+
"""
|
|
134
|
+
activations: dict[str, float] = {fid: 1.0 for fid in seed_ids}
|
|
135
|
+
frontier = list(seed_ids)
|
|
136
|
+
|
|
137
|
+
for depth in range(max_depth):
|
|
138
|
+
next_frontier: list[str] = []
|
|
139
|
+
for fid in frontier:
|
|
140
|
+
current_activation = activations.get(fid, 0.0)
|
|
141
|
+
if current_activation < 0.01:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
edges = self._db.get_edges_for_node(fid, profile_id)
|
|
145
|
+
for edge in edges:
|
|
146
|
+
other_id = (
|
|
147
|
+
edge.target_id
|
|
148
|
+
if edge.source_id == fid
|
|
149
|
+
else edge.source_id
|
|
150
|
+
)
|
|
151
|
+
mu = _TYPED_MU.get(edge.edge_type.value, 0.8)
|
|
152
|
+
propagated = current_activation * _DECAY * mu
|
|
153
|
+
|
|
154
|
+
if propagated > activations.get(other_id, 0.0):
|
|
155
|
+
activations[other_id] = propagated
|
|
156
|
+
if other_id not in seed_ids:
|
|
157
|
+
next_frontier.append(other_id)
|
|
158
|
+
|
|
159
|
+
frontier = next_frontier
|
|
160
|
+
if not frontier:
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
# Return non-seed nodes with activation
|
|
164
|
+
results = [
|
|
165
|
+
(fid, score)
|
|
166
|
+
for fid, score in activations.items()
|
|
167
|
+
if fid not in set(seed_ids) and score > 0.01
|
|
168
|
+
]
|
|
169
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
170
|
+
return results[:budget]
|
|
@@ -0,0 +1,390 @@
|
|
|
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 — Retrieval Engine (4-Channel Orchestrator).
|
|
6
|
+
|
|
7
|
+
4 channels -> single RRF fusion -> optional cross-encoder rerank.
|
|
8
|
+
Replaces V1's broken 10-channel triple-re-fusion pipeline.
|
|
9
|
+
|
|
10
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
11
|
+
License: MIT
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import math
|
|
17
|
+
import re
|
|
18
|
+
import time
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
20
|
+
|
|
21
|
+
from superlocalmemory.core.config import ChannelWeights, RetrievalConfig
|
|
22
|
+
from superlocalmemory.retrieval.fusion import FusionResult, weighted_rrf
|
|
23
|
+
from superlocalmemory.retrieval.strategy import QueryStrategy, QueryStrategyClassifier
|
|
24
|
+
from superlocalmemory.storage.models import (
|
|
25
|
+
AtomicFact, Mode, RecallResponse, RetrievalResult,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from superlocalmemory.retrieval.bm25_channel import BM25Channel
|
|
30
|
+
from superlocalmemory.retrieval.entity_channel import EntityGraphChannel
|
|
31
|
+
from superlocalmemory.retrieval.semantic_channel import SemanticChannel
|
|
32
|
+
from superlocalmemory.retrieval.temporal_channel import TemporalChannel
|
|
33
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
34
|
+
from superlocalmemory.trust.scorer import TrustScorer
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CrossEncoderProtocol(Protocol):
|
|
40
|
+
"""Duck-typed cross-encoder interface."""
|
|
41
|
+
def rerank(self, query: str, candidates: list[tuple[str, str]]) -> list[tuple[str, float]]: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EmbeddingProvider(Protocol):
|
|
45
|
+
"""Duck-typed embedding provider."""
|
|
46
|
+
def embed(self, text: str) -> list[float]: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RetrievalEngine:
|
|
50
|
+
"""4-channel retrieval: semantic + BM25 + entity_graph + temporal.
|
|
51
|
+
|
|
52
|
+
Usage::
|
|
53
|
+
engine = RetrievalEngine(db, config, channels, embedder)
|
|
54
|
+
response = engine.recall("What did Alice do?", "default", Mode.A)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self, db: DatabaseManager, config: RetrievalConfig,
|
|
59
|
+
channels: dict[str, Any],
|
|
60
|
+
embedder: EmbeddingProvider | None = None,
|
|
61
|
+
reranker: CrossEncoderProtocol | None = None,
|
|
62
|
+
strategy: QueryStrategyClassifier | None = None,
|
|
63
|
+
base_weights: ChannelWeights | None = None,
|
|
64
|
+
profile_channel: Any | None = None,
|
|
65
|
+
bridge_discovery: Any | None = None,
|
|
66
|
+
trust_scorer: TrustScorer | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
self._db = db
|
|
69
|
+
self._config = config
|
|
70
|
+
self._semantic: SemanticChannel | None = channels.get("semantic")
|
|
71
|
+
self._bm25: BM25Channel | None = channels.get("bm25")
|
|
72
|
+
self._entity: EntityGraphChannel | None = channels.get("entity_graph")
|
|
73
|
+
self._temporal: TemporalChannel | None = channels.get("temporal")
|
|
74
|
+
self._embedder = embedder
|
|
75
|
+
self._reranker = reranker
|
|
76
|
+
self._strategy = strategy or QueryStrategyClassifier()
|
|
77
|
+
self._base_weights = (base_weights or ChannelWeights()).as_dict()
|
|
78
|
+
self._profile_channel = profile_channel
|
|
79
|
+
self._bridge = bridge_discovery
|
|
80
|
+
self._trust_scorer = trust_scorer
|
|
81
|
+
|
|
82
|
+
def recall(
|
|
83
|
+
self, query: str, profile_id: str,
|
|
84
|
+
mode: Mode = Mode.A, limit: int = 20,
|
|
85
|
+
) -> RecallResponse:
|
|
86
|
+
"""Full retrieval pipeline: strategy -> channels -> RRF -> rerank."""
|
|
87
|
+
t0 = time.monotonic()
|
|
88
|
+
|
|
89
|
+
# 1. Classify query, get adaptive weights
|
|
90
|
+
strat = self._strategy.classify(query, self._base_weights)
|
|
91
|
+
|
|
92
|
+
# Profile shortcut (runs before channel search)
|
|
93
|
+
if self._profile_channel is not None:
|
|
94
|
+
try:
|
|
95
|
+
profile_hits = self._profile_channel.search(
|
|
96
|
+
query, profile_id, top_k=10,
|
|
97
|
+
)
|
|
98
|
+
if profile_hits:
|
|
99
|
+
strat.weights["profile"] = 2.0
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
logger.warning("Profile channel: %s", exc)
|
|
102
|
+
profile_hits = []
|
|
103
|
+
else:
|
|
104
|
+
profile_hits = []
|
|
105
|
+
|
|
106
|
+
# Dynamic top-k for aggregation queries
|
|
107
|
+
effective_limit = 50 if strat.query_type == "aggregation" else limit
|
|
108
|
+
|
|
109
|
+
# 3. Run 4 channels
|
|
110
|
+
ch_results = self._run_channels(query, profile_id, strat)
|
|
111
|
+
if profile_hits:
|
|
112
|
+
ch_results["profile"] = profile_hits
|
|
113
|
+
total = sum(len(v) for v in ch_results.values())
|
|
114
|
+
|
|
115
|
+
# 3. Single-pass RRF fusion
|
|
116
|
+
fused = weighted_rrf(ch_results, strat.weights, k=self._config.rrf_k)
|
|
117
|
+
|
|
118
|
+
# Bridge discovery for multi-hop queries
|
|
119
|
+
if self._bridge is not None and strat.query_type == "multi_hop":
|
|
120
|
+
try:
|
|
121
|
+
seed_ids = [fr.fact_id for fr in fused[:10]]
|
|
122
|
+
bridges = self._bridge.discover(seed_ids, profile_id, max_bridges=10)
|
|
123
|
+
spread = self._bridge.spreading_activation(seed_ids, profile_id)
|
|
124
|
+
extra = bridges + spread
|
|
125
|
+
for fid, score in extra:
|
|
126
|
+
if not any(fr.fact_id == fid for fr in fused):
|
|
127
|
+
fused.append(FusionResult(
|
|
128
|
+
fact_id=fid, fused_score=score * 0.8,
|
|
129
|
+
channel_ranks={}, channel_scores={},
|
|
130
|
+
))
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
logger.warning("Bridge discovery: %s", exc)
|
|
133
|
+
|
|
134
|
+
# Scene expansion
|
|
135
|
+
if fused:
|
|
136
|
+
try:
|
|
137
|
+
expanded_ids: set[str] = set()
|
|
138
|
+
for fr in fused[:20]:
|
|
139
|
+
scenes = self._db.get_scenes_for_fact(fr.fact_id, profile_id)
|
|
140
|
+
for scene in scenes[:2]:
|
|
141
|
+
for sfid in scene.fact_ids:
|
|
142
|
+
if not any(f.fact_id == sfid for f in fused) and sfid not in expanded_ids:
|
|
143
|
+
expanded_ids.add(sfid)
|
|
144
|
+
fused.append(FusionResult(
|
|
145
|
+
fact_id=sfid, fused_score=fr.fused_score * 0.8,
|
|
146
|
+
channel_ranks={}, channel_scores={},
|
|
147
|
+
))
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
logger.warning("Scene expansion: %s", exc)
|
|
150
|
+
|
|
151
|
+
# 4. Load facts for rerank pool
|
|
152
|
+
pool = min(len(fused), max(effective_limit * 3, 30))
|
|
153
|
+
top = fused[:pool]
|
|
154
|
+
facts = self._load_facts(top, profile_id)
|
|
155
|
+
|
|
156
|
+
# 5. Cross-encoder rerank (optional)
|
|
157
|
+
# Bug 4 fix: reduced alpha for multi-hop/temporal to preserve diversity
|
|
158
|
+
if self._reranker is not None and facts:
|
|
159
|
+
ce_alpha = 0.5 if strat.query_type in ("multi_hop", "temporal") else 0.75
|
|
160
|
+
top = self._apply_reranker(query, top, facts, alpha=ce_alpha)
|
|
161
|
+
|
|
162
|
+
# 6. Build response
|
|
163
|
+
results = self._build_results(top[:effective_limit], facts, strat)
|
|
164
|
+
ms = (time.monotonic() - t0) * 1000.0
|
|
165
|
+
return RecallResponse(
|
|
166
|
+
query=query, mode=mode, results=results,
|
|
167
|
+
query_type=strat.query_type, channel_weights=strat.weights,
|
|
168
|
+
total_candidates=total, retrieval_time_ms=ms,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# -- Channel execution --------------------------------------------------
|
|
172
|
+
|
|
173
|
+
def _run_channels(
|
|
174
|
+
self, query: str, profile_id: str, strat: QueryStrategy,
|
|
175
|
+
) -> dict[str, list[tuple[str, float]]]:
|
|
176
|
+
"""Run active retrieval channels. Respects disabled_channels config for ablation."""
|
|
177
|
+
out: dict[str, list[tuple[str, float]]] = {}
|
|
178
|
+
# Skip channels listed in disabled_channels (ablation support)
|
|
179
|
+
disabled = set(self._config.disabled_channels)
|
|
180
|
+
|
|
181
|
+
if self._semantic is not None and self._embedder is not None and "semantic" not in disabled:
|
|
182
|
+
try:
|
|
183
|
+
q_emb = self._embedder.embed(query)
|
|
184
|
+
r = self._semantic.search(q_emb, profile_id, self._config.semantic_top_k)
|
|
185
|
+
if r:
|
|
186
|
+
out["semantic"] = r
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
logger.warning("Semantic channel: %s", exc)
|
|
189
|
+
|
|
190
|
+
if self._bm25 is not None and "bm25" not in disabled:
|
|
191
|
+
try:
|
|
192
|
+
r = self._bm25.search(query, profile_id, self._config.bm25_top_k)
|
|
193
|
+
if r:
|
|
194
|
+
out["bm25"] = r
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
logger.warning("BM25 channel: %s", exc)
|
|
197
|
+
|
|
198
|
+
if self._entity is not None and "entity_graph" not in disabled:
|
|
199
|
+
try:
|
|
200
|
+
r = self._entity.search(query, profile_id, top_k=self._config.bm25_top_k)
|
|
201
|
+
if r:
|
|
202
|
+
out["entity_graph"] = r
|
|
203
|
+
except Exception as exc:
|
|
204
|
+
logger.warning("Entity channel: %s", exc)
|
|
205
|
+
|
|
206
|
+
if self._temporal is not None and "temporal" not in disabled:
|
|
207
|
+
try:
|
|
208
|
+
r = self._temporal.search(query, profile_id, top_k=self._config.bm25_top_k)
|
|
209
|
+
if r:
|
|
210
|
+
out["temporal"] = r
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
logger.warning("Temporal channel: %s", exc)
|
|
213
|
+
|
|
214
|
+
return out
|
|
215
|
+
|
|
216
|
+
# -- Fact loading -------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
def _load_facts(
|
|
219
|
+
self, fused: list[FusionResult], profile_id: str,
|
|
220
|
+
) -> dict[str, AtomicFact]:
|
|
221
|
+
needed = {fr.fact_id for fr in fused}
|
|
222
|
+
if not needed:
|
|
223
|
+
return {}
|
|
224
|
+
all_facts = self._db.get_all_facts(profile_id)
|
|
225
|
+
return {f.fact_id: f for f in all_facts if f.fact_id in needed}
|
|
226
|
+
|
|
227
|
+
# -- Cross-encoder rerank -----------------------------------------------
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _sigmoid(x: float) -> float:
|
|
231
|
+
"""Numerically stable sigmoid."""
|
|
232
|
+
x = max(-500.0, min(500.0, x))
|
|
233
|
+
return 1.0 / (1.0 + math.exp(-x))
|
|
234
|
+
|
|
235
|
+
def _apply_reranker(
|
|
236
|
+
self, query: str, fused: list[FusionResult],
|
|
237
|
+
fact_map: dict[str, AtomicFact],
|
|
238
|
+
alpha: float = 0.75,
|
|
239
|
+
) -> list[FusionResult]:
|
|
240
|
+
"""Rerank with blended CE + RRF scores (Bug 1 fix).
|
|
241
|
+
|
|
242
|
+
Blended: alpha * sigmoid(CE_score) + (1 - alpha) * rrf_score.
|
|
243
|
+
Speaker tags stripped before scoring (Bug 3 fix).
|
|
244
|
+
"""
|
|
245
|
+
# Bug 2 fix: score ALL candidates, not just top_k
|
|
246
|
+
candidates = [
|
|
247
|
+
(fact_map[fr.fact_id], fr.fused_score)
|
|
248
|
+
for fr in fused if fr.fact_id in fact_map
|
|
249
|
+
]
|
|
250
|
+
if not candidates:
|
|
251
|
+
return fused
|
|
252
|
+
|
|
253
|
+
# Bug 3 fix: strip speaker tags from content before CE scoring
|
|
254
|
+
clean_candidates: list[tuple[AtomicFact, float]] = []
|
|
255
|
+
for fact, score in candidates:
|
|
256
|
+
cleaned_content = re.sub(r'^\[[A-Za-z]+\]:\s*', '', fact.content)
|
|
257
|
+
clean_fact = AtomicFact(
|
|
258
|
+
fact_id=fact.fact_id, memory_id=fact.memory_id,
|
|
259
|
+
profile_id=fact.profile_id, content=cleaned_content,
|
|
260
|
+
fact_type=fact.fact_type, entities=fact.entities,
|
|
261
|
+
canonical_entities=fact.canonical_entities,
|
|
262
|
+
observation_date=fact.observation_date,
|
|
263
|
+
referenced_date=fact.referenced_date,
|
|
264
|
+
confidence=fact.confidence, importance=fact.importance,
|
|
265
|
+
evidence_count=fact.evidence_count,
|
|
266
|
+
access_count=fact.access_count,
|
|
267
|
+
embedding=fact.embedding, created_at=fact.created_at,
|
|
268
|
+
)
|
|
269
|
+
clean_candidates.append((clean_fact, score))
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
scored = self._reranker.rerank( # type: ignore[union-attr]
|
|
273
|
+
query, clean_candidates, top_k=len(clean_candidates),
|
|
274
|
+
)
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
logger.warning("Cross-encoder rerank failed: %s", exc)
|
|
277
|
+
return fused
|
|
278
|
+
|
|
279
|
+
score_map = {fact.fact_id: score for fact, score in scored}
|
|
280
|
+
|
|
281
|
+
updated = [
|
|
282
|
+
FusionResult(
|
|
283
|
+
fact_id=fr.fact_id,
|
|
284
|
+
fused_score=(
|
|
285
|
+
alpha * self._sigmoid(score_map.get(fr.fact_id, 0.0))
|
|
286
|
+
+ (1.0 - alpha) * fr.fused_score
|
|
287
|
+
),
|
|
288
|
+
channel_ranks=fr.channel_ranks,
|
|
289
|
+
channel_scores=fr.channel_scores,
|
|
290
|
+
)
|
|
291
|
+
for fr in fused
|
|
292
|
+
]
|
|
293
|
+
updated.sort(key=lambda r: r.fused_score, reverse=True)
|
|
294
|
+
return updated
|
|
295
|
+
|
|
296
|
+
# -- Agentic adapter -----------------------------------
|
|
297
|
+
|
|
298
|
+
def recall_facts(
|
|
299
|
+
self, query: str, profile_id: str,
|
|
300
|
+
top_k: int = 20, skip_agentic: bool = True,
|
|
301
|
+
) -> list[tuple[AtomicFact, float]]:
|
|
302
|
+
"""Simplified recall returning (fact, score) tuples.
|
|
303
|
+
|
|
304
|
+
Used by AgenticRetriever for round-2 re-retrieval.
|
|
305
|
+
skip_agentic is always True here to prevent infinite recursion.
|
|
306
|
+
"""
|
|
307
|
+
response = self.recall(query, profile_id, limit=top_k)
|
|
308
|
+
return [(r.fact, r.score) for r in response.results]
|
|
309
|
+
|
|
310
|
+
# -- Trust weighting ----------------------------------------------------
|
|
311
|
+
|
|
312
|
+
def _get_trust_weight(self, fact: AtomicFact, profile_id: str) -> tuple[float, float]:
|
|
313
|
+
"""Look up Bayesian trust score and convert to a multiplicative weight.
|
|
314
|
+
|
|
315
|
+
Returns (trust_weight, raw_trust_score).
|
|
316
|
+
trust_weight is clamped to [0.5, 1.5]:
|
|
317
|
+
- trust=0.0 -> weight=0.5 (demote untrusted facts)
|
|
318
|
+
- trust=0.5 -> weight=1.0 (neutral, default prior)
|
|
319
|
+
- trust=1.0 -> weight=1.5 (promote highly trusted facts)
|
|
320
|
+
If trust scoring is disabled or unavailable, returns (1.0, 0.5).
|
|
321
|
+
"""
|
|
322
|
+
if not self._config.use_trust_weighting or self._trust_scorer is None:
|
|
323
|
+
return 1.0, 0.5
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
raw = self._trust_scorer.get_fact_trust(fact.fact_id, profile_id)
|
|
327
|
+
except Exception:
|
|
328
|
+
return 1.0, 0.5
|
|
329
|
+
|
|
330
|
+
# Linear map: trust 0.0->0.5, 0.5->1.0, 1.0->1.5
|
|
331
|
+
weight = 0.5 + raw # raw in [0, 1] -> weight in [0.5, 1.5]
|
|
332
|
+
return weight, raw
|
|
333
|
+
|
|
334
|
+
# -- Response building --------------------------------------------------
|
|
335
|
+
|
|
336
|
+
def _build_results(
|
|
337
|
+
self, fused: list[FusionResult], fact_map: dict[str, AtomicFact],
|
|
338
|
+
strat: QueryStrategy,
|
|
339
|
+
) -> list[RetrievalResult]:
|
|
340
|
+
from datetime import UTC, datetime
|
|
341
|
+
now = datetime.now(UTC)
|
|
342
|
+
results: list[RetrievalResult] = []
|
|
343
|
+
profile_id = next(
|
|
344
|
+
(f.profile_id for f in fact_map.values()), "default",
|
|
345
|
+
)
|
|
346
|
+
for fr in fused:
|
|
347
|
+
fact = fact_map.get(fr.fact_id)
|
|
348
|
+
if fact is None:
|
|
349
|
+
continue
|
|
350
|
+
evidence = [
|
|
351
|
+
f"{ch}(rank={rk}, score={fr.channel_scores.get(ch, 0.0):.4f})"
|
|
352
|
+
for ch, rk in sorted(fr.channel_ranks.items(), key=lambda x: x[1])
|
|
353
|
+
if rk < 1000
|
|
354
|
+
]
|
|
355
|
+
# Recency boost: recent facts get up to 1.1x, old facts 0.9x
|
|
356
|
+
age_days = 0.0
|
|
357
|
+
if fact.created_at:
|
|
358
|
+
try:
|
|
359
|
+
created = datetime.fromisoformat(fact.created_at.replace("Z", "+00:00"))
|
|
360
|
+
age_days = max(0.0, (now - created).total_seconds() / 86400.0)
|
|
361
|
+
except (ValueError, TypeError):
|
|
362
|
+
pass
|
|
363
|
+
recency = max(0.1, 1.0 - age_days / 365.0)
|
|
364
|
+
recency_boost = 1.0 + 0.2 * (recency - 0.5)
|
|
365
|
+
|
|
366
|
+
# Content quality: penalize short/low-info facts that rank high
|
|
367
|
+
# due to BM25 name-matching (greetings like "Hey Caroline!" score high
|
|
368
|
+
# on BM25 but have zero retrieval value)
|
|
369
|
+
content_len = len(fact.content.strip())
|
|
370
|
+
if content_len < 25:
|
|
371
|
+
quality = 0.1
|
|
372
|
+
elif content_len < 50:
|
|
373
|
+
quality = 0.5
|
|
374
|
+
elif content_len < 80:
|
|
375
|
+
quality = 0.8
|
|
376
|
+
else:
|
|
377
|
+
quality = 1.0
|
|
378
|
+
|
|
379
|
+
# Trust weighting: Bayesian trust modulates final ranking
|
|
380
|
+
trust_weight, raw_trust = self._get_trust_weight(fact, profile_id)
|
|
381
|
+
|
|
382
|
+
boosted_score = fr.fused_score * recency_boost * quality * trust_weight
|
|
383
|
+
confidence = min(1.0, boosted_score * 10.0) * fact.confidence
|
|
384
|
+
results.append(RetrievalResult(
|
|
385
|
+
fact=fact, score=boosted_score,
|
|
386
|
+
channel_scores=fr.channel_scores,
|
|
387
|
+
confidence=confidence, evidence_chain=evidence,
|
|
388
|
+
trust_score=raw_trust,
|
|
389
|
+
))
|
|
390
|
+
return results
|