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
@@ -34,6 +34,7 @@ class ProfileMixin:
34
34
  self,
35
35
  user_id: str,
36
36
  status_filter: list[Status | None] | None = None,
37
+ tags: list[str] | None = None,
37
38
  ) -> list[UserProfile]:
38
39
  raise NotImplementedError
39
40
 
@@ -78,6 +79,13 @@ class ProfileMixin:
78
79
  ) -> None:
79
80
  raise NotImplementedError
80
81
 
82
+ @abstractmethod
83
+ def update_user_profile_tags(
84
+ self, user_id: str, profile_id: str, tags: list[str]
85
+ ) -> None:
86
+ """Replace only the tags of a profile, leaving content and embedding untouched."""
87
+ raise NotImplementedError
88
+
81
89
  @abstractmethod
82
90
  def delete_all_interactions_for_user(self, user_id: str) -> None:
83
91
  raise NotImplementedError
@@ -178,6 +186,22 @@ class ProfileMixin:
178
186
  """
179
187
  raise NotImplementedError
180
188
 
189
+ @abstractmethod
190
+ def get_profile_by_id(
191
+ self, profile_id: str, *, include_tombstones: bool = False
192
+ ) -> UserProfile | None:
193
+ """Fetch a single profile by primary key.
194
+
195
+ Args:
196
+ profile_id: The profile's primary key.
197
+ include_tombstones: When False (default), MERGED/SUPERSEDED profiles
198
+ return None. Set to True for lineage resolution (resolve_current).
199
+
200
+ Returns:
201
+ The UserProfile if found and not filtered, otherwise None.
202
+ """
203
+ raise NotImplementedError
204
+
181
205
  @abstractmethod
182
206
  def archive_profile_by_id(self, user_id: str, profile_id: str) -> bool:
183
207
  """Atomically archive a single profile by id, only if currently CURRENT.
@@ -220,17 +244,113 @@ class ProfileMixin:
220
244
  raise NotImplementedError
221
245
 
222
246
  @abstractmethod
223
- def delete_profiles_by_ids(self, profile_ids: list[str]) -> int:
247
+ def delete_profiles_by_ids(
248
+ self, profile_ids: list[str], *, emit_hard_delete: bool = True
249
+ ) -> int:
224
250
  """Delete profiles by their IDs.
225
251
 
226
252
  Args:
227
253
  profile_ids (list[str]): List of profile IDs to delete
254
+ emit_hard_delete (bool): When True (default), emit a ``hard_delete``
255
+ lineage event for each id before removing the row. Pass ``False``
256
+ from rollback callers that cleaned up a never-CURRENT successor to
257
+ avoid poisoning the audit log with phantom erasures.
228
258
 
229
259
  Returns:
230
260
  int: Number of profiles deleted
231
261
  """
232
262
  raise NotImplementedError
233
263
 
