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,485 @@
|
|
|
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 — Memory Consolidator.
|
|
6
|
+
|
|
7
|
+
Mem0-style ADD/UPDATE/SUPERSEDE/NOOP logic for incoming facts.
|
|
8
|
+
V1 was append-only (never updated, never deleted, never merged).
|
|
9
|
+
This module gives a ~26% uplift by deduplicating, updating, and
|
|
10
|
+
resolving contradictions at encoding time.
|
|
11
|
+
|
|
12
|
+
Mode A: keyword-based contradiction detection (zero LLM).
|
|
13
|
+
Mode B/C: LLM-assisted contradiction detection when available.
|
|
14
|
+
|
|
15
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
16
|
+
License: MIT
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import math
|
|
23
|
+
from typing import Any, Protocol
|
|
24
|
+
|
|
25
|
+
from superlocalmemory.core.config import EncodingConfig
|
|
26
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
27
|
+
from superlocalmemory.storage.models import (
|
|
28
|
+
AtomicFact,
|
|
29
|
+
ConsolidationAction,
|
|
30
|
+
ConsolidationActionType,
|
|
31
|
+
EdgeType,
|
|
32
|
+
GraphEdge,
|
|
33
|
+
MemoryLifecycle,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Protocols (avoid tight coupling to concrete classes)
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
class Embedder(Protocol):
|
|
43
|
+
"""Anything that produces an embedding vector from text."""
|
|
44
|
+
|
|
45
|
+
def encode(self, text: str) -> list[float]: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LLM(Protocol):
|
|
49
|
+
"""Anything that can generate text from a prompt."""
|
|
50
|
+
|
|
51
|
+
def generate(self, prompt: str, system: str = "") -> str: ...
|
|
52
|
+
|
|
53
|
+
def is_available(self) -> bool: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Negation patterns for Mode A keyword contradiction detection
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
_NEGATION_MARKERS: frozenset[str] = frozenset({
|
|
61
|
+
"not", "no longer", "never", "stopped", "quit", "left",
|
|
62
|
+
"changed", "moved", "divorced", "fired", "resigned",
|
|
63
|
+
"broke up", "ended", "cancelled", "dropped", "switched",
|
|
64
|
+
"former", "ex-", "previously", "used to", "no more",
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
# Score thresholds (match EncodingConfig defaults)
|
|
68
|
+
_NOOP_THRESHOLD = 0.95
|
|
69
|
+
_MATCH_THRESHOLD = 0.85
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# MemoryConsolidator
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
class MemoryConsolidator:
|
|
77
|
+
"""Decides ADD/UPDATE/SUPERSEDE/NOOP for each incoming fact.
|
|
78
|
+
|
|
79
|
+
For each new fact the consolidator:
|
|
80
|
+
1. Finds candidate matches via entity overlap + semantic similarity.
|
|
81
|
+
2. Scores each candidate (Jaccard entity + cosine embedding).
|
|
82
|
+
3. Classifies the relationship and executes the action.
|
|
83
|
+
4. Logs every decision in ``consolidation_log``.
|
|
84
|
+
|
|
85
|
+
Thread-safe — delegates all DB ops to DatabaseManager which holds a lock.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
db: DatabaseManager,
|
|
91
|
+
embedder: Embedder | None = None,
|
|
92
|
+
llm: LLM | None = None,
|
|
93
|
+
config: EncodingConfig | None = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
self._db = db
|
|
96
|
+
self._embedder = embedder
|
|
97
|
+
self._llm = llm
|
|
98
|
+
self._cfg = config or EncodingConfig()
|
|
99
|
+
|
|
100
|
+
# -- Public API ---------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def consolidate(
|
|
103
|
+
self, new_fact: AtomicFact, profile_id: str,
|
|
104
|
+
) -> ConsolidationAction:
|
|
105
|
+
"""Consolidate *new_fact* against existing knowledge.
|
|
106
|
+
|
|
107
|
+
Returns a ``ConsolidationAction`` describing what was done.
|
|
108
|
+
"""
|
|
109
|
+
candidates = self._find_candidates(new_fact, profile_id)
|
|
110
|
+
|
|
111
|
+
if not candidates:
|
|
112
|
+
return self._execute_add(new_fact, profile_id, reason="no matching facts")
|
|
113
|
+
|
|
114
|
+
best_fact, best_score = candidates[0]
|
|
115
|
+
|
|
116
|
+
if best_score > _NOOP_THRESHOLD:
|
|
117
|
+
# Even near-duplicates might be contradictions (e.g. negation).
|
|
118
|
+
# Check contradiction BEFORE declaring NOOP.
|
|
119
|
+
if self._is_contradicting(new_fact, best_fact):
|
|
120
|
+
return self._execute_supersede(
|
|
121
|
+
new_fact, best_fact, profile_id,
|
|
122
|
+
reason=f"contradiction detected (score={best_score:.3f})",
|
|
123
|
+
)
|
|
124
|
+
return self._execute_noop(
|
|
125
|
+
new_fact, best_fact, profile_id,
|
|
126
|
+
reason=f"near-duplicate (score={best_score:.3f})",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if best_score > _MATCH_THRESHOLD:
|
|
130
|
+
if self._is_contradicting(new_fact, best_fact):
|
|
131
|
+
return self._execute_supersede(
|
|
132
|
+
new_fact, best_fact, profile_id,
|
|
133
|
+
reason=f"contradiction detected (score={best_score:.3f})",
|
|
134
|
+
)
|
|
135
|
+
if new_fact.fact_type == best_fact.fact_type:
|
|
136
|
+
return self._execute_update(
|
|
137
|
+
new_fact, best_fact, profile_id,
|
|
138
|
+
reason=f"refines existing (score={best_score:.3f})",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return self._execute_add(
|
|
142
|
+
new_fact, profile_id,
|
|
143
|
+
reason=f"new information (best_score={best_score:.3f})",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def get_consolidation_history(
|
|
147
|
+
self, profile_id: str, limit: int = 50,
|
|
148
|
+
) -> list[ConsolidationAction]:
|
|
149
|
+
"""Recent consolidation actions for audit/debugging."""
|
|
150
|
+
rows = self._db.execute(
|
|
151
|
+
"SELECT * FROM consolidation_log WHERE profile_id = ? "
|
|
152
|
+
"ORDER BY timestamp DESC LIMIT ?",
|
|
153
|
+
(profile_id, limit),
|
|
154
|
+
)
|
|
155
|
+
return [
|
|
156
|
+
ConsolidationAction(
|
|
157
|
+
action_id=(d := dict(r))["action_id"],
|
|
158
|
+
profile_id=d["profile_id"],
|
|
159
|
+
action_type=ConsolidationActionType(d["action_type"]),
|
|
160
|
+
new_fact_id=d["new_fact_id"],
|
|
161
|
+
existing_fact_id=d["existing_fact_id"],
|
|
162
|
+
reason=d["reason"],
|
|
163
|
+
timestamp=d["timestamp"],
|
|
164
|
+
)
|
|
165
|
+
for r in rows
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
# -- Candidate search ---------------------------------------------------
|
|
169
|
+
|
|
170
|
+
def _find_candidates(
|
|
171
|
+
self, new_fact: AtomicFact, profile_id: str,
|
|
172
|
+
) -> list[tuple[AtomicFact, float]]:
|
|
173
|
+
"""Find and score candidate matches from existing facts.
|
|
174
|
+
|
|
175
|
+
Uses two signals:
|
|
176
|
+
1. Entity overlap — ``get_facts_by_entity`` for each canonical entity.
|
|
177
|
+
2. Semantic similarity — cosine of embedding vectors.
|
|
178
|
+
|
|
179
|
+
Returns sorted list of (fact, combined_score), descending.
|
|
180
|
+
"""
|
|
181
|
+
seen_ids: set[str] = set()
|
|
182
|
+
candidate_facts: list[AtomicFact] = []
|
|
183
|
+
|
|
184
|
+
# --- entity-based candidates ---
|
|
185
|
+
for entity in new_fact.canonical_entities:
|
|
186
|
+
for fact in self._db.get_facts_by_entity(entity, profile_id):
|
|
187
|
+
if fact.fact_id not in seen_ids:
|
|
188
|
+
seen_ids.add(fact.fact_id)
|
|
189
|
+
candidate_facts.append(fact)
|
|
190
|
+
|
|
191
|
+
# --- semantic candidates (top-K by embedding) ---
|
|
192
|
+
if new_fact.embedding is not None and self._embedder is not None:
|
|
193
|
+
all_facts = self._db.get_all_facts(profile_id)
|
|
194
|
+
semantic_scored: list[tuple[AtomicFact, float]] = []
|
|
195
|
+
for fact in all_facts:
|
|
196
|
+
if fact.fact_id in seen_ids:
|
|
197
|
+
continue
|
|
198
|
+
sim = _compute_similarity(new_fact.embedding, fact.embedding)
|
|
199
|
+
if sim > 0.5:
|
|
200
|
+
semantic_scored.append((fact, sim))
|
|
201
|
+
semantic_scored.sort(key=lambda t: t[1], reverse=True)
|
|
202
|
+
for fact, _ in semantic_scored[: self._cfg.max_consolidation_candidates]:
|
|
203
|
+
if fact.fact_id not in seen_ids:
|
|
204
|
+
seen_ids.add(fact.fact_id)
|
|
205
|
+
candidate_facts.append(fact)
|
|
206
|
+
|
|
207
|
+
# --- score all candidates ---
|
|
208
|
+
scored: list[tuple[AtomicFact, float]] = []
|
|
209
|
+
for cand in candidate_facts:
|
|
210
|
+
entity_overlap = _jaccard(
|
|
211
|
+
set(new_fact.canonical_entities),
|
|
212
|
+
set(cand.canonical_entities),
|
|
213
|
+
)
|
|
214
|
+
semantic_sim = _compute_similarity(
|
|
215
|
+
new_fact.embedding, cand.embedding,
|
|
216
|
+
)
|
|
217
|
+
combined = 0.4 * entity_overlap + 0.6 * semantic_sim
|
|
218
|
+
scored.append((cand, combined))
|
|
219
|
+
|
|
220
|
+
scored.sort(key=lambda t: t[1], reverse=True)
|
|
221
|
+
return scored
|
|
222
|
+
|
|
223
|
+
# -- Contradiction detection --------------------------------------------
|
|
224
|
+
|
|
225
|
+
def _is_contradicting(
|
|
226
|
+
self, fact_a: AtomicFact, fact_b: AtomicFact,
|
|
227
|
+
) -> bool:
|
|
228
|
+
"""Detect if *fact_a* contradicts *fact_b*.
|
|
229
|
+
|
|
230
|
+
Mode B/C: delegates to LLM for nuanced judgment.
|
|
231
|
+
Mode A: keyword-based negation detection.
|
|
232
|
+
"""
|
|
233
|
+
if self._llm is not None and self._llm.is_available():
|
|
234
|
+
return self._llm_contradiction_check(fact_a, fact_b)
|
|
235
|
+
return self._keyword_contradiction_check(fact_a, fact_b)
|
|
236
|
+
|
|
237
|
+
def _keyword_contradiction_check(
|
|
238
|
+
self, fact_a: AtomicFact, fact_b: AtomicFact,
|
|
239
|
+
) -> bool:
|
|
240
|
+
"""Heuristic: check negation markers in either fact's content."""
|
|
241
|
+
text_a = fact_a.content.lower()
|
|
242
|
+
text_b = fact_b.content.lower()
|
|
243
|
+
for marker in _NEGATION_MARKERS:
|
|
244
|
+
if marker in text_a and marker not in text_b:
|
|
245
|
+
return True
|
|
246
|
+
if marker in text_b and marker not in text_a:
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
# Opposing emotional valence with same entities
|
|
250
|
+
shared_entities = set(fact_a.canonical_entities) & set(fact_b.canonical_entities)
|
|
251
|
+
if shared_entities:
|
|
252
|
+
valence_diff = abs(fact_a.emotional_valence - fact_b.emotional_valence)
|
|
253
|
+
if valence_diff > 1.2:
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def _llm_contradiction_check(
|
|
259
|
+
self, fact_a: AtomicFact, fact_b: AtomicFact,
|
|
260
|
+
) -> bool:
|
|
261
|
+
"""Ask the LLM whether two facts contradict each other."""
|
|
262
|
+
assert self._llm is not None # guarded by caller
|
|
263
|
+
prompt = (
|
|
264
|
+
"Do these two statements contradict each other?\n\n"
|
|
265
|
+
f"Statement A: {fact_a.content}\n"
|
|
266
|
+
f"Statement B: {fact_b.content}\n\n"
|
|
267
|
+
"Answer ONLY 'yes' or 'no'."
|
|
268
|
+
)
|
|
269
|
+
response = self._llm.generate(
|
|
270
|
+
prompt, system="You are a precise fact-checker.",
|
|
271
|
+
)
|
|
272
|
+
return response.strip().lower().startswith("yes")
|
|
273
|
+
|
|
274
|
+
# -- Action executors ---------------------------------------------------
|
|
275
|
+
|
|
276
|
+
def _execute_add(
|
|
277
|
+
self, new_fact: AtomicFact, profile_id: str, *, reason: str,
|
|
278
|
+
) -> ConsolidationAction:
|
|
279
|
+
"""Store the new fact and link to related facts via semantic edges."""
|
|
280
|
+
self._db.store_fact(new_fact)
|
|
281
|
+
self._create_semantic_edges(new_fact, profile_id)
|
|
282
|
+
action = self._log_action(
|
|
283
|
+
ConsolidationActionType.ADD, new_fact.fact_id, "", profile_id, reason,
|
|
284
|
+
)
|
|
285
|
+
logger.debug("ADD fact %s: %s", new_fact.fact_id, reason)
|
|
286
|
+
return action
|
|
287
|
+
|
|
288
|
+
def _execute_update(
|
|
289
|
+
self,
|
|
290
|
+
new_fact: AtomicFact,
|
|
291
|
+
existing: AtomicFact,
|
|
292
|
+
profile_id: str,
|
|
293
|
+
*,
|
|
294
|
+
reason: str,
|
|
295
|
+
) -> ConsolidationAction:
|
|
296
|
+
"""Update existing fact: bump evidence, optionally merge content."""
|
|
297
|
+
new_evidence = existing.evidence_count + 1
|
|
298
|
+
new_confidence = min(1.0, existing.confidence + 0.05)
|
|
299
|
+
updates: dict[str, Any] = {
|
|
300
|
+
"evidence_count": new_evidence,
|
|
301
|
+
"confidence": new_confidence,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# If LLM available, merge content for a richer fact
|
|
305
|
+
if self._llm is not None and self._llm.is_available():
|
|
306
|
+
merged = self._merge_facts(existing.content, new_fact.content)
|
|
307
|
+
if merged:
|
|
308
|
+
updates["content"] = merged
|
|
309
|
+
|
|
310
|
+
self._db.update_fact(existing.fact_id, updates)
|
|
311
|
+
action = self._log_action(
|
|
312
|
+
ConsolidationActionType.UPDATE,
|
|
313
|
+
new_fact.fact_id, existing.fact_id,
|
|
314
|
+
profile_id, reason,
|
|
315
|
+
)
|
|
316
|
+
logger.debug(
|
|
317
|
+
"UPDATE fact %s (evidence=%d): %s",
|
|
318
|
+
existing.fact_id, new_evidence, reason,
|
|
319
|
+
)
|
|
320
|
+
return action
|
|
321
|
+
|
|
322
|
+
def _execute_supersede(
|
|
323
|
+
self,
|
|
324
|
+
new_fact: AtomicFact,
|
|
325
|
+
existing: AtomicFact,
|
|
326
|
+
profile_id: str,
|
|
327
|
+
*,
|
|
328
|
+
reason: str,
|
|
329
|
+
) -> ConsolidationAction:
|
|
330
|
+
"""Archive old fact, store new, create contradiction edge."""
|
|
331
|
+
# Archive old fact (keep for history but deprioritize in retrieval)
|
|
332
|
+
self._db.update_fact(
|
|
333
|
+
existing.fact_id,
|
|
334
|
+
{"lifecycle": MemoryLifecycle.ARCHIVED},
|
|
335
|
+
)
|
|
336
|
+
# Store new fact
|
|
337
|
+
self._db.store_fact(new_fact)
|
|
338
|
+
# Create contradiction + supersedes edges
|
|
339
|
+
self._db.store_edge(GraphEdge(
|
|
340
|
+
profile_id=profile_id,
|
|
341
|
+
source_id=new_fact.fact_id,
|
|
342
|
+
target_id=existing.fact_id,
|
|
343
|
+
edge_type=EdgeType.CONTRADICTION,
|
|
344
|
+
weight=1.0,
|
|
345
|
+
))
|
|
346
|
+
self._db.store_edge(GraphEdge(
|
|
347
|
+
profile_id=profile_id,
|
|
348
|
+
source_id=new_fact.fact_id,
|
|
349
|
+
target_id=existing.fact_id,
|
|
350
|
+
edge_type=EdgeType.SUPERSEDES,
|
|
351
|
+
weight=1.0,
|
|
352
|
+
))
|
|
353
|
+
action = self._log_action(
|
|
354
|
+
ConsolidationActionType.SUPERSEDE,
|
|
355
|
+
new_fact.fact_id, existing.fact_id,
|
|
356
|
+
profile_id, reason,
|
|
357
|
+
)
|
|
358
|
+
logger.debug(
|
|
359
|
+
"SUPERSEDE %s → %s: %s",
|
|
360
|
+
new_fact.fact_id, existing.fact_id, reason,
|
|
361
|
+
)
|
|
362
|
+
return action
|
|
363
|
+
|
|
364
|
+
def _execute_noop(
|
|
365
|
+
self,
|
|
366
|
+
new_fact: AtomicFact,
|
|
367
|
+
existing: AtomicFact,
|
|
368
|
+
profile_id: str,
|
|
369
|
+
*,
|
|
370
|
+
reason: str,
|
|
371
|
+
) -> ConsolidationAction:
|
|
372
|
+
"""Near-duplicate — just bump access count on existing."""
|
|
373
|
+
self._db.update_fact(
|
|
374
|
+
existing.fact_id,
|
|
375
|
+
{"access_count": existing.access_count + 1},
|
|
376
|
+
)
|
|
377
|
+
action = self._log_action(
|
|
378
|
+
ConsolidationActionType.NOOP,
|
|
379
|
+
new_fact.fact_id, existing.fact_id,
|
|
380
|
+
profile_id, reason,
|
|
381
|
+
)
|
|
382
|
+
logger.debug("NOOP for %s (dup of %s): %s", new_fact.fact_id, existing.fact_id, reason)
|
|
383
|
+
return action
|
|
384
|
+
|
|
385
|
+
# -- Helpers ------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
def _create_semantic_edges(
|
|
388
|
+
self, new_fact: AtomicFact, profile_id: str,
|
|
389
|
+
) -> None:
|
|
390
|
+
"""Link new fact to top-K most similar existing facts."""
|
|
391
|
+
if new_fact.embedding is None:
|
|
392
|
+
return
|
|
393
|
+
all_facts = self._db.get_all_facts(profile_id)
|
|
394
|
+
scored: list[tuple[str, float]] = []
|
|
395
|
+
for fact in all_facts:
|
|
396
|
+
if fact.fact_id == new_fact.fact_id:
|
|
397
|
+
continue
|
|
398
|
+
sim = _compute_similarity(new_fact.embedding, fact.embedding)
|
|
399
|
+
if sim > 0.5:
|
|
400
|
+
scored.append((fact.fact_id, sim))
|
|
401
|
+
scored.sort(key=lambda t: t[1], reverse=True)
|
|
402
|
+
for target_id, weight in scored[: self._cfg.semantic_edge_top_k]:
|
|
403
|
+
self._db.store_edge(GraphEdge(
|
|
404
|
+
profile_id=profile_id,
|
|
405
|
+
source_id=new_fact.fact_id,
|
|
406
|
+
target_id=target_id,
|
|
407
|
+
edge_type=EdgeType.SEMANTIC,
|
|
408
|
+
weight=weight,
|
|
409
|
+
))
|
|
410
|
+
|
|
411
|
+
def _merge_facts(self, existing_text: str, new_text: str) -> str:
|
|
412
|
+
"""Use LLM to merge two fact statements into one richer fact."""
|
|
413
|
+
assert self._llm is not None
|
|
414
|
+
prompt = (
|
|
415
|
+
"Merge these two statements about the same topic into one "
|
|
416
|
+
"concise, accurate fact. Keep all unique information.\n\n"
|
|
417
|
+
f"Existing: {existing_text}\n"
|
|
418
|
+
f"New: {new_text}\n\n"
|
|
419
|
+
"Merged statement:"
|
|
420
|
+
)
|
|
421
|
+
result = self._llm.generate(prompt, system="You are a precise editor.")
|
|
422
|
+
return result.strip() if result.strip() else ""
|
|
423
|
+
|
|
424
|
+
def _log_action(
|
|
425
|
+
self,
|
|
426
|
+
action_type: ConsolidationActionType,
|
|
427
|
+
new_fact_id: str,
|
|
428
|
+
existing_fact_id: str,
|
|
429
|
+
profile_id: str,
|
|
430
|
+
reason: str,
|
|
431
|
+
) -> ConsolidationAction:
|
|
432
|
+
"""Persist action to consolidation_log and return the model."""
|
|
433
|
+
action = ConsolidationAction(
|
|
434
|
+
profile_id=profile_id,
|
|
435
|
+
action_type=action_type,
|
|
436
|
+
new_fact_id=new_fact_id,
|
|
437
|
+
existing_fact_id=existing_fact_id,
|
|
438
|
+
reason=reason,
|
|
439
|
+
)
|
|
440
|
+
self._db.execute(
|
|
441
|
+
"INSERT INTO consolidation_log "
|
|
442
|
+
"(action_id, profile_id, action_type, new_fact_id, "
|
|
443
|
+
" existing_fact_id, reason, timestamp) "
|
|
444
|
+
"VALUES (?,?,?,?,?,?,?)",
|
|
445
|
+
(
|
|
446
|
+
action.action_id, action.profile_id,
|
|
447
|
+
action.action_type.value, action.new_fact_id,
|
|
448
|
+
action.existing_fact_id, action.reason, action.timestamp,
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
return action
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# ---------------------------------------------------------------------------
|
|
455
|
+
# Module-level helpers (pure functions, no side effects)
|
|
456
|
+
# ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
def _compute_similarity(
|
|
459
|
+
emb_a: list[float] | None, emb_b: list[float] | None,
|
|
460
|
+
) -> float:
|
|
461
|
+
"""Cosine similarity between two embedding vectors.
|
|
462
|
+
|
|
463
|
+
Returns 0.0 if either embedding is None or empty.
|
|
464
|
+
"""
|
|
465
|
+
if not emb_a or not emb_b:
|
|
466
|
+
return 0.0
|
|
467
|
+
if len(emb_a) != len(emb_b):
|
|
468
|
+
return 0.0
|
|
469
|
+
|
|
470
|
+
dot = sum(a * b for a, b in zip(emb_a, emb_b))
|
|
471
|
+
norm_a = math.sqrt(sum(a * a for a in emb_a))
|
|
472
|
+
norm_b = math.sqrt(sum(b * b for b in emb_b))
|
|
473
|
+
|
|
474
|
+
if norm_a < 1e-12 or norm_b < 1e-12:
|
|
475
|
+
return 0.0
|
|
476
|
+
return dot / (norm_a * norm_b)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _jaccard(set_a: set[str], set_b: set[str]) -> float:
|
|
480
|
+
"""Jaccard similarity of two string sets. Returns 0.0 if both empty."""
|
|
481
|
+
if not set_a and not set_b:
|
|
482
|
+
return 0.0
|
|
483
|
+
intersection = len(set_a & set_b)
|
|
484
|
+
union = len(set_a | set_b)
|
|
485
|
+
return intersection / union if union > 0 else 0.0
|
|
@@ -0,0 +1,125 @@
|
|
|
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 — Emotional Tagging (VADER).
|
|
6
|
+
|
|
7
|
+
Extracts emotional valence and arousal from text.
|
|
8
|
+
Emotionally charged memories are stored more strongly and retrieved more easily
|
|
9
|
+
(amygdala tagging principle from neuroscience).
|
|
10
|
+
|
|
11
|
+
Ported from V1 — VADER-based, zero-LLM, works in all modes.
|
|
12
|
+
|
|
13
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import math
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_vader_analyzer = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_vader():
|
|
28
|
+
"""Lazy-load VADER to avoid import cost on startup."""
|
|
29
|
+
global _vader_analyzer
|
|
30
|
+
if _vader_analyzer is not None:
|
|
31
|
+
return _vader_analyzer
|
|
32
|
+
try:
|
|
33
|
+
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
|
|
34
|
+
_vader_analyzer = SentimentIntensityAnalyzer()
|
|
35
|
+
except ImportError:
|
|
36
|
+
logger.warning("vaderSentiment not installed — emotional tagging disabled")
|
|
37
|
+
_vader_analyzer = None
|
|
38
|
+
return _vader_analyzer
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class EmotionalTag:
|
|
43
|
+
"""Emotional metadata for a memory or fact."""
|
|
44
|
+
|
|
45
|
+
valence: float # -1.0 (negative) to +1.0 (positive)
|
|
46
|
+
arousal: float # 0.0 (calm) to 1.0 (intense)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def tag_emotion(text: str) -> EmotionalTag:
|
|
50
|
+
"""Extract emotional valence and arousal from text.
|
|
51
|
+
|
|
52
|
+
Valence: VADER compound score [-1, +1].
|
|
53
|
+
Arousal: absolute compound + max(pos, neg) — higher = more emotional intensity.
|
|
54
|
+
Falls back to keyword heuristic when VADER is unavailable.
|
|
55
|
+
"""
|
|
56
|
+
analyzer = _get_vader()
|
|
57
|
+
if analyzer is None:
|
|
58
|
+
return _keyword_fallback(text)
|
|
59
|
+
|
|
60
|
+
scores = analyzer.polarity_scores(text)
|
|
61
|
+
compound = scores["compound"] # -1 to +1
|
|
62
|
+
pos = scores["pos"] # 0 to 1
|
|
63
|
+
neg = scores["neg"] # 0 to 1
|
|
64
|
+
|
|
65
|
+
valence = compound
|
|
66
|
+
# Arousal = emotional intensity regardless of direction
|
|
67
|
+
arousal = min(1.0, abs(compound) * 0.6 + max(pos, neg) * 0.4)
|
|
68
|
+
|
|
69
|
+
return EmotionalTag(valence=round(valence, 4), arousal=round(arousal, 4))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_POSITIVE_WORDS: frozenset[str] = frozenset({
|
|
73
|
+
"love", "amazing", "wonderful", "great", "happy", "fantastic",
|
|
74
|
+
"excellent", "beautiful", "awesome", "brilliant", "incredible",
|
|
75
|
+
"joy", "thrilled", "grateful", "delighted", "superb",
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
_NEGATIVE_WORDS: frozenset[str] = frozenset({
|
|
79
|
+
"hate", "terrible", "horrible", "awful", "bad", "worst",
|
|
80
|
+
"angry", "frustrated", "disappointed", "sad", "miserable",
|
|
81
|
+
"disgusting", "dreadful", "pathetic", "furious", "outraged",
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _keyword_fallback(text: str) -> EmotionalTag:
|
|
86
|
+
"""Lightweight sentiment heuristic when VADER is unavailable.
|
|
87
|
+
|
|
88
|
+
Counts positive/negative keywords and derives approximate valence/arousal.
|
|
89
|
+
"""
|
|
90
|
+
if not text.strip():
|
|
91
|
+
return EmotionalTag(valence=0.0, arousal=0.0)
|
|
92
|
+
|
|
93
|
+
words = set(text.lower().split())
|
|
94
|
+
pos_count = len(words & _POSITIVE_WORDS)
|
|
95
|
+
neg_count = len(words & _NEGATIVE_WORDS)
|
|
96
|
+
total = pos_count + neg_count
|
|
97
|
+
|
|
98
|
+
if total == 0:
|
|
99
|
+
return EmotionalTag(valence=0.0, arousal=0.0)
|
|
100
|
+
|
|
101
|
+
# Valence: positive - negative, normalised to [-1, 1]
|
|
102
|
+
raw_valence = (pos_count - neg_count) / total
|
|
103
|
+
valence = max(-1.0, min(1.0, raw_valence))
|
|
104
|
+
|
|
105
|
+
# Arousal: how many emotional words relative to total word count
|
|
106
|
+
word_count = max(len(words), 1)
|
|
107
|
+
arousal = min(1.0, total / word_count * 2.0)
|
|
108
|
+
|
|
109
|
+
return EmotionalTag(valence=round(valence, 4), arousal=round(arousal, 4))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_emotionally_significant(tag: EmotionalTag, threshold: float = 0.3) -> bool:
|
|
113
|
+
"""Check if the emotional signal is strong enough to boost importance."""
|
|
114
|
+
return tag.arousal >= threshold
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def emotional_importance_boost(tag: EmotionalTag) -> float:
|
|
118
|
+
"""Compute importance boost from emotional signal.
|
|
119
|
+
|
|
120
|
+
Returns 0.0-0.3 boost. High arousal memories get stored more strongly
|
|
121
|
+
(amygdala-inspired encoding enhancement).
|
|
122
|
+
"""
|
|
123
|
+
if tag.arousal <= 0.2:
|
|
124
|
+
return 0.0
|
|
125
|
+
return min(0.3, tag.arousal * 0.3)
|