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,376 @@
|
|
|
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 — Learning Database.
|
|
6
|
+
|
|
7
|
+
Persistent storage for the adaptive ranker: feedback signals, feature
|
|
8
|
+
vectors, engagement metrics, and serialized model state. Uses direct
|
|
9
|
+
sqlite3 connections (independent of V3 DatabaseManager) for isolation.
|
|
10
|
+
|
|
11
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import sqlite3
|
|
19
|
+
import threading
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Optional, Union
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_SCHEMA = """
|
|
27
|
+
CREATE TABLE IF NOT EXISTS learning_signals (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
profile_id TEXT NOT NULL,
|
|
30
|
+
query TEXT NOT NULL,
|
|
31
|
+
fact_id TEXT NOT NULL,
|
|
32
|
+
signal_type TEXT NOT NULL,
|
|
33
|
+
value REAL DEFAULT 1.0,
|
|
34
|
+
created_at TEXT NOT NULL
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_signals_profile
|
|
38
|
+
ON learning_signals(profile_id);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_signals_fact
|
|
40
|
+
ON learning_signals(fact_id);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS learning_features (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
profile_id TEXT NOT NULL,
|
|
45
|
+
query_id TEXT NOT NULL,
|
|
46
|
+
fact_id TEXT NOT NULL,
|
|
47
|
+
features_json TEXT NOT NULL,
|
|
48
|
+
label REAL NOT NULL,
|
|
49
|
+
created_at TEXT NOT NULL
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_features_profile
|
|
53
|
+
ON learning_features(profile_id);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS learning_model_state (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
profile_id TEXT NOT NULL UNIQUE,
|
|
58
|
+
state_bytes BLOB NOT NULL,
|
|
59
|
+
updated_at TEXT NOT NULL
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS engagement_metrics (
|
|
63
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
64
|
+
profile_id TEXT NOT NULL,
|
|
65
|
+
metric_type TEXT NOT NULL,
|
|
66
|
+
value REAL NOT NULL DEFAULT 0,
|
|
67
|
+
updated_at TEXT NOT NULL
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_engagement_profile_metric
|
|
71
|
+
ON engagement_metrics(profile_id, metric_type);
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LearningDatabase:
|
|
76
|
+
"""Persistent storage for the adaptive ranker's training pipeline.
|
|
77
|
+
|
|
78
|
+
Owns its own sqlite3 connection — independent of the main DB manager.
|
|
79
|
+
Thread-safe writes via a lock. WAL mode for concurrent reads.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
db_path: Path to the learning SQLite database file.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, db_path: Union[str, Path]) -> None:
|
|
86
|
+
self._db_path = str(db_path)
|
|
87
|
+
self._lock = threading.Lock()
|
|
88
|
+
self._init_schema()
|
|
89
|
+
|
|
90
|
+
def _connect(self) -> sqlite3.Connection:
|
|
91
|
+
"""Create a configured connection to the learning database."""
|
|
92
|
+
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
93
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
94
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
95
|
+
conn.row_factory = sqlite3.Row
|
|
96
|
+
return conn
|
|
97
|
+
|
|
98
|
+
def _init_schema(self) -> None:
|
|
99
|
+
"""Create tables and indexes if they do not exist."""
|
|
100
|
+
conn = self._connect()
|
|
101
|
+
try:
|
|
102
|
+
conn.executescript(_SCHEMA)
|
|
103
|
+
conn.commit()
|
|
104
|
+
finally:
|
|
105
|
+
conn.close()
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _now() -> str:
|
|
109
|
+
return datetime.now(timezone.utc).isoformat()
|
|
110
|
+
|
|
111
|
+
def store_signal(
|
|
112
|
+
self,
|
|
113
|
+
profile_id: str,
|
|
114
|
+
query: str,
|
|
115
|
+
fact_id: str,
|
|
116
|
+
signal_type: str,
|
|
117
|
+
value: float = 1.0,
|
|
118
|
+
) -> int:
|
|
119
|
+
"""Record a feedback signal (e.g. recall_hit, recall_miss).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
profile_id: Owning profile.
|
|
123
|
+
query: The user query that produced this signal.
|
|
124
|
+
fact_id: The fact that was matched (or missed).
|
|
125
|
+
signal_type: Label for the signal kind.
|
|
126
|
+
value: Numeric value (1.0 = strong positive, 0.0 = negative).
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Row ID of the inserted signal.
|
|
130
|
+
"""
|
|
131
|
+
with self._lock:
|
|
132
|
+
conn = self._connect()
|
|
133
|
+
try:
|
|
134
|
+
cur = conn.execute(
|
|
135
|
+
"INSERT INTO learning_signals "
|
|
136
|
+
"(profile_id, query, fact_id, signal_type, value, created_at) "
|
|
137
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
138
|
+
(profile_id, query, fact_id, signal_type, value, self._now()),
|
|
139
|
+
)
|
|
140
|
+
conn.commit()
|
|
141
|
+
return cur.lastrowid # type: ignore[return-value]
|
|
142
|
+
except sqlite3.Error as exc:
|
|
143
|
+
conn.rollback()
|
|
144
|
+
logger.error("store_signal failed: %s", exc)
|
|
145
|
+
raise
|
|
146
|
+
finally:
|
|
147
|
+
conn.close()
|
|
148
|
+
|
|
149
|
+
def get_signal_count(self, profile_id: str) -> int:
|
|
150
|
+
"""Count feedback signals for a profile.
|
|
151
|
+
|
|
152
|
+
Used to determine the adaptive ranker phase:
|
|
153
|
+
phase 1 (<20 signals) = defaults, phase 2 (20-200) = heuristic,
|
|
154
|
+
phase 3 (>200) = LightGBM.
|
|
155
|
+
"""
|
|
156
|
+
conn = self._connect()
|
|
157
|
+
try:
|
|
158
|
+
row = conn.execute(
|
|
159
|
+
"SELECT COUNT(*) AS cnt FROM learning_signals "
|
|
160
|
+
"WHERE profile_id = ?",
|
|
161
|
+
(profile_id,),
|
|
162
|
+
).fetchone()
|
|
163
|
+
return int(row["cnt"]) if row else 0
|
|
164
|
+
finally:
|
|
165
|
+
conn.close()
|
|
166
|
+
|
|
167
|
+
def store_features(
|
|
168
|
+
self,
|
|
169
|
+
profile_id: str,
|
|
170
|
+
query_id: str,
|
|
171
|
+
fact_id: str,
|
|
172
|
+
features: dict[str, float],
|
|
173
|
+
label: float,
|
|
174
|
+
) -> int:
|
|
175
|
+
"""Store a labeled feature vector for LightGBM training.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
profile_id: Owning profile.
|
|
179
|
+
query_id: Identifier for the query (hash or UUID).
|
|
180
|
+
fact_id: Identifier for the candidate fact.
|
|
181
|
+
features: Feature dict (e.g. semantic_score, bm25_score, ...).
|
|
182
|
+
label: Relevance label (1.0 = relevant, 0.0 = irrelevant).
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Row ID of the inserted record.
|
|
186
|
+
"""
|
|
187
|
+
features_json = json.dumps(features, sort_keys=True)
|
|
188
|
+
with self._lock:
|
|
189
|
+
conn = self._connect()
|
|
190
|
+
try:
|
|
191
|
+
cur = conn.execute(
|
|
192
|
+
"INSERT INTO learning_features "
|
|
193
|
+
"(profile_id, query_id, fact_id, features_json, label, created_at) "
|
|
194
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
195
|
+
(profile_id, query_id, fact_id, features_json, label, self._now()),
|
|
196
|
+
)
|
|
197
|
+
conn.commit()
|
|
198
|
+
return cur.lastrowid # type: ignore[return-value]
|
|
199
|
+
except sqlite3.Error as exc:
|
|
200
|
+
conn.rollback()
|
|
201
|
+
logger.error("store_features failed: %s", exc)
|
|
202
|
+
raise
|
|
203
|
+
finally:
|
|
204
|
+
conn.close()
|
|
205
|
+
|
|
206
|
+
def get_training_data(
|
|
207
|
+
self, profile_id: str, limit: int = 5000
|
|
208
|
+
) -> list[dict[str, Any]]:
|
|
209
|
+
"""Retrieve labeled feature vectors for model training.
|
|
210
|
+
|
|
211
|
+
Returns newest examples first. Each dict contains:
|
|
212
|
+
query_id, fact_id, features (dict), label, created_at.
|
|
213
|
+
"""
|
|
214
|
+
conn = self._connect()
|
|
215
|
+
try:
|
|
216
|
+
rows = conn.execute(
|
|
217
|
+
"SELECT query_id, fact_id, features_json, label, created_at "
|
|
218
|
+
"FROM learning_features WHERE profile_id = ? "
|
|
219
|
+
"ORDER BY created_at DESC LIMIT ?",
|
|
220
|
+
(profile_id, limit),
|
|
221
|
+
).fetchall()
|
|
222
|
+
results: list[dict[str, Any]] = []
|
|
223
|
+
for row in rows:
|
|
224
|
+
d = dict(row)
|
|
225
|
+
d["features"] = json.loads(d.pop("features_json"))
|
|
226
|
+
results.append(d)
|
|
227
|
+
return results
|
|
228
|
+
finally:
|
|
229
|
+
conn.close()
|
|
230
|
+
|
|
231
|
+
def store_model_state(self, profile_id: str, state_bytes: bytes) -> None:
|
|
232
|
+
"""Persist serialized model weights for a profile.
|
|
233
|
+
|
|
234
|
+
Uses INSERT OR REPLACE so only one state row per profile exists.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
profile_id: Owning profile.
|
|
238
|
+
state_bytes: Serialized model bytes (from joblib or similar).
|
|
239
|
+
"""
|
|
240
|
+
with self._lock:
|
|
241
|
+
conn = self._connect()
|
|
242
|
+
try:
|
|
243
|
+
conn.execute(
|
|
244
|
+
"INSERT INTO learning_model_state "
|
|
245
|
+
"(profile_id, state_bytes, updated_at) "
|
|
246
|
+
"VALUES (?, ?, ?) "
|
|
247
|
+
"ON CONFLICT(profile_id) DO UPDATE SET "
|
|
248
|
+
"state_bytes = excluded.state_bytes, "
|
|
249
|
+
"updated_at = excluded.updated_at",
|
|
250
|
+
(profile_id, state_bytes, self._now()),
|
|
251
|
+
)
|
|
252
|
+
conn.commit()
|
|
253
|
+
except sqlite3.Error as exc:
|
|
254
|
+
conn.rollback()
|
|
255
|
+
logger.error("store_model_state failed: %s", exc)
|
|
256
|
+
raise
|
|
257
|
+
finally:
|
|
258
|
+
conn.close()
|
|
259
|
+
|
|
260
|
+
def load_model_state(self, profile_id: str) -> Optional[bytes]:
|
|
261
|
+
"""Load serialized model weights for a profile.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
The stored bytes, or None if no model has been persisted.
|
|
265
|
+
"""
|
|
266
|
+
conn = self._connect()
|
|
267
|
+
try:
|
|
268
|
+
row = conn.execute(
|
|
269
|
+
"SELECT state_bytes FROM learning_model_state "
|
|
270
|
+
"WHERE profile_id = ?",
|
|
271
|
+
(profile_id,),
|
|
272
|
+
).fetchone()
|
|
273
|
+
return bytes(row["state_bytes"]) if row else None
|
|
274
|
+
finally:
|
|
275
|
+
conn.close()
|
|
276
|
+
|
|
277
|
+
def record_engagement(
|
|
278
|
+
self,
|
|
279
|
+
profile_id: str,
|
|
280
|
+
metric_type: str,
|
|
281
|
+
value: float = 1.0,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Increment (or create) an engagement counter.
|
|
284
|
+
|
|
285
|
+
Examples of metric_type: recall_count, store_count,
|
|
286
|
+
search_count, feedback_count.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
profile_id: Owning profile.
|
|
290
|
+
metric_type: The metric name.
|
|
291
|
+
value: Amount to add (default 1).
|
|
292
|
+
"""
|
|
293
|
+
with self._lock:
|
|
294
|
+
conn = self._connect()
|
|
295
|
+
try:
|
|
296
|
+
existing = conn.execute(
|
|
297
|
+
"SELECT id, value FROM engagement_metrics "
|
|
298
|
+
"WHERE profile_id = ? AND metric_type = ?",
|
|
299
|
+
(profile_id, metric_type),
|
|
300
|
+
).fetchone()
|
|
301
|
+
|
|
302
|
+
if existing:
|
|
303
|
+
new_value = float(existing["value"]) + value
|
|
304
|
+
conn.execute(
|
|
305
|
+
"UPDATE engagement_metrics SET value = ?, updated_at = ? "
|
|
306
|
+
"WHERE id = ?",
|
|
307
|
+
(new_value, self._now(), existing["id"]),
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
conn.execute(
|
|
311
|
+
"INSERT INTO engagement_metrics "
|
|
312
|
+
"(profile_id, metric_type, value, updated_at) "
|
|
313
|
+
"VALUES (?, ?, ?, ?)",
|
|
314
|
+
(profile_id, metric_type, value, self._now()),
|
|
315
|
+
)
|
|
316
|
+
conn.commit()
|
|
317
|
+
except sqlite3.Error as exc:
|
|
318
|
+
conn.rollback()
|
|
319
|
+
logger.error("record_engagement failed: %s", exc)
|
|
320
|
+
raise
|
|
321
|
+
finally:
|
|
322
|
+
conn.close()
|
|
323
|
+
|
|
324
|
+
def get_engagement_stats(self, profile_id: str) -> dict[str, float]:
|
|
325
|
+
"""Get all engagement counters for a profile.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Dict mapping metric_type to cumulative value.
|
|
329
|
+
Empty dict if no engagement data exists.
|
|
330
|
+
"""
|
|
331
|
+
conn = self._connect()
|
|
332
|
+
try:
|
|
333
|
+
rows = conn.execute(
|
|
334
|
+
"SELECT metric_type, value FROM engagement_metrics "
|
|
335
|
+
"WHERE profile_id = ?",
|
|
336
|
+
(profile_id,),
|
|
337
|
+
).fetchall()
|
|
338
|
+
return {row["metric_type"]: float(row["value"]) for row in rows}
|
|
339
|
+
finally:
|
|
340
|
+
conn.close()
|
|
341
|
+
|
|
342
|
+
def reset(self, profile_id: Optional[str] = None) -> None:
|
|
343
|
+
"""Delete learning data. GDPR Article 17 handler.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
profile_id: If provided, only erase data for that profile.
|
|
347
|
+
If None, erase ALL learning data.
|
|
348
|
+
"""
|
|
349
|
+
with self._lock:
|
|
350
|
+
conn = self._connect()
|
|
351
|
+
try:
|
|
352
|
+
tables = [
|
|
353
|
+
"learning_signals",
|
|
354
|
+
"learning_features",
|
|
355
|
+
"learning_model_state",
|
|
356
|
+
"engagement_metrics",
|
|
357
|
+
]
|
|
358
|
+
for table in tables:
|
|
359
|
+
if profile_id:
|
|
360
|
+
conn.execute(
|
|
361
|
+
f"DELETE FROM {table} WHERE profile_id = ?",
|
|
362
|
+
(profile_id,),
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
conn.execute(f"DELETE FROM {table}")
|
|
366
|
+
conn.commit()
|
|
367
|
+
logger.info(
|
|
368
|
+
"Learning data reset%s",
|
|
369
|
+
f" for profile {profile_id}" if profile_id else " (all)",
|
|
370
|
+
)
|
|
371
|
+
except sqlite3.Error as exc:
|
|
372
|
+
conn.rollback()
|
|
373
|
+
logger.error("reset failed: %s", exc)
|
|
374
|
+
raise
|
|
375
|
+
finally:
|
|
376
|
+
conn.close()
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2026 Qualixar / SuperLocalMemory (superlocalmemory.com)
|
|
4
|
+
# Part of Qualixar | Author: Varun Pratap Bhardwaj (qualixar.com | varunpratap.com)
|
|
5
|
+
"""
|
|
6
|
+
EngagementTracker -- Local-only engagement metrics for V3 learning.
|
|
7
|
+
|
|
8
|
+
Tracks user engagement events per profile:
|
|
9
|
+
- recall_count, store_count, delete_count
|
|
10
|
+
- session_count, active_days
|
|
11
|
+
- composite engagement_score
|
|
12
|
+
- health classification: active / warm / cold / inactive
|
|
13
|
+
|
|
14
|
+
All data stays local. Uses direct sqlite3 with a self-contained
|
|
15
|
+
``engagement_events`` table. NOT coupled to V3 DatabaseManager.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import sqlite3
|
|
22
|
+
import threading
|
|
23
|
+
from datetime import date, datetime, timedelta, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Dict, Optional
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("superlocalmemory.learning.engagement")
|
|
28
|
+
|
|
29
|
+
# Valid event types
|
|
30
|
+
VALID_EVENT_TYPES = frozenset({"recall", "store", "delete", "session_start"})
|
|
31
|
+
|
|
32
|
+
_CREATE_TABLE = """
|
|
33
|
+
CREATE TABLE IF NOT EXISTS engagement_events (
|
|
34
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
+
profile_id TEXT NOT NULL,
|
|
36
|
+
event_type TEXT NOT NULL,
|
|
37
|
+
created_at TEXT NOT NULL
|
|
38
|
+
)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_CREATE_INDEX = """
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_engagement_profile
|
|
43
|
+
ON engagement_events (profile_id, event_type)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# Health thresholds (events in last 7 days)
|
|
47
|
+
_ACTIVE_THRESHOLD = 10
|
|
48
|
+
_WARM_THRESHOLD = 3
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _utcnow_iso() -> str:
|
|
52
|
+
"""Return current UTC time as ISO-8601 string."""
|
|
53
|
+
return datetime.now(timezone.utc).isoformat()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _today_iso() -> str:
|
|
57
|
+
"""Return today's date as ISO-8601 string."""
|
|
58
|
+
return date.today().isoformat()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class EngagementTracker:
|
|
62
|
+
"""
|
|
63
|
+
Tracks per-profile engagement events for the V3 learning system.
|
|
64
|
+
|
|
65
|
+
Each instance owns a sqlite3 database at *db_path*.
|
|
66
|
+
Thread-safe: all writes serialised through a lock.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
db_path: Path to the sqlite3 database file.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, db_path: Path) -> None:
|
|
73
|
+
self._db_path = Path(db_path)
|
|
74
|
+
self._lock = threading.Lock()
|
|
75
|
+
self._ensure_schema()
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# Schema
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def _ensure_schema(self) -> None:
|
|
82
|
+
conn = self._connect()
|
|
83
|
+
try:
|
|
84
|
+
conn.execute(_CREATE_TABLE)
|
|
85
|
+
conn.execute(_CREATE_INDEX)
|
|
86
|
+
conn.commit()
|
|
87
|
+
finally:
|
|
88
|
+
conn.close()
|
|
89
|
+
|
|
90
|
+
def _connect(self) -> sqlite3.Connection:
|
|
91
|
+
conn = sqlite3.connect(str(self._db_path), timeout=10)
|
|
92
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
93
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
94
|
+
conn.row_factory = sqlite3.Row
|
|
95
|
+
return conn
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------
|
|
98
|
+
# Public API: record events
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def record_event(self, profile_id: str, event_type: str) -> Optional[int]:
|
|
102
|
+
"""
|
|
103
|
+
Record an engagement event.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
profile_id: Profile that generated the event.
|
|
107
|
+
event_type: One of ``recall``, ``store``, ``delete``,
|
|
108
|
+
``session_start``.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Row ID of the inserted record, or None on invalid input.
|
|
112
|
+
"""
|
|
113
|
+
if not profile_id:
|
|
114
|
+
return None
|
|
115
|
+
if event_type not in VALID_EVENT_TYPES:
|
|
116
|
+
logger.warning("Invalid event type: %r", event_type)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
now = _utcnow_iso()
|
|
120
|
+
with self._lock:
|
|
121
|
+
conn = self._connect()
|
|
122
|
+
try:
|
|
123
|
+
cursor = conn.execute(
|
|
124
|
+
"INSERT INTO engagement_events "
|
|
125
|
+
"(profile_id, event_type, created_at) VALUES (?, ?, ?)",
|
|
126
|
+
(profile_id, event_type, now),
|
|
127
|
+
)
|
|
128
|
+
conn.commit()
|
|
129
|
+
return cursor.lastrowid
|
|
130
|
+
finally:
|
|
131
|
+
conn.close()
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Public API: stats
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def get_stats(self, profile_id: str) -> Dict[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Return engagement statistics for a profile.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dict with keys: recall_count, store_count, delete_count,
|
|
143
|
+
session_count, active_days, total_events, engagement_score.
|
|
144
|
+
"""
|
|
145
|
+
conn = self._connect()
|
|
146
|
+
try:
|
|
147
|
+
# Count by event type
|
|
148
|
+
rows = conn.execute(
|
|
149
|
+
"SELECT event_type, COUNT(*) AS cnt "
|
|
150
|
+
"FROM engagement_events WHERE profile_id = ? "
|
|
151
|
+
"GROUP BY event_type",
|
|
152
|
+
(profile_id,),
|
|
153
|
+
).fetchall()
|
|
154
|
+
counts: Dict[str, int] = {r["event_type"]: r["cnt"] for r in rows}
|
|
155
|
+
|
|
156
|
+
recall_count = counts.get("recall", 0)
|
|
157
|
+
store_count = counts.get("store", 0)
|
|
158
|
+
delete_count = counts.get("delete", 0)
|
|
159
|
+
session_count = counts.get("session_start", 0)
|
|
160
|
+
total_events = sum(counts.values())
|
|
161
|
+
|
|
162
|
+
# Active days: count distinct dates
|
|
163
|
+
active_days = self._count_active_days(conn, profile_id)
|
|
164
|
+
|
|
165
|
+
# Composite engagement score
|
|
166
|
+
score = self._compute_engagement_score(
|
|
167
|
+
recall_count=recall_count,
|
|
168
|
+
store_count=store_count,
|
|
169
|
+
session_count=session_count,
|
|
170
|
+
active_days=active_days,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"recall_count": recall_count,
|
|
175
|
+
"store_count": store_count,
|
|
176
|
+
"delete_count": delete_count,
|
|
177
|
+
"session_count": session_count,
|
|
178
|
+
"active_days": active_days,
|
|
179
|
+
"total_events": total_events,
|
|
180
|
+
"engagement_score": round(score, 4),
|
|
181
|
+
}
|
|
182
|
+
finally:
|
|
183
|
+
conn.close()
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------------------
|
|
186
|
+
# Public API: health
|
|
187
|
+
# ------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
def get_health(self, profile_id: str) -> str:
|
|
190
|
+
"""
|
|
191
|
+
Classify engagement health for a profile.
|
|
192
|
+
|
|
193
|
+
Uses events in the last 7 days:
|
|
194
|
+
- **active**: >= 10 events
|
|
195
|
+
- **warm**: >= 3 events
|
|
196
|
+
- **cold**: >= 1 event
|
|
197
|
+
- **inactive**: 0 events
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
profile_id: Profile to check.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
One of ``"active"``, ``"warm"``, ``"cold"``, ``"inactive"``.
|
|
204
|
+
"""
|
|
205
|
+
recent = self._count_recent_events(profile_id, days=7)
|
|
206
|
+
|
|
207
|
+
if recent >= _ACTIVE_THRESHOLD:
|
|
208
|
+
return "active"
|
|
209
|
+
if recent >= _WARM_THRESHOLD:
|
|
210
|
+
return "warm"
|
|
211
|
+
if recent >= 1:
|
|
212
|
+
return "cold"
|
|
213
|
+
return "inactive"
|
|
214
|
+
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
# Public API: weekly summary
|
|
217
|
+
# ------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def get_weekly_summary(self, profile_id: str) -> Dict[str, Any]:
|
|
220
|
+
"""
|
|
221
|
+
Summarise the last 7 days of engagement.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Dict with period_start, period_end, total_events,
|
|
225
|
+
by_type, active_days.
|
|
226
|
+
"""
|
|
227
|
+
cutoff = (
|
|
228
|
+
datetime.now(timezone.utc) - timedelta(days=7)
|
|
229
|
+
).isoformat()
|
|
230
|
+
|
|
231
|
+
conn = self._connect()
|
|
232
|
+
try:
|
|
233
|
+
rows = conn.execute(
|
|
234
|
+
"SELECT event_type, COUNT(*) AS cnt "
|
|
235
|
+
"FROM engagement_events "
|
|
236
|
+
"WHERE profile_id = ? AND created_at >= ? "
|
|
237
|
+
"GROUP BY event_type",
|
|
238
|
+
(profile_id, cutoff),
|
|
239
|
+
).fetchall()
|
|
240
|
+
by_type = {r["event_type"]: r["cnt"] for r in rows}
|
|
241
|
+
total = sum(by_type.values())
|
|
242
|
+
|
|
243
|
+
# Active days in the window
|
|
244
|
+
day_rows = conn.execute(
|
|
245
|
+
"SELECT COUNT(DISTINCT SUBSTR(created_at, 1, 10)) "
|
|
246
|
+
"FROM engagement_events "
|
|
247
|
+
"WHERE profile_id = ? AND created_at >= ?",
|
|
248
|
+
(profile_id, cutoff),
|
|
249
|
+
).fetchone()
|
|
250
|
+
active_days = day_rows[0] if day_rows else 0
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
"period_start": (date.today() - timedelta(days=6)).isoformat(),
|
|
254
|
+
"period_end": _today_iso(),
|
|
255
|
+
"total_events": total,
|
|
256
|
+
"by_type": by_type,
|
|
257
|
+
"active_days": active_days,
|
|
258
|
+
}
|
|
259
|
+
finally:
|
|
260
|
+
conn.close()
|
|
261
|
+
|
|
262
|
+
# ------------------------------------------------------------------
|
|
263
|
+
# Internal helpers
|
|
264
|
+
# ------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def _count_active_days(
|
|
267
|
+
self,
|
|
268
|
+
conn: sqlite3.Connection,
|
|
269
|
+
profile_id: str,
|
|
270
|
+
) -> int:
|
|
271
|
+
"""Count distinct dates with at least one event."""
|
|
272
|
+
row = conn.execute(
|
|
273
|
+
"SELECT COUNT(DISTINCT SUBSTR(created_at, 1, 10)) "
|
|
274
|
+
"FROM engagement_events WHERE profile_id = ?",
|
|
275
|
+
(profile_id,),
|
|
276
|
+
).fetchone()
|
|
277
|
+
return row[0] if row else 0
|
|
278
|
+
|
|
279
|
+
def _count_recent_events(self, profile_id: str, days: int = 7) -> int:
|
|
280
|
+
"""Count events in the last *days* days."""
|
|
281
|
+
cutoff = (
|
|
282
|
+
datetime.now(timezone.utc) - timedelta(days=days)
|
|
283
|
+
).isoformat()
|
|
284
|
+
|
|
285
|
+
conn = self._connect()
|
|
286
|
+
try:
|
|
287
|
+
row = conn.execute(
|
|
288
|
+
"SELECT COUNT(*) FROM engagement_events "
|
|
289
|
+
"WHERE profile_id = ? AND created_at >= ?",
|
|
290
|
+
(profile_id, cutoff),
|
|
291
|
+
).fetchone()
|
|
292
|
+
return row[0] if row else 0
|
|
293
|
+
finally:
|
|
294
|
+
conn.close()
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _compute_engagement_score(
|
|
298
|
+
recall_count: int,
|
|
299
|
+
store_count: int,
|
|
300
|
+
session_count: int,
|
|
301
|
+
active_days: int,
|
|
302
|
+
) -> float:
|
|
303
|
+
"""
|
|
304
|
+
Compute a composite engagement score in [0.0, 1.0].
|
|
305
|
+
|
|
306
|
+
Weighted sum normalised by a soft ceiling:
|
|
307
|
+
raw = 0.4 * recall + 0.3 * store + 0.2 * session + 0.1 * days
|
|
308
|
+
score = raw / (raw + 20) (sigmoid-like saturation at ~20)
|
|
309
|
+
|
|
310
|
+
This gives:
|
|
311
|
+
- 0 activity -> 0.0
|
|
312
|
+
- 20 events -> ~0.5
|
|
313
|
+
- 100 events -> ~0.83
|
|
314
|
+
"""
|
|
315
|
+
raw = (
|
|
316
|
+
0.4 * recall_count
|
|
317
|
+
+ 0.3 * store_count
|
|
318
|
+
+ 0.2 * session_count
|
|
319
|
+
+ 0.1 * active_days
|
|
320
|
+
)
|
|
321
|
+
if raw <= 0:
|
|
322
|
+
return 0.0
|
|
323
|
+
return raw / (raw + 20.0)
|