264
+ @abstractmethod
265
+ def get_distinct_generated_from_request_ids(self) -> list[str]:
266
+ """Return the DISTINCT non-empty generated_from_request_id values present on profiles.
267
+
268
+ Scoped to the org. Includes profiles of any status (tombstones included) so
269
+ that an add-only run whose profiles were later tombstoned is still discoverable.
270
+ Empty-string values are excluded by the query (they must never form a group).
271
+
272
+ Used by ``reconstruct_profile_change_log`` to discover add-only dedup runs
273
+ (runs that added new profiles but superseded nothing, which emit no lineage
274
+ event and would otherwise be invisible).
275
+
276
+ Returns:
277
+ list[str]: Distinct non-empty ``generated_from_request_id`` values.
278
+ """
279
+ raise NotImplementedError
280
+
281
+ @abstractmethod
282
+ def get_profiles_by_generated_from_request_id(
283
+ self,
284
+ request_id: str,
285
+ ) -> list[UserProfile]:
286
+ """Return all profiles (any status, including tombstones) for a given generated_from_request_id.
287
+
288
+ Scoped to the org. Used by reconstruct_profile_change_log to find the
289
+ "added" side of a dedup run without depending on mutable status columns.
290
+ The column is set at profile creation and never changes — it is the
291
+ time-travel-stable signal for "added in run R".
292
+
293
+ Args:
294
+ request_id (str): The generated_from_request_id value to filter on.
295
+
296
+ Returns:
297
+ list[UserProfile]: All profiles (live or tombstone) whose
298
+ ``generated_from_request_id`` matches, scoped by org.
299
+ """
300
+ raise NotImplementedError
301
+
302
+ def get_all_generated_profiles(self) -> list[UserProfile]:
303
+ """Return every profile (any status, incl. tombstones) with a non-empty
304
+ ``generated_from_request_id``, scoped to the org.
305
+
306
+ Bulk equivalent of :meth:`get_profiles_by_generated_from_request_id` — it
307
+ lets ``reconstruct_profile_change_log`` resolve the "added" side of every
308
+ run in a single query instead of one read per candidate request_id (an
309
+ org-history-sized fan-out on network-backed storage).
310
+
311
+ The default implementation loops the per-id reads, so every backend is
312
+ correct without an override; backends SHOULD override it with a single
313
+ ``WHERE generated_from_request_id <> ''`` query for the performance win.
314
+
315
+ Returns:
316
+ list[UserProfile]: All profiles with a non-empty
317
+ ``generated_from_request_id``.
318
+ """
319
+ out: list[UserProfile] = []
320
+ for request_id in self.get_distinct_generated_from_request_ids():
321
+ out.extend(self.get_profiles_by_generated_from_request_id(request_id))
322
+ return out
323
+
324
+ @abstractmethod
325
+ def supersede_profiles_by_ids(
326
+ self,
327
+ user_id: str,
328
+ profile_ids: list[str],
329
+ request_id: str,
330
+ ) -> list[str]:
331
+ """Soft-delete profiles by setting status to SUPERSEDED, emitting set-based lineage.
332
+
333
+ For each profile id that matches (user_id, current status in {NULL/CURRENT,
334
+ PENDING}), updates status to SUPERSEDED and emits one ``status_change``
335
+ lineage event under the shared ``request_id``. Rows are NOT physically
336
+ deleted — reads that exclude tombstones will simply filter them out by status.
337
+
338
+ Args:
339
+ user_id (str): Owning user id. Predicate scoped to this user.
340
+ profile_ids (list[str]): Profile ids to supersede. Already-superseded or
341
+ non-existent ids are silently skipped.
342
+ request_id (str): Shared request id to stamp on all emitted events so the
343
+ entire dedup run is reconstructible from a single id.
344
+
345
+ Returns:
346
+ list[str]: The profile ids that were actually superseded by this call, in
347
+ input order. Already-superseded or non-existent ids are omitted (so an
348
+ empty list means nothing committed). Callers needing a count use
349
+ ``len(...)``. This is the commit-accurate set used to build a
350
+ commit-atomic legacy change-log "removed" entry.
351
+ """
352
+ raise NotImplementedError
353
+
234
354
  # Search methods
235
355
  @abstractmethod
