superlocalmemory 2.8.5 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +9 -1
- package/NOTICE +63 -0
- package/README.md +165 -480
- package/bin/slm +17 -449
- package/bin/slm-npm +2 -2
- package/bin/slm.bat +4 -2
- package/conftest.py +5 -0
- package/docs/api-reference.md +284 -0
- package/docs/architecture.md +149 -0
- package/docs/auto-memory.md +150 -0
- package/docs/cli-reference.md +276 -0
- package/docs/compliance.md +191 -0
- package/docs/configuration.md +182 -0
- package/docs/getting-started.md +102 -0
- package/docs/ide-setup.md +261 -0
- package/docs/mcp-tools.md +220 -0
- package/docs/migration-from-v2.md +170 -0
- package/docs/profiles.md +173 -0
- package/docs/troubleshooting.md +310 -0
- package/{configs → ide/configs}/antigravity-mcp.json +3 -3
- package/ide/configs/chatgpt-desktop-mcp.json +16 -0
- package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
- package/{configs → ide/configs}/codex-mcp.toml +4 -4
- package/{configs → ide/configs}/continue-mcp.yaml +4 -3
- package/{configs → ide/configs}/continue-skills.yaml +6 -6
- package/ide/configs/cursor-mcp.json +15 -0
- package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
- package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
- package/{configs → ide/configs}/opencode-mcp.json +2 -2
- package/{configs → ide/configs}/perplexity-mcp.json +2 -2
- package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
- package/{configs → ide/configs}/windsurf-mcp.json +3 -3
- package/{configs → ide/configs}/zed-mcp.json +2 -2
- package/{hooks → ide/hooks}/context-hook.js +9 -20
- package/ide/hooks/memory-list-skill.js +70 -0
- package/ide/hooks/memory-profile-skill.js +101 -0
- package/ide/hooks/memory-recall-skill.js +62 -0
- package/ide/hooks/memory-remember-skill.js +68 -0
- package/ide/hooks/memory-reset-skill.js +160 -0
- package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
- package/ide/integrations/langchain/README.md +106 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
- package/ide/integrations/langchain/pyproject.toml +38 -0
- package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
- package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
- package/ide/integrations/langchain/tests/test_security.py +117 -0
- package/ide/integrations/llamaindex/README.md +81 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
- package/ide/integrations/llamaindex/pyproject.toml +43 -0
- package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
- package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
- package/ide/integrations/llamaindex/tests/test_security.py +241 -0
- package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
- package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
- package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
- package/package.json +13 -22
- package/pyproject.toml +85 -0
- package/scripts/build-dmg.sh +417 -0
- package/scripts/install-skills.ps1 +334 -0
- package/{install.ps1 → scripts/install.ps1} +36 -4
- package/{install.sh → scripts/install.sh} +14 -13
- package/scripts/postinstall.js +2 -2
- package/scripts/start-dashboard.ps1 +52 -0
- package/scripts/start-dashboard.sh +41 -0
- package/scripts/sync-wiki.ps1 +127 -0
- package/scripts/sync-wiki.sh +82 -0
- package/scripts/test-dmg.sh +161 -0
- package/scripts/test-npm-package.ps1 +252 -0
- package/scripts/test-npm-package.sh +207 -0
- package/scripts/verify-install.ps1 +294 -0
- package/scripts/verify-install.sh +266 -0
- package/src/superlocalmemory/__init__.py +0 -0
- package/src/superlocalmemory/attribution/__init__.py +9 -0
- package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
- package/src/superlocalmemory/attribution/signer.py +153 -0
- package/src/superlocalmemory/attribution/watermark.py +189 -0
- package/src/superlocalmemory/cli/__init__.py +5 -0
- package/src/superlocalmemory/cli/commands.py +245 -0
- package/src/superlocalmemory/cli/main.py +89 -0
- package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
- package/src/superlocalmemory/cli/post_install.py +99 -0
- package/src/superlocalmemory/cli/setup_wizard.py +129 -0
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +204 -0
- package/src/superlocalmemory/compliance/audit.py +314 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
- package/src/superlocalmemory/compliance/gdpr.py +294 -0
- package/src/superlocalmemory/compliance/lifecycle.py +158 -0
- package/src/superlocalmemory/compliance/retention.py +232 -0
- package/src/superlocalmemory/compliance/scheduler.py +148 -0
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/config.py +391 -0
- package/src/superlocalmemory/core/embeddings.py +293 -0
- package/src/superlocalmemory/core/engine.py +701 -0
- package/src/superlocalmemory/core/hooks.py +65 -0
- package/src/superlocalmemory/core/maintenance.py +172 -0
- package/src/superlocalmemory/core/modes.py +140 -0
- package/src/superlocalmemory/core/profiles.py +234 -0
- package/src/superlocalmemory/core/registry.py +117 -0
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/consolidator.py +485 -0
- package/src/superlocalmemory/encoding/emotional.py +125 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
- package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
- package/src/superlocalmemory/encoding/foresight.py +91 -0
- package/src/superlocalmemory/encoding/graph_builder.py +302 -0
- package/src/superlocalmemory/encoding/observation_builder.py +160 -0
- package/src/superlocalmemory/encoding/scene_builder.py +183 -0
- package/src/superlocalmemory/encoding/signal_inference.py +90 -0
- package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
- package/src/superlocalmemory/encoding/type_router.py +235 -0
- package/src/superlocalmemory/hooks/__init__.py +3 -0
- package/src/superlocalmemory/hooks/auto_capture.py +111 -0
- package/src/superlocalmemory/hooks/auto_recall.py +93 -0
- package/src/superlocalmemory/hooks/ide_connector.py +204 -0
- package/src/superlocalmemory/hooks/rules_engine.py +99 -0
- package/src/superlocalmemory/infra/__init__.py +3 -0
- package/src/superlocalmemory/infra/auth_middleware.py +82 -0
- package/src/superlocalmemory/infra/backup.py +317 -0
- package/src/superlocalmemory/infra/cache_manager.py +267 -0
- package/src/superlocalmemory/infra/event_bus.py +381 -0
- package/src/superlocalmemory/infra/rate_limiter.py +135 -0
- package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +172 -0
- package/src/superlocalmemory/learning/behavioral.py +490 -0
- package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
- package/src/superlocalmemory/learning/bootstrap.py +298 -0
- package/src/superlocalmemory/learning/cross_project.py +399 -0
- package/src/superlocalmemory/learning/database.py +376 -0
- package/src/superlocalmemory/learning/engagement.py +323 -0
- package/src/superlocalmemory/learning/features.py +138 -0
- package/src/superlocalmemory/learning/feedback.py +316 -0
- package/src/superlocalmemory/learning/outcomes.py +255 -0
- package/src/superlocalmemory/learning/project_context.py +366 -0
- package/src/superlocalmemory/learning/ranker.py +155 -0
- package/src/superlocalmemory/learning/source_quality.py +303 -0
- package/src/superlocalmemory/learning/workflows.py +309 -0
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +316 -0
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/fisher.py +356 -0
- package/src/superlocalmemory/math/langevin.py +398 -0
- package/src/superlocalmemory/math/sheaf.py +257 -0
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/resources.py +245 -0
- package/src/superlocalmemory/mcp/server.py +61 -0
- package/src/superlocalmemory/mcp/tools.py +18 -0
- package/src/superlocalmemory/mcp/tools_core.py +305 -0
- package/src/superlocalmemory/mcp/tools_v28.py +223 -0
- package/src/superlocalmemory/mcp/tools_v3.py +286 -0
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +295 -0
- package/src/superlocalmemory/retrieval/ann_index.py +223 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
- package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
- package/src/superlocalmemory/retrieval/engine.py +390 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
- package/src/superlocalmemory/retrieval/fusion.py +78 -0
- package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
- package/src/superlocalmemory/retrieval/reranker.py +154 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
- package/src/superlocalmemory/retrieval/strategy.py +96 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
- package/src/superlocalmemory/server/__init__.py +1 -0
- package/src/superlocalmemory/server/api.py +248 -0
- package/src/superlocalmemory/server/routes/__init__.py +4 -0
- package/src/superlocalmemory/server/routes/agents.py +107 -0
- package/src/superlocalmemory/server/routes/backup.py +91 -0
- package/src/superlocalmemory/server/routes/behavioral.py +127 -0
- package/src/superlocalmemory/server/routes/compliance.py +160 -0
- package/src/superlocalmemory/server/routes/data_io.py +188 -0
- package/src/superlocalmemory/server/routes/events.py +183 -0
- package/src/superlocalmemory/server/routes/helpers.py +85 -0
- package/src/superlocalmemory/server/routes/learning.py +273 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
- package/src/superlocalmemory/server/routes/memories.py +399 -0
- package/src/superlocalmemory/server/routes/profiles.py +219 -0
- package/src/superlocalmemory/server/routes/stats.py +346 -0
- package/src/superlocalmemory/server/routes/v3_api.py +365 -0
- package/src/superlocalmemory/server/routes/ws.py +82 -0
- package/src/superlocalmemory/server/security_middleware.py +57 -0
- package/src/superlocalmemory/server/ui.py +245 -0
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/access_control.py +182 -0
- package/src/superlocalmemory/storage/database.py +594 -0
- package/src/superlocalmemory/storage/migrations.py +303 -0
- package/src/superlocalmemory/storage/models.py +406 -0
- package/src/superlocalmemory/storage/schema.py +726 -0
- package/src/superlocalmemory/storage/v2_migrator.py +317 -0
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +130 -0
- package/src/superlocalmemory/trust/provenance.py +124 -0
- package/src/superlocalmemory/trust/scorer.py +347 -0
- package/src/superlocalmemory/trust/signals.py +153 -0
- package/ui/index.html +278 -5
- package/ui/js/auto-settings.js +70 -0
- package/ui/js/dashboard.js +90 -0
- package/ui/js/fact-detail.js +92 -0
- package/ui/js/feedback.js +2 -2
- package/ui/js/ide-status.js +102 -0
- package/ui/js/math-health.js +98 -0
- package/ui/js/recall-lab.js +127 -0
- package/ui/js/settings.js +2 -2
- package/ui/js/trust-dashboard.js +73 -0
- package/api_server.py +0 -724
- package/bin/aider-smart +0 -72
- package/bin/superlocalmemoryv2-learning +0 -4
- package/bin/superlocalmemoryv2-list +0 -3
- package/bin/superlocalmemoryv2-patterns +0 -4
- package/bin/superlocalmemoryv2-profile +0 -3
- package/bin/superlocalmemoryv2-recall +0 -3
- package/bin/superlocalmemoryv2-remember +0 -3
- package/bin/superlocalmemoryv2-reset +0 -3
- package/bin/superlocalmemoryv2-status +0 -3
- package/configs/chatgpt-desktop-mcp.json +0 -16
- package/configs/cursor-mcp.json +0 -15
- package/docs/SECURITY-QUICK-REFERENCE.md +0 -214
- package/hooks/memory-list-skill.js +0 -139
- package/hooks/memory-profile-skill.js +0 -273
- package/hooks/memory-recall-skill.js +0 -114
- package/hooks/memory-remember-skill.js +0 -127
- package/hooks/memory-reset-skill.js +0 -274
- package/mcp_server.py +0 -1800
- package/requirements-core.txt +0 -22
- package/requirements-learning.txt +0 -12
- package/requirements.txt +0 -12
- package/src/agent_registry.py +0 -411
- package/src/auth_middleware.py +0 -61
- package/src/auto_backup.py +0 -459
- package/src/behavioral/__init__.py +0 -49
- package/src/behavioral/behavioral_listener.py +0 -203
- package/src/behavioral/behavioral_patterns.py +0 -275
- package/src/behavioral/cross_project_transfer.py +0 -206
- package/src/behavioral/outcome_inference.py +0 -194
- package/src/behavioral/outcome_tracker.py +0 -193
- package/src/behavioral/tests/__init__.py +0 -4
- package/src/behavioral/tests/test_behavioral_integration.py +0 -108
- package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
- package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
- package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
- package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
- package/src/behavioral/tests/test_outcome_inference.py +0 -107
- package/src/behavioral/tests/test_outcome_tracker.py +0 -96
- package/src/cache_manager.py +0 -518
- package/src/compliance/__init__.py +0 -48
- package/src/compliance/abac_engine.py +0 -149
- package/src/compliance/abac_middleware.py +0 -116
- package/src/compliance/audit_db.py +0 -215
- package/src/compliance/audit_logger.py +0 -148
- package/src/compliance/retention_manager.py +0 -289
- package/src/compliance/retention_scheduler.py +0 -186
- package/src/compliance/tests/__init__.py +0 -4
- package/src/compliance/tests/test_abac_enforcement.py +0 -95
- package/src/compliance/tests/test_abac_engine.py +0 -124
- package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
- package/src/compliance/tests/test_audit_db.py +0 -123
- package/src/compliance/tests/test_audit_logger.py +0 -98
- package/src/compliance/tests/test_mcp_audit.py +0 -128
- package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
- package/src/compliance/tests/test_retention_manager.py +0 -131
- package/src/compliance/tests/test_retention_scheduler.py +0 -99
- package/src/compression/__init__.py +0 -25
- package/src/compression/cli.py +0 -150
- package/src/compression/cold_storage.py +0 -217
- package/src/compression/config.py +0 -72
- package/src/compression/orchestrator.py +0 -133
- package/src/compression/tier2_compressor.py +0 -228
- package/src/compression/tier3_compressor.py +0 -153
- package/src/compression/tier_classifier.py +0 -148
- package/src/db_connection_manager.py +0 -536
- package/src/embedding_engine.py +0 -63
- package/src/embeddings/__init__.py +0 -47
- package/src/embeddings/cache.py +0 -70
- package/src/embeddings/cli.py +0 -113
- package/src/embeddings/constants.py +0 -47
- package/src/embeddings/database.py +0 -91
- package/src/embeddings/engine.py +0 -247
- package/src/embeddings/model_loader.py +0 -145
- package/src/event_bus.py +0 -562
- package/src/graph/__init__.py +0 -36
- package/src/graph/build_helpers.py +0 -74
- package/src/graph/cli.py +0 -87
- package/src/graph/cluster_builder.py +0 -188
- package/src/graph/cluster_summary.py +0 -148
- package/src/graph/constants.py +0 -47
- package/src/graph/edge_builder.py +0 -162
- package/src/graph/entity_extractor.py +0 -95
- package/src/graph/graph_core.py +0 -226
- package/src/graph/graph_search.py +0 -231
- package/src/graph/hierarchical.py +0 -207
- package/src/graph/schema.py +0 -99
- package/src/graph_engine.py +0 -52
- package/src/hnsw_index.py +0 -628
- package/src/hybrid_search.py +0 -46
- package/src/learning/__init__.py +0 -217
- package/src/learning/adaptive_ranker.py +0 -682
- package/src/learning/bootstrap/__init__.py +0 -69
- package/src/learning/bootstrap/constants.py +0 -93
- package/src/learning/bootstrap/db_queries.py +0 -316
- package/src/learning/bootstrap/sampling.py +0 -82
- package/src/learning/bootstrap/text_utils.py +0 -71
- package/src/learning/cross_project_aggregator.py +0 -857
- package/src/learning/db/__init__.py +0 -40
- package/src/learning/db/constants.py +0 -44
- package/src/learning/db/schema.py +0 -279
- package/src/learning/engagement_tracker.py +0 -628
- package/src/learning/feature_extractor.py +0 -708
- package/src/learning/feedback_collector.py +0 -806
- package/src/learning/learning_db.py +0 -915
- package/src/learning/project_context_manager.py +0 -572
- package/src/learning/ranking/__init__.py +0 -33
- package/src/learning/ranking/constants.py +0 -84
- package/src/learning/ranking/helpers.py +0 -278
- package/src/learning/source_quality_scorer.py +0 -676
- package/src/learning/synthetic_bootstrap.py +0 -755
- package/src/learning/tests/test_adaptive_ranker.py +0 -325
- package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
- package/src/learning/tests/test_aggregator.py +0 -306
- package/src/learning/tests/test_auto_retrain_v28.py +0 -35
- package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
- package/src/learning/tests/test_feature_extractor_v28.py +0 -93
- package/src/learning/tests/test_feedback_collector.py +0 -294
- package/src/learning/tests/test_learning_db.py +0 -602
- package/src/learning/tests/test_learning_db_v28.py +0 -110
- package/src/learning/tests/test_learning_init_v28.py +0 -48
- package/src/learning/tests/test_outcome_signals.py +0 -48
- package/src/learning/tests/test_project_context.py +0 -292
- package/src/learning/tests/test_schema_migration.py +0 -319
- package/src/learning/tests/test_signal_inference.py +0 -397
- package/src/learning/tests/test_source_quality.py +0 -351
- package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
- package/src/learning/tests/test_workflow_miner.py +0 -318
- package/src/learning/workflow_pattern_miner.py +0 -655
- package/src/lifecycle/__init__.py +0 -54
- package/src/lifecycle/bounded_growth.py +0 -239
- package/src/lifecycle/compaction_engine.py +0 -226
- package/src/lifecycle/lifecycle_engine.py +0 -355
- package/src/lifecycle/lifecycle_evaluator.py +0 -257
- package/src/lifecycle/lifecycle_scheduler.py +0 -130
- package/src/lifecycle/retention_policy.py +0 -285
- package/src/lifecycle/tests/test_bounded_growth.py +0 -193
- package/src/lifecycle/tests/test_compaction.py +0 -179
- package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
- package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
- package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
- package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
- package/src/lifecycle/tests/test_mcp_compact.py +0 -149
- package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
- package/src/lifecycle/tests/test_retention_policy.py +0 -162
- package/src/mcp_tools_v28.py +0 -281
- package/src/memory/__init__.py +0 -36
- package/src/memory/cli.py +0 -205
- package/src/memory/constants.py +0 -39
- package/src/memory/helpers.py +0 -28
- package/src/memory/schema.py +0 -166
- package/src/memory-profiles.py +0 -595
- package/src/memory-reset.py +0 -491
- package/src/memory_compression.py +0 -989
- package/src/memory_store_v2.py +0 -1155
- package/src/migrate_v1_to_v2.py +0 -629
- package/src/pattern_learner.py +0 -34
- package/src/patterns/__init__.py +0 -24
- package/src/patterns/analyzers.py +0 -251
- package/src/patterns/learner.py +0 -271
- package/src/patterns/scoring.py +0 -171
- package/src/patterns/store.py +0 -225
- package/src/patterns/terminology.py +0 -140
- package/src/provenance_tracker.py +0 -312
- package/src/qualixar_attribution.py +0 -139
- package/src/qualixar_watermark.py +0 -78
- package/src/query_optimizer.py +0 -511
- package/src/rate_limiter.py +0 -83
- package/src/search/__init__.py +0 -20
- package/src/search/cli.py +0 -77
- package/src/search/constants.py +0 -26
- package/src/search/engine.py +0 -241
- package/src/search/fusion.py +0 -122
- package/src/search/index_loader.py +0 -114
- package/src/search/methods.py +0 -162
- package/src/search_engine_v2.py +0 -401
- package/src/setup_validator.py +0 -482
- package/src/subscription_manager.py +0 -391
- package/src/tree/__init__.py +0 -59
- package/src/tree/builder.py +0 -185
- package/src/tree/nodes.py +0 -202
- package/src/tree/queries.py +0 -257
- package/src/tree/schema.py +0 -80
- package/src/tree_manager.py +0 -19
- package/src/trust/__init__.py +0 -45
- package/src/trust/constants.py +0 -66
- package/src/trust/queries.py +0 -157
- package/src/trust/schema.py +0 -95
- package/src/trust/scorer.py +0 -299
- package/src/trust/signals.py +0 -95
- package/src/trust_scorer.py +0 -44
- package/ui/app.js +0 -1588
- package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
- package/ui/js/graph-cytoscape.js +0 -1168
- package/ui/js/graph-d3-backup.js +0 -32
- package/ui/js/graph.js +0 -32
- package/ui_server.py +0 -266
- /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
- /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
- /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
- /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
- /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
- /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
- /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
- /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
- /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
- /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
- /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
- /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
- /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
- /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
- /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
- /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
- /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
- /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
- /package/{completions → ide/completions}/slm.bash +0 -0
- /package/{completions → ide/completions}/slm.zsh +0 -0
- /package/{configs → ide/configs}/cody-commands.json +0 -0
- /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
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 — Foresight Signal Extraction.
|
|
6
|
+
|
|
7
|
+
Extracts time-bounded intentions and planned events from conversations.
|
|
8
|
+
V1 extracted foresight signals but NEVER STORED them. Now persisted as TemporalEvents.
|
|
9
|
+
|
|
10
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import re
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
from superlocalmemory.storage.models import AtomicFact, TemporalEvent
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Patterns that indicate future intent or planned events
|
|
24
|
+
_FORESIGHT_PATTERNS = [
|
|
25
|
+
re.compile(r"\b(plan(?:ning|s|ned)?)\s+to\b", re.I),
|
|
26
|
+
re.compile(r"\b(going)\s+to\b", re.I),
|
|
27
|
+
re.compile(r"\b(will|shall)\s+\w+", re.I),
|
|
28
|
+
re.compile(r"\b(schedul(?:e|ed|ing))\b", re.I),
|
|
29
|
+
re.compile(r"\b(appointment|reservation|booking)\b", re.I),
|
|
30
|
+
re.compile(r"\b(remind(?:er)?|don't forget)\b", re.I),
|
|
31
|
+
re.compile(r"\b(taking|starting|beginning)\s+\w+\s+(next|tomorrow|soon)\b", re.I),
|
|
32
|
+
re.compile(r"\b(deadline|due date|due by)\b", re.I),
|
|
33
|
+
re.compile(r"\bnext\s+(week|month|year|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b", re.I),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_foresight_signals(fact: AtomicFact) -> list[dict]:
|
|
38
|
+
"""Extract foresight signals from a fact's content.
|
|
39
|
+
|
|
40
|
+
Returns list of dicts with: content, pattern_matched.
|
|
41
|
+
These get converted to TemporalEvents by the encoding pipeline.
|
|
42
|
+
"""
|
|
43
|
+
signals = []
|
|
44
|
+
for pattern in _FORESIGHT_PATTERNS:
|
|
45
|
+
match = pattern.search(fact.content)
|
|
46
|
+
if match:
|
|
47
|
+
signals.append({
|
|
48
|
+
"content": fact.content,
|
|
49
|
+
"pattern": match.group(0),
|
|
50
|
+
"fact_id": fact.fact_id,
|
|
51
|
+
})
|
|
52
|
+
break # One signal per fact is sufficient
|
|
53
|
+
|
|
54
|
+
return signals
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def foresight_to_temporal_events(
|
|
58
|
+
fact: AtomicFact,
|
|
59
|
+
entity_ids: list[str],
|
|
60
|
+
profile_id: str,
|
|
61
|
+
) -> list[TemporalEvent]:
|
|
62
|
+
"""Convert foresight signals into TemporalEvents for persistence.
|
|
63
|
+
|
|
64
|
+
Each signal becomes a temporal event linked to the fact's entities.
|
|
65
|
+
The temporal_parser handles actual date extraction — we just mark
|
|
66
|
+
the fact as having foresight intent.
|
|
67
|
+
"""
|
|
68
|
+
signals = extract_foresight_signals(fact)
|
|
69
|
+
if not signals:
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
events = []
|
|
73
|
+
for eid in entity_ids:
|
|
74
|
+
event = TemporalEvent(
|
|
75
|
+
profile_id=profile_id,
|
|
76
|
+
entity_id=eid,
|
|
77
|
+
fact_id=fact.fact_id,
|
|
78
|
+
observation_date=fact.observation_date,
|
|
79
|
+
referenced_date=fact.referenced_date,
|
|
80
|
+
interval_start=fact.interval_start,
|
|
81
|
+
interval_end=fact.interval_end,
|
|
82
|
+
description=f"Foresight: {fact.content[:200]}",
|
|
83
|
+
)
|
|
84
|
+
events.append(event)
|
|
85
|
+
|
|
86
|
+
return events
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def has_foresight(text: str) -> bool:
|
|
90
|
+
"""Quick check if text contains foresight signals."""
|
|
91
|
+
return any(p.search(text) for p in _FORESIGHT_PATTERNS)
|
|
@@ -0,0 +1,302 @@
|
|
|
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
|
+
"""Knowledge graph construction — 4 edge types, zero memory limits.
|
|
6
|
+
|
|
7
|
+
Builds edges between AtomicFacts at encoding time. V1 only checked the
|
|
8
|
+
last 50 memories; this version searches ALL facts per canonical entity.
|
|
9
|
+
|
|
10
|
+
Edge types: ENTITY (shared entity, weight 1.0), TEMPORAL (exp-decay,
|
|
11
|
+
1-week window), SEMANTIC (ANN cosine > 0.7), CAUSAL (causal markers,
|
|
12
|
+
weight 0.8). CONTRADICTION exposed for external Sheaf module (Wave 4).
|
|
13
|
+
|
|
14
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
15
|
+
License: MIT
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import math
|
|
21
|
+
import re
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from typing import Any, Protocol, runtime_checkable
|
|
24
|
+
|
|
25
|
+
from superlocalmemory.storage.database import DatabaseManager
|
|
26
|
+
from superlocalmemory.storage.models import AtomicFact, EdgeType, GraphEdge
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# Constants
|
|
31
|
+
_TEMPORAL_MAX_HOURS: float = 168.0 # 1 week
|
|
32
|
+
_TEMPORAL_DECAY_DAYS: float = 30.0 # exp(-delta_days / 30)
|
|
33
|
+
_SEMANTIC_THRESHOLD: float = 0.7
|
|
34
|
+
_SEMANTIC_TOP_K: int = 5
|
|
35
|
+
_CAUSAL_WEIGHT: float = 0.8
|
|
36
|
+
_ENTITY_WEIGHT: float = 1.0
|
|
37
|
+
|
|
38
|
+
# Causal cue patterns — longer phrases first to avoid partial matches.
|
|
39
|
+
_CAUSAL_CUES: tuple[re.Pattern[str], ...] = (
|
|
40
|
+
re.compile(r"\bbecause of\b", re.I),
|
|
41
|
+
re.compile(r"\bas a result\b", re.I),
|
|
42
|
+
re.compile(r"\bresulted in\b", re.I),
|
|
43
|
+
re.compile(r"\bin order to\b", re.I),
|
|
44
|
+
re.compile(r"\bso that\b", re.I),
|
|
45
|
+
re.compile(r"\bcaused by\b", re.I),
|
|
46
|
+
re.compile(r"\bdue to\b", re.I),
|
|
47
|
+
re.compile(r"\bled to\b", re.I),
|
|
48
|
+
re.compile(r"\btherefore\b", re.I),
|
|
49
|
+
re.compile(r"\bconsequently\b", re.I),
|
|
50
|
+
re.compile(r"\bbecause\b", re.I),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@runtime_checkable
|
|
55
|
+
class ANNSearchable(Protocol):
|
|
56
|
+
"""Minimal protocol for approximate-nearest-neighbor search."""
|
|
57
|
+
|
|
58
|
+
def search(self, query: Any, top_k: int = 5) -> list[tuple[str, float]]:
|
|
59
|
+
"""Return (fact_id, similarity_score) pairs."""
|
|
60
|
+
... # pragma: no cover
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GraphBuilder:
|
|
64
|
+
"""Build knowledge-graph edges for newly stored facts.
|
|
65
|
+
|
|
66
|
+
Searches the ENTIRE corpus per canonical entity (not just last 50).
|
|
67
|
+
Adds semantic similarity edges via optional ANN index and detects
|
|
68
|
+
causal language for directed cause/effect edges.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, db: DatabaseManager, ann_index: ANNSearchable | None = None) -> None:
|
|
72
|
+
self._db = db
|
|
73
|
+
self._ann = ann_index
|
|
74
|
+
|
|
75
|
+
# -- Public API --------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def build_edges(self, new_fact: AtomicFact, profile_id: str) -> list[GraphEdge]:
|
|
78
|
+
"""Create ALL relevant edges for *new_fact*. Persists and returns them."""
|
|
79
|
+
edges: list[GraphEdge] = []
|
|
80
|
+
edges.extend(self._build_entity_edges(new_fact, profile_id))
|
|
81
|
+
edges.extend(self._build_temporal_edges(new_fact, profile_id))
|
|
82
|
+
edges.extend(self._build_semantic_edges(new_fact, profile_id))
|
|
83
|
+
edges.extend(self._build_causal_edges(new_fact, profile_id))
|
|
84
|
+
|
|
85
|
+
for edge in edges:
|
|
86
|
+
self._db.store_edge(edge)
|
|
87
|
+
|
|
88
|
+
if edges:
|
|
89
|
+
logger.debug(
|
|
90
|
+
"GraphBuilder: %d edges for %s (E=%d T=%d S=%d C=%d)",
|
|
91
|
+
len(edges), new_fact.fact_id,
|
|
92
|
+
sum(1 for e in edges if e.edge_type == EdgeType.ENTITY),
|
|
93
|
+
sum(1 for e in edges if e.edge_type == EdgeType.TEMPORAL),
|
|
94
|
+
sum(1 for e in edges if e.edge_type == EdgeType.SEMANTIC),
|
|
95
|
+
sum(1 for e in edges if e.edge_type == EdgeType.CAUSAL),
|
|
96
|
+
)
|
|
97
|
+
return edges
|
|
98
|
+
|
|
99
|
+
def add_contradiction_edge(
|
|
100
|
+
self, fact_id_a: str, fact_id_b: str, profile_id: str,
|
|
101
|
+
severity: float = 1.0,
|
|
102
|
+
) -> GraphEdge:
|
|
103
|
+
"""Add a contradiction edge. Called by Sheaf module (Wave 4)."""
|
|
104
|
+
edge = GraphEdge(
|
|
105
|
+
profile_id=profile_id,
|
|
106
|
+
source_id=fact_id_a,
|
|
107
|
+
target_id=fact_id_b,
|
|
108
|
+
edge_type=EdgeType.CONTRADICTION,
|
|
109
|
+
weight=max(0.0, min(1.0, severity)),
|
|
110
|
+
)
|
|
111
|
+
self._db.store_edge(edge)
|
|
112
|
+
logger.info("Contradiction %s -> %s (%.2f)", fact_id_a, fact_id_b, severity)
|
|
113
|
+
return edge
|
|
114
|
+
|
|
115
|
+
def get_graph_stats(self, profile_id: str) -> dict[str, Any]:
|
|
116
|
+
"""Edge counts by type, node count, average degree."""
|
|
117
|
+
rows = self._db.execute(
|
|
118
|
+
"SELECT edge_type, COUNT(*) AS cnt FROM graph_edges "
|
|
119
|
+
"WHERE profile_id = ? GROUP BY edge_type", (profile_id,),
|
|
120
|
+
)
|
|
121
|
+
edge_counts: dict[str, int] = {
|
|
122
|
+
dict(r)["edge_type"]: int(dict(r)["cnt"]) for r in rows
|
|
123
|
+
}
|
|
124
|
+
total_edges = sum(edge_counts.values())
|
|
125
|
+
|
|
126
|
+
node_rows = self._db.execute(
|
|
127
|
+
"SELECT COUNT(DISTINCT n) AS c FROM ("
|
|
128
|
+
" SELECT source_id AS n FROM graph_edges WHERE profile_id = ? "
|
|
129
|
+
" UNION "
|
|
130
|
+
" SELECT target_id AS n FROM graph_edges WHERE profile_id = ?"
|
|
131
|
+
")", (profile_id, profile_id),
|
|
132
|
+
)
|
|
133
|
+
node_count = int(dict(node_rows[0])["c"]) if node_rows else 0
|
|
134
|
+
avg_degree = (2.0 * total_edges / node_count) if node_count > 0 else 0.0
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"edge_counts": edge_counts,
|
|
138
|
+
"total_edges": total_edges,
|
|
139
|
+
"node_count": node_count,
|
|
140
|
+
"avg_degree": round(avg_degree, 2),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# -- Edge builders (private) -------------------------------------------
|
|
144
|
+
|
|
145
|
+
def _build_entity_edges(
|
|
146
|
+
self, new_fact: AtomicFact, profile_id: str,
|
|
147
|
+
) -> list[GraphEdge]:
|
|
148
|
+
"""ENTITY edges: shared canonical entity — NO 50-memory limit."""
|
|
149
|
+
if not new_fact.canonical_entities:
|
|
150
|
+
return []
|
|
151
|
+
edges: list[GraphEdge] = []
|
|
152
|
+
seen: set[str] = set()
|
|
153
|
+
|
|
154
|
+
for entity_id in new_fact.canonical_entities:
|
|
155
|
+
for other in self._db.get_facts_by_entity(entity_id, profile_id):
|
|
156
|
+
if other.fact_id == new_fact.fact_id or other.fact_id in seen:
|
|
157
|
+
continue
|
|
158
|
+
if self._edge_exists(new_fact.fact_id, other.fact_id, EdgeType.ENTITY, profile_id):
|
|
159
|
+
continue
|
|
160
|
+
seen.add(other.fact_id)
|
|
161
|
+
edges.append(GraphEdge(
|
|
162
|
+
profile_id=profile_id, source_id=new_fact.fact_id,
|
|
163
|
+
target_id=other.fact_id, edge_type=EdgeType.ENTITY,
|
|
164
|
+
weight=_ENTITY_WEIGHT,
|
|
165
|
+
))
|
|
166
|
+
return edges
|
|
167
|
+
|
|
168
|
+
def _build_temporal_edges(
|
|
169
|
+
self, new_fact: AtomicFact, profile_id: str,
|
|
170
|
+
) -> list[GraphEdge]:
|
|
171
|
+
"""TEMPORAL edges: bidirectional, exp-decay, 1-week window per entity.
|
|
172
|
+
|
|
173
|
+
Only creates temporal edges when an explicit observation_date is set.
|
|
174
|
+
Falling back to created_at would produce spurious temporal edges for
|
|
175
|
+
facts that have no real temporal context.
|
|
176
|
+
"""
|
|
177
|
+
if not new_fact.observation_date or not new_fact.canonical_entities:
|
|
178
|
+
return []
|
|
179
|
+
new_dt = _parse_date(new_fact.observation_date)
|
|
180
|
+
if new_dt is None:
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
edges: list[GraphEdge] = []
|
|
184
|
+
seen_pairs: set[tuple[str, str]] = set()
|
|
185
|
+
|
|
186
|
+
for entity_id in new_fact.canonical_entities:
|
|
187
|
+
for other in self._db.get_facts_by_entity(entity_id, profile_id):
|
|
188
|
+
if other.fact_id == new_fact.fact_id:
|
|
189
|
+
continue
|
|
190
|
+
other_dt = _parse_date(other.observation_date)
|
|
191
|
+
if other_dt is None:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
delta_hours = abs((new_dt - other_dt).total_seconds()) / 3600.0
|
|
195
|
+
if delta_hours > _TEMPORAL_MAX_HOURS:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
pair_key = (min(new_fact.fact_id, other.fact_id),
|
|
199
|
+
max(new_fact.fact_id, other.fact_id))
|
|
200
|
+
if pair_key in seen_pairs:
|
|
201
|
+
continue
|
|
202
|
+
if self._edge_exists(new_fact.fact_id, other.fact_id, EdgeType.TEMPORAL, profile_id):
|
|
203
|
+
seen_pairs.add(pair_key)
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
weight = round(max(math.exp(-(delta_hours / 24.0) / _TEMPORAL_DECAY_DAYS), 0.01), 4)
|
|
207
|
+
seen_pairs.add(pair_key)
|
|
208
|
+
|
|
209
|
+
# Forward: new -> other
|
|
210
|
+
edges.append(GraphEdge(
|
|
211
|
+
profile_id=profile_id, source_id=new_fact.fact_id,
|
|
212
|
+
target_id=other.fact_id, edge_type=EdgeType.TEMPORAL,
|
|
213
|
+
weight=weight,
|
|
214
|
+
))
|
|
215
|
+
# Reverse: other -> new
|
|
216
|
+
if not self._edge_exists(other.fact_id, new_fact.fact_id, EdgeType.TEMPORAL, profile_id):
|
|
217
|
+
edges.append(GraphEdge(
|
|
218
|
+
profile_id=profile_id, source_id=other.fact_id,
|
|
219
|
+
target_id=new_fact.fact_id, edge_type=EdgeType.TEMPORAL,
|
|
220
|
+
weight=weight,
|
|
221
|
+
))
|
|
222
|
+
return edges
|
|
223
|
+
|
|
224
|
+
def _build_semantic_edges(
|
|
225
|
+
self, new_fact: AtomicFact, profile_id: str,
|
|
226
|
+
) -> list[GraphEdge]:
|
|
227
|
+
"""SEMANTIC edges: ANN embedding similarity > 0.7 threshold."""
|
|
228
|
+
if self._ann is None or new_fact.embedding is None:
|
|
229
|
+
return []
|
|
230
|
+
try:
|
|
231
|
+
import numpy as np
|
|
232
|
+
query_vec = np.asarray(new_fact.embedding, dtype=np.float32)
|
|
233
|
+
except (ImportError, ValueError):
|
|
234
|
+
return []
|
|
235
|
+
|
|
236
|
+
edges: list[GraphEdge] = []
|
|
237
|
+
for fact_id, score in self._ann.search(query_vec, top_k=_SEMANTIC_TOP_K + 1):
|
|
238
|
+
if fact_id == new_fact.fact_id or score < _SEMANTIC_THRESHOLD:
|
|
239
|
+
continue
|
|
240
|
+
if self._edge_exists(new_fact.fact_id, fact_id, EdgeType.SEMANTIC, profile_id):
|
|
241
|
+
continue
|
|
242
|
+
edges.append(GraphEdge(
|
|
243
|
+
profile_id=profile_id, source_id=new_fact.fact_id,
|
|
244
|
+
target_id=fact_id, edge_type=EdgeType.SEMANTIC,
|
|
245
|
+
weight=round(float(score), 4),
|
|
246
|
+
))
|
|
247
|
+
if len(edges) >= _SEMANTIC_TOP_K:
|
|
248
|
+
break
|
|
249
|
+
return edges
|
|
250
|
+
|
|
251
|
+
def _build_causal_edges(
|
|
252
|
+
self, new_fact: AtomicFact, profile_id: str,
|
|
253
|
+
) -> list[GraphEdge]:
|
|
254
|
+
"""CAUSAL edges: causal markers + shared entity. Direction: cause -> effect."""
|
|
255
|
+
if not any(p.search(new_fact.content) for p in _CAUSAL_CUES):
|
|
256
|
+
return []
|
|
257
|
+
if not new_fact.canonical_entities:
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
edges: list[GraphEdge] = []
|
|
261
|
+
seen: set[str] = set()
|
|
262
|
+
for entity_id in new_fact.canonical_entities:
|
|
263
|
+
for other in self._db.get_facts_by_entity(entity_id, profile_id):
|
|
264
|
+
if other.fact_id == new_fact.fact_id or other.fact_id in seen:
|
|
265
|
+
continue
|
|
266
|
+
if self._edge_exists(other.fact_id, new_fact.fact_id, EdgeType.CAUSAL, profile_id):
|
|
267
|
+
continue
|
|
268
|
+
seen.add(other.fact_id)
|
|
269
|
+
edges.append(GraphEdge(
|
|
270
|
+
profile_id=profile_id, source_id=other.fact_id,
|
|
271
|
+
target_id=new_fact.fact_id, edge_type=EdgeType.CAUSAL,
|
|
272
|
+
weight=_CAUSAL_WEIGHT,
|
|
273
|
+
))
|
|
274
|
+
return edges
|
|
275
|
+
|
|
276
|
+
# -- Helpers -----------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
def _edge_exists(
|
|
279
|
+
self, source_id: str, target_id: str,
|
|
280
|
+
edge_type: EdgeType, profile_id: str,
|
|
281
|
+
) -> bool:
|
|
282
|
+
"""Check if an edge already exists (prevents duplicates)."""
|
|
283
|
+
rows = self._db.execute(
|
|
284
|
+
"SELECT 1 FROM graph_edges "
|
|
285
|
+
"WHERE profile_id = ? AND source_id = ? AND target_id = ? "
|
|
286
|
+
"AND edge_type = ? LIMIT 1",
|
|
287
|
+
(profile_id, source_id, target_id, edge_type.value),
|
|
288
|
+
)
|
|
289
|
+
return len(rows) > 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _parse_date(raw: str | None) -> datetime | None:
|
|
293
|
+
"""Best-effort ISO-8601 datetime parse."""
|
|
294
|
+
if not raw:
|
|
295
|
+
return None
|
|
296
|
+
for fmt in ("%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S",
|
|
297
|
+
"%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
|
|
298
|
+
try:
|
|
299
|
+
return datetime.strptime(raw, fmt)
|
|
300
|
+
except ValueError:
|
|
301
|
+
continue
|
|
302
|
+
return None
|
|
@@ -0,0 +1,160 @@
|
|
|
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 — Observation Builder (Entity Profiles).
|
|
6
|
+
|
|
7
|
+
Builds and updates accumulated knowledge profiles per entity.
|
|
8
|
+
When a new fact mentions an entity, the entity's profile is updated.
|
|
9
|
+
|
|
10
|
+
V1 had this module but NEVER CALLED it. Now wired into the encoding pipeline.
|
|
11
|
+
|
|
12
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
from datetime import UTC, datetime
|
|
20
|
+
|
|
21
|
+
from superlocalmemory.storage.models import AtomicFact, EntityProfile
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ObservationBuilder:
|
|
27
|
+
"""Build and maintain entity knowledge profiles.
|
|
28
|
+
|
|
29
|
+
Each canonical entity gets a running profile that accumulates
|
|
30
|
+
all facts known about it. Used for:
|
|
31
|
+
- Entity-centric retrieval (return profile as context)
|
|
32
|
+
- Consolidation (detect when new info conflicts with profile)
|
|
33
|
+
- Answer generation (entity profiles provide rich context)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, db) -> None:
|
|
37
|
+
self._db = db
|
|
38
|
+
|
|
39
|
+
def update_profile(
|
|
40
|
+
self,
|
|
41
|
+
entity_id: str,
|
|
42
|
+
new_fact: AtomicFact,
|
|
43
|
+
profile_id: str,
|
|
44
|
+
) -> EntityProfile:
|
|
45
|
+
"""Update (or create) entity profile with new fact.
|
|
46
|
+
|
|
47
|
+
Appends fact to profile's fact list and regenerates summary.
|
|
48
|
+
"""
|
|
49
|
+
existing = self._get_profile(entity_id, profile_id)
|
|
50
|
+
|
|
51
|
+
if existing is not None:
|
|
52
|
+
fact_ids = existing.fact_ids
|
|
53
|
+
if new_fact.fact_id not in fact_ids:
|
|
54
|
+
fact_ids = [*fact_ids, new_fact.fact_id]
|
|
55
|
+
summary = self._build_summary(entity_id, fact_ids, profile_id)
|
|
56
|
+
updated = EntityProfile(
|
|
57
|
+
profile_entry_id=existing.profile_entry_id,
|
|
58
|
+
entity_id=entity_id,
|
|
59
|
+
profile_id=profile_id,
|
|
60
|
+
knowledge_summary=summary,
|
|
61
|
+
fact_ids=fact_ids,
|
|
62
|
+
last_updated=datetime.now(UTC).isoformat(),
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
summary = self._build_summary(entity_id, [new_fact.fact_id], profile_id)
|
|
66
|
+
updated = EntityProfile(
|
|
67
|
+
entity_id=entity_id,
|
|
68
|
+
profile_id=profile_id,
|
|
69
|
+
knowledge_summary=summary,
|
|
70
|
+
fact_ids=[new_fact.fact_id],
|
|
71
|
+
last_updated=datetime.now(UTC).isoformat(),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
self._save_profile(updated)
|
|
75
|
+
return updated
|
|
76
|
+
|
|
77
|
+
def get_profile(self, entity_id: str, profile_id: str) -> EntityProfile | None:
|
|
78
|
+
"""Get the current knowledge profile for an entity."""
|
|
79
|
+
return self._get_profile(entity_id, profile_id)
|
|
80
|
+
|
|
81
|
+
def build_all_profiles(self, profile_id: str) -> list[EntityProfile]:
|
|
82
|
+
"""Rebuild all entity profiles from scratch. Use after migration."""
|
|
83
|
+
rows = self._db.execute(
|
|
84
|
+
"SELECT DISTINCT entity_id FROM canonical_entities WHERE profile_id = ?",
|
|
85
|
+
(profile_id,),
|
|
86
|
+
)
|
|
87
|
+
profiles = []
|
|
88
|
+
for row in rows:
|
|
89
|
+
eid = dict(row)["entity_id"]
|
|
90
|
+
facts = self._db.get_facts_by_entity(eid, profile_id)
|
|
91
|
+
if facts:
|
|
92
|
+
fact_ids = [f.fact_id for f in facts]
|
|
93
|
+
summary = self._build_summary(eid, fact_ids, profile_id)
|
|
94
|
+
ep = EntityProfile(
|
|
95
|
+
entity_id=eid,
|
|
96
|
+
profile_id=profile_id,
|
|
97
|
+
knowledge_summary=summary,
|
|
98
|
+
fact_ids=fact_ids,
|
|
99
|
+
last_updated=datetime.now(UTC).isoformat(),
|
|
100
|
+
)
|
|
101
|
+
self._save_profile(ep)
|
|
102
|
+
profiles.append(ep)
|
|
103
|
+
return profiles
|
|
104
|
+
|
|
105
|
+
# -- Internal ----------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def _get_profile(self, entity_id: str, profile_id: str) -> EntityProfile | None:
|
|
108
|
+
"""Load entity profile from DB."""
|
|
109
|
+
rows = self._db.execute(
|
|
110
|
+
"SELECT * FROM entity_profiles WHERE entity_id = ? AND profile_id = ?",
|
|
111
|
+
(entity_id, profile_id),
|
|
112
|
+
)
|
|
113
|
+
if not rows:
|
|
114
|
+
return None
|
|
115
|
+
d = dict(rows[0])
|
|
116
|
+
return EntityProfile(
|
|
117
|
+
profile_entry_id=d["profile_entry_id"],
|
|
118
|
+
entity_id=d["entity_id"],
|
|
119
|
+
profile_id=d["profile_id"],
|
|
120
|
+
knowledge_summary=d.get("knowledge_summary", ""),
|
|
121
|
+
fact_ids=json.loads(d.get("fact_ids_json", "[]")),
|
|
122
|
+
last_updated=d.get("last_updated", ""),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _save_profile(self, profile: EntityProfile) -> None:
|
|
126
|
+
"""Upsert entity profile to DB."""
|
|
127
|
+
self._db.execute(
|
|
128
|
+
"""INSERT OR REPLACE INTO entity_profiles
|
|
129
|
+
(profile_entry_id, entity_id, profile_id,
|
|
130
|
+
knowledge_summary, fact_ids_json, last_updated)
|
|
131
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
132
|
+
(
|
|
133
|
+
profile.profile_entry_id,
|
|
134
|
+
profile.entity_id,
|
|
135
|
+
profile.profile_id,
|
|
136
|
+
profile.knowledge_summary,
|
|
137
|
+
json.dumps(profile.fact_ids),
|
|
138
|
+
profile.last_updated,
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _build_summary(
|
|
143
|
+
self, entity_id: str, fact_ids: list[str], profile_id: str
|
|
144
|
+
) -> str:
|
|
145
|
+
"""Build a knowledge summary from all facts about an entity.
|
|
146
|
+
|
|
147
|
+
Simple concatenation for now. Mode B/C could use LLM summarization.
|
|
148
|
+
"""
|
|
149
|
+
facts = []
|
|
150
|
+
for fid in fact_ids[-20:]: # Last 20 facts to keep summary manageable
|
|
151
|
+
rows = self._db.execute(
|
|
152
|
+
"SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
153
|
+
(fid, profile_id),
|
|
154
|
+
)
|
|
155
|
+
if rows:
|
|
156
|
+
facts.append(dict(rows[0])["content"])
|
|
157
|
+
|
|
158
|
+
if not facts:
|
|
159
|
+
return ""
|
|
160
|
+
return " | ".join(facts)
|