claude-smart 0.2.41 → 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.
Files changed (396) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/README.md +1 -1
  3. package/bin/claude-smart.js +86 -48
  4. package/package.json +10 -3
  5. package/plugin/.claude-plugin/plugin.json +9 -3
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +2 -2
  8. package/plugin/dashboard/next.config.ts +9 -1
  9. package/plugin/pyproject.toml +2 -2
  10. package/plugin/scripts/_lib.sh +91 -0
  11. package/plugin/scripts/backend-service.sh +46 -15
  12. package/plugin/scripts/cli.sh +29 -1
  13. package/plugin/scripts/codex-hook.js +72 -4
  14. package/plugin/scripts/dashboard-build.sh +1 -0
  15. package/plugin/scripts/dashboard-service.sh +1 -0
  16. package/plugin/scripts/ensure-plugin-root.sh +7 -14
  17. package/plugin/scripts/hook_entry.sh +1 -0
  18. package/plugin/scripts/smart-install.sh +18 -2
  19. package/plugin/src/claude_smart/cli.py +72 -38
  20. package/plugin/src/claude_smart/context_format.py +11 -12
  21. package/plugin/src/claude_smart/cs_cite.py +26 -12
  22. package/plugin/src/claude_smart/ids.py +13 -5
  23. package/plugin/uv.lock +1 -1
  24. package/plugin/vendor/reflexio/.env.example +53 -0
  25. package/plugin/vendor/reflexio/LICENSE +201 -0
  26. package/plugin/vendor/reflexio/README.md +338 -0
  27. package/plugin/vendor/reflexio/pyproject.toml +271 -0
  28. package/plugin/vendor/reflexio/reflexio/README.md +184 -0
  29. package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
  30. package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
  31. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
  32. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
  33. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
  34. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
  35. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
  36. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
  37. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
  38. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
  39. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
  40. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
  41. package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
  42. package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
  43. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
  44. package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
  45. package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
  46. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +266 -0
  47. package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
  48. package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
  49. package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
  50. package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
  51. package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
  52. package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
  53. package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
  54. package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +961 -0
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
  63. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
  64. package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
  65. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
  66. package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
  67. package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
  68. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
  69. package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
  70. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
  71. package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
  72. package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
  73. package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
  74. package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
  75. package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
  145. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
  146. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
  147. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
  148. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
  149. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
  150. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
  151. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
  152. package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
  153. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
  154. package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
  155. package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
  156. package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
  157. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
  158. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
  159. package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
  160. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
  161. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
  162. package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
  163. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
  164. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
  165. package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
  166. package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
  167. package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
  168. package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
  169. package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
  170. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
  171. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1103 -0
  172. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
  173. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
  174. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
  175. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
  176. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +766 -0
  177. package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
  178. package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
  179. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
  180. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
  181. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
  182. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
  183. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +280 -0
  184. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
  185. package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
  186. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
  187. package/plugin/vendor/reflexio/reflexio/server/README.md +616 -0
  188. package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
  189. package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
  190. package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
  191. package/plugin/vendor/reflexio/reflexio/server/api.py +2714 -0
  192. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
  193. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
  194. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
  195. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
  196. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +540 -0
  197. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
  198. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
  199. package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
  200. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
  201. package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
  202. package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
  203. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +110 -0
  204. package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
  205. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
  206. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
  207. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -0
  208. package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
  209. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
  210. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
  211. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +210 -0
  212. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
  213. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +255 -0
  214. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
  215. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +177 -0
  216. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
  217. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -0
  218. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
  219. package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
  220. package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
  221. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
  222. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
  223. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
  224. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
  225. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
  226. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
  227. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
  228. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
  229. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
  230. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
  231. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
  232. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
  233. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
  234. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
  235. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
  236. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
  237. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
  238. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
  239. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
  240. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
  241. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
  242. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
  243. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
  244. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
  245. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
  246. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +279 -0
  247. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
  248. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
  249. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
  250. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
  251. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
  252. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +127 -0
  253. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
  254. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
  255. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
  256. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
  257. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
  258. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
  259. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
  260. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
  261. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
  262. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
  263. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
  264. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
  265. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
  266. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
  267. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
  268. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
  269. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
  270. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
  271. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
  272. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
  273. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
  274. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
  275. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
  276. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
  277. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
  278. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
  279. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
  281. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
  282. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +340 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1626 -0
  289. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
  290. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
  292. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
  296. package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
  297. package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
  311. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
  312. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +351 -0
  313. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +468 -0
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +777 -0
  318. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
  319. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
  320. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
  321. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -0
  322. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
  323. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
  324. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
  325. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +960 -0
  326. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
  327. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
  328. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
  329. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
  330. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
  331. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
  332. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
  333. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
  334. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
  335. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
  336. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
  337. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
  338. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
  339. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
  340. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
  341. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
  342. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +741 -0
  343. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
  344. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +734 -0
  345. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
  346. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
  347. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
  348. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +800 -0
  349. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
  350. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
  351. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +70 -0
  352. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
  353. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -0
  354. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
  355. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
  356. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
  357. package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
  358. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
  359. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
  360. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
  361. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
  362. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1253 -0
  363. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
  364. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
  365. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
  366. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
  367. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
  368. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
  369. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
  370. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
  371. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
  372. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
  373. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +372 -0
  374. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
  375. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
  376. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
  377. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
  378. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
  379. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
  380. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
  381. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
  382. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
  383. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +568 -0
  384. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
  385. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
  386. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
  387. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
  388. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
  389. package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
  390. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
  391. package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
  392. package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
  393. package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
  394. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
  395. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
  396. package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
