superlocalmemory 2.8.6 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -1
- package/NOTICE +63 -0
- package/README.md +165 -480
- package/bin/slm +17 -449
- package/bin/slm-npm +62 -48
- 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,316 @@
|
|
|
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
|
+
"""LLM backbone — unified interface for LLM providers.
|
|
6
|
+
|
|
7
|
+
Supports OpenAI, Anthropic, Azure OpenAI, and Ollama via raw HTTP (httpx).
|
|
8
|
+
Falls back gracefully when no API key is configured — Mode A still works.
|
|
9
|
+
|
|
10
|
+
Providers:
|
|
11
|
+
- ``"ollama"``: Local Ollama (OpenAI-compatible, no auth needed).
|
|
12
|
+
- ``"openai"``: OpenAI API (GPT-4o, etc.).
|
|
13
|
+
- ``"anthropic"``: Anthropic API (Claude, etc.).
|
|
14
|
+
- ``"azure"``: Azure OpenAI (via AI Foundry deployment).
|
|
15
|
+
|
|
16
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
17
|
+
License: MIT
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import socket
|
|
25
|
+
import time
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import httpx
|
|
30
|
+
|
|
31
|
+
from superlocalmemory.core.config import LLMConfig
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Constants
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
_OPENAI_URL = "https://api.openai.com/v1/chat/completions"
|
|
40
|
+
_OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
|
|
41
|
+
_ANTHROPIC_URL = "https://api.anthropic.com/v1/messages"
|
|
42
|
+
_ANTHROPIC_API_VERSION = "2023-06-01"
|
|
43
|
+
_AZURE_API_VERSION = "2024-12-01-preview"
|
|
44
|
+
_OLLAMA_DEFAULT_BASE = "http://localhost:11434"
|
|
45
|
+
|
|
46
|
+
_ENV_KEYS: dict[str, str] = {
|
|
47
|
+
"openai": "OPENAI_API_KEY",
|
|
48
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
49
|
+
"azure": "AZURE_OPENAI_API_KEY",
|
|
50
|
+
"ollama": "OLLAMA_HOST",
|
|
51
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_SUPPORTED_PROVIDERS = frozenset({"openai", "anthropic", "azure", "ollama", "openrouter"})
|
|
55
|
+
|
|
56
|
+
_MAX_RETRIES = 3
|
|
57
|
+
_RETRY_BASE_DELAY = 1.0 # seconds, doubles each retry
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Exceptions
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
class LLMUnavailableError(Exception):
|
|
65
|
+
"""Raised when no API key is available for the configured provider."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Response dataclass
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class LLMResponse:
|
|
74
|
+
"""Immutable container for a single LLM generation result."""
|
|
75
|
+
|
|
76
|
+
text: str
|
|
77
|
+
model: str
|
|
78
|
+
tokens_used: int
|
|
79
|
+
latency_ms: float
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Main class
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
class LLMBackbone:
|
|
87
|
+
"""Unified LLM interface driven by LLMConfig.
|
|
88
|
+
|
|
89
|
+
All HTTP via httpx — no provider SDKs needed.
|
|
90
|
+
Includes retry logic (3 attempts, exponential backoff) and
|
|
91
|
+
socket-level timeout as a hard backstop for SSL hangs (S15 lesson).
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, config: LLMConfig) -> None:
|
|
95
|
+
if config.provider and config.provider not in _SUPPORTED_PROVIDERS:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Unsupported provider '{config.provider}'. "
|
|
98
|
+
f"Choose from {sorted(_SUPPORTED_PROVIDERS)}."
|
|
99
|
+
)
|
|
100
|
+
self._provider = config.provider
|
|
101
|
+
self._model = config.model
|
|
102
|
+
self._timeout = config.timeout_seconds
|
|
103
|
+
self._default_temperature = config.temperature
|
|
104
|
+
self._default_max_tokens = config.max_tokens
|
|
105
|
+
|
|
106
|
+
# Resolve API key: config > environment variable.
|
|
107
|
+
if self._provider == "ollama":
|
|
108
|
+
self._api_key = ""
|
|
109
|
+
host = config.api_base or os.environ.get(
|
|
110
|
+
"OLLAMA_HOST", _OLLAMA_DEFAULT_BASE,
|
|
111
|
+
)
|
|
112
|
+
self._base_url = f"{host.rstrip('/')}/v1/chat/completions"
|
|
113
|
+
elif self._provider == "openrouter":
|
|
114
|
+
self._api_key = config.api_key or os.environ.get(
|
|
115
|
+
_ENV_KEYS.get(self._provider, ""), "",
|
|
116
|
+
)
|
|
117
|
+
self._base_url = config.api_base or _OPENROUTER_URL
|
|
118
|
+
elif self._provider:
|
|
119
|
+
self._api_key = config.api_key or os.environ.get(
|
|
120
|
+
_ENV_KEYS.get(self._provider, ""), "",
|
|
121
|
+
)
|
|
122
|
+
self._base_url = config.api_base
|
|
123
|
+
else:
|
|
124
|
+
self._api_key = ""
|
|
125
|
+
self._base_url = ""
|
|
126
|
+
|
|
127
|
+
# -- Properties ---------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def is_available(self) -> bool:
|
|
130
|
+
"""True when the provider is ready for requests."""
|
|
131
|
+
if not self._provider:
|
|
132
|
+
return False
|
|
133
|
+
if self._provider == "ollama":
|
|
134
|
+
return True
|
|
135
|
+
return bool(self._api_key)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def provider(self) -> str:
|
|
139
|
+
return self._provider
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def model(self) -> str:
|
|
143
|
+
return self._model
|
|
144
|
+
|
|
145
|
+
# -- Core generation ----------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def generate(
|
|
148
|
+
self,
|
|
149
|
+
prompt: str,
|
|
150
|
+
system: str = "",
|
|
151
|
+
temperature: float | None = None,
|
|
152
|
+
max_tokens: int | None = None,
|
|
153
|
+
) -> str:
|
|
154
|
+
"""Send prompt to the LLM and return generated text.
|
|
155
|
+
|
|
156
|
+
Returns empty string on content-filter errors (Azure 400)
|
|
157
|
+
instead of crashing — lets callers continue gracefully.
|
|
158
|
+
Retries up to 3 times with exponential backoff on transient errors.
|
|
159
|
+
"""
|
|
160
|
+
if not self.is_available():
|
|
161
|
+
raise LLMUnavailableError(
|
|
162
|
+
f"No API key for provider '{self._provider}'. "
|
|
163
|
+
f"Set {_ENV_KEYS.get(self._provider, 'API_KEY')} or pass api_key=."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
temp = temperature if temperature is not None else self._default_temperature
|
|
167
|
+
tokens = max_tokens if max_tokens is not None else self._default_max_tokens
|
|
168
|
+
url, headers, payload = self._build_request(prompt, system, tokens, temp)
|
|
169
|
+
|
|
170
|
+
last_error: Exception | None = None
|
|
171
|
+
for attempt in range(_MAX_RETRIES):
|
|
172
|
+
try:
|
|
173
|
+
response = self._send(url, headers, payload)
|
|
174
|
+
return self._extract_text(response)
|
|
175
|
+
except httpx.HTTPStatusError as exc:
|
|
176
|
+
# Azure content filter returns 400 — not retryable.
|
|
177
|
+
if exc.response.status_code == 400:
|
|
178
|
+
logger.warning("Content filter or bad request (400). Returning empty.")
|
|
179
|
+
return ""
|
|
180
|
+
last_error = exc
|
|
181
|
+
except (httpx.TimeoutException, httpx.ConnectError) as exc:
|
|
182
|
+
last_error = exc
|
|
183
|
+
|
|
184
|
+
if attempt < _MAX_RETRIES - 1:
|
|
185
|
+
delay = _RETRY_BASE_DELAY * (2 ** attempt)
|
|
186
|
+
logger.info("Retry %d/%d after %.1fs", attempt + 1, _MAX_RETRIES, delay)
|
|
187
|
+
time.sleep(delay)
|
|
188
|
+
|
|
189
|
+
logger.error("All %d retries exhausted: %s", _MAX_RETRIES, last_error)
|
|
190
|
+
return ""
|
|
191
|
+
|
|
192
|
+
# -- HTTP transport -----------------------------------------------------
|
|
193
|
+
|
|
194
|
+
def _send(self, url: str, headers: dict, payload: dict) -> dict:
|
|
195
|
+
"""Execute HTTP POST with socket-level SSL backstop."""
|
|
196
|
+
old_default = socket.getdefaulttimeout()
|
|
197
|
+
socket.setdefaulttimeout(self._timeout + 30)
|
|
198
|
+
try:
|
|
199
|
+
timeout = httpx.Timeout(
|
|
200
|
+
connect=10.0,
|
|
201
|
+
read=self._timeout,
|
|
202
|
+
write=10.0,
|
|
203
|
+
pool=10.0,
|
|
204
|
+
)
|
|
205
|
+
with httpx.Client(timeout=timeout) as client:
|
|
206
|
+
resp = client.post(url, headers=headers, json=payload)
|
|
207
|
+
resp.raise_for_status()
|
|
208
|
+
return resp.json()
|
|
209
|
+
finally:
|
|
210
|
+
socket.setdefaulttimeout(old_default)
|
|
211
|
+
|
|
212
|
+
# -- Request builders ---------------------------------------------------
|
|
213
|
+
|
|
214
|
+
def _build_request(
|
|
215
|
+
self, prompt: str, system: str, max_tokens: int, temperature: float,
|
|
216
|
+
) -> tuple[str, dict[str, str], dict]:
|
|
217
|
+
"""Build provider-specific (url, headers, payload)."""
|
|
218
|
+
builders = {
|
|
219
|
+
"ollama": self._build_ollama,
|
|
220
|
+
"anthropic": self._build_anthropic,
|
|
221
|
+
"azure": self._build_azure,
|
|
222
|
+
}
|
|
223
|
+
builder = builders.get(self._provider, self._build_openai)
|
|
224
|
+
return builder(prompt, system, max_tokens, temperature)
|
|
225
|
+
|
|
226
|
+
def _build_openai(
|
|
227
|
+
self, prompt: str, system: str, max_tokens: int, temperature: float,
|
|
228
|
+
) -> tuple[str, dict[str, str], dict]:
|
|
229
|
+
messages = self._make_messages(system, prompt)
|
|
230
|
+
headers = {
|
|
231
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
232
|
+
"Content-Type": "application/json",
|
|
233
|
+
}
|
|
234
|
+
payload = {
|
|
235
|
+
"model": self._model,
|
|
236
|
+
"messages": messages,
|
|
237
|
+
"max_tokens": max_tokens,
|
|
238
|
+
"temperature": temperature,
|
|
239
|
+
}
|
|
240
|
+
url = self._base_url or _OPENAI_URL
|
|
241
|
+
return url, headers, payload
|
|
242
|
+
|
|
243
|
+
def _build_ollama(
|
|
244
|
+
self, prompt: str, system: str, max_tokens: int, temperature: float,
|
|
245
|
+
) -> tuple[str, dict[str, str], dict]:
|
|
246
|
+
messages = self._make_messages(system, prompt)
|
|
247
|
+
headers = {"Content-Type": "application/json"}
|
|
248
|
+
payload = {
|
|
249
|
+
"model": self._model,
|
|
250
|
+
"messages": messages,
|
|
251
|
+
"max_tokens": max_tokens,
|
|
252
|
+
"temperature": temperature,
|
|
253
|
+
}
|
|
254
|
+
return self._base_url, headers, payload
|
|
255
|
+
|
|
256
|
+
def _build_anthropic(
|
|
257
|
+
self, prompt: str, system: str, max_tokens: int, temperature: float,
|
|
258
|
+
) -> tuple[str, dict[str, str], dict]:
|
|
259
|
+
headers = {
|
|
260
|
+
"x-api-key": self._api_key,
|
|
261
|
+
"anthropic-version": _ANTHROPIC_API_VERSION,
|
|
262
|
+
"Content-Type": "application/json",
|
|
263
|
+
}
|
|
264
|
+
payload: dict[str, Any] = {
|
|
265
|
+
"model": self._model,
|
|
266
|
+
"max_tokens": max_tokens,
|
|
267
|
+
"temperature": temperature,
|
|
268
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
269
|
+
}
|
|
270
|
+
if system:
|
|
271
|
+
payload["system"] = system
|
|
272
|
+
return _ANTHROPIC_URL, headers, payload
|
|
273
|
+
|
|
274
|
+
def _build_azure(
|
|
275
|
+
self, prompt: str, system: str, max_tokens: int, temperature: float,
|
|
276
|
+
) -> tuple[str, dict[str, str], dict]:
|
|
277
|
+
if not self._base_url:
|
|
278
|
+
raise ValueError("Azure provider requires api_base URL.")
|
|
279
|
+
url = (
|
|
280
|
+
f"{self._base_url.rstrip('/')}/openai/deployments/"
|
|
281
|
+
f"{self._model}/chat/completions"
|
|
282
|
+
f"?api-version={_AZURE_API_VERSION}"
|
|
283
|
+
)
|
|
284
|
+
messages = self._make_messages(system, prompt)
|
|
285
|
+
headers = {"api-key": self._api_key, "Content-Type": "application/json"}
|
|
286
|
+
payload: dict[str, Any] = {"messages": messages}
|
|
287
|
+
if "gpt-5" in self._model.lower():
|
|
288
|
+
payload["max_completion_tokens"] = max(max_tokens, 200)
|
|
289
|
+
payload["reasoning_effort"] = "none"
|
|
290
|
+
if temperature > 0:
|
|
291
|
+
payload["temperature"] = temperature
|
|
292
|
+
else:
|
|
293
|
+
payload["max_tokens"] = max_tokens
|
|
294
|
+
payload["temperature"] = temperature
|
|
295
|
+
return url, headers, payload
|
|
296
|
+
|
|
297
|
+
# -- Response parsing ---------------------------------------------------
|
|
298
|
+
|
|
299
|
+
def _extract_text(self, data: dict) -> str:
|
|
300
|
+
"""Extract text from provider-specific JSON response."""
|
|
301
|
+
if self._provider == "anthropic":
|
|
302
|
+
return data.get("content", [{}])[0].get("text", "").strip()
|
|
303
|
+
# OpenAI / Azure / Ollama share response format.
|
|
304
|
+
choices = data.get("choices", [{}])
|
|
305
|
+
return choices[0].get("message", {}).get("content", "").strip()
|
|
306
|
+
|
|
307
|
+
# -- Helpers ------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def _make_messages(system: str, prompt: str) -> list[dict[str, str]]:
|
|
311
|
+
"""Build messages array with optional system message."""
|
|
312
|
+
messages: list[dict[str, str]] = []
|
|
313
|
+
if system:
|
|
314
|
+
messages.append({"role": "system", "content": system})
|
|
315
|
+
messages.append({"role": "user", "content": prompt})
|
|
316
|
+
return messages
|
|
File without changes
|
|
@@ -0,0 +1,356 @@
|
|
|
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
|
+
"""Fisher-Rao geodesic metric with Bayesian variance update.
|
|
6
|
+
|
|
7
|
+
Information-geometric similarity on the statistical manifold of diagonal
|
|
8
|
+
Gaussians. Two embeddings with the same mean but different uncertainty
|
|
9
|
+
have zero cosine distance but nonzero Fisher distance --- this is the
|
|
10
|
+
core insight that differentiates Fisher-Rao from cosine similarity.
|
|
11
|
+
|
|
12
|
+
Geodesic distance (Atkinson & Mitchell 1981, Pinele et al. 2020):
|
|
13
|
+
|
|
14
|
+
Univariate component i:
|
|
15
|
+
delta_i = ((mu1_i - mu2_i)^2 + 2*(sigma1_i - sigma2_i)^2)
|
|
16
|
+
/ (4 * sigma1_i * sigma2_i)
|
|
17
|
+
d_i = sqrt(2) * arccosh(1 + delta_i)
|
|
18
|
+
|
|
19
|
+
Diagonal multivariate (product-manifold decomposition):
|
|
20
|
+
d_FR(p, q) = sqrt( sum_i d_i^2 )
|
|
21
|
+
|
|
22
|
+
Bayesian variance update (NEW in Innovation Wave 4):
|
|
23
|
+
|
|
24
|
+
V1 bug: query always received UNIFORM variance, so Fisher degenerated
|
|
25
|
+
to a monotonic transform of cosine. FIX: every fact maintains its own
|
|
26
|
+
variance vector that narrows on each observation.
|
|
27
|
+
|
|
28
|
+
new_var_i = 1 / (1/old_var_i + 1/obs_var_i)
|
|
29
|
+
|
|
30
|
+
Multiple consistent observations reduce variance, increasing confidence.
|
|
31
|
+
This gives Fisher-Rao a genuine ranking advantage over cosine: well-
|
|
32
|
+
confirmed memories have tighter distributions and score higher when the
|
|
33
|
+
query matches.
|
|
34
|
+
|
|
35
|
+
References:
|
|
36
|
+
Rao C R (1945). Information and the accuracy attainable in the
|
|
37
|
+
estimation of statistical parameters. Bull. Calcutta Math. Soc.
|
|
38
|
+
Atkinson C & Mitchell A (1981). Rao's distance measure.
|
|
39
|
+
Sankhya: The Indian Journal of Statistics, Series A.
|
|
40
|
+
Pinele J, Strapasson J E & Costa S I R (2020). The Fisher-Rao
|
|
41
|
+
distance between multivariate normal distributions: special
|
|
42
|
+
cases, bounds and applications. Entropy 22(4):404.
|
|
43
|
+
|
|
44
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
45
|
+
License: MIT
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from __future__ import annotations
|
|
49
|
+
|
|
50
|
+
import math
|
|
51
|
+
|
|
52
|
+
import numpy as np
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Constants
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
_VARIANCE_FLOOR: float = 0.05 # Minimum per-dimension variance (40x dynamic range)
|
|
59
|
+
_VARIANCE_CEIL: float = 2.0 # Maximum per-dimension variance (initial uncertainty)
|
|
60
|
+
_DEFAULT_TEMPERATURE: float = 15.0
|
|
61
|
+
_SMALL_DELTA_THRESHOLD: float = 1e-7
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Public API
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
class FisherRaoMetric:
|
|
69
|
+
"""Fisher-Rao geodesic metric with Bayesian variance tracking.
|
|
70
|
+
|
|
71
|
+
Each fact is modelled as a diagonal Gaussian N(mu, diag(sigma^2)).
|
|
72
|
+
Distance is the Fisher-Rao geodesic on this statistical manifold.
|
|
73
|
+
Variance starts high (uncertain) and NARROWS on repeated access via
|
|
74
|
+
Bayesian update, giving well-confirmed memories a ranking advantage.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
temperature: Softmax scaling for similarity conversion.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
__slots__ = ("temperature",)
|
|
81
|
+
|
|
82
|
+
def __init__(self, temperature: float = _DEFAULT_TEMPERATURE) -> None:
|
|
83
|
+
if temperature <= 0:
|
|
84
|
+
raise ValueError(f"temperature must be positive, got {temperature}")
|
|
85
|
+
self.temperature = temperature
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
# Parameter derivation
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def compute_params(
|
|
92
|
+
self,
|
|
93
|
+
embedding: list[float],
|
|
94
|
+
) -> tuple[list[float], list[float]]:
|
|
95
|
+
"""Derive Fisher parameters (mean, variance) from a raw embedding.
|
|
96
|
+
|
|
97
|
+
Mean is the L2-normalised embedding. Variance is content-derived
|
|
98
|
+
and *heterogeneous*: dimensions with strong signal get low variance,
|
|
99
|
+
weak-signal dimensions get high variance.
|
|
100
|
+
|
|
101
|
+
Mapping: ``var_i = CEIL - (CEIL - FLOOR) * |normed_i| / max_abs``
|
|
102
|
+
Strong signal (large |normed_i|) -> low variance (high confidence).
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
embedding: Raw embedding vector, any dimensionality.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
(mean, variance) as plain Python lists, same length as input.
|
|
109
|
+
"""
|
|
110
|
+
arr = np.asarray(embedding, dtype=np.float64)
|
|
111
|
+
norm = np.linalg.norm(arr)
|
|
112
|
+
if norm < 1e-12:
|
|
113
|
+
mean = np.zeros_like(arr)
|
|
114
|
+
variance = np.full_like(arr, _VARIANCE_CEIL)
|
|
115
|
+
return mean.tolist(), variance.tolist()
|
|
116
|
+
|
|
117
|
+
mean = arr / norm
|
|
118
|
+
|
|
119
|
+
abs_vals = np.abs(mean)
|
|
120
|
+
max_abs = float(np.max(abs_vals))
|
|
121
|
+
if max_abs < 1e-12:
|
|
122
|
+
max_abs = 1.0
|
|
123
|
+
|
|
124
|
+
normalised_signal = abs_vals / max_abs # in [0, 1]
|
|
125
|
+
variance = _VARIANCE_CEIL - (_VARIANCE_CEIL - _VARIANCE_FLOOR) * normalised_signal
|
|
126
|
+
variance = np.clip(variance, _VARIANCE_FLOOR, _VARIANCE_CEIL)
|
|
127
|
+
|
|
128
|
+
return mean.tolist(), variance.tolist()
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Geodesic distance
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def distance(
|
|
135
|
+
self,
|
|
136
|
+
mean_a: list[float],
|
|
137
|
+
var_a: list[float],
|
|
138
|
+
mean_b: list[float],
|
|
139
|
+
var_b: list[float],
|
|
140
|
+
) -> float:
|
|
141
|
+
"""Exact Fisher-Rao geodesic distance between diagonal Gaussians.
|
|
142
|
+
|
|
143
|
+
Uses Atkinson & Mitchell (1981) per-component closed form with
|
|
144
|
+
Pinele et al. (2020) product-manifold decomposition.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
mean_a: Mean of distribution A.
|
|
148
|
+
var_a: Per-dimension variance of A (strictly positive).
|
|
149
|
+
mean_b: Mean of distribution B.
|
|
150
|
+
var_b: Per-dimension variance of B (strictly positive).
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Non-negative geodesic distance.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ValueError: On NaN, non-positive variance, or length mismatch.
|
|
157
|
+
"""
|
|
158
|
+
mu1 = np.asarray(mean_a, dtype=np.float64)
|
|
159
|
+
sig1 = np.asarray(var_a, dtype=np.float64)
|
|
160
|
+
mu2 = np.asarray(mean_b, dtype=np.float64)
|
|
161
|
+
sig2 = np.asarray(var_b, dtype=np.float64)
|
|
162
|
+
|
|
163
|
+
_validate(mu1, sig1, mu2, sig2)
|
|
164
|
+
|
|
165
|
+
# Per-component: delta_i = ((mu1-mu2)^2 + 2*(sigma1-sigma2)^2) / (4*s1*s2)
|
|
166
|
+
mu_diff_sq = (mu1 - mu2) ** 2
|
|
167
|
+
sig_diff_sq = (sig1 - sig2) ** 2
|
|
168
|
+
product = sig1 * sig2
|
|
169
|
+
|
|
170
|
+
delta = (mu_diff_sq + 2.0 * sig_diff_sq) / (4.0 * product)
|
|
171
|
+
|
|
172
|
+
# Per-component squared distance: 2 * arccosh(1 + delta_i)^2
|
|
173
|
+
acosh_vals = _stable_arccosh_1p_vec(delta)
|
|
174
|
+
total_dist_sq = float(np.sum(2.0 * acosh_vals ** 2))
|
|
175
|
+
|
|
176
|
+
return math.sqrt(max(total_dist_sq, 0.0))
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
# Similarity (exponential kernel)
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def similarity(
|
|
183
|
+
self,
|
|
184
|
+
mean_a: list[float],
|
|
185
|
+
var_a: list[float],
|
|
186
|
+
mean_b: list[float],
|
|
187
|
+
var_b: list[float],
|
|
188
|
+
) -> float:
|
|
189
|
+
"""Convert Fisher-Rao distance to similarity in [0, 1].
|
|
190
|
+
|
|
191
|
+
sim = exp(-distance / temperature)
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
mean_a: Mean of distribution A.
|
|
195
|
+
var_a: Per-dimension variance of A.
|
|
196
|
+
mean_b: Mean of distribution B.
|
|
197
|
+
var_b: Per-dimension variance of B.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Similarity in [0, 1]. 1 = identical, 0 = maximally different.
|
|
201
|
+
"""
|
|
202
|
+
d = self.distance(mean_a, var_a, mean_b, var_b)
|
|
203
|
+
return float(np.exp(-d / self.temperature))
|
|
204
|
+
|
|
205
|
+
# ------------------------------------------------------------------
|
|
206
|
+
# Bayesian variance update (THE key V1 fix)
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def bayesian_update(
|
|
210
|
+
self,
|
|
211
|
+
old_var: list[float],
|
|
212
|
+
observation_var: list[float],
|
|
213
|
+
) -> list[float]:
|
|
214
|
+
"""Bayesian precision-additive variance update.
|
|
215
|
+
|
|
216
|
+
Each observation tightens the posterior variance:
|
|
217
|
+
|
|
218
|
+
1/new_var_i = 1/old_var_i + 1/obs_var_i
|
|
219
|
+
|
|
220
|
+
This is the standard conjugate update for a Gaussian likelihood
|
|
221
|
+
with known mean and unknown precision. After *k* observations
|
|
222
|
+
with identical variance sigma^2:
|
|
223
|
+
|
|
224
|
+
var_k = sigma^2 / k
|
|
225
|
+
|
|
226
|
+
So variance shrinks as 1/k --- giving well-confirmed memories
|
|
227
|
+
significantly tighter distributions and higher Fisher similarity
|
|
228
|
+
to matching queries.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
old_var: Current per-dimension variance.
|
|
232
|
+
observation_var: Variance of the new observation (derived from
|
|
233
|
+
the embedding of the confirming content).
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Updated variance (strictly within [FLOOR, CEIL]).
|
|
237
|
+
"""
|
|
238
|
+
old = np.asarray(old_var, dtype=np.float64)
|
|
239
|
+
obs = np.asarray(observation_var, dtype=np.float64)
|
|
240
|
+
|
|
241
|
+
if old.shape != obs.shape:
|
|
242
|
+
raise ValueError(
|
|
243
|
+
f"Variance shape mismatch: {old.shape} vs {obs.shape}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Clamp inputs to valid range before update
|
|
247
|
+
old = np.clip(old, _VARIANCE_FLOOR, _VARIANCE_CEIL)
|
|
248
|
+
obs = np.clip(obs, _VARIANCE_FLOOR, _VARIANCE_CEIL)
|
|
249
|
+
|
|
250
|
+
# Precision-additive update: 1/new = 1/old + 1/obs
|
|
251
|
+
new_precision = (1.0 / old) + (1.0 / obs)
|
|
252
|
+
new_var = 1.0 / new_precision
|
|
253
|
+
|
|
254
|
+
new_var = np.clip(new_var, _VARIANCE_FLOOR, _VARIANCE_CEIL)
|
|
255
|
+
return new_var.tolist()
|
|
256
|
+
|
|
257
|
+
# ------------------------------------------------------------------
|
|
258
|
+
# Adaptive temperature
|
|
259
|
+
# ------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def adaptive_temperature(
|
|
262
|
+
self,
|
|
263
|
+
variances: list[list[float]],
|
|
264
|
+
) -> float:
|
|
265
|
+
"""Compute data-driven temperature from corpus variance statistics.
|
|
266
|
+
|
|
267
|
+
Instead of a fixed temperature, adapt to the actual spread of
|
|
268
|
+
variances in the memory store. High average variance (uncertain
|
|
269
|
+
corpus) -> higher temperature (softer discrimination). Low average
|
|
270
|
+
variance (well-confirmed corpus) -> lower temperature (sharper).
|
|
271
|
+
|
|
272
|
+
Formula:
|
|
273
|
+
T = base_T * (1 + avg_variance) / 2
|
|
274
|
+
|
|
275
|
+
This ensures T stays close to base_T when avg_variance ~ 1.0
|
|
276
|
+
(the typical midpoint) and scales proportionally.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
variances: List of per-fact variance vectors.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Adapted temperature (always positive).
|
|
283
|
+
"""
|
|
284
|
+
if not variances:
|
|
285
|
+
return self.temperature
|
|
286
|
+
|
|
287
|
+
all_vars = np.array(variances, dtype=np.float64)
|
|
288
|
+
avg = float(np.mean(all_vars))
|
|
289
|
+
|
|
290
|
+
adapted = self.temperature * (1.0 + avg) / 2.0
|
|
291
|
+
return max(adapted, 0.1) # floor to prevent division issues
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
# Validation
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
def _validate(
|
|
299
|
+
mu1: np.ndarray,
|
|
300
|
+
sig1: np.ndarray,
|
|
301
|
+
mu2: np.ndarray,
|
|
302
|
+
sig2: np.ndarray,
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Validate Fisher metric inputs."""
|
|
305
|
+
for name, arr in [("mu1", mu1), ("sig1", sig1), ("mu2", mu2), ("sig2", sig2)]:
|
|
306
|
+
if np.any(np.isnan(arr)):
|
|
307
|
+
raise ValueError(f"{name} contains NaN")
|
|
308
|
+
|
|
309
|
+
if mu1.shape != mu2.shape:
|
|
310
|
+
raise ValueError(f"Mean shape mismatch: {mu1.shape} vs {mu2.shape}")
|
|
311
|
+
if sig1.shape != sig2.shape:
|
|
312
|
+
raise ValueError(f"Sigma shape mismatch: {sig1.shape} vs {sig2.shape}")
|
|
313
|
+
if mu1.shape[0] != sig1.shape[0]:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"Mean/sigma length mismatch: {mu1.shape[0]} vs {sig1.shape[0]}"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if np.any(sig1 <= 0):
|
|
319
|
+
raise ValueError("sig1 must be strictly positive")
|
|
320
|
+
if np.any(sig2 <= 0):
|
|
321
|
+
raise ValueError("sig2 must be strictly positive")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
# Numerically stable arccosh
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
def _stable_arccosh_1p_vec(delta: np.ndarray) -> np.ndarray:
|
|
329
|
+
"""Vectorised stable arccosh(1 + delta).
|
|
330
|
+
|
|
331
|
+
For small delta, uses Taylor expansion to avoid catastrophic
|
|
332
|
+
cancellation: arccosh(1+d) ~ sqrt(2d) * (1 - d/12).
|
|
333
|
+
For larger delta, uses the identity:
|
|
334
|
+
arccosh(1+d) = log(1 + d + sqrt(d*(d+2))).
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
delta: Non-negative array.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
arccosh(1 + delta) element-wise.
|
|
341
|
+
"""
|
|
342
|
+
delta = np.maximum(delta, 0.0)
|
|
343
|
+
result = np.empty_like(delta)
|
|
344
|
+
|
|
345
|
+
small = delta < _SMALL_DELTA_THRESHOLD
|
|
346
|
+
large = ~small
|
|
347
|
+
|
|
348
|
+
if np.any(small):
|
|
349
|
+
d_s = delta[small]
|
|
350
|
+
result[small] = np.sqrt(2.0 * d_s) * (1.0 - d_s / 12.0)
|
|
351
|
+
|
|
352
|
+
if np.any(large):
|
|
353
|
+
d_l = delta[large]
|
|
354
|
+
result[large] = np.log1p(d_l + np.sqrt(d_l * (d_l + 2.0)))
|
|
355
|
+
|
|
356
|
+
return result
|