236
356
  def search_interaction(
@@ -4,6 +4,7 @@ from reflexio.models.api_schema.domain import Request
4
4
  from reflexio.models.api_schema.internal_schema import (
5
5
  RequestInteractionDataModel,
6
6
  SessionDescriptor,
7
+ SessionFirstRequest,
7
8
  )
8
9
 
9
10
 
@@ -152,3 +153,62 @@ class RequestMixin:
152
153
  list[SessionDescriptor]: Deduped descriptors ordered by session_id.
153
154
  """
154
155
  raise NotImplementedError
156
+
157
+ def get_first_requests_by_session_ids(
158
+ self, session_ids: list[str]
159
+ ) -> dict[str, SessionFirstRequest]:
160
+ """Return earliest-request metadata for each requested session.
161
+
162
+ Default implementation preserves the historical storage contract by
163
+ calling ``get_sessions`` per session. SQL backends should override with
164
+ a set-based query.
165
+ """
166
+ out: dict[str, SessionFirstRequest] = {}
167
+ for session_id in set(session_ids):
168
+ requests = []
169
+ page_size = 1000
170
+ offset = 0
171
+ while True:
172
+ grouped = self.get_sessions(
173
+ session_id=session_id,
174
+ top_k=page_size,
175
+ offset=offset,
176
+ )
177
+ rows = grouped.get(session_id) or []
178
+ requests.extend(r.request for r in rows if r.request is not None)
179
+ if len(rows) < page_size:
180
+ break
181
+ offset += page_size
182
+ if not requests:
183
+ continue
184
+ first = min(requests, key=lambda r: r.created_at)
185
+ out[session_id] = SessionFirstRequest(
186
+ session_id=session_id,
187
+ user_id=first.user_id,
188
+ source=first.source or "",
189
+ created_at=first.created_at,
190
+ )
191
+ return out
192
+
193
+ def get_first_requests_by_user_session_pairs(
194
+ self, pairs: list[tuple[str, str]]
195
+ ) -> dict[tuple[str, str], SessionFirstRequest]:
196
+ """Return earliest-request metadata for each requested (user_id, session_id).
197
+
198
+ Default implementation preserves backend compatibility by calling the
199
+ existing user-scoped request lookup per distinct pair. SQL backends
200
+ override with set-based queries.
201
+ """
202
+ out: dict[tuple[str, str], SessionFirstRequest] = {}
203
+ for user_id, session_id in set(pairs):
204
+ requests = self.get_requests_by_session(user_id, session_id)
205
+ if not requests:
206
+ continue
207
+ first = min(requests, key=lambda r: r.created_at)
208
+ out[(user_id, session_id)] = SessionFirstRequest(
209
+ session_id=session_id,
210
+ user_id=user_id,
211
+ source=first.source or "",
212
+ created_at=first.created_at,
213
+ )
214
+ return out
@@ -0,0 +1,51 @@
1
+ from reflexio.models.api_schema.domain.entities import PlaybookRetrievalLog
2
+
3
+
4
+ class RetrievalLogMixin:
5
+ """Mixin for playbook retrieval log storage methods.
6
+
7
+ These methods are optional retrieval-capture storage hooks. They are
8
+ intentionally concrete (not ``@abstractmethod``) so concrete storage classes
9
+ remain instantiable; each raises ``NotImplementedError`` until a backend
10
+ provides a real implementation.
11
+ """
12
+
13
+ def save_playbook_retrieval_log(self, log: PlaybookRetrievalLog) -> int:
14
+ """Persist a retrieval log entry and return its assigned id.
15
+
16
+ Args:
17
+ log (PlaybookRetrievalLog): The log entry to save. ``retrieval_log_id``
18
+ may be 0; the storage layer assigns a real id on insert.
19
+
20
+ Returns:
21
+ int: The assigned ``retrieval_log_id``.
22
+ """
23
+ raise NotImplementedError
24
+
25
+ def get_playbook_retrieval_logs(
26
+ self,
27
+ *,
28
+ session_id: str | None = None,
29
+ request_id: str | None = None,
30
+ interaction_id: int | None = None,
31
+ user_id: str | None = None,
32
+ start_time: int | None = None,
33
+ end_time: int | None = None,
34
+ ) -> list[PlaybookRetrievalLog]:
35
+ """Retrieve playbook retrieval log entries, optionally filtered.
36
+
37
+ Args:
38
+ session_id (str | None): Filter to logs for this session. If None,
39
+ no session filter is applied.
40
+ request_id (str | None): Filter to logs for this request. If None,
41
+ no request filter is applied.
42
+ interaction_id (int | None): Filter to logs for this interaction.
43
+ user_id (str | None): Filter to logs for this user.
44
+ start_time (int | None): Inclusive lower bound on ``created_at``.
45
+ end_time (int | None): Inclusive upper bound on ``created_at``.
46
+
47
+ Returns:
48
+ list[PlaybookRetrievalLog]: Matching log entries, ordered by
49
+ ``created_at`` ascending.
50
+ """
51
+ raise NotImplementedError
@@ -108,6 +108,23 @@ class ShadowVerdictsMixin:
108
108
  f"{type(self).__name__} does not support shadow_comparison_verdicts"
109
109
  )
110
110
 
111
+ def get_recent_shadow_comparison_verdicts(
112
+ self,
113
+ from_ts: int,
114
+ to_ts: int,
115
+ judge_prompt_version: str,
116
+ limit: int,
117
+ ) -> list[ShadowComparisonVerdict]:
118
+ """Fetch newest verdicts in descending ``created_at`` order."""
119
+ if limit <= 0:
120
+ return []
121
+ verdicts = self.get_shadow_comparison_verdicts(
122
+ from_ts=from_ts,
123
+ to_ts=to_ts,
124
+ judge_prompt_version=judge_prompt_version,
125
+ )
126
+ return list(reversed(verdicts))[:limit]
127
+
111
128
  def delete_shadow_comparison_verdicts_by_session(self, session_id: str) -> int:
112
129
  """Remove all verdicts for one session.
113
130
 
@@ -0,0 +1,8 @@
1
+ # tagging
2
+
3
+ Compact post-generation entity tagging capability.
4
+
5
+ - `service.py` tags profiles and playbooks with the configured tagging prompts.
6
+ - `tagging_scheduler.py` debounces post-publish tagging and rebuilds request context for background execution.
7
+
8
+ This package intentionally does not use `components/`: the service and scheduler are the only module responsibilities.
@@ -0,0 +1 @@
1
+ """Post-generation entity tagging services."""
@@ -0,0 +1,200 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+
6
+ from pydantic import ConfigDict, Field
7
+
8
+ from reflexio.models.api_schema.service_schemas import (
9
+ AgentPlaybook,
10
+ UserPlaybook,
11
+ UserProfile,
12
+ )
13
+ from reflexio.models.structured_output import StrictStructuredOutput
14
+ from reflexio.server.api_endpoints.request_context import RequestContext
15
+ from reflexio.server.llm.litellm_client import LiteLLMClient
16
+ from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
17
+ from reflexio.server.services.service_utils import log_llm_messages, log_model_response
18
+ from reflexio.server.site_var.site_var_manager import SiteVarManager
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ TAGGING_PROMPT_ID = "tagging"
23
+
24
+ # Upper bound on how many current playbooks a single tagging pass will scan for
25
+ # untagged entries. Tagging is idempotent (already-tagged entities are skipped),
26
+ # so steady-state passes only tag newly generated entities; this bound just keeps
27
+ # a one-time backfill from being silently truncated at the storage default of 100.
28
+ _TAGGING_FETCH_LIMIT = 1000
29
+
30
+
31
+ class TagsOutput(StrictStructuredOutput):
32
+ tags: list[str] = Field(default_factory=list)
33
+
34
+ model_config = ConfigDict(
35
+ extra="allow",
36
+ json_schema_extra={"additionalProperties": False},
37
+ )
38
+
39
+
40
+ class TaggingService:
41
+ def __init__(
42
+ self,
43
+ llm_client: LiteLLMClient,
44
+ request_context: RequestContext,
45
+ ) -> None:
46
+ self.client = llm_client
47
+ self.request_context = request_context
48
+ self.configurator = request_context.configurator
49
+ self.storage = request_context.storage
50
+
51
+ model_setting = SiteVarManager().get_site_var("llm_model_setting")
52
+ site_var = model_setting if isinstance(model_setting, dict) else {}
53
+ config = self.configurator.get_config()
54
+ llm_config = config.llm_config if config else None
55
+ api_key_config = config.api_key_config if config else None
56
+ self.model_name = resolve_model_name(
57
+ ModelRole.GENERATION,
58
+ site_var_value=site_var.get("default_generation_model_name"),
59
+ config_override=llm_config.generation_model_name if llm_config else None,
60
+ api_key_config=api_key_config,
61
+ )
62
+
63
+ def run(
64
+ self,
65
+ *,
66
+ user_id: str,
67
+ agent_version: str,
68
+ tag_profiles: bool = True,
69
+ tag_playbooks: bool = True,
70
+ ) -> None:
71
+ if self.storage is None:
72
+ return
73
+
74
+ config = self.configurator.get_config()
75
+ if config is None:
76
+ return
77
+
78
+ profile_prompt = getattr(
79
+ config.profile_extractor_config, "tagging_definition_prompt", None
80
+ )
81
+ user_playbook_prompt = getattr(
82
+ config.user_playbook_extractor_config, "tagging_definition_prompt", None
83
+ )
84
+
85
+ if tag_profiles and profile_prompt:
86
+ self._tag_profiles(
87
+ user_id=user_id, tagging_definition_prompt=profile_prompt
88
+ )
89
+ if tag_playbooks and user_playbook_prompt:
90
+ self._tag_user_playbooks(
91
+ user_id=user_id,
92
+ agent_version=agent_version,
93
+ tagging_definition_prompt=user_playbook_prompt,
94
+ )
95
+ self._tag_agent_playbooks(
96
+ agent_version=agent_version,
97
+ tagging_definition_prompt=user_playbook_prompt,
98
+ )
99
+
100
+ def _tag_profiles(self, *, user_id: str, tagging_definition_prompt: str) -> None:
101
+ profiles = self.storage.get_user_profile(user_id) # type: ignore[union-attr]
102
+ for profile in profiles:
103
+ if profile.tags is not None:
104
+ continue # already tagged (incl. empty result) — tag each entity once
105
+ tags = self._generate_tags(
106
+ tagging_definition_prompt=tagging_definition_prompt,
107
+ content=self._profile_content(profile),
108
+ )
109
+ self.storage.update_user_profile_tags( # type: ignore[union-attr]
110
+ user_id, profile.profile_id, tags
111
+ )
112
+
113
+ def _tag_user_playbooks(
114
+ self,
115
+ *,
116
+ user_id: str,
117
+ agent_version: str,
118
+ tagging_definition_prompt: str,
119
+ ) -> None:
120
+ playbooks = self.storage.get_user_playbooks( # type: ignore[union-attr]
121
+ limit=_TAGGING_FETCH_LIMIT,
122
+ user_id=user_id,
123
+ agent_version=agent_version,
124
+ status_filter=[None],
125
+ )
126
+ for playbook in playbooks:
127
+ if playbook.tags is not None:
128
+ continue # already tagged (incl. empty result) — tag each entity once
129
+ tags = self._generate_tags(
130
+ tagging_definition_prompt=tagging_definition_prompt,
131
+ content=self._playbook_content(playbook),
132
+ )
133
+ self.storage.update_user_playbook( # type: ignore[union-attr]
134
+ playbook.user_playbook_id,
135
+ tags=tags,
136
+ )
137
+
138
+ def _tag_agent_playbooks(
139
+ self,
140
+ *,
141
+ agent_version: str,
142
+ tagging_definition_prompt: str,
143
+ ) -> None:
144
+ playbooks = self.storage.get_agent_playbooks( # type: ignore[union-attr]
145
+ limit=_TAGGING_FETCH_LIMIT,
146
+ agent_version=agent_version,
147
+ status_filter=[None],
148
+ )
149
+ for playbook in playbooks:
150
+ if playbook.tags is not None:
151
+ continue # already tagged (incl. empty result) — tag each entity once
152
+ tags = self._generate_tags(
153
+ tagging_definition_prompt=tagging_definition_prompt,
154
+ content=self._playbook_content(playbook),
155
+ )
156
+ self.storage.update_agent_playbook( # type: ignore[union-attr]
157
+ playbook.agent_playbook_id,
158
+ tags=tags,
159
+ )
160
+
161
+ def _generate_tags(
162
+ self, *, tagging_definition_prompt: str, content: str
163
+ ) -> list[str]:
164
+ if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
165
+ return ["example_tag"]
166
+
167
+ prompt = self.request_context.prompt_manager.render_prompt(
168
+ TAGGING_PROMPT_ID,
169
+ {
170
+ "tagging_definition_prompt": tagging_definition_prompt.strip(),
171
+ "content": content,
172
+ },
173
+ )
174
+ messages = [{"role": "user", "content": prompt}]
175
+ log_llm_messages(logger, "Tagging", messages)
176
+ response = self.client.generate_chat_response(
177
+ messages=messages,
178
+ model=self.model_name,
179
+ response_format=TagsOutput,
180
+ )
181
+ log_model_response(logger, "Tagging response", response)
182
+ if not isinstance(response, TagsOutput):
183
+ logger.warning(
184
+ "Unexpected response type from tagging LLM: %s", type(response)
185
+ )
186
+ return []
187
+ return [tag.strip() for tag in response.tags if tag.strip()]
188
+
189
+ @staticmethod
190
+ def _profile_content(profile: UserProfile) -> str:
191
+ return profile.content
192
+
193
+ @staticmethod
194
+ def _playbook_content(playbook: UserPlaybook | AgentPlaybook) -> str:
195
+ parts = [playbook.content]
196
+ if playbook.trigger:
197
+ parts.append(f"Trigger: {playbook.trigger}")
198
+ if playbook.rationale:
199
+ parts.append(f"Rationale: {playbook.rationale}")
200
+ return "\n".join(part for part in parts if part)
@@ -0,0 +1,149 @@
1
+ """Singleton scheduler for deferred, off-publish-path entity tagging.
2
+
3
+ Tagging runs an LLM call per newly generated profile/playbook, so it must not
4
+ block the publish request. This scheduler mirrors
5
+ :class:`GroupEvaluationScheduler`: a single daemon thread with a min-heap, where
6
+ each publish upserts the fire time for its ``(org_id, user_id, agent_version)``
7
+ key. Rapid successive publishes for the same key debounce into a single tagging
8
+ pass; when the timer fires, the tagging callback runs on its own daemon thread.
9
+
10
+ Tagging is idempotent (already-tagged entities are skipped), so a deferred pass
11
+ naturally handles both newly generated entities and the one-time backfill that
12
+ happens when a tagging definition is first configured.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import heapq
18
+ import logging
19
+ import os
20
+ import threading
21
+ import time
22
+ from collections.abc import Callable
23
+
24
+ from reflexio.server.api_endpoints.request_context import RequestContext
25
+ from reflexio.server.llm.litellm_client import LiteLLMClient
26
+ from reflexio.server.services.tagging.service import TaggingService
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Delay before a tagging pass fires after the last publish for a key. Long enough
31
+ # to debounce a burst of publishes into one pass, short enough that tags appear
32
+ # promptly. Kept as a patch point for tests.
33
+ TAGGING_DELAY_SECONDS = 15
34
+ IS_TEST_ENV = os.environ.get("IS_TEST_ENV", "false").strip().lower() == "true"
35
+ _EFFECTIVE_DELAY_SECONDS = 1 if IS_TEST_ENV else TAGGING_DELAY_SECONDS
36
+
37
+ # (org_id, user_id, agent_version)
38
+ TaggingKey = tuple[str, str, str]
39
+
40
+
41
+ class TaggingScheduler:
42
+ """Singleton scheduler that fires entity tagging after a short debounce."""
43
+
44
+ _instance = None
45
+ _lock = threading.Lock()
46
+
47
+ @classmethod
48
+ def get_instance(cls) -> TaggingScheduler:
49
+ if cls._instance is None:
50
+ with cls._lock:
51
+ if cls._instance is None:
52
+ cls._instance = cls()
53
+ return cls._instance
54
+
55
+ def __init__(self) -> None:
56
+ self._scheduled: dict[TaggingKey, tuple[float, Callable]] = {}
57
+ self._heap: list[tuple[float, TaggingKey]] = []
58
+ self._mutex = threading.Lock()
59
+ self._wake_event = threading.Event()
60
+ self._thread = threading.Thread(
61
+ target=self._scheduler_loop, daemon=True, name="tagging-scheduler"
62
+ )
63
+ self._thread.start()
64
+ logger.info("TaggingScheduler started")
65
+
66
+ def schedule(self, key: TaggingKey, callback: Callable) -> None:
67
+ """Schedule or reschedule a tagging pass for ``key`` (slides the fire time forward)."""
68
+ fire_time = time.monotonic() + _EFFECTIVE_DELAY_SECONDS
69
+ with self._mutex:
70
+ self._scheduled[key] = (fire_time, callback)
71
+ heapq.heappush(self._heap, (fire_time, key))
72
+ self._wake_event.set()
73
+
74
+ def _scheduler_loop(self) -> None:
75
+ while True:
76
+ try:
77
+ with self._mutex:
78
+ next_fire_time = self._heap[0][0] if self._heap else None
79
+
80
+ if next_fire_time is None:
81
+ self._wake_event.wait()
82
+ self._wake_event.clear()
83
+ continue
84
+
85
+ wait_seconds = next_fire_time - time.monotonic()
86
+ if wait_seconds > 0:
87
+ self._wake_event.wait(timeout=wait_seconds)
88
+ self._wake_event.clear()
89
+ continue
90
+
91
+ with self._mutex:
92
+ while self._heap and self._heap[0][0] <= time.monotonic():
93
+ fire_time, key = heapq.heappop(self._heap)
94
+
95
+ current = self._scheduled.get(key)
96
+ if current is None:
97
+ continue
98
+ current_fire_time, callback = current
99
+ if abs(current_fire_time - fire_time) > 0.001:
100
+ # Superseded by a newer schedule for the same key.
101
+ continue
102
+
103
+ del self._scheduled[key]
104
+ t = threading.Thread(
105
+ target=self._run_callback,
106
+ args=(key, callback),
107
+ daemon=True,
108
+ name=f"tagging-{key[1][:20]}",
109
+ )
110
+ t.start()
111
+ except Exception:
112
+ logger.exception("Error in tagging scheduler loop")
113
+ time.sleep(1)
114
+
115
+ @staticmethod
116
+ def _run_callback(key: TaggingKey, callback: Callable) -> None:
117
+ try:
118
+ logger.info("Firing tagging for key=%s", key)
119
+ callback()
120
+ logger.info("Completed tagging for key=%s", key)
121
+ except Exception:
122
+ logger.exception("Tagging callback failed for key=%s", key)
123
+
124
+
125
+ def schedule_tagging(
126
+ *,
127
+ org_id: str,
128
+ user_id: str,
129
+ agent_version: str,
130
+ request_context: RequestContext,
131
+ llm_client: LiteLLMClient,
132
+ ) -> None:
133
+ """Enqueue a deferred tagging pass for the given user/agent off the publish path."""
134
+ if not user_id:
135
+ return
136
+
137
+ key: TaggingKey = (org_id, user_id, agent_version)
138
+ storage_base_dir = getattr(request_context, "storage_base_dir", None)
139
+
140
+ def callback() -> None:
141
+ fresh_context = RequestContext(
142
+ org_id=org_id,
143
+ storage_base_dir=storage_base_dir,
144
+ )
145
+ TaggingService(llm_client=llm_client, request_context=fresh_context).run(
146
+ user_id=user_id, agent_version=agent_version
147
+ )
148
+
149
+ TaggingScheduler.get_instance().schedule(key, callback)