claude-smart 0.2.44 → 0.2.46

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 (323) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +34 -5
  3. package/bin/claude-smart.js +295 -5
  4. package/package.json +22 -3
  5. package/plugin/.claude-plugin/plugin.json +4 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +22 -2
  8. package/plugin/commands/clear-all.md +1 -0
  9. package/plugin/commands/dashboard.md +1 -0
  10. package/plugin/commands/learn.md +1 -0
  11. package/plugin/commands/restart.md +1 -0
  12. package/plugin/commands/show.md +1 -0
  13. package/plugin/dashboard/app/configure/env/page.tsx +40 -14
  14. package/plugin/dashboard/app/configure/server/page.tsx +51 -1
  15. package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  18. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  19. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  20. package/plugin/dashboard/lib/status.ts +10 -3
  21. package/plugin/dashboard/lib/types.ts +18 -6
  22. package/plugin/hooks/codex-hooks.json +0 -1
  23. package/plugin/opencode/assistant-buffer.ts +108 -0
  24. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  25. package/plugin/opencode/dist/internal.js +12 -0
  26. package/plugin/opencode/dist/payload.js +85 -0
  27. package/plugin/opencode/dist/server.mjs +158 -0
  28. package/plugin/opencode/internal.ts +18 -0
  29. package/plugin/opencode/package.json +3 -0
  30. package/plugin/opencode/payload.ts +90 -0
  31. package/plugin/opencode/server.mts +174 -0
  32. package/plugin/opencode/tsconfig.json +13 -0
  33. package/plugin/pyproject.toml +20 -3
  34. package/plugin/scripts/_lib.sh +77 -29
  35. package/plugin/scripts/backend-service.sh +27 -5
  36. package/plugin/scripts/dashboard-service.sh +7 -5
  37. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  38. package/plugin/scripts/hook_entry.sh +3 -3
  39. package/plugin/scripts/opencode-claude-compat +4 -0
  40. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  41. package/plugin/scripts/opencode-claude-compat.js +225 -0
  42. package/plugin/scripts/smart-install.sh +10 -8
  43. package/plugin/src/README.md +1 -1
  44. package/plugin/src/claude_smart/cli.py +304 -6
  45. package/plugin/src/claude_smart/env_config.py +12 -4
  46. package/plugin/src/claude_smart/events/session_start.py +26 -7
  47. package/plugin/src/claude_smart/events/stop.py +1 -1
  48. package/plugin/src/claude_smart/ids.py +1 -1
  49. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  50. package/plugin/src/claude_smart/runtime.py +7 -1
  51. package/plugin/uv.lock +1 -1
  52. package/plugin/vendor/reflexio/.env.example +54 -30
  53. package/plugin/vendor/reflexio/README.md +14 -8
  54. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  55. package/plugin/vendor/reflexio/reflexio/README.md +1 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  57. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  58. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
  63. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  64. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  65. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  66. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  67. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  68. package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
  69. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  70. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  71. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  142. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
  143. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  144. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
  145. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  146. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
  147. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  148. package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
  149. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
  150. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  151. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  152. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  153. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  157. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  158. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
  159. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  160. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  161. package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
  162. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  163. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  164. package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
  165. package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
  166. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  167. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
  168. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  169. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  170. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  171. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  172. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  173. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  174. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  175. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  176. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  177. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  178. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  179. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  180. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  181. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  182. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  183. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  184. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  185. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  186. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  187. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  188. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
  189. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  197. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  198. package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
  199. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  200. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  201. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  202. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  203. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  204. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  205. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  206. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  207. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  208. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  209. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  211. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  212. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  214. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  215. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  216. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  217. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  218. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  219. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  220. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  221. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
  222. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  223. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
  225. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  226. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  227. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
  228. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  229. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
  230. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  231. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  232. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  233. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  234. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  235. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  237. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  238. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  239. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  241. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  242. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
  243. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  244. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  245. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  246. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  247. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  248. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  249. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
  250. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  251. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  252. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  253. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
  254. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  255. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  256. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  257. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
  258. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
  259. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
  260. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  261. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
  262. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
  263. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
  264. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  265. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  266. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  277. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  278. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  279. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  281. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
  282. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  283. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  284. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  285. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  286. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  287. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  288. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  289. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  290. package/scripts/setup-claude-smart.sh +8 -3
  291. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  292. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  293. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  294. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  295. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  296. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  297. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  298. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  299. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  300. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  301. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  302. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  303. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  304. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  305. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  306. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  307. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  308. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  309. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  310. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  311. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  312. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  313. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  314. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  318. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  319. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  320. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  321. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  322. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  323. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -1,1125 +0,0 @@
