superlocalmemory 2.8.6 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -1
- package/NOTICE +63 -0
- package/README.md +165 -480
- package/bin/slm +17 -449
- package/bin/slm-npm +62 -48
- package/conftest.py +5 -0
- package/docs/api-reference.md +284 -0
- package/docs/architecture.md +149 -0
- package/docs/auto-memory.md +150 -0
- package/docs/cli-reference.md +276 -0
- package/docs/compliance.md +191 -0
- package/docs/configuration.md +182 -0
- package/docs/getting-started.md +102 -0
- package/docs/ide-setup.md +261 -0
- package/docs/mcp-tools.md +220 -0
- package/docs/migration-from-v2.md +170 -0
- package/docs/profiles.md +173 -0
- package/docs/troubleshooting.md +310 -0
- package/{configs → ide/configs}/antigravity-mcp.json +3 -3
- package/ide/configs/chatgpt-desktop-mcp.json +16 -0
- package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
- package/{configs → ide/configs}/codex-mcp.toml +4 -4
- package/{configs → ide/configs}/continue-mcp.yaml +4 -3
- package/{configs → ide/configs}/continue-skills.yaml +6 -6
- package/ide/configs/cursor-mcp.json +15 -0
- package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
- package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
- package/{configs → ide/configs}/opencode-mcp.json +2 -2
- package/{configs → ide/configs}/perplexity-mcp.json +2 -2
- package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
- package/{configs → ide/configs}/windsurf-mcp.json +3 -3
- package/{configs → ide/configs}/zed-mcp.json +2 -2
- package/{hooks → ide/hooks}/context-hook.js +9 -20
- package/ide/hooks/memory-list-skill.js +70 -0
- package/ide/hooks/memory-profile-skill.js +101 -0
- package/ide/hooks/memory-recall-skill.js +62 -0
- package/ide/hooks/memory-remember-skill.js +68 -0
- package/ide/hooks/memory-reset-skill.js +160 -0
- package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
- package/ide/integrations/langchain/README.md +106 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
- package/ide/integrations/langchain/pyproject.toml +38 -0
- package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
- package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
- package/ide/integrations/langchain/tests/test_security.py +117 -0
- package/ide/integrations/llamaindex/README.md +81 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
- package/ide/integrations/llamaindex/pyproject.toml +43 -0
- package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
- package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
- package/ide/integrations/llamaindex/tests/test_security.py +241 -0
- package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
- package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
- package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
- package/package.json +13 -22
- package/pyproject.toml +85 -0
- package/scripts/build-dmg.sh +417 -0
- package/scripts/install-skills.ps1 +334 -0
- package/scripts/postinstall.js +2 -2
- package/scripts/start-dashboard.ps1 +52 -0
- package/scripts/start-dashboard.sh +41 -0
- package/scripts/sync-wiki.ps1 +127 -0
- package/scripts/sync-wiki.sh +82 -0
- package/scripts/test-dmg.sh +161 -0
- package/scripts/test-npm-package.ps1 +252 -0
- package/scripts/test-npm-package.sh +207 -0
- package/scripts/verify-install.ps1 +294 -0
- package/scripts/verify-install.sh +266 -0
- package/src/superlocalmemory/__init__.py +0 -0
- package/src/superlocalmemory/attribution/__init__.py +9 -0
- package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
- package/src/superlocalmemory/attribution/signer.py +153 -0
- package/src/superlocalmemory/attribution/watermark.py +189 -0
- package/src/superlocalmemory/cli/__init__.py +5 -0
- package/src/superlocalmemory/cli/commands.py +245 -0
- package/src/superlocalmemory/cli/main.py +89 -0
- package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
- package/src/superlocalmemory/cli/post_install.py +99 -0
- package/src/superlocalmemory/cli/setup_wizard.py +129 -0
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +204 -0
- package/src/superlocalmemory/compliance/audit.py +314 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
- package/src/superlocalmemory/compliance/gdpr.py +294 -0
- package/src/superlocalmemory/compliance/lifecycle.py +158 -0
- package/src/superlocalmemory/compliance/retention.py +232 -0
- package/src/superlocalmemory/compliance/scheduler.py +148 -0
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/config.py +391 -0
- package/src/superlocalmemory/core/embeddings.py +293 -0
- package/src/superlocalmemory/core/engine.py +701 -0
- package/src/superlocalmemory/core/hooks.py +65 -0
- package/src/superlocalmemory/core/maintenance.py +172 -0
- package/src/superlocalmemory/core/modes.py +140 -0
- package/src/superlocalmemory/core/profiles.py +234 -0
- package/src/superlocalmemory/core/registry.py +117 -0
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/consolidator.py +485 -0
- package/src/superlocalmemory/encoding/emotional.py +125 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
- package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
- package/src/superlocalmemory/encoding/foresight.py +91 -0
- package/src/superlocalmemory/encoding/graph_builder.py +302 -0
- package/src/superlocalmemory/encoding/observation_builder.py +160 -0
- package/src/superlocalmemory/encoding/scene_builder.py +183 -0
- package/src/superlocalmemory/encoding/signal_inference.py +90 -0
- package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
- package/src/superlocalmemory/encoding/type_router.py +235 -0
- package/src/superlocalmemory/hooks/__init__.py +3 -0
- package/src/superlocalmemory/hooks/auto_capture.py +111 -0
- package/src/superlocalmemory/hooks/auto_recall.py +93 -0
- package/src/superlocalmemory/hooks/ide_connector.py +204 -0
- package/src/superlocalmemory/hooks/rules_engine.py +99 -0
- package/src/superlocalmemory/infra/__init__.py +3 -0
- package/src/superlocalmemory/infra/auth_middleware.py +82 -0
- package/src/superlocalmemory/infra/backup.py +317 -0
- package/src/superlocalmemory/infra/cache_manager.py +267 -0
- package/src/superlocalmemory/infra/event_bus.py +381 -0
- package/src/superlocalmemory/infra/rate_limiter.py +135 -0
- package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +172 -0
- package/src/superlocalmemory/learning/behavioral.py +490 -0
- package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
- package/src/superlocalmemory/learning/bootstrap.py +298 -0
- package/src/superlocalmemory/learning/cross_project.py +399 -0
- package/src/superlocalmemory/learning/database.py +376 -0
- package/src/superlocalmemory/learning/engagement.py +323 -0
- package/src/superlocalmemory/learning/features.py +138 -0
- package/src/superlocalmemory/learning/feedback.py +316 -0
- package/src/superlocalmemory/learning/outcomes.py +255 -0
- package/src/superlocalmemory/learning/project_context.py +366 -0
- package/src/superlocalmemory/learning/ranker.py +155 -0
- package/src/superlocalmemory/learning/source_quality.py +303 -0
- package/src/superlocalmemory/learning/workflows.py +309 -0
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +316 -0
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/fisher.py +356 -0
- package/src/superlocalmemory/math/langevin.py +398 -0
- package/src/superlocalmemory/math/sheaf.py +257 -0
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/resources.py +245 -0
- package/src/superlocalmemory/mcp/server.py +61 -0
- package/src/superlocalmemory/mcp/tools.py +18 -0
- package/src/superlocalmemory/mcp/tools_core.py +305 -0
- package/src/superlocalmemory/mcp/tools_v28.py +223 -0
- package/src/superlocalmemory/mcp/tools_v3.py +286 -0
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +295 -0
- package/src/superlocalmemory/retrieval/ann_index.py +223 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
- package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
- package/src/superlocalmemory/retrieval/engine.py +390 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
- package/src/superlocalmemory/retrieval/fusion.py +78 -0
- package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
- package/src/superlocalmemory/retrieval/reranker.py +154 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
- package/src/superlocalmemory/retrieval/strategy.py +96 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
- package/src/superlocalmemory/server/__init__.py +1 -0
- package/src/superlocalmemory/server/api.py +248 -0
- package/src/superlocalmemory/server/routes/__init__.py +4 -0
- package/src/superlocalmemory/server/routes/agents.py +107 -0
- package/src/superlocalmemory/server/routes/backup.py +91 -0
- package/src/superlocalmemory/server/routes/behavioral.py +127 -0
- package/src/superlocalmemory/server/routes/compliance.py +160 -0
- package/src/superlocalmemory/server/routes/data_io.py +188 -0
- package/src/superlocalmemory/server/routes/events.py +183 -0
- package/src/superlocalmemory/server/routes/helpers.py +85 -0
- package/src/superlocalmemory/server/routes/learning.py +273 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
- package/src/superlocalmemory/server/routes/memories.py +399 -0
- package/src/superlocalmemory/server/routes/profiles.py +219 -0
- package/src/superlocalmemory/server/routes/stats.py +346 -0
- package/src/superlocalmemory/server/routes/v3_api.py +365 -0
- package/src/superlocalmemory/server/routes/ws.py +82 -0
- package/src/superlocalmemory/server/security_middleware.py +57 -0
- package/src/superlocalmemory/server/ui.py +245 -0
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/access_control.py +182 -0
- package/src/superlocalmemory/storage/database.py +594 -0
- package/src/superlocalmemory/storage/migrations.py +303 -0
- package/src/superlocalmemory/storage/models.py +406 -0
- package/src/superlocalmemory/storage/schema.py +726 -0
- package/src/superlocalmemory/storage/v2_migrator.py +317 -0
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +130 -0
- package/src/superlocalmemory/trust/provenance.py +124 -0
- package/src/superlocalmemory/trust/scorer.py +347 -0
- package/src/superlocalmemory/trust/signals.py +153 -0
- package/ui/index.html +278 -5
- package/ui/js/auto-settings.js +70 -0
- package/ui/js/dashboard.js +90 -0
- package/ui/js/fact-detail.js +92 -0
- package/ui/js/feedback.js +2 -2
- package/ui/js/ide-status.js +102 -0
- package/ui/js/math-health.js +98 -0
- package/ui/js/recall-lab.js +127 -0
- package/ui/js/settings.js +2 -2
- package/ui/js/trust-dashboard.js +73 -0
- package/api_server.py +0 -724
- package/bin/aider-smart +0 -72
- package/bin/superlocalmemoryv2-learning +0 -4
- package/bin/superlocalmemoryv2-list +0 -3
- package/bin/superlocalmemoryv2-patterns +0 -4
- package/bin/superlocalmemoryv2-profile +0 -3
- package/bin/superlocalmemoryv2-recall +0 -3
- package/bin/superlocalmemoryv2-remember +0 -3
- package/bin/superlocalmemoryv2-reset +0 -3
- package/bin/superlocalmemoryv2-status +0 -3
- package/configs/chatgpt-desktop-mcp.json +0 -16
- package/configs/cursor-mcp.json +0 -15
- package/hooks/memory-list-skill.js +0 -139
- package/hooks/memory-profile-skill.js +0 -273
- package/hooks/memory-recall-skill.js +0 -114
- package/hooks/memory-remember-skill.js +0 -127
- package/hooks/memory-reset-skill.js +0 -274
- package/mcp_server.py +0 -1808
- package/requirements-core.txt +0 -22
- package/requirements-learning.txt +0 -12
- package/requirements.txt +0 -12
- package/src/agent_registry.py +0 -411
- package/src/auth_middleware.py +0 -61
- package/src/auto_backup.py +0 -459
- package/src/behavioral/__init__.py +0 -49
- package/src/behavioral/behavioral_listener.py +0 -203
- package/src/behavioral/behavioral_patterns.py +0 -275
- package/src/behavioral/cross_project_transfer.py +0 -206
- package/src/behavioral/outcome_inference.py +0 -194
- package/src/behavioral/outcome_tracker.py +0 -193
- package/src/behavioral/tests/__init__.py +0 -4
- package/src/behavioral/tests/test_behavioral_integration.py +0 -108
- package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
- package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
- package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
- package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
- package/src/behavioral/tests/test_outcome_inference.py +0 -107
- package/src/behavioral/tests/test_outcome_tracker.py +0 -96
- package/src/cache_manager.py +0 -518
- package/src/compliance/__init__.py +0 -48
- package/src/compliance/abac_engine.py +0 -149
- package/src/compliance/abac_middleware.py +0 -116
- package/src/compliance/audit_db.py +0 -215
- package/src/compliance/audit_logger.py +0 -148
- package/src/compliance/retention_manager.py +0 -289
- package/src/compliance/retention_scheduler.py +0 -186
- package/src/compliance/tests/__init__.py +0 -4
- package/src/compliance/tests/test_abac_enforcement.py +0 -95
- package/src/compliance/tests/test_abac_engine.py +0 -124
- package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
- package/src/compliance/tests/test_audit_db.py +0 -123
- package/src/compliance/tests/test_audit_logger.py +0 -98
- package/src/compliance/tests/test_mcp_audit.py +0 -128
- package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
- package/src/compliance/tests/test_retention_manager.py +0 -131
- package/src/compliance/tests/test_retention_scheduler.py +0 -99
- package/src/compression/__init__.py +0 -25
- package/src/compression/cli.py +0 -150
- package/src/compression/cold_storage.py +0 -217
- package/src/compression/config.py +0 -72
- package/src/compression/orchestrator.py +0 -133
- package/src/compression/tier2_compressor.py +0 -228
- package/src/compression/tier3_compressor.py +0 -153
- package/src/compression/tier_classifier.py +0 -148
- package/src/db_connection_manager.py +0 -536
- package/src/embedding_engine.py +0 -63
- package/src/embeddings/__init__.py +0 -47
- package/src/embeddings/cache.py +0 -70
- package/src/embeddings/cli.py +0 -113
- package/src/embeddings/constants.py +0 -47
- package/src/embeddings/database.py +0 -91
- package/src/embeddings/engine.py +0 -247
- package/src/embeddings/model_loader.py +0 -145
- package/src/event_bus.py +0 -562
- package/src/graph/__init__.py +0 -36
- package/src/graph/build_helpers.py +0 -74
- package/src/graph/cli.py +0 -87
- package/src/graph/cluster_builder.py +0 -188
- package/src/graph/cluster_summary.py +0 -148
- package/src/graph/constants.py +0 -47
- package/src/graph/edge_builder.py +0 -162
- package/src/graph/entity_extractor.py +0 -95
- package/src/graph/graph_core.py +0 -226
- package/src/graph/graph_search.py +0 -231
- package/src/graph/hierarchical.py +0 -207
- package/src/graph/schema.py +0 -99
- package/src/graph_engine.py +0 -52
- package/src/hnsw_index.py +0 -628
- package/src/hybrid_search.py +0 -46
- package/src/learning/__init__.py +0 -217
- package/src/learning/adaptive_ranker.py +0 -682
- package/src/learning/bootstrap/__init__.py +0 -69
- package/src/learning/bootstrap/constants.py +0 -93
- package/src/learning/bootstrap/db_queries.py +0 -316
- package/src/learning/bootstrap/sampling.py +0 -82
- package/src/learning/bootstrap/text_utils.py +0 -71
- package/src/learning/cross_project_aggregator.py +0 -857
- package/src/learning/db/__init__.py +0 -40
- package/src/learning/db/constants.py +0 -44
- package/src/learning/db/schema.py +0 -279
- package/src/learning/engagement_tracker.py +0 -628
- package/src/learning/feature_extractor.py +0 -708
- package/src/learning/feedback_collector.py +0 -806
- package/src/learning/learning_db.py +0 -915
- package/src/learning/project_context_manager.py +0 -572
- package/src/learning/ranking/__init__.py +0 -33
- package/src/learning/ranking/constants.py +0 -84
- package/src/learning/ranking/helpers.py +0 -278
- package/src/learning/source_quality_scorer.py +0 -676
- package/src/learning/synthetic_bootstrap.py +0 -755
- package/src/learning/tests/test_adaptive_ranker.py +0 -325
- package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
- package/src/learning/tests/test_aggregator.py +0 -306
- package/src/learning/tests/test_auto_retrain_v28.py +0 -35
- package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
- package/src/learning/tests/test_feature_extractor_v28.py +0 -93
- package/src/learning/tests/test_feedback_collector.py +0 -294
- package/src/learning/tests/test_learning_db.py +0 -602
- package/src/learning/tests/test_learning_db_v28.py +0 -110
- package/src/learning/tests/test_learning_init_v28.py +0 -48
- package/src/learning/tests/test_outcome_signals.py +0 -48
- package/src/learning/tests/test_project_context.py +0 -292
- package/src/learning/tests/test_schema_migration.py +0 -319
- package/src/learning/tests/test_signal_inference.py +0 -397
- package/src/learning/tests/test_source_quality.py +0 -351
- package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
- package/src/learning/tests/test_workflow_miner.py +0 -318
- package/src/learning/workflow_pattern_miner.py +0 -655
- package/src/lifecycle/__init__.py +0 -54
- package/src/lifecycle/bounded_growth.py +0 -239
- package/src/lifecycle/compaction_engine.py +0 -226
- package/src/lifecycle/lifecycle_engine.py +0 -355
- package/src/lifecycle/lifecycle_evaluator.py +0 -257
- package/src/lifecycle/lifecycle_scheduler.py +0 -130
- package/src/lifecycle/retention_policy.py +0 -285
- package/src/lifecycle/tests/test_bounded_growth.py +0 -193
- package/src/lifecycle/tests/test_compaction.py +0 -179
- package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
- package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
- package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
- package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
- package/src/lifecycle/tests/test_mcp_compact.py +0 -149
- package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
- package/src/lifecycle/tests/test_retention_policy.py +0 -162
- package/src/mcp_tools_v28.py +0 -281
- package/src/memory/__init__.py +0 -36
- package/src/memory/cli.py +0 -205
- package/src/memory/constants.py +0 -39
- package/src/memory/helpers.py +0 -28
- package/src/memory/schema.py +0 -166
- package/src/memory-profiles.py +0 -595
- package/src/memory-reset.py +0 -491
- package/src/memory_compression.py +0 -989
- package/src/memory_store_v2.py +0 -1155
- package/src/migrate_v1_to_v2.py +0 -629
- package/src/pattern_learner.py +0 -34
- package/src/patterns/__init__.py +0 -24
- package/src/patterns/analyzers.py +0 -251
- package/src/patterns/learner.py +0 -271
- package/src/patterns/scoring.py +0 -171
- package/src/patterns/store.py +0 -225
- package/src/patterns/terminology.py +0 -140
- package/src/provenance_tracker.py +0 -312
- package/src/qualixar_attribution.py +0 -139
- package/src/qualixar_watermark.py +0 -78
- package/src/query_optimizer.py +0 -511
- package/src/rate_limiter.py +0 -83
- package/src/search/__init__.py +0 -20
- package/src/search/cli.py +0 -77
- package/src/search/constants.py +0 -26
- package/src/search/engine.py +0 -241
- package/src/search/fusion.py +0 -122
- package/src/search/index_loader.py +0 -114
- package/src/search/methods.py +0 -162
- package/src/search_engine_v2.py +0 -401
- package/src/setup_validator.py +0 -482
- package/src/subscription_manager.py +0 -391
- package/src/tree/__init__.py +0 -59
- package/src/tree/builder.py +0 -185
- package/src/tree/nodes.py +0 -202
- package/src/tree/queries.py +0 -257
- package/src/tree/schema.py +0 -80
- package/src/tree_manager.py +0 -19
- package/src/trust/__init__.py +0 -45
- package/src/trust/constants.py +0 -66
- package/src/trust/queries.py +0 -157
- package/src/trust/schema.py +0 -95
- package/src/trust/scorer.py +0 -299
- package/src/trust/signals.py +0 -95
- package/src/trust_scorer.py +0 -44
- package/ui/app.js +0 -1588
- package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
- package/ui/js/graph-cytoscape.js +0 -1168
- package/ui/js/graph-d3-backup.js +0 -32
- package/ui/js/graph.js +0 -32
- package/ui_server.py +0 -286
- /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
- /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
- /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
- /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
- /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
- /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
- /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
- /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
- /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
- /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
- /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
- /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
- /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
- /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
- /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
- /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
- /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
- /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
- /package/{completions → ide/completions}/slm.bash +0 -0
- /package/{completions → ide/completions}/slm.zsh +0 -0
- /package/{configs → ide/configs}/cody-commands.json +0 -0
- /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
- /package/{install.ps1 → scripts/install.ps1} +0 -0
- /package/{install.sh → scripts/install.sh} +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
"""Sliding-window rate limiter for per-agent request throttling.
|
|
5
|
+
|
|
6
|
+
Pure stdlib -- no external dependencies. Thread-safe.
|
|
7
|
+
|
|
8
|
+
Defaults (configurable via env vars):
|
|
9
|
+
SLM_RATE_LIMIT_WRITE = 100 req / window
|
|
10
|
+
SLM_RATE_LIMIT_READ = 300 req / window
|
|
11
|
+
SLM_RATE_LIMIT_WINDOW = 60 seconds
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
from typing import Dict, List, Tuple
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("superlocalmemory.ratelimit")
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Module-level defaults (overridable via environment)
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
WRITE_LIMIT = int(os.environ.get("SLM_RATE_LIMIT_WRITE", "100"))
|
|
27
|
+
READ_LIMIT = int(os.environ.get("SLM_RATE_LIMIT_READ", "300"))
|
|
28
|
+
WINDOW_SECONDS = int(os.environ.get("SLM_RATE_LIMIT_WINDOW", "60"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RateLimiter:
|
|
32
|
+
"""Thread-safe sliding-window rate limiter.
|
|
33
|
+
|
|
34
|
+
Each *client_id* (agent name, IP, etc.) gets its own independent
|
|
35
|
+
request window. Expired timestamps are pruned lazily on every call
|
|
36
|
+
to ``allow()`` or ``is_allowed()``.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
max_requests: Maximum requests allowed per window.
|
|
40
|
+
window_seconds: Length of the sliding window in seconds.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
max_requests: int = 100,
|
|
46
|
+
window_seconds: int = 60,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.max_requests = max_requests
|
|
49
|
+
self.window = window_seconds
|
|
50
|
+
self._requests: Dict[str, List[float]] = defaultdict(list)
|
|
51
|
+
self._lock = threading.Lock()
|
|
52
|
+
|
|
53
|
+
# ----- public API -----
|
|
54
|
+
|
|
55
|
+
def allow(self, client_id: str) -> bool:
|
|
56
|
+
"""Check **and record** a request for *client_id*.
|
|
57
|
+
|
|
58
|
+
Returns ``True`` when the request is allowed, ``False`` when the
|
|
59
|
+
client has exceeded its limit for the current window.
|
|
60
|
+
"""
|
|
61
|
+
allowed, _ = self.is_allowed(client_id)
|
|
62
|
+
return allowed
|
|
63
|
+
|
|
64
|
+
def is_allowed(self, client_id: str) -> Tuple[bool, int]:
|
|
65
|
+
"""Check and record a request.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
``(allowed, remaining)`` -- whether the request is permitted
|
|
69
|
+
and how many requests remain in the current window.
|
|
70
|
+
"""
|
|
71
|
+
now = time.time()
|
|
72
|
+
cutoff = now - self.window
|
|
73
|
+
|
|
74
|
+
with self._lock:
|
|
75
|
+
# Prune expired timestamps
|
|
76
|
+
self._requests[client_id] = [
|
|
77
|
+
t for t in self._requests[client_id] if t > cutoff
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
current = len(self._requests[client_id])
|
|
81
|
+
|
|
82
|
+
if current >= self.max_requests:
|
|
83
|
+
return False, 0
|
|
84
|
+
|
|
85
|
+
self._requests[client_id].append(now)
|
|
86
|
+
return True, self.max_requests - current - 1
|
|
87
|
+
|
|
88
|
+
def remaining(self, client_id: str) -> int:
|
|
89
|
+
"""Return how many requests *client_id* has left without recording one."""
|
|
90
|
+
now = time.time()
|
|
91
|
+
cutoff = now - self.window
|
|
92
|
+
|
|
93
|
+
with self._lock:
|
|
94
|
+
active = [t for t in self._requests.get(client_id, []) if t > cutoff]
|
|
95
|
+
return max(0, self.max_requests - len(active))
|
|
96
|
+
|
|
97
|
+
def reset(self, client_id: str) -> None:
|
|
98
|
+
"""Clear all recorded requests for *client_id*."""
|
|
99
|
+
with self._lock:
|
|
100
|
+
self._requests.pop(client_id, None)
|
|
101
|
+
|
|
102
|
+
def cleanup(self) -> int:
|
|
103
|
+
"""Remove stale entries for clients with no recent requests.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Number of client entries removed.
|
|
107
|
+
"""
|
|
108
|
+
now = time.time()
|
|
109
|
+
cutoff = now - self.window * 2 # keep 2 windows of data
|
|
110
|
+
|
|
111
|
+
with self._lock:
|
|
112
|
+
stale = [
|
|
113
|
+
k
|
|
114
|
+
for k, v in self._requests.items()
|
|
115
|
+
if not v or max(v) < cutoff
|
|
116
|
+
]
|
|
117
|
+
for k in stale:
|
|
118
|
+
del self._requests[k]
|
|
119
|
+
return len(stale)
|
|
120
|
+
|
|
121
|
+
def get_stats(self) -> dict:
|
|
122
|
+
"""Return a snapshot of limiter state."""
|
|
123
|
+
with self._lock:
|
|
124
|
+
return {
|
|
125
|
+
"max_requests": self.max_requests,
|
|
126
|
+
"window_seconds": self.window,
|
|
127
|
+
"tracked_clients": len(self._requests),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Module-level convenience singletons
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
write_limiter = RateLimiter(max_requests=WRITE_LIMIT, window_seconds=WINDOW_SECONDS)
|
|
135
|
+
read_limiter = RateLimiter(max_requests=READ_LIMIT, window_seconds=WINDOW_SECONDS)
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
"""
|
|
5
|
-
WebhookDispatcher — Delivers events via HTTP POST to configured webhook URLs.
|
|
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
|
+
"""WebhookDispatcher -- background HTTP POST delivery for memory events.
|
|
6
5
|
|
|
7
|
-
Runs on a
|
|
8
|
-
|
|
6
|
+
Runs on a daemon thread so webhook delivery never blocks the main event
|
|
7
|
+
flow. Failed deliveries are retried with exponential back-off (up to
|
|
8
|
+
``MAX_RETRIES`` attempts).
|
|
9
9
|
|
|
10
10
|
Security:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- No private/internal IP blocking in v2.5 (added in v2.6 with trust enforcement)
|
|
11
|
+
* Only ``http://`` and ``https://`` URLs are accepted.
|
|
12
|
+
* Private / loopback IPs are rejected.
|
|
13
|
+
* 10-second timeout per outgoing request.
|
|
15
14
|
"""
|
|
16
15
|
|
|
17
16
|
import ipaddress
|
|
@@ -21,44 +20,41 @@ import socket
|
|
|
21
20
|
import threading
|
|
22
21
|
import time
|
|
23
22
|
import urllib.parse
|
|
24
|
-
from queue import Queue, Empty
|
|
25
|
-
from typing import Optional, Dict
|
|
26
23
|
from datetime import datetime
|
|
24
|
+
from queue import Empty, Queue
|
|
25
|
+
from typing import Dict, Optional
|
|
27
26
|
|
|
28
27
|
logger = logging.getLogger("superlocalmemory.webhooks")
|
|
29
28
|
|
|
30
|
-
#
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Configuration constants
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
31
32
|
MAX_RETRIES = 3
|
|
32
|
-
RETRY_BACKOFF_BASE = 2
|
|
33
|
-
REQUEST_TIMEOUT = 10
|
|
33
|
+
RETRY_BACKOFF_BASE = 2 # seconds: 2, 4, 8
|
|
34
|
+
REQUEST_TIMEOUT = 10 # seconds
|
|
34
35
|
MAX_QUEUE_SIZE = 1000
|
|
36
|
+
VERSION = "3.0.0"
|
|
35
37
|
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
from urllib.error import URLError, HTTPError
|
|
40
|
-
HTTP_AVAILABLE = True
|
|
41
|
-
except ImportError:
|
|
42
|
-
HTTP_AVAILABLE = False
|
|
38
|
+
# stdlib HTTP -- always available
|
|
39
|
+
from urllib.request import Request, urlopen # noqa: E402
|
|
40
|
+
from urllib.error import HTTPError, URLError # noqa: E402
|
|
43
41
|
|
|
44
42
|
|
|
45
43
|
def _is_private_ip(hostname: str) -> bool:
|
|
46
|
-
"""
|
|
44
|
+
"""Return ``True`` if *hostname* resolves to a private / loopback IP."""
|
|
47
45
|
try:
|
|
48
46
|
ip_str = socket.gethostbyname(hostname)
|
|
49
47
|
ip = ipaddress.ip_address(ip_str)
|
|
50
48
|
return ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved
|
|
51
49
|
except (socket.gaierror, ValueError):
|
|
52
|
-
return False
|
|
50
|
+
return False
|
|
53
51
|
|
|
54
52
|
|
|
55
53
|
class WebhookDispatcher:
|
|
56
|
-
"""
|
|
57
|
-
Background webhook delivery with retry logic.
|
|
54
|
+
"""Background webhook delivery with retry logic.
|
|
58
55
|
|
|
59
|
-
Thread-safe.
|
|
60
|
-
|
|
61
|
-
exponential backoff.
|
|
56
|
+
Thread-safe. Enqueues deliveries and processes them on a dedicated
|
|
57
|
+
daemon thread.
|
|
62
58
|
"""
|
|
63
59
|
|
|
64
60
|
_instances: Dict[str, "WebhookDispatcher"] = {}
|
|
@@ -66,7 +62,7 @@ class WebhookDispatcher:
|
|
|
66
62
|
|
|
67
63
|
@classmethod
|
|
68
64
|
def get_instance(cls, name: str = "default") -> "WebhookDispatcher":
|
|
69
|
-
"""Get or create a singleton
|
|
65
|
+
"""Get or create a named singleton."""
|
|
70
66
|
with cls._instances_lock:
|
|
71
67
|
if name not in cls._instances:
|
|
72
68
|
cls._instances[name] = cls()
|
|
@@ -74,7 +70,7 @@ class WebhookDispatcher:
|
|
|
74
70
|
|
|
75
71
|
@classmethod
|
|
76
72
|
def reset_instance(cls, name: Optional[str] = None) -> None:
|
|
77
|
-
"""Remove singleton(s).
|
|
73
|
+
"""Remove singleton(s). Primarily for testing."""
|
|
78
74
|
with cls._instances_lock:
|
|
79
75
|
if name is None:
|
|
80
76
|
for inst in cls._instances.values():
|
|
@@ -84,7 +80,7 @@ class WebhookDispatcher:
|
|
|
84
80
|
cls._instances[name].close()
|
|
85
81
|
del cls._instances[name]
|
|
86
82
|
|
|
87
|
-
def __init__(self):
|
|
83
|
+
def __init__(self) -> None:
|
|
88
84
|
self._queue: Queue = Queue(maxsize=MAX_QUEUE_SIZE)
|
|
89
85
|
self._closed = False
|
|
90
86
|
self._stats = {
|
|
@@ -95,7 +91,6 @@ class WebhookDispatcher:
|
|
|
95
91
|
}
|
|
96
92
|
self._stats_lock = threading.Lock()
|
|
97
93
|
|
|
98
|
-
# Background worker thread
|
|
99
94
|
self._worker = threading.Thread(
|
|
100
95
|
target=self._worker_loop,
|
|
101
96
|
name="slm-webhook-worker",
|
|
@@ -104,82 +99,106 @@ class WebhookDispatcher:
|
|
|
104
99
|
self._worker.start()
|
|
105
100
|
logger.info("WebhookDispatcher started")
|
|
106
101
|
|
|
107
|
-
|
|
108
|
-
"""
|
|
109
|
-
Enqueue a webhook delivery.
|
|
102
|
+
# ----- public API -----
|
|
110
103
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
webhook_url: URL to POST to
|
|
104
|
+
def dispatch(self, event: dict, webhook_url: str) -> None:
|
|
105
|
+
"""Enqueue a webhook delivery.
|
|
114
106
|
|
|
115
107
|
Raises:
|
|
116
|
-
ValueError: If webhook_url is invalid
|
|
117
|
-
RuntimeError: If dispatcher is closed
|
|
108
|
+
ValueError: If *webhook_url* is invalid or private.
|
|
109
|
+
RuntimeError: If the dispatcher is closed.
|
|
118
110
|
"""
|
|
119
111
|
if self._closed:
|
|
120
112
|
raise RuntimeError("WebhookDispatcher is closed")
|
|
121
113
|
|
|
122
|
-
if not webhook_url or not (
|
|
114
|
+
if not webhook_url or not (
|
|
115
|
+
webhook_url.startswith("http://") or webhook_url.startswith("https://")
|
|
116
|
+
):
|
|
123
117
|
raise ValueError(f"Invalid webhook URL: {webhook_url}")
|
|
124
118
|
|
|
125
119
|
parsed = urllib.parse.urlparse(webhook_url)
|
|
126
120
|
if parsed.hostname and _is_private_ip(parsed.hostname):
|
|
127
|
-
raise ValueError(
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"Webhook URL points to private/internal network: {webhook_url}"
|
|
123
|
+
)
|
|
128
124
|
|
|
129
125
|
try:
|
|
130
|
-
self._queue.put_nowait(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
126
|
+
self._queue.put_nowait(
|
|
127
|
+
{
|
|
128
|
+
"event": event,
|
|
129
|
+
"url": webhook_url,
|
|
130
|
+
"attempt": 0,
|
|
131
|
+
"enqueued_at": datetime.now().isoformat(),
|
|
132
|
+
}
|
|
133
|
+
)
|
|
136
134
|
with self._stats_lock:
|
|
137
135
|
self._stats["dispatched"] += 1
|
|
138
136
|
except Exception:
|
|
139
137
|
logger.warning("Webhook queue full, dropping event for %s", webhook_url)
|
|
140
138
|
|
|
141
|
-
def
|
|
142
|
-
"""
|
|
139
|
+
def get_stats(self) -> dict:
|
|
140
|
+
"""Return delivery statistics snapshot."""
|
|
141
|
+
with self._stats_lock:
|
|
142
|
+
return dict(self._stats)
|
|
143
|
+
|
|
144
|
+
def close(self) -> None:
|
|
145
|
+
"""Shut down the dispatcher, draining remaining items."""
|
|
146
|
+
if self._closed:
|
|
147
|
+
return
|
|
148
|
+
self._closed = True
|
|
149
|
+
self._queue.put(None) # sentinel
|
|
150
|
+
if self._worker.is_alive():
|
|
151
|
+
self._worker.join(timeout=5)
|
|
152
|
+
logger.info("WebhookDispatcher closed: stats=%s", self._stats)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def is_closed(self) -> bool:
|
|
156
|
+
return self._closed
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def queue_size(self) -> int:
|
|
160
|
+
return self._queue.qsize()
|
|
161
|
+
|
|
162
|
+
# ----- internal -----
|
|
163
|
+
|
|
164
|
+
def _worker_loop(self) -> None:
|
|
165
|
+
"""Background loop: dequeue and deliver."""
|
|
143
166
|
while not self._closed:
|
|
144
167
|
try:
|
|
145
168
|
item = self._queue.get(timeout=1.0)
|
|
146
169
|
except Empty:
|
|
147
170
|
continue
|
|
148
171
|
|
|
149
|
-
if item is None: #
|
|
172
|
+
if item is None: # shutdown sentinel
|
|
150
173
|
self._queue.task_done()
|
|
151
174
|
break
|
|
152
175
|
|
|
153
176
|
self._deliver(item)
|
|
154
177
|
self._queue.task_done()
|
|
155
178
|
|
|
156
|
-
def _deliver(self, item: dict):
|
|
157
|
-
"""Attempt
|
|
179
|
+
def _deliver(self, item: dict) -> None:
|
|
180
|
+
"""Attempt delivery with exponential-backoff retry."""
|
|
158
181
|
event = item["event"]
|
|
159
182
|
url = item["url"]
|
|
160
183
|
attempt = item["attempt"]
|
|
161
184
|
|
|
162
|
-
if not HTTP_AVAILABLE:
|
|
163
|
-
logger.error("HTTP library not available, cannot deliver webhook to %s", url)
|
|
164
|
-
with self._stats_lock:
|
|
165
|
-
self._stats["failed"] += 1
|
|
166
|
-
return
|
|
167
|
-
|
|
168
185
|
try:
|
|
169
|
-
payload = json.dumps(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
186
|
+
payload = json.dumps(
|
|
187
|
+
{
|
|
188
|
+
"event": event,
|
|
189
|
+
"delivered_at": datetime.now().isoformat(),
|
|
190
|
+
"attempt": attempt + 1,
|
|
191
|
+
"source": "superlocalmemory",
|
|
192
|
+
"version": VERSION,
|
|
193
|
+
}
|
|
194
|
+
).encode("utf-8")
|
|
176
195
|
|
|
177
196
|
req = Request(
|
|
178
197
|
url,
|
|
179
198
|
data=payload,
|
|
180
199
|
headers={
|
|
181
200
|
"Content-Type": "application/json",
|
|
182
|
-
"User-Agent": "SuperLocalMemory/
|
|
201
|
+
"User-Agent": f"SuperLocalMemory/{VERSION}",
|
|
183
202
|
"X-SLM-Event-Type": event.get("event_type", "unknown"),
|
|
184
203
|
},
|
|
185
204
|
method="POST",
|
|
@@ -190,47 +209,31 @@ class WebhookDispatcher:
|
|
|
190
209
|
if 200 <= status < 300:
|
|
191
210
|
with self._stats_lock:
|
|
192
211
|
self._stats["succeeded"] += 1
|
|
193
|
-
logger.debug("Webhook delivered: url=%s
|
|
212
|
+
logger.debug("Webhook delivered: url=%s status=%d", url, status)
|
|
194
213
|
return
|
|
195
|
-
|
|
196
|
-
raise HTTPError(url, status, f"HTTP {status}", {}, None)
|
|
214
|
+
raise HTTPError(url, status, f"HTTP {status}", {}, None)
|
|
197
215
|
|
|
198
|
-
except Exception as
|
|
199
|
-
logger.warning(
|
|
200
|
-
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
logger.warning(
|
|
218
|
+
"Webhook delivery failed (attempt %d/%d): url=%s error=%s",
|
|
219
|
+
attempt + 1,
|
|
220
|
+
MAX_RETRIES,
|
|
221
|
+
url,
|
|
222
|
+
exc,
|
|
223
|
+
)
|
|
201
224
|
|
|
202
225
|
if attempt + 1 < MAX_RETRIES:
|
|
203
|
-
# Retry with exponential backoff
|
|
204
226
|
backoff = RETRY_BACKOFF_BASE ** (attempt + 1)
|
|
205
227
|
time.sleep(backoff)
|
|
206
228
|
with self._stats_lock:
|
|
207
229
|
self._stats["retries"] += 1
|
|
208
230
|
item["attempt"] = attempt + 1
|
|
209
|
-
self._deliver(item)
|
|
231
|
+
self._deliver(item)
|
|
210
232
|
else:
|
|
211
233
|
with self._stats_lock:
|
|
212
234
|
self._stats["failed"] += 1
|
|
213
|
-
logger.error(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return dict(self._stats)
|
|
219
|
-
|
|
220
|
-
def close(self) -> None:
|
|
221
|
-
"""Shut down the dispatcher. Drains remaining items."""
|
|
222
|
-
if self._closed:
|
|
223
|
-
return
|
|
224
|
-
self._closed = True
|
|
225
|
-
self._queue.put(None) # Shutdown sentinel
|
|
226
|
-
if self._worker.is_alive():
|
|
227
|
-
self._worker.join(timeout=5)
|
|
228
|
-
logger.info("WebhookDispatcher closed: stats=%s", self._stats)
|
|
229
|
-
|
|
230
|
-
@property
|
|
231
|
-
def is_closed(self) -> bool:
|
|
232
|
-
return self._closed
|
|
233
|
-
|
|
234
|
-
@property
|
|
235
|
-
def queue_size(self) -> int:
|
|
236
|
-
return self._queue.qsize()
|
|
235
|
+
logger.error(
|
|
236
|
+
"Webhook permanently failed after %d attempts: url=%s",
|
|
237
|
+
MAX_RETRIES,
|
|
238
|
+
url,
|
|
239
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,172 @@
|
|
|
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 — Adaptive Learning (3-Phase).
|
|
6
|
+
|
|
7
|
+
Learns optimal retrieval weights from user feedback.
|
|
8
|
+
Ported from V2.8 LightGBM-based learning system.
|
|
9
|
+
Profile-scoped: each profile learns independently.
|
|
10
|
+
|
|
11
|
+
Phase 1: Collect feedback (ranking_feedback)
|
|
12
|
+
Phase 2: Train model on feedback patterns
|
|
13
|
+
Phase 3: Apply learned weights to retrieval
|
|
14
|
+
|
|
15
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
from collections import defaultdict
|
|
23
|
+
from datetime import UTC, datetime
|
|
24
|
+
|
|
25
|
+
from superlocalmemory.storage.models import FeedbackRecord
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# Minimum feedback records before training
|
|
30
|
+
_MIN_FEEDBACK_FOR_TRAINING = 20
|
|
31
|
+
|
|
32
|
+
# Default channel weights (before learning)
|
|
33
|
+
_DEFAULT_WEIGHTS = {
|
|
34
|
+
"semantic": 1.2,
|
|
35
|
+
"bm25": 1.0,
|
|
36
|
+
"entity_graph": 1.0,
|
|
37
|
+
"temporal": 0.8,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AdaptiveLearner:
|
|
42
|
+
"""3-phase adaptive learning for retrieval weight optimization.
|
|
43
|
+
|
|
44
|
+
Learns from user feedback which channels produce the best results
|
|
45
|
+
for different query types. Profile-scoped.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, db) -> None:
|
|
49
|
+
self._db = db
|
|
50
|
+
self._learned_weights: dict[str, dict[str, float]] = {}
|
|
51
|
+
|
|
52
|
+
# -- Phase 1: Collect feedback -----------------------------------------
|
|
53
|
+
|
|
54
|
+
def record_feedback(
|
|
55
|
+
self,
|
|
56
|
+
query: str,
|
|
57
|
+
fact_id: str,
|
|
58
|
+
feedback_type: str,
|
|
59
|
+
profile_id: str,
|
|
60
|
+
dwell_time_ms: int = 0,
|
|
61
|
+
) -> FeedbackRecord:
|
|
62
|
+
"""Record user feedback on a retrieval result.
|
|
63
|
+
|
|
64
|
+
feedback_type: "relevant", "irrelevant", "partial"
|
|
65
|
+
"""
|
|
66
|
+
record = FeedbackRecord(
|
|
67
|
+
profile_id=profile_id,
|
|
68
|
+
query=query,
|
|
69
|
+
fact_id=fact_id,
|
|
70
|
+
feedback_type=feedback_type,
|
|
71
|
+
dwell_time_ms=dwell_time_ms,
|
|
72
|
+
timestamp=datetime.now(UTC).isoformat(),
|
|
73
|
+
)
|
|
74
|
+
self._db.execute(
|
|
75
|
+
"INSERT INTO feedback_records "
|
|
76
|
+
"(feedback_id, profile_id, query, fact_id, feedback_type, "
|
|
77
|
+
"dwell_time_ms, timestamp) VALUES (?,?,?,?,?,?,?)",
|
|
78
|
+
(record.feedback_id, record.profile_id, record.query,
|
|
79
|
+
record.fact_id, record.feedback_type, record.dwell_time_ms,
|
|
80
|
+
record.timestamp),
|
|
81
|
+
)
|
|
82
|
+
return record
|
|
83
|
+
|
|
84
|
+
def get_feedback_count(self, profile_id: str) -> int:
|
|
85
|
+
"""Count feedback records for a profile."""
|
|
86
|
+
rows = self._db.execute(
|
|
87
|
+
"SELECT COUNT(*) AS c FROM feedback_records WHERE profile_id = ?",
|
|
88
|
+
(profile_id,),
|
|
89
|
+
)
|
|
90
|
+
return int(dict(rows[0])["c"]) if rows else 0
|
|
91
|
+
|
|
92
|
+
# -- Phase 2: Learn patterns -------------------------------------------
|
|
93
|
+
|
|
94
|
+
def train(self, profile_id: str) -> dict[str, dict[str, float]]:
|
|
95
|
+
"""Learn optimal weights from feedback patterns.
|
|
96
|
+
|
|
97
|
+
Simple heuristic approach (LightGBM port deferred to production):
|
|
98
|
+
- Analyze which channels produced "relevant" results
|
|
99
|
+
- Boost channels that correlate with positive feedback
|
|
100
|
+
- Reduce channels that correlate with negative feedback
|
|
101
|
+
"""
|
|
102
|
+
count = self.get_feedback_count(profile_id)
|
|
103
|
+
if count < _MIN_FEEDBACK_FOR_TRAINING:
|
|
104
|
+
logger.info(
|
|
105
|
+
"Only %d feedback records (need %d). Using defaults.",
|
|
106
|
+
count, _MIN_FEEDBACK_FOR_TRAINING,
|
|
107
|
+
)
|
|
108
|
+
return {}
|
|
109
|
+
|
|
110
|
+
rows = self._db.execute(
|
|
111
|
+
"SELECT query, fact_id, feedback_type FROM feedback_records "
|
|
112
|
+
"WHERE profile_id = ? ORDER BY timestamp DESC LIMIT 500",
|
|
113
|
+
(profile_id,),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Count positive/negative per query pattern
|
|
117
|
+
positive_count = 0
|
|
118
|
+
negative_count = 0
|
|
119
|
+
for row in rows:
|
|
120
|
+
d = dict(row)
|
|
121
|
+
if d["feedback_type"] == "relevant":
|
|
122
|
+
positive_count += 1
|
|
123
|
+
elif d["feedback_type"] == "irrelevant":
|
|
124
|
+
negative_count += 1
|
|
125
|
+
|
|
126
|
+
# Simple relevance ratio → weight adjustment
|
|
127
|
+
if positive_count + negative_count == 0:
|
|
128
|
+
return {}
|
|
129
|
+
|
|
130
|
+
relevance_ratio = positive_count / (positive_count + negative_count)
|
|
131
|
+
|
|
132
|
+
# If retrieval is generally good (>70% relevant), trust current weights
|
|
133
|
+
# If poor (<50%), boost BM25 and entity (more precise channels)
|
|
134
|
+
if relevance_ratio < 0.5:
|
|
135
|
+
learned = {
|
|
136
|
+
"general": {
|
|
137
|
+
"semantic": 1.0,
|
|
138
|
+
"bm25": 1.5, # Boost precision
|
|
139
|
+
"entity_graph": 1.3, # Boost entity matching
|
|
140
|
+
"temporal": 0.8,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
else:
|
|
144
|
+
learned = {
|
|
145
|
+
"general": dict(_DEFAULT_WEIGHTS),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
self._learned_weights = learned
|
|
149
|
+
logger.info("Learned weights (ratio=%.2f): %s", relevance_ratio, learned)
|
|
150
|
+
return learned
|
|
151
|
+
|
|
152
|
+
# -- Phase 3: Apply weights --------------------------------------------
|
|
153
|
+
|
|
154
|
+
def get_weights(
|
|
155
|
+
self, query_type: str, profile_id: str
|
|
156
|
+
) -> dict[str, float]:
|
|
157
|
+
"""Get learned weights for a query type.
|
|
158
|
+
|
|
159
|
+
Falls back to defaults if no learned weights available.
|
|
160
|
+
"""
|
|
161
|
+
if not self._learned_weights:
|
|
162
|
+
self.train(profile_id)
|
|
163
|
+
|
|
164
|
+
if query_type in self._learned_weights:
|
|
165
|
+
return self._learned_weights[query_type]
|
|
166
|
+
if "general" in self._learned_weights:
|
|
167
|
+
return self._learned_weights["general"]
|
|
168
|
+
return dict(_DEFAULT_WEIGHTS)
|
|
169
|
+
|
|
170
|
+
def is_trained(self, profile_id: str) -> bool:
|
|
171
|
+
"""Check if the learner has enough data to provide learned weights."""
|
|
172
|
+
return self.get_feedback_count(profile_id) >= _MIN_FEEDBACK_FOR_TRAINING
|