@@ -0,0 +1,747 @@
1
+ """Profile and interaction CRUD + search mixins for SQLite storage."""
2
+
3
+ import logging
4
+ import sqlite3
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from typing import Any
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ from reflexio.models.api_schema.retriever_schema import (
11
+ SearchInteractionRequest,
12
+ SearchUserProfileRequest,
13
+ )
14
+ from reflexio.models.api_schema.service_schemas import (
15
+ DeleteUserInteractionRequest,
16
+ DeleteUserProfileRequest,
17
+ Interaction,
18
+ Status,
19
+ UserProfile,
20
+ )
21
+ from reflexio.models.config_schema import SearchMode
22
+ from reflexio.server.llm.providers.embedding_service_provider import (
23
+ EmbeddingUnavailableError,
24
+ )
25
+
26
+ from ._base import (
27
+ SQLiteStorageBase,
28
+ _build_status_sql,
29
+ _effective_search_mode,
30
+ _epoch_now,
31
+ _epoch_to_iso,
32
+ _iso_now,
33
+ _json_dumps,
34
+ _row_to_interaction,
35
+ _row_to_profile,
36
+ _sanitize_fts_query,
37
+ _true_rrf_merge,
38
+ _vector_rank_rows,
39
+ )
40
+
41
+
42
+ class ProfileMixin:
43
+ """Mixin providing profile and interaction CRUD + search."""
44
+
45
+ # Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
46
+ _lock: Any
47
+ conn: sqlite3.Connection
48
+ _execute: Any
49
+ _fetchone: Any
50
+ _fetchall: Any
51
+ _get_embedding: Any
52
+ _should_expand_documents: Any
53
+ _expand_document: Any
54
+ _fts_upsert: Any
55
+ _fts_delete: Any
56
+ _fts_upsert_profile: Any
57
+ _fts_delete_profile: Any
58
+ _vec_upsert: Any
59
+ _vec_delete: Any
60
+ _has_sqlite_vec: bool
61
+ llm_client: Any
62
+ embedding_model_name: str
63
+ embedding_dimensions: int
64
+
65
+ # ------------------------------------------------------------------
66
+ # CRUD — Profiles
67
+ # ------------------------------------------------------------------
68
+
69
+ @SQLiteStorageBase.handle_exceptions
70
+ def get_all_profiles(
71
+ self,
72
+ limit: int = 100,
73
+ status_filter: list[Status | None] | None = None,
74
+ ) -> list[UserProfile]:
75
+ if status_filter is None:
76
+ status_filter = [None]
77
+ frag, params = _build_status_sql(status_filter)
78
+ sql = f"SELECT * FROM profiles WHERE {frag} ORDER BY last_modified_timestamp DESC LIMIT ?"
79
+ params.append(limit)
80
+ return [_row_to_profile(r) for r in self._fetchall(sql, params)]
81
+
82
+ @SQLiteStorageBase.handle_exceptions
83
+ def get_user_profile(
84
+ self,
85
+ user_id: str,
86
+ status_filter: list[Status | None] | None = None,
87
+ ) -> list[UserProfile]:
88
+ if status_filter is None:
89
+ status_filter = [None]
90
+ current_ts = _epoch_now()
91
+ frag, params = _build_status_sql(status_filter)
92
+ sql = f"SELECT * FROM profiles WHERE user_id = ? AND expiration_timestamp >= ? AND {frag}"
93
+ all_params: list[Any] = [user_id, current_ts, *params]
94
+ return [_row_to_profile(r) for r in self._fetchall(sql, all_params)]
95
+
96
+ @SQLiteStorageBase.handle_exceptions
97
+ def add_user_profile(self, user_id: str, user_profiles: list[UserProfile]) -> None: # noqa: ARG002
98
+ for profile in user_profiles:
99
+ embedding_text = "\n".join([profile.content, str(profile.custom_features)])
100
+ if self._should_expand_documents():
101
+ with ThreadPoolExecutor(max_workers=2) as executor:
102
+ emb_future = executor.submit(self._get_embedding, embedding_text)
103
+ exp_future = executor.submit(self._expand_document, profile.content)
104
+ profile.embedding = emb_future.result(timeout=15)
105
+ profile.expanded_terms = exp_future.result(timeout=15)
106
+ else:
107
+ profile.embedding = self._get_embedding(embedding_text)
108
+ embedding = profile.embedding
109
+ self._execute(
110
+ """INSERT OR REPLACE INTO profiles
111
+ (profile_id, user_id, content, last_modified_timestamp,
112
+ generated_from_request_id, profile_time_to_live,
113
+ expiration_timestamp, custom_features, embedding, source,
114
+ status, extractor_names, expanded_terms,
115
+ source_span, notes, reader_angle, created_at)
116
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
117
+ (
118
+ profile.profile_id,
119
+ profile.user_id,
120
+ profile.content,
121
+ profile.last_modified_timestamp,
122
+ profile.generated_from_request_id,
123
+ profile.profile_time_to_live.value,
124
+ profile.expiration_timestamp,
125
+ _json_dumps(profile.custom_features),
126
+ _json_dumps(profile.embedding),
127
+ profile.source,
128
+ profile.status.value if profile.status else None,
129
+ _json_dumps(profile.extractor_names),
130
+ profile.expanded_terms,
131
+ profile.source_span,
132
+ profile.notes,
133
+ profile.reader_angle,
134
+ _iso_now(),
135
+ ),
136
+ )
137
+ fts_parts = [profile.content or ""]
138
+ if profile.custom_features:
139
+ fts_parts.extend(str(v) for v in profile.custom_features.values() if v)
140
+ if profile.expanded_terms:
141
+ fts_parts.append(profile.expanded_terms)
142
+ self._fts_upsert_profile(profile.profile_id, " ".join(fts_parts))
143
+ # Sync vec table — look up implicit rowid via primary key
144
+ row = self._fetchone(
145
+ "SELECT rowid FROM profiles WHERE profile_id = ?",
146
+ (profile.profile_id,),
147
+ )
148
+ if row and embedding:
149
+ self._vec_upsert("profiles_vec", row["rowid"], embedding)
150
+
151
+ @SQLiteStorageBase.handle_exceptions
152
+ def update_user_profile_by_id(
153
+ self, user_id: str, profile_id: str, new_profile: UserProfile
154
+ ) -> None:
155
+ current_ts = _epoch_now()
156
+ row = self._fetchone(
157
+ "SELECT profile_id FROM profiles WHERE user_id = ? AND profile_id = ? AND expiration_timestamp >= ?",
158
+ (user_id, profile_id, current_ts),
159
+ )
160
+ if not row:
161
+ import logging
162
+
163
+ logger = logging.getLogger(__name__)
164
+ logger.warning("User profile not found for user id: %s", user_id)
165
+ return
166
+ embedding = self._get_embedding(
167
+ "\n".join([new_profile.content, str(new_profile.custom_features)])
168
+ )
169
+ new_profile.embedding = embedding
170
+ self._execute(
171
+ """UPDATE profiles SET content=?, last_modified_timestamp=?,
172
+ generated_from_request_id=?, profile_time_to_live=?,
173
+ expiration_timestamp=?, custom_features=?, embedding=?,
174
+ source=?, status=?, extractor_names=?, expanded_terms=?,
175
+ source_span=?, notes=?, reader_angle=?
176
+ WHERE profile_id=?""",
177
+ (
178
+ new_profile.content,
179
+ new_profile.last_modified_timestamp,
180
+ new_profile.generated_from_request_id,
181
+ new_profile.profile_time_to_live.value,
182
+ new_profile.expiration_timestamp,
183
+ _json_dumps(new_profile.custom_features),
184
+ _json_dumps(new_profile.embedding),
185
+ new_profile.source,
186
+ new_profile.status.value if new_profile.status else None,
187
+ _json_dumps(new_profile.extractor_names),
188
+ new_profile.expanded_terms,
189
+ new_profile.source_span,
190
+ new_profile.notes,
191
+ new_profile.reader_angle,
192
+ profile_id,
193
+ ),
194
+ )
195
+ fts_parts = [new_profile.content or ""]
196
+ if new_profile.custom_features:
197
+ fts_parts.extend(str(v) for v in new_profile.custom_features.values() if v)
198
+ if new_profile.expanded_terms:
199
+ fts_parts.append(new_profile.expanded_terms)
200
+ self._fts_upsert_profile(profile_id, " ".join(fts_parts))
201
+ rowid_row = self._fetchone(
202
+ "SELECT rowid FROM profiles WHERE profile_id = ?", (profile_id,)
203
+ )
204
+ if rowid_row and embedding:
205
+ self._vec_upsert("profiles_vec", rowid_row["rowid"], embedding)
206
+
207
+ @SQLiteStorageBase.handle_exceptions
208
+ def delete_user_profile(self, request: DeleteUserProfileRequest) -> None:
209
+ rowid_row = self._fetchone(
210
+ "SELECT rowid FROM profiles WHERE profile_id = ?",
211
+ (request.profile_id,),
212
+ )
213
+ self._fts_delete_profile(request.profile_id)
214
+ if rowid_row:
215
+ self._vec_delete("profiles_vec", rowid_row["rowid"])
216
+ self._execute(
217
+ "DELETE FROM profiles WHERE user_id = ? AND profile_id = ?",
218
+ (request.user_id, request.profile_id),
219
+ )
220
+
221
+ @SQLiteStorageBase.handle_exceptions
222
+ def delete_all_profiles_for_user(self, user_id: str) -> None:
223
+ pids = [
224
+ r["profile_id"]
225
+ for r in self._fetchall(
226
+ "SELECT profile_id FROM profiles WHERE user_id = ?", (user_id,)
227
+ )
228
+ ]
229
+ for pid in pids:
230
+ self._fts_delete_profile(pid)
231
+ self._execute("DELETE FROM profiles WHERE user_id = ?", (user_id,))
232
+
233
+ @SQLiteStorageBase.handle_exceptions
234
+ def delete_all_profiles(self) -> None:
235
+ with self._lock:
236
+ self.conn.execute("DELETE FROM profiles_fts")
237
+ self.conn.execute("DELETE FROM profiles")
238
+ self.conn.commit()
239
+
240
+ @SQLiteStorageBase.handle_exceptions
241
+ def count_all_profiles(self) -> int:
242
+ row = self._fetchone("SELECT COUNT(*) as cnt FROM profiles")
243
+ return row["cnt"] if row else 0
244
+
245
+ @SQLiteStorageBase.handle_exceptions
246
+ def update_all_profiles_status(
247
+ self,
248
+ old_status: Status | None,
249
+ new_status: Status | None,
250
+ user_ids: list[str] | None = None,
251
+ ) -> int:
252
+ new_val = new_status.value if new_status else None
253
+ now_ts = _epoch_now()
254
+ params: list[Any] = [new_val, now_ts]
255
+
256
+ if old_status is None or (
257
+ hasattr(old_status, "value") and old_status.value is None
258
+ ):
259
+ where = "status IS NULL"
260
+ else:
261
+ where = "status = ?"
262
+ params.append(old_status.value)
263
+
264
+ if user_ids is not None:
265
+ placeholders = ",".join("?" for _ in user_ids)
266
+ where += f" AND user_id IN ({placeholders})"
267
+ params.extend(user_ids)
268
+
269
+ cur = self._execute(
270
+ f"UPDATE profiles SET status = ?, last_modified_timestamp = ? WHERE {where}",
271
+ params,
272
+ )
273
+ return cur.rowcount
274
+
275
+ @SQLiteStorageBase.handle_exceptions
276
+ def get_profiles_by_ids(
277
+ self,
278
+ user_id: str,
279
+ profile_ids: list[str],
280
+ status_filter: list[Status | None] | None = None,
281
+ ) -> list[UserProfile]:
282
+ if not profile_ids:
283
+ return []
284
+ if status_filter is None:
285
+ status_filter = [None]
286
+ current_ts = _epoch_now()
287
+ frag, sparams = _build_status_sql(status_filter)
288
+ ph = ",".join("?" for _ in profile_ids)
289
+ sql = (
290
+ f"SELECT * FROM profiles "
291
+ f"WHERE user_id = ? AND profile_id IN ({ph}) "
292
+ f"AND expiration_timestamp >= ? AND {frag}"
293
+ )
294
+ params: list[Any] = [user_id, *profile_ids, current_ts, *sparams]
295
+ return [_row_to_profile(r) for r in self._fetchall(sql, params)]
296
+
297
+ @SQLiteStorageBase.handle_exceptions
298
+ def archive_profile_by_id(self, user_id: str, profile_id: str) -> bool:
299
+ cur = self._execute(
300
+ "UPDATE profiles SET status = ?, last_modified_timestamp = ? "
301
+ "WHERE profile_id = ? AND user_id = ? AND status IS NULL",
302
+ (Status.ARCHIVED.value, _epoch_now(), profile_id, user_id),
303
+ )
304
+ return cur.rowcount > 0
305
+
306
+ @SQLiteStorageBase.handle_exceptions
307
+ def delete_all_profiles_by_status(self, status: Status) -> int:
308
+ # Clean up FTS for profiles being deleted
309
+ pids = [
310
+ r["profile_id"]
311
+ for r in self._fetchall(
312
+ "SELECT profile_id FROM profiles WHERE status = ?", (status.value,)
313
+ )
314
+ ]
315
+ for pid in pids:
316
+ self._fts_delete_profile(pid)
317
+ cur = self._execute("DELETE FROM profiles WHERE status = ?", (status.value,))
318
+ return cur.rowcount
319
+
320
+ @SQLiteStorageBase.handle_exceptions
321
+ def get_user_ids_with_status(self, status: Status | None) -> list[str]:
322
+ if status is None or (hasattr(status, "value") and status.value is None):
323
+ rows = self._fetchall(
324
+ "SELECT DISTINCT user_id FROM profiles WHERE status IS NULL"
325
+ )
326
+ else:
327
+ rows = self._fetchall(
328
+ "SELECT DISTINCT user_id FROM profiles WHERE status = ?",
329
+ (status.value,),
330
+ )
331
+ return [r["user_id"] for r in rows]
332
+
333
+ @SQLiteStorageBase.handle_exceptions
334
+ def delete_profiles_by_ids(self, profile_ids: list[str]) -> int:
335
+ if not profile_ids:
336
+ return 0
337
+ for pid in profile_ids:
338
+ self._fts_delete_profile(pid)
339
+ ph = ",".join("?" for _ in profile_ids)
340
+ cur = self._execute(
341
+ f"DELETE FROM profiles WHERE profile_id IN ({ph})", profile_ids
342
+ )
343
+ return cur.rowcount
344
+
345
+ # ------------------------------------------------------------------
346
+ # CRUD — Interactions
347
+ # ------------------------------------------------------------------
348
+
349
+ @SQLiteStorageBase.handle_exceptions
350
+ def get_all_interactions(self, limit: int = 100) -> list[Interaction]:
351
+ rows = self._fetchall(
352
+ "SELECT * FROM interactions ORDER BY created_at DESC LIMIT ?", (limit,)
353
+ )
354
+ return [_row_to_interaction(r) for r in rows]
355
+
356
+ @SQLiteStorageBase.handle_exceptions
357
+ def get_user_interaction(self, user_id: str) -> list[Interaction]:
358
+ rows = self._fetchall(
359
+ "SELECT * FROM interactions WHERE user_id = ?", (user_id,)
360
+ )
361
+ return [_row_to_interaction(r) for r in rows]
362
+
363
+ @SQLiteStorageBase.handle_exceptions
364
+ def add_user_interaction(self, user_id: str, interaction: Interaction) -> None: # noqa: ARG002
365
+ embedding = self._get_embedding(
366
+ f"{interaction.content}\n{interaction.user_action_description}"
367
+ )
368
+ interaction.embedding = embedding
369
+ self._insert_interaction(interaction)
370
+
371
+ def _insert_interaction(self, interaction: Interaction) -> int:
372
+ created_at_iso = _epoch_to_iso(interaction.created_at)
373
+ with self._lock:
374
+ if interaction.interaction_id:
375
+ self.conn.execute(
376
+ """INSERT OR REPLACE INTO interactions
377
+ (interaction_id, user_id, content, request_id, created_at,
378
+ role, user_action, user_action_description,
379
+ interacted_image_url, shadow_content, expert_content,
380
+ tools_used, citations, embedding)
381
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
382
+ (
383
+ interaction.interaction_id,
384
+ interaction.user_id,
385
+ interaction.content,
386
+ interaction.request_id,
387
+ created_at_iso,
388
+ interaction.role,
389
+ interaction.user_action.value,
390
+ interaction.user_action_description,
391
+ interaction.interacted_image_url,
392
+ interaction.shadow_content,
393
+ interaction.expert_content,
394
+ _json_dumps([t.model_dump() for t in interaction.tools_used]),
395
+ _json_dumps([c.model_dump() for c in interaction.citations]),
396
+ _json_dumps(interaction.embedding),
397
+ ),
398
+ )
399
+ iid = interaction.interaction_id
400
+ else:
401
+ cur = self.conn.execute(
402
+ """INSERT INTO interactions
403
+ (user_id, content, request_id, created_at,
404
+ role, user_action, user_action_description,
405
+ interacted_image_url, shadow_content, expert_content,
406
+ tools_used, citations, embedding)
407
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
408
+ (
409
+ interaction.user_id,
410
+ interaction.content,
411
+ interaction.request_id,
412
+ created_at_iso,
413
+ interaction.role,
414
+ interaction.user_action.value,
415
+ interaction.user_action_description,
416
+ interaction.interacted_image_url,
417
+ interaction.shadow_content,
418
+ interaction.expert_content,
419
+ _json_dumps([t.model_dump() for t in interaction.tools_used]),
420
+ _json_dumps([c.model_dump() for c in interaction.citations]),
421
+ _json_dumps(interaction.embedding),
422
+ ),
423
+ )
424
+ iid = cur.lastrowid or 0
425
+ interaction.interaction_id = iid
426
+ self.conn.commit()
427
+ # Update FTS and vec
428
+ self._fts_upsert(
429
+ "interactions_fts",
430
+ iid,
431
+ content=interaction.content,
432
+ user_action_description=interaction.user_action_description,
433
+ )
434
+ if interaction.embedding:
435
+ self._vec_upsert("interactions_vec", iid, interaction.embedding)
436
+ return iid
437
+
438
+ @SQLiteStorageBase.handle_exceptions
439
+ def add_user_interactions_bulk(
440
+ self,
441
+ user_id: str, # noqa: ARG002
442
+ interactions: list[Interaction],
443
+ ) -> None:
444
+ if not interactions:
445
+ return
446
+ texts = [
447
+ "\n".join([i.content or "", i.user_action_description or ""])
448
+ for i in interactions
449
+ ]
450
+ try:
451
+ embeddings = self.llm_client.get_embeddings(
452
+ texts, self.embedding_model_name, self.embedding_dimensions
453
+ )
454
+ except EmbeddingUnavailableError as exc:
455
+ logger.warning(
456
+ "Embedding unavailable for interaction bulk insert; "
457
+ "continuing without vectors: %s",
458
+ exc,
459
+ )
460
+ embeddings = [[] for _ in texts]
461
+ for interaction, embedding in zip(interactions, embeddings, strict=False):
462
+ interaction.embedding = embedding
463
+ self._insert_interaction(interaction)
464
+
465
+ @SQLiteStorageBase.handle_exceptions
466
+ def delete_user_interaction(self, request: DeleteUserInteractionRequest) -> None:
467
+ self._fts_delete("interactions_fts", request.interaction_id)
468
+ self._execute(
469
+ "DELETE FROM interactions WHERE user_id = ? AND interaction_id = ?",
470
+ (request.user_id, request.interaction_id),
471
+ )
472
+
473
+ @SQLiteStorageBase.handle_exceptions
474
+ def delete_all_interactions_for_user(self, user_id: str) -> None:
475
+ # Delete FTS entries for this user's interactions
476
+ ids = [
477
+ r["interaction_id"]
478
+ for r in self._fetchall(
479
+ "SELECT interaction_id FROM interactions WHERE user_id = ?", (user_id,)
480
+ )
481
+ ]
482
+ if ids:
483
+ placeholders = ",".join("?" for _ in ids)
484
+ with self._lock:
485
+ self.conn.execute(
486
+ f"DELETE FROM interactions_fts WHERE rowid IN ({placeholders})", ids
487
+ )
488
+ self.conn.commit()
489
+ self._execute("DELETE FROM interactions WHERE user_id = ?", (user_id,))
490
+
491
+ @SQLiteStorageBase.handle_exceptions
492
+ def delete_all_interactions(self) -> None:
493
+ with self._lock:
494
+ self.conn.execute("DELETE FROM interactions_fts")
495
+ self.conn.execute("DELETE FROM interactions")
496
+ self.conn.commit()
497
+
498
+ @SQLiteStorageBase.handle_exceptions
499
+ def count_all_interactions(self) -> int:
500
+ row = self._fetchone("SELECT COUNT(*) as cnt FROM interactions")
501
+ return row["cnt"] if row else 0
502
+
503
+ @SQLiteStorageBase.handle_exceptions
504
+ def delete_oldest_interactions(self, count: int) -> int:
505
+ if count <= 0:
506
+ return 0
507
+ rows = self._fetchall(
508
+ "SELECT interaction_id FROM interactions ORDER BY created_at ASC LIMIT ?",
509
+ (count,),
510
+ )
511
+ if not rows:
512
+ return 0
513
+ ids = [r["interaction_id"] for r in rows]
514
+ placeholders = ",".join("?" for _ in ids)
515
+ with self._lock:
516
+ self.conn.execute(
517
+ f"DELETE FROM interactions_fts WHERE rowid IN ({placeholders})", ids
518
+ )
519
+ self.conn.execute(
520
+ f"DELETE FROM interactions WHERE interaction_id IN ({placeholders})",
521
+ ids,
522
+ )
523
+ self.conn.commit()
524
+ return len(ids)
525
+
526
+ # ------------------------------------------------------------------
527
+ # Search — Interactions & Profiles
528
+ # ------------------------------------------------------------------
529
+
530
+ @SQLiteStorageBase.handle_exceptions
531
+ def search_interaction(
532
+ self,
533
+ search_interaction_request: SearchInteractionRequest,
534
+ query_embedding: list[float] | None = None,
535
+ ) -> list[Interaction]:
536
+ req = search_interaction_request
537
+ has_query = bool(req.query)
538
+ match_count = req.most_recent_k or 10
539
+ mode = _effective_search_mode(req.search_mode, query_embedding, req.query)
540
+
541
+ conditions: list[str] = ["i.user_id = ?"]
542
+ params: list[str | int | float] = [req.user_id]
543
+
544
+ if req.request_id:
545
+ conditions.append("i.request_id = ?")
546
+ params.append(req.request_id)
547
+ if req.start_time:
548
+ conditions.append("i.created_at >= ?")
549
+ params.append(req.start_time.timestamp())
550
+ if req.end_time:
551
+ conditions.append("i.created_at <= ?")
552
+ params.append(req.end_time.timestamp())
553
+
554
+ where_clause = " AND ".join(conditions)
555
+ overfetch = match_count * 5 if mode != SearchMode.FTS else match_count
556
+
557
+ # Vector-only: rank by embedding similarity
558
+ if (
559
+ mode in (SearchMode.VECTOR, SearchMode.HYBRID)
560
+ and query_embedding
561
+ and not has_query
562
+ ):
563
+ vector_limit = match_count * 10
564
+ sql = f"""SELECT i.* FROM interactions i
565
+ WHERE {where_clause}
566
+ ORDER BY i.created_at DESC
567
+ LIMIT ?"""
568
+ rows = self._fetchall(sql, (*params, vector_limit))
569
+ rows = _vector_rank_rows(rows, query_embedding, match_count)
570
+ elif has_query:
571
+ # FTS search (with optional HYBRID re-ranking)
572
+ fts_query = _sanitize_fts_query(req.query) # type: ignore[arg-type]
573
+ fts_conditions = ["interactions_fts MATCH ?", *conditions]
574
+ fts_where = " AND ".join(fts_conditions)
575
+ fts_params: list[str | int | float] = [fts_query, *params, overfetch]
576
+ sql = f"""SELECT i.* FROM interactions i
577
+ JOIN interactions_fts f ON i.interaction_id = f.rowid
578
+ WHERE {fts_where}
579
+ ORDER BY bm25(interactions_fts, 1.0, 2.0)
580
+ LIMIT ?"""
581
+ fts_rows = self._fetchall(sql, tuple(fts_params))
582
+
583
+ if mode == SearchMode.HYBRID and query_embedding:
584
+ vec_limit = match_count * 10
585
+ vec_sql = f"""SELECT i.* FROM interactions i
586
+ WHERE {where_clause}
587
+ ORDER BY i.created_at DESC
588
+ LIMIT ?"""
589
+ vec_candidates = self._fetchall(vec_sql, (*params, vec_limit))
590
+ vec_rows = _vector_rank_rows(vec_candidates, query_embedding, overfetch)
591
+ rows = _true_rrf_merge(
592
+ fts_rows,
593
+ vec_rows,
594
+ "interaction_id",
595
+ match_count,
596
+ )
597
+ else:
598
+ rows = fts_rows[:match_count]
599
+ else:
600
+ if req.most_recent_k:
601
+ # No query — just fetch most recent interactions by time
602
+ sql = f"""SELECT i.* FROM interactions i
603
+ WHERE {where_clause}
604
+ ORDER BY i.created_at DESC
605
+ LIMIT ?"""
606
+ rows = self._fetchall(sql, (*params, req.most_recent_k))
607
+ return [_row_to_interaction(r) for r in reversed(rows)]
608
+ return []
609
+
610
+ interactions = [_row_to_interaction(r) for r in rows]
611
+ if req.most_recent_k:
612
+ sorted_ints = sorted(interactions, key=lambda x: x.created_at, reverse=True)
613
+ return list(reversed(sorted_ints[: req.most_recent_k]))
614
+ return interactions
615
+
616
+ @SQLiteStorageBase.handle_exceptions
617
+ def search_user_profile( # noqa: C901
618
+ self,
619
+ search_user_profile_request: SearchUserProfileRequest,
620
+ status_filter: list[Status | None] | None = None,
621
+ query_embedding: list[float] | None = None,
622
+ ) -> list[UserProfile]:
623
+ if status_filter is None:
624
+ status_filter = [None]
625
+
626
+ req = search_user_profile_request
627
+ match_count = req.top_k or 10
628
+ current_ts = _epoch_now()
629
+ has_query = bool(req.query)
630
+ mode = _effective_search_mode(req.search_mode, query_embedding, req.query)
631
+ has_embedding = query_embedding is not None
632
+ logger.info(
633
+ "Profile search: requested_mode=%s, effective_mode=%s, has_query=%s, has_embedding=%s, user_id=%s",
634
+ req.search_mode,
635
+ mode,
636
+ has_query,
637
+ has_embedding,
638
+ req.user_id,
639
+ )
640
+
641
+ conditions: list[str] = ["p.expiration_timestamp >= ?"]
642
+ params: list[object] = [current_ts]
643
+
644
+ if req.user_id:
645
+ conditions.append("p.user_id = ?")
646
+ params.append(req.user_id)
647
+ if req.start_time:
648
+ conditions.append("p.last_modified_timestamp >= ?")
649
+ params.append(int(req.start_time.timestamp()))
650
+ if req.end_time:
651
+ conditions.append("p.last_modified_timestamp <= ?")
652
+ params.append(int(req.end_time.timestamp()))
653
+ if req.source:
654
+ conditions.append("LOWER(p.source) = LOWER(?)")
655
+ params.append(req.source)
656
+ if status_filter is not None:
657
+ frag, sparams = _build_status_sql(status_filter)
658
+ conditions.append(frag)
659
+ params.extend(sparams)
660
+
661
+ where_clause = " AND ".join(conditions)
662
+ overfetch = match_count * 5 if mode != SearchMode.FTS else match_count
663
+
664
+ # Pure vector search: fetch all candidates, rank by cosine similarity
665
+ if mode == SearchMode.VECTOR and query_embedding:
666
+ if req.generated_from_request_id:
667
+ conditions.append("p.generated_from_request_id = ?")
668
+ params.append(req.generated_from_request_id)
669
+ where_clause = " AND ".join(conditions)
670
+ sql = f"""SELECT p.* FROM profiles p
671
+ WHERE {where_clause}
672
+ ORDER BY p.last_modified_timestamp DESC"""
673
+ rows = self._fetchall(sql, tuple(params))
674
+ logger.info(
675
+ "VECTOR search: %d candidates fetched, ranking by embedding", len(rows)
676
+ )
677
+ rows = _vector_rank_rows(rows, query_embedding, match_count)
678
+ elif has_query:
679
+ fts_query = _sanitize_fts_query(req.query) # type: ignore[arg-type]
680
+ sql = f"""SELECT p.* FROM profiles p
681
+ JOIN profiles_fts f ON p.profile_id = f.profile_id
682
+ WHERE profiles_fts MATCH ?
683
+ AND {where_clause}
684
+ ORDER BY bm25(profiles_fts, 0.0, 1.0)
685
+ LIMIT ?"""
686
+ params_list: list[object] = [fts_query, *params, overfetch]
687
+ fts_rows = self._fetchall(sql, tuple(params_list))
688
+ logger.info("FTS search: %d results from BM25", len(fts_rows))
689
+
690
+ if mode == SearchMode.HYBRID and query_embedding:
691
+ logger.info("HYBRID merging FTS + vector results via RRF")
692
+ vec_limit = match_count * 10
693
+ vec_sql = f"""SELECT p.* FROM profiles p
694
+ WHERE {where_clause}
695
+ ORDER BY p.last_modified_timestamp DESC
696
+ LIMIT ?"""
697
+ vec_candidates = self._fetchall(vec_sql, (*params, vec_limit))
698
+ vec_rows = _vector_rank_rows(vec_candidates, query_embedding, overfetch)
699
+ rows = _true_rrf_merge(
700
+ fts_rows,
701
+ vec_rows,
702
+ "profile_id",
703
+ match_count,
704
+ )
705
+ else:
706
+ rows = fts_rows
707
+ elif query_embedding:
708
+ # HYBRID without query text: rank by embedding only
709
+ if req.generated_from_request_id:
710
+ conditions.append("p.generated_from_request_id = ?")
711
+ params.append(req.generated_from_request_id)
712
+ where_clause = " AND ".join(conditions)
713
+ sql = f"""SELECT p.* FROM profiles p
714
+ WHERE {where_clause}
715
+ ORDER BY p.last_modified_timestamp DESC"""
716
+ rows = self._fetchall(sql, tuple(params))
717
+ logger.info(
718
+ "HYBRID (no query text) search: %d candidates, ranking by embedding",
719
+ len(rows),
720
+ )
721
+ rows = _vector_rank_rows(rows, query_embedding, match_count)
722
+ else:
723
+ if req.generated_from_request_id:
724
+ conditions.append("p.generated_from_request_id = ?")
725
+ params.append(req.generated_from_request_id)
726
+ where_clause = " AND ".join(conditions)
727
+ sql = f"""SELECT p.* FROM profiles p
728
+ WHERE {where_clause}
729
+ ORDER BY p.last_modified_timestamp DESC
730
+ LIMIT ?"""
731
+ params_list = [*params, overfetch]
732
+ rows = self._fetchall(sql, tuple(params_list))
733
+
734
+ profiles = [_row_to_profile(r) for r in rows]
735
+ logger.info("Profile search: %d profiles before post-filtering", len(profiles))
736
+
737
+ # Apply filters that can't easily go into SQL
738
+ filtered: list[UserProfile] = []
739
+ for profile in profiles:
740
+ if req.custom_feature and (
741
+ req.custom_feature.lower() not in str(profile.custom_features).lower()
742
+ ):
743
+ continue
744
+ filtered.append(profile)
745
+ if len(filtered) >= match_count:
746
+ break
747
+ return filtered