1
- """Atomic tool handlers for the agentic-v2 extraction + search pipelines.
2
-
3
- Each handler:
4
- - Receives args (Pydantic model validated by ToolRegistry)
5
- - Receives (storage, ctx)
6
- - Calls an existing BaseStorage method
7
- - Returns a dict projection suitable for the LLM
8
-
9
- Read handlers populate ctx.known_ids (for invariant B) and ctx.search_count
10
- (for invariant A). Mutating handlers (Task 5) append PlanOps to ctx.plan
11
- without hitting storage; commit_plan applies them via apply_plan_op after
12
- invariants pass.
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import logging
18
- import uuid
19
- from datetime import UTC, datetime
20
- from typing import Annotated, Any, Literal
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
- from pydantic import BaseModel, Field
25
-
26
- from reflexio.models.api_schema.domain.entities import (
27
- PlaybookStatus,
28
- Status,
29
- UserPlaybook,
30
- UserProfile,
31
- )
32
- from reflexio.models.api_schema.domain.enums import ProfileTimeToLive
33
- from reflexio.models.api_schema.retriever_schema import (
34
- SearchAgentPlaybookRequest,
35
- SearchMode,
36
- SearchUserPlaybookRequest,
37
- SearchUserProfileRequest,
38
- )
39
- from reflexio.models.config_schema import SINGLETON_USER_PLAYBOOK_NAME, SearchOptions
40
- from reflexio.server.services.extraction.plan import (
41
- CreateUserPlaybookOp,
42
- CreateUserProfileOp,
43
- DeleteUserPlaybookOp,
44
- DeleteUserProfileOp,
45
- ExtractionCtx,
46
- )
47
- from reflexio.server.services.profile.profile_generation_service_utils import (
48
- calculate_expiration_timestamp,
49
- )
50
-
51
- TOP_K_CAP = 25
52
-
53
-
54
- # ====================================================================
55
- # Arg schemas (what the LLM emits)
56
- # ====================================================================
57
-
58
-
59
- class SearchUserProfilesArgs(BaseModel):
60
- """Semantic/keyword search the current user's profiles, with optional
61
- cross-encoder rerank.
62
-
63
- Default behaviour (``rerank=False``): hybrid retrieval (BM25 + vector
64
- via RRF) only, return top ``top_k``. Cheap.
65
-
66
- Set ``rerank=True`` to add a cross-encoder reranking pass on top of the
67
- hybrid retrieval. The server over-fetches a wider candidate pool, scores
68
- each (query, content) pair with the cross-encoder, and returns the top
69
- ``top_k`` by descending rerank score. More accurate ranking when the
70
- bi-encoder near-duplicate confuses the LLM, but adds ~5-15 ms latency
71
- and the encoder may not weight all facets the same as the LLM would.
72
-
73
- Optional ``refine_with``: when set, used as the rerank query INSTEAD of
74
- ``query``. Only takes effect when ``rerank=True`` or ``llm_rerank=True``.
75
- Lets you broadly fetch ("bike maintenance") then narrow on a specific
76
- facet ("dollar amounts spent") without round-tripping candidate ids
77
- through the agent.
78
-
79
- Set ``llm_rerank=True`` to use an LLM relevance-judge instead of the
80
- cross-encoder. The LLM brings world knowledge that lexical/semantic
81
- cross-encoders lack — bridges brand→category gaps (e.g. "Thrive Market"
82
- = grocery service, "Italian designer boots" = luxury footwear) that
83
- sink the relevant profile under literal-keyword matches. Costs ~1 s
84
- of latency and one LLM call per search; use it on the FIRST pass when
85
- the query is brand/category/synonym-prone, NOT as a refinement step.
86
- Mutually exclusive with ``rerank``: when both are True, ``llm_rerank``
87
- wins.
88
-
89
- Each hit includes a ``session_id`` field — pass that to
90
- ``read_session_text`` if you need the verbatim source turns behind a
91
- profile.
92
- """
93
-
94
- query: Annotated[str, Field(min_length=1)]
95
- top_k: int = 10
96
- rerank: bool = False
97
- llm_rerank: bool = False
98
- refine_with: str | None = None
99
-
100
-
101
- class GetUserProfileArgs(BaseModel):
102
- """Retrieve a single UserProfile by id."""
103
-
104
- id: Annotated[str, Field(min_length=1)]
105
-
106
-
107
- class SearchUserPlaybooksArgs(BaseModel):
108
- """Search the current user's playbooks. See SearchUserProfilesArgs for
109
- the rerank semantics — same toggles, same handler shape.
110
- """
111
-
112
- query: Annotated[str, Field(min_length=1)]
113
- top_k: int = 10
114
- status: Literal["current", "pending", "archived"] = "current"
115
- rerank: bool = False
116
- llm_rerank: bool = False
117
- refine_with: str | None = None
118
-
119
-
120
- class GetUserPlaybookArgs(BaseModel):
121
- """Retrieve a single UserPlaybook by id."""
122
-
123
- id: Annotated[str, Field(min_length=1)]
124
-
125
-
126
- class SearchAgentPlaybooksArgs(BaseModel):
127
- """Search agent-version-scoped playbooks (read-only; search pipeline only).
128
- See SearchUserProfilesArgs for the rerank semantics.
129
- """
130
-
131
- query: Annotated[str, Field(min_length=1)]
132
- top_k: int = 10
133
- status: Literal["current", "pending", "archived"] = "current"
134
- rerank: bool = False
135
- llm_rerank: bool = False
136
- refine_with: str | None = None
137
-
138
-
139
- class GetAgentPlaybookArgs(BaseModel):
140
- """Retrieve a single AgentPlaybook by id."""
141
-
142
- id: Annotated[str, Field(min_length=1)]
143
-
144
-
145
- class ReadSessionTextArgs(BaseModel):
146
- """Retrieve and compress the verbatim turns of one or more sessions.
147
-
148
- Fetches raw interactions for each ``session_id`` and runs an in-tool
149
- compression pass against ``query`` so the returned text is a denoised
150
- excerpt focused on the question — not the full transcript. Sessions are
151
- concatenated with ``=== session <id> ===`` headers. Falls back to
152
- role-prefixed raw turns (truncated at ``max_chars_per_session``) when
153
- compression is unavailable or fails.
154
-
155
- Use after ``search_user_profiles`` when the profile content compresses
156
- away detail you need (multi-action structure, exact dates buried in
157
- narrative, prior assistant statements). Pass 1–4 session_ids in a single
158
- call when the question requires data from multiple sessions (e.g.
159
- ordering 4 events, summing per-event amounts across 3 transactions).
160
-
161
- The ``query`` field is what compression scores against — typically the
162
- user's question or a focused subquery. Pass an empty string to skip
163
- compression and receive raw role-prefixed turns instead.
164
-
165
- The ``session_ids`` come from profile hits' ``session_id`` field (the
166
- same value the search response exposes). ``max_chars_per_session``
167
- bounds the raw-fallback output; compressed output is bounded by the
168
- compressor's own discretion (typically much smaller).
169
- """
170
-
171
- session_ids: Annotated[list[str], Field(min_length=1, max_length=4)]
172
- query: str = ""
173
- max_chars_per_session: int = 16000
174
-
175
-
176
- class RerankUserProfilesArgs(BaseModel):
177
- """Rerank a list of profile ids by query relevance using a cross-encoder.
178
-
179
- Use after `search_user_profiles` when the initial results are noisy and
180
- you need to surface the most semantically relevant ones to the question.
181
- """
182
-
183
- query: Annotated[str, Field(min_length=1)]
184
- profile_ids: list[str]
185
- top_k: int = 10
186
-
187
-
188
- class StorageStatsArgs(BaseModel):
189
- """Get a quick count of how many profiles/playbooks the user has and the date range.
190
-
191
- Useful for sizing search top_k appropriately before retrieval.
192
- """
193
-
194
-
195
- # ====================================================================
196
- # Helpers
197
- # ====================================================================
198
-
199
-
200
- def _cap_top_k(k: int) -> int:
201
- return min(max(1, k), TOP_K_CAP)
202
-
203
-
204
- def _maybe_embed_query(storage: Any, query: str) -> list[float] | None:
205
- """Compute a query embedding via the storage backend's embedder.
206
-
207
- Returns ``None`` on any failure (backend doesn't expose ``_get_embedding``,
208
- embedding provider unavailable, or embed call raises). Without an embedding,
209
- storage downgrades HYBRID/VECTOR search to FTS-only — the classic search
210
- path (``unified_search_service.py:151-158``) uses the same helper pattern.
211
-
212
- Args:
213
- storage (Any): BaseStorage instance.
214
- query (str): The search query to embed.
215
-
216
- Returns:
217
- list[float] | None: The embedding vector, or ``None`` when unavailable.
218
- """
219
- embed_fn = getattr(storage, "_get_embedding", None)
220
- if embed_fn is None:
221
- return None
222
- try:
223
- return embed_fn(query)
224
- except Exception: # noqa: BLE001 — embedder failures must not break search
225
- return None
226
-
227
-
228
- def _status_from_str(s: str) -> Status | None:
229
- return {"current": None, "pending": Status.PENDING, "archived": Status.ARCHIVED}[s]
230
-
231
-
232
- def _agent_playbook_statuses_from_ctx(ctx: ExtractionCtx) -> list[PlaybookStatus]:
233
- values = ctx.agent_playbook_status_filter or ["approved", "pending"]
234
- out: list[PlaybookStatus] = []
235
- for value in values:
236
- try:
237
- status = PlaybookStatus(value)
238
- except ValueError:
239
- continue
240
- if status not in out:
241
- out.append(status)
242
- return out
243
-
244
-
245
- RERANK_POOL_SIZE = 30
246
- """How many candidates we hand the reranker when ``rerank=True`` or
247
- ``llm_rerank=True``.
248
-
249
- If ``final_k`` exceeds this constant (e.g. agent asks for top_k=50 with
250
- rerank=True), we still fetch ``final_k`` so we have a full set to return.
251
- """
252
-
253
-
254
- def _fetch_k_for_rerank(final_k: int, rerank: bool, llm_rerank: bool = False) -> int:
255
- """Pick the initial-fetch size given whether any rerank stage is enabled.
256
-
257
- Without rerank: trust the hybrid retrieval order, fetch exactly ``final_k``.
258
- With rerank (cross-encoder OR LLM): fetch ``RERANK_POOL_SIZE`` candidates
259
- so the reranker has headroom to reorder. If ``final_k`` already exceeds
260
- the pool size, use ``final_k`` (agent asked for more than we'd otherwise
261
- fetch).
262
-
263
- Args:
264
- final_k (int): Number of hits the agent asked for.
265
- rerank (bool): Whether the cross-encoder rerank stage is enabled.
266
- llm_rerank (bool): Whether the LLM rerank stage is enabled.
267
-
268
- Returns:
269
- int: The initial-fetch size for hybrid retrieval.
270
- """
271
- if not (rerank or llm_rerank):
272
- return final_k
273
- return max(final_k, RERANK_POOL_SIZE)
274
-
275
-
276
- def _maybe_rerank_hits(
277
- hits: list[Any],
278
- rerank: bool,
279
- rerank_query: str,
280
- final_k: int,
281
- llm_rerank: bool = False,
282
- llm_client: Any | None = None,
283
- prompt_manager: Any | None = None,
284
- ) -> list[Any]:
285
- """Apply rerank if any rerank flag is set and we have headroom.
286
-
287
- Dispatch order: ``llm_rerank`` (if True and infra available) takes
288
- precedence over ``rerank`` (cross-encoder). Both fall back to hybrid
289
- order on any failure. ``llm_rerank`` further falls back to the
290
- cross-encoder if the LLM call fails AND ``rerank`` was also requested.
291
-
292
- Args:
293
- hits: Candidates from the initial hybrid retrieval. Each must expose
294
- a ``content`` attribute (profile, playbook, etc).
295
- rerank: Cross-encoder toggle from the agent's tool args.
296
- rerank_query: The string to score candidates against. The caller
297
- decides whether this is the original query or a refinement
298
- (e.g. ``args.refine_with or args.query``).
299
- final_k: Number of hits to return after reranking.
300
- llm_rerank: LLM-rerank toggle. When True (and ``llm_client`` and
301
- ``prompt_manager`` are wired in), the LLM judges relevance.
302
- Use this for synonym/brand/category-knowledge gaps where the
303
- cross-encoder misses semantic equivalents.
304
- llm_client: A ``LiteLLMClient`` for the LLM rerank call. Required
305
- when ``llm_rerank=True``.
306
- prompt_manager: A ``PromptManager`` for rendering the rerank prompt.
307
- Required when ``llm_rerank=True``.
308
-
309
- Returns:
310
- The re-ordered hits, capped at ``final_k``. If neither flag is set
311
- or ``len(hits) <= final_k``, returns ``hits[:final_k]`` unchanged.
312
- """
313
- if not (rerank or llm_rerank) or len(hits) <= final_k:
314
- return hits[:final_k]
315
-
316
- if llm_rerank:
317
- ordered = _try_llm_rerank(
318
- hits, rerank_query, final_k, llm_client, prompt_manager
319
- )
320
- if ordered is not None:
321
- return ordered
322
- # LLM rerank failed — fall through to cross-encoder ONLY if also requested.
323
- if not rerank:
324
- return hits[:final_k]
325
-
326
- return _try_cross_encoder_rerank(hits, rerank_query, final_k)
327
-
328
-
329
- def _try_llm_rerank(
330
- hits: list[Any],
331
- rerank_query: str,
332
- final_k: int,
333
- llm_client: Any | None,
334
- prompt_manager: Any | None,
335
- ) -> list[Any] | None:
336
- """Run the LLM rerank stage; return ``None`` on any failure.
337
-
338
- Args:
339
- hits: Candidate hits to rerank.
340
- rerank_query: Query to score against.
341
- final_k: How many to return.
342
- llm_client: ``LiteLLMClient`` instance, or ``None``.
343
- prompt_manager: ``PromptManager`` instance, or ``None``.
344
-
345
- Returns:
346
- Re-ordered top-``final_k`` on success; ``None`` on failure so the
347
- caller can chain to the cross-encoder fallback.
348
- """
349
- try:
350
- from reflexio.server.llm.rerank import score_pairs_llm
351
- except ImportError:
352
- return None
353
- scores = score_pairs_llm(
354
- rerank_query, [h.content for h in hits], llm_client, prompt_manager
355
- )
356
- if scores is None:
357
- return None
358
- ranked = sorted(
359
- zip(hits, scores, strict=True), key=lambda pair: pair[1], reverse=True
360
- )
361
- return [h for h, _ in ranked[:final_k]]
362
-
363
-
364
- def _try_cross_encoder_rerank(
365
- hits: list[Any], rerank_query: str, final_k: int
366
- ) -> list[Any]:
367
- """Run the cross-encoder rerank stage; fall back to hybrid order on failure.
368
-
369
- Args:
370
- hits: Candidate hits to rerank.
371
- rerank_query: Query to score against.
372
- final_k: How many to return.
373
-
374
- Returns:
375
- Re-ordered top-``final_k`` on success; ``hits[:final_k]`` on failure.
376
- """
377
- try:
378
- from reflexio.server.llm.rerank import score_pairs
379
-
380
- scores = score_pairs(rerank_query, [h.content for h in hits])
381
- ranked = sorted(
382
- zip(hits, scores, strict=True), key=lambda pair: pair[1], reverse=True
383
- )
384
- return [h for h, _ in ranked[:final_k]]
385
- except Exception: # noqa: BLE001 — fall back to hybrid order on failure
386
- return hits[:final_k]
387
-
388
-
389
- def _project_profile_for_llm(p: Any) -> dict[str, Any]:
390
- # ``session_id`` is the agent-facing name for the storage-internal
391
- # ``generated_from_request_id``. Exposing it lets the agent chain
392
- # ``search_user_profiles`` → ``read_session_text(session_ids=[...])``
393
- # without needing a separate lookup. The internal field name stays as
394
- # ``generated_from_request_id`` everywhere else (storage, schemas);
395
- # the rename is purely a cognitive affordance for the LLM.
396
- return {
397
- "id": getattr(p, "profile_id", "") or "",
398
- "content": p.content,
399
- "session_id": getattr(p, "generated_from_request_id", "") or "",
400
- "ttl": p.profile_time_to_live,
401
- "last_modified": p.last_modified_timestamp,
402
- "source_span": getattr(p, "source_span", None),
403
- }
404
-
405
-
406
- def _project_user_playbook_for_llm(pb: Any) -> dict[str, Any]:
407
- return {
408
- "id": str(pb.user_playbook_id),
409
- "trigger": pb.trigger,
410
- "content": pb.content,
411
- "rationale": pb.rationale,
412
- "last_modified": getattr(pb, "created_at", 0),
413
- }
414
-
415
-
416
- def _project_agent_playbook_for_llm(pb: Any) -> dict[str, Any]:
417
- return {
418
- "id": str(pb.agent_playbook_id),
419
- "trigger": pb.trigger,
420
- "content": pb.content,
421
- "rationale": pb.rationale,
422
- "playbook_status": getattr(pb, "playbook_status", None),
423
- "last_modified": getattr(pb, "created_at", 0),
424
- }
425
-
426
-
427
- # ====================================================================
428
- # Read handlers
429
- # ====================================================================
430
-
431
-
432
- def _handle_search_user_profiles(
433
- args: SearchUserProfilesArgs,
434
- storage: Any,
435
- ctx: ExtractionCtx,
436
- llm_client: Any | None = None,
437
- prompt_manager: Any | None = None,
438
- ) -> dict[str, Any]:
439
- """Search the current user's profiles and bump search_count.
440
-
441
- Two-stage retrieval: hybrid (BM25 + vector via RRF) over-fetches a wider
442
- candidate pool, then an optional rerank scores ``(query, content)`` pairs
443
- and returns the top ``args.top_k``. ``args.rerank`` selects the local
444
- cross-encoder; ``args.llm_rerank`` selects an LLM relevance-judge that
445
- has world knowledge for brand→category gaps the cross-encoder can't
446
- bridge. ``llm_rerank`` wins when both are set.
447
-
448
- The over-fetch + rerank pattern fixes the class of failures where the
449
- bi-encoder ranks the right profile at #2-#15 by cosine but the top-1
450
- is a near-duplicate or literal-keyword match that the answer LLM picks
451
- first.
452
-
453
- Args:
454
- args (SearchUserProfilesArgs): Query, top_k, rerank toggles, refine_with.
455
- storage (Any): BaseStorage instance.
456
- ctx (ExtractionCtx): Per-run state; search_count incremented in place.
457
- llm_client (Any | None): ``LiteLLMClient`` for ``llm_rerank=True``.
458
- prompt_manager (Any | None): ``PromptManager`` for ``llm_rerank=True``.
459
-
460
- Returns:
461
- dict[str, Any]: ``{"hits": [...]}`` with LLM-facing profile projections.
462
- """
463
- final_k = _cap_top_k(args.top_k)
464
- fetch_k = _fetch_k_for_rerank(final_k, args.rerank, args.llm_rerank)
465
-
466
- request = SearchUserProfileRequest(
467
- query=args.query,
468
- user_id=ctx.user_id,
469
- top_k=fetch_k,
470
- )
471
- hits = storage.search_user_profile(
472
- request,
473
- query_embedding=_maybe_embed_query(storage, args.query),
474
- )
475
- ctx.search_count += 1
476
-
477
- hits = _maybe_rerank_hits(
478
- hits=hits,
479
- rerank=args.rerank,
480
- rerank_query=args.refine_with or args.query,
481
- final_k=final_k,
482
- llm_rerank=args.llm_rerank,
483
- llm_client=llm_client,
484
- prompt_manager=prompt_manager,
485
- )
486
-
487
- for h in hits:
488
- pid = getattr(h, "profile_id", "") or ""
489
- if pid:
490
- ctx.known_ids.add(pid)
491
- return {"hits": [_project_profile_for_llm(h) for h in hits]}
492
-
493
-
494
- def _handle_get_user_profile(
495
- args: GetUserProfileArgs, storage: Any, ctx: ExtractionCtx
496
- ) -> dict[str, Any]:
497
- """Retrieve a single UserProfile by id without bumping search_count.
498
-
499
- Args:
500
- args (GetUserProfileArgs): Profile id to look up.
501
- storage (Any): BaseStorage instance.
502
- ctx (ExtractionCtx): Per-run state; known_ids updated on hit.
503
-
504
- Returns:
505
- dict[str, Any]: ``{"profile": {...}}`` on hit, ``{"error": "not found"}`` on miss.
506
- """
507
- all_profiles = storage.get_user_profile(ctx.user_id)
508
- for p in all_profiles:
509
- if (getattr(p, "profile_id", "") or "") == args.id:
510
- ctx.known_ids.add(args.id)
511
- return {"profile": _project_profile_for_llm(p)}
512
- return {"error": "not found"}
513
-
514
-
515
- def _handle_search_user_playbooks(
516
- args: SearchUserPlaybooksArgs,
517
- storage: Any,
518
- ctx: ExtractionCtx,
519
- llm_client: Any | None = None,
520
- prompt_manager: Any | None = None,
521
- ) -> dict[str, Any]:
522
- """Search the current user's playbooks and bump search_count.
523
-
524
- Args:
525
- args (SearchUserPlaybooksArgs): Query, top_k, status filter, rerank toggles.
526
- storage (Any): BaseStorage instance.
527
- ctx (ExtractionCtx): Per-run state; search_count and known_ids updated.
528
- llm_client (Any | None): ``LiteLLMClient`` for ``llm_rerank=True``.
529
- prompt_manager (Any | None): ``PromptManager`` for ``llm_rerank=True``.
530
-
531
- Returns:
532
- dict[str, Any]: ``{"hits": [...]}`` with LLM-facing playbook projections.
533
- """
534
- final_k = _cap_top_k(args.top_k)
535
- fetch_k = _fetch_k_for_rerank(final_k, args.rerank, args.llm_rerank)
536
- request = SearchUserPlaybookRequest(
537
- query=args.query,
538
- user_id=ctx.user_id,
539
- agent_version=ctx.agent_version,
540
- top_k=fetch_k,
541
- status_filter=[_status_from_str(args.status)],
542
- search_mode=SearchMode.HYBRID,
543
- threshold=0.4,
544
- )
545
- hits = storage.search_user_playbooks(
546
- request,
547
- options=SearchOptions(query_embedding=_maybe_embed_query(storage, args.query)),
548
- )
549
- ctx.search_count += 1
550
- hits = _maybe_rerank_hits(
551
- hits=hits,
552
- rerank=args.rerank,
553
- rerank_query=args.refine_with or args.query,
554
- final_k=final_k,
555
- llm_rerank=args.llm_rerank,
556
- llm_client=llm_client,
557
- prompt_manager=prompt_manager,
558
- )
559
- for h in hits:
560
- ctx.known_ids.add(str(h.user_playbook_id))
561
- return {"hits": [_project_user_playbook_for_llm(h) for h in hits]}
562
-
563
-
564
- def _handle_get_user_playbook(
565
- args: GetUserPlaybookArgs, storage: Any, ctx: ExtractionCtx
566
- ) -> dict[str, Any]:
567
- """Retrieve a single UserPlaybook by id without bumping search_count.
568
-
569
- Args:
570
- args (GetUserPlaybookArgs): Playbook id to look up.
571
- storage (Any): BaseStorage instance.
572
- ctx (ExtractionCtx): Per-run state; known_ids updated on hit.
573
-
574
- Returns:
575
- dict[str, Any]: ``{"playbook": {...}}`` on hit, ``{"error": "not found"}`` on miss.
576
- """
577
- candidates = storage.get_user_playbooks(
578
- user_id=ctx.user_id, agent_version=ctx.agent_version
579
- )
580
- for pb in candidates:
581
- if str(pb.user_playbook_id) == args.id:
582
- ctx.known_ids.add(args.id)
583
- return {"playbook": _project_user_playbook_for_llm(pb)}
584
- return {"error": "not found"}
585
-
586
-
587
- def _handle_search_agent_playbooks(
588
- args: SearchAgentPlaybooksArgs,
589
- storage: Any,
590
- ctx: ExtractionCtx,
591
- llm_client: Any | None = None,
592
- prompt_manager: Any | None = None,
593
- ) -> dict[str, Any]:
594
- """Search agent-version-scoped playbooks and bump search_count.
595
-
596
- Args:
597
- args (SearchAgentPlaybooksArgs): Query, top_k, status filter, rerank toggles.
598
- storage (Any): BaseStorage instance.
599
- ctx (ExtractionCtx): Per-run state; search_count and known_ids updated.
600
- llm_client (Any | None): ``LiteLLMClient`` for ``llm_rerank=True``.
601
- prompt_manager (Any | None): ``PromptManager`` for ``llm_rerank=True``.
602
-
603
- Returns:
604
- dict[str, Any]: ``{"hits": [...]}`` with LLM-facing agent playbook projections.
605
- """
606
- final_k = _cap_top_k(args.top_k)
607
- fetch_k = _fetch_k_for_rerank(final_k, args.rerank, args.llm_rerank)
608
- options = SearchOptions(query_embedding=_maybe_embed_query(storage, args.query))
609
- hits: list[Any] = []
610
- seen_ids: set[str] = set()
611
- for playbook_status in _agent_playbook_statuses_from_ctx(ctx):
612
- request = SearchAgentPlaybookRequest(
613
- query=args.query,
614
- agent_version=ctx.agent_version,
615
- top_k=fetch_k,
616
- status_filter=[_status_from_str(args.status)],
617
- playbook_status_filter=playbook_status,
618
- search_mode=SearchMode.HYBRID,
619
- threshold=0.4,
620
- )
621
- for hit in storage.search_agent_playbooks(request, options=options):
622
- hit_id = str(getattr(hit, "agent_playbook_id", ""))
623
- if hit_id and hit_id not in seen_ids:
624
- seen_ids.add(hit_id)
625
- hits.append(hit)
626
- if len(hits) >= fetch_k:
627
- break
628
- if len(hits) >= fetch_k:
629
- break
630
- ctx.search_count += 1
631
- hits = _maybe_rerank_hits(
632
- hits=hits,
633
- rerank=args.rerank,
634
- rerank_query=args.refine_with or args.query,
635
- final_k=final_k,
636
- llm_rerank=args.llm_rerank,
637
- llm_client=llm_client,
638
- prompt_manager=prompt_manager,
639
- )
640
- for h in hits:
641
- ctx.known_ids.add(str(h.agent_playbook_id))
642
- return {"hits": [_project_agent_playbook_for_llm(h) for h in hits]}
643
-
644
-
645
- def _handle_get_agent_playbook(
646
- args: GetAgentPlaybookArgs, storage: Any, ctx: ExtractionCtx
647
- ) -> dict[str, Any]:
648
- """Retrieve a single AgentPlaybook by id without bumping search_count.
649
-
650
- Args:
651
- args (GetAgentPlaybookArgs): Agent playbook id to look up.
652
- storage (Any): BaseStorage instance.
653
- ctx (ExtractionCtx): Per-run state; known_ids updated on hit.
654
-
655
- Returns:
656
- dict[str, Any]: ``{"playbook": {...}}`` on hit, ``{"error": "not found"}`` on miss.
657
- """
658
- candidates = storage.get_agent_playbooks(agent_version=ctx.agent_version)
659
- for pb in candidates:
660
- if str(pb.agent_playbook_id) == args.id:
661
- ctx.known_ids.add(args.id)
662
- return {"playbook": _project_agent_playbook_for_llm(pb)}
663
- return {"error": "not found"}
664
-
665
-
666
- _COMPRESS_PROMPT_ID = "compress_session_for_query"
667
- # 5s was far too aggressive: under 10-worker benchmark concurrency every
668
- # gpt-5-mini compression call timed out, silently degrading every
669
- # rehydration to raw turns. 30s gives gpt-5-mini room under load while
670
- # still failing fast on genuine model outages (the raw-turns fallback
671
- # kicks in beyond that).
672
- _COMPRESS_LLM_TIMEOUT = 30
673
- _COMPRESS_LLM_MAX_RETRIES = 1
674
-
675
-
676
- def _format_raw_turns(
677
- session_ids: list[str],
678
- by_session: dict[str, list[Any]],
679
- max_chars_per_session: int,
680
- ) -> str:
681
- """Format role-prefixed raw turns with per-session caps.
682
-
683
- Used both as the compressor's input and as the fallback output when
684
- compression is unavailable or fails.
685
-
686
- Args:
687
- session_ids (list[str]): Session ids in the order requested by the agent.
688
- by_session (dict[str, list[Any]]): Interactions grouped by session id.
689
- max_chars_per_session (int): Per-session truncation cap (chars).
690
-
691
- Returns:
692
- str: Concatenated ``=== session <id> ===\\n[role] content`` blocks.
693
- """
694
- blocks: list[str] = []
695
- for sid in session_ids:
696
- sess_interactions = by_session.get(sid, [])
697
- if not sess_interactions:
698
- blocks.append(f"=== session {sid} ===\n(no interactions found)")
699
- continue
700
- # Derive the session date from the earliest interaction timestamp.
701
- # Including this in the header lets the downstream answer LLM resolve
702
- # relative-time phrases ("yesterday", "last week", "X days ago") in
703
- # the raw turns against an absolute anchor — without it, the LLM ends
704
- # up quoting the relative phrase verbatim instead of computing the
705
- # date (the multi-hop temporal failure pattern).
706
- timestamps: list[float] = []
707
- for interaction in sess_interactions:
708
- created_at = getattr(interaction, "created_at", None)
709
- if isinstance(created_at, datetime):
710
- timestamps.append(created_at.timestamp())
711
- elif isinstance(created_at, int | float):
712
- timestamps.append(float(created_at))
713
- date_suffix = ""
714
- if timestamps:
715
- try:
716
- date_iso = datetime.fromtimestamp(min(timestamps), tz=UTC).strftime(
717
- "%Y-%m-%d"
718
- )
719
- date_suffix = f" (date: {date_iso})"
720
- except (OverflowError, OSError, ValueError):
721
- pass
722
- lines = [
723
- f"[{getattr(i, 'role', '?')}] {i.content}"
724
- for i in sess_interactions
725
- if getattr(i, "content", None)
726
- ]
727
- body = "\n".join(lines)
728
- if len(body) > max_chars_per_session:
729
- body = body[:max_chars_per_session] + "…"
730
- blocks.append(f"=== session {sid}{date_suffix} ===\n{body}")
731
- return "\n\n".join(blocks)
732
-
733
-
734
- def _compress_raw_turns(
735
- raw_turns: str,
736
- query: str,
737
- llm_client: Any,
738
- prompt_manager: Any,
739
- ) -> str | None:
740
- """Run the compress_session_for_query prompt against raw_turns + query.
741
-
742
- Returns the compressed text on success, or ``None`` on any failure
743
- (timeout, exception, empty/whitespace output). Caller falls back to
744
- raw_turns when ``None`` is returned.
745
-
746
- Args:
747
- raw_turns (str): The role-prefixed raw transcript (output of
748
- ``_format_raw_turns``).
749
- query (str): The query the compressor scores against.
750
- llm_client (Any): LiteLLMClient with a ``generate_response`` method.
751
- prompt_manager (Any): PromptManager with a ``render_prompt`` method.
752
-
753
- Returns:
754
- str | None: Compressed transcript, or None to indicate fallback.
755
- """
756
- # Diagnostic: hard-disable compression to measure its net impact vs raw
757
- # turns. r60 measured working v1.2.0 cost ~4pp; r65 confirmed v1.3.0
758
- # near-pass-through still cost. Returning None here forces the raw-turns
759
- # fallback path — same behavior as the original 5s-timeout era at r45.
760
- return None
761
-
762
- try: # noqa pragma: no cover — disabled compression path
763
- prompt = prompt_manager.render_prompt(
764
- _COMPRESS_PROMPT_ID,
765
- variables={"query": query, "raw_turns": raw_turns},
766
- )
767
- result = llm_client.generate_response(
768
- prompt,
769
- timeout=_COMPRESS_LLM_TIMEOUT,
770
- max_retries=_COMPRESS_LLM_MAX_RETRIES,
771
- )
772
- except Exception as e: # noqa: BLE001 — broad: LLM stack raises diverse exception types
773
- logger.warning(
774
- "read_session_text compression failed, using raw fallback: %s", e
775
- )
776
- return None
777
-
778
- if not isinstance(result, str) or not result.strip():
779
- logger.warning(
780
- "read_session_text compression returned empty, using raw fallback"
781
- )
782
- return None
783
- return result.strip()
784
-
785
-
786
- def _handle_read_session_text(
787
- args: ReadSessionTextArgs,
788
- storage: Any,
789
- ctx: ExtractionCtx,
790
- llm_client: Any = None,
791
- prompt_manager: Any = None,
792
- ) -> dict[str, Any]:
793
- """Return a denoised excerpt of one or more sessions, scored against ``query``.
794
-
795
- Fetches raw interactions for ``session_ids`` and — when ``query`` is
796
- non-empty and the compression layer is wired — runs an in-tool LLM call
797
- that compresses the raw turns into the smallest excerpt preserving every
798
- operand relevant to the query. Falls back to role-prefixed raw turns
799
- (truncated at ``max_chars_per_session``) on:
800
-
801
- - empty ``query``
802
- - no ``llm_client`` / ``prompt_manager`` available (test paths or
803
- backends that don't pass them through ``HandlerBundle``)
804
- - any compression-LLM exception
805
- - empty/whitespace compression output
806
-
807
- Args:
808
- args (ReadSessionTextArgs): Session ids (1–4), query, and per-session char cap.
809
- storage (Any): BaseStorage instance; must implement
810
- ``get_interactions_by_request_ids(request_ids: list[str])``.
811
- ctx (ExtractionCtx): Per-run state (unused for reads, present for consistency).
812
- llm_client (Any | None): LiteLLMClient for compression. Defaults to None
813
- so callers that don't enable compression still get raw turns.
814
- prompt_manager (Any | None): PromptManager for rendering the compression
815
- prompt. Defaults to None.
816
-
817
- Returns:
818
- dict[str, Any]: ``{"text": str}`` with compressed-or-raw session text,
819
- or ``{"error": str}`` when the storage backend doesn't support batch
820
- fetch or no interactions exist for any of the requested sessions.
821
- """
822
- try:
823
- interactions = storage.get_interactions_by_request_ids(args.session_ids)
824
- except AttributeError:
825
- return {"error": "read_session_text requires get_interactions_by_request_ids"}
826
- if not interactions:
827
- return {"error": "no interactions found for any of the requested sessions"}
828
-
829
- by_session: dict[str, list[Any]] = {}
830
- for i in interactions:
831
- sid = getattr(i, "request_id", None)
832
- if sid is None:
833
- continue
834
- by_session.setdefault(sid, []).append(i)
835
-
836
- # Clamp agent-supplied max_chars_per_session to a 16000 floor: agents
837
- # have been observed to pick values as low as 3000-4000 inconsistently,
838
- # which truncates Pattern G (recall-prior-statement) sessions before the
839
- # answer turn appears. The handler enforces the floor regardless of the
840
- # arg.
841
- effective_max_chars = max(args.max_chars_per_session, 16000)
842
- raw_text = _format_raw_turns(args.session_ids, by_session, effective_max_chars)
843
-
844
- # Skip compression on empty query or missing wiring; raw turns are still useful.
845
- if not args.query.strip() or llm_client is None or prompt_manager is None:
846
- ctx.rehydrated_excerpts.append(raw_text)
847
- return {"text": raw_text}
848
-
849
- compressed = _compress_raw_turns(raw_text, args.query, llm_client, prompt_manager)
850
- final_text = raw_text if compressed is None else compressed
851
- ctx.rehydrated_excerpts.append(final_text)
852
- return {"text": final_text}
853
-
854
-
855
- def _handle_rerank_user_profiles(
856
- args: RerankUserProfilesArgs, storage: Any, ctx: ExtractionCtx
857
- ) -> dict[str, Any]:
858
- """Rerank known profile ids with a local cross-encoder.
859
-
860
- Fetches the candidate profiles (scoped to ``ctx.user_id``), scores
861
- ``(query, content)`` pairs, and returns the top_k by descending score.
862
- Bumps ``search_count`` so reranking still counts against the search
863
- budget enforced by invariant A.
864
-
865
- Args:
866
- args (RerankUserProfilesArgs): Query, candidate ids, and top_k.
867
- storage (Any): BaseStorage instance.
868
- ctx (ExtractionCtx): Per-run state; ``search_count`` and
869
- ``known_ids`` updated in place.
870
-
871
- Returns:
872
- dict[str, Any]: ``{"hits": [...]}`` with LLM-facing profile
873
- projections sorted by descending relevance.
874
- """
875
- if not args.profile_ids:
876
- ctx.search_count += 1
877
- return {"hits": []}
878
- all_profiles = storage.get_user_profile(ctx.user_id)
879
- wanted = set(args.profile_ids)
880
- candidates = [
881
- p for p in all_profiles if (getattr(p, "profile_id", "") or "") in wanted
882
- ]
883
- ctx.search_count += 1
884
- if not candidates:
885
- return {"hits": []}
886
- # Lazy import — keeps unit-test collection fast and avoids loading
887
- # torch when no rerank tool call is made in a given run.
888
- from reflexio.server.llm.rerank import score_pairs
889
-
890
- scores = score_pairs(args.query, [p.content for p in candidates])
891
- ranked = sorted(
892
- zip(candidates, scores, strict=True),
893
- key=lambda pair: pair[1],
894
- reverse=True,
895
- )
896
- top = [profile for profile, _score in ranked[: _cap_top_k(args.top_k)]]
897
- for h in top:
898
- pid = getattr(h, "profile_id", "") or ""
899
- if pid:
900
- ctx.known_ids.add(pid)
901
- return {"hits": [_project_profile_for_llm(h) for h in top]}
902
-
903
-
904
- def _handle_storage_stats(
905
- args: StorageStatsArgs, # noqa: ARG001
906
- storage: Any,
907
- ctx: ExtractionCtx,
908
- ) -> dict[str, Any]:
909
- """Return profile/playbook counts and modified-time range for ``ctx.user_id``.
910
-
911
- Does not bump ``search_count`` — this is metadata, not retrieval.
912
-
913
- Args:
914
- args (StorageStatsArgs): No fields (sentinel call).
915
- storage (Any): BaseStorage instance.
916
- ctx (ExtractionCtx): Per-run state; only ``user_id`` is read.
917
-
918
- Returns:
919
- dict[str, Any]: Counts and ISO 8601 timestamps. Timestamps are
920
- ``None`` when the user has no profiles.
921
- """
922
- profiles = storage.get_user_profile(ctx.user_id)
923
- if profiles:
924
- timestamps = [p.last_modified_timestamp for p in profiles]
925
- oldest_ts = datetime.fromtimestamp(min(timestamps), tz=UTC).isoformat()
926
- newest_ts = datetime.fromtimestamp(max(timestamps), tz=UTC).isoformat()
927
- else:
928
- oldest_ts = None
929
- newest_ts = None
930
- playbook_count = storage.count_user_playbooks(user_id=ctx.user_id)
931
- return {
932
- "profile_count": len(profiles),
933
- "playbook_count": playbook_count,
934
- "oldest_profile_modified": oldest_ts,
935
- "newest_profile_modified": newest_ts,
936
- }
937
-
938
-
939
- def new_profile_id() -> str:
940
- """Generate a short (12-char hex) profile id.
941
-
942
- Format chosen for LLM tool-call reliability: full ``str(uuid.uuid4())``
943
- is 36 characters of hex+dashes, error-prone for smaller LLMs to copy
944
- verbatim from a search result back into a delete/update tool arg.
945
- Twelve hex chars is short enough for high-fidelity copy and long enough
946
- that birthday-paradox collision probability is vanishingly small at any
947
- realistic per-user scale (16^12 ≈ 2.8e14 unique values; PRIMARY KEY
948
- constraint catches the rare collision).
949
-
950
- Profile ids are LLM-facing because the agent receives them in
951
- ``search_user_profiles`` results and must echo them back when calling
952
- ``delete_user_profile`` / ``update_user_profile``. Playbook ids are
953
- INTEGER autoincrements and don't have this problem.
954
-
955
- Returns:
956
- str: 12 lowercase hex characters, e.g. ``"b8a3f74e2c91"``.
957
- """
958
- return uuid.uuid4().hex[:12]
959
-
960
-
961
- # ====================================================================
962
- # Commit-stage: apply a PlanOp to storage
963
- # ====================================================================
964
-
965
-
966
- def apply_plan_op(op: Any, storage: Any, ctx: ExtractionCtx) -> None:
967
- """Deterministically apply one PlanOp to storage. Called by commit_plan.
968
-
969
- Args:
970
- op (Any): A PlanOp variant (CreateUserProfileOp, DeleteUserProfileOp,
971
- CreateUserPlaybookOp, DeleteUserPlaybookOp).
972
- storage (Any): BaseStorage handle.
973
- ctx (ExtractionCtx): Per-run state providing user_id and agent_version.
974
-
975
- Raises:
976
- TypeError: If ``op`` is not a recognised PlanOp type.
977
- """
978
- if isinstance(op, CreateUserProfileOp):
979
- now_ts = int(datetime.now(UTC).timestamp())
980
- ttl = ProfileTimeToLive(op.ttl)
981
- storage.add_user_profile(
982
- ctx.user_id,
983
- [
984
- UserProfile(
985
- user_id=ctx.user_id,
986
- profile_id=new_profile_id(),
987
- content=op.content,
988
- profile_time_to_live=ttl,
989
- last_modified_timestamp=now_ts,
990
- expiration_timestamp=calculate_expiration_timestamp(now_ts, ttl),
991
- source="agentic_v2",
992
- source_span=op.source_span,
993
- generated_from_request_id=ctx.request_id,
994
- )
995
- ],
996
- )
997
- elif isinstance(op, DeleteUserProfileOp):
998
- storage.delete_profiles_by_ids([op.id])
999
- elif isinstance(op, CreateUserPlaybookOp):
1000
- new_playbook = UserPlaybook(
1001
- user_playbook_id=0, # storage assigns
1002
- user_id=ctx.user_id,
1003
- agent_version=ctx.agent_version,
1004
- request_id=ctx.request_id,
1005
- playbook_name=SINGLETON_USER_PLAYBOOK_NAME,
1006
- content=op.content,
1007
- trigger=op.trigger,
1008
- rationale=op.rationale,
1009
- source_span=op.source_span,
1010
- )
1011
- storage.save_user_playbooks([new_playbook])
1012
- elif isinstance(op, DeleteUserPlaybookOp):
1013
- try:
1014
- playbook_id = int(op.id)
1015
- except (TypeError, ValueError) as e:
1016
- raise TypeError(
1017
- f"DeleteUserPlaybookOp.id must be a numeric string, got {op.id!r}"
1018
- ) from e
1019
- storage.delete_user_playbooks_by_ids([playbook_id])
1020
- else:
1021
- raise TypeError(f"Unknown PlanOp: {type(op).__name__}")
1022
-
1023
-
1024
- # ====================================================================
1025
- # Bundle adapter + Tool registries
1026
- # ====================================================================
1027
-
1028
- from collections.abc import Callable # noqa: E402
1029
-
1030
- from reflexio.server.llm.tools import Tool # noqa: E402
1031
-
1032
-
1033
- def _bundle_handler(
1034
- inner: Callable[[Any, Any, Any], dict[str, Any]],
1035
- ) -> Callable[[Any, Any], dict[str, Any]]:
1036
- """Adapt a (args, storage, ctx)-style handler to (args, bundle) for run_tool_loop.
1037
-
1038
- Callers build a HandlerBundle with .storage and .ctx attributes; this
1039
- adapter unpacks them so the registry accepts our 3-arg handlers.
1040
-
1041
- Args:
1042
- inner (Callable[[Any, Any, Any], dict[str, Any]]): A handler callable
1043
- with signature ``(args, storage, ctx) -> dict``.
1044
-
1045
- Returns:
1046
- Callable[[Any, Any], dict[str, Any]]: A 2-arg callable
1047
- ``(args, bundle) -> dict`` compatible with ``Tool.handler``.
1048
- """
1049
-
1050
- def wrapped(args: Any, bundle: Any) -> dict[str, Any]:
1051
- return inner(args, bundle.storage, bundle.ctx)
1052
-
1053
- return wrapped
1054
-
1055
-
1056
- def _bundle_handler_with_llm(
1057
- inner: Callable[..., dict[str, Any]],
1058
- ) -> Callable[[Any, Any], dict[str, Any]]:
1059
- """Adapter variant for handlers that also need ``llm_client`` and
1060
- ``prompt_manager`` from the bundle (e.g., the compression-enabled
1061
- rehydration tool).
1062
-
1063
- The inner handler signature is
1064
- ``(args, storage, ctx, llm_client=None, prompt_manager=None) -> dict``.
1065
- Both LLM-side fields default to ``None`` so the handler degrades to a
1066
- no-LLM path when callers don't wire them through.
1067
-
1068
- Args:
1069
- inner (Callable[..., dict[str, Any]]): Handler accepting
1070
- ``(args, storage, ctx, llm_client, prompt_manager)``.
1071
-
1072
- Returns:
1073
- Callable[[Any, Any], dict[str, Any]]: A 2-arg callable compatible
1074
- with ``Tool.handler``.
1075
- """
1076
-
1077
- def wrapped(args: Any, bundle: Any) -> dict[str, Any]:
1078
- return inner(
1079
- args,
1080
- bundle.storage,
1081
- bundle.ctx,
1082
- llm_client=getattr(bundle, "llm_client", None),
1083
- prompt_manager=getattr(bundle, "prompt_manager", None),
1084
- )
1085
-
1086
- return wrapped
1087
-
1088
-
1089
- _READ_TOOLS = [
1090
- Tool(
1091
- name="search_user_profiles",
1092
- args_model=SearchUserProfilesArgs,
1093
- handler=_bundle_handler_with_llm(_handle_search_user_profiles),
1094
- ),
1095
- Tool(
1096
- name="get_user_profile",
1097
- args_model=GetUserProfileArgs,
1098
- handler=_bundle_handler(_handle_get_user_profile),
1099
- ),
1100
- Tool(
1101
- name="search_user_playbooks",
1102
- args_model=SearchUserPlaybooksArgs,
1103
- handler=_bundle_handler_with_llm(_handle_search_user_playbooks),
1104
- ),
1105
- Tool(
1106
- name="get_user_playbook",
1107
- args_model=GetUserPlaybookArgs,
1108
- handler=_bundle_handler(_handle_get_user_playbook),
1109
- ),
1110
- Tool(
1111
- name="search_agent_playbooks",
1112
- args_model=SearchAgentPlaybooksArgs,
1113
- handler=_bundle_handler_with_llm(_handle_search_agent_playbooks),
1114
- ),
1115
- Tool(
1116
- name="get_agent_playbook",
1117
- args_model=GetAgentPlaybookArgs,
1118
- handler=_bundle_handler(_handle_get_agent_playbook),
1119
- ),
1120
- Tool(
1121
- name="read_session_text",
1122
- args_model=ReadSessionTextArgs,
1123
- handler=_bundle_handler_with_llm(_handle_read_session_text),
1124
- ),
1125
- ]