superlocalmemory 2.8.6 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -1
- package/NOTICE +63 -0
- package/README.md +165 -480
- package/bin/slm +17 -449
- package/bin/slm-npm +1 -1
- package/conftest.py +5 -0
- package/docs/api-reference.md +284 -0
- package/docs/architecture.md +149 -0
- package/docs/auto-memory.md +150 -0
- package/docs/cli-reference.md +276 -0
- package/docs/compliance.md +191 -0
- package/docs/configuration.md +182 -0
- package/docs/getting-started.md +102 -0
- package/docs/ide-setup.md +261 -0
- package/docs/mcp-tools.md +220 -0
- package/docs/migration-from-v2.md +170 -0
- package/docs/profiles.md +173 -0
- package/docs/troubleshooting.md +310 -0
- package/{configs → ide/configs}/antigravity-mcp.json +3 -3
- package/ide/configs/chatgpt-desktop-mcp.json +16 -0
- package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
- package/{configs → ide/configs}/codex-mcp.toml +4 -4
- package/{configs → ide/configs}/continue-mcp.yaml +4 -3
- package/{configs → ide/configs}/continue-skills.yaml +6 -6
- package/ide/configs/cursor-mcp.json +15 -0
- package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
- package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
- package/{configs → ide/configs}/opencode-mcp.json +2 -2
- package/{configs → ide/configs}/perplexity-mcp.json +2 -2
- package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
- package/{configs → ide/configs}/windsurf-mcp.json +3 -3
- package/{configs → ide/configs}/zed-mcp.json +2 -2
- package/{hooks → ide/hooks}/context-hook.js +9 -20
- package/ide/hooks/memory-list-skill.js +70 -0
- package/ide/hooks/memory-profile-skill.js +101 -0
- package/ide/hooks/memory-recall-skill.js +62 -0
- package/ide/hooks/memory-remember-skill.js +68 -0
- package/ide/hooks/memory-reset-skill.js +160 -0
- package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
- package/ide/integrations/langchain/README.md +106 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
- package/ide/integrations/langchain/pyproject.toml +38 -0
- package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
- package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
- package/ide/integrations/langchain/tests/test_security.py +117 -0
- package/ide/integrations/llamaindex/README.md +81 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
- package/ide/integrations/llamaindex/pyproject.toml +43 -0
- package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
- package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
- package/ide/integrations/llamaindex/tests/test_security.py +241 -0
- package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
- package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
- package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
- package/package.json +13 -22
- package/pyproject.toml +85 -0
- package/scripts/build-dmg.sh +417 -0
- package/scripts/install-skills.ps1 +334 -0
- package/scripts/postinstall.js +2 -2
- package/scripts/start-dashboard.ps1 +52 -0
- package/scripts/start-dashboard.sh +41 -0
- package/scripts/sync-wiki.ps1 +127 -0
- package/scripts/sync-wiki.sh +82 -0
- package/scripts/test-dmg.sh +161 -0
- package/scripts/test-npm-package.ps1 +252 -0
- package/scripts/test-npm-package.sh +207 -0
- package/scripts/verify-install.ps1 +294 -0
- package/scripts/verify-install.sh +266 -0
- package/src/superlocalmemory/__init__.py +0 -0
- package/src/superlocalmemory/attribution/__init__.py +9 -0
- package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
- package/src/superlocalmemory/attribution/signer.py +153 -0
- package/src/superlocalmemory/attribution/watermark.py +189 -0
- package/src/superlocalmemory/cli/__init__.py +5 -0
- package/src/superlocalmemory/cli/commands.py +245 -0
- package/src/superlocalmemory/cli/main.py +89 -0
- package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
- package/src/superlocalmemory/cli/post_install.py +99 -0
- package/src/superlocalmemory/cli/setup_wizard.py +129 -0
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +204 -0
- package/src/superlocalmemory/compliance/audit.py +314 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
- package/src/superlocalmemory/compliance/gdpr.py +294 -0
- package/src/superlocalmemory/compliance/lifecycle.py +158 -0
- package/src/superlocalmemory/compliance/retention.py +232 -0
- package/src/superlocalmemory/compliance/scheduler.py +148 -0
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/config.py +391 -0
- package/src/superlocalmemory/core/embeddings.py +293 -0
- package/src/superlocalmemory/core/engine.py +701 -0
- package/src/superlocalmemory/core/hooks.py +65 -0
- package/src/superlocalmemory/core/maintenance.py +172 -0
- package/src/superlocalmemory/core/modes.py +140 -0
- package/src/superlocalmemory/core/profiles.py +234 -0
- package/src/superlocalmemory/core/registry.py +117 -0
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/consolidator.py +485 -0
- package/src/superlocalmemory/encoding/emotional.py +125 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
- package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
- package/src/superlocalmemory/encoding/foresight.py +91 -0
- package/src/superlocalmemory/encoding/graph_builder.py +302 -0
- package/src/superlocalmemory/encoding/observation_builder.py +160 -0
- package/src/superlocalmemory/encoding/scene_builder.py +183 -0
- package/src/superlocalmemory/encoding/signal_inference.py +90 -0
- package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
- package/src/superlocalmemory/encoding/type_router.py +235 -0
- package/src/superlocalmemory/hooks/__init__.py +3 -0
- package/src/superlocalmemory/hooks/auto_capture.py +111 -0
- package/src/superlocalmemory/hooks/auto_recall.py +93 -0
- package/src/superlocalmemory/hooks/ide_connector.py +204 -0
- package/src/superlocalmemory/hooks/rules_engine.py +99 -0
- package/src/superlocalmemory/infra/__init__.py +3 -0
- package/src/superlocalmemory/infra/auth_middleware.py +82 -0
- package/src/superlocalmemory/infra/backup.py +317 -0
- package/src/superlocalmemory/infra/cache_manager.py +267 -0
- package/src/superlocalmemory/infra/event_bus.py +381 -0
- package/src/superlocalmemory/infra/rate_limiter.py +135 -0
- package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +172 -0
- package/src/superlocalmemory/learning/behavioral.py +490 -0
- package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
- package/src/superlocalmemory/learning/bootstrap.py +298 -0
- package/src/superlocalmemory/learning/cross_project.py +399 -0
- package/src/superlocalmemory/learning/database.py +376 -0
- package/src/superlocalmemory/learning/engagement.py +323 -0
- package/src/superlocalmemory/learning/features.py +138 -0
- package/src/superlocalmemory/learning/feedback.py +316 -0
- package/src/superlocalmemory/learning/outcomes.py +255 -0
- package/src/superlocalmemory/learning/project_context.py +366 -0
- package/src/superlocalmemory/learning/ranker.py +155 -0
- package/src/superlocalmemory/learning/source_quality.py +303 -0
- package/src/superlocalmemory/learning/workflows.py +309 -0
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +316 -0
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/fisher.py +356 -0
- package/src/superlocalmemory/math/langevin.py +398 -0
- package/src/superlocalmemory/math/sheaf.py +257 -0
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/resources.py +245 -0
- package/src/superlocalmemory/mcp/server.py +61 -0
- package/src/superlocalmemory/mcp/tools.py +18 -0
- package/src/superlocalmemory/mcp/tools_core.py +305 -0
- package/src/superlocalmemory/mcp/tools_v28.py +223 -0
- package/src/superlocalmemory/mcp/tools_v3.py +286 -0
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +295 -0
- package/src/superlocalmemory/retrieval/ann_index.py +223 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
- package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
- package/src/superlocalmemory/retrieval/engine.py +390 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
- package/src/superlocalmemory/retrieval/fusion.py +78 -0
- package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
- package/src/superlocalmemory/retrieval/reranker.py +154 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
- package/src/superlocalmemory/retrieval/strategy.py +96 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
- package/src/superlocalmemory/server/__init__.py +1 -0
- package/src/superlocalmemory/server/api.py +248 -0
- package/src/superlocalmemory/server/routes/__init__.py +4 -0
- package/src/superlocalmemory/server/routes/agents.py +107 -0
- package/src/superlocalmemory/server/routes/backup.py +91 -0
- package/src/superlocalmemory/server/routes/behavioral.py +127 -0
- package/src/superlocalmemory/server/routes/compliance.py +160 -0
- package/src/superlocalmemory/server/routes/data_io.py +188 -0
- package/src/superlocalmemory/server/routes/events.py +183 -0
- package/src/superlocalmemory/server/routes/helpers.py +85 -0
- package/src/superlocalmemory/server/routes/learning.py +273 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
- package/src/superlocalmemory/server/routes/memories.py +399 -0
- package/src/superlocalmemory/server/routes/profiles.py +219 -0
- package/src/superlocalmemory/server/routes/stats.py +346 -0
- package/src/superlocalmemory/server/routes/v3_api.py +365 -0
- package/src/superlocalmemory/server/routes/ws.py +82 -0
- package/src/superlocalmemory/server/security_middleware.py +57 -0
- package/src/superlocalmemory/server/ui.py +245 -0
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/access_control.py +182 -0
- package/src/superlocalmemory/storage/database.py +594 -0
- package/src/superlocalmemory/storage/migrations.py +303 -0
- package/src/superlocalmemory/storage/models.py +406 -0
- package/src/superlocalmemory/storage/schema.py +726 -0
- package/src/superlocalmemory/storage/v2_migrator.py +317 -0
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +130 -0
- package/src/superlocalmemory/trust/provenance.py +124 -0
- package/src/superlocalmemory/trust/scorer.py +347 -0
- package/src/superlocalmemory/trust/signals.py +153 -0
- package/ui/index.html +278 -5
- package/ui/js/auto-settings.js +70 -0
- package/ui/js/dashboard.js +90 -0
- package/ui/js/fact-detail.js +92 -0
- package/ui/js/feedback.js +2 -2
- package/ui/js/ide-status.js +102 -0
- package/ui/js/math-health.js +98 -0
- package/ui/js/recall-lab.js +127 -0
- package/ui/js/settings.js +2 -2
- package/ui/js/trust-dashboard.js +73 -0
- package/api_server.py +0 -724
- package/bin/aider-smart +0 -72
- package/bin/superlocalmemoryv2-learning +0 -4
- package/bin/superlocalmemoryv2-list +0 -3
- package/bin/superlocalmemoryv2-patterns +0 -4
- package/bin/superlocalmemoryv2-profile +0 -3
- package/bin/superlocalmemoryv2-recall +0 -3
- package/bin/superlocalmemoryv2-remember +0 -3
- package/bin/superlocalmemoryv2-reset +0 -3
- package/bin/superlocalmemoryv2-status +0 -3
- package/configs/chatgpt-desktop-mcp.json +0 -16
- package/configs/cursor-mcp.json +0 -15
- package/hooks/memory-list-skill.js +0 -139
- package/hooks/memory-profile-skill.js +0 -273
- package/hooks/memory-recall-skill.js +0 -114
- package/hooks/memory-remember-skill.js +0 -127
- package/hooks/memory-reset-skill.js +0 -274
- package/mcp_server.py +0 -1808
- package/requirements-core.txt +0 -22
- package/requirements-learning.txt +0 -12
- package/requirements.txt +0 -12
- package/src/agent_registry.py +0 -411
- package/src/auth_middleware.py +0 -61
- package/src/auto_backup.py +0 -459
- package/src/behavioral/__init__.py +0 -49
- package/src/behavioral/behavioral_listener.py +0 -203
- package/src/behavioral/behavioral_patterns.py +0 -275
- package/src/behavioral/cross_project_transfer.py +0 -206
- package/src/behavioral/outcome_inference.py +0 -194
- package/src/behavioral/outcome_tracker.py +0 -193
- package/src/behavioral/tests/__init__.py +0 -4
- package/src/behavioral/tests/test_behavioral_integration.py +0 -108
- package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
- package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
- package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
- package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
- package/src/behavioral/tests/test_outcome_inference.py +0 -107
- package/src/behavioral/tests/test_outcome_tracker.py +0 -96
- package/src/cache_manager.py +0 -518
- package/src/compliance/__init__.py +0 -48
- package/src/compliance/abac_engine.py +0 -149
- package/src/compliance/abac_middleware.py +0 -116
- package/src/compliance/audit_db.py +0 -215
- package/src/compliance/audit_logger.py +0 -148
- package/src/compliance/retention_manager.py +0 -289
- package/src/compliance/retention_scheduler.py +0 -186
- package/src/compliance/tests/__init__.py +0 -4
- package/src/compliance/tests/test_abac_enforcement.py +0 -95
- package/src/compliance/tests/test_abac_engine.py +0 -124
- package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
- package/src/compliance/tests/test_audit_db.py +0 -123
- package/src/compliance/tests/test_audit_logger.py +0 -98
- package/src/compliance/tests/test_mcp_audit.py +0 -128
- package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
- package/src/compliance/tests/test_retention_manager.py +0 -131
- package/src/compliance/tests/test_retention_scheduler.py +0 -99
- package/src/compression/__init__.py +0 -25
- package/src/compression/cli.py +0 -150
- package/src/compression/cold_storage.py +0 -217
- package/src/compression/config.py +0 -72
- package/src/compression/orchestrator.py +0 -133
- package/src/compression/tier2_compressor.py +0 -228
- package/src/compression/tier3_compressor.py +0 -153
- package/src/compression/tier_classifier.py +0 -148
- package/src/db_connection_manager.py +0 -536
- package/src/embedding_engine.py +0 -63
- package/src/embeddings/__init__.py +0 -47
- package/src/embeddings/cache.py +0 -70
- package/src/embeddings/cli.py +0 -113
- package/src/embeddings/constants.py +0 -47
- package/src/embeddings/database.py +0 -91
- package/src/embeddings/engine.py +0 -247
- package/src/embeddings/model_loader.py +0 -145
- package/src/event_bus.py +0 -562
- package/src/graph/__init__.py +0 -36
- package/src/graph/build_helpers.py +0 -74
- package/src/graph/cli.py +0 -87
- package/src/graph/cluster_builder.py +0 -188
- package/src/graph/cluster_summary.py +0 -148
- package/src/graph/constants.py +0 -47
- package/src/graph/edge_builder.py +0 -162
- package/src/graph/entity_extractor.py +0 -95
- package/src/graph/graph_core.py +0 -226
- package/src/graph/graph_search.py +0 -231
- package/src/graph/hierarchical.py +0 -207
- package/src/graph/schema.py +0 -99
- package/src/graph_engine.py +0 -52
- package/src/hnsw_index.py +0 -628
- package/src/hybrid_search.py +0 -46
- package/src/learning/__init__.py +0 -217
- package/src/learning/adaptive_ranker.py +0 -682
- package/src/learning/bootstrap/__init__.py +0 -69
- package/src/learning/bootstrap/constants.py +0 -93
- package/src/learning/bootstrap/db_queries.py +0 -316
- package/src/learning/bootstrap/sampling.py +0 -82
- package/src/learning/bootstrap/text_utils.py +0 -71
- package/src/learning/cross_project_aggregator.py +0 -857
- package/src/learning/db/__init__.py +0 -40
- package/src/learning/db/constants.py +0 -44
- package/src/learning/db/schema.py +0 -279
- package/src/learning/engagement_tracker.py +0 -628
- package/src/learning/feature_extractor.py +0 -708
- package/src/learning/feedback_collector.py +0 -806
- package/src/learning/learning_db.py +0 -915
- package/src/learning/project_context_manager.py +0 -572
- package/src/learning/ranking/__init__.py +0 -33
- package/src/learning/ranking/constants.py +0 -84
- package/src/learning/ranking/helpers.py +0 -278
- package/src/learning/source_quality_scorer.py +0 -676
- package/src/learning/synthetic_bootstrap.py +0 -755
- package/src/learning/tests/test_adaptive_ranker.py +0 -325
- package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
- package/src/learning/tests/test_aggregator.py +0 -306
- package/src/learning/tests/test_auto_retrain_v28.py +0 -35
- package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
- package/src/learning/tests/test_feature_extractor_v28.py +0 -93
- package/src/learning/tests/test_feedback_collector.py +0 -294
- package/src/learning/tests/test_learning_db.py +0 -602
- package/src/learning/tests/test_learning_db_v28.py +0 -110
- package/src/learning/tests/test_learning_init_v28.py +0 -48
- package/src/learning/tests/test_outcome_signals.py +0 -48
- package/src/learning/tests/test_project_context.py +0 -292
- package/src/learning/tests/test_schema_migration.py +0 -319
- package/src/learning/tests/test_signal_inference.py +0 -397
- package/src/learning/tests/test_source_quality.py +0 -351
- package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
- package/src/learning/tests/test_workflow_miner.py +0 -318
- package/src/learning/workflow_pattern_miner.py +0 -655
- package/src/lifecycle/__init__.py +0 -54
- package/src/lifecycle/bounded_growth.py +0 -239
- package/src/lifecycle/compaction_engine.py +0 -226
- package/src/lifecycle/lifecycle_engine.py +0 -355
- package/src/lifecycle/lifecycle_evaluator.py +0 -257
- package/src/lifecycle/lifecycle_scheduler.py +0 -130
- package/src/lifecycle/retention_policy.py +0 -285
- package/src/lifecycle/tests/test_bounded_growth.py +0 -193
- package/src/lifecycle/tests/test_compaction.py +0 -179
- package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
- package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
- package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
- package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
- package/src/lifecycle/tests/test_mcp_compact.py +0 -149
- package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
- package/src/lifecycle/tests/test_retention_policy.py +0 -162
- package/src/mcp_tools_v28.py +0 -281
- package/src/memory/__init__.py +0 -36
- package/src/memory/cli.py +0 -205
- package/src/memory/constants.py +0 -39
- package/src/memory/helpers.py +0 -28
- package/src/memory/schema.py +0 -166
- package/src/memory-profiles.py +0 -595
- package/src/memory-reset.py +0 -491
- package/src/memory_compression.py +0 -989
- package/src/memory_store_v2.py +0 -1155
- package/src/migrate_v1_to_v2.py +0 -629
- package/src/pattern_learner.py +0 -34
- package/src/patterns/__init__.py +0 -24
- package/src/patterns/analyzers.py +0 -251
- package/src/patterns/learner.py +0 -271
- package/src/patterns/scoring.py +0 -171
- package/src/patterns/store.py +0 -225
- package/src/patterns/terminology.py +0 -140
- package/src/provenance_tracker.py +0 -312
- package/src/qualixar_attribution.py +0 -139
- package/src/qualixar_watermark.py +0 -78
- package/src/query_optimizer.py +0 -511
- package/src/rate_limiter.py +0 -83
- package/src/search/__init__.py +0 -20
- package/src/search/cli.py +0 -77
- package/src/search/constants.py +0 -26
- package/src/search/engine.py +0 -241
- package/src/search/fusion.py +0 -122
- package/src/search/index_loader.py +0 -114
- package/src/search/methods.py +0 -162
- package/src/search_engine_v2.py +0 -401
- package/src/setup_validator.py +0 -482
- package/src/subscription_manager.py +0 -391
- package/src/tree/__init__.py +0 -59
- package/src/tree/builder.py +0 -185
- package/src/tree/nodes.py +0 -202
- package/src/tree/queries.py +0 -257
- package/src/tree/schema.py +0 -80
- package/src/tree_manager.py +0 -19
- package/src/trust/__init__.py +0 -45
- package/src/trust/constants.py +0 -66
- package/src/trust/queries.py +0 -157
- package/src/trust/schema.py +0 -95
- package/src/trust/scorer.py +0 -299
- package/src/trust/signals.py +0 -95
- package/src/trust_scorer.py +0 -44
- package/ui/app.js +0 -1588
- package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
- package/ui/js/graph-cytoscape.js +0 -1168
- package/ui/js/graph-d3-backup.js +0 -32
- package/ui/js/graph.js +0 -32
- package/ui_server.py +0 -286
- /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
- /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
- /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
- /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
- /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
- /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
- /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
- /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
- /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
- /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
- /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
- /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
- /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
- /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
- /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
- /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
- /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
- /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
- /package/{completions → ide/completions}/slm.bash +0 -0
- /package/{completions → ide/completions}/slm.zsh +0 -0
- /package/{configs → ide/configs}/cody-commands.json +0 -0
- /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
- /package/{install.ps1 → scripts/install.ps1} +0 -0
- /package/{install.sh → scripts/install.sh} +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
4
|
+
"""SuperLocalMemory V2 - LlamaIndex Chat Store Backend
|
|
5
|
+
|
|
6
|
+
Implements LlamaIndex's BaseChatStore backed by SuperLocalMemory V2's
|
|
7
|
+
local SQLite storage. All data stays on-device — zero cloud, zero telemetry.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from llama_index.storage.chat_store.superlocalmemory import SuperLocalMemoryChatStore
|
|
11
|
+
from llama_index.core.memory import ChatMemoryBuffer
|
|
12
|
+
|
|
13
|
+
chat_store = SuperLocalMemoryChatStore()
|
|
14
|
+
memory = ChatMemoryBuffer.from_defaults(chat_store=chat_store, chat_store_key="user-123")
|
|
15
|
+
"""
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from llama_index.core.base.llms.types import ChatMessage, MessageRole
|
|
23
|
+
from llama_index.core.storage.chat_store.base import BaseChatStore
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Locate and import SuperLocalMemory V2's MemoryStoreV2
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
_SLM_PATH = Path.home() / ".claude-memory"
|
|
29
|
+
if str(_SLM_PATH) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(_SLM_PATH))
|
|
31
|
+
|
|
32
|
+
# Also support the source tree layout (for development / tests that pass db_path)
|
|
33
|
+
_SLM_SRC_PATH = Path(__file__).resolve().parents[6] / "src"
|
|
34
|
+
if _SLM_SRC_PATH.exists() and str(_SLM_SRC_PATH) not in sys.path:
|
|
35
|
+
sys.path.insert(0, str(_SLM_SRC_PATH))
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
from memory_store_v2 import MemoryStoreV2
|
|
39
|
+
except ImportError as exc:
|
|
40
|
+
raise ImportError(
|
|
41
|
+
"SuperLocalMemory V2 is not installed. "
|
|
42
|
+
"Run: curl -fsSL https://raw.githubusercontent.com/varun369/SuperLocalMemoryV2/main/install.sh | bash\n"
|
|
43
|
+
"Or visit: https://github.com/varun369/SuperLocalMemoryV2"
|
|
44
|
+
) from exc
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Constants
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Tag format: "li:chat:<hash>" where hash is a 24-char SHA-256 prefix of the key.
|
|
51
|
+
# Total tag length = 8 + 24 = 32 chars, well under SLM's 50-char MAX_TAG_LENGTH.
|
|
52
|
+
# The full session key is stored inside the serialized content JSON so get_keys()
|
|
53
|
+
# can reconstruct the original key even when the tag is hash-based.
|
|
54
|
+
_TAG_PREFIX = "li:chat:"
|
|
55
|
+
_PROJECT_NAME = "llamaindex"
|
|
56
|
+
_IMPORTANCE = 3
|
|
57
|
+
_LIST_LIMIT = 10000 # Upper bound when listing all memories for filtering
|
|
58
|
+
_HASH_LEN = 24 # Characters of SHA-256 hex digest to use in tags
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _key_hash(key: str) -> str:
|
|
62
|
+
"""Produce a deterministic short hash of a session key for use in SLM tags."""
|
|
63
|
+
return hashlib.sha256(key.encode("utf-8")).hexdigest()[:_HASH_LEN]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _make_tag(key: str) -> str:
|
|
67
|
+
"""Build the SLM tag for a chat session key.
|
|
68
|
+
|
|
69
|
+
Uses a hash of the key to guarantee the tag never exceeds SLM's
|
|
70
|
+
50-character limit regardless of key length or content.
|
|
71
|
+
"""
|
|
72
|
+
return f"{_TAG_PREFIX}{_key_hash(key)}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _serialize_message(key: str, message: ChatMessage) -> str:
|
|
76
|
+
"""Serialize a ChatMessage to JSON string for SLM content storage.
|
|
77
|
+
|
|
78
|
+
The session *key* is embedded in the payload so that ``get_keys()`` can
|
|
79
|
+
reconstruct the original key from stored memories (the tag only contains
|
|
80
|
+
a hash).
|
|
81
|
+
"""
|
|
82
|
+
return json.dumps(
|
|
83
|
+
{
|
|
84
|
+
"key": key,
|
|
85
|
+
"role": message.role.value,
|
|
86
|
+
"content": message.content or "",
|
|
87
|
+
"additional_kwargs": message.additional_kwargs,
|
|
88
|
+
},
|
|
89
|
+
ensure_ascii=False,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _deserialize_message(content: str) -> Optional[ChatMessage]:
|
|
94
|
+
"""Deserialize a JSON string back to a ChatMessage.
|
|
95
|
+
|
|
96
|
+
Returns None if the content is not valid chat message JSON.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
data = json.loads(content)
|
|
100
|
+
except (json.JSONDecodeError, TypeError):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
if not isinstance(data, dict) or "role" not in data:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
role = MessageRole(data["role"])
|
|
108
|
+
except ValueError:
|
|
109
|
+
role = MessageRole.USER
|
|
110
|
+
|
|
111
|
+
return ChatMessage(
|
|
112
|
+
role=role,
|
|
113
|
+
content=data.get("content", ""),
|
|
114
|
+
additional_kwargs=data.get("additional_kwargs", {}),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _extract_key(content: str) -> Optional[str]:
|
|
119
|
+
"""Extract the session key from a serialized memory content string."""
|
|
120
|
+
try:
|
|
121
|
+
data = json.loads(content)
|
|
122
|
+
except (json.JSONDecodeError, TypeError):
|
|
123
|
+
return None
|
|
124
|
+
if isinstance(data, dict):
|
|
125
|
+
return data.get("key")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class SuperLocalMemoryChatStore(BaseChatStore):
|
|
130
|
+
"""LlamaIndex chat store backed by SuperLocalMemory V2.
|
|
131
|
+
|
|
132
|
+
Stores chat messages in SuperLocalMemory's local SQLite database,
|
|
133
|
+
keeping all data on-device with zero cloud calls.
|
|
134
|
+
|
|
135
|
+
Each message is stored as a separate SLM memory entry tagged with
|
|
136
|
+
``li:chat:<hash>`` so different conversation sessions are cleanly
|
|
137
|
+
isolated. The full session key is preserved inside the serialized
|
|
138
|
+
content JSON for lossless round-tripping.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
db_path: Optional path to the SQLite database file.
|
|
142
|
+
Defaults to ``~/.claude-memory/memory.db``.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
# Pydantic fields ---------------------------------------------------
|
|
146
|
+
# We store the db_path as a string so the model stays JSON-serializable.
|
|
147
|
+
_db_path: Optional[str] = None
|
|
148
|
+
_store: Any = None # MemoryStoreV2 — not serializable, set in __init__
|
|
149
|
+
|
|
150
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
151
|
+
|
|
152
|
+
def __init__(self, db_path: Optional[str] = None, **kwargs: Any) -> None:
|
|
153
|
+
"""Initialize SuperLocalMemoryChatStore.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
db_path: Optional path to the SLM SQLite database.
|
|
157
|
+
If None, uses the default ``~/.claude-memory/memory.db``.
|
|
158
|
+
"""
|
|
159
|
+
super().__init__(**kwargs)
|
|
160
|
+
self._db_path = db_path
|
|
161
|
+
if db_path:
|
|
162
|
+
self._store = MemoryStoreV2(db_path=Path(db_path))
|
|
163
|
+
else:
|
|
164
|
+
self._store = MemoryStoreV2()
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def class_name(cls) -> str:
|
|
168
|
+
"""Get class name."""
|
|
169
|
+
return "SuperLocalMemoryChatStore"
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
# Internal helpers
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def _get_memories_for_key(self, key: str) -> List[Dict[str, Any]]:
|
|
176
|
+
"""Retrieve all SLM memories that belong to a chat session *key*.
|
|
177
|
+
|
|
178
|
+
Returns memories sorted by ``created_at`` ascending (oldest first)
|
|
179
|
+
to preserve conversation order.
|
|
180
|
+
"""
|
|
181
|
+
tag = _make_tag(key)
|
|
182
|
+
all_memories = self._store.list_all(limit=_LIST_LIMIT)
|
|
183
|
+
|
|
184
|
+
matched: List[Dict[str, Any]] = []
|
|
185
|
+
for mem in all_memories:
|
|
186
|
+
mem_tags = mem.get("tags", [])
|
|
187
|
+
if isinstance(mem_tags, str):
|
|
188
|
+
try:
|
|
189
|
+
mem_tags = json.loads(mem_tags)
|
|
190
|
+
except (json.JSONDecodeError, TypeError):
|
|
191
|
+
mem_tags = [t.strip() for t in mem_tags.split(",") if t.strip()]
|
|
192
|
+
if tag in (mem_tags or []):
|
|
193
|
+
matched.append(mem)
|
|
194
|
+
|
|
195
|
+
# Sort by created_at ascending for correct conversation order
|
|
196
|
+
matched.sort(key=lambda m: m.get("created_at", ""))
|
|
197
|
+
return matched
|
|
198
|
+
|
|
199
|
+
def _memories_to_messages(
|
|
200
|
+
self, memories: List[Dict[str, Any]]
|
|
201
|
+
) -> List[ChatMessage]:
|
|
202
|
+
"""Convert a list of SLM memory dicts to ChatMessage objects."""
|
|
203
|
+
messages: List[ChatMessage] = []
|
|
204
|
+
for mem in memories:
|
|
205
|
+
msg = _deserialize_message(mem.get("content", ""))
|
|
206
|
+
if msg is not None:
|
|
207
|
+
messages.append(msg)
|
|
208
|
+
return messages
|
|
209
|
+
|
|
210
|
+
# ------------------------------------------------------------------
|
|
211
|
+
# BaseChatStore abstract method implementations
|
|
212
|
+
# ------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
def set_messages(self, key: str, messages: List[ChatMessage]) -> None:
|
|
215
|
+
"""Set messages for a key (replaces any existing messages)."""
|
|
216
|
+
self.delete_messages(key)
|
|
217
|
+
for message in messages:
|
|
218
|
+
self.add_message(key, message)
|
|
219
|
+
|
|
220
|
+
def get_messages(self, key: str) -> List[ChatMessage]:
|
|
221
|
+
"""Get messages for a key, ordered by creation time."""
|
|
222
|
+
memories = self._get_memories_for_key(key)
|
|
223
|
+
return self._memories_to_messages(memories)
|
|
224
|
+
|
|
225
|
+
def add_message(self, key: str, message: ChatMessage) -> None:
|
|
226
|
+
"""Add a single message for a key."""
|
|
227
|
+
content = _serialize_message(key, message)
|
|
228
|
+
tag = _make_tag(key)
|
|
229
|
+
self._store.add_memory(
|
|
230
|
+
content=content,
|
|
231
|
+
tags=[tag],
|
|
232
|
+
project_name=_PROJECT_NAME,
|
|
233
|
+
importance=_IMPORTANCE,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def delete_messages(self, key: str) -> Optional[List[ChatMessage]]:
|
|
237
|
+
"""Delete all messages for a key.
|
|
238
|
+
|
|
239
|
+
Returns the deleted messages, or None if the key had no messages.
|
|
240
|
+
"""
|
|
241
|
+
memories = self._get_memories_for_key(key)
|
|
242
|
+
if not memories:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
messages = self._memories_to_messages(memories)
|
|
246
|
+
|
|
247
|
+
for mem in memories:
|
|
248
|
+
self._store.delete_memory(mem["id"])
|
|
249
|
+
|
|
250
|
+
return messages
|
|
251
|
+
|
|
252
|
+
def delete_message(self, key: str, idx: int) -> Optional[ChatMessage]:
|
|
253
|
+
"""Delete specific message for a key by index.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
key: The session key.
|
|
257
|
+
idx: Zero-based index of the message to delete.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
The deleted ChatMessage, or None if index is out of range.
|
|
261
|
+
"""
|
|
262
|
+
memories = self._get_memories_for_key(key)
|
|
263
|
+
if not memories or idx < 0 or idx >= len(memories):
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
target = memories[idx]
|
|
267
|
+
msg = _deserialize_message(target.get("content", ""))
|
|
268
|
+
self._store.delete_memory(target["id"])
|
|
269
|
+
return msg
|
|
270
|
+
|
|
271
|
+
def delete_last_message(self, key: str) -> Optional[ChatMessage]:
|
|
272
|
+
"""Delete the last (most recent) message for a key.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
The deleted ChatMessage, or None if the key has no messages.
|
|
276
|
+
"""
|
|
277
|
+
memories = self._get_memories_for_key(key)
|
|
278
|
+
if not memories:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
last = memories[-1]
|
|
282
|
+
msg = _deserialize_message(last.get("content", ""))
|
|
283
|
+
self._store.delete_memory(last["id"])
|
|
284
|
+
return msg
|
|
285
|
+
|
|
286
|
+
def get_keys(self) -> List[str]:
|
|
287
|
+
"""Get all unique session keys that have stored messages.
|
|
288
|
+
|
|
289
|
+
Keys are extracted from the serialized content JSON (the ``key``
|
|
290
|
+
field) rather than from tags, because tags contain only a hash of
|
|
291
|
+
the key for length-safety.
|
|
292
|
+
"""
|
|
293
|
+
all_memories = self._store.list_all(limit=_LIST_LIMIT)
|
|
294
|
+
keys_seen: set[str] = set()
|
|
295
|
+
|
|
296
|
+
for mem in all_memories:
|
|
297
|
+
# Only consider memories whose tags indicate they belong to us
|
|
298
|
+
mem_tags = mem.get("tags", [])
|
|
299
|
+
if isinstance(mem_tags, str):
|
|
300
|
+
try:
|
|
301
|
+
mem_tags = json.loads(mem_tags)
|
|
302
|
+
except (json.JSONDecodeError, TypeError):
|
|
303
|
+
mem_tags = [t.strip() for t in mem_tags.split(",") if t.strip()]
|
|
304
|
+
|
|
305
|
+
is_ours = any(
|
|
306
|
+
isinstance(t, str) and t.startswith(_TAG_PREFIX)
|
|
307
|
+
for t in (mem_tags or [])
|
|
308
|
+
)
|
|
309
|
+
if not is_ours:
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
key = _extract_key(mem.get("content", ""))
|
|
313
|
+
if key is not None:
|
|
314
|
+
keys_seen.add(key)
|
|
315
|
+
|
|
316
|
+
return sorted(keys_seen)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "llama-index-storage-chat-store-superlocalmemory"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LlamaIndex chat store backed by SuperLocalMemory — 100% local, zero cloud"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Varun Pratap Bhardwaj" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["llamaindex", "llama-index", "memory", "local-first", "privacy", "sqlite", "chat-store", "mcp"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"llama-index-core>=0.13.0,<0.15",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/varun369/SuperLocalMemoryV2"
|
|
29
|
+
Documentation = "https://superlocalmemory.com/"
|
|
30
|
+
Repository = "https://github.com/varun369/SuperLocalMemoryV2"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["llama_index"]
|
|
34
|
+
|
|
35
|
+
[tool.llamahub]
|
|
36
|
+
contains_example = false
|
|
37
|
+
import_path = "llama_index.storage.chat_store.superlocalmemory"
|
|
38
|
+
|
|
39
|
+
[tool.llamahub.class_authors]
|
|
40
|
+
SuperLocalMemoryChatStore = "varun369"
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# Copyright (c) 2026 SuperLocalMemory (superlocalmemory.com)
|
|
4
|
+
"""SuperLocalMemory V2 - LlamaIndex Chat Store Tests
|
|
5
|
+
|
|
6
|
+
Comprehensive tests for the SuperLocalMemoryChatStore implementation.
|
|
7
|
+
All tests use temporary databases — no production data is touched.
|
|
8
|
+
"""
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
# Ensure the SLM source tree is importable for tests
|
|
16
|
+
_SLM_SRC = os.path.join(
|
|
17
|
+
os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, "src"
|
|
18
|
+
)
|
|
19
|
+
_SLM_SRC = os.path.abspath(_SLM_SRC)
|
|
20
|
+
if _SLM_SRC not in sys.path:
|
|
21
|
+
sys.path.insert(0, _SLM_SRC)
|
|
22
|
+
|
|
23
|
+
from llama_index.core.base.llms.types import ChatMessage, MessageRole
|
|
24
|
+
from llama_index.storage.chat_store.superlocalmemory import SuperLocalMemoryChatStore
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Fixtures
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def tmp_db():
|
|
34
|
+
"""Provide a path to a temporary database file that is cleaned up after use."""
|
|
35
|
+
with tempfile.TemporaryDirectory() as d:
|
|
36
|
+
yield os.path.join(d, "test_memory.db")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def store(tmp_db):
|
|
41
|
+
"""Provide a fresh SuperLocalMemoryChatStore backed by a temp database."""
|
|
42
|
+
return SuperLocalMemoryChatStore(db_path=tmp_db)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Helpers
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _make_messages(
|
|
51
|
+
texts: list[str], roles: list[MessageRole] | None = None
|
|
52
|
+
) -> list[ChatMessage]:
|
|
53
|
+
"""Create a list of ChatMessages from plain text strings."""
|
|
54
|
+
if roles is None:
|
|
55
|
+
# Alternate user / assistant
|
|
56
|
+
roles = [
|
|
57
|
+
MessageRole.USER if i % 2 == 0 else MessageRole.ASSISTANT
|
|
58
|
+
for i in range(len(texts))
|
|
59
|
+
]
|
|
60
|
+
return [ChatMessage(role=r, content=t) for r, t in zip(roles, texts)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Tests
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestSetAndGetMessages:
|
|
69
|
+
"""Tests for set_messages / get_messages round-trip."""
|
|
70
|
+
|
|
71
|
+
def test_set_and_get_messages(self, store):
|
|
72
|
+
"""Set 3 messages and retrieve them in order."""
|
|
73
|
+
msgs = _make_messages(["Hello", "Hi there", "How are you?"])
|
|
74
|
+
store.set_messages("session-1", msgs)
|
|
75
|
+
|
|
76
|
+
retrieved = store.get_messages("session-1")
|
|
77
|
+
assert len(retrieved) == 3
|
|
78
|
+
assert retrieved[0].content == "Hello"
|
|
79
|
+
assert retrieved[1].content == "Hi there"
|
|
80
|
+
assert retrieved[2].content == "How are you?"
|
|
81
|
+
|
|
82
|
+
def test_get_messages_empty(self, store):
|
|
83
|
+
"""Getting messages for a non-existent key returns an empty list."""
|
|
84
|
+
assert store.get_messages("nonexistent") == []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestAddMessage:
|
|
88
|
+
"""Tests for add_message."""
|
|
89
|
+
|
|
90
|
+
def test_add_single_message(self, store):
|
|
91
|
+
"""Add one message and verify it appears in get_messages."""
|
|
92
|
+
msg = ChatMessage(role=MessageRole.USER, content="solo message")
|
|
93
|
+
store.add_message("key-a", msg)
|
|
94
|
+
|
|
95
|
+
retrieved = store.get_messages("key-a")
|
|
96
|
+
assert len(retrieved) == 1
|
|
97
|
+
assert retrieved[0].content == "solo message"
|
|
98
|
+
assert retrieved[0].role == MessageRole.USER
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TestDeleteMessages:
|
|
102
|
+
"""Tests for delete_messages (all messages for a key)."""
|
|
103
|
+
|
|
104
|
+
def test_delete_messages(self, store):
|
|
105
|
+
"""Set, delete, verify empty and check return value."""
|
|
106
|
+
msgs = _make_messages(["A", "B", "C"])
|
|
107
|
+
store.set_messages("del-key", msgs)
|
|
108
|
+
|
|
109
|
+
deleted = store.delete_messages("del-key")
|
|
110
|
+
assert deleted is not None
|
|
111
|
+
assert len(deleted) == 3
|
|
112
|
+
assert deleted[0].content == "A"
|
|
113
|
+
|
|
114
|
+
# After deletion, key should be empty
|
|
115
|
+
assert store.get_messages("del-key") == []
|
|
116
|
+
|
|
117
|
+
def test_delete_messages_nonexistent(self, store):
|
|
118
|
+
"""Deleting messages for a key with no data returns None."""
|
|
119
|
+
assert store.delete_messages("ghost-key") is None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class TestDeleteMessageByIndex:
|
|
123
|
+
"""Tests for delete_message (single message by index)."""
|
|
124
|
+
|
|
125
|
+
def test_delete_message_by_index(self, store):
|
|
126
|
+
"""Set 3 messages, delete index 1, verify 2 remain in correct order."""
|
|
127
|
+
msgs = _make_messages(["first", "second", "third"])
|
|
128
|
+
store.set_messages("idx-key", msgs)
|
|
129
|
+
|
|
130
|
+
deleted = store.delete_message("idx-key", 1)
|
|
131
|
+
assert deleted is not None
|
|
132
|
+
assert deleted.content == "second"
|
|
133
|
+
|
|
134
|
+
remaining = store.get_messages("idx-key")
|
|
135
|
+
assert len(remaining) == 2
|
|
136
|
+
assert remaining[0].content == "first"
|
|
137
|
+
assert remaining[1].content == "third"
|
|
138
|
+
|
|
139
|
+
def test_delete_message_out_of_range(self, store):
|
|
140
|
+
"""Deleting an out-of-range index returns None."""
|
|
141
|
+
msgs = _make_messages(["only"])
|
|
142
|
+
store.set_messages("range-key", msgs)
|
|
143
|
+
assert store.delete_message("range-key", 5) is None
|
|
144
|
+
|
|
145
|
+
def test_delete_message_negative_index(self, store):
|
|
146
|
+
"""Deleting a negative index returns None."""
|
|
147
|
+
msgs = _make_messages(["x"])
|
|
148
|
+
store.set_messages("neg-key", msgs)
|
|
149
|
+
assert store.delete_message("neg-key", -1) is None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestDeleteLastMessage:
|
|
153
|
+
"""Tests for delete_last_message."""
|
|
154
|
+
|
|
155
|
+
def test_delete_last_message(self, store):
|
|
156
|
+
"""Set 3 messages, delete last, verify 2 remain."""
|
|
157
|
+
msgs = _make_messages(["alpha", "beta", "gamma"])
|
|
158
|
+
store.set_messages("last-key", msgs)
|
|
159
|
+
|
|
160
|
+
deleted = store.delete_last_message("last-key")
|
|
161
|
+
assert deleted is not None
|
|
162
|
+
assert deleted.content == "gamma"
|
|
163
|
+
|
|
164
|
+
remaining = store.get_messages("last-key")
|
|
165
|
+
assert len(remaining) == 2
|
|
166
|
+
assert remaining[-1].content == "beta"
|
|
167
|
+
|
|
168
|
+
def test_delete_last_message_empty(self, store):
|
|
169
|
+
"""Deleting last message on an empty key returns None."""
|
|
170
|
+
assert store.delete_last_message("empty-key") is None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class TestGetKeys:
|
|
174
|
+
"""Tests for get_keys."""
|
|
175
|
+
|
|
176
|
+
def test_get_keys(self, store):
|
|
177
|
+
"""Set messages for 3 different keys and verify get_keys returns all 3."""
|
|
178
|
+
for key in ["alice", "bob", "carol"]:
|
|
179
|
+
store.add_message(key, ChatMessage(role=MessageRole.USER, content=f"hi from {key}"))
|
|
180
|
+
|
|
181
|
+
keys = store.get_keys()
|
|
182
|
+
assert sorted(keys) == ["alice", "bob", "carol"]
|
|
183
|
+
|
|
184
|
+
def test_get_keys_empty(self, store):
|
|
185
|
+
"""No data returns empty list."""
|
|
186
|
+
assert store.get_keys() == []
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestSessionIsolation:
|
|
190
|
+
"""Tests that different keys don't interfere with each other."""
|
|
191
|
+
|
|
192
|
+
def test_session_isolation(self, store):
|
|
193
|
+
"""Messages in key A should not appear in key B."""
|
|
194
|
+
store.set_messages("sess-A", _make_messages(["A1", "A2"]))
|
|
195
|
+
store.set_messages("sess-B", _make_messages(["B1", "B2", "B3"]))
|
|
196
|
+
|
|
197
|
+
a_msgs = store.get_messages("sess-A")
|
|
198
|
+
b_msgs = store.get_messages("sess-B")
|
|
199
|
+
|
|
200
|
+
assert len(a_msgs) == 2
|
|
201
|
+
assert len(b_msgs) == 3
|
|
202
|
+
assert a_msgs[0].content == "A1"
|
|
203
|
+
assert b_msgs[0].content == "B1"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class TestSetOverwrites:
|
|
207
|
+
"""Tests that set_messages replaces previous messages."""
|
|
208
|
+
|
|
209
|
+
def test_set_overwrites(self, store):
|
|
210
|
+
"""Setting messages twice replaces the first batch entirely."""
|
|
211
|
+
store.set_messages("ow-key", _make_messages(["old1", "old2", "old3"]))
|
|
212
|
+
store.set_messages("ow-key", _make_messages(["new1"]))
|
|
213
|
+
|
|
214
|
+
msgs = store.get_messages("ow-key")
|
|
215
|
+
assert len(msgs) == 1
|
|
216
|
+
assert msgs[0].content == "new1"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class TestPersistence:
|
|
220
|
+
"""Tests that data survives across store instances."""
|
|
221
|
+
|
|
222
|
+
def test_persistence(self, tmp_db):
|
|
223
|
+
"""Create store, add data, create NEW store on same db, verify data."""
|
|
224
|
+
store1 = SuperLocalMemoryChatStore(db_path=tmp_db)
|
|
225
|
+
store1.set_messages("persist-key", _make_messages(["remember me"]))
|
|
226
|
+
|
|
227
|
+
# New store instance pointing at same database
|
|
228
|
+
store2 = SuperLocalMemoryChatStore(db_path=tmp_db)
|
|
229
|
+
msgs = store2.get_messages("persist-key")
|
|
230
|
+
assert len(msgs) == 1
|
|
231
|
+
assert msgs[0].content == "remember me"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TestContentEdgeCases:
|
|
235
|
+
"""Tests for special content: unicode, long text, multiple roles."""
|
|
236
|
+
|
|
237
|
+
def test_unicode_content(self, store):
|
|
238
|
+
"""Unicode characters (CJK, emoji, diacritics) round-trip correctly."""
|
|
239
|
+
texts = [
|
|
240
|
+
"Hello, world!",
|
|
241
|
+
"Hej varlden!",
|
|
242
|
+
"Bonjour le monde!",
|
|
243
|
+
]
|
|
244
|
+
store.set_messages("unicode-key", _make_messages(texts))
|
|
245
|
+
msgs = store.get_messages("unicode-key")
|
|
246
|
+
assert len(msgs) == 3
|
|
247
|
+
assert msgs[1].content == "Hej varlden!"
|
|
248
|
+
|
|
249
|
+
def test_long_content(self, store):
|
|
250
|
+
"""A 10K character message round-trips correctly."""
|
|
251
|
+
long_text = "x" * 10_000
|
|
252
|
+
store.add_message(
|
|
253
|
+
"long-key", ChatMessage(role=MessageRole.USER, content=long_text)
|
|
254
|
+
)
|
|
255
|
+
msgs = store.get_messages("long-key")
|
|
256
|
+
assert len(msgs) == 1
|
|
257
|
+
assert len(msgs[0].content) == 10_000
|
|
258
|
+
|
|
259
|
+
def test_message_roles(self, store):
|
|
260
|
+
"""User, assistant, and system roles all round-trip correctly."""
|
|
261
|
+
msgs = [
|
|
262
|
+
ChatMessage(role=MessageRole.SYSTEM, content="You are helpful."),
|
|
263
|
+
ChatMessage(role=MessageRole.USER, content="Hi"),
|
|
264
|
+
ChatMessage(role=MessageRole.ASSISTANT, content="Hello!"),
|
|
265
|
+
]
|
|
266
|
+
store.set_messages("roles-key", msgs)
|
|
267
|
+
retrieved = store.get_messages("roles-key")
|
|
268
|
+
|
|
269
|
+
assert retrieved[0].role == MessageRole.SYSTEM
|
|
270
|
+
assert retrieved[1].role == MessageRole.USER
|
|
271
|
+
assert retrieved[2].role == MessageRole.ASSISTANT
|
|
272
|
+
|
|
273
|
+
def test_empty_content_message(self, store):
|
|
274
|
+
"""A message with empty content still round-trips."""
|
|
275
|
+
store.add_message(
|
|
276
|
+
"empty-msg", ChatMessage(role=MessageRole.ASSISTANT, content="")
|
|
277
|
+
)
|
|
278
|
+
msgs = store.get_messages("empty-msg")
|
|
279
|
+
assert len(msgs) == 1
|
|
280
|
+
# Empty content serializes as "" and deserializes back
|
|
281
|
+
assert msgs[0].content == "" or msgs[0].content is None
|
|
282
|
+
|
|
283
|
+
def test_additional_kwargs(self, store):
|
|
284
|
+
"""additional_kwargs round-trip correctly."""
|
|
285
|
+
msg = ChatMessage(
|
|
286
|
+
role=MessageRole.USER,
|
|
287
|
+
content="with metadata",
|
|
288
|
+
additional_kwargs={"source": "test", "count": 42},
|
|
289
|
+
)
|
|
290
|
+
store.add_message("kwargs-key", msg)
|
|
291
|
+
msgs = store.get_messages("kwargs-key")
|
|
292
|
+
assert len(msgs) == 1
|
|
293
|
+
assert msgs[0].additional_kwargs.get("source") == "test"
|
|
294
|
+
assert msgs[0].additional_kwargs.get("count") == 42
|