claude-smart 0.2.42 → 0.2.43
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/.claude-plugin/marketplace.json +3 -3
- package/README.md +1 -1
- package/package.json +9 -3
- package/plugin/.claude-plugin/plugin.json +9 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +2 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +91 -0
- package/plugin/scripts/backend-service.sh +1 -0
- package/plugin/scripts/cli.sh +1 -0
- package/plugin/scripts/codex-hook.js +72 -4
- package/plugin/scripts/dashboard-build.sh +1 -0
- package/plugin/scripts/dashboard-service.sh +1 -0
- package/plugin/scripts/hook_entry.sh +1 -0
- package/plugin/scripts/smart-install.sh +1 -0
- package/plugin/src/claude_smart/context_format.py +11 -12
- package/plugin/src/claude_smart/cs_cite.py +26 -12
- package/plugin/src/claude_smart/ids.py +13 -5
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +53 -0
- package/plugin/vendor/reflexio/LICENSE +201 -0
- package/plugin/vendor/reflexio/README.md +338 -0
- package/plugin/vendor/reflexio/pyproject.toml +271 -0
- package/plugin/vendor/reflexio/reflexio/README.md +184 -0
- package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
- package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
- package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
- package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +266 -0
- package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +961 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
- package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
- package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
- package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
- package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
- package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
- package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
- package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
- package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
- package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
- package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
- package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
- package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
- package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
- package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
- package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
- package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
- package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1103 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +766 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +280 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
- package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
- package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +616 -0
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +2714 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +540 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
- package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +255 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +279 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +127 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +340 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1626 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +351 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +468 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +777 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +960 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +741 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +734 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +800 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +70 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1253 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +372 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +568 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
- package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
- package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
- package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
- package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
|
@@ -0,0 +1,1945 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class and module-level helpers for SQLite storage.
|
|
3
|
+
|
|
4
|
+
Supports hybrid search combining FTS5 (BM25) with embedding cosine similarity
|
|
5
|
+
via Reciprocal Rank Fusion (RRF). Falls back to FTS-only when no embeddings
|
|
6
|
+
are available.
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import functools
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import math
|
|
14
|
+
import re
|
|
15
|
+
import sqlite3
|
|
16
|
+
import threading
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from datetime import UTC, datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, ClassVar, Literal
|
|
21
|
+
|
|
22
|
+
from reflexio.models.api_schema.common import BlockingIssue
|
|
23
|
+
from reflexio.models.api_schema.service_schemas import (
|
|
24
|
+
AgentPlaybook,
|
|
25
|
+
AgentPlaybookSnapshot,
|
|
26
|
+
AgentPlaybookUpdateEntry,
|
|
27
|
+
AgentSuccessEvaluationResult,
|
|
28
|
+
Citation,
|
|
29
|
+
Interaction,
|
|
30
|
+
PlaybookAggregationChangeLog,
|
|
31
|
+
PlaybookStatus,
|
|
32
|
+
ProfileChangeLog,
|
|
33
|
+
ProfileTimeToLive,
|
|
34
|
+
RegularVsShadow,
|
|
35
|
+
Request,
|
|
36
|
+
Status,
|
|
37
|
+
ToolUsed,
|
|
38
|
+
UserActionType,
|
|
39
|
+
UserPlaybook,
|
|
40
|
+
UserProfile,
|
|
41
|
+
)
|
|
42
|
+
from reflexio.models.config_schema import (
|
|
43
|
+
EMBEDDING_DIMENSIONS,
|
|
44
|
+
APIKeyConfig,
|
|
45
|
+
LLMConfig,
|
|
46
|
+
SearchMode,
|
|
47
|
+
)
|
|
48
|
+
from reflexio.server.llm.litellm_client import LiteLLMClient, LiteLLMConfig
|
|
49
|
+
from reflexio.server.llm.model_defaults import (
|
|
50
|
+
ModelRole,
|
|
51
|
+
resolve_model_name,
|
|
52
|
+
)
|
|
53
|
+
from reflexio.server.llm.providers.embedding_service_provider import (
|
|
54
|
+
EmbeddingUnavailableError,
|
|
55
|
+
)
|
|
56
|
+
from reflexio.server.services.storage.error import StorageError
|
|
57
|
+
from reflexio.server.services.storage.retention import RetentionTarget
|
|
58
|
+
from reflexio.server.services.storage.retention_mixin import (
|
|
59
|
+
RETENTION_DELETE_CHUNK,
|
|
60
|
+
RetentionMixin,
|
|
61
|
+
chunked,
|
|
62
|
+
)
|
|
63
|
+
from reflexio.server.services.storage.storage_base import BaseStorage
|
|
64
|
+
from reflexio.server.site_var.site_var_manager import SiteVarManager
|
|
65
|
+
|
|
66
|
+
from ._stall_state import init_stall_state_table
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Module-level helpers
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _json_dumps(obj: Any) -> str | None:
|
|
77
|
+
"""Serialize a Python object to a JSON string, or None if the object is None."""
|
|
78
|
+
if obj is None:
|
|
79
|
+
return None
|
|
80
|
+
return json.dumps(obj, default=str)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _json_loads(text: str | None) -> Any:
|
|
84
|
+
"""Deserialize a JSON string, returning None for None/empty input."""
|
|
85
|
+
if not text:
|
|
86
|
+
return None
|
|
87
|
+
return json.loads(text)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
_FTS5_OPERATORS = frozenset({"OR", "AND", "NOT"})
|
|
91
|
+
_FTS5_RESERVED = _FTS5_OPERATORS | {"NEAR"}
|
|
92
|
+
_TOKEN_RE = re.compile(r"[a-zA-Z0-9_]+")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _sanitize_fts_query(text: str) -> str:
|
|
96
|
+
"""Sanitize a query string for FTS5, defaulting to OR between tokens.
|
|
97
|
+
|
|
98
|
+
Bare (unquoted) tokens preserve Porter stemming. Explicit OR/AND/NOT
|
|
99
|
+
operators are passed through. A trailing ``*`` is appended to the last
|
|
100
|
+
token for prefix matching.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
text: Raw user query string (may contain FTS5 boolean operators like OR)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
FTS5-safe query string with stemming enabled and OR default
|
|
107
|
+
"""
|
|
108
|
+
tokens = _TOKEN_RE.findall(text)
|
|
109
|
+
if not tokens:
|
|
110
|
+
return '""'
|
|
111
|
+
|
|
112
|
+
has_explicit_operator = any(t in _FTS5_OPERATORS for t in tokens)
|
|
113
|
+
|
|
114
|
+
parts: list[str] = []
|
|
115
|
+
for t in tokens:
|
|
116
|
+
if t in _FTS5_OPERATORS:
|
|
117
|
+
if not parts or parts[-1] in _FTS5_OPERATORS:
|
|
118
|
+
continue
|
|
119
|
+
parts.append(t)
|
|
120
|
+
elif t in _FTS5_RESERVED:
|
|
121
|
+
continue
|
|
122
|
+
else:
|
|
123
|
+
if not has_explicit_operator and parts and parts[-1] not in _FTS5_OPERATORS:
|
|
124
|
+
parts.append("OR")
|
|
125
|
+
parts.append(t)
|
|
126
|
+
|
|
127
|
+
if parts and parts[-1] in _FTS5_OPERATORS:
|
|
128
|
+
parts.pop()
|
|
129
|
+
if not parts:
|
|
130
|
+
return '""'
|
|
131
|
+
|
|
132
|
+
# Append prefix wildcard to last token for partial-word matching
|
|
133
|
+
parts[-1] = parts[-1] + "*"
|
|
134
|
+
return " ".join(parts)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _cosine_similarity(a: list[float], b: list[float]) -> float:
|
|
138
|
+
"""Compute cosine similarity between two vectors.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
a: First embedding vector.
|
|
142
|
+
b: Second embedding vector.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Cosine similarity in [-1, 1], or 0.0 for degenerate inputs.
|
|
146
|
+
"""
|
|
147
|
+
if len(a) != len(b) or not a:
|
|
148
|
+
return 0.0
|
|
149
|
+
dot = sum(x * y for x, y in zip(a, b, strict=True))
|
|
150
|
+
mag_a = math.sqrt(sum(x * x for x in a))
|
|
151
|
+
mag_b = math.sqrt(sum(x * x for x in b))
|
|
152
|
+
if mag_a == 0.0 or mag_b == 0.0:
|
|
153
|
+
return 0.0
|
|
154
|
+
return dot / (mag_a * mag_b)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _effective_search_mode(
|
|
158
|
+
mode: SearchMode,
|
|
159
|
+
query_embedding: list[float] | None,
|
|
160
|
+
query: str | None,
|
|
161
|
+
) -> SearchMode:
|
|
162
|
+
"""Downgrade search mode when the required embedding is unavailable.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
mode: Requested search mode.
|
|
166
|
+
query_embedding: Pre-computed query embedding, or None.
|
|
167
|
+
query: Original query text. The fallback warning is suppressed when
|
|
168
|
+
this is falsy, since an empty-query HYBRID/VECTOR request has no
|
|
169
|
+
semantic intent to lose.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The effective SearchMode — falls back to FTS when HYBRID/VECTOR lacks an embedding.
|
|
173
|
+
"""
|
|
174
|
+
if mode in (SearchMode.HYBRID, SearchMode.VECTOR) and not query_embedding:
|
|
175
|
+
if query:
|
|
176
|
+
logger.warning(
|
|
177
|
+
"Search mode '%s' requested but no query embedding provided — falling back to FTS",
|
|
178
|
+
mode,
|
|
179
|
+
)
|
|
180
|
+
return SearchMode.FTS
|
|
181
|
+
return mode
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _vector_rank_rows(
|
|
185
|
+
rows: list[sqlite3.Row],
|
|
186
|
+
query_embedding: list[float],
|
|
187
|
+
match_count: int,
|
|
188
|
+
) -> list[sqlite3.Row]:
|
|
189
|
+
"""Rank rows by cosine similarity to the query embedding.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
rows: Candidate rows with stored embeddings.
|
|
193
|
+
query_embedding: The query's embedding vector.
|
|
194
|
+
match_count: Number of results to return.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Top ``match_count`` rows sorted by cosine similarity descending.
|
|
198
|
+
"""
|
|
199
|
+
scored: list[tuple[sqlite3.Row, float]] = []
|
|
200
|
+
for row in rows:
|
|
201
|
+
raw_emb = row["embedding"] if "embedding" in row.keys() else None # noqa: SIM118
|
|
202
|
+
emb = _json_loads(raw_emb) if raw_emb else None
|
|
203
|
+
if emb:
|
|
204
|
+
sim = _cosine_similarity(query_embedding, emb)
|
|
205
|
+
scored.append((row, sim))
|
|
206
|
+
|
|
207
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
208
|
+
# Diagnostic: log the full pre-cut score distribution so retrieval
|
|
209
|
+
# misses are debuggable. Without this, the only signal callers see is
|
|
210
|
+
# "K results returned" with no way to tell whether a relevant row
|
|
211
|
+
# scored 0.39 (close, just below an upstream threshold filter) vs
|
|
212
|
+
# 0.05 (semantic mismatch, no threshold tuning will save it). Top 10
|
|
213
|
+
# is sufficient context; logs at INFO so it shows up in production
|
|
214
|
+
# backend.log without an explicit debug flag. Cost is one log line
|
|
215
|
+
# per vector search call (~200 bytes).
|
|
216
|
+
if scored:
|
|
217
|
+
top = [round(s, 3) for _, s in scored[:10]]
|
|
218
|
+
logger.info(
|
|
219
|
+
"vector_rank: candidates=%d match_count=%d top_scores=%s",
|
|
220
|
+
len(scored),
|
|
221
|
+
match_count,
|
|
222
|
+
top,
|
|
223
|
+
)
|
|
224
|
+
return [row for row, _ in scored[:match_count]]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _true_rrf_merge(
|
|
228
|
+
fts_rows: list[sqlite3.Row],
|
|
229
|
+
vec_rows: list[sqlite3.Row],
|
|
230
|
+
id_column: str,
|
|
231
|
+
match_count: int,
|
|
232
|
+
rrf_k: int = 60,
|
|
233
|
+
vector_weight: float = 1.0,
|
|
234
|
+
fts_weight: float = 1.0,
|
|
235
|
+
) -> list[sqlite3.Row]:
|
|
236
|
+
"""Merge independent FTS and vector result sets via Reciprocal Rank Fusion.
|
|
237
|
+
|
|
238
|
+
Unlike ``_rrf_rerank`` (which re-ranks FTS results only), this function
|
|
239
|
+
takes two independently-produced result lists and unions them so that
|
|
240
|
+
documents appearing in *either* modality can surface.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
fts_rows: Rows from an FTS query, in BM25-ranked order.
|
|
244
|
+
vec_rows: Rows from a vector query, in cosine-similarity order.
|
|
245
|
+
id_column: Column name used as primary key to deduplicate rows.
|
|
246
|
+
match_count: Number of results to return.
|
|
247
|
+
rrf_k: RRF smoothing constant (default 60).
|
|
248
|
+
vector_weight: Weight for vector similarity contribution.
|
|
249
|
+
fts_weight: Weight for FTS contribution.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Top ``match_count`` rows sorted by combined RRF score.
|
|
253
|
+
"""
|
|
254
|
+
if not fts_rows and not vec_rows:
|
|
255
|
+
return []
|
|
256
|
+
|
|
257
|
+
# Collect unique rows by ID (first-seen wins for the Row object)
|
|
258
|
+
row_by_id: dict[str | int, sqlite3.Row] = {}
|
|
259
|
+
for row in (*fts_rows, *vec_rows):
|
|
260
|
+
rid = row[id_column]
|
|
261
|
+
if rid not in row_by_id:
|
|
262
|
+
row_by_id[rid] = row
|
|
263
|
+
|
|
264
|
+
# Build rank maps (1-based); missing entries get a penalty rank
|
|
265
|
+
fts_rank: dict[str | int, int] = {
|
|
266
|
+
row[id_column]: i + 1 for i, row in enumerate(fts_rows)
|
|
267
|
+
}
|
|
268
|
+
vec_rank: dict[str | int, int] = {
|
|
269
|
+
row[id_column]: i + 1 for i, row in enumerate(vec_rows)
|
|
270
|
+
}
|
|
271
|
+
fts_penalty = len(fts_rows) + 1
|
|
272
|
+
vec_penalty = len(vec_rows) + 1
|
|
273
|
+
|
|
274
|
+
scored: list[tuple[sqlite3.Row, float]] = []
|
|
275
|
+
for rid, row in row_by_id.items():
|
|
276
|
+
f_rank = fts_rank.get(rid, fts_penalty)
|
|
277
|
+
v_rank = vec_rank.get(rid, vec_penalty)
|
|
278
|
+
score = fts_weight / (rrf_k + f_rank) + vector_weight / (rrf_k + v_rank)
|
|
279
|
+
scored.append((row, score))
|
|
280
|
+
|
|
281
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
282
|
+
return [row for row, _ in scored[:match_count]]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _status_value(status: Status | None) -> str | None:
|
|
286
|
+
"""Convert a Status enum (or None) to its DB string value."""
|
|
287
|
+
if status is None:
|
|
288
|
+
return None
|
|
289
|
+
if hasattr(status, "value"):
|
|
290
|
+
return status.value
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _build_status_sql(
|
|
295
|
+
status_filter: list[Status | None],
|
|
296
|
+
col: str = "status",
|
|
297
|
+
) -> tuple[str, list[Any]]:
|
|
298
|
+
"""Build a SQL WHERE fragment for a list of status values.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
status_filter: List of Status enum values (may include None for CURRENT)
|
|
302
|
+
col: Column name to filter on
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Tuple of (SQL fragment, parameter list) ready for AND-chaining
|
|
306
|
+
"""
|
|
307
|
+
has_none = False
|
|
308
|
+
values: list[str] = []
|
|
309
|
+
for s in status_filter:
|
|
310
|
+
v = _status_value(s)
|
|
311
|
+
if v is None:
|
|
312
|
+
has_none = True
|
|
313
|
+
else:
|
|
314
|
+
values.append(v)
|
|
315
|
+
|
|
316
|
+
if has_none and values:
|
|
317
|
+
placeholders = ",".join("?" for _ in values)
|
|
318
|
+
return f"({col} IS NULL OR {col} IN ({placeholders}))", values
|
|
319
|
+
if has_none:
|
|
320
|
+
return f"{col} IS NULL", []
|
|
321
|
+
if values:
|
|
322
|
+
placeholders = ",".join("?" for _ in values)
|
|
323
|
+
return f"{col} IN ({placeholders})", values
|
|
324
|
+
return "1=1", []
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _iso_now() -> str:
|
|
328
|
+
"""Return current UTC time as ISO 8601 string."""
|
|
329
|
+
return datetime.now(UTC).isoformat()
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _epoch_now() -> int:
|
|
333
|
+
"""Return current UTC Unix timestamp."""
|
|
334
|
+
return int(datetime.now(UTC).timestamp())
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _iso_to_epoch(iso_str: str | None) -> int:
|
|
338
|
+
"""Convert an ISO datetime string to Unix timestamp."""
|
|
339
|
+
if not iso_str:
|
|
340
|
+
return _epoch_now()
|
|
341
|
+
try:
|
|
342
|
+
cleaned = iso_str.replace("Z", "+00:00")
|
|
343
|
+
return int(datetime.fromisoformat(cleaned).timestamp())
|
|
344
|
+
except (ValueError, TypeError):
|
|
345
|
+
return _epoch_now()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _epoch_to_iso(ts: int) -> str:
|
|
349
|
+
"""Convert a Unix timestamp to ISO 8601 string."""
|
|
350
|
+
return datetime.fromtimestamp(ts, tz=UTC).isoformat()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
# Row-to-model converters
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _row_to_profile(row: sqlite3.Row) -> UserProfile:
|
|
359
|
+
d = dict(row)
|
|
360
|
+
return UserProfile(
|
|
361
|
+
profile_id=d["profile_id"],
|
|
362
|
+
user_id=d["user_id"],
|
|
363
|
+
content=d["content"],
|
|
364
|
+
last_modified_timestamp=d["last_modified_timestamp"],
|
|
365
|
+
generated_from_request_id=d["generated_from_request_id"],
|
|
366
|
+
profile_time_to_live=ProfileTimeToLive(d["profile_time_to_live"]),
|
|
367
|
+
expiration_timestamp=d["expiration_timestamp"],
|
|
368
|
+
custom_features=_json_loads(d.get("custom_features")),
|
|
369
|
+
source=d.get("source") or "",
|
|
370
|
+
status=Status(d["status"]) if d.get("status") else None,
|
|
371
|
+
extractor_names=_json_loads(d.get("extractor_names")),
|
|
372
|
+
expanded_terms=d.get("expanded_terms"),
|
|
373
|
+
source_span=d.get("source_span"),
|
|
374
|
+
notes=d.get("notes"),
|
|
375
|
+
reader_angle=d.get("reader_angle"),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _row_to_interaction(row: sqlite3.Row) -> Interaction:
|
|
380
|
+
d = dict(row)
|
|
381
|
+
tools_used_raw = _json_loads(d.get("tools_used"))
|
|
382
|
+
tools_used = (
|
|
383
|
+
[ToolUsed(**t) for t in tools_used_raw if isinstance(t, dict)]
|
|
384
|
+
if tools_used_raw and isinstance(tools_used_raw, list)
|
|
385
|
+
else []
|
|
386
|
+
)
|
|
387
|
+
citations_raw = _json_loads(d.get("citations"))
|
|
388
|
+
citations = (
|
|
389
|
+
[Citation(**c) for c in citations_raw if isinstance(c, dict)]
|
|
390
|
+
if citations_raw and isinstance(citations_raw, list)
|
|
391
|
+
else []
|
|
392
|
+
)
|
|
393
|
+
return Interaction(
|
|
394
|
+
interaction_id=d["interaction_id"],
|
|
395
|
+
user_id=d["user_id"],
|
|
396
|
+
content=d["content"],
|
|
397
|
+
request_id=d["request_id"],
|
|
398
|
+
created_at=_iso_to_epoch(d["created_at"]),
|
|
399
|
+
role=d.get("role") or "User",
|
|
400
|
+
user_action=UserActionType(d["user_action"]),
|
|
401
|
+
user_action_description=d["user_action_description"],
|
|
402
|
+
interacted_image_url=d["interacted_image_url"],
|
|
403
|
+
shadow_content=d.get("shadow_content") or "",
|
|
404
|
+
expert_content=d.get("expert_content") or "",
|
|
405
|
+
tools_used=tools_used,
|
|
406
|
+
citations=citations,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _row_to_request(row: sqlite3.Row) -> Request:
|
|
411
|
+
d = dict(row)
|
|
412
|
+
metadata_raw = d.get("metadata") or "{}"
|
|
413
|
+
try:
|
|
414
|
+
parsed = _json_loads(metadata_raw)
|
|
415
|
+
metadata = parsed if isinstance(parsed, dict) else {}
|
|
416
|
+
except json.JSONDecodeError:
|
|
417
|
+
logger.warning(
|
|
418
|
+
"Malformed metadata JSON for request %s; defaulting to empty dict",
|
|
419
|
+
d.get("request_id"),
|
|
420
|
+
)
|
|
421
|
+
metadata = {}
|
|
422
|
+
return Request(
|
|
423
|
+
request_id=d["request_id"],
|
|
424
|
+
user_id=d["user_id"],
|
|
425
|
+
created_at=_iso_to_epoch(d["created_at"]),
|
|
426
|
+
source=d.get("source") or "",
|
|
427
|
+
agent_version=d.get("agent_version") or "",
|
|
428
|
+
session_id=d.get("session_id"),
|
|
429
|
+
metadata=metadata,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _row_to_user_playbook(
|
|
434
|
+
row: sqlite3.Row, include_embedding: bool = False
|
|
435
|
+
) -> UserPlaybook:
|
|
436
|
+
d = dict(row)
|
|
437
|
+
embedding: list[float] = []
|
|
438
|
+
if include_embedding and d.get("embedding"):
|
|
439
|
+
raw_emb = _json_loads(d["embedding"])
|
|
440
|
+
if isinstance(raw_emb, list):
|
|
441
|
+
embedding = [float(x) for x in raw_emb]
|
|
442
|
+
return UserPlaybook(
|
|
443
|
+
user_playbook_id=d["user_playbook_id"],
|
|
444
|
+
user_id=d.get("user_id"),
|
|
445
|
+
playbook_name=d["playbook_name"],
|
|
446
|
+
created_at=_iso_to_epoch(d["created_at"]),
|
|
447
|
+
request_id=d["request_id"],
|
|
448
|
+
agent_version=d["agent_version"],
|
|
449
|
+
content=d["content"],
|
|
450
|
+
trigger=d.get("trigger"),
|
|
451
|
+
rationale=d.get("rationale"),
|
|
452
|
+
blocking_issue=BlockingIssue(**json.loads(d["blocking_issue"]))
|
|
453
|
+
if d.get("blocking_issue")
|
|
454
|
+
else None,
|
|
455
|
+
status=Status(d["status"]) if d.get("status") else None,
|
|
456
|
+
source=d.get("source"),
|
|
457
|
+
source_interaction_ids=_json_loads(d.get("source_interaction_ids")) or [],
|
|
458
|
+
embedding=embedding,
|
|
459
|
+
expanded_terms=d.get("expanded_terms"),
|
|
460
|
+
source_span=d.get("source_span"),
|
|
461
|
+
notes=d.get("notes"),
|
|
462
|
+
reader_angle=d.get("reader_angle"),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _row_to_agent_playbook(row: sqlite3.Row) -> AgentPlaybook:
|
|
467
|
+
d = dict(row)
|
|
468
|
+
return AgentPlaybook(
|
|
469
|
+
agent_playbook_id=d["agent_playbook_id"],
|
|
470
|
+
playbook_name=d["playbook_name"],
|
|
471
|
+
created_at=_iso_to_epoch(d["created_at"]),
|
|
472
|
+
agent_version=d["agent_version"],
|
|
473
|
+
content=d["content"],
|
|
474
|
+
trigger=d.get("trigger"),
|
|
475
|
+
rationale=d.get("rationale"),
|
|
476
|
+
blocking_issue=BlockingIssue(**json.loads(d["blocking_issue"]))
|
|
477
|
+
if d.get("blocking_issue")
|
|
478
|
+
else None,
|
|
479
|
+
playbook_status=PlaybookStatus(d["playbook_status"])
|
|
480
|
+
if d.get("playbook_status")
|
|
481
|
+
else PlaybookStatus.PENDING,
|
|
482
|
+
playbook_metadata=d.get("playbook_metadata") or "",
|
|
483
|
+
embedding=[],
|
|
484
|
+
status=Status(d["status"]) if d.get("status") else None,
|
|
485
|
+
expanded_terms=d.get("expanded_terms"),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _row_to_eval_result(row: sqlite3.Row) -> AgentSuccessEvaluationResult:
|
|
490
|
+
d = dict(row)
|
|
491
|
+
return AgentSuccessEvaluationResult(
|
|
492
|
+
result_id=d["result_id"],
|
|
493
|
+
session_id=d["session_id"],
|
|
494
|
+
agent_version=d["agent_version"],
|
|
495
|
+
evaluation_name=d.get("evaluation_name"),
|
|
496
|
+
is_success=bool(d["is_success"]),
|
|
497
|
+
failure_type=d.get("failure_type"),
|
|
498
|
+
failure_reason=d.get("failure_reason"),
|
|
499
|
+
created_at=_iso_to_epoch(d["created_at"]),
|
|
500
|
+
regular_vs_shadow=(
|
|
501
|
+
RegularVsShadow(d["regular_vs_shadow"])
|
|
502
|
+
if d.get("regular_vs_shadow")
|
|
503
|
+
else None
|
|
504
|
+
),
|
|
505
|
+
number_of_correction_per_session=d.get("number_of_correction_per_session") or 0,
|
|
506
|
+
user_turns_to_resolution=d.get("user_turns_to_resolution"),
|
|
507
|
+
is_escalated=bool(d.get("is_escalated", False)),
|
|
508
|
+
embedding=[],
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _row_to_profile_change_log(row: sqlite3.Row) -> ProfileChangeLog:
|
|
513
|
+
d = dict(row)
|
|
514
|
+
return ProfileChangeLog(
|
|
515
|
+
id=d["id"],
|
|
516
|
+
user_id=d["user_id"],
|
|
517
|
+
request_id=d["request_id"],
|
|
518
|
+
created_at=d["created_at"],
|
|
519
|
+
added_profiles=[
|
|
520
|
+
UserProfile(**p) for p in (_json_loads(d["added_profiles"]) or [])
|
|
521
|
+
],
|
|
522
|
+
removed_profiles=[
|
|
523
|
+
UserProfile(**p) for p in (_json_loads(d["removed_profiles"]) or [])
|
|
524
|
+
],
|
|
525
|
+
mentioned_profiles=[
|
|
526
|
+
UserProfile(**p) for p in (_json_loads(d["mentioned_profiles"]) or [])
|
|
527
|
+
],
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _row_to_playbook_aggregation_change_log(
|
|
532
|
+
row: sqlite3.Row,
|
|
533
|
+
) -> PlaybookAggregationChangeLog:
|
|
534
|
+
d = dict(row)
|
|
535
|
+
return PlaybookAggregationChangeLog(
|
|
536
|
+
id=d["id"],
|
|
537
|
+
created_at=d["created_at"],
|
|
538
|
+
playbook_name=d["playbook_name"],
|
|
539
|
+
agent_version=d["agent_version"],
|
|
540
|
+
run_mode=d["run_mode"],
|
|
541
|
+
added_agent_playbooks=[
|
|
542
|
+
AgentPlaybookSnapshot(**fb)
|
|
543
|
+
for fb in (_json_loads(d.get("added_playbooks")) or [])
|
|
544
|
+
],
|
|
545
|
+
removed_agent_playbooks=[
|
|
546
|
+
AgentPlaybookSnapshot(**fb)
|
|
547
|
+
for fb in (_json_loads(d.get("removed_playbooks")) or [])
|
|
548
|
+
],
|
|
549
|
+
updated_agent_playbooks=[
|
|
550
|
+
AgentPlaybookUpdateEntry(
|
|
551
|
+
before=AgentPlaybookSnapshot(**entry["before"]),
|
|
552
|
+
after=AgentPlaybookSnapshot(**entry["after"]),
|
|
553
|
+
)
|
|
554
|
+
for entry in (_json_loads(d.get("updated_playbooks")) or [])
|
|
555
|
+
],
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# ---------------------------------------------------------------------------
|
|
560
|
+
# SQLiteStorageBase
|
|
561
|
+
# ---------------------------------------------------------------------------
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class SQLiteStorageBase(RetentionMixin, BaseStorage):
|
|
565
|
+
"""SQLite-backed storage base class for local/self-hosted deployments."""
|
|
566
|
+
|
|
567
|
+
supports_embedding: ClassVar[bool] = True
|
|
568
|
+
|
|
569
|
+
@staticmethod
|
|
570
|
+
def handle_exceptions(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
571
|
+
@functools.wraps(func)
|
|
572
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
573
|
+
try:
|
|
574
|
+
return func(*args, **kwargs)
|
|
575
|
+
except StorageError:
|
|
576
|
+
raise
|
|
577
|
+
except Exception as e:
|
|
578
|
+
import traceback
|
|
579
|
+
|
|
580
|
+
stack_trace = traceback.format_exc()
|
|
581
|
+
logger.error(
|
|
582
|
+
"Error in %s: %s\nStack trace:\n%s",
|
|
583
|
+
func.__name__,
|
|
584
|
+
str(e),
|
|
585
|
+
stack_trace,
|
|
586
|
+
)
|
|
587
|
+
raise StorageError(message=f"{e}\nStack trace:\n{stack_trace}") from e
|
|
588
|
+
|
|
589
|
+
return wrapper
|
|
590
|
+
|
|
591
|
+
def __init__(
|
|
592
|
+
self,
|
|
593
|
+
org_id: str,
|
|
594
|
+
db_path: str | None = None,
|
|
595
|
+
api_key_config: APIKeyConfig | None = None,
|
|
596
|
+
llm_config: LLMConfig | None = None,
|
|
597
|
+
enable_document_expansion: bool = False,
|
|
598
|
+
) -> None:
|
|
599
|
+
super().__init__(org_id)
|
|
600
|
+
self.api_key_config = api_key_config
|
|
601
|
+
self._enable_document_expansion = enable_document_expansion
|
|
602
|
+
|
|
603
|
+
# Resolve db_path: explicit arg > LOCAL_STORAGE_PATH env var > ~/.reflexio/data/
|
|
604
|
+
if db_path is None:
|
|
605
|
+
from reflexio.server import LOCAL_STORAGE_PATH
|
|
606
|
+
|
|
607
|
+
db_path = str(Path(LOCAL_STORAGE_PATH) / "reflexio.db")
|
|
608
|
+
|
|
609
|
+
self.db_path = db_path
|
|
610
|
+
self._lock = threading.RLock()
|
|
611
|
+
|
|
612
|
+
logger.info("SQLite Storage for org %s using db_path: %s", org_id, db_path)
|
|
613
|
+
|
|
614
|
+
# Ensure parent directory exists
|
|
615
|
+
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
616
|
+
|
|
617
|
+
# Open connection
|
|
618
|
+
self.conn = sqlite3.connect(db_path, check_same_thread=False)
|
|
619
|
+
self.conn.row_factory = sqlite3.Row
|
|
620
|
+
self.conn.execute("PRAGMA journal_mode=WAL")
|
|
621
|
+
self.conn.execute("PRAGMA foreign_keys=ON")
|
|
622
|
+
|
|
623
|
+
# LLM client for embeddings
|
|
624
|
+
model_setting = SiteVarManager().get_site_var("llm_model_setting")
|
|
625
|
+
site_var = model_setting if isinstance(model_setting, dict) else {}
|
|
626
|
+
|
|
627
|
+
self.embedding_model_name = resolve_model_name(
|
|
628
|
+
ModelRole.EMBEDDING,
|
|
629
|
+
site_var_value=site_var.get("embedding_model_name"),
|
|
630
|
+
config_override=llm_config.embedding_model_name if llm_config else None,
|
|
631
|
+
api_key_config=self.api_key_config,
|
|
632
|
+
)
|
|
633
|
+
self.embedding_dimensions = EMBEDDING_DIMENSIONS
|
|
634
|
+
|
|
635
|
+
litellm_config = LiteLLMConfig(
|
|
636
|
+
model=self.embedding_model_name,
|
|
637
|
+
temperature=0.0,
|
|
638
|
+
api_key_config=self.api_key_config,
|
|
639
|
+
)
|
|
640
|
+
self.llm_client = LiteLLMClient(litellm_config)
|
|
641
|
+
|
|
642
|
+
# Optionally load sqlite-vec for native KNN vector search
|
|
643
|
+
self._has_sqlite_vec = self._try_load_sqlite_vec()
|
|
644
|
+
|
|
645
|
+
# Create tables
|
|
646
|
+
self.migrate()
|
|
647
|
+
|
|
648
|
+
# ------------------------------------------------------------------
|
|
649
|
+
# DDL / migration
|
|
650
|
+
# ------------------------------------------------------------------
|
|
651
|
+
|
|
652
|
+
def migrate(self) -> bool:
|
|
653
|
+
self._migrate_feedback_schema()
|
|
654
|
+
self._migrate_interactions_schema()
|
|
655
|
+
with self._lock:
|
|
656
|
+
cur = self.conn.cursor()
|
|
657
|
+
cur.executescript(_DDL)
|
|
658
|
+
self.conn.commit()
|
|
659
|
+
if self._has_sqlite_vec:
|
|
660
|
+
self._create_vec_tables()
|
|
661
|
+
self._migrate_vec_tables()
|
|
662
|
+
# Run after DDL so tables exist on fresh databases
|
|
663
|
+
self._migrate_agent_runs_schema()
|
|
664
|
+
self._migrate_pending_tool_calls_schema()
|
|
665
|
+
self._migrate_expanded_terms()
|
|
666
|
+
self._migrate_agentic_signals()
|
|
667
|
+
self._migrate_agent_playbook_source_windows()
|
|
668
|
+
self._migrate_request_metadata()
|
|
669
|
+
self._migrate_shadow_comparison_verdicts()
|
|
670
|
+
self._migrate_user_playbook_polarity()
|
|
671
|
+
init_stall_state_table(self.conn)
|
|
672
|
+
return True
|
|
673
|
+
|
|
674
|
+
# -- Retention hooks (see RetentionMixin) --
|
|
675
|
+
|
|
676
|
+
@handle_exceptions
|
|
677
|
+
def _retention_table_exists(self, table_name: str) -> bool:
|
|
678
|
+
row = self._fetchone(
|
|
679
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
680
|
+
(table_name,),
|
|
681
|
+
)
|
|
682
|
+
return row is not None
|
|
683
|
+
|
|
684
|
+
@handle_exceptions
|
|
685
|
+
def _retention_count_rows(self, target: RetentionTarget) -> int:
|
|
686
|
+
row = self._fetchone(f"SELECT COUNT(*) as cnt FROM {target.table_name}") # noqa: S608
|
|
687
|
+
return int(row["cnt"]) if row else 0
|
|
688
|
+
|
|
689
|
+
@handle_exceptions
|
|
690
|
+
def _retention_select_oldest_keys(
|
|
691
|
+
self, target: RetentionTarget, count: int
|
|
692
|
+
) -> list[tuple[Any, ...]]:
|
|
693
|
+
id_sql = ", ".join(target.id_columns)
|
|
694
|
+
tiebreak_sql = id_sql
|
|
695
|
+
rows = self._fetchall(
|
|
696
|
+
f"SELECT {id_sql} FROM {target.table_name} " # noqa: S608
|
|
697
|
+
f"ORDER BY {target.order_column} ASC, {tiebreak_sql} ASC LIMIT ?",
|
|
698
|
+
(count,),
|
|
699
|
+
)
|
|
700
|
+
return [tuple(row[col] for col in target.id_columns) for row in rows]
|
|
701
|
+
|
|
702
|
+
@handle_exceptions
|
|
703
|
+
def _retention_perform_delete(
|
|
704
|
+
self, target: RetentionTarget, keys: list[tuple[Any, ...]]
|
|
705
|
+
) -> None:
|
|
706
|
+
# Wrap dependency + target deletes in a single critical section so
|
|
707
|
+
# concurrent writers see either both or neither.
|
|
708
|
+
with self._lock:
|
|
709
|
+
self._retention_delete_dependencies(target, keys)
|
|
710
|
+
self._retention_delete_target_rows(target, keys)
|
|
711
|
+
self.conn.commit()
|
|
712
|
+
|
|
713
|
+
def _retention_delete_dependencies(
|
|
714
|
+
self, target: RetentionTarget, keys: list[tuple[Any, ...]]
|
|
715
|
+
) -> None:
|
|
716
|
+
ids = [key[0] for key in keys]
|
|
717
|
+
target_name = target.name
|
|
718
|
+
if target_name == "requests":
|
|
719
|
+
self._delete_interactions_for_request_ids([str(v) for v in ids])
|
|
720
|
+
elif target_name == "interactions":
|
|
721
|
+
self._delete_interaction_search_rows([int(v) for v in ids])
|
|
722
|
+
elif target_name == "profiles":
|
|
723
|
+
self._delete_profile_search_rows([str(v) for v in ids])
|
|
724
|
+
elif target_name == "user_playbooks":
|
|
725
|
+
self._delete_source_windows_for_user_playbook_ids([int(v) for v in ids])
|
|
726
|
+
self._delete_playbook_search_rows("user", [int(v) for v in ids])
|
|
727
|
+
elif target_name == "agent_playbooks":
|
|
728
|
+
self._delete_source_windows_for_agent_playbook_ids([int(v) for v in ids])
|
|
729
|
+
self._delete_playbook_search_rows("agent", [int(v) for v in ids])
|
|
730
|
+
elif target_name == "playbook_optimization_jobs":
|
|
731
|
+
self._delete_optimizer_rows_for_job_ids([int(v) for v in ids])
|
|
732
|
+
elif target_name == "playbook_optimization_candidates":
|
|
733
|
+
self._delete_optimizer_evaluations_for_candidate_ids([int(v) for v in ids])
|
|
734
|
+
|
|
735
|
+
def _retention_delete_target_rows(
|
|
736
|
+
self, target: RetentionTarget, keys: list[tuple[Any, ...]]
|
|
737
|
+
) -> None:
|
|
738
|
+
if len(target.id_columns) == 1:
|
|
739
|
+
self._delete_in_chunks(
|
|
740
|
+
target.table_name,
|
|
741
|
+
target.id_columns[0],
|
|
742
|
+
[key[0] for key in keys],
|
|
743
|
+
)
|
|
744
|
+
return
|
|
745
|
+
# Composite-key delete: chunk by row to bound parameter count.
|
|
746
|
+
params_per_key = len(target.id_columns)
|
|
747
|
+
rows_per_chunk = max(1, RETENTION_DELETE_CHUNK // params_per_key)
|
|
748
|
+
for chunk in chunked(keys, rows_per_chunk):
|
|
749
|
+
where = " OR ".join(
|
|
750
|
+
"("
|
|
751
|
+
+ " AND ".join(f"{column} = ?" for column in target.id_columns)
|
|
752
|
+
+ ")"
|
|
753
|
+
for _ in chunk
|
|
754
|
+
)
|
|
755
|
+
params = [value for key in chunk for value in key]
|
|
756
|
+
self.conn.execute(
|
|
757
|
+
f"DELETE FROM {target.table_name} WHERE {where}", # noqa: S608
|
|
758
|
+
params,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
# -- Chunked-delete primitives shared by the cascade helpers --
|
|
762
|
+
|
|
763
|
+
def _delete_in_chunks(
|
|
764
|
+
self, table_name: str, column_name: str, values: list[Any]
|
|
765
|
+
) -> None:
|
|
766
|
+
"""Chunked ``DELETE FROM table WHERE col IN (...)``.
|
|
767
|
+
|
|
768
|
+
Chunking keeps parameter count under ``SQLITE_MAX_VARIABLE_NUMBER``
|
|
769
|
+
on older sqlite builds (default 999) and avoids degenerate plans
|
|
770
|
+
on very large IN lists.
|
|
771
|
+
"""
|
|
772
|
+
if not values:
|
|
773
|
+
return
|
|
774
|
+
for chunk in chunked(values):
|
|
775
|
+
placeholders = ",".join("?" for _ in chunk)
|
|
776
|
+
self.conn.execute(
|
|
777
|
+
f"DELETE FROM {table_name} WHERE {column_name} IN ({placeholders})", # noqa: S608
|
|
778
|
+
chunk,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
def _select_in_chunks(self, sql_template: str, values: list[Any]) -> list[Any]:
|
|
782
|
+
"""Run ``sql_template`` (containing ``{placeholders}``) over chunks of
|
|
783
|
+
``values`` and aggregate the result rows."""
|
|
784
|
+
results: list[Any] = []
|
|
785
|
+
for chunk in chunked(values):
|
|
786
|
+
placeholders = ",".join("?" for _ in chunk)
|
|
787
|
+
stmt = sql_template.format(placeholders=placeholders)
|
|
788
|
+
results.extend(self.conn.execute(stmt, chunk).fetchall())
|
|
789
|
+
return results
|
|
790
|
+
|
|
791
|
+
def _delete_interactions_for_request_ids(self, request_ids: list[str]) -> None:
|
|
792
|
+
if not request_ids:
|
|
793
|
+
return
|
|
794
|
+
rows = self._select_in_chunks(
|
|
795
|
+
"SELECT interaction_id FROM interactions WHERE request_id IN ({placeholders})",
|
|
796
|
+
request_ids,
|
|
797
|
+
)
|
|
798
|
+
self._delete_interaction_search_rows(
|
|
799
|
+
[int(row["interaction_id"]) for row in rows]
|
|
800
|
+
)
|
|
801
|
+
self._delete_in_chunks("interactions", "request_id", request_ids)
|
|
802
|
+
|
|
803
|
+
def _delete_interaction_search_rows(self, interaction_ids: list[int]) -> None:
|
|
804
|
+
if not interaction_ids:
|
|
805
|
+
return
|
|
806
|
+
self._delete_in_chunks("interactions_fts", "rowid", interaction_ids)
|
|
807
|
+
for interaction_id in interaction_ids:
|
|
808
|
+
self._vec_delete("interactions_vec", interaction_id)
|
|
809
|
+
|
|
810
|
+
def _delete_profile_search_rows(self, profile_ids: list[str]) -> None:
|
|
811
|
+
if not profile_ids:
|
|
812
|
+
return
|
|
813
|
+
rows = self._select_in_chunks(
|
|
814
|
+
"SELECT rowid, profile_id FROM profiles WHERE profile_id IN ({placeholders})",
|
|
815
|
+
profile_ids,
|
|
816
|
+
)
|
|
817
|
+
for row in rows:
|
|
818
|
+
self._fts_delete_profile(row["profile_id"])
|
|
819
|
+
self._vec_delete("profiles_vec", row["rowid"])
|
|
820
|
+
|
|
821
|
+
def _delete_playbook_search_rows(self, kind: str, ids: list[int]) -> None:
|
|
822
|
+
if not ids:
|
|
823
|
+
return
|
|
824
|
+
self._delete_in_chunks(f"{kind}_playbooks_fts", "rowid", ids)
|
|
825
|
+
for item_id in ids:
|
|
826
|
+
self._vec_delete(f"{kind}_playbooks_vec", item_id)
|
|
827
|
+
|
|
828
|
+
def _delete_source_windows_for_agent_playbook_ids(
|
|
829
|
+
self, agent_playbook_ids: list[int]
|
|
830
|
+
) -> None:
|
|
831
|
+
self._delete_in_chunks(
|
|
832
|
+
"agent_playbook_source_user_playbooks",
|
|
833
|
+
"agent_playbook_id",
|
|
834
|
+
agent_playbook_ids,
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
def _delete_source_windows_for_user_playbook_ids(
|
|
838
|
+
self, user_playbook_ids: list[int]
|
|
839
|
+
) -> None:
|
|
840
|
+
self._delete_in_chunks(
|
|
841
|
+
"agent_playbook_source_user_playbooks",
|
|
842
|
+
"user_playbook_id",
|
|
843
|
+
user_playbook_ids,
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
def _delete_optimizer_rows_for_job_ids(self, job_ids: list[int]) -> None:
|
|
847
|
+
if not job_ids:
|
|
848
|
+
return
|
|
849
|
+
for table in (
|
|
850
|
+
"playbook_optimization_evaluations",
|
|
851
|
+
"playbook_optimization_events",
|
|
852
|
+
"playbook_optimization_candidates",
|
|
853
|
+
):
|
|
854
|
+
self._delete_in_chunks(table, "job_id", job_ids)
|
|
855
|
+
|
|
856
|
+
def _delete_optimizer_evaluations_for_candidate_ids(
|
|
857
|
+
self, candidate_ids: list[int]
|
|
858
|
+
) -> None:
|
|
859
|
+
self._delete_in_chunks(
|
|
860
|
+
"playbook_optimization_evaluations",
|
|
861
|
+
"candidate_id",
|
|
862
|
+
candidate_ids,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
def _try_load_sqlite_vec(self) -> bool:
|
|
866
|
+
"""Attempt to load the sqlite-vec extension for native KNN search.
|
|
867
|
+
|
|
868
|
+
Returns:
|
|
869
|
+
True if the extension was loaded successfully, False otherwise.
|
|
870
|
+
"""
|
|
871
|
+
try:
|
|
872
|
+
import sqlite_vec # type: ignore[import-untyped]
|
|
873
|
+
|
|
874
|
+
# AttributeError covers Python builds without loadable-extension
|
|
875
|
+
# support (common with pyenv/Homebrew on macOS) — the method
|
|
876
|
+
# itself is absent rather than raising at runtime.
|
|
877
|
+
self.conn.enable_load_extension(True)
|
|
878
|
+
sqlite_vec.load(self.conn)
|
|
879
|
+
self.conn.enable_load_extension(False)
|
|
880
|
+
logger.info("sqlite-vec extension loaded — native KNN search enabled")
|
|
881
|
+
return True
|
|
882
|
+
except (ImportError, OSError, AttributeError, sqlite3.OperationalError) as e:
|
|
883
|
+
logger.info("sqlite-vec not available, using Python fallback: %s", e)
|
|
884
|
+
return False
|
|
885
|
+
|
|
886
|
+
def _create_vec_tables(self) -> None:
|
|
887
|
+
"""Create vec0 virtual tables for each entity that stores embeddings."""
|
|
888
|
+
dim = self.embedding_dimensions
|
|
889
|
+
vec_ddl = f"""
|
|
890
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS interactions_vec USING vec0(
|
|
891
|
+
embedding float[{dim}]
|
|
892
|
+
);
|
|
893
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS profiles_vec USING vec0(
|
|
894
|
+
embedding float[{dim}]
|
|
895
|
+
);
|
|
896
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS user_playbooks_vec USING vec0(
|
|
897
|
+
embedding float[{dim}]
|
|
898
|
+
);
|
|
899
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS agent_playbooks_vec USING vec0(
|
|
900
|
+
embedding float[{dim}]
|
|
901
|
+
);
|
|
902
|
+
"""
|
|
903
|
+
with self._lock:
|
|
904
|
+
self.conn.executescript(vec_ddl)
|
|
905
|
+
self.conn.commit()
|
|
906
|
+
|
|
907
|
+
def _migrate_vec_tables(self) -> None:
|
|
908
|
+
"""Backfill vec tables from existing embedding TEXT columns (idempotent)."""
|
|
909
|
+
entity_map = [
|
|
910
|
+
("interactions", "interactions_vec", "interaction_id"),
|
|
911
|
+
("profiles", "profiles_vec", "profile_id"),
|
|
912
|
+
("user_playbooks", "user_playbooks_vec", "user_playbook_id"),
|
|
913
|
+
("agent_playbooks", "agent_playbooks_vec", "agent_playbook_id"),
|
|
914
|
+
]
|
|
915
|
+
for main_table, vec_table, _id_col in entity_map:
|
|
916
|
+
row = self._fetchone(f"SELECT COUNT(*) as cnt FROM {vec_table}")
|
|
917
|
+
if row and row["cnt"] > 0:
|
|
918
|
+
continue # Already populated
|
|
919
|
+
rows = self._fetchall(
|
|
920
|
+
f"SELECT rowid AS rid, embedding FROM {main_table} WHERE embedding IS NOT NULL"
|
|
921
|
+
)
|
|
922
|
+
for r in rows:
|
|
923
|
+
emb = _json_loads(r["embedding"])
|
|
924
|
+
if emb:
|
|
925
|
+
self._vec_upsert(vec_table, r["rid"], emb)
|
|
926
|
+
|
|
927
|
+
def _migrate_interactions_schema(self) -> None:
|
|
928
|
+
"""Add new columns to existing interactions table if missing."""
|
|
929
|
+
with self._lock:
|
|
930
|
+
cur = self.conn.execute("PRAGMA table_info(interactions)")
|
|
931
|
+
columns = {row[1] for row in cur.fetchall()}
|
|
932
|
+
|
|
933
|
+
if not columns:
|
|
934
|
+
return
|
|
935
|
+
|
|
936
|
+
if "expert_content" not in columns:
|
|
937
|
+
logger.info("Adding expert_content column to interactions table.")
|
|
938
|
+
with self._lock:
|
|
939
|
+
self.conn.execute(
|
|
940
|
+
"ALTER TABLE interactions ADD COLUMN expert_content TEXT NOT NULL DEFAULT ''"
|
|
941
|
+
)
|
|
942
|
+
self.conn.commit()
|
|
943
|
+
|
|
944
|
+
if "citations" not in columns:
|
|
945
|
+
logger.info("Adding citations column to interactions table.")
|
|
946
|
+
with self._lock:
|
|
947
|
+
self.conn.execute("ALTER TABLE interactions ADD COLUMN citations TEXT")
|
|
948
|
+
self.conn.commit()
|
|
949
|
+
|
|
950
|
+
def _migrate_feedback_schema(self) -> None:
|
|
951
|
+
"""Drop old-schema feedback/playbook tables so _DDL can recreate them.
|
|
952
|
+
|
|
953
|
+
Checks for two migration scenarios:
|
|
954
|
+
1. Old column layout (missing ``trigger``) -- drop data tables + FTS.
|
|
955
|
+
2. Old FTS column name (``feedback_content`` instead of ``search_text``)
|
|
956
|
+
-- drop only the FTS tables so they are recreated with the new column.
|
|
957
|
+
|
|
958
|
+
Also handles migration from old table names (raw_feedbacks/feedbacks)
|
|
959
|
+
to new names (user_playbooks/agent_playbooks), renames
|
|
960
|
+
feedback_aggregation_change_logs to playbook_aggregation_change_logs,
|
|
961
|
+
and renames columns on related tables (skills, profiles,
|
|
962
|
+
playbook_aggregation_change_logs).
|
|
963
|
+
|
|
964
|
+
Since SQLite is used only for local development, data loss is acceptable.
|
|
965
|
+
"""
|
|
966
|
+
# Check for old table names and rename if needed
|
|
967
|
+
with self._lock:
|
|
968
|
+
old_tables = {
|
|
969
|
+
row[0]
|
|
970
|
+
for row in self.conn.execute(
|
|
971
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
972
|
+
).fetchall()
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if "raw_feedbacks" in old_tables and "user_playbooks" not in old_tables:
|
|
976
|
+
logger.warning(
|
|
977
|
+
"Detected old table names (raw_feedbacks/feedbacks). "
|
|
978
|
+
"Dropping old tables so they can be recreated with the new schema."
|
|
979
|
+
)
|
|
980
|
+
with self._lock:
|
|
981
|
+
self.conn.executescript("""
|
|
982
|
+
DROP TABLE IF EXISTS raw_feedbacks_fts;
|
|
983
|
+
DROP TABLE IF EXISTS feedbacks_fts;
|
|
984
|
+
DROP TABLE IF EXISTS raw_feedbacks;
|
|
985
|
+
DROP TABLE IF EXISTS feedbacks;
|
|
986
|
+
""")
|
|
987
|
+
self.conn.commit()
|
|
988
|
+
|
|
989
|
+
if (
|
|
990
|
+
"feedback_aggregation_change_logs" in old_tables
|
|
991
|
+
and "playbook_aggregation_change_logs" not in old_tables
|
|
992
|
+
):
|
|
993
|
+
logger.warning(
|
|
994
|
+
"Renaming table feedback_aggregation_change_logs → playbook_aggregation_change_logs."
|
|
995
|
+
)
|
|
996
|
+
with self._lock:
|
|
997
|
+
self.conn.execute(
|
|
998
|
+
"ALTER TABLE feedback_aggregation_change_logs RENAME TO playbook_aggregation_change_logs"
|
|
999
|
+
)
|
|
1000
|
+
self.conn.commit()
|
|
1001
|
+
|
|
1002
|
+
# Migrate renamed columns on related tables (skills, profiles, change_logs)
|
|
1003
|
+
self._migrate_renamed_columns()
|
|
1004
|
+
|
|
1005
|
+
with self._lock:
|
|
1006
|
+
cur = self.conn.execute("PRAGMA table_info(user_playbooks)")
|
|
1007
|
+
columns = {row[1] for row in cur.fetchall()}
|
|
1008
|
+
|
|
1009
|
+
# Table doesn't exist yet -- nothing to migrate
|
|
1010
|
+
if not columns:
|
|
1011
|
+
return
|
|
1012
|
+
|
|
1013
|
+
# Scenario 1: old data schema (missing trigger column — pre-flattening)
|
|
1014
|
+
if "trigger" not in columns:
|
|
1015
|
+
logger.warning(
|
|
1016
|
+
"Detected old playbook schema (missing trigger column). "
|
|
1017
|
+
"Dropping playbook tables so they can be recreated with the new schema."
|
|
1018
|
+
)
|
|
1019
|
+
with self._lock:
|
|
1020
|
+
self.conn.executescript("""
|
|
1021
|
+
DROP TABLE IF EXISTS user_playbooks_fts;
|
|
1022
|
+
DROP TABLE IF EXISTS agent_playbooks_fts;
|
|
1023
|
+
DROP TABLE IF EXISTS user_playbooks;
|
|
1024
|
+
DROP TABLE IF EXISTS agent_playbooks;
|
|
1025
|
+
""")
|
|
1026
|
+
self.conn.commit()
|
|
1027
|
+
return
|
|
1028
|
+
|
|
1029
|
+
# Scenario 2: old FTS column name (feedback_content -> search_text)
|
|
1030
|
+
with self._lock:
|
|
1031
|
+
cur = self.conn.execute("PRAGMA table_info(user_playbooks_fts)")
|
|
1032
|
+
fts_columns = {row[1] for row in cur.fetchall()}
|
|
1033
|
+
|
|
1034
|
+
if fts_columns and "search_text" not in fts_columns:
|
|
1035
|
+
logger.warning(
|
|
1036
|
+
"Detected old FTS column name. "
|
|
1037
|
+
"Dropping FTS tables so they can be recreated with the new schema."
|
|
1038
|
+
)
|
|
1039
|
+
with self._lock:
|
|
1040
|
+
self.conn.executescript("""
|
|
1041
|
+
DROP TABLE IF EXISTS user_playbooks_fts;
|
|
1042
|
+
DROP TABLE IF EXISTS agent_playbooks_fts;
|
|
1043
|
+
""")
|
|
1044
|
+
self.conn.commit()
|
|
1045
|
+
|
|
1046
|
+
def _migrate_renamed_columns(self) -> None:
|
|
1047
|
+
"""Rename columns on tables affected by the feedback→playbook rename.
|
|
1048
|
+
|
|
1049
|
+
Handles: skills (feedback_name→playbook_name, raw_feedback_ids→user_playbook_ids),
|
|
1050
|
+
profiles (profile_content→content), playbook_aggregation_change_logs (feedback_name→playbook_name).
|
|
1051
|
+
|
|
1052
|
+
Since SQLite is used only for local development, we drop and recreate if needed.
|
|
1053
|
+
"""
|
|
1054
|
+
renames = [
|
|
1055
|
+
("skills", "feedback_name", "playbook_name"),
|
|
1056
|
+
("skills", "raw_feedback_ids", "user_playbook_ids"),
|
|
1057
|
+
("profiles", "profile_content", "content"),
|
|
1058
|
+
("playbook_aggregation_change_logs", "feedback_name", "playbook_name"),
|
|
1059
|
+
]
|
|
1060
|
+
|
|
1061
|
+
for table, old_col, new_col in renames:
|
|
1062
|
+
with self._lock:
|
|
1063
|
+
try:
|
|
1064
|
+
cols = {
|
|
1065
|
+
row[1]
|
|
1066
|
+
for row in self.conn.execute(
|
|
1067
|
+
f"PRAGMA table_info({table})"
|
|
1068
|
+
).fetchall() # noqa: S608
|
|
1069
|
+
}
|
|
1070
|
+
except Exception: # noqa: S112
|
|
1071
|
+
continue # Table doesn't exist yet
|
|
1072
|
+
|
|
1073
|
+
if not cols:
|
|
1074
|
+
continue # Table doesn't exist
|
|
1075
|
+
|
|
1076
|
+
if old_col in cols and new_col not in cols:
|
|
1077
|
+
logger.info(
|
|
1078
|
+
"Renaming column %s.%s -> %s",
|
|
1079
|
+
table,
|
|
1080
|
+
old_col,
|
|
1081
|
+
new_col,
|
|
1082
|
+
)
|
|
1083
|
+
try:
|
|
1084
|
+
self.conn.execute(
|
|
1085
|
+
f"ALTER TABLE {table} RENAME COLUMN {old_col} TO {new_col}" # noqa: S608
|
|
1086
|
+
)
|
|
1087
|
+
self.conn.commit()
|
|
1088
|
+
except Exception as e:
|
|
1089
|
+
logger.warning(
|
|
1090
|
+
"Could not rename %s.%s -> %s: %s. "
|
|
1091
|
+
"Dropping table so it can be recreated.",
|
|
1092
|
+
table,
|
|
1093
|
+
old_col,
|
|
1094
|
+
new_col,
|
|
1095
|
+
e,
|
|
1096
|
+
)
|
|
1097
|
+
self.conn.execute(f"DROP TABLE IF EXISTS {table}") # noqa: S608
|
|
1098
|
+
self.conn.commit()
|
|
1099
|
+
|
|
1100
|
+
def _migrate_expanded_terms(self) -> None:
|
|
1101
|
+
"""Add expanded_terms column if missing (for databases created before this feature)."""
|
|
1102
|
+
for table in ("profiles", "user_playbooks", "agent_playbooks"):
|
|
1103
|
+
cols = {
|
|
1104
|
+
row["name"]
|
|
1105
|
+
for row in self.conn.execute(f"PRAGMA table_info({table})").fetchall()
|
|
1106
|
+
}
|
|
1107
|
+
if "expanded_terms" not in cols:
|
|
1108
|
+
self.conn.execute(f"ALTER TABLE {table} ADD COLUMN expanded_terms TEXT")
|
|
1109
|
+
logger.info("Added expanded_terms column to %s", table)
|
|
1110
|
+
self.conn.commit()
|
|
1111
|
+
|
|
1112
|
+
def _migrate_agent_runs_schema(self) -> None:
|
|
1113
|
+
"""Add resumable-agent run columns if missing from existing SQLite DBs."""
|
|
1114
|
+
cols = {
|
|
1115
|
+
row["name"]
|
|
1116
|
+
for row in self.conn.execute("PRAGMA table_info(_agent_runs)").fetchall()
|
|
1117
|
+
}
|
|
1118
|
+
if not cols:
|
|
1119
|
+
return
|
|
1120
|
+
if "max_steps_remaining" not in cols:
|
|
1121
|
+
self.conn.execute(
|
|
1122
|
+
"ALTER TABLE _agent_runs ADD COLUMN max_steps_remaining INTEGER"
|
|
1123
|
+
)
|
|
1124
|
+
logger.info("Added max_steps_remaining column to _agent_runs")
|
|
1125
|
+
self.conn.commit()
|
|
1126
|
+
|
|
1127
|
+
def _migrate_pending_tool_calls_schema(self) -> None:
|
|
1128
|
+
"""Add pending-tool-call columns if missing from existing SQLite DBs."""
|
|
1129
|
+
cols = {
|
|
1130
|
+
row["name"]
|
|
1131
|
+
for row in self.conn.execute(
|
|
1132
|
+
"PRAGMA table_info(_pending_tool_calls)"
|
|
1133
|
+
).fetchall()
|
|
1134
|
+
}
|
|
1135
|
+
if not cols:
|
|
1136
|
+
return
|
|
1137
|
+
if "superseded_by" not in cols:
|
|
1138
|
+
self.conn.execute(
|
|
1139
|
+
"ALTER TABLE _pending_tool_calls ADD COLUMN superseded_by TEXT"
|
|
1140
|
+
)
|
|
1141
|
+
logger.info("Added superseded_by column to _pending_tool_calls")
|
|
1142
|
+
self.conn.commit()
|
|
1143
|
+
|
|
1144
|
+
def _migrate_agentic_signals(self) -> None:
|
|
1145
|
+
"""Add source_span/notes/reader_angle columns if missing.
|
|
1146
|
+
|
|
1147
|
+
Backfill-safe: columns are nullable with no default. Applies to both
|
|
1148
|
+
the profiles and user_playbooks tables — the agentic extraction
|
|
1149
|
+
pipeline populates them per-row; classic extraction leaves them NULL.
|
|
1150
|
+
"""
|
|
1151
|
+
for table in ("profiles", "user_playbooks"):
|
|
1152
|
+
cols = {
|
|
1153
|
+
row["name"]
|
|
1154
|
+
for row in self.conn.execute(f"PRAGMA table_info({table})").fetchall()
|
|
1155
|
+
}
|
|
1156
|
+
for col in ("source_span", "notes", "reader_angle"):
|
|
1157
|
+
if col not in cols:
|
|
1158
|
+
self.conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} TEXT") # noqa: S608
|
|
1159
|
+
logger.info("Added %s column to %s", col, table)
|
|
1160
|
+
self.conn.commit()
|
|
1161
|
+
|
|
1162
|
+
def _migrate_user_playbook_polarity(self) -> None:
|
|
1163
|
+
"""Drop the legacy ``polarity`` column from ``user_playbooks`` if present.
|
|
1164
|
+
|
|
1165
|
+
Polarity is retired under Option B (orientation lives in rule wording and
|
|
1166
|
+
is LLM-judged, never a stored field). This mirrors the Supabase drop
|
|
1167
|
+
migration and brings databases created while the column existed back in
|
|
1168
|
+
line with the current schema, which no longer defines it.
|
|
1169
|
+
"""
|
|
1170
|
+
cols = {
|
|
1171
|
+
row["name"]
|
|
1172
|
+
for row in self.conn.execute("PRAGMA table_info(user_playbooks)").fetchall()
|
|
1173
|
+
}
|
|
1174
|
+
if not cols:
|
|
1175
|
+
return
|
|
1176
|
+
if "polarity" in cols:
|
|
1177
|
+
self.conn.execute("ALTER TABLE user_playbooks DROP COLUMN polarity")
|
|
1178
|
+
logger.info("Dropped legacy polarity column from user_playbooks")
|
|
1179
|
+
self.conn.commit()
|
|
1180
|
+
|
|
1181
|
+
def _migrate_agent_playbook_source_windows(self) -> None:
|
|
1182
|
+
"""Add source window snapshots to existing agent source mappings."""
|
|
1183
|
+
cols = {
|
|
1184
|
+
row["name"]
|
|
1185
|
+
for row in self.conn.execute(
|
|
1186
|
+
"PRAGMA table_info(agent_playbook_source_user_playbooks)"
|
|
1187
|
+
).fetchall()
|
|
1188
|
+
}
|
|
1189
|
+
if not cols:
|
|
1190
|
+
return
|
|
1191
|
+
if "source_interaction_ids" not in cols:
|
|
1192
|
+
self.conn.execute(
|
|
1193
|
+
"ALTER TABLE agent_playbook_source_user_playbooks "
|
|
1194
|
+
"ADD COLUMN source_interaction_ids TEXT NOT NULL DEFAULT '[]'"
|
|
1195
|
+
)
|
|
1196
|
+
logger.info(
|
|
1197
|
+
"Added source_interaction_ids column to "
|
|
1198
|
+
"agent_playbook_source_user_playbooks"
|
|
1199
|
+
)
|
|
1200
|
+
self.conn.execute(
|
|
1201
|
+
"CREATE INDEX IF NOT EXISTS idx_apsup_user "
|
|
1202
|
+
"ON agent_playbook_source_user_playbooks(user_playbook_id)"
|
|
1203
|
+
)
|
|
1204
|
+
self.conn.commit()
|
|
1205
|
+
|
|
1206
|
+
def _migrate_request_metadata(self) -> None:
|
|
1207
|
+
"""Add metadata column to requests for databases created before F2.
|
|
1208
|
+
|
|
1209
|
+
The column stores a JSON-encoded dict per request (see Request.metadata
|
|
1210
|
+
in the Pydantic model). Backfill-safe: existing rows pick up the
|
|
1211
|
+
``'{}'`` default and round-trip as an empty dict.
|
|
1212
|
+
"""
|
|
1213
|
+
cols = {
|
|
1214
|
+
row["name"]
|
|
1215
|
+
for row in self.conn.execute("PRAGMA table_info(requests)").fetchall()
|
|
1216
|
+
}
|
|
1217
|
+
if not cols:
|
|
1218
|
+
return
|
|
1219
|
+
if "metadata" not in cols:
|
|
1220
|
+
self.conn.execute(
|
|
1221
|
+
"ALTER TABLE requests ADD COLUMN metadata TEXT NOT NULL DEFAULT '{}'"
|
|
1222
|
+
)
|
|
1223
|
+
logger.info("Added metadata column to requests")
|
|
1224
|
+
self.conn.commit()
|
|
1225
|
+
|
|
1226
|
+
def _migrate_shadow_comparison_verdicts(self) -> None:
|
|
1227
|
+
"""F1: create the shadow_comparison_verdicts table if missing.
|
|
1228
|
+
|
|
1229
|
+
Idempotent; safe to run on every startup. The PRAGMA-LBYL guard
|
|
1230
|
+
avoids running the CREATE statements on every boot for
|
|
1231
|
+
already-migrated DBs — mirrors the :meth:`_migrate_request_metadata`
|
|
1232
|
+
pattern. The ``CREATE TABLE IF NOT EXISTS`` in :data:`_DDL` will
|
|
1233
|
+
also create this table on a fresh database, so this helper is a
|
|
1234
|
+
no-op there; its purpose is explicit symmetry with the per-feature
|
|
1235
|
+
migration convention and a single named hook the disk/supabase
|
|
1236
|
+
backends in Tasks 6/7 can mirror.
|
|
1237
|
+
"""
|
|
1238
|
+
cur = self.conn.execute("PRAGMA table_info(shadow_comparison_verdicts)")
|
|
1239
|
+
cols = cur.fetchall()
|
|
1240
|
+
if cols:
|
|
1241
|
+
return
|
|
1242
|
+
self.conn.executescript("""
|
|
1243
|
+
CREATE TABLE IF NOT EXISTS shadow_comparison_verdicts (
|
|
1244
|
+
verdict_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1245
|
+
interaction_id TEXT NOT NULL,
|
|
1246
|
+
session_id TEXT NOT NULL,
|
|
1247
|
+
agent_version TEXT NOT NULL,
|
|
1248
|
+
reflexio_is_request_1 INTEGER NOT NULL,
|
|
1249
|
+
better_request TEXT NOT NULL CHECK (better_request IN ('1','2','tie')),
|
|
1250
|
+
is_significantly_better INTEGER NOT NULL,
|
|
1251
|
+
comparison_reason TEXT,
|
|
1252
|
+
judge_prompt_version TEXT NOT NULL,
|
|
1253
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
1254
|
+
);
|
|
1255
|
+
CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_session
|
|
1256
|
+
ON shadow_comparison_verdicts (session_id, agent_version);
|
|
1257
|
+
CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_created_at
|
|
1258
|
+
ON shadow_comparison_verdicts (created_at);
|
|
1259
|
+
CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_prompt_v
|
|
1260
|
+
ON shadow_comparison_verdicts (judge_prompt_version);
|
|
1261
|
+
""")
|
|
1262
|
+
self.conn.commit()
|
|
1263
|
+
logger.info("Created shadow_comparison_verdicts table (F1 migration)")
|
|
1264
|
+
|
|
1265
|
+
# ------------------------------------------------------------------
|
|
1266
|
+
# Internal helpers
|
|
1267
|
+
# ------------------------------------------------------------------
|
|
1268
|
+
|
|
1269
|
+
def _execute(
|
|
1270
|
+
self, sql: str, params: tuple[Any, ...] | list[Any] = ()
|
|
1271
|
+
) -> sqlite3.Cursor:
|
|
1272
|
+
with self._lock:
|
|
1273
|
+
cur = self.conn.execute(sql, params)
|
|
1274
|
+
self.conn.commit()
|
|
1275
|
+
return cur
|
|
1276
|
+
|
|
1277
|
+
def _fetchone(
|
|
1278
|
+
self, sql: str, params: tuple[Any, ...] | list[Any] = ()
|
|
1279
|
+
) -> sqlite3.Row | None:
|
|
1280
|
+
with self._lock:
|
|
1281
|
+
return self.conn.execute(sql, params).fetchone()
|
|
1282
|
+
|
|
1283
|
+
def _fetchall(
|
|
1284
|
+
self, sql: str, params: tuple[Any, ...] | list[Any] = ()
|
|
1285
|
+
) -> list[sqlite3.Row]:
|
|
1286
|
+
with self._lock:
|
|
1287
|
+
return self.conn.execute(sql, params).fetchall()
|
|
1288
|
+
|
|
1289
|
+
def _get_embedding(
|
|
1290
|
+
self, text: str, purpose: Literal["document", "query"] = "document"
|
|
1291
|
+
) -> list[float]:
|
|
1292
|
+
"""Generate an embedding with a purpose-specific prefix.
|
|
1293
|
+
|
|
1294
|
+
Args:
|
|
1295
|
+
text: The text to embed.
|
|
1296
|
+
purpose: Either ``"document"`` (stored embeddings) or ``"query"``
|
|
1297
|
+
(search-time embeddings). The prefix improves asymmetric
|
|
1298
|
+
retrieval quality for models that support it.
|
|
1299
|
+
|
|
1300
|
+
Returns:
|
|
1301
|
+
The embedding vector as a list of floats.
|
|
1302
|
+
"""
|
|
1303
|
+
prefix = "search_document: " if purpose == "document" else "search_query: "
|
|
1304
|
+
try:
|
|
1305
|
+
return self.llm_client.get_embedding(
|
|
1306
|
+
prefix + text, self.embedding_model_name, self.embedding_dimensions
|
|
1307
|
+
)
|
|
1308
|
+
except EmbeddingUnavailableError as exc:
|
|
1309
|
+
logger.warning(
|
|
1310
|
+
"Embedding unavailable for %s text; continuing without vector: %s",
|
|
1311
|
+
purpose,
|
|
1312
|
+
exc,
|
|
1313
|
+
)
|
|
1314
|
+
return []
|
|
1315
|
+
|
|
1316
|
+
def _should_expand_documents(self) -> bool:
|
|
1317
|
+
"""Check if document expansion is enabled."""
|
|
1318
|
+
return self._enable_document_expansion
|
|
1319
|
+
|
|
1320
|
+
def _expand_document(self, content: str) -> str | None:
|
|
1321
|
+
"""Expand document content with synonyms for FTS recall.
|
|
1322
|
+
|
|
1323
|
+
Uses DocumentExpander to generate synonym groups. Returns the
|
|
1324
|
+
expanded_terms string (e.g., "backup, sync; failure, error")
|
|
1325
|
+
or None on failure.
|
|
1326
|
+
|
|
1327
|
+
Args:
|
|
1328
|
+
content (str): Document text to expand
|
|
1329
|
+
|
|
1330
|
+
Returns:
|
|
1331
|
+
str or None: Expanded terms text, or None if expansion fails/disabled
|
|
1332
|
+
"""
|
|
1333
|
+
if not content:
|
|
1334
|
+
return None
|
|
1335
|
+
try:
|
|
1336
|
+
from reflexio.server.prompt.prompt_manager import PromptManager
|
|
1337
|
+
from reflexio.server.services.pre_retrieval import DocumentExpander
|
|
1338
|
+
|
|
1339
|
+
expander = DocumentExpander(
|
|
1340
|
+
llm_client=self.llm_client,
|
|
1341
|
+
prompt_manager=PromptManager(),
|
|
1342
|
+
)
|
|
1343
|
+
result = expander.expand(content)
|
|
1344
|
+
return result.expanded_text or None
|
|
1345
|
+
except Exception:
|
|
1346
|
+
logger.warning("Document expansion failed", exc_info=True)
|
|
1347
|
+
return None
|
|
1348
|
+
|
|
1349
|
+
def _current_timestamp(self) -> str:
|
|
1350
|
+
return datetime.now(UTC).isoformat()
|
|
1351
|
+
|
|
1352
|
+
# FTS helpers
|
|
1353
|
+
def _fts_upsert(self, table: str, rowid: int, **text_fields: str | None) -> None:
|
|
1354
|
+
"""Insert or update an FTS row. Deletes old entry first to avoid duplicates."""
|
|
1355
|
+
with self._lock:
|
|
1356
|
+
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1357
|
+
cols = list(text_fields.keys())
|
|
1358
|
+
vals = [text_fields[c] or "" for c in cols]
|
|
1359
|
+
placeholders = ",".join("?" for _ in cols)
|
|
1360
|
+
col_str = ",".join(cols)
|
|
1361
|
+
self.conn.execute(
|
|
1362
|
+
f"INSERT INTO {table}(rowid, {col_str}) VALUES (?, {placeholders})",
|
|
1363
|
+
[rowid, *vals],
|
|
1364
|
+
)
|
|
1365
|
+
self.conn.commit()
|
|
1366
|
+
|
|
1367
|
+
def _fts_delete(self, table: str, rowid: int) -> None:
|
|
1368
|
+
with self._lock:
|
|
1369
|
+
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1370
|
+
self.conn.commit()
|
|
1371
|
+
|
|
1372
|
+
def _fts_upsert_profile(self, profile_id: str, content: str) -> None:
|
|
1373
|
+
"""FTS for profiles uses profile_id TEXT as key column."""
|
|
1374
|
+
with self._lock:
|
|
1375
|
+
self.conn.execute(
|
|
1376
|
+
"DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
|
|
1377
|
+
)
|
|
1378
|
+
self.conn.execute(
|
|
1379
|
+
"INSERT INTO profiles_fts(profile_id, content) VALUES (?, ?)",
|
|
1380
|
+
(profile_id, content),
|
|
1381
|
+
)
|
|
1382
|
+
self.conn.commit()
|
|
1383
|
+
|
|
1384
|
+
def _fts_delete_profile(self, profile_id: str) -> None:
|
|
1385
|
+
with self._lock:
|
|
1386
|
+
self.conn.execute(
|
|
1387
|
+
"DELETE FROM profiles_fts WHERE profile_id = ?", (profile_id,)
|
|
1388
|
+
)
|
|
1389
|
+
self.conn.commit()
|
|
1390
|
+
|
|
1391
|
+
# Vec helpers (sqlite-vec)
|
|
1392
|
+
def _vec_upsert(self, table: str, rowid: int, embedding: list[float]) -> None:
|
|
1393
|
+
"""Insert or update a vec table row. No-op when sqlite-vec is unavailable."""
|
|
1394
|
+
if not self._has_sqlite_vec:
|
|
1395
|
+
return
|
|
1396
|
+
with self._lock:
|
|
1397
|
+
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1398
|
+
self.conn.execute(
|
|
1399
|
+
f"INSERT INTO {table}(rowid, embedding) VALUES (?, ?)",
|
|
1400
|
+
(rowid, json.dumps(embedding)),
|
|
1401
|
+
)
|
|
1402
|
+
self.conn.commit()
|
|
1403
|
+
|
|
1404
|
+
def _vec_delete(self, table: str, rowid: int) -> None:
|
|
1405
|
+
"""Delete a vec table row. No-op when sqlite-vec is unavailable."""
|
|
1406
|
+
if not self._has_sqlite_vec:
|
|
1407
|
+
return
|
|
1408
|
+
with self._lock:
|
|
1409
|
+
self.conn.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
|
1410
|
+
self.conn.commit()
|
|
1411
|
+
|
|
1412
|
+
def _vec_knn_search(
|
|
1413
|
+
self,
|
|
1414
|
+
vec_table: str,
|
|
1415
|
+
main_table: str,
|
|
1416
|
+
query_embedding: list[float],
|
|
1417
|
+
match_count: int,
|
|
1418
|
+
conditions: list[str] | None = None,
|
|
1419
|
+
params: list[Any] | None = None,
|
|
1420
|
+
) -> list[sqlite3.Row]:
|
|
1421
|
+
"""Run a native KNN search via sqlite-vec and join back to the main table.
|
|
1422
|
+
|
|
1423
|
+
Over-fetches from the KNN index (5x ``match_count``) so that post-filter
|
|
1424
|
+
WHERE conditions (org, user, status, etc.) don't silently reduce the
|
|
1425
|
+
result set below the requested count.
|
|
1426
|
+
|
|
1427
|
+
Args:
|
|
1428
|
+
vec_table: Name of the vec0 virtual table.
|
|
1429
|
+
main_table: Name of the main data table.
|
|
1430
|
+
query_embedding: Query embedding vector.
|
|
1431
|
+
match_count: Number of results to return.
|
|
1432
|
+
conditions: Optional WHERE conditions for the main table.
|
|
1433
|
+
params: Parameters for the conditions.
|
|
1434
|
+
|
|
1435
|
+
Returns:
|
|
1436
|
+
Up to ``match_count`` rows from the main table, ordered by vector
|
|
1437
|
+
distance (ascending).
|
|
1438
|
+
"""
|
|
1439
|
+
knn_overfetch = match_count * 5
|
|
1440
|
+
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
|
1441
|
+
sql = f"""SELECT m.* FROM {main_table} m
|
|
1442
|
+
JOIN (
|
|
1443
|
+
SELECT rowid, distance FROM {vec_table}
|
|
1444
|
+
WHERE embedding MATCH ?
|
|
1445
|
+
ORDER BY distance
|
|
1446
|
+
LIMIT ?
|
|
1447
|
+
) v ON m.rowid = v.rowid
|
|
1448
|
+
WHERE {where_clause}
|
|
1449
|
+
ORDER BY v.distance
|
|
1450
|
+
LIMIT ?"""
|
|
1451
|
+
all_params = [
|
|
1452
|
+
json.dumps(query_embedding),
|
|
1453
|
+
knn_overfetch,
|
|
1454
|
+
*(params or []),
|
|
1455
|
+
match_count,
|
|
1456
|
+
]
|
|
1457
|
+
return self._fetchall(sql, all_params)
|
|
1458
|
+
|
|
1459
|
+
# ------------------------------------------------------------------
|
|
1460
|
+
# Per-user data clear
|
|
1461
|
+
# ------------------------------------------------------------------
|
|
1462
|
+
|
|
1463
|
+
def clear_user_data(self, user_id: str) -> dict[str, int]:
|
|
1464
|
+
"""Atomic per-``user_id`` row deletion across all user-scoped tables.
|
|
1465
|
+
|
|
1466
|
+
Overrides the BaseStorage default with a single-transaction SQL
|
|
1467
|
+
implementation. Removes interactions, user playbooks, profiles,
|
|
1468
|
+
and requests scoped to the user. Intentionally does NOT touch
|
|
1469
|
+
``agent_playbooks`` — they are the cross-project rollup of
|
|
1470
|
+
skills and have no ``user_id`` column.
|
|
1471
|
+
|
|
1472
|
+
Also cleans up FTS and vector sidecars for the user's rows so
|
|
1473
|
+
subsequent searches don't surface deleted data.
|
|
1474
|
+
|
|
1475
|
+
Args:
|
|
1476
|
+
user_id (str): The user id whose rows should be deleted.
|
|
1477
|
+
|
|
1478
|
+
Returns:
|
|
1479
|
+
dict[str, int]: Per-entity deletion counts with keys
|
|
1480
|
+
``interactions``, ``user_playbooks``, ``profiles``, and
|
|
1481
|
+
``requests``.
|
|
1482
|
+
"""
|
|
1483
|
+
with self._lock:
|
|
1484
|
+
# Snapshot rowids/ids that need FTS or vector cleanup before
|
|
1485
|
+
# the DELETE removes them from the main tables.
|
|
1486
|
+
interaction_ids = [
|
|
1487
|
+
r["interaction_id"]
|
|
1488
|
+
for r in self.conn.execute(
|
|
1489
|
+
"SELECT interaction_id FROM interactions WHERE user_id = ?",
|
|
1490
|
+
(user_id,),
|
|
1491
|
+
).fetchall()
|
|
1492
|
+
]
|
|
1493
|
+
user_playbook_ids = [
|
|
1494
|
+
r["user_playbook_id"]
|
|
1495
|
+
for r in self.conn.execute(
|
|
1496
|
+
"SELECT user_playbook_id FROM user_playbooks WHERE user_id = ?",
|
|
1497
|
+
(user_id,),
|
|
1498
|
+
).fetchall()
|
|
1499
|
+
]
|
|
1500
|
+
profile_rows = self.conn.execute(
|
|
1501
|
+
"SELECT rowid, profile_id FROM profiles WHERE user_id = ?",
|
|
1502
|
+
(user_id,),
|
|
1503
|
+
).fetchall()
|
|
1504
|
+
profile_rowids = [r["rowid"] for r in profile_rows]
|
|
1505
|
+
profile_ids = [r["profile_id"] for r in profile_rows]
|
|
1506
|
+
|
|
1507
|
+
# FTS cleanup
|
|
1508
|
+
if interaction_ids:
|
|
1509
|
+
ph = ",".join("?" for _ in interaction_ids)
|
|
1510
|
+
self.conn.execute(
|
|
1511
|
+
f"DELETE FROM interactions_fts WHERE rowid IN ({ph})",
|
|
1512
|
+
interaction_ids,
|
|
1513
|
+
)
|
|
1514
|
+
if user_playbook_ids:
|
|
1515
|
+
ph = ",".join("?" for _ in user_playbook_ids)
|
|
1516
|
+
self.conn.execute(
|
|
1517
|
+
f"DELETE FROM user_playbooks_fts WHERE rowid IN ({ph})",
|
|
1518
|
+
user_playbook_ids,
|
|
1519
|
+
)
|
|
1520
|
+
if profile_ids:
|
|
1521
|
+
ph = ",".join("?" for _ in profile_ids)
|
|
1522
|
+
self.conn.execute(
|
|
1523
|
+
f"DELETE FROM profiles_fts WHERE profile_id IN ({ph})",
|
|
1524
|
+
profile_ids,
|
|
1525
|
+
)
|
|
1526
|
+
|
|
1527
|
+
# Vector index cleanup (best-effort: only if sqlite-vec loaded)
|
|
1528
|
+
if self._has_sqlite_vec:
|
|
1529
|
+
vec_targets = (
|
|
1530
|
+
("interactions_vec", interaction_ids),
|
|
1531
|
+
("user_playbooks_vec", user_playbook_ids),
|
|
1532
|
+
("profiles_vec", profile_rowids),
|
|
1533
|
+
)
|
|
1534
|
+
for vec_table, rowids in vec_targets:
|
|
1535
|
+
if rowids:
|
|
1536
|
+
ph = ",".join("?" for _ in rowids)
|
|
1537
|
+
self.conn.execute(
|
|
1538
|
+
f"DELETE FROM {vec_table} WHERE rowid IN ({ph})",
|
|
1539
|
+
rowids,
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
# Main-table deletes. Order matters only for foreign key
|
|
1543
|
+
# integrity; SQLite default has FK off for most tables here
|
|
1544
|
+
# so order is chosen for readability.
|
|
1545
|
+
interactions_cur = self.conn.execute(
|
|
1546
|
+
"DELETE FROM interactions WHERE user_id = ?", (user_id,)
|
|
1547
|
+
)
|
|
1548
|
+
user_playbooks_cur = self.conn.execute(
|
|
1549
|
+
"DELETE FROM user_playbooks WHERE user_id = ?", (user_id,)
|
|
1550
|
+
)
|
|
1551
|
+
profiles_cur = self.conn.execute(
|
|
1552
|
+
"DELETE FROM profiles WHERE user_id = ?", (user_id,)
|
|
1553
|
+
)
|
|
1554
|
+
requests_cur = self.conn.execute(
|
|
1555
|
+
"DELETE FROM requests WHERE user_id = ?", (user_id,)
|
|
1556
|
+
)
|
|
1557
|
+
self.conn.commit()
|
|
1558
|
+
|
|
1559
|
+
return {
|
|
1560
|
+
"interactions": interactions_cur.rowcount,
|
|
1561
|
+
"user_playbooks": user_playbooks_cur.rowcount,
|
|
1562
|
+
"profiles": profiles_cur.rowcount,
|
|
1563
|
+
"requests": requests_cur.rowcount,
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
# ---------------------------------------------------------------------------
|
|
1568
|
+
# DDL — table and FTS definitions
|
|
1569
|
+
# ---------------------------------------------------------------------------
|
|
1570
|
+
|
|
1571
|
+
_DDL = """
|
|
1572
|
+
CREATE TABLE IF NOT EXISTS profiles (
|
|
1573
|
+
profile_id TEXT PRIMARY KEY,
|
|
1574
|
+
user_id TEXT NOT NULL,
|
|
1575
|
+
content TEXT NOT NULL DEFAULT '',
|
|
1576
|
+
last_modified_timestamp INTEGER NOT NULL,
|
|
1577
|
+
generated_from_request_id TEXT NOT NULL DEFAULT '',
|
|
1578
|
+
profile_time_to_live TEXT NOT NULL DEFAULT 'infinity',
|
|
1579
|
+
expiration_timestamp INTEGER NOT NULL DEFAULT 4102444800,
|
|
1580
|
+
custom_features TEXT,
|
|
1581
|
+
embedding TEXT,
|
|
1582
|
+
source TEXT DEFAULT '',
|
|
1583
|
+
status TEXT,
|
|
1584
|
+
extractor_names TEXT,
|
|
1585
|
+
expanded_terms TEXT,
|
|
1586
|
+
source_span TEXT,
|
|
1587
|
+
notes TEXT,
|
|
1588
|
+
reader_angle TEXT,
|
|
1589
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
1590
|
+
);
|
|
1591
|
+
CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
|
|
1592
|
+
CREATE INDEX IF NOT EXISTS idx_profiles_status ON profiles(status);
|
|
1593
|
+
|
|
1594
|
+
CREATE TABLE IF NOT EXISTS interactions (
|
|
1595
|
+
interaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1596
|
+
user_id TEXT NOT NULL,
|
|
1597
|
+
content TEXT NOT NULL DEFAULT '',
|
|
1598
|
+
request_id TEXT NOT NULL,
|
|
1599
|
+
created_at TEXT NOT NULL,
|
|
1600
|
+
role TEXT NOT NULL DEFAULT 'User',
|
|
1601
|
+
user_action TEXT NOT NULL DEFAULT 'none',
|
|
1602
|
+
user_action_description TEXT NOT NULL DEFAULT '',
|
|
1603
|
+
interacted_image_url TEXT NOT NULL DEFAULT '',
|
|
1604
|
+
shadow_content TEXT NOT NULL DEFAULT '',
|
|
1605
|
+
expert_content TEXT NOT NULL DEFAULT '',
|
|
1606
|
+
tools_used TEXT,
|
|
1607
|
+
citations TEXT,
|
|
1608
|
+
embedding TEXT
|
|
1609
|
+
);
|
|
1610
|
+
CREATE INDEX IF NOT EXISTS idx_interactions_user_id ON interactions(user_id);
|
|
1611
|
+
CREATE INDEX IF NOT EXISTS idx_interactions_request_id ON interactions(request_id);
|
|
1612
|
+
CREATE INDEX IF NOT EXISTS idx_interactions_created_at ON interactions(created_at);
|
|
1613
|
+
|
|
1614
|
+
CREATE TABLE IF NOT EXISTS requests (
|
|
1615
|
+
request_id TEXT PRIMARY KEY,
|
|
1616
|
+
user_id TEXT NOT NULL,
|
|
1617
|
+
created_at TEXT NOT NULL,
|
|
1618
|
+
source TEXT NOT NULL DEFAULT '',
|
|
1619
|
+
agent_version TEXT NOT NULL DEFAULT '',
|
|
1620
|
+
session_id TEXT,
|
|
1621
|
+
metadata TEXT NOT NULL DEFAULT '{}'
|
|
1622
|
+
);
|
|
1623
|
+
CREATE INDEX IF NOT EXISTS idx_requests_user_id ON requests(user_id);
|
|
1624
|
+
CREATE INDEX IF NOT EXISTS idx_requests_session_id ON requests(session_id);
|
|
1625
|
+
CREATE INDEX IF NOT EXISTS idx_requests_created_at ON requests(created_at);
|
|
1626
|
+
|
|
1627
|
+
CREATE TABLE IF NOT EXISTS user_playbooks (
|
|
1628
|
+
user_playbook_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1629
|
+
user_id TEXT,
|
|
1630
|
+
playbook_name TEXT NOT NULL DEFAULT '',
|
|
1631
|
+
created_at TEXT NOT NULL,
|
|
1632
|
+
request_id TEXT NOT NULL,
|
|
1633
|
+
agent_version TEXT NOT NULL DEFAULT '',
|
|
1634
|
+
content TEXT NOT NULL DEFAULT '',
|
|
1635
|
+
trigger TEXT,
|
|
1636
|
+
rationale TEXT,
|
|
1637
|
+
blocking_issue TEXT,
|
|
1638
|
+
source_interaction_ids TEXT,
|
|
1639
|
+
status TEXT,
|
|
1640
|
+
source TEXT,
|
|
1641
|
+
embedding TEXT,
|
|
1642
|
+
expanded_terms TEXT,
|
|
1643
|
+
source_span TEXT,
|
|
1644
|
+
notes TEXT,
|
|
1645
|
+
reader_angle TEXT
|
|
1646
|
+
);
|
|
1647
|
+
CREATE INDEX IF NOT EXISTS idx_user_playbooks_playbook_name ON user_playbooks(playbook_name);
|
|
1648
|
+
CREATE INDEX IF NOT EXISTS idx_user_playbooks_agent_version ON user_playbooks(agent_version);
|
|
1649
|
+
CREATE INDEX IF NOT EXISTS idx_user_playbooks_status ON user_playbooks(status);
|
|
1650
|
+
CREATE INDEX IF NOT EXISTS idx_user_playbooks_created_at ON user_playbooks(created_at);
|
|
1651
|
+
|
|
1652
|
+
CREATE TABLE IF NOT EXISTS agent_playbooks (
|
|
1653
|
+
agent_playbook_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1654
|
+
playbook_name TEXT NOT NULL DEFAULT '',
|
|
1655
|
+
created_at TEXT NOT NULL,
|
|
1656
|
+
agent_version TEXT NOT NULL DEFAULT '',
|
|
1657
|
+
content TEXT NOT NULL DEFAULT '',
|
|
1658
|
+
trigger TEXT,
|
|
1659
|
+
rationale TEXT,
|
|
1660
|
+
blocking_issue TEXT,
|
|
1661
|
+
playbook_status TEXT NOT NULL DEFAULT 'pending',
|
|
1662
|
+
playbook_metadata TEXT NOT NULL DEFAULT '',
|
|
1663
|
+
embedding TEXT,
|
|
1664
|
+
expanded_terms TEXT,
|
|
1665
|
+
status TEXT
|
|
1666
|
+
);
|
|
1667
|
+
CREATE INDEX IF NOT EXISTS idx_agent_playbooks_playbook_name ON agent_playbooks(playbook_name);
|
|
1668
|
+
CREATE INDEX IF NOT EXISTS idx_agent_playbooks_agent_version ON agent_playbooks(agent_version);
|
|
1669
|
+
CREATE INDEX IF NOT EXISTS idx_agent_playbooks_status ON agent_playbooks(status);
|
|
1670
|
+
CREATE INDEX IF NOT EXISTS idx_agent_playbooks_created_at ON agent_playbooks(created_at);
|
|
1671
|
+
|
|
1672
|
+
CREATE TABLE IF NOT EXISTS agent_success_evaluation_result (
|
|
1673
|
+
result_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1674
|
+
session_id TEXT NOT NULL,
|
|
1675
|
+
agent_version TEXT NOT NULL DEFAULT '',
|
|
1676
|
+
evaluation_name TEXT,
|
|
1677
|
+
is_success INTEGER NOT NULL DEFAULT 0,
|
|
1678
|
+
failure_type TEXT,
|
|
1679
|
+
failure_reason TEXT,
|
|
1680
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
1681
|
+
regular_vs_shadow TEXT,
|
|
1682
|
+
number_of_correction_per_session INTEGER NOT NULL DEFAULT 0,
|
|
1683
|
+
user_turns_to_resolution INTEGER,
|
|
1684
|
+
is_escalated INTEGER NOT NULL DEFAULT 0,
|
|
1685
|
+
embedding TEXT
|
|
1686
|
+
);
|
|
1687
|
+
CREATE INDEX IF NOT EXISTS idx_eval_agent_version ON agent_success_evaluation_result(agent_version);
|
|
1688
|
+
CREATE INDEX IF NOT EXISTS idx_eval_created_at ON agent_success_evaluation_result(created_at);
|
|
1689
|
+
|
|
1690
|
+
CREATE TABLE IF NOT EXISTS profile_change_logs (
|
|
1691
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1692
|
+
user_id TEXT NOT NULL,
|
|
1693
|
+
request_id TEXT NOT NULL,
|
|
1694
|
+
created_at INTEGER NOT NULL,
|
|
1695
|
+
added_profiles TEXT NOT NULL DEFAULT '[]',
|
|
1696
|
+
removed_profiles TEXT NOT NULL DEFAULT '[]',
|
|
1697
|
+
mentioned_profiles TEXT NOT NULL DEFAULT '[]'
|
|
1698
|
+
);
|
|
1699
|
+
CREATE INDEX IF NOT EXISTS idx_pcl_user_id ON profile_change_logs(user_id);
|
|
1700
|
+
CREATE INDEX IF NOT EXISTS idx_pcl_created_at ON profile_change_logs(created_at);
|
|
1701
|
+
|
|
1702
|
+
CREATE TABLE IF NOT EXISTS playbook_aggregation_change_logs (
|
|
1703
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1704
|
+
created_at INTEGER NOT NULL,
|
|
1705
|
+
playbook_name TEXT NOT NULL,
|
|
1706
|
+
agent_version TEXT NOT NULL,
|
|
1707
|
+
run_mode TEXT NOT NULL,
|
|
1708
|
+
added_playbooks TEXT NOT NULL DEFAULT '[]',
|
|
1709
|
+
removed_playbooks TEXT NOT NULL DEFAULT '[]',
|
|
1710
|
+
updated_playbooks TEXT NOT NULL DEFAULT '[]'
|
|
1711
|
+
);
|
|
1712
|
+
CREATE INDEX IF NOT EXISTS idx_pacl_playbook_name ON playbook_aggregation_change_logs(playbook_name);
|
|
1713
|
+
CREATE INDEX IF NOT EXISTS idx_pacl_agent_version ON playbook_aggregation_change_logs(agent_version);
|
|
1714
|
+
|
|
1715
|
+
CREATE TABLE IF NOT EXISTS agent_playbook_source_user_playbooks (
|
|
1716
|
+
agent_playbook_id INTEGER NOT NULL,
|
|
1717
|
+
user_playbook_id INTEGER NOT NULL,
|
|
1718
|
+
source_interaction_ids TEXT NOT NULL DEFAULT '[]',
|
|
1719
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
1720
|
+
PRIMARY KEY (agent_playbook_id, user_playbook_id)
|
|
1721
|
+
);
|
|
1722
|
+
CREATE INDEX IF NOT EXISTS idx_apsup_agent ON agent_playbook_source_user_playbooks(agent_playbook_id);
|
|
1723
|
+
CREATE INDEX IF NOT EXISTS idx_apsup_user ON agent_playbook_source_user_playbooks(user_playbook_id);
|
|
1724
|
+
|
|
1725
|
+
CREATE TABLE IF NOT EXISTS playbook_optimization_jobs (
|
|
1726
|
+
job_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1727
|
+
target_kind TEXT NOT NULL,
|
|
1728
|
+
target_id INTEGER NOT NULL,
|
|
1729
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
1730
|
+
best_candidate_id INTEGER,
|
|
1731
|
+
successor_target_id INTEGER,
|
|
1732
|
+
decision_reason TEXT NOT NULL DEFAULT '',
|
|
1733
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
1734
|
+
created_at INTEGER NOT NULL,
|
|
1735
|
+
updated_at INTEGER NOT NULL
|
|
1736
|
+
);
|
|
1737
|
+
CREATE INDEX IF NOT EXISTS idx_poj_target ON playbook_optimization_jobs(target_kind, target_id);
|
|
1738
|
+
CREATE INDEX IF NOT EXISTS idx_poj_status ON playbook_optimization_jobs(status);
|
|
1739
|
+
|
|
1740
|
+
CREATE TABLE IF NOT EXISTS playbook_optimization_candidates (
|
|
1741
|
+
candidate_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1742
|
+
job_id INTEGER NOT NULL,
|
|
1743
|
+
candidate_index INTEGER NOT NULL DEFAULT 0,
|
|
1744
|
+
content TEXT NOT NULL,
|
|
1745
|
+
parent_candidate_ids TEXT NOT NULL DEFAULT '[]',
|
|
1746
|
+
aggregate_score REAL,
|
|
1747
|
+
is_winner INTEGER NOT NULL DEFAULT 0,
|
|
1748
|
+
created_at INTEGER NOT NULL
|
|
1749
|
+
);
|
|
1750
|
+
CREATE INDEX IF NOT EXISTS idx_poc_job ON playbook_optimization_candidates(job_id);
|
|
1751
|
+
|
|
1752
|
+
CREATE TABLE IF NOT EXISTS playbook_optimization_evaluations (
|
|
1753
|
+
evaluation_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1754
|
+
job_id INTEGER NOT NULL,
|
|
1755
|
+
candidate_id INTEGER NOT NULL,
|
|
1756
|
+
target_kind TEXT NOT NULL,
|
|
1757
|
+
target_id INTEGER NOT NULL,
|
|
1758
|
+
scenario_user_playbook_id INTEGER,
|
|
1759
|
+
source_interaction_ids TEXT NOT NULL DEFAULT '[]',
|
|
1760
|
+
score REAL NOT NULL DEFAULT 0.0,
|
|
1761
|
+
verdict TEXT NOT NULL DEFAULT 'tie',
|
|
1762
|
+
likert INTEGER NOT NULL DEFAULT 0,
|
|
1763
|
+
rationale TEXT NOT NULL DEFAULT '',
|
|
1764
|
+
asi_json TEXT NOT NULL DEFAULT '{}',
|
|
1765
|
+
incumbent_rollout_json TEXT NOT NULL DEFAULT '[]',
|
|
1766
|
+
candidate_rollout_json TEXT NOT NULL DEFAULT '[]',
|
|
1767
|
+
created_at INTEGER NOT NULL
|
|
1768
|
+
);
|
|
1769
|
+
CREATE INDEX IF NOT EXISTS idx_poe_job ON playbook_optimization_evaluations(job_id);
|
|
1770
|
+
CREATE INDEX IF NOT EXISTS idx_poe_candidate ON playbook_optimization_evaluations(candidate_id);
|
|
1771
|
+
|
|
1772
|
+
CREATE TABLE IF NOT EXISTS playbook_optimization_events (
|
|
1773
|
+
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1774
|
+
job_id INTEGER NOT NULL,
|
|
1775
|
+
event_type TEXT NOT NULL,
|
|
1776
|
+
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
1777
|
+
created_at INTEGER NOT NULL
|
|
1778
|
+
);
|
|
1779
|
+
CREATE INDEX IF NOT EXISTS idx_poev_job ON playbook_optimization_events(job_id);
|
|
1780
|
+
|
|
1781
|
+
CREATE TABLE IF NOT EXISTS _operation_state (
|
|
1782
|
+
service_name TEXT PRIMARY KEY,
|
|
1783
|
+
operation_state TEXT NOT NULL DEFAULT '{}',
|
|
1784
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
1785
|
+
);
|
|
1786
|
+
|
|
1787
|
+
CREATE TABLE IF NOT EXISTS _agent_runs (
|
|
1788
|
+
id TEXT PRIMARY KEY,
|
|
1789
|
+
org_id TEXT NOT NULL,
|
|
1790
|
+
extractor_kind TEXT NOT NULL,
|
|
1791
|
+
user_id TEXT,
|
|
1792
|
+
request_id TEXT NOT NULL,
|
|
1793
|
+
agent_version TEXT,
|
|
1794
|
+
source TEXT,
|
|
1795
|
+
source_interaction_ids TEXT NOT NULL DEFAULT '[]',
|
|
1796
|
+
window_start_interaction_id INTEGER,
|
|
1797
|
+
window_end_interaction_id INTEGER,
|
|
1798
|
+
extractor_config_hash TEXT,
|
|
1799
|
+
status TEXT NOT NULL,
|
|
1800
|
+
generation_request_snapshot TEXT NOT NULL DEFAULT '{}',
|
|
1801
|
+
service_config_snapshot TEXT,
|
|
1802
|
+
agent_context_snapshot TEXT,
|
|
1803
|
+
committed_output TEXT,
|
|
1804
|
+
pending_tool_call_ids TEXT NOT NULL DEFAULT '[]',
|
|
1805
|
+
max_steps_remaining INTEGER,
|
|
1806
|
+
resume_attempts INTEGER NOT NULL DEFAULT 0,
|
|
1807
|
+
finalization_attempts INTEGER NOT NULL DEFAULT 0,
|
|
1808
|
+
next_resume_at TEXT,
|
|
1809
|
+
claimed_by TEXT,
|
|
1810
|
+
claimed_at TEXT,
|
|
1811
|
+
agent_completed_at TEXT,
|
|
1812
|
+
finalized_at TEXT,
|
|
1813
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
1814
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
1815
|
+
expires_at TEXT,
|
|
1816
|
+
last_error TEXT
|
|
1817
|
+
);
|
|
1818
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_ready ON _agent_runs(status, next_resume_at, updated_at);
|
|
1819
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_binding ON _agent_runs(org_id, extractor_kind, user_id);
|
|
1820
|
+
|
|
1821
|
+
CREATE TABLE IF NOT EXISTS _pending_tool_calls (
|
|
1822
|
+
id TEXT PRIMARY KEY,
|
|
1823
|
+
org_id TEXT NOT NULL,
|
|
1824
|
+
user_id TEXT,
|
|
1825
|
+
scope TEXT NOT NULL DEFAULT '{}',
|
|
1826
|
+
scope_hash TEXT NOT NULL,
|
|
1827
|
+
tool_name TEXT NOT NULL,
|
|
1828
|
+
dedup_key TEXT NOT NULL,
|
|
1829
|
+
status TEXT NOT NULL,
|
|
1830
|
+
question_text TEXT NOT NULL,
|
|
1831
|
+
answer_format TEXT,
|
|
1832
|
+
args TEXT NOT NULL DEFAULT '{}',
|
|
1833
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
1834
|
+
result TEXT,
|
|
1835
|
+
embedding TEXT,
|
|
1836
|
+
superseded_by TEXT,
|
|
1837
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
1838
|
+
resolved_at TEXT,
|
|
1839
|
+
expires_at TEXT NOT NULL,
|
|
1840
|
+
cache_until TEXT NOT NULL,
|
|
1841
|
+
valid_until TEXT
|
|
1842
|
+
);
|
|
1843
|
+
CREATE INDEX IF NOT EXISTS idx_pending_tool_calls_active ON _pending_tool_calls(org_id, scope_hash, tool_name, dedup_key, status, cache_until);
|
|
1844
|
+
CREATE INDEX IF NOT EXISTS idx_pending_tool_calls_prior ON _pending_tool_calls(org_id, scope_hash, tool_name, status, valid_until);
|
|
1845
|
+
|
|
1846
|
+
CREATE TABLE IF NOT EXISTS _run_tool_dependencies (
|
|
1847
|
+
run_id TEXT NOT NULL,
|
|
1848
|
+
pending_tool_call_id TEXT NOT NULL,
|
|
1849
|
+
dependency_kind TEXT NOT NULL DEFAULT 'followup',
|
|
1850
|
+
resolved_at TEXT,
|
|
1851
|
+
consumed_at TEXT,
|
|
1852
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
1853
|
+
PRIMARY KEY (run_id, pending_tool_call_id),
|
|
1854
|
+
FOREIGN KEY (run_id) REFERENCES _agent_runs(id) ON DELETE CASCADE,
|
|
1855
|
+
FOREIGN KEY (pending_tool_call_id) REFERENCES _pending_tool_calls(id) ON DELETE CASCADE
|
|
1856
|
+
);
|
|
1857
|
+
CREATE INDEX IF NOT EXISTS idx_run_tool_dependencies_pending ON _run_tool_dependencies(pending_tool_call_id, resolved_at, consumed_at);
|
|
1858
|
+
CREATE INDEX IF NOT EXISTS idx_run_tool_dependencies_ready ON _run_tool_dependencies(run_id, resolved_at, consumed_at);
|
|
1859
|
+
|
|
1860
|
+
-- FTS5 virtual tables
|
|
1861
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS interactions_fts USING fts5(
|
|
1862
|
+
content, user_action_description,
|
|
1863
|
+
tokenize="porter unicode61"
|
|
1864
|
+
);
|
|
1865
|
+
|
|
1866
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS profiles_fts USING fts5(
|
|
1867
|
+
profile_id, content,
|
|
1868
|
+
tokenize="porter unicode61"
|
|
1869
|
+
);
|
|
1870
|
+
|
|
1871
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS user_playbooks_fts USING fts5(
|
|
1872
|
+
search_text,
|
|
1873
|
+
tokenize="porter unicode61"
|
|
1874
|
+
);
|
|
1875
|
+
|
|
1876
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS agent_playbooks_fts USING fts5(
|
|
1877
|
+
search_text,
|
|
1878
|
+
tokenize="porter unicode61"
|
|
1879
|
+
);
|
|
1880
|
+
|
|
1881
|
+
CREATE TABLE IF NOT EXISTS share_links (
|
|
1882
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1883
|
+
org_id TEXT NOT NULL,
|
|
1884
|
+
token TEXT NOT NULL UNIQUE,
|
|
1885
|
+
resource_type TEXT NOT NULL,
|
|
1886
|
+
resource_id TEXT NOT NULL,
|
|
1887
|
+
created_at INTEGER NOT NULL,
|
|
1888
|
+
expires_at INTEGER,
|
|
1889
|
+
created_by_email TEXT
|
|
1890
|
+
);
|
|
1891
|
+
CREATE INDEX IF NOT EXISTS idx_share_links_resource ON share_links(resource_type, resource_id);
|
|
1892
|
+
|
|
1893
|
+
-- ============================================================================
|
|
1894
|
+
-- Braintrust connector (Plan C-backend)
|
|
1895
|
+
-- ============================================================================
|
|
1896
|
+
|
|
1897
|
+
CREATE TABLE IF NOT EXISTS braintrust_connection (
|
|
1898
|
+
org_id TEXT PRIMARY KEY,
|
|
1899
|
+
api_key_enc TEXT NOT NULL,
|
|
1900
|
+
workspace_id TEXT NOT NULL,
|
|
1901
|
+
workspace_name TEXT NOT NULL DEFAULT '',
|
|
1902
|
+
project_ids TEXT NOT NULL DEFAULT '[]',
|
|
1903
|
+
last_sync_ts INTEGER,
|
|
1904
|
+
last_error TEXT
|
|
1905
|
+
);
|
|
1906
|
+
|
|
1907
|
+
CREATE TABLE IF NOT EXISTS imported_score (
|
|
1908
|
+
rowid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1909
|
+
org_id TEXT NOT NULL,
|
|
1910
|
+
source TEXT NOT NULL,
|
|
1911
|
+
source_run_id TEXT NOT NULL,
|
|
1912
|
+
session_id TEXT,
|
|
1913
|
+
scorer_name TEXT NOT NULL,
|
|
1914
|
+
value REAL NOT NULL,
|
|
1915
|
+
ts INTEGER NOT NULL,
|
|
1916
|
+
UNIQUE (org_id, source, source_run_id, scorer_name)
|
|
1917
|
+
);
|
|
1918
|
+
CREATE INDEX IF NOT EXISTS idx_imported_score_session
|
|
1919
|
+
ON imported_score (org_id, session_id) WHERE session_id IS NOT NULL;
|
|
1920
|
+
CREATE INDEX IF NOT EXISTS idx_imported_score_ts ON imported_score (org_id, ts);
|
|
1921
|
+
|
|
1922
|
+
-- ============================================================================
|
|
1923
|
+
-- Per-turn shadow comparison verdicts (F1)
|
|
1924
|
+
-- ============================================================================
|
|
1925
|
+
|
|
1926
|
+
CREATE TABLE IF NOT EXISTS shadow_comparison_verdicts (
|
|
1927
|
+
verdict_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1928
|
+
interaction_id TEXT NOT NULL,
|
|
1929
|
+
session_id TEXT NOT NULL,
|
|
1930
|
+
agent_version TEXT NOT NULL,
|
|
1931
|
+
reflexio_is_request_1 INTEGER NOT NULL,
|
|
1932
|
+
better_request TEXT NOT NULL CHECK (better_request IN ('1','2','tie')),
|
|
1933
|
+
is_significantly_better INTEGER NOT NULL,
|
|
1934
|
+
comparison_reason TEXT,
|
|
1935
|
+
judge_prompt_version TEXT NOT NULL,
|
|
1936
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
1937
|
+
);
|
|
1938
|
+
CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_session
|
|
1939
|
+
ON shadow_comparison_verdicts (session_id, agent_version);
|
|
1940
|
+
CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_created_at
|
|
1941
|
+
ON shadow_comparison_verdicts (created_at);
|
|
1942
|
+
CREATE INDEX IF NOT EXISTS idx_shadow_verdicts_prompt_v
|
|
1943
|
+
ON shadow_comparison_verdicts (judge_prompt_version);
|
|
1944
|
+
|
|
1945
|
+
"""
|