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,183 @@
|
|
|
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 — Scene Builder (Memory Clustering).
|
|
6
|
+
|
|
7
|
+
Groups related facts into thematic scenes (EverMemOS MemScene pattern).
|
|
8
|
+
Scenes provide contextual retrieval — related facts come together.
|
|
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, MemoryScene
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Similarity threshold for assigning fact to existing scene
|
|
26
|
+
_ASSIGN_THRESHOLD = 0.6
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SceneBuilder:
|
|
30
|
+
"""Cluster related facts into thematic scenes.
|
|
31
|
+
|
|
32
|
+
When a new fact arrives:
|
|
33
|
+
1. Compute similarity to existing scenes (via scene theme embedding)
|
|
34
|
+
2. If above threshold: assign to nearest scene, update scene
|
|
35
|
+
3. If below threshold: create new scene
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, db, embedder=None) -> None:
|
|
39
|
+
self._db = db
|
|
40
|
+
self._embedder = embedder
|
|
41
|
+
self._scene_embeddings_cache: dict[str, list[float]] = {}
|
|
42
|
+
|
|
43
|
+
def assign_to_scene(
|
|
44
|
+
self,
|
|
45
|
+
new_fact: AtomicFact,
|
|
46
|
+
profile_id: str,
|
|
47
|
+
) -> MemoryScene:
|
|
48
|
+
"""Assign a fact to an existing scene or create a new one.
|
|
49
|
+
|
|
50
|
+
Always embeds the incoming fact content (when embedder is available)
|
|
51
|
+
so that the embedding is ready for comparison against existing scenes.
|
|
52
|
+
"""
|
|
53
|
+
if self._embedder is None:
|
|
54
|
+
return self._create_scene(new_fact, profile_id)
|
|
55
|
+
|
|
56
|
+
# Always compute fact embedding first — needed for comparisons
|
|
57
|
+
fact_emb = self._embedder.embed(new_fact.content)
|
|
58
|
+
|
|
59
|
+
scenes = self._get_scenes(profile_id)
|
|
60
|
+
if not scenes:
|
|
61
|
+
return self._create_scene(new_fact, profile_id)
|
|
62
|
+
|
|
63
|
+
# Find best matching scene
|
|
64
|
+
best_scene: MemoryScene | None = None
|
|
65
|
+
best_sim = -1.0
|
|
66
|
+
|
|
67
|
+
for scene in scenes:
|
|
68
|
+
# Use cached embedding if available, otherwise compute fresh
|
|
69
|
+
if scene.theme in self._scene_embeddings_cache:
|
|
70
|
+
theme_emb = self._scene_embeddings_cache[scene.theme]
|
|
71
|
+
else:
|
|
72
|
+
theme_emb = self._embedder.embed(scene.theme)
|
|
73
|
+
self._scene_embeddings_cache[scene.theme] = theme_emb
|
|
74
|
+
sim = _cosine(fact_emb, theme_emb)
|
|
75
|
+
if sim > best_sim:
|
|
76
|
+
best_sim = sim
|
|
77
|
+
best_scene = scene
|
|
78
|
+
|
|
79
|
+
if best_scene is not None and best_sim >= _ASSIGN_THRESHOLD:
|
|
80
|
+
return self._add_to_scene(best_scene, new_fact, profile_id)
|
|
81
|
+
|
|
82
|
+
return self._create_scene(new_fact, profile_id)
|
|
83
|
+
|
|
84
|
+
def get_scene_for_fact(self, fact_id: str, profile_id: str) -> MemoryScene | None:
|
|
85
|
+
"""Get the scene containing a specific fact."""
|
|
86
|
+
rows = self._db.execute(
|
|
87
|
+
"SELECT * FROM memory_scenes WHERE profile_id = ?", (profile_id,)
|
|
88
|
+
)
|
|
89
|
+
for row in rows:
|
|
90
|
+
d = dict(row)
|
|
91
|
+
fids = json.loads(d.get("fact_ids_json", "[]"))
|
|
92
|
+
if fact_id in fids:
|
|
93
|
+
return self._row_to_scene(d)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def get_all_scenes(self, profile_id: str) -> list[MemoryScene]:
|
|
97
|
+
"""Get all scenes for a profile."""
|
|
98
|
+
return self._get_scenes(profile_id)
|
|
99
|
+
|
|
100
|
+
# -- Internal ----------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def _create_scene(self, fact: AtomicFact, profile_id: str) -> MemoryScene:
|
|
103
|
+
"""Create a new scene from a single fact.
|
|
104
|
+
|
|
105
|
+
Pre-computes and caches the theme embedding for efficient later
|
|
106
|
+
comparisons in assign_to_scene.
|
|
107
|
+
"""
|
|
108
|
+
theme = fact.content[:200]
|
|
109
|
+
# Pre-compute theme embedding for future comparisons
|
|
110
|
+
if self._embedder is not None:
|
|
111
|
+
self._scene_embeddings_cache[theme] = self._embedder.embed(theme)
|
|
112
|
+
|
|
113
|
+
scene = MemoryScene(
|
|
114
|
+
profile_id=profile_id,
|
|
115
|
+
theme=theme,
|
|
116
|
+
fact_ids=[fact.fact_id],
|
|
117
|
+
entity_ids=list(fact.canonical_entities),
|
|
118
|
+
created_at=datetime.now(UTC).isoformat(),
|
|
119
|
+
last_updated=datetime.now(UTC).isoformat(),
|
|
120
|
+
)
|
|
121
|
+
self._save_scene(scene)
|
|
122
|
+
return scene
|
|
123
|
+
|
|
124
|
+
def _add_to_scene(
|
|
125
|
+
self, scene: MemoryScene, fact: AtomicFact, profile_id: str
|
|
126
|
+
) -> MemoryScene:
|
|
127
|
+
"""Add a fact to an existing scene."""
|
|
128
|
+
new_fact_ids = [*scene.fact_ids, fact.fact_id]
|
|
129
|
+
new_entity_ids = list(set(scene.entity_ids) | set(fact.canonical_entities))
|
|
130
|
+
updated = MemoryScene(
|
|
131
|
+
scene_id=scene.scene_id,
|
|
132
|
+
profile_id=profile_id,
|
|
133
|
+
theme=scene.theme,
|
|
134
|
+
fact_ids=new_fact_ids,
|
|
135
|
+
entity_ids=new_entity_ids,
|
|
136
|
+
created_at=scene.created_at,
|
|
137
|
+
last_updated=datetime.now(UTC).isoformat(),
|
|
138
|
+
)
|
|
139
|
+
self._save_scene(updated)
|
|
140
|
+
return updated
|
|
141
|
+
|
|
142
|
+
def _get_scenes(self, profile_id: str) -> list[MemoryScene]:
|
|
143
|
+
"""Load all scenes from DB."""
|
|
144
|
+
rows = self._db.execute(
|
|
145
|
+
"SELECT * FROM memory_scenes WHERE profile_id = ? ORDER BY last_updated DESC",
|
|
146
|
+
(profile_id,),
|
|
147
|
+
)
|
|
148
|
+
return [self._row_to_scene(dict(r)) for r in rows]
|
|
149
|
+
|
|
150
|
+
def _save_scene(self, scene: MemoryScene) -> None:
|
|
151
|
+
"""Upsert scene to DB."""
|
|
152
|
+
self._db.execute(
|
|
153
|
+
"""INSERT OR REPLACE INTO memory_scenes
|
|
154
|
+
(scene_id, profile_id, theme, fact_ids_json, entity_ids_json,
|
|
155
|
+
created_at, last_updated)
|
|
156
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
157
|
+
(
|
|
158
|
+
scene.scene_id, scene.profile_id, scene.theme,
|
|
159
|
+
json.dumps(scene.fact_ids), json.dumps(scene.entity_ids),
|
|
160
|
+
scene.created_at, scene.last_updated,
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _row_to_scene(d: dict) -> MemoryScene:
|
|
166
|
+
return MemoryScene(
|
|
167
|
+
scene_id=d["scene_id"],
|
|
168
|
+
profile_id=d["profile_id"],
|
|
169
|
+
theme=d.get("theme", ""),
|
|
170
|
+
fact_ids=json.loads(d.get("fact_ids_json", "[]")),
|
|
171
|
+
entity_ids=json.loads(d.get("entity_ids_json", "[]")),
|
|
172
|
+
created_at=d.get("created_at", ""),
|
|
173
|
+
last_updated=d.get("last_updated", ""),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _cosine(a: list[float], b: list[float]) -> float:
|
|
178
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
179
|
+
na = sum(x * x for x in a) ** 0.5
|
|
180
|
+
nb = sum(x * x for x in b) ** 0.5
|
|
181
|
+
if na == 0 or nb == 0:
|
|
182
|
+
return 0.0
|
|
183
|
+
return dot / (na * nb)
|
|
@@ -0,0 +1,90 @@
|
|
|
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 — Signal Inference (6 Types).
|
|
6
|
+
|
|
7
|
+
Infers the signal type of a memory/fact from its content.
|
|
8
|
+
Ported from V2.8 — used to adjust retrieval channel weights.
|
|
9
|
+
|
|
10
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from superlocalmemory.storage.models import SignalType
|
|
18
|
+
|
|
19
|
+
# Compiled patterns for each signal type
|
|
20
|
+
_PATTERNS: dict[SignalType, re.Pattern] = {
|
|
21
|
+
SignalType.EMOTIONAL: re.compile(
|
|
22
|
+
r"\b(happy|sad|angry|frustrated|excited|worried|anxious|"
|
|
23
|
+
r"love|hate|afraid|grateful|disappointed|thrilled|upset|"
|
|
24
|
+
r"feeling|emotion|mood)\b",
|
|
25
|
+
re.IGNORECASE,
|
|
26
|
+
),
|
|
27
|
+
SignalType.TEMPORAL: re.compile(
|
|
28
|
+
r"\b(when|date|time|schedule|deadline|tomorrow|yesterday|"
|
|
29
|
+
r"last week|next month|ago|soon|later|earlier|"
|
|
30
|
+
r"january|february|march|april|may|june|july|"
|
|
31
|
+
r"august|september|october|november|december|"
|
|
32
|
+
r"monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b",
|
|
33
|
+
re.IGNORECASE,
|
|
34
|
+
),
|
|
35
|
+
SignalType.OPINION: re.compile(
|
|
36
|
+
r"\b(think|believe|prefer|opinion|recommend|suggest|"
|
|
37
|
+
r"should|better|worse|best|worst|favorite|"
|
|
38
|
+
r"in my view|personally|i feel|i guess)\b",
|
|
39
|
+
re.IGNORECASE,
|
|
40
|
+
),
|
|
41
|
+
SignalType.REQUEST: re.compile(
|
|
42
|
+
r"\b(please|could you|would you|can you|help|need|"
|
|
43
|
+
r"want|looking for|searching|find|tell me|show me|"
|
|
44
|
+
r"remind me|remember)\b",
|
|
45
|
+
re.IGNORECASE,
|
|
46
|
+
),
|
|
47
|
+
SignalType.SOCIAL: re.compile(
|
|
48
|
+
r"\b(friend|family|colleague|partner|boss|team|"
|
|
49
|
+
r"relationship|together|married|dating|met with|"
|
|
50
|
+
r"call|message|email|chat)\b",
|
|
51
|
+
re.IGNORECASE,
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Priority order: more specific types checked first.
|
|
56
|
+
# SOCIAL before TEMPORAL because social signals often co-occur with
|
|
57
|
+
# temporal markers ("met yesterday") and the social context is primary.
|
|
58
|
+
_PRIORITY = [
|
|
59
|
+
SignalType.REQUEST,
|
|
60
|
+
SignalType.SOCIAL,
|
|
61
|
+
SignalType.TEMPORAL,
|
|
62
|
+
SignalType.EMOTIONAL,
|
|
63
|
+
SignalType.OPINION,
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def infer_signal(text: str) -> SignalType:
|
|
68
|
+
"""Infer the signal type of a text.
|
|
69
|
+
|
|
70
|
+
Returns the most specific matching type, or FACTUAL as default.
|
|
71
|
+
"""
|
|
72
|
+
for stype in _PRIORITY:
|
|
73
|
+
pattern = _PATTERNS.get(stype)
|
|
74
|
+
if pattern and pattern.search(text):
|
|
75
|
+
return stype
|
|
76
|
+
return SignalType.FACTUAL
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def infer_signal_scores(text: str) -> dict[SignalType, float]:
|
|
80
|
+
"""Compute a signal score for each type (0.0-1.0).
|
|
81
|
+
|
|
82
|
+
Useful for multi-signal content that mixes types.
|
|
83
|
+
Score = number of pattern matches / 5 (capped at 1.0).
|
|
84
|
+
"""
|
|
85
|
+
scores: dict[SignalType, float] = {}
|
|
86
|
+
for stype, pattern in _PATTERNS.items():
|
|
87
|
+
matches = pattern.findall(text)
|
|
88
|
+
scores[stype] = min(1.0, len(matches) / 5.0)
|
|
89
|
+
scores[SignalType.FACTUAL] = 1.0 - max(scores.values()) if scores else 1.0
|
|
90
|
+
return scores
|
|
@@ -0,0 +1,426 @@
|
|
|
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 — Temporal Parser (3-Date Model).
|
|
6
|
+
|
|
7
|
+
Parses and enriches temporal information for every memory:
|
|
8
|
+
1. observation_date — when the conversation happened
|
|
9
|
+
2. referenced_date — date mentioned in content ("last Tuesday")
|
|
10
|
+
3. temporal_interval — [start, end] for duration events
|
|
11
|
+
|
|
12
|
+
Mastra achieved 95.5% temporal reasoning with this pattern.
|
|
13
|
+
V1 stored session_date as raw strings and ignored content dates entirely.
|
|
14
|
+
|
|
15
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import re
|
|
22
|
+
from datetime import UTC, datetime, timedelta
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from dateutil.parser import parse as dateutil_parse, ParserError
|
|
26
|
+
from dateutil.relativedelta import relativedelta
|
|
27
|
+
|
|
28
|
+
from superlocalmemory.storage.models import AtomicFact, TemporalEvent
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Compiled regex patterns (compiled once, reused)
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
_ISO_DATE = re.compile(r"\b\d{4}-\d{2}-\d{2}\b")
|
|
37
|
+
|
|
38
|
+
_US_DATE = re.compile(r"\b\d{1,2}/\d{1,2}/\d{2,4}\b")
|
|
39
|
+
|
|
40
|
+
_WRITTEN_DATE = re.compile(
|
|
41
|
+
r"\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|"
|
|
42
|
+
r"Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)"
|
|
43
|
+
r"\s+\d{1,2},?\s*\d{4}\b",
|
|
44
|
+
re.IGNORECASE,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
_WRITTEN_DATE_DMY = re.compile(
|
|
48
|
+
r"\b\d{1,2}\s+(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|"
|
|
49
|
+
r"Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)"
|
|
50
|
+
r",?\s*\d{4}\b",
|
|
51
|
+
re.IGNORECASE,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# "January 2023", "March 2024" — month + year without day
|
|
55
|
+
_MONTH_YEAR = re.compile(
|
|
56
|
+
r"\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|"
|
|
57
|
+
r"Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)"
|
|
58
|
+
r"\s+\d{4}\b",
|
|
59
|
+
re.IGNORECASE,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
_WEEKDAYS = r"(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)"
|
|
63
|
+
_TIME_UNITS = r"(?:days?|weeks?|months?|years?)"
|
|
64
|
+
|
|
65
|
+
_RELATIVE = re.compile(
|
|
66
|
+
rf"\b(?:last|next|this)\s+(?:{_WEEKDAYS}|week|month|year|spring|summer|autumn|fall|winter)\b",
|
|
67
|
+
re.IGNORECASE,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
_AGO = re.compile(
|
|
71
|
+
rf"\b(\d+)\s+{_TIME_UNITS}\s+ago\b",
|
|
72
|
+
re.IGNORECASE,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
_IN_FUTURE = re.compile(
|
|
76
|
+
rf"\bin\s+(\d+)\s+{_TIME_UNITS}\b",
|
|
77
|
+
re.IGNORECASE,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
_DURATION = re.compile(
|
|
81
|
+
r"\bfrom\s+(.+?)\s+to\s+(.+?)(?:\.|,|;|$)",
|
|
82
|
+
re.IGNORECASE,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
_FOR_DURATION = re.compile(
|
|
86
|
+
rf"\bfor\s+(\d+)\s+{_TIME_UNITS}\b",
|
|
87
|
+
re.IGNORECASE,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
_VAGUE_TERMS: dict[str, int] = {
|
|
91
|
+
"yesterday": -1,
|
|
92
|
+
"today": 0,
|
|
93
|
+
"tomorrow": 1,
|
|
94
|
+
"recently": -7,
|
|
95
|
+
"a while ago": -30,
|
|
96
|
+
"soon": 7,
|
|
97
|
+
"the other day": -3,
|
|
98
|
+
"last night": -1,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_SEASON_MONTHS: dict[str, tuple[int, int]] = {
|
|
102
|
+
"spring": (3, 5),
|
|
103
|
+
"summer": (6, 8),
|
|
104
|
+
"autumn": (9, 11),
|
|
105
|
+
"fall": (9, 11),
|
|
106
|
+
"winter": (12, 2),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_WEEKDAY_MAP: dict[str, int] = {
|
|
110
|
+
"monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3,
|
|
111
|
+
"friday": 4, "saturday": 5, "sunday": 6,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _safe_iso(dt: datetime | None) -> str | None:
|
|
116
|
+
"""Convert datetime to ISO-8601 string, or None."""
|
|
117
|
+
if dt is None:
|
|
118
|
+
return None
|
|
119
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _extract_unit(text: str) -> str:
|
|
123
|
+
"""Extract time unit from a matched span (days, weeks, months, years)."""
|
|
124
|
+
lower = text.lower()
|
|
125
|
+
for unit in ("year", "month", "week", "day"):
|
|
126
|
+
if unit in lower:
|
|
127
|
+
return unit
|
|
128
|
+
return "day"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TemporalParser:
|
|
132
|
+
"""3-date temporal parser for memory enrichment.
|
|
133
|
+
|
|
134
|
+
Extracts observation_date, referenced_date, and temporal intervals
|
|
135
|
+
from session dates and fact content. Handles absolute, relative,
|
|
136
|
+
duration, and vague temporal expressions.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, reference_date: str | None = None) -> None:
|
|
140
|
+
"""Initialize with an optional reference date for relative computation.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
reference_date: ISO-8601 string used as "today" for relative dates.
|
|
144
|
+
Defaults to current UTC time if None.
|
|
145
|
+
"""
|
|
146
|
+
if reference_date is not None:
|
|
147
|
+
try:
|
|
148
|
+
parsed = dateutil_parse(reference_date)
|
|
149
|
+
self._ref = parsed.replace(tzinfo=UTC) if parsed.tzinfo is None else parsed
|
|
150
|
+
except (ParserError, ValueError):
|
|
151
|
+
logger.warning("Unparseable reference_date %r, using UTC now", reference_date)
|
|
152
|
+
self._ref = datetime.now(UTC)
|
|
153
|
+
else:
|
|
154
|
+
self._ref = datetime.now(UTC)
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------------
|
|
157
|
+
# Public API
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def parse_session_date(self, raw_date: str) -> str | None:
|
|
161
|
+
"""Parse LoCoMo-style session dates into ISO-8601.
|
|
162
|
+
|
|
163
|
+
Handles formats like:
|
|
164
|
+
- "1:56 pm on 8 May, 2023"
|
|
165
|
+
- "May 8, 2023"
|
|
166
|
+
- "2023-05-08"
|
|
167
|
+
- "8:56 pm on 20 July, 2023"
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
ISO-8601 string or None if unparseable.
|
|
171
|
+
"""
|
|
172
|
+
if not raw_date or not raw_date.strip():
|
|
173
|
+
return None
|
|
174
|
+
try:
|
|
175
|
+
dt = dateutil_parse(raw_date, fuzzy=True)
|
|
176
|
+
return _safe_iso(dt)
|
|
177
|
+
except (ParserError, ValueError, OverflowError):
|
|
178
|
+
logger.debug("Could not parse session_date: %r", raw_date)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def extract_dates_from_text(self, text: str) -> dict[str, str | None]:
|
|
182
|
+
"""Extract dates mentioned in fact content.
|
|
183
|
+
|
|
184
|
+
Strategy:
|
|
185
|
+
1. Regex pass for structured date patterns
|
|
186
|
+
2. Relative date resolution against reference_date
|
|
187
|
+
3. Vague term mapping to approximate offsets
|
|
188
|
+
4. Duration extraction for intervals
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
dict with keys: referenced_date, interval_start, interval_end
|
|
192
|
+
All values are ISO-8601 strings or None.
|
|
193
|
+
"""
|
|
194
|
+
if not text:
|
|
195
|
+
return {"referenced_date": None, "interval_start": None, "interval_end": None}
|
|
196
|
+
|
|
197
|
+
dates_found: list[datetime] = []
|
|
198
|
+
interval_start: datetime | None = None
|
|
199
|
+
interval_end: datetime | None = None
|
|
200
|
+
|
|
201
|
+
# --- Pass 1: Duration expressions ("from X to Y") ---
|
|
202
|
+
dur_match = _DURATION.search(text)
|
|
203
|
+
if dur_match:
|
|
204
|
+
start_dt = self._try_parse(dur_match.group(1).strip())
|
|
205
|
+
end_dt = self._try_parse(dur_match.group(2).strip())
|
|
206
|
+
if start_dt is not None and end_dt is not None:
|
|
207
|
+
interval_start = start_dt
|
|
208
|
+
interval_end = end_dt
|
|
209
|
+
|
|
210
|
+
# --- Pass 2: "for N units" duration ---
|
|
211
|
+
if interval_start is None:
|
|
212
|
+
for_match = _FOR_DURATION.search(text)
|
|
213
|
+
if for_match:
|
|
214
|
+
count = int(for_match.group(1))
|
|
215
|
+
unit = _extract_unit(for_match.group(0))
|
|
216
|
+
delta = self._unit_delta(count, unit)
|
|
217
|
+
interval_start = self._ref
|
|
218
|
+
interval_end = self._ref + delta
|
|
219
|
+
|
|
220
|
+
# --- Pass 3: Absolute date patterns ---
|
|
221
|
+
for pattern in (_ISO_DATE, _WRITTEN_DATE, _WRITTEN_DATE_DMY, _MONTH_YEAR, _US_DATE):
|
|
222
|
+
for match in pattern.finditer(text):
|
|
223
|
+
dt = self._try_parse(match.group(0))
|
|
224
|
+
if dt is not None:
|
|
225
|
+
dates_found.append(dt)
|
|
226
|
+
|
|
227
|
+
# --- Pass 4: Relative expressions ---
|
|
228
|
+
for match in _RELATIVE.finditer(text):
|
|
229
|
+
dt = self._resolve_relative(match.group(0))
|
|
230
|
+
if dt is not None:
|
|
231
|
+
dates_found.append(dt)
|
|
232
|
+
|
|
233
|
+
# --- Pass 5: "N units ago" ---
|
|
234
|
+
for match in _AGO.finditer(text):
|
|
235
|
+
count = int(match.group(1))
|
|
236
|
+
unit = _extract_unit(match.group(0))
|
|
237
|
+
dt = self._ref - self._unit_delta(count, unit)
|
|
238
|
+
dates_found.append(dt)
|
|
239
|
+
|
|
240
|
+
# --- Pass 6: "in N units" ---
|
|
241
|
+
for match in _IN_FUTURE.finditer(text):
|
|
242
|
+
count = int(match.group(1))
|
|
243
|
+
unit = _extract_unit(match.group(0))
|
|
244
|
+
dt = self._ref + self._unit_delta(count, unit)
|
|
245
|
+
dates_found.append(dt)
|
|
246
|
+
|
|
247
|
+
# --- Pass 7: Vague terms ---
|
|
248
|
+
text_lower = text.lower()
|
|
249
|
+
for term, offset_days in _VAGUE_TERMS.items():
|
|
250
|
+
if term in text_lower:
|
|
251
|
+
dates_found.append(self._ref + timedelta(days=offset_days))
|
|
252
|
+
break # Take first vague match only
|
|
253
|
+
|
|
254
|
+
# --- Assemble result ---
|
|
255
|
+
referenced: datetime | None = None
|
|
256
|
+
if dates_found:
|
|
257
|
+
referenced = dates_found[0]
|
|
258
|
+
# Two distinct dates without explicit "from...to" -> interval
|
|
259
|
+
if len(dates_found) >= 2 and interval_start is None:
|
|
260
|
+
# Normalize tz-awareness before comparing
|
|
261
|
+
_normed = [d.replace(tzinfo=UTC) if d.tzinfo is None else d for d in dates_found[:2]]
|
|
262
|
+
sorted_dates = sorted(_normed)
|
|
263
|
+
interval_start = sorted_dates[0]
|
|
264
|
+
interval_end = sorted_dates[1]
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"referenced_date": _safe_iso(referenced),
|
|
268
|
+
"interval_start": _safe_iso(interval_start),
|
|
269
|
+
"interval_end": _safe_iso(interval_end),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
def build_temporal_event(
|
|
273
|
+
self,
|
|
274
|
+
fact: AtomicFact,
|
|
275
|
+
session_date: str | None,
|
|
276
|
+
entity_id: str,
|
|
277
|
+
) -> TemporalEvent | None:
|
|
278
|
+
"""Create a TemporalEvent for a fact-entity pair.
|
|
279
|
+
|
|
280
|
+
Combines the parsed session_date (observation) with dates
|
|
281
|
+
extracted from fact content (referenced + interval).
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
TemporalEvent if any temporal info exists, else None.
|
|
285
|
+
"""
|
|
286
|
+
obs_date = self.parse_session_date(session_date) if session_date else None
|
|
287
|
+
content_dates = self.extract_dates_from_text(fact.content)
|
|
288
|
+
|
|
289
|
+
ref_date = content_dates["referenced_date"]
|
|
290
|
+
int_start = content_dates["interval_start"]
|
|
291
|
+
int_end = content_dates["interval_end"]
|
|
292
|
+
|
|
293
|
+
# Use fact-level fields if already populated (from upstream)
|
|
294
|
+
if fact.observation_date and not obs_date:
|
|
295
|
+
obs_date = fact.observation_date
|
|
296
|
+
if fact.referenced_date and not ref_date:
|
|
297
|
+
ref_date = fact.referenced_date
|
|
298
|
+
if fact.interval_start and not int_start:
|
|
299
|
+
int_start = fact.interval_start
|
|
300
|
+
if fact.interval_end and not int_end:
|
|
301
|
+
int_end = fact.interval_end
|
|
302
|
+
|
|
303
|
+
# Only create event if we have at least one temporal anchor
|
|
304
|
+
if not any([obs_date, ref_date, int_start, int_end]):
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
return TemporalEvent(
|
|
308
|
+
profile_id=fact.profile_id,
|
|
309
|
+
entity_id=entity_id,
|
|
310
|
+
fact_id=fact.fact_id,
|
|
311
|
+
observation_date=obs_date,
|
|
312
|
+
referenced_date=ref_date,
|
|
313
|
+
interval_start=int_start,
|
|
314
|
+
interval_end=int_end,
|
|
315
|
+
description=fact.content[:200],
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def build_entity_timeline(
|
|
319
|
+
self,
|
|
320
|
+
entity_id: str,
|
|
321
|
+
facts: list[AtomicFact],
|
|
322
|
+
session_date: str | None = None,
|
|
323
|
+
) -> list[TemporalEvent]:
|
|
324
|
+
"""Build a chronological timeline for an entity from its facts.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
List of TemporalEvents sorted by earliest available date.
|
|
328
|
+
"""
|
|
329
|
+
events: list[TemporalEvent] = []
|
|
330
|
+
for fact in facts:
|
|
331
|
+
s_date = session_date or fact.observation_date
|
|
332
|
+
event = self.build_temporal_event(fact, s_date, entity_id)
|
|
333
|
+
if event is not None:
|
|
334
|
+
events.append(event)
|
|
335
|
+
|
|
336
|
+
# Sort by the earliest available date
|
|
337
|
+
def _sort_key(ev: TemporalEvent) -> str:
|
|
338
|
+
for field in (ev.referenced_date, ev.observation_date, ev.interval_start):
|
|
339
|
+
if field:
|
|
340
|
+
return field
|
|
341
|
+
return "9999-12-31T23:59:59"
|
|
342
|
+
|
|
343
|
+
return sorted(events, key=_sort_key)
|
|
344
|
+
|
|
345
|
+
# ------------------------------------------------------------------
|
|
346
|
+
# Internal helpers
|
|
347
|
+
# ------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
def _try_parse(self, text: str) -> datetime | None:
|
|
350
|
+
"""Attempt to parse a text fragment as a date."""
|
|
351
|
+
if not text or len(text.strip()) < 3:
|
|
352
|
+
return None
|
|
353
|
+
try:
|
|
354
|
+
return dateutil_parse(text, fuzzy=True)
|
|
355
|
+
except (ParserError, ValueError, OverflowError):
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
def _resolve_relative(self, expr: str) -> datetime | None:
|
|
359
|
+
"""Resolve a relative expression like 'last Tuesday' or 'next month'."""
|
|
360
|
+
lower = expr.lower().strip()
|
|
361
|
+
parts = lower.split()
|
|
362
|
+
if len(parts) < 2:
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
modifier = parts[0] # last / next / this
|
|
366
|
+
target = parts[1]
|
|
367
|
+
|
|
368
|
+
# Weekday resolution
|
|
369
|
+
if target in _WEEKDAY_MAP:
|
|
370
|
+
target_day = _WEEKDAY_MAP[target]
|
|
371
|
+
current_day = self._ref.weekday()
|
|
372
|
+
if modifier == "last":
|
|
373
|
+
diff = (current_day - target_day) % 7
|
|
374
|
+
diff = diff if diff > 0 else 7
|
|
375
|
+
return self._ref - timedelta(days=diff)
|
|
376
|
+
elif modifier == "next":
|
|
377
|
+
diff = (target_day - current_day) % 7
|
|
378
|
+
diff = diff if diff > 0 else 7
|
|
379
|
+
return self._ref + timedelta(days=diff)
|
|
380
|
+
else: # this
|
|
381
|
+
diff = (target_day - current_day) % 7
|
|
382
|
+
return self._ref + timedelta(days=diff)
|
|
383
|
+
|
|
384
|
+
# Week / month / year
|
|
385
|
+
if target == "week":
|
|
386
|
+
offset = {"last": -7, "next": 7, "this": 0}
|
|
387
|
+
return self._ref + timedelta(days=offset.get(modifier, 0))
|
|
388
|
+
if target == "month":
|
|
389
|
+
offset = {"last": -1, "next": 1, "this": 0}
|
|
390
|
+
return self._ref + relativedelta(months=offset.get(modifier, 0))
|
|
391
|
+
if target == "year":
|
|
392
|
+
offset = {"last": -1, "next": 1, "this": 0}
|
|
393
|
+
return self._ref + relativedelta(years=offset.get(modifier, 0))
|
|
394
|
+
|
|
395
|
+
# Seasons
|
|
396
|
+
if target in _SEASON_MONTHS:
|
|
397
|
+
start_m, end_m = _SEASON_MONTHS[target]
|
|
398
|
+
year = self._ref.year
|
|
399
|
+
if modifier == "last":
|
|
400
|
+
year -= 1
|
|
401
|
+
elif modifier == "next":
|
|
402
|
+
year += 1
|
|
403
|
+
# Return midpoint of season
|
|
404
|
+
if start_m <= end_m:
|
|
405
|
+
mid_m = (start_m + end_m) // 2
|
|
406
|
+
else:
|
|
407
|
+
mid_m = 1 # winter spans Dec-Feb, midpoint ~ Jan
|
|
408
|
+
try:
|
|
409
|
+
return datetime(year, mid_m, 15)
|
|
410
|
+
except ValueError:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
@staticmethod
|
|
416
|
+
def _unit_delta(count: int, unit: str) -> timedelta | relativedelta:
|
|
417
|
+
"""Build a timedelta/relativedelta from count + unit string."""
|
|
418
|
+
if unit == "day":
|
|
419
|
+
return timedelta(days=count)
|
|
420
|
+
if unit == "week":
|
|
421
|
+
return timedelta(weeks=count)
|
|
422
|
+
if unit == "month":
|
|
423
|
+
return relativedelta(months=count)
|
|
424
|
+
if unit == "year":
|
|
425
|
+
return relativedelta(years=count)
|
|
426
|
+
return timedelta(days=count)
|