claude-smart 0.2.42 → 0.2.44

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 (402) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +1 -1
  3. package/bin/claude-smart.js +2 -2
  4. package/package.json +9 -3
  5. package/plugin/.claude-plugin/plugin.json +9 -3
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +23 -3
  8. package/plugin/pyproject.toml +3 -3
  9. package/plugin/scripts/_lib.sh +91 -0
  10. package/plugin/scripts/backend-service.sh +51 -4
  11. package/plugin/scripts/cli.sh +3 -1
  12. package/plugin/scripts/codex-hook.js +72 -4
  13. package/plugin/scripts/dashboard-build.sh +1 -0
  14. package/plugin/scripts/dashboard-service.sh +1 -0
  15. package/plugin/scripts/ensure-plugin-root.sh +1 -0
  16. package/plugin/scripts/hook_entry.sh +6 -3
  17. package/plugin/scripts/smart-install.sh +3 -2
  18. package/plugin/src/README.md +57 -0
  19. package/plugin/src/claude_smart/context_format.py +11 -12
  20. package/plugin/src/claude_smart/cs_cite.py +26 -12
  21. package/plugin/src/claude_smart/ids.py +13 -5
  22. package/plugin/uv.lock +126 -5
  23. package/plugin/vendor/reflexio/.env.example +62 -0
  24. package/plugin/vendor/reflexio/LICENSE +201 -0
  25. package/plugin/vendor/reflexio/README.md +338 -0
  26. package/plugin/vendor/reflexio/pyproject.toml +274 -0
  27. package/plugin/vendor/reflexio/reflexio/README.md +184 -0
  28. package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
  29. package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
  30. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
  31. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
  32. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
  33. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
  34. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
  35. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
  36. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
  37. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
  38. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
  39. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
  40. package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
  41. package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
  42. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
  43. package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
  44. package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
  45. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +265 -0
  46. package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
  47. package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
  48. package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
  49. package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
  50. package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
  51. package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
  52. package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
  53. package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
  54. package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +964 -0
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
  62. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
  63. package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
  64. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
  65. package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
  66. package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
  67. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
  68. package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
  69. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
  70. package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
  71. package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
  72. package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
  73. package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
  74. package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
  75. package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
  145. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
  146. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
  147. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
  148. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
  149. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
  150. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
  151. package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
  152. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
  153. package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
  154. package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
  155. package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
  156. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
  157. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
  158. package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
  159. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
  160. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
  161. package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
  162. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
  163. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
  164. package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
  165. package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
  166. package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
  167. package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
  168. package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
  169. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
  170. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1112 -0
  171. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
  172. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
  173. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
  174. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
  175. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +768 -0
  176. package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
  177. package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
  178. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
  179. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
  180. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
  181. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
  182. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +333 -0
  183. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
  184. package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
  185. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
  186. package/plugin/vendor/reflexio/reflexio/server/README.md +622 -0
  187. package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
  188. package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
  189. package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
  190. package/plugin/vendor/reflexio/reflexio/server/api.py +2868 -0
  191. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +34 -0
  192. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
  193. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
  194. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
  195. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
  196. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +562 -0
  197. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
  198. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
  199. package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
  200. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
  201. package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
  202. package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
  203. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +359 -0
  204. package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
  205. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1871 -0
  206. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +140 -0
  207. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +479 -0
  208. package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
  209. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
  210. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
  211. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +338 -0
  212. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
  213. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +288 -0
  214. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
  215. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +187 -0
  216. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
  217. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +716 -0
  218. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
  219. package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
  220. package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
  221. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
  222. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
  223. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
  224. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
  225. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
  226. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
  227. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
  228. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
  229. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
  230. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
  231. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
  232. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
  233. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
  234. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
  235. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
  236. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
  237. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
  238. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
  239. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
  240. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
  241. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
  242. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
  243. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
  244. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md +69 -0
  245. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +71 -0
  246. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
  247. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
  248. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +283 -0
  249. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md +234 -0
  250. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +244 -0
  251. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
  252. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
  253. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
  254. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
  255. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
  256. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +137 -0
  257. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
  258. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
  259. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
  260. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
  261. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
  262. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
  263. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
  264. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
  265. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
  266. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
  267. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
  268. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
  269. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
  270. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
  271. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
  272. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
  273. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
  274. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
  275. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
  276. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
  277. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
  278. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
  279. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
  280. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
  281. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
  282. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
  283. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/README.md +58 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
  289. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
  290. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +336 -0
  292. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1668 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
  296. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
  297. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/embedding_text.py +62 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
  311. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
  312. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
  313. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
  318. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +358 -0
  319. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
  320. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
  321. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +535 -0
  322. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
  323. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +779 -0
  324. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
  325. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +94 -0
  326. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
  327. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +702 -0
  328. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
  329. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
  330. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
  331. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +1045 -0
  332. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
  333. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
  334. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
  335. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
  336. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
  337. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
  338. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
  339. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
  340. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
  341. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
  342. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
  343. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
  344. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
  345. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
  346. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
  347. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
  348. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +772 -0
  349. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
  350. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +737 -0
  351. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
  352. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
  353. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
  354. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +803 -0
  355. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
  356. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
  357. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +80 -0
  358. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
  359. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +756 -0
  360. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
  361. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
  362. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
  363. package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
  364. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
  365. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
  366. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
  367. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
  368. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1298 -0
  369. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
  370. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
  371. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
  372. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
  373. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
  374. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
  375. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
  376. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
  377. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
  378. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
  379. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +384 -0
  380. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
  381. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
  382. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
  383. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
  384. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
  385. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
  386. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
  387. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
  388. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
  389. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +572 -0
  390. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
  391. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
  392. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
  393. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
  394. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
  395. package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
  396. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
  397. package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
  398. package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
  399. package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
  400. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
  401. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
  402. package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
@@ -0,0 +1,2868 @@
1
+ import asyncio
2
+ import inspect
3
+ import logging
4
+ import os
5
+ from collections.abc import Callable
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from datetime import UTC, datetime
8
+ from typing import Any
9
+
10
+ from fastapi import (
11
+ APIRouter,
12
+ BackgroundTasks,
13
+ Depends,
14
+ FastAPI,
15
+ HTTPException,
16
+ Request,
17
+ status,
18
+ )
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+ from slowapi import Limiter, _rate_limit_exceeded_handler
21
+ from slowapi.errors import RateLimitExceeded
22
+ from slowapi.util import get_remote_address
23
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
24
+ from starlette.responses import Response
25
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
26
+
27
+ from reflexio.models.api_schema.braintrust_schema import (
28
+ BraintrustStatusResponse,
29
+ ConnectBraintrustRequest,
30
+ ConnectBraintrustResponse,
31
+ SelectProjectsRequest,
32
+ SelectProjectsResponse,
33
+ SyncBraintrustResponse,
34
+ )
35
+ from reflexio.models.api_schema.eval_overview_schema import (
36
+ GetEvaluationOverviewRequest,
37
+ GetEvaluationOverviewResponse,
38
+ GetRecentShadowComparisonsResponse,
39
+ GradeOnDemandRequest,
40
+ GradeOnDemandResponse,
41
+ RegenerateFailure,
42
+ RegenerateRequest,
43
+ RegenerateStartResponse,
44
+ RegenerateStatusResponse,
45
+ )
46
+ from reflexio.models.api_schema.retriever_schema import (
47
+ GetAgentPlaybooksRequest,
48
+ GetAgentPlaybooksViewResponse,
49
+ GetAgentSuccessEvaluationResultsRequest,
50
+ GetDashboardStatsRequest,
51
+ GetDashboardStatsResponse,
52
+ GetEvaluationResultsViewResponse,
53
+ GetInteractionsRequest,
54
+ GetInteractionsViewResponse,
55
+ GetPlaybookApplicationStatsRequest,
56
+ GetPlaybookApplicationStatsResponse,
57
+ GetProfileStatisticsResponse,
58
+ GetProfilesViewResponse,
59
+ GetRequestsRequest,
60
+ GetRequestsViewResponse,
61
+ GetUserPlaybooksRequest,
62
+ GetUserPlaybooksViewResponse,
63
+ GetUserProfilesRequest,
64
+ ProfileChangeLogViewResponse,
65
+ RequestDataView,
66
+ RerankUserProfilesRequest,
67
+ SearchAgentPlaybookRequest,
68
+ SearchAgentPlaybooksViewResponse,
69
+ SearchInteractionRequest,
70
+ SearchInteractionsViewResponse,
71
+ SearchProfilesViewResponse,
72
+ SearchUserPlaybookRequest,
73
+ SearchUserPlaybooksViewResponse,
74
+ SearchUserProfileRequest,
75
+ SessionView,
76
+ SetConfigResponse,
77
+ StorageStatsRequest,
78
+ StorageStatsResponse,
79
+ UnifiedSearchRequest,
80
+ UnifiedSearchViewResponse,
81
+ UpdateAgentPlaybookRequest,
82
+ UpdateAgentPlaybookResponse,
83
+ UpdatePlaybookStatusRequest,
84
+ UpdatePlaybookStatusResponse,
85
+ UpdateUserPlaybookRequest,
86
+ UpdateUserPlaybookResponse,
87
+ UpdateUserProfileRequest,
88
+ UpdateUserProfileResponse,
89
+ )
90
+ from reflexio.models.api_schema.service_schemas import (
91
+ AddAgentPlaybookRequest,
92
+ AddAgentPlaybookResponse,
93
+ AddUserPlaybookRequest,
94
+ AddUserPlaybookResponse,
95
+ AddUserProfileRequest,
96
+ AddUserProfileResponse,
97
+ AdminInvalidateCacheRequest,
98
+ AdminInvalidateCacheResponse,
99
+ BulkDeleteResponse,
100
+ CancelOperationRequest,
101
+ CancelOperationResponse,
102
+ ClearUserDataRequest,
103
+ ClearUserDataResponse,
104
+ DeleteAgentPlaybookRequest,
105
+ DeleteAgentPlaybookResponse,
106
+ DeleteAgentPlaybooksByIdsRequest,
107
+ DeleteProfilesByIdsRequest,
108
+ DeleteRequestRequest,
109
+ DeleteRequestResponse,
110
+ DeleteRequestsByIdsRequest,
111
+ DeleteSessionRequest,
112
+ DeleteSessionResponse,
113
+ DeleteUserInteractionRequest,
114
+ DeleteUserInteractionResponse,
115
+ DeleteUserPlaybookRequest,
116
+ DeleteUserPlaybookResponse,
117
+ DeleteUserPlaybooksByIdsRequest,
118
+ DeleteUserProfileRequest,
119
+ DeleteUserProfileResponse,
120
+ DowngradeProfilesRequest,
121
+ DowngradeProfilesResponse,
122
+ DowngradeUserPlaybooksRequest,
123
+ DowngradeUserPlaybooksResponse,
124
+ GetOperationStatusRequest,
125
+ GetOperationStatusResponse,
126
+ ManualPlaybookGenerationRequest,
127
+ ManualPlaybookGenerationResponse,
128
+ ManualProfileGenerationRequest,
129
+ ManualProfileGenerationResponse,
130
+ MyConfigResponse,
131
+ PlaybookAggregationChangeLogResponse,
132
+ PublishUserInteractionRequest,
133
+ PublishUserInteractionResponse,
134
+ RerunPlaybookGenerationRequest,
135
+ RerunPlaybookGenerationResponse,
136
+ RerunProfileGenerationRequest,
137
+ RerunProfileGenerationResponse,
138
+ RunPlaybookAggregationRequest,
139
+ RunPlaybookAggregationResponse,
140
+ Status,
141
+ UpgradeProfilesRequest,
142
+ UpgradeProfilesResponse,
143
+ UpgradeUserPlaybooksRequest,
144
+ UpgradeUserPlaybooksResponse,
145
+ WhoamiResponse,
146
+ )
147
+ from reflexio.models.api_schema.ui.converters import (
148
+ to_agent_playbook_view,
149
+ to_evaluation_result_view,
150
+ to_interaction_view,
151
+ to_profile_change_log_view,
152
+ to_profile_view,
153
+ to_user_playbook_view,
154
+ )
155
+ from reflexio.models.config_schema import (
156
+ SINGLETON_AGENT_SUCCESS_EVALUATION_NAME,
157
+ Config,
158
+ )
159
+ from reflexio.server._auth import DEFAULT_ORG_ID, default_get_org_id
160
+ from reflexio.server.api_endpoints import (
161
+ account_api,
162
+ health_api,
163
+ pending_tool_call_api,
164
+ publisher_api,
165
+ stall_state_api,
166
+ )
167
+ from reflexio.server.cache.reflexio_cache import (
168
+ get_reflexio,
169
+ invalidate_reflexio_cache,
170
+ )
171
+ from reflexio.server.correlation import correlation_id_var, generate_correlation_id
172
+ from reflexio.server.operation_limiter import (
173
+ OperationName,
174
+ limiter_http_exception,
175
+ run_with_operation_limit,
176
+ )
177
+ from reflexio.server.services.agent_success_evaluation.group_evaluation_runner import (
178
+ run_group_evaluation,
179
+ )
180
+ from reflexio.server.services.agent_success_evaluation.regen_jobs import (
181
+ REGEN_JOBS,
182
+ run_regen,
183
+ )
184
+
185
+ logger = logging.getLogger(__name__)
186
+
187
+ # Re-exported for backwards compatibility — callers that did
188
+ # ``from reflexio.server.api import default_get_org_id`` or ``DEFAULT_ORG_ID``
189
+ # continue to work.
190
+ __all__ = ["DEFAULT_ORG_ID", "create_app", "default_get_org_id"]
191
+
192
+ # Bot protection configuration
193
+ REQUEST_TIMEOUT_SECONDS = 60
194
+ SYNC_REQUEST_TIMEOUT_SECONDS = (
195
+ 600 # Longer timeout for synchronous processing (wait_for_response=true)
196
+ )
197
+ SUSPICIOUS_USER_AGENTS = ["bot", "crawler", "spider", "scraper", "curl", "wget"]
198
+ ALLOWED_EMPTY_UA_PATHS = ["/health", "/"] # Paths that allow empty user agents
199
+ DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024
200
+ REGENERATE_MAX_WORKERS = 2
201
+ _regen_executor = ThreadPoolExecutor(
202
+ max_workers=REGENERATE_MAX_WORKERS,
203
+ thread_name_prefix="reflexio-regen",
204
+ )
205
+
206
+
207
+ def _resolve_cors_origins() -> list[str]:
208
+ """Resolve browser origins allowed to make credentialed CORS requests."""
209
+ configured_origins = os.getenv("REFLEXIO_ALLOWED_ORIGINS", "").strip()
210
+ if configured_origins:
211
+ origins = [
212
+ origin.strip().rstrip("/")
213
+ for origin in configured_origins.split(",")
214
+ if origin.strip()
215
+ ]
216
+ return origins or ["http://localhost:8080"]
217
+
218
+ frontend_url = os.getenv("FRONTEND_URL", "").strip()
219
+ if frontend_url:
220
+ return [frontend_url.rstrip("/")]
221
+
222
+ return ["http://localhost:8080"]
223
+
224
+
225
+ def _max_body_bytes_from_env() -> int:
226
+ raw_value = os.getenv("REFLEXIO_MAX_BODY_BYTES", str(DEFAULT_MAX_BODY_BYTES))
227
+ try:
228
+ max_bytes = int(raw_value)
229
+ except ValueError:
230
+ logger.warning(
231
+ "Ignoring invalid REFLEXIO_MAX_BODY_BYTES=%r; using %s",
232
+ raw_value,
233
+ DEFAULT_MAX_BODY_BYTES,
234
+ )
235
+ return DEFAULT_MAX_BODY_BYTES
236
+ if max_bytes <= 0:
237
+ logger.warning(
238
+ "Ignoring non-positive REFLEXIO_MAX_BODY_BYTES=%r; using %s",
239
+ raw_value,
240
+ DEFAULT_MAX_BODY_BYTES,
241
+ )
242
+ return DEFAULT_MAX_BODY_BYTES
243
+ return max_bytes
244
+
245
+
246
+ def get_rate_limit_key(request: Request) -> str:
247
+ """Get rate limit key based on IP address.
248
+
249
+ Args:
250
+ request (Request): The incoming request
251
+
252
+ Returns:
253
+ str: Rate limit key (IP address)
254
+ """
255
+ return get_remote_address(request)
256
+
257
+
258
+ # Initialize rate limiter
259
+ limiter = Limiter(key_func=get_rate_limit_key)
260
+
261
+
262
+ def _run_limited_api[T](
263
+ org_id: str,
264
+ operation: OperationName,
265
+ fn: Callable[[], T],
266
+ ) -> T:
267
+ try:
268
+ return run_with_operation_limit(
269
+ org_id=org_id,
270
+ operation=operation,
271
+ fn=fn,
272
+ )
273
+ except TimeoutError as exc:
274
+ http_exc = limiter_http_exception(operation)
275
+ raise http_exc from exc
276
+
277
+
278
+ def configure_rate_limiter(key_func: Callable[..., str]) -> None:
279
+ """
280
+ Replace the rate limiter's key function.
281
+
282
+ This is the supported way to override the default IP-based key function
283
+ (e.g. with an org-scoped or token-scoped variant in the enterprise layer).
284
+
285
+ Args:
286
+ key_func: A callable that accepts a Request and returns a string key.
287
+ """
288
+ limiter._key_func = key_func # type: ignore[reportAttributeAccessIssue]
289
+
290
+
291
+ class BotProtectionMiddleware(BaseHTTPMiddleware):
292
+ """Middleware to detect and block suspicious bot-like requests."""
293
+
294
+ async def dispatch(
295
+ self, request: Request, call_next: RequestResponseEndpoint
296
+ ) -> Response:
297
+ """Process request and block suspicious patterns.
298
+
299
+ Args:
300
+ request (Request): The incoming request
301
+ call_next (RequestResponseEndpoint): Next middleware/handler in chain
302
+
303
+ Returns:
304
+ Response: The response from the next handler or a 403 JSON response
305
+ """
306
+ from starlette.responses import JSONResponse
307
+
308
+ user_agent = request.headers.get("user-agent", "").lower()
309
+ path = request.url.path
310
+
311
+ # Allow health check and root without user agent
312
+ if path not in ALLOWED_EMPTY_UA_PATHS:
313
+ # Block requests with no user agent
314
+ if not user_agent:
315
+ return JSONResponse(
316
+ status_code=status.HTTP_403_FORBIDDEN,
317
+ content={"detail": "Forbidden: Missing user agent"},
318
+ )
319
+
320
+ # Block requests with suspicious user agents
321
+ for suspicious in SUSPICIOUS_USER_AGENTS:
322
+ if suspicious in user_agent:
323
+ return JSONResponse(
324
+ status_code=status.HTTP_403_FORBIDDEN,
325
+ content={"detail": "Forbidden: Suspicious user agent"},
326
+ )
327
+
328
+ return await call_next(request)
329
+
330
+
331
+ class TimeoutMiddleware(BaseHTTPMiddleware):
332
+ """Middleware to enforce request timeout."""
333
+
334
+ async def dispatch(
335
+ self, request: Request, call_next: RequestResponseEndpoint
336
+ ) -> Response:
337
+ """Process request with timeout enforcement.
338
+
339
+ Args:
340
+ request (Request): The incoming request
341
+ call_next (RequestResponseEndpoint): Next middleware/handler in chain
342
+
343
+ Returns:
344
+ Response: The response from the next handler or a 504 JSON response
345
+ """
346
+ from starlette.responses import JSONResponse
347
+
348
+ # Use longer timeout for synchronous processing requests
349
+ timeout = REQUEST_TIMEOUT_SECONDS
350
+ if request.query_params.get("wait_for_response", "").lower() == "true":
351
+ timeout = SYNC_REQUEST_TIMEOUT_SECONDS
352
+
353
+ try:
354
+ return await asyncio.wait_for(call_next(request), timeout=timeout)
355
+ except TimeoutError:
356
+ return JSONResponse(
357
+ status_code=status.HTTP_504_GATEWAY_TIMEOUT,
358
+ content={"detail": "Request timeout"},
359
+ )
360
+
361
+
362
+ class _RequestBodyTooLargeError(Exception):
363
+ """Raised when the streamed request body exceeds the configured limit."""
364
+
365
+
366
+ class BodySizeLimitMiddleware:
367
+ """Reject requests whose declared or streamed body size exceeds the limit."""
368
+
369
+ def __init__(self, app: ASGIApp) -> None:
370
+ self.app = app
371
+
372
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
373
+ from starlette.responses import JSONResponse
374
+
375
+ if scope["type"] != "http":
376
+ await self.app(scope, receive, send)
377
+ return
378
+
379
+ max_body_bytes = _max_body_bytes_from_env()
380
+ content_length = None
381
+ for name, value in scope.get("headers", []):
382
+ if name.lower() == b"content-length":
383
+ content_length = value.decode("latin-1")
384
+ break
385
+
386
+ if content_length is not None:
387
+ try:
388
+ body_bytes = int(content_length)
389
+ except ValueError:
390
+ body_bytes = 0
391
+ if body_bytes > max_body_bytes:
392
+ await JSONResponse(
393
+ status_code=status.HTTP_413_CONTENT_TOO_LARGE,
394
+ content={"detail": "Request body too large"},
395
+ )(scope, receive, send)
396
+ return
397
+
398
+ consumed_bytes = 0
399
+
400
+ async def limited_receive() -> Message:
401
+ nonlocal consumed_bytes
402
+ message = await receive()
403
+ if message["type"] == "http.request":
404
+ consumed_bytes += len(message.get("body", b""))
405
+ if consumed_bytes > max_body_bytes:
406
+ raise _RequestBodyTooLargeError
407
+ return message
408
+
409
+ try:
410
+ await self.app(scope, limited_receive, send)
411
+ except _RequestBodyTooLargeError:
412
+ await JSONResponse(
413
+ status_code=status.HTTP_413_CONTENT_TOO_LARGE,
414
+ content={"detail": "Request body too large"},
415
+ )(scope, receive, send)
416
+
417
+
418
+ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
419
+ """Attach conservative browser security headers to every response."""
420
+
421
+ async def dispatch(
422
+ self, request: Request, call_next: RequestResponseEndpoint
423
+ ) -> Response:
424
+ response = await call_next(request)
425
+ response.headers["X-Content-Type-Options"] = "nosniff"
426
+ response.headers["X-Frame-Options"] = "DENY"
427
+ response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
428
+ if (
429
+ request.url.scheme == "https"
430
+ or request.headers.get("x-forwarded-proto", "").lower() == "https"
431
+ ):
432
+ response.headers["Strict-Transport-Security"] = (
433
+ "max-age=31536000; includeSubDomains"
434
+ )
435
+ return response
436
+
437
+
438
+ class CorrelationIdMiddleware(BaseHTTPMiddleware):
439
+ """Middleware that assigns a unique correlation ID to each request.
440
+
441
+ The ID is stored in a ContextVar so it propagates to log records
442
+ (via CorrelationIdFilter) and to ThreadPoolExecutor workers when
443
+ ``contextvars.copy_context()`` is used.
444
+ """
445
+
446
+ async def dispatch(
447
+ self, request: Request, call_next: RequestResponseEndpoint
448
+ ) -> Response:
449
+ cid = generate_correlation_id()
450
+ correlation_id_var.set(cid)
451
+ response = await call_next(request)
452
+ response.headers["X-Correlation-ID"] = cid
453
+ return response
454
+
455
+
456
+ core_router = APIRouter()
457
+
458
+
459
+ @core_router.get("/")
460
+ def root() -> dict[str, str]:
461
+ return {
462
+ "service": "Reflexio API",
463
+ "docs": "/docs",
464
+ "health": "/health",
465
+ }
466
+
467
+
468
+ @core_router.get("/health")
469
+ def health_check() -> dict[str, str]:
470
+ """Health check endpoint for ECS/container orchestration."""
471
+ return {"status": "healthy"}
472
+
473
+
474
+ @core_router.get(
475
+ "/api/whoami",
476
+ response_model=WhoamiResponse,
477
+ response_model_exclude_none=True,
478
+ )
479
+ def whoami_endpoint(
480
+ org_id: str = Depends(default_get_org_id),
481
+ ) -> WhoamiResponse:
482
+ """Return the caller's org and masked storage routing.
483
+
484
+ Powers ``reflexio status``. Safe to call unauthenticated in
485
+ self-host mode; the enterprise server wraps this in Bearer auth.
486
+ """
487
+ return account_api.whoami(org_id=org_id)
488
+
489
+
490
+ @core_router.get(
491
+ "/api/my_config",
492
+ response_model=MyConfigResponse,
493
+ response_model_exclude_none=True,
494
+ )
495
+ def my_config_endpoint(
496
+ request: Request,
497
+ org_id: str = Depends(default_get_org_id),
498
+ ) -> MyConfigResponse:
499
+ """Return raw storage credentials for the caller's org.
500
+
501
+ Enablement is controlled by two independent opt-ins so the endpoint
502
+ is closed by default on unauthenticated self-host deployments:
503
+
504
+ - ``request.app.state.my_config_enabled`` — set to True by
505
+ :func:`create_app` when the host wires in a Bearer-auth
506
+ ``get_org_id`` dependency, so enterprise callers are always
507
+ authenticated before they reach this route.
508
+ - ``REFLEXIO_ALLOW_MY_CONFIG=true`` — OS self-host escape hatch.
509
+
510
+ If neither is set we return a closed response instead of a 404 so
511
+ the CLI can display an actionable hint.
512
+ """
513
+ app_state_enabled = bool(getattr(request.app.state, "my_config_enabled", False))
514
+ if not (app_state_enabled or account_api.my_config_allowed()):
515
+ return MyConfigResponse(
516
+ success=False,
517
+ message=(
518
+ "GET /api/my_config is disabled. Set "
519
+ "REFLEXIO_ALLOW_MY_CONFIG=true to enable."
520
+ ),
521
+ )
522
+ return account_api.my_config(org_id=org_id)
523
+
524
+
525
+ @core_router.post(
526
+ "/api/publish_interaction",
527
+ response_model=PublishUserInteractionResponse,
528
+ response_model_exclude_none=True,
529
+ )
530
+ @limiter.limit("60/minute") # Rate limit for write operations
531
+ def publish_user_interaction(
532
+ request: Request,
533
+ payload: PublishUserInteractionRequest,
534
+ background_tasks: BackgroundTasks,
535
+ org_id: str = Depends(default_get_org_id),
536
+ wait_for_response: bool = False,
537
+ ) -> PublishUserInteractionResponse:
538
+ if wait_for_response:
539
+ # Process synchronously so the caller gets the real result
540
+ return _run_limited_api(
541
+ org_id,
542
+ "publish",
543
+ lambda: publisher_api.add_user_interaction(org_id=org_id, request=payload),
544
+ )
545
+
546
+ def _limited_publish_task() -> None:
547
+ try:
548
+ run_with_operation_limit(
549
+ org_id=org_id,
550
+ operation="publish",
551
+ fn=lambda: publisher_api.add_user_interaction(
552
+ org_id=org_id, request=payload
553
+ ),
554
+ )
555
+ except TimeoutError:
556
+ logger.warning(
557
+ "Dropped queued publish for org %s because publish limiter is saturated",
558
+ org_id,
559
+ )
560
+
561
+ # Run in background — caller gets immediate acknowledgement
562
+ background_tasks.add_task(_limited_publish_task)
563
+ return PublishUserInteractionResponse(
564
+ success=True, message="Interaction queued for processing"
565
+ )
566
+
567
+
568
+ @core_router.post(
569
+ "/api/add_user_playbook",
570
+ response_model=AddUserPlaybookResponse,
571
+ response_model_exclude_none=True,
572
+ )
573
+ @limiter.limit("60/minute") # Rate limit for write operations
574
+ def add_user_playbook_endpoint(
575
+ request: Request,
576
+ payload: AddUserPlaybookRequest,
577
+ org_id: str = Depends(default_get_org_id),
578
+ ) -> AddUserPlaybookResponse:
579
+ """Add user playbook directly to storage.
580
+
581
+ Args:
582
+ request (Request): The HTTP request object (for rate limiting)
583
+ payload (AddUserPlaybookRequest): The request containing user playbooks
584
+ org_id (str): Organization ID
585
+
586
+ Returns:
587
+ AddUserPlaybookResponse: Response containing success status, message, and added count
588
+ """
589
+ return publisher_api.add_user_playbook(org_id=org_id, request=payload)
590
+
591
+
592
+ @core_router.post(
593
+ "/api/add_agent_playbook",
594
+ response_model=AddAgentPlaybookResponse,
595
+ response_model_exclude_none=True,
596
+ )
597
+ @limiter.limit("60/minute") # Rate limit for write operations
598
+ def add_agent_playbook_endpoint(
599
+ request: Request,
600
+ payload: AddAgentPlaybookRequest,
601
+ org_id: str = Depends(default_get_org_id),
602
+ ) -> AddAgentPlaybookResponse:
603
+ """Add agent playbook directly to storage.
604
+
605
+ Args:
606
+ request (Request): The HTTP request object (for rate limiting)
607
+ payload (AddAgentPlaybookRequest): The request containing agent playbooks
608
+ org_id (str): Organization ID
609
+
610
+ Returns:
611
+ AddAgentPlaybookResponse: Response containing success status, message, and added count
612
+ """
613
+ return publisher_api.add_agent_playbook(org_id=org_id, request=payload)
614
+
615
+
616
+ @core_router.post(
617
+ "/api/add_user_profile",
618
+ response_model=AddUserProfileResponse,
619
+ response_model_exclude_none=True,
620
+ )
621
+ @limiter.limit("60/minute") # Rate limit for write operations
622
+ def add_user_profile_endpoint(
623
+ request: Request,
624
+ payload: AddUserProfileRequest,
625
+ org_id: str = Depends(default_get_org_id),
626
+ ) -> AddUserProfileResponse:
627
+ """Add user profile directly to storage, bypassing inference.
628
+
629
+ Args:
630
+ request (Request): The HTTP request object (for rate limiting)
631
+ payload (AddUserProfileRequest): The request containing user profiles
632
+ org_id (str): Organization ID
633
+
634
+ Returns:
635
+ AddUserProfileResponse: Response containing success status, message, and added count
636
+ """
637
+ return publisher_api.add_user_profile(org_id=org_id, request=payload)
638
+
639
+
640
+ @core_router.post(
641
+ "/api/search_profiles",
642
+ response_model=SearchProfilesViewResponse,
643
+ response_model_exclude_none=True,
644
+ )
645
+ @limiter.limit("120/minute") # Rate limit for read operations
646
+ def search_user_profiles(
647
+ request: Request,
648
+ payload: SearchUserProfileRequest,
649
+ org_id: str = Depends(default_get_org_id),
650
+ ) -> SearchProfilesViewResponse:
651
+ response = _run_limited_api(
652
+ org_id,
653
+ "search",
654
+ lambda: get_reflexio(org_id=org_id).search_user_profiles(payload),
655
+ )
656
+ return SearchProfilesViewResponse(
657
+ success=response.success,
658
+ user_profiles=[to_profile_view(p) for p in response.user_profiles],
659
+ msg=response.msg,
660
+ )
661
+
662
+
663
+ @core_router.post(
664
+ "/api/rerank_user_profiles",
665
+ response_model=SearchProfilesViewResponse,
666
+ response_model_exclude_none=True,
667
+ )
668
+ @limiter.limit("120/minute") # Rate limit for read operations
669
+ def rerank_user_profiles(
670
+ request: Request,
671
+ payload: RerankUserProfilesRequest,
672
+ org_id: str = Depends(default_get_org_id),
673
+ ) -> SearchProfilesViewResponse:
674
+ """Rerank a list of profile ids by query relevance using a cross-encoder.
675
+
676
+ Args:
677
+ request (Request): The HTTP request object (for rate limiting)
678
+ payload (RerankUserProfilesRequest): The rerank request
679
+ org_id (str): Organization ID
680
+
681
+ Returns:
682
+ SearchProfilesViewResponse: Reranked profiles, top_k entries.
683
+ """
684
+ response = _run_limited_api(
685
+ org_id,
686
+ "search",
687
+ lambda: get_reflexio(org_id=org_id).rerank_user_profiles(payload),
688
+ )
689
+ return SearchProfilesViewResponse(
690
+ success=response.success,
691
+ user_profiles=[to_profile_view(p) for p in response.user_profiles],
692
+ msg=response.msg,
693
+ )
694
+
695
+
696
+ @core_router.get(
697
+ "/api/storage_stats",
698
+ response_model=StorageStatsResponse,
699
+ response_model_exclude_none=True,
700
+ )
701
+ @limiter.limit("120/minute") # Rate limit for read operations
702
+ def storage_stats(
703
+ request: Request,
704
+ user_id: str,
705
+ org_id: str = Depends(default_get_org_id),
706
+ ) -> StorageStatsResponse:
707
+ """Return lightweight metadata about a user's profiles and playbooks.
708
+
709
+ Args:
710
+ request (Request): The HTTP request object (for rate limiting)
711
+ user_id (str): Target user id, passed as a query parameter so this is
712
+ a cacheable, idempotent GET.
713
+ org_id (str): Organization ID
714
+
715
+ Returns:
716
+ StorageStatsResponse: Counts and timestamp range for the user.
717
+ """
718
+ return _run_limited_api(
719
+ org_id,
720
+ "search",
721
+ lambda: get_reflexio(org_id=org_id).storage_stats(
722
+ StorageStatsRequest(user_id=user_id)
723
+ ),
724
+ )
725
+
726
+
727
+ @core_router.post(
728
+ "/api/search_interactions",
729
+ response_model=SearchInteractionsViewResponse,
730
+ response_model_exclude_none=True,
731
+ )
732
+ @limiter.limit("120/minute") # Rate limit for read operations
733
+ def search_interactions(
734
+ request: Request,
735
+ payload: SearchInteractionRequest,
736
+ org_id: str = Depends(default_get_org_id),
737
+ ) -> SearchInteractionsViewResponse:
738
+ response = _run_limited_api(
739
+ org_id,
740
+ "search",
741
+ lambda: get_reflexio(org_id=org_id).search_interactions(payload),
742
+ )
743
+ return SearchInteractionsViewResponse(
744
+ success=response.success,
745
+ interactions=[to_interaction_view(i) for i in response.interactions],
746
+ msg=response.msg,
747
+ )
748
+
749
+
750
+ @core_router.post(
751
+ "/api/search_user_playbooks",
752
+ response_model=SearchUserPlaybooksViewResponse,
753
+ response_model_exclude_none=True,
754
+ )
755
+ @limiter.limit("120/minute") # Rate limit for read operations
756
+ def search_user_playbooks_endpoint(
757
+ request: Request,
758
+ payload: SearchUserPlaybookRequest,
759
+ org_id: str = Depends(default_get_org_id),
760
+ ) -> SearchUserPlaybooksViewResponse:
761
+ """Search user playbooks with semantic search and advanced filtering.
762
+
763
+ Supports filtering by user_id (via request_id linkage), agent_version,
764
+ playbook_name, datetime range, and status.
765
+
766
+ Args:
767
+ request (Request): The HTTP request object (for rate limiting)
768
+ payload (SearchUserPlaybookRequest): The search request
769
+ org_id (str): Organization ID
770
+
771
+ Returns:
772
+ SearchUserPlaybooksViewResponse: Response containing matching user playbooks
773
+ """
774
+ response = _run_limited_api(
775
+ org_id,
776
+ "search",
777
+ lambda: get_reflexio(org_id=org_id).search_user_playbooks(payload),
778
+ )
779
+ return SearchUserPlaybooksViewResponse(
780
+ success=response.success,
781
+ user_playbooks=[to_user_playbook_view(rf) for rf in response.user_playbooks],
782
+ msg=response.msg,
783
+ )
784
+
785
+
786
+ @core_router.post(
787
+ "/api/search_agent_playbooks",
788
+ response_model=SearchAgentPlaybooksViewResponse,
789
+ response_model_exclude_none=True,
790
+ )
791
+ @limiter.limit("120/minute") # Rate limit for read operations
792
+ def search_agent_playbooks_endpoint(
793
+ request: Request,
794
+ payload: SearchAgentPlaybookRequest,
795
+ org_id: str = Depends(default_get_org_id),
796
+ ) -> SearchAgentPlaybooksViewResponse:
797
+ """Search agent playbooks with semantic search and advanced filtering.
798
+
799
+ Supports filtering by agent_version, playbook_name, datetime range,
800
+ status_filter, and playbook_status_filter.
801
+
802
+ Args:
803
+ request (Request): The HTTP request object (for rate limiting)
804
+ payload (SearchAgentPlaybookRequest): The search request
805
+ org_id (str): Organization ID
806
+
807
+ Returns:
808
+ SearchAgentPlaybooksViewResponse: Response containing matching agent playbooks
809
+ """
810
+ response = _run_limited_api(
811
+ org_id,
812
+ "search",
813
+ lambda: get_reflexio(org_id=org_id).search_agent_playbooks(payload),
814
+ )
815
+ return SearchAgentPlaybooksViewResponse(
816
+ success=response.success,
817
+ agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
818
+ msg=response.msg,
819
+ )
820
+
821
+
822
+ @core_router.post(
823
+ "/api/search",
824
+ response_model=UnifiedSearchViewResponse,
825
+ response_model_exclude_none=True,
826
+ )
827
+ @limiter.limit("120/minute")
828
+ def unified_search_endpoint(
829
+ request: Request,
830
+ payload: UnifiedSearchRequest,
831
+ org_id: str = Depends(default_get_org_id),
832
+ ) -> UnifiedSearchViewResponse:
833
+ """Search across all entity types (profiles, agent playbooks, user playbooks).
834
+
835
+ Runs query rewriting and embedding generation in parallel, then searches
836
+ all entity types in parallel. Query rewriting is gated behind the
837
+ enable_reformulation request param.
838
+
839
+ Args:
840
+ request (Request): The HTTP request object (for rate limiting)
841
+ payload (UnifiedSearchRequest): The unified search request
842
+ org_id (str): Organization ID
843
+
844
+ Returns:
845
+ UnifiedSearchViewResponse: Combined search results
846
+ """
847
+ response = _run_limited_api(
848
+ org_id,
849
+ "search",
850
+ lambda: get_reflexio(org_id=org_id).unified_search(payload, org_id=org_id),
851
+ )
852
+ return UnifiedSearchViewResponse(
853
+ success=response.success,
854
+ profiles=[to_profile_view(p) for p in response.profiles],
855
+ agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
856
+ user_playbooks=[to_user_playbook_view(rf) for rf in response.user_playbooks],
857
+ reformulated_query=response.reformulated_query,
858
+ msg=response.msg,
859
+ agent_trace=response.agent_trace,
860
+ rehydrated_text=response.rehydrated_text,
861
+ )
862
+
863
+
864
+ @core_router.get("/api/profile_change_log", response_model=ProfileChangeLogViewResponse)
865
+ def get_profile_change_log(
866
+ org_id: str = Depends(default_get_org_id),
867
+ ) -> ProfileChangeLogViewResponse:
868
+ response = get_reflexio(org_id=org_id).get_profile_change_logs()
869
+ return ProfileChangeLogViewResponse(
870
+ success=response.success,
871
+ profile_change_logs=[
872
+ to_profile_change_log_view(log) for log in response.profile_change_logs
873
+ ],
874
+ )
875
+
876
+
877
+ @core_router.get(
878
+ "/api/playbook_aggregation_change_logs",
879
+ response_model=PlaybookAggregationChangeLogResponse,
880
+ )
881
+ def get_playbook_aggregation_change_logs(
882
+ playbook_name: str,
883
+ agent_version: str,
884
+ org_id: str = Depends(default_get_org_id),
885
+ ) -> PlaybookAggregationChangeLogResponse:
886
+ return get_reflexio(org_id=org_id).get_playbook_aggregation_change_logs(
887
+ playbook_name=playbook_name,
888
+ agent_version=agent_version,
889
+ )
890
+
891
+
892
+ @core_router.delete(
893
+ "/api/delete_profile",
894
+ response_model=DeleteUserProfileResponse,
895
+ response_model_exclude_none=True,
896
+ )
897
+ def delete_profile(
898
+ request: DeleteUserProfileRequest,
899
+ org_id: str = Depends(default_get_org_id),
900
+ ) -> DeleteUserProfileResponse:
901
+ return publisher_api.delete_user_profile(org_id=org_id, request=request)
902
+
903
+
904
+ @core_router.delete(
905
+ "/api/delete_interaction",
906
+ response_model=DeleteUserInteractionResponse,
907
+ response_model_exclude_none=True,
908
+ )
909
+ def delete_interaction(
910
+ request: DeleteUserInteractionRequest,
911
+ org_id: str = Depends(default_get_org_id),
912
+ ) -> DeleteUserInteractionResponse:
913
+ return publisher_api.delete_user_interaction(org_id=org_id, request=request)
914
+
915
+
916
+ @core_router.delete(
917
+ "/api/delete_request",
918
+ response_model=DeleteRequestResponse,
919
+ response_model_exclude_none=True,
920
+ )
921
+ def delete_request(
922
+ request: DeleteRequestRequest,
923
+ org_id: str = Depends(default_get_org_id),
924
+ ) -> DeleteRequestResponse:
925
+ return publisher_api.delete_request(org_id=org_id, request=request)
926
+
927
+
928
+ @core_router.delete(
929
+ "/api/delete_session",
930
+ response_model=DeleteSessionResponse,
931
+ response_model_exclude_none=True,
932
+ )
933
+ def delete_session(
934
+ request: DeleteSessionRequest,
935
+ org_id: str = Depends(default_get_org_id),
936
+ ) -> DeleteSessionResponse:
937
+ return publisher_api.delete_session(org_id=org_id, request=request)
938
+
939
+
940
+ @core_router.delete(
941
+ "/api/delete_agent_playbook",
942
+ response_model=DeleteAgentPlaybookResponse,
943
+ response_model_exclude_none=True,
944
+ )
945
+ def delete_agent_playbook(
946
+ request: DeleteAgentPlaybookRequest,
947
+ org_id: str = Depends(default_get_org_id),
948
+ ) -> DeleteAgentPlaybookResponse:
949
+ return publisher_api.delete_agent_playbook(org_id=org_id, request=request)
950
+
951
+
952
+ @core_router.delete(
953
+ "/api/delete_user_playbook",
954
+ response_model=DeleteUserPlaybookResponse,
955
+ response_model_exclude_none=True,
956
+ )
957
+ def delete_user_playbook(
958
+ request: DeleteUserPlaybookRequest,
959
+ org_id: str = Depends(default_get_org_id),
960
+ ) -> DeleteUserPlaybookResponse:
961
+ return publisher_api.delete_user_playbook(org_id=org_id, request=request)
962
+
963
+
964
+ @core_router.delete(
965
+ "/api/delete_requests_by_ids",
966
+ response_model=BulkDeleteResponse,
967
+ response_model_exclude_none=True,
968
+ )
969
+ def delete_requests_by_ids(
970
+ request: DeleteRequestsByIdsRequest,
971
+ org_id: str = Depends(default_get_org_id),
972
+ ) -> BulkDeleteResponse:
973
+ """Delete multiple requests by their IDs.
974
+
975
+ Args:
976
+ request (DeleteRequestsByIdsRequest): Request containing list of request IDs to delete
977
+ org_id (str): Organization ID
978
+
979
+ Returns:
980
+ BulkDeleteResponse: Response containing success status and deleted count
981
+ """
982
+ return publisher_api.delete_requests_by_ids(org_id=org_id, request=request)
983
+
984
+
985
+ @core_router.delete(
986
+ "/api/delete_profiles_by_ids",
987
+ response_model=BulkDeleteResponse,
988
+ response_model_exclude_none=True,
989
+ )
990
+ def delete_profiles_by_ids(
991
+ request: DeleteProfilesByIdsRequest,
992
+ org_id: str = Depends(default_get_org_id),
993
+ ) -> BulkDeleteResponse:
994
+ """Delete multiple profiles by their IDs.
995
+
996
+ Args:
997
+ request (DeleteProfilesByIdsRequest): Request containing list of profile IDs to delete
998
+ org_id (str): Organization ID
999
+
1000
+ Returns:
1001
+ BulkDeleteResponse: Response containing success status and deleted count
1002
+ """
1003
+ return publisher_api.delete_profiles_by_ids(org_id=org_id, request=request)
1004
+
1005
+
1006
+ @core_router.delete(
1007
+ "/api/delete_agent_playbooks_by_ids",
1008
+ response_model=BulkDeleteResponse,
1009
+ response_model_exclude_none=True,
1010
+ )
1011
+ def delete_agent_playbooks_by_ids(
1012
+ request: DeleteAgentPlaybooksByIdsRequest,
1013
+ org_id: str = Depends(default_get_org_id),
1014
+ ) -> BulkDeleteResponse:
1015
+ """Delete multiple agent playbooks by their IDs.
1016
+
1017
+ Args:
1018
+ request (DeleteAgentPlaybooksByIdsRequest): Request containing list of agent playbook IDs to delete
1019
+ org_id (str): Organization ID
1020
+
1021
+ Returns:
1022
+ BulkDeleteResponse: Response containing success status and deleted count
1023
+ """
1024
+ return publisher_api.delete_agent_playbooks_by_ids_bulk(
1025
+ org_id=org_id, request=request
1026
+ )
1027
+
1028
+
1029
+ @core_router.delete(
1030
+ "/api/delete_user_playbooks_by_ids",
1031
+ response_model=BulkDeleteResponse,
1032
+ response_model_exclude_none=True,
1033
+ )
1034
+ def delete_user_playbooks_by_ids(
1035
+ request: DeleteUserPlaybooksByIdsRequest,
1036
+ org_id: str = Depends(default_get_org_id),
1037
+ ) -> BulkDeleteResponse:
1038
+ """Delete multiple user playbooks by their IDs.
1039
+
1040
+ Args:
1041
+ request (DeleteUserPlaybooksByIdsRequest): Request containing list of user playbook IDs to delete
1042
+ org_id (str): Organization ID
1043
+
1044
+ Returns:
1045
+ BulkDeleteResponse: Response containing success status and deleted count
1046
+ """
1047
+ return publisher_api.delete_user_playbooks_by_ids_bulk(
1048
+ org_id=org_id, request=request
1049
+ )
1050
+
1051
+
1052
+ @core_router.delete(
1053
+ "/api/delete_all_interactions",
1054
+ response_model=BulkDeleteResponse,
1055
+ response_model_exclude_none=True,
1056
+ )
1057
+ @limiter.limit("10/minute")
1058
+ def delete_all_interactions(
1059
+ request: Request,
1060
+ org_id: str = Depends(default_get_org_id),
1061
+ ) -> BulkDeleteResponse:
1062
+ """Delete all requests and their associated interactions.
1063
+
1064
+ Args:
1065
+ org_id (str): Organization ID
1066
+
1067
+ Returns:
1068
+ BulkDeleteResponse: Response containing success status and deleted count
1069
+ """
1070
+ return publisher_api.delete_all_interactions_bulk(org_id=org_id)
1071
+
1072
+
1073
+ @core_router.delete(
1074
+ "/api/delete_all_profiles",
1075
+ response_model=BulkDeleteResponse,
1076
+ response_model_exclude_none=True,
1077
+ )
1078
+ @limiter.limit("10/minute")
1079
+ def delete_all_profiles(
1080
+ request: Request,
1081
+ org_id: str = Depends(default_get_org_id),
1082
+ ) -> BulkDeleteResponse:
1083
+ """Delete all profiles.
1084
+
1085
+ Args:
1086
+ org_id (str): Organization ID
1087
+
1088
+ Returns:
1089
+ BulkDeleteResponse: Response containing success status and deleted count
1090
+ """
1091
+ return publisher_api.delete_all_profiles_bulk(org_id=org_id)
1092
+
1093
+
1094
+ @core_router.delete(
1095
+ "/api/delete_all_playbooks",
1096
+ response_model=BulkDeleteResponse,
1097
+ response_model_exclude_none=True,
1098
+ )
1099
+ @limiter.limit("10/minute")
1100
+ def delete_all_playbooks(
1101
+ request: Request,
1102
+ org_id: str = Depends(default_get_org_id),
1103
+ ) -> BulkDeleteResponse:
1104
+ """Delete all playbooks (both user and agent).
1105
+
1106
+ Args:
1107
+ org_id (str): Organization ID
1108
+
1109
+ Returns:
1110
+ BulkDeleteResponse: Response containing success status and deleted count
1111
+ """
1112
+ return publisher_api.delete_all_playbooks_bulk(org_id=org_id)
1113
+
1114
+
1115
+ @core_router.delete(
1116
+ "/api/delete_all_user_playbooks",
1117
+ response_model=BulkDeleteResponse,
1118
+ response_model_exclude_none=True,
1119
+ )
1120
+ @limiter.limit("10/minute")
1121
+ def delete_all_user_playbooks(
1122
+ request: Request,
1123
+ org_id: str = Depends(default_get_org_id),
1124
+ ) -> BulkDeleteResponse:
1125
+ """Delete all user playbooks (user only, not agent).
1126
+
1127
+ Args:
1128
+ org_id (str): Organization ID
1129
+
1130
+ Returns:
1131
+ BulkDeleteResponse: Response containing success status and deleted count
1132
+ """
1133
+ return publisher_api.delete_all_user_playbooks_bulk(org_id=org_id)
1134
+
1135
+
1136
+ @core_router.delete(
1137
+ "/api/delete_all_agent_playbooks",
1138
+ response_model=BulkDeleteResponse,
1139
+ response_model_exclude_none=True,
1140
+ )
1141
+ @limiter.limit("10/minute")
1142
+ def delete_all_agent_playbooks(
1143
+ request: Request,
1144
+ org_id: str = Depends(default_get_org_id),
1145
+ ) -> BulkDeleteResponse:
1146
+ """Delete all agent playbooks (agent only, not user).
1147
+
1148
+ Args:
1149
+ org_id (str): Organization ID
1150
+
1151
+ Returns:
1152
+ BulkDeleteResponse: Response containing success status and deleted count
1153
+ """
1154
+ return publisher_api.delete_all_agent_playbooks_bulk(org_id=org_id)
1155
+
1156
+
1157
+ @core_router.post(
1158
+ "/api/clear_user_data",
1159
+ response_model=ClearUserDataResponse,
1160
+ response_model_exclude_none=True,
1161
+ )
1162
+ @limiter.limit("10/minute")
1163
+ def clear_user_data(
1164
+ request: Request,
1165
+ payload: ClearUserDataRequest,
1166
+ org_id: str = Depends(default_get_org_id),
1167
+ ) -> ClearUserDataResponse:
1168
+ """Delete all rows scoped to a single ``user_id``.
1169
+
1170
+ Removes the user's interactions, user playbooks, profiles, and
1171
+ requests. Does NOT touch ``agent_playbooks`` — they are
1172
+ intentionally shared cross-project. Used by paired-protocol
1173
+ harnesses (e.g. SWE-bench) to isolate per-task data on a shared
1174
+ backend without one task's clear-all nuking another in-flight
1175
+ task's rows.
1176
+
1177
+ Args:
1178
+ request (ClearUserDataRequest): Request containing the target user_id
1179
+ org_id (str): Organization ID
1180
+
1181
+ Returns:
1182
+ ClearUserDataResponse: Response with per-entity deletion counts
1183
+ """
1184
+ return publisher_api.clear_user_data(org_id=org_id, request=payload)
1185
+
1186
+
1187
+ @core_router.post(
1188
+ "/api/get_interactions",
1189
+ response_model=GetInteractionsViewResponse,
1190
+ response_model_exclude_none=True,
1191
+ )
1192
+ def get_interactions(
1193
+ request: GetInteractionsRequest,
1194
+ org_id: str = Depends(default_get_org_id),
1195
+ ) -> GetInteractionsViewResponse:
1196
+ response = get_reflexio(org_id=org_id).get_interactions(request)
1197
+ return GetInteractionsViewResponse(
1198
+ success=response.success,
1199
+ interactions=[to_interaction_view(i) for i in response.interactions],
1200
+ msg=response.msg,
1201
+ )
1202
+
1203
+
1204
+ @core_router.get(
1205
+ "/api/get_all_interactions",
1206
+ response_model=GetInteractionsViewResponse,
1207
+ response_model_exclude_none=True,
1208
+ )
1209
+ @limiter.limit("30/minute")
1210
+ def get_all_interactions(
1211
+ request: Request,
1212
+ limit: int = 100,
1213
+ org_id: str = Depends(default_get_org_id),
1214
+ ) -> GetInteractionsViewResponse:
1215
+ """Get all user interactions across all users.
1216
+
1217
+ Args:
1218
+ limit (int, optional): Maximum number of interactions to return. Defaults to 100.
1219
+ org_id (str): Organization ID
1220
+
1221
+ Returns:
1222
+ GetInteractionsViewResponse: Response containing all user interactions
1223
+ """
1224
+ reflexio = get_reflexio(org_id=org_id)
1225
+ response = reflexio.get_all_interactions(limit=limit)
1226
+ return GetInteractionsViewResponse(
1227
+ success=response.success,
1228
+ interactions=[to_interaction_view(i) for i in response.interactions],
1229
+ msg=response.msg,
1230
+ )
1231
+
1232
+
1233
+ @core_router.post(
1234
+ "/api/get_requests",
1235
+ response_model=GetRequestsViewResponse,
1236
+ response_model_exclude_none=True,
1237
+ )
1238
+ def get_requests_endpoint(
1239
+ request: GetRequestsRequest,
1240
+ org_id: str = Depends(default_get_org_id),
1241
+ ) -> GetRequestsViewResponse:
1242
+ """Get requests with their associated interactions.
1243
+
1244
+ Args:
1245
+ request (GetRequestsRequest): The get request
1246
+ org_id (str): Organization ID
1247
+
1248
+ Returns:
1249
+ GetRequestsViewResponse: Response containing requests with their interactions
1250
+ """
1251
+ internal_response = get_reflexio(org_id=org_id).get_requests(request)
1252
+ return GetRequestsViewResponse(
1253
+ success=internal_response.success,
1254
+ sessions=[
1255
+ SessionView(
1256
+ session_id=s.session_id,
1257
+ requests=[
1258
+ RequestDataView(
1259
+ request=rd.request,
1260
+ interactions=[to_interaction_view(i) for i in rd.interactions],
1261
+ )
1262
+ for rd in s.requests
1263
+ ],
1264
+ )
1265
+ for s in internal_response.sessions
1266
+ ],
1267
+ has_more=internal_response.has_more,
1268
+ msg=internal_response.msg,
1269
+ )
1270
+
1271
+
1272
+ @core_router.post(
1273
+ "/api/get_profiles",
1274
+ response_model=GetProfilesViewResponse,
1275
+ response_model_exclude_none=True,
1276
+ )
1277
+ def get_profiles(
1278
+ request: GetUserProfilesRequest,
1279
+ org_id: str = Depends(default_get_org_id),
1280
+ ) -> GetProfilesViewResponse:
1281
+ response = get_reflexio(org_id=org_id).get_profiles(request)
1282
+ return GetProfilesViewResponse(
1283
+ success=response.success,
1284
+ user_profiles=[to_profile_view(p) for p in response.user_profiles],
1285
+ msg=response.msg,
1286
+ )
1287
+
1288
+
1289
+ @core_router.get(
1290
+ "/api/get_all_profiles",
1291
+ response_model=GetProfilesViewResponse,
1292
+ response_model_exclude_none=True,
1293
+ )
1294
+ @limiter.limit("30/minute")
1295
+ def get_all_profiles(
1296
+ request: Request,
1297
+ limit: int = 100,
1298
+ status_filter: str | None = None,
1299
+ org_id: str = Depends(default_get_org_id),
1300
+ ) -> GetProfilesViewResponse:
1301
+ """Get all user profiles across all users.
1302
+
1303
+ Args:
1304
+ limit (int, optional): Maximum number of profiles to return. Defaults to 100.
1305
+ status_filter (str, optional): Filter by profile status. Can be "current", "pending", or "archived".
1306
+ org_id (str): Organization ID
1307
+
1308
+ Returns:
1309
+ GetProfilesViewResponse: Response containing all user profiles
1310
+ """
1311
+ reflexio = get_reflexio(org_id=org_id)
1312
+
1313
+ # Map status_filter string to Status list
1314
+ status_filter_list = None
1315
+ if status_filter == "current":
1316
+ status_filter_list = [None]
1317
+ elif status_filter == "pending":
1318
+ status_filter_list = [Status.PENDING]
1319
+ elif status_filter == "archived":
1320
+ status_filter_list = [Status.ARCHIVED]
1321
+
1322
+ response = reflexio.get_all_profiles(limit=limit, status_filter=status_filter_list) # type: ignore[reportArgumentType]
1323
+ return GetProfilesViewResponse(
1324
+ success=response.success,
1325
+ user_profiles=[to_profile_view(p) for p in response.user_profiles],
1326
+ msg=response.msg,
1327
+ )
1328
+
1329
+
1330
+ @core_router.get(
1331
+ "/api/get_profile_statistics",
1332
+ response_model=GetProfileStatisticsResponse,
1333
+ response_model_exclude_none=True,
1334
+ )
1335
+ def get_profile_statistics(
1336
+ org_id: str = Depends(default_get_org_id),
1337
+ ) -> GetProfileStatisticsResponse:
1338
+ """Get efficient profile statistics using storage layer queries.
1339
+
1340
+ Args:
1341
+ org_id (str): Organization ID
1342
+
1343
+ Returns:
1344
+ GetProfileStatisticsResponse: Response containing profile counts by status
1345
+ """
1346
+ # Create Reflexio instance
1347
+ reflexio = get_reflexio(org_id=org_id)
1348
+
1349
+ # Get profile statistics using Reflexio's method
1350
+ return reflexio.get_profile_statistics()
1351
+
1352
+
1353
+ @core_router.post(
1354
+ "/api/run_playbook_aggregation",
1355
+ response_model=RunPlaybookAggregationResponse,
1356
+ response_model_exclude_none=True,
1357
+ )
1358
+ @limiter.limit("10/minute") # Strict limit for expensive operations
1359
+ def run_playbook_aggregation(
1360
+ request: Request,
1361
+ payload: RunPlaybookAggregationRequest,
1362
+ org_id: str = Depends(default_get_org_id),
1363
+ ) -> RunPlaybookAggregationResponse:
1364
+ return _run_limited_api(
1365
+ org_id,
1366
+ "aggregation",
1367
+ lambda: publisher_api.run_playbook_aggregation(org_id=org_id, request=payload),
1368
+ )
1369
+
1370
+
1371
+ @core_router.post("/api/set_config")
1372
+ @limiter.limit("10/minute")
1373
+ def set_config(
1374
+ request: Request,
1375
+ config: dict[str, Any],
1376
+ org_id: str = Depends(default_get_org_id),
1377
+ ) -> SetConfigResponse:
1378
+ """Set configuration for the organization.
1379
+
1380
+ Args:
1381
+ config (Config): The configuration to set
1382
+ org_id (str): Organization ID
1383
+
1384
+ Returns:
1385
+ dict: Response containing success status and message
1386
+ """
1387
+ # Create Reflexio instance to access the configurator through request_context
1388
+ reflexio = get_reflexio(org_id=org_id)
1389
+
1390
+ # Set the config using Reflexio's set_config method
1391
+ response = reflexio.set_config(config)
1392
+
1393
+ # Invalidate cache on successful config change to ensure fresh instance next request
1394
+ if response.success:
1395
+ invalidate_reflexio_cache(org_id=org_id)
1396
+
1397
+ return response
1398
+
1399
+
1400
+ @core_router.post("/api/update_config")
1401
+ @limiter.limit("10/minute")
1402
+ def update_config(
1403
+ request: Request,
1404
+ partial: dict[str, Any],
1405
+ org_id: str = Depends(default_get_org_id),
1406
+ ) -> SetConfigResponse:
1407
+ """Apply a partial update to the org's config (PATCH semantics).
1408
+
1409
+ Performs a **top-level shallow merge** of *partial* over the existing
1410
+ config and round-trips through ``Config(**merged)`` so Pydantic
1411
+ validates the result and rejects bogus top-level fields.
1412
+
1413
+ .. warning::
1414
+ Nested objects (e.g. ``storage_config``, ``profile_extractor_config``,
1415
+ ``user_playbook_extractor_config``) are **replaced wholesale**.
1416
+ Deep merging is intentionally not supported -- the discriminator on
1417
+ ``storage_config`` would be lost on partial updates, and merging nested
1418
+ dicts has ambiguous semantics.
1419
+
1420
+ To update a field inside an extractor config you must resend that
1421
+ extractor object fully populated (including ``extractor_name``,
1422
+ ``extraction_definition_prompt``, etc.). For one-off mutations prefer
1423
+ ``GET /api/get_config`` followed by ``POST /api/set_config`` with the
1424
+ modified full config.
1425
+
1426
+ Unlike :func:`set_config`, callers do not need to re-send the full
1427
+ config (including required fields like ``storage_config``) just to
1428
+ flip a single top-level boolean. The merge happens server-side
1429
+ atomically within the request, eliminating the read-modify-write
1430
+ race a client would otherwise hit.
1431
+
1432
+ Args:
1433
+ partial: Top-level fields to overlay on the existing config.
1434
+ org_id: Organization ID resolved by the auth layer.
1435
+
1436
+ Returns:
1437
+ SetConfigResponse: Success status and message from
1438
+ :meth:`Reflexio.set_config`.
1439
+ """
1440
+ from pydantic import ValidationError
1441
+
1442
+ reflexio = get_reflexio(org_id=org_id)
1443
+ existing = reflexio.request_context.configurator.get_config().model_dump(
1444
+ mode="python"
1445
+ )
1446
+ merged = {**existing, **partial}
1447
+ # Pydantic validates the merged shape and rejects unknown / malformed
1448
+ # fields here, before storage validation in reflexio.set_config.
1449
+ # Convert ValidationError into 422 so callers passing a partial that
1450
+ # would replace a nested extractor object with an incomplete dict (e.g.
1451
+ # {"user_playbook_extractor_config": {"aggregation_config": {...}}})
1452
+ # get a clean client-error response instead of a 500.
1453
+ try:
1454
+ merged_config = Config(**merged)
1455
+ except ValidationError as exc:
1456
+ raise HTTPException(
1457
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
1458
+ detail={
1459
+ "error": "Invalid partial config (top-level shallow merge)",
1460
+ "hint": (
1461
+ "Nested objects (e.g. user_playbook_extractor_config) "
1462
+ "are replaced wholesale, not deep-merged. To mutate a "
1463
+ "single nested field, fetch the full config via "
1464
+ "/api/get_config, edit, and POST it back via "
1465
+ "/api/set_config."
1466
+ ),
1467
+ "validation_errors": exc.errors(),
1468
+ },
1469
+ ) from exc
1470
+ response = reflexio.set_config(merged_config)
1471
+ if response.success:
1472
+ invalidate_reflexio_cache(org_id=org_id)
1473
+ return response
1474
+
1475
+
1476
+ @core_router.post("/api/admin/cache/invalidate")
1477
+ def admin_invalidate_cache(
1478
+ payload: AdminInvalidateCacheRequest,
1479
+ org_id: str = Depends(default_get_org_id),
1480
+ ) -> AdminInvalidateCacheResponse:
1481
+ """Explicitly evict the per-org Reflexio cache entry.
1482
+
1483
+ Necessary when the running config has been mutated through a
1484
+ channel the server can't observe — e.g. another replica wrote to
1485
+ the shared DB, or an operator hand-edited a self-host config file
1486
+ on a backend that doesn't support cheap version probing. The
1487
+ file-mtime check (Phase 1) and DB version check (Phase 3) cover
1488
+ most cases automatically; this endpoint is the manual escape hatch.
1489
+
1490
+ Auth uses the same dependency as ``/api/set_config`` — callers
1491
+ can only invalidate their own org's cache. If the request body
1492
+ supplies ``org_id`` it must match the dep-resolved value;
1493
+ cross-org invalidation is intentionally NOT exposed here.
1494
+
1495
+ Args:
1496
+ payload: Optional ``org_id`` (verification only — must match
1497
+ the caller's authenticated org if provided).
1498
+ org_id: Organization ID resolved by the auth layer.
1499
+
1500
+ Returns:
1501
+ AdminInvalidateCacheResponse: ``invalidated`` is True iff an
1502
+ entry was evicted (False is a successful no-op when nothing
1503
+ was cached).
1504
+
1505
+ Raises:
1506
+ HTTPException: 403 when the body's ``org_id`` differs from the
1507
+ caller's authenticated org.
1508
+ """
1509
+ if payload.org_id is not None and payload.org_id != org_id:
1510
+ raise HTTPException(
1511
+ status_code=status.HTTP_403_FORBIDDEN,
1512
+ detail=(
1513
+ "Cross-org cache invalidation is not supported; "
1514
+ "omit org_id or pass your own."
1515
+ ),
1516
+ )
1517
+ invalidated = invalidate_reflexio_cache(org_id=org_id)
1518
+ return AdminInvalidateCacheResponse(invalidated=invalidated, org_id=org_id)
1519
+
1520
+
1521
+ @core_router.get("/api/get_config")
1522
+ def get_config(
1523
+ org_id: str = Depends(default_get_org_id),
1524
+ ) -> dict[str, Any]:
1525
+ """Get configuration for the organization.
1526
+
1527
+ Args:
1528
+ org_id (str): Organization ID
1529
+
1530
+ Returns:
1531
+ Config: The current configuration
1532
+ """
1533
+ # Create Reflexio instance to access the configurator through request_context
1534
+ reflexio = get_reflexio(org_id=org_id)
1535
+
1536
+ return reflexio.request_context.configurator.get_config_for_response()
1537
+
1538
+
1539
+ @core_router.post(
1540
+ "/api/get_user_playbooks",
1541
+ response_model=GetUserPlaybooksViewResponse,
1542
+ response_model_exclude_none=True,
1543
+ )
1544
+ def get_user_playbooks(
1545
+ request: GetUserPlaybooksRequest,
1546
+ org_id: str = Depends(default_get_org_id),
1547
+ ) -> GetUserPlaybooksViewResponse:
1548
+ """Get user playbooks with internal fields filtered out.
1549
+
1550
+ Args:
1551
+ request (GetUserPlaybooksRequest): The get request
1552
+ org_id (str): Organization ID
1553
+
1554
+ Returns:
1555
+ GetUserPlaybooksViewResponse: Response containing user playbooks without internal fields
1556
+ """
1557
+ reflexio = get_reflexio(org_id=org_id)
1558
+ response = reflexio.get_user_playbooks(request)
1559
+ return GetUserPlaybooksViewResponse(
1560
+ success=response.success,
1561
+ user_playbooks=[to_user_playbook_view(rf) for rf in response.user_playbooks],
1562
+ msg=response.msg,
1563
+ )
1564
+
1565
+
1566
+ @core_router.post(
1567
+ "/api/get_agent_playbooks",
1568
+ response_model=GetAgentPlaybooksViewResponse,
1569
+ response_model_exclude_none=True,
1570
+ )
1571
+ def get_agent_playbooks(
1572
+ request: GetAgentPlaybooksRequest,
1573
+ org_id: str = Depends(default_get_org_id),
1574
+ ) -> GetAgentPlaybooksViewResponse:
1575
+ """Get agent playbooks with internal fields filtered out.
1576
+
1577
+ Args:
1578
+ request (GetAgentPlaybooksRequest): The get request
1579
+ org_id (str): Organization ID
1580
+
1581
+ Returns:
1582
+ GetAgentPlaybooksViewResponse: Response containing agent playbooks without internal fields
1583
+ """
1584
+ reflexio = get_reflexio(org_id=org_id)
1585
+ response = reflexio.get_agent_playbooks(request)
1586
+ return GetAgentPlaybooksViewResponse(
1587
+ success=response.success,
1588
+ agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
1589
+ msg=response.msg,
1590
+ )
1591
+
1592
+
1593
+ @core_router.post(
1594
+ "/api/get_agent_success_evaluation_results",
1595
+ response_model=GetEvaluationResultsViewResponse,
1596
+ response_model_exclude_none=True,
1597
+ )
1598
+ def get_agent_success_evaluation_results(
1599
+ request: GetAgentSuccessEvaluationResultsRequest,
1600
+ org_id: str = Depends(default_get_org_id),
1601
+ ) -> GetEvaluationResultsViewResponse:
1602
+ """Get agent success evaluation results.
1603
+
1604
+ Args:
1605
+ request (GetAgentSuccessEvaluationResultsRequest): The get request
1606
+ org_id (str): Organization ID
1607
+
1608
+ Returns:
1609
+ GetEvaluationResultsViewResponse: Response containing agent success evaluation results
1610
+ """
1611
+ reflexio = get_reflexio(org_id=org_id)
1612
+ response = reflexio.get_agent_success_evaluation_results(request)
1613
+ return GetEvaluationResultsViewResponse(
1614
+ success=response.success,
1615
+ agent_success_evaluation_results=[
1616
+ to_evaluation_result_view(r)
1617
+ for r in response.agent_success_evaluation_results
1618
+ ],
1619
+ msg=response.msg,
1620
+ )
1621
+
1622
+
1623
+ @core_router.put(
1624
+ "/api/update_agent_playbook_status",
1625
+ response_model=UpdatePlaybookStatusResponse,
1626
+ response_model_exclude_none=True,
1627
+ )
1628
+ def update_agent_playbook_status_endpoint(
1629
+ request: UpdatePlaybookStatusRequest,
1630
+ org_id: str = Depends(default_get_org_id),
1631
+ ) -> UpdatePlaybookStatusResponse:
1632
+ """Update the status of a specific playbook.
1633
+
1634
+ Args:
1635
+ request (UpdatePlaybookStatusRequest): The update request
1636
+ org_id (str): Organization ID
1637
+
1638
+ Returns:
1639
+ UpdatePlaybookStatusResponse: Response containing success status and message
1640
+ """
1641
+ return publisher_api.update_agent_playbook_status(org_id=org_id, request=request)
1642
+
1643
+
1644
+ @core_router.put(
1645
+ "/api/update_agent_playbook",
1646
+ response_model=UpdateAgentPlaybookResponse,
1647
+ response_model_exclude_none=True,
1648
+ )
1649
+ def update_agent_playbook_endpoint(
1650
+ request: UpdateAgentPlaybookRequest,
1651
+ org_id: str = Depends(default_get_org_id),
1652
+ ) -> UpdateAgentPlaybookResponse:
1653
+ """Update editable fields of a specific agent playbook.
1654
+
1655
+ Args:
1656
+ request (UpdateAgentPlaybookRequest): The update request
1657
+ org_id (str): Organization ID
1658
+
1659
+ Returns:
1660
+ UpdateAgentPlaybookResponse: Response containing success status and message
1661
+ """
1662
+ return publisher_api.update_agent_playbook(org_id=org_id, request=request)
1663
+
1664
+
1665
+ @core_router.put(
1666
+ "/api/update_user_playbook",
1667
+ response_model=UpdateUserPlaybookResponse,
1668
+ response_model_exclude_none=True,
1669
+ )
1670
+ def update_user_playbook_endpoint(
1671
+ request: UpdateUserPlaybookRequest,
1672
+ org_id: str = Depends(default_get_org_id),
1673
+ ) -> UpdateUserPlaybookResponse:
1674
+ """Update editable fields of a specific user playbook.
1675
+
1676
+ Args:
1677
+ request (UpdateUserPlaybookRequest): The update request
1678
+ org_id (str): Organization ID
1679
+
1680
+ Returns:
1681
+ UpdateUserPlaybookResponse: Response containing success status and message
1682
+ """
1683
+ return publisher_api.update_user_playbook(org_id=org_id, request=request)
1684
+
1685
+
1686
+ @core_router.put(
1687
+ "/api/update_user_profile",
1688
+ response_model=UpdateUserProfileResponse,
1689
+ response_model_exclude_none=True,
1690
+ )
1691
+ def update_user_profile_endpoint(
1692
+ request: UpdateUserProfileRequest,
1693
+ org_id: str = Depends(default_get_org_id),
1694
+ ) -> UpdateUserProfileResponse:
1695
+ """Apply a partial update to an existing user profile.
1696
+
1697
+ Args:
1698
+ request (UpdateUserProfileRequest): The update request
1699
+ org_id (str): Organization ID
1700
+
1701
+ Returns:
1702
+ UpdateUserProfileResponse: Response containing success status and message
1703
+ """
1704
+ return publisher_api.update_user_profile(org_id=org_id, request=request)
1705
+
1706
+
1707
+ @core_router.post(
1708
+ "/api/get_dashboard_stats",
1709
+ response_model=GetDashboardStatsResponse,
1710
+ response_model_exclude_none=True,
1711
+ )
1712
+ @limiter.limit("30/minute")
1713
+ def get_dashboard_stats(
1714
+ request: Request,
1715
+ payload: GetDashboardStatsRequest,
1716
+ org_id: str = Depends(default_get_org_id),
1717
+ ) -> GetDashboardStatsResponse:
1718
+ """Get comprehensive dashboard statistics including counts and time-series data.
1719
+
1720
+ Args:
1721
+ request (GetDashboardStatsRequest): Request containing days_back and granularity
1722
+ org_id (str): Organization ID
1723
+
1724
+ Returns:
1725
+ GetDashboardStatsResponse: Response containing dashboard statistics
1726
+ """
1727
+ # Create Reflexio instance
1728
+ reflexio = get_reflexio(org_id=org_id)
1729
+
1730
+ # Get dashboard stats using Reflexio's method
1731
+ return reflexio.get_dashboard_stats(payload)
1732
+
1733
+
1734
+ @core_router.post(
1735
+ "/api/get_playbook_application_stats",
1736
+ response_model=GetPlaybookApplicationStatsResponse,
1737
+ response_model_exclude_none=True,
1738
+ )
1739
+ def get_playbook_application_stats(
1740
+ request: GetPlaybookApplicationStatsRequest,
1741
+ org_id: str = Depends(default_get_org_id),
1742
+ ) -> GetPlaybookApplicationStatsResponse:
1743
+ """Get per-rule citation counts aggregated from interactions.
1744
+
1745
+ Returns one row per cited (kind, real_id) over the look-back window,
1746
+ sorted by applied_count descending. Lets the dashboard show users a
1747
+ per-rule "track record" — how often each playbook or profile has been
1748
+ applied and when it last fired.
1749
+
1750
+ Args:
1751
+ request (GetPlaybookApplicationStatsRequest): Request containing
1752
+ days_back.
1753
+ org_id (str): Organization ID.
1754
+
1755
+ Returns:
1756
+ GetPlaybookApplicationStatsResponse: Response containing aggregated
1757
+ stats.
1758
+ """
1759
+ reflexio = get_reflexio(org_id=org_id)
1760
+ return reflexio.get_playbook_application_stats(request)
1761
+
1762
+
1763
+ # ============================================================================
1764
+ # Braintrust connector (Plan C-backend)
1765
+ # ============================================================================
1766
+
1767
+
1768
+ @core_router.post(
1769
+ "/api/braintrust/connect",
1770
+ response_model=ConnectBraintrustResponse,
1771
+ response_model_exclude_none=True,
1772
+ )
1773
+ @limiter.limit("10/minute")
1774
+ def braintrust_connect(
1775
+ request: Request,
1776
+ payload: ConnectBraintrustRequest,
1777
+ org_id: str = Depends(default_get_org_id),
1778
+ ) -> ConnectBraintrustResponse:
1779
+ """Step 1: validate the Braintrust API key and list workspaces/projects.
1780
+
1781
+ Persists nothing — call `/api/braintrust/select_projects` to commit.
1782
+
1783
+ Args:
1784
+ request (Request): The HTTP request object for rate limiting.
1785
+ payload (ConnectBraintrustRequest): Customer's Braintrust API key.
1786
+ org_id (str): Resolved by auth dependency.
1787
+
1788
+ Returns:
1789
+ ConnectBraintrustResponse: Workspaces tree on success; `success=False`
1790
+ with a message when the key is rejected.
1791
+ """
1792
+ reflexio = get_reflexio(org_id=org_id)
1793
+ return reflexio.braintrust_connect(payload)
1794
+
1795
+
1796
+ @core_router.post(
1797
+ "/api/braintrust/select_projects",
1798
+ response_model=SelectProjectsResponse,
1799
+ response_model_exclude_none=True,
1800
+ )
1801
+ @limiter.limit("10/minute")
1802
+ def braintrust_select_projects(
1803
+ request: Request,
1804
+ payload: SelectProjectsRequest,
1805
+ org_id: str = Depends(default_get_org_id),
1806
+ ) -> SelectProjectsResponse:
1807
+ """Step 2: commit the Braintrust connection with selected projects.
1808
+
1809
+ The API key is encrypted at rest. Subsequent syncs use the persisted
1810
+ connection until the customer calls DELETE /api/braintrust/connection.
1811
+ """
1812
+ reflexio = get_reflexio(org_id=org_id)
1813
+ return reflexio.braintrust_select_projects(payload)
1814
+
1815
+
1816
+ @core_router.get(
1817
+ "/api/braintrust/status",
1818
+ response_model=BraintrustStatusResponse,
1819
+ response_model_exclude_none=True,
1820
+ )
1821
+ def braintrust_status(
1822
+ org_id: str = Depends(default_get_org_id),
1823
+ ) -> BraintrustStatusResponse:
1824
+ """Return Braintrust connection state. Never echoes the API key."""
1825
+ reflexio = get_reflexio(org_id=org_id)
1826
+ return reflexio.braintrust_status()
1827
+
1828
+
1829
+ @core_router.delete("/api/braintrust/connection")
1830
+ @limiter.limit("10/minute")
1831
+ def braintrust_disconnect(
1832
+ request: Request,
1833
+ org_id: str = Depends(default_get_org_id),
1834
+ ) -> dict:
1835
+ """Delete the persisted Braintrust connection for the org.
1836
+
1837
+ Args:
1838
+ org_id (str): Resolved by auth dependency.
1839
+
1840
+ Returns:
1841
+ dict: ``{"success": True}`` on completion.
1842
+ """
1843
+ reflexio = get_reflexio(org_id=org_id)
1844
+ reflexio.braintrust_disconnect()
1845
+ return {"success": True}
1846
+
1847
+
1848
+ @core_router.post(
1849
+ "/api/braintrust/sync",
1850
+ response_model=SyncBraintrustResponse,
1851
+ response_model_exclude_none=True,
1852
+ )
1853
+ @limiter.limit("10/minute")
1854
+ def braintrust_sync(
1855
+ request: Request,
1856
+ org_id: str = Depends(default_get_org_id),
1857
+ ) -> SyncBraintrustResponse:
1858
+ """Trigger a one-shot sync of Braintrust scorer outputs.
1859
+
1860
+ Scheduled (cron) sync is a follow-up; for now the endpoint exists so
1861
+ operators can drive a manual import.
1862
+ """
1863
+ reflexio = get_reflexio(org_id=org_id)
1864
+ return reflexio.braintrust_sync()
1865
+
1866
+
1867
+ @core_router.post(
1868
+ "/api/get_evaluation_overview",
1869
+ response_model=GetEvaluationOverviewResponse,
1870
+ response_model_exclude_none=True,
1871
+ )
1872
+ def get_evaluation_overview(
1873
+ request: GetEvaluationOverviewRequest,
1874
+ org_id: str = Depends(default_get_org_id),
1875
+ ) -> GetEvaluationOverviewResponse:
1876
+ """Return the redesigned /evaluations page payload.
1877
+
1878
+ Aggregates hero state, four context tiles with deltas, top rule
1879
+ attribution, and a corrections-per-session distribution into a single
1880
+ response shaped exactly as the frontend renders it.
1881
+
1882
+ Args:
1883
+ request (GetEvaluationOverviewRequest): Window + bucket granularity.
1884
+ org_id (str): Organization ID resolved by the auth dependency.
1885
+
1886
+ Returns:
1887
+ GetEvaluationOverviewResponse: Full overview payload.
1888
+ """
1889
+ reflexio = get_reflexio(org_id=org_id)
1890
+ return reflexio.get_evaluation_overview(request)
1891
+
1892
+
1893
+ # ---------------------------------------------------------------------------
1894
+ # /api/evaluations/regenerate — replay the LLM judge over a window
1895
+ # ---------------------------------------------------------------------------
1896
+
1897
+
1898
+ @core_router.post(
1899
+ "/api/evaluations/regenerate",
1900
+ response_model=RegenerateStartResponse,
1901
+ response_model_exclude_none=True,
1902
+ )
1903
+ @limiter.limit("5/minute")
1904
+ def start_regenerate(
1905
+ request: Request,
1906
+ payload: RegenerateRequest,
1907
+ org_id: str = Depends(default_get_org_id),
1908
+ ) -> RegenerateStartResponse:
1909
+ """Start a singleton regenerate job over a time window.
1910
+
1911
+ Args:
1912
+ payload (RegenerateRequest): Window bounds plus optional legacy evaluator name.
1913
+ org_id (str): Organization ID resolved by the auth dependency.
1914
+
1915
+ Returns:
1916
+ RegenerateStartResponse: ``job_id`` to poll/cancel and ``total``
1917
+ tuples queued.
1918
+
1919
+ Raises:
1920
+ HTTPException: 409 when a regenerate for the same org is already
1921
+ running. 503 when storage is not configured.
1922
+ """
1923
+ reflexio = get_reflexio(org_id=org_id)
1924
+ storage = reflexio.request_context.storage
1925
+ if storage is None:
1926
+ raise HTTPException(status_code=503, detail="Storage not configured")
1927
+ descriptors = storage.get_session_ids_in_window(
1928
+ from_ts=payload.from_ts, to_ts=payload.to_ts
1929
+ )
1930
+ try:
1931
+ job = REGEN_JOBS.create(
1932
+ org_id=org_id,
1933
+ from_ts=payload.from_ts,
1934
+ to_ts=payload.to_ts,
1935
+ total=len(descriptors),
1936
+ )
1937
+ except RuntimeError as e:
1938
+ raise HTTPException(status_code=409, detail=str(e)) from e
1939
+ _regen_executor.submit(
1940
+ run_regen,
1941
+ job=job,
1942
+ request_context=reflexio.request_context,
1943
+ llm_client=reflexio.llm_client,
1944
+ )
1945
+ return RegenerateStartResponse(job_id=job.job_id, total=job.total)
1946
+
1947
+
1948
+ @core_router.get(
1949
+ "/api/evaluations/regenerate/{job_id}",
1950
+ response_model=RegenerateStatusResponse,
1951
+ response_model_exclude_none=True,
1952
+ )
1953
+ def get_regenerate_status(
1954
+ job_id: str,
1955
+ org_id: str = Depends(default_get_org_id),
1956
+ ) -> RegenerateStatusResponse:
1957
+ """Poll the status of a regenerate job.
1958
+
1959
+ Args:
1960
+ job_id (str): Opaque handle returned by POST /api/evaluations/regenerate.
1961
+ org_id (str): Organization ID resolved by the auth dependency.
1962
+
1963
+ Returns:
1964
+ RegenerateStatusResponse: Counters, status, and failure list.
1965
+
1966
+ Raises:
1967
+ HTTPException: 404 when ``job_id`` is unknown or belongs to a
1968
+ different org.
1969
+ """
1970
+ job = REGEN_JOBS.get(job_id)
1971
+ if job is None or job.org_id != org_id:
1972
+ raise HTTPException(status_code=404, detail="Unknown job_id")
1973
+ return RegenerateStatusResponse(
1974
+ job_id=job.job_id,
1975
+ status=job.status,
1976
+ total=job.total,
1977
+ completed=job.completed,
1978
+ failed=job.failed,
1979
+ failures=[
1980
+ RegenerateFailure(session_id=f.session_id, reason=f.reason)
1981
+ for f in job.failures
1982
+ ],
1983
+ started_at=job.started_at,
1984
+ finished_at=job.finished_at,
1985
+ # F3 informational counters — surface sampler + concurrency facts
1986
+ # so the dashboard can render "n_sampled / total_candidates" and
1987
+ # the configured worker cap without a second round-trip.
1988
+ total_candidates=job.total_candidates,
1989
+ sampled_count=job.sampled_count,
1990
+ concurrency_limit=job.concurrency_limit,
1991
+ )
1992
+
1993
+
1994
+ @core_router.delete("/api/evaluations/regenerate/{job_id}")
1995
+ def cancel_regenerate(
1996
+ job_id: str,
1997
+ org_id: str = Depends(default_get_org_id),
1998
+ ) -> dict[str, str]:
1999
+ """Request cancellation of a running regenerate job.
2000
+
2001
+ Sets the worker's cancel event; the worker checks the flag between
2002
+ sessions and transitions to ``"cancelled"`` on its next iteration.
2003
+
2004
+ Args:
2005
+ job_id (str): Opaque handle returned by POST /api/evaluations/regenerate.
2006
+ org_id (str): Organization ID resolved by the auth dependency.
2007
+
2008
+ Returns:
2009
+ dict[str, str]: ``{"status": "cancelled"}`` on successful flag set.
2010
+
2011
+ Raises:
2012
+ HTTPException: 404 when ``job_id`` is unknown or belongs to a
2013
+ different org.
2014
+ """
2015
+ job = REGEN_JOBS.get(job_id)
2016
+ if job is None or job.org_id != org_id:
2017
+ raise HTTPException(status_code=404, detail="Unknown job_id")
2018
+ REGEN_JOBS.cancel(job_id)
2019
+ return {"status": "cancelled"}
2020
+
2021
+
2022
+ # ---------------------------------------------------------------------------
2023
+ # /api/evaluations/grade_on_demand — single-session click-through grading
2024
+ # ---------------------------------------------------------------------------
2025
+ #
2026
+ # F3 sampling means most sessions in a regen window are NEVER graded —
2027
+ # the sampler keeps cost predictable by capping each (day, group) stratum
2028
+ # at ``Config.eval_sample_n_per_stratum``. Plan 3 (F1) surfaces a
2029
+ # bounded list of sessions in the UI; when a customer clicks one that
2030
+ # wasn't in the sampled subset, the frontend hits this endpoint to grade
2031
+ # it on demand. The 24h cache prevents repeated clicks from triggering
2032
+ # redundant LLM calls.
2033
+
2034
+ _GRADE_ON_DEMAND_CACHE_TTL_SECONDS = 24 * 60 * 60
2035
+ _GRADE_ON_DEMAND_CACHE_KEY_PREFIX = "grade_on_demand"
2036
+
2037
+
2038
+ def _grade_on_demand_cache_key(
2039
+ org_id: str, session_id: str, agent_version: str, evaluation_name: str
2040
+ ) -> str:
2041
+ """Build the operation_state key for the on-demand grading cache.
2042
+
2043
+ The key embeds every active singleton dimension that could change the
2044
+ verdict: org_id (multi-tenant scope), session_id (the unit of work),
2045
+ agent_version (eval results are versioned), and evaluation_name (kept as a
2046
+ compatibility/readback discriminator for historical multi-evaluator rows).
2047
+
2048
+ Args:
2049
+ org_id (str): Tenant identifier from the auth context.
2050
+ session_id (str): Target session.
2051
+ agent_version (str): Agent version filter.
2052
+ evaluation_name (str): Evaluator/result namespace to isolate cache rows.
2053
+ Returns:
2054
+ str: A namespaced key suitable for ``storage.upsert_operation_state``.
2055
+ """
2056
+ return (
2057
+ f"{_GRADE_ON_DEMAND_CACHE_KEY_PREFIX}::{org_id}::{session_id}"
2058
+ f"::{agent_version}::{evaluation_name}"
2059
+ )
2060
+
2061
+
2062
+ def _read_grade_on_demand_cache(
2063
+ storage: Any, cache_key: str, *, now: int
2064
+ ) -> int | None:
2065
+ """Return the cached ``result_id`` if a valid entry exists, else None.
2066
+
2067
+ Returns None on three conditions: no entry, malformed entry, or entry
2068
+ whose ``last_graded_at`` is older than the 24h TTL. Keeps the handler
2069
+ body focused on the happy path.
2070
+
2071
+ Args:
2072
+ storage: The request's storage backend.
2073
+ cache_key (str): Key produced by ``_grade_on_demand_cache_key``.
2074
+ now (int): Current Unix-seconds wall-clock timestamp.
2075
+
2076
+ Returns:
2077
+ int | None: Cached result_id when fresh, else None.
2078
+ """
2079
+ cached_state = storage.get_operation_state(cache_key)
2080
+ if not cached_state:
2081
+ return None
2082
+ state = cached_state.get("operation_state")
2083
+ if not isinstance(state, dict):
2084
+ return None
2085
+ last_graded_at = state.get("last_graded_at")
2086
+ if not isinstance(last_graded_at, int):
2087
+ return None
2088
+ if (now - last_graded_at) >= _GRADE_ON_DEMAND_CACHE_TTL_SECONDS:
2089
+ return None
2090
+ cached_result_id = state.get("result_id")
2091
+ return cached_result_id if isinstance(cached_result_id, int) else None
2092
+
2093
+
2094
+ def _resolve_session_user_id(storage: Any, session_id: str) -> str | None:
2095
+ """Look up the user_id that owns a session_id without requiring it as input.
2096
+
2097
+ Uses ``get_sessions(session_id=...)`` because it's the only storage
2098
+ method on ``BaseStorage`` that accepts session_id alone — every other
2099
+ request-fetcher requires a user_id paired with it. Returns None when
2100
+ the session has no requests, signalling NO_REQUESTS to the caller.
2101
+
2102
+ Args:
2103
+ storage: The request's storage backend.
2104
+ session_id (str): The target session whose owner to resolve.
2105
+
2106
+ Returns:
2107
+ str | None: The user_id of the earliest request in the session,
2108
+ or None when no requests exist.
2109
+ """
2110
+ sessions = storage.get_sessions(session_id=session_id, top_k=100)
2111
+ rows = sessions.get(session_id) or []
2112
+ if not rows:
2113
+ return None
2114
+ earliest = min(rows, key=lambda r: r.request.created_at)
2115
+ return earliest.request.user_id
2116
+
2117
+
2118
+ def _find_fresh_result_id(
2119
+ storage: Any,
2120
+ *,
2121
+ session_id: str,
2122
+ agent_version: str,
2123
+ evaluation_name: str,
2124
+ previous_result_ids: set[int],
2125
+ ) -> int | None:
2126
+ """Locate the result_id written by the most-recent grade for this session.
2127
+
2128
+ The runner writes rows but doesn't return the id. Reading back through
2129
+ ``get_agent_success_evaluation_results`` matches the pattern used by
2130
+ the regen worker's prior-row capture (group_evaluation_runner step 5).
2131
+
2132
+ Args:
2133
+ storage: The request's storage backend.
2134
+ session_id (str): The graded session.
2135
+ agent_version (str): The version dimension.
2136
+ evaluation_name (str): Evaluator/result namespace to isolate readback.
2137
+ previous_result_ids (set[int]): Matching rows observed before grading.
2138
+
2139
+ Returns:
2140
+ int | None: result_id of the latest matching row, or None if the
2141
+ runner wrote nothing.
2142
+ """
2143
+ rows = storage.get_agent_success_evaluation_results(
2144
+ limit=1000, agent_version=agent_version
2145
+ )
2146
+ matched = [
2147
+ r
2148
+ for r in rows
2149
+ if r.session_id == session_id
2150
+ and r.evaluation_name == evaluation_name
2151
+ and r.result_id not in previous_result_ids
2152
+ ]
2153
+ if not matched:
2154
+ return None
2155
+ latest = max(matched, key=lambda r: r.created_at)
2156
+ return latest.result_id
2157
+
2158
+
2159
+ @core_router.post(
2160
+ "/api/evaluations/grade_on_demand",
2161
+ response_model=GradeOnDemandResponse,
2162
+ response_model_exclude_none=False,
2163
+ )
2164
+ def grade_on_demand(
2165
+ payload: GradeOnDemandRequest,
2166
+ org_id: str = Depends(default_get_org_id),
2167
+ ) -> GradeOnDemandResponse:
2168
+ """Grade a single session synchronously; serve cached results within 24h.
2169
+
2170
+ Flow:
2171
+ 1. Read the operation_state cache; if a fresh entry exists, return it
2172
+ with ``cached=True``.
2173
+ 2. Resolve the session's user_id from storage (skip with ``NO_REQUESTS``
2174
+ when the session is unknown — surfaced as 200 + ``skipped_reason``
2175
+ so the frontend's bounded-list click-through can handle stale ids
2176
+ locally without polluting 5xx telemetry).
2177
+ 3. Invoke ``run_group_evaluation(force_regenerate=True)`` so the
2178
+ "already evaluated" short-circuit doesn't suppress a customer's
2179
+ explicit click.
2180
+ 4. Find the freshly-written result_id and persist it in the cache
2181
+ with ``last_graded_at`` so future calls within 24h short-circuit.
2182
+
2183
+ Args:
2184
+ payload (GradeOnDemandRequest): Session + version plus optional legacy evaluator name.
2185
+ org_id (str): Tenant identifier resolved by the auth dependency.
2186
+
2187
+ Returns:
2188
+ GradeOnDemandResponse: Echoes ``session_id`` and carries either
2189
+ a fresh ``result_id`` (``cached=False``), a cached one
2190
+ (``cached=True``), or a ``skipped_reason`` (NO_REQUESTS).
2191
+
2192
+ Raises:
2193
+ HTTPException: 503 when storage is not configured.
2194
+ """
2195
+ reflexio = get_reflexio(org_id=org_id)
2196
+ storage = reflexio.request_context.storage
2197
+ if storage is None:
2198
+ raise HTTPException(status_code=503, detail="Storage not configured")
2199
+
2200
+ evaluation_name = payload.evaluation_name or SINGLETON_AGENT_SUCCESS_EVALUATION_NAME
2201
+ cache_key = _grade_on_demand_cache_key(
2202
+ org_id,
2203
+ payload.session_id,
2204
+ payload.agent_version,
2205
+ evaluation_name,
2206
+ )
2207
+ now = int(datetime.now(UTC).timestamp())
2208
+
2209
+ cached_result_id = _read_grade_on_demand_cache(storage, cache_key, now=now)
2210
+ if cached_result_id is not None:
2211
+ return GradeOnDemandResponse(
2212
+ session_id=payload.session_id,
2213
+ result_id=cached_result_id,
2214
+ cached=True,
2215
+ skipped_reason=None,
2216
+ )
2217
+
2218
+ user_id = _resolve_session_user_id(storage, payload.session_id)
2219
+ if user_id is None:
2220
+ return GradeOnDemandResponse(
2221
+ session_id=payload.session_id,
2222
+ result_id=None,
2223
+ cached=False,
2224
+ skipped_reason="NO_REQUESTS",
2225
+ )
2226
+
2227
+ previous_result_ids = {
2228
+ r.result_id
2229
+ for r in storage.get_agent_success_evaluation_results(
2230
+ limit=1000, agent_version=payload.agent_version
2231
+ )
2232
+ if r.session_id == payload.session_id and r.evaluation_name == evaluation_name
2233
+ }
2234
+
2235
+ # Two operation_state rows are intentionally written for this session:
2236
+ # 1) `grade_on_demand::{org_id}::{session_id}::{agent_version}::{evaluation_name}`
2237
+ # — our 24h cache, set below after the result_id is resolved.
2238
+ # 2) `agent_success_group_eval::{org_id}::{user_id}::{session_id}`
2239
+ # — the runner's own "evaluated" marker, written by
2240
+ # run_group_evaluation. Future background runs without
2241
+ # force_regenerate will skip this session as a result.
2242
+ # The cache key namespaces are distinct so the two markers do not
2243
+ # interfere; the explicit force_regenerate=True here is what makes
2244
+ # an on-demand grade always do real work on a cache miss.
2245
+ run_group_evaluation(
2246
+ org_id=org_id,
2247
+ user_id=user_id,
2248
+ session_id=payload.session_id,
2249
+ agent_version=payload.agent_version,
2250
+ source=None,
2251
+ request_context=reflexio.request_context,
2252
+ llm_client=reflexio.llm_client,
2253
+ force_regenerate=True,
2254
+ )
2255
+
2256
+ result_id = _find_fresh_result_id(
2257
+ storage,
2258
+ session_id=payload.session_id,
2259
+ agent_version=payload.agent_version,
2260
+ evaluation_name=evaluation_name,
2261
+ previous_result_ids=previous_result_ids,
2262
+ )
2263
+
2264
+ storage.upsert_operation_state(
2265
+ cache_key,
2266
+ {"last_graded_at": now, "result_id": result_id},
2267
+ )
2268
+
2269
+ return GradeOnDemandResponse(
2270
+ session_id=payload.session_id,
2271
+ result_id=result_id,
2272
+ cached=False,
2273
+ skipped_reason=None,
2274
+ )
2275
+
2276
+
2277
+ # ---------------------------------------------------------------------------
2278
+ # /api/evaluations/shadow_comparisons/recent — F1 drawer + Top 10 widget
2279
+ # ---------------------------------------------------------------------------
2280
+ #
2281
+ # Powers two surfaces on /evaluations:
2282
+ # 1. The drawer triggered from the per-turn comparison tile — shows the
2283
+ # N most recent verdicts so customers can spot-check the judge.
2284
+ # 2. The "Top 10 disagreements" widget — fetches a wider pool and the
2285
+ # frontend filters to ``is_significantly_better=True`` losses to surface
2286
+ # actionable rule-correction candidates.
2287
+ #
2288
+ # Filtering is restricted to the org's currently pinned
2289
+ # ``shadow_comparison_judge_prompt_version`` so verdicts from an older rubric
2290
+ # never mix into the drawer. The 30-day lookback is a defensive cap that lets
2291
+ # the storage layer use an index range scan instead of a full table read.
2292
+
2293
+ _RECENT_SHADOW_COMPARISONS_LOOKBACK_SECONDS = 30 * 24 * 60 * 60
2294
+ _RECENT_SHADOW_COMPARISONS_MAX_LIMIT = 100
2295
+
2296
+
2297
+ @core_router.get(
2298
+ "/api/evaluations/shadow_comparisons/recent",
2299
+ response_model=GetRecentShadowComparisonsResponse,
2300
+ )
2301
+ def get_recent_shadow_comparisons(
2302
+ limit: int = 10,
2303
+ org_id: str = Depends(default_get_org_id),
2304
+ ) -> GetRecentShadowComparisonsResponse:
2305
+ """Return the N most recent shadow comparison verdicts for the pinned rubric.
2306
+
2307
+ Filters to the org's currently pinned
2308
+ ``Config.shadow_comparison_judge_prompt_version`` so verdicts produced
2309
+ under an older rubric do not mix into the drawer or the Top 10
2310
+ disagreements widget. Storage returns verdicts in ascending ``created_at``
2311
+ order; we reverse to "newest first" and cap at ``limit``.
2312
+
2313
+ Args:
2314
+ limit (int): Max verdicts to return. Clamped to ``[1, 100]``.
2315
+ Default 10 — matches the size of the drawer and Top 10 widget.
2316
+ org_id (str): Tenant identifier resolved by the auth dependency.
2317
+
2318
+ Returns:
2319
+ GetRecentShadowComparisonsResponse: Verdicts in newest-first order.
2320
+ Empty list when the backend does not support the
2321
+ ``shadow_comparison_verdicts`` storage feature, when no verdicts
2322
+ exist in the 30-day window, or when no verdicts match the pinned
2323
+ prompt version.
2324
+
2325
+ Raises:
2326
+ HTTPException: 503 when storage is not configured.
2327
+ """
2328
+ clamped_limit = max(1, min(int(limit), _RECENT_SHADOW_COMPARISONS_MAX_LIMIT))
2329
+ reflexio = get_reflexio(org_id=org_id)
2330
+ storage = reflexio.request_context.storage
2331
+ if storage is None:
2332
+ raise HTTPException(status_code=503, detail="Storage not configured")
2333
+
2334
+ config = reflexio.request_context.configurator.get_config()
2335
+ pinned_version = (
2336
+ config.shadow_comparison_judge_prompt_version
2337
+ if config is not None
2338
+ else "v1.0.0"
2339
+ )
2340
+
2341
+ now = int(datetime.now(UTC).timestamp())
2342
+ try:
2343
+ verdicts = storage.get_shadow_comparison_verdicts(
2344
+ from_ts=now - _RECENT_SHADOW_COMPARISONS_LOOKBACK_SECONDS,
2345
+ to_ts=now,
2346
+ judge_prompt_version=pinned_version,
2347
+ )
2348
+ except NotImplementedError:
2349
+ # Backends that don't support shadow verdicts (e.g., Disk) should
2350
+ # quietly return empty rather than 5xx — the surface degrades to
2351
+ # "no data yet" in the UI.
2352
+ return GetRecentShadowComparisonsResponse(verdicts=[])
2353
+
2354
+ # Storage contract returns ascending — flip to "newest first" and cap.
2355
+ newest_first = list(reversed(verdicts))[:clamped_limit]
2356
+ return GetRecentShadowComparisonsResponse(verdicts=newest_first)
2357
+
2358
+
2359
+ @core_router.post(
2360
+ "/api/rerun_profile_generation",
2361
+ response_model=RerunProfileGenerationResponse,
2362
+ response_model_exclude_none=True,
2363
+ )
2364
+ @limiter.limit("5/minute") # Strict limit for expensive operations
2365
+ def rerun_profile_generation_endpoint(
2366
+ request: Request,
2367
+ payload: RerunProfileGenerationRequest,
2368
+ background_tasks: BackgroundTasks,
2369
+ org_id: str = Depends(default_get_org_id),
2370
+ ) -> RerunProfileGenerationResponse:
2371
+ """Rerun profile generation for a user with filtered interactions.
2372
+
2373
+ Args:
2374
+ request (Request): The HTTP request object (for rate limiting)
2375
+ payload (RerunProfileGenerationRequest): Request containing user_id, time filters, and source
2376
+ background_tasks (BackgroundTasks): Background task runner
2377
+ org_id (str): Organization ID
2378
+
2379
+ Returns:
2380
+ RerunProfileGenerationResponse: Response containing success status and profiles generated count
2381
+ """
2382
+ # Create Reflexio instance
2383
+ reflexio = get_reflexio(org_id=org_id)
2384
+
2385
+ # Run the long-running task in the background to avoid proxy timeout
2386
+ # Client polls get_operation_status for progress
2387
+ background_tasks.add_task(reflexio.rerun_profile_generation, payload)
2388
+
2389
+ return RerunProfileGenerationResponse(
2390
+ success=True, msg="Profile generation started"
2391
+ )
2392
+
2393
+
2394
+ @core_router.post(
2395
+ "/api/manual_profile_generation",
2396
+ response_model=ManualProfileGenerationResponse,
2397
+ response_model_exclude_none=True,
2398
+ )
2399
+ @limiter.limit("5/minute") # Strict limit for expensive operations
2400
+ def manual_profile_generation_endpoint(
2401
+ request: Request,
2402
+ payload: ManualProfileGenerationRequest,
2403
+ org_id: str = Depends(default_get_org_id),
2404
+ ) -> ManualProfileGenerationResponse:
2405
+ """Manually trigger profile generation with window-sized interactions and CURRENT output.
2406
+
2407
+ Runs with auto_run=False, which bypasses the regular stride/should_run
2408
+ gates. Only profile extraction is triggered. Each extractor uses its own
2409
+ window_size_override when present, falling back to the global window_size.
2410
+ Output is CURRENT profiles only.
2411
+
2412
+ Args:
2413
+ request (Request): The HTTP request object (for rate limiting)
2414
+ payload (ManualProfileGenerationRequest): Request containing user_id, source, and extractor_names
2415
+ org_id (str): Organization ID
2416
+
2417
+ Returns:
2418
+ ManualProfileGenerationResponse: Response containing success status and profiles generated count
2419
+ """
2420
+ # Create Reflexio instance
2421
+ reflexio = get_reflexio(org_id=org_id)
2422
+
2423
+ # Call manual_profile_generation
2424
+ return reflexio.manual_profile_generation(payload)
2425
+
2426
+
2427
+ @core_router.post(
2428
+ "/api/rerun_playbook_generation",
2429
+ response_model=RerunPlaybookGenerationResponse,
2430
+ response_model_exclude_none=True,
2431
+ )
2432
+ @limiter.limit("5/minute") # Strict limit for expensive operations
2433
+ def rerun_playbook_generation_endpoint(
2434
+ request: Request,
2435
+ payload: RerunPlaybookGenerationRequest,
2436
+ background_tasks: BackgroundTasks,
2437
+ org_id: str = Depends(default_get_org_id),
2438
+ ) -> RerunPlaybookGenerationResponse:
2439
+ """Rerun playbook generation with filtered interactions.
2440
+
2441
+ Args:
2442
+ request (Request): The HTTP request object (for rate limiting)
2443
+ payload (RerunPlaybookGenerationRequest): Request containing agent_version, time filters, and optional playbook_name
2444
+ background_tasks (BackgroundTasks): Background task runner
2445
+ org_id (str): Organization ID
2446
+
2447
+ Returns:
2448
+ RerunPlaybookGenerationResponse: Response containing success status and playbooks generated count
2449
+ """
2450
+ # Create Reflexio instance
2451
+ reflexio = get_reflexio(org_id=org_id)
2452
+
2453
+ # Run the long-running task in the background to avoid proxy timeout
2454
+ # Client polls get_operation_status for progress
2455
+ background_tasks.add_task(reflexio.rerun_playbook_generation, payload)
2456
+
2457
+ return RerunPlaybookGenerationResponse(
2458
+ success=True, msg="Playbook generation started"
2459
+ )
2460
+
2461
+
2462
+ @core_router.post(
2463
+ "/api/manual_playbook_generation",
2464
+ response_model=ManualPlaybookGenerationResponse,
2465
+ response_model_exclude_none=True,
2466
+ )
2467
+ @limiter.limit("5/minute") # Strict limit for expensive operations
2468
+ def manual_playbook_generation_endpoint(
2469
+ request: Request,
2470
+ payload: ManualPlaybookGenerationRequest,
2471
+ org_id: str = Depends(default_get_org_id),
2472
+ ) -> ManualPlaybookGenerationResponse:
2473
+ """Manually trigger playbook generation with window-sized interactions and CURRENT output.
2474
+
2475
+ Runs with auto_run=False, which bypasses the regular stride/should_run
2476
+ gates. Only playbook extraction is triggered. Each extractor uses its own
2477
+ window_size_override when present, falling back to the global window_size.
2478
+ Output is CURRENT playbooks only.
2479
+
2480
+ Args:
2481
+ request (Request): The HTTP request object (for rate limiting)
2482
+ payload (ManualPlaybookGenerationRequest): Request containing agent_version, source, and playbook_name
2483
+ org_id (str): Organization ID
2484
+
2485
+ Returns:
2486
+ ManualPlaybookGenerationResponse: Response containing success status and playbooks generated count
2487
+ """
2488
+ # Create Reflexio instance
2489
+ reflexio = get_reflexio(org_id=org_id)
2490
+
2491
+ # Call manual_playbook_generation
2492
+ return reflexio.manual_playbook_generation(payload)
2493
+
2494
+
2495
+ @core_router.post(
2496
+ "/api/upgrade_all_profiles",
2497
+ response_model=UpgradeProfilesResponse,
2498
+ response_model_exclude_none=True,
2499
+ )
2500
+ def upgrade_all_profiles_endpoint(
2501
+ request: UpgradeProfilesRequest,
2502
+ org_id: str = Depends(default_get_org_id),
2503
+ ) -> UpgradeProfilesResponse:
2504
+ """Upgrade all profiles by deleting old ARCHIVED, archiving CURRENT, and promoting PENDING.
2505
+
2506
+ This operation performs three atomic steps:
2507
+ 1. Delete all ARCHIVED profiles (old archived profiles from previous upgrades)
2508
+ 2. Archive all CURRENT profiles → ARCHIVED (save current state for potential rollback)
2509
+ 3. Promote all PENDING profiles → CURRENT (activate new profiles)
2510
+
2511
+ Args:
2512
+ request (UpgradeProfilesRequest): The upgrade request with only_affected_users parameter
2513
+ org_id (str): Organization ID
2514
+
2515
+ Returns:
2516
+ UpgradeProfilesResponse: Response containing success status and counts
2517
+ """
2518
+ # Create Reflexio instance
2519
+ reflexio = get_reflexio(org_id=org_id)
2520
+
2521
+ # Call upgrade_all_profiles with request
2522
+ return reflexio.upgrade_all_profiles(request=request)
2523
+
2524
+
2525
+ @core_router.post(
2526
+ "/api/downgrade_all_profiles",
2527
+ response_model=DowngradeProfilesResponse,
2528
+ response_model_exclude_none=True,
2529
+ )
2530
+ def downgrade_all_profiles_endpoint(
2531
+ request: DowngradeProfilesRequest,
2532
+ org_id: str = Depends(default_get_org_id),
2533
+ ) -> DowngradeProfilesResponse:
2534
+ """Downgrade all profiles by demoting CURRENT to PENDING and restoring ARCHIVED.
2535
+
2536
+ This operation performs two atomic steps:
2537
+ 1. Demote all CURRENT profiles → PENDING
2538
+ 2. Restore all ARCHIVED profiles → CURRENT
2539
+
2540
+ Args:
2541
+ request (DowngradeProfilesRequest): The downgrade request with only_affected_users parameter
2542
+ org_id (str): Organization ID
2543
+
2544
+ Returns:
2545
+ DowngradeProfilesResponse: Response containing success status and counts
2546
+ """
2547
+ # Create Reflexio instance
2548
+ reflexio = get_reflexio(org_id=org_id)
2549
+
2550
+ # Call downgrade_all_profiles with request
2551
+ return reflexio.downgrade_all_profiles(request=request)
2552
+
2553
+
2554
+ @core_router.post(
2555
+ "/api/upgrade_all_user_playbooks",
2556
+ response_model=UpgradeUserPlaybooksResponse,
2557
+ response_model_exclude_none=True,
2558
+ )
2559
+ def upgrade_all_user_playbooks_endpoint(
2560
+ request: UpgradeUserPlaybooksRequest,
2561
+ org_id: str = Depends(default_get_org_id),
2562
+ ) -> UpgradeUserPlaybooksResponse:
2563
+ """Upgrade all user playbooks by deleting old ARCHIVED, archiving CURRENT, and promoting PENDING.
2564
+
2565
+ This operation performs three atomic steps:
2566
+ 1. Delete all ARCHIVED user playbooks (old archived from previous upgrades)
2567
+ 2. Archive all CURRENT user playbooks → ARCHIVED (save current state for potential rollback)
2568
+ 3. Promote all PENDING user playbooks → CURRENT (activate new user playbooks)
2569
+
2570
+ Args:
2571
+ request (UpgradeUserPlaybooksRequest): The upgrade request with optional agent_version and playbook_name filters
2572
+ org_id (str): Organization ID
2573
+
2574
+ Returns:
2575
+ UpgradeUserPlaybooksResponse: Response containing success status and counts
2576
+ """
2577
+ # Create Reflexio instance
2578
+ reflexio = get_reflexio(org_id=org_id)
2579
+
2580
+ # Call upgrade_all_user_playbooks with request
2581
+ return reflexio.upgrade_all_user_playbooks(request=request)
2582
+
2583
+
2584
+ @core_router.post(
2585
+ "/api/downgrade_all_user_playbooks",
2586
+ response_model=DowngradeUserPlaybooksResponse,
2587
+ response_model_exclude_none=True,
2588
+ )
2589
+ def downgrade_all_user_playbooks_endpoint(
2590
+ request: DowngradeUserPlaybooksRequest,
2591
+ org_id: str = Depends(default_get_org_id),
2592
+ ) -> DowngradeUserPlaybooksResponse:
2593
+ """Downgrade all user playbooks by archiving CURRENT and restoring ARCHIVED.
2594
+
2595
+ This operation performs three atomic steps:
2596
+ 1. Mark all CURRENT user playbooks → ARCHIVE_IN_PROGRESS (temporary status)
2597
+ 2. Restore all ARCHIVED user playbooks → CURRENT
2598
+ 3. Move all ARCHIVE_IN_PROGRESS user playbooks → ARCHIVED
2599
+
2600
+ Args:
2601
+ request (DowngradeUserPlaybooksRequest): The downgrade request with optional agent_version and playbook_name filters
2602
+ org_id (str): Organization ID
2603
+
2604
+ Returns:
2605
+ DowngradeUserPlaybooksResponse: Response containing success status and counts
2606
+ """
2607
+ # Create Reflexio instance
2608
+ reflexio = get_reflexio(org_id=org_id)
2609
+
2610
+ # Call downgrade_all_user_playbooks with request
2611
+ return reflexio.downgrade_all_user_playbooks(request=request)
2612
+
2613
+
2614
+ @core_router.get(
2615
+ "/api/get_operation_status",
2616
+ response_model=GetOperationStatusResponse,
2617
+ response_model_exclude_none=True,
2618
+ )
2619
+ def get_operation_status_endpoint(
2620
+ service_name: str = "profile_generation",
2621
+ org_id: str = Depends(default_get_org_id),
2622
+ ) -> GetOperationStatusResponse:
2623
+ """Get the status of an operation (e.g., profile generation rerun or manual).
2624
+
2625
+ Args:
2626
+ service_name (str): The service name to query. Defaults to "profile_generation"
2627
+ org_id (str): Organization ID
2628
+
2629
+ Returns:
2630
+ GetOperationStatusResponse: Response containing operation status info
2631
+ """
2632
+ # Create Reflexio instance
2633
+ reflexio = get_reflexio(org_id=org_id)
2634
+
2635
+ # Get operation status
2636
+ request = GetOperationStatusRequest(service_name=service_name)
2637
+ return reflexio.get_operation_status(request)
2638
+
2639
+
2640
+ @core_router.post(
2641
+ "/api/cancel_operation",
2642
+ response_model=CancelOperationResponse,
2643
+ response_model_exclude_none=True,
2644
+ )
2645
+ @limiter.limit("10/minute")
2646
+ def cancel_operation_endpoint(
2647
+ request: Request,
2648
+ payload: CancelOperationRequest,
2649
+ org_id: str = Depends(default_get_org_id),
2650
+ ) -> CancelOperationResponse:
2651
+ """Cancel an in-progress operation (rerun or manual generation).
2652
+
2653
+ Args:
2654
+ request (Request): The HTTP request object (for rate limiting)
2655
+ payload (CancelOperationRequest): Request containing optional service_name
2656
+ org_id (str): Organization ID
2657
+
2658
+ Returns:
2659
+ CancelOperationResponse: Response with list of services that were cancelled
2660
+ """
2661
+ reflexio = get_reflexio(org_id=org_id)
2662
+ return reflexio.cancel_operation(payload)
2663
+
2664
+
2665
+ # Paths that should remain publicly accessible (no lock icon in Swagger)
2666
+ _PUBLIC_PATHS = frozenset(
2667
+ {"/", "/health", "/meta/version", "/token", "/docs", "/openapi.json"}
2668
+ )
2669
+ _PUBLIC_PATH_PREFIXES = ("/api/register", "/api/registration-config", "/api/auth/")
2670
+
2671
+
2672
+ def _add_openapi_security(app: FastAPI) -> None:
2673
+ """Inject Bearer auth security scheme into the OpenAPI spec.
2674
+
2675
+ Overrides the default openapi() method to add a global HTTPBearer security
2676
+ requirement while exempting public endpoints (login, register, health, etc.).
2677
+ """
2678
+ original_openapi = app.openapi
2679
+
2680
+ def custom_openapi() -> dict: # type: ignore[type-arg]
2681
+ if app.openapi_schema:
2682
+ return app.openapi_schema
2683
+
2684
+ schema = original_openapi()
2685
+
2686
+ # Add security scheme
2687
+ schema.setdefault("components", {}).setdefault("securitySchemes", {})
2688
+ schema["components"]["securitySchemes"]["BearerAuth"] = {
2689
+ "type": "http",
2690
+ "scheme": "bearer",
2691
+ "description": "API key or JWT token. Pass as: Authorization: Bearer <token>",
2692
+ }
2693
+
2694
+ # Apply security globally, then remove from public endpoints
2695
+ for path, methods in schema.get("paths", {}).items():
2696
+ is_public = path in _PUBLIC_PATHS or any(
2697
+ path.startswith(prefix) for prefix in _PUBLIC_PATH_PREFIXES
2698
+ )
2699
+ for method_detail in methods.values():
2700
+ if isinstance(method_detail, dict):
2701
+ if is_public:
2702
+ method_detail["security"] = []
2703
+ else:
2704
+ method_detail.setdefault("security", [{"BearerAuth": []}])
2705
+
2706
+ app.openapi_schema = schema
2707
+ return schema
2708
+
2709
+ app.openapi = custom_openapi # type: ignore[method-assign]
2710
+
2711
+
2712
+ def create_app(
2713
+ get_org_id: Callable[..., str] | None = None,
2714
+ additional_routers: list[APIRouter] | None = None,
2715
+ middleware_config: dict | None = None,
2716
+ require_auth: bool = False,
2717
+ ) -> FastAPI:
2718
+ """Factory to create a FastAPI app.
2719
+
2720
+ Args:
2721
+ get_org_id: Custom dependency for resolving org_id (e.g., from JWT auth).
2722
+ When provided, overrides the default_get_org_id dependency globally.
2723
+ additional_routers: Extra APIRouter instances (e.g., enterprise login/oauth).
2724
+ middleware_config: Optional middleware overrides (not used yet, reserved for future).
2725
+ require_auth: When True, declares a Bearer security scheme in the OpenAPI spec
2726
+ so Swagger UI shows lock icons and the Authorize button works.
2727
+
2728
+ Returns:
2729
+ Configured FastAPI application.
2730
+ """
2731
+ from collections.abc import AsyncIterator
2732
+ from contextlib import asynccontextmanager
2733
+
2734
+ from reflexio.server._auth import default_get_org_id
2735
+ from reflexio.server.api_endpoints.request_context import RequestContext
2736
+ from reflexio.server.llm.model_defaults import validate_llm_availability
2737
+ from reflexio.server.services.extraction.resume_scheduler import (
2738
+ maybe_start_resume_scheduler,
2739
+ )
2740
+
2741
+ def _lifespan_org_id() -> str:
2742
+ if get_org_id is None:
2743
+ return default_get_org_id()
2744
+ try:
2745
+ signature = inspect.signature(get_org_id)
2746
+ except (TypeError, ValueError):
2747
+ return default_get_org_id()
2748
+ if signature.parameters:
2749
+ return default_get_org_id()
2750
+ try:
2751
+ return str(get_org_id())
2752
+ except Exception:
2753
+ logger.exception("Failed to resolve lifespan org_id; using default org")
2754
+ return default_get_org_id()
2755
+
2756
+ @asynccontextmanager
2757
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]: # noqa: ARG001
2758
+ validate_llm_availability()
2759
+ from reflexio.server.llm.rerank import prewarm as _prewarm_cross_encoder
2760
+
2761
+ _prewarm_cross_encoder()
2762
+ # The scheduler discovers every org with resumable work each tick and
2763
+ # drives a per-org worker with org-scoped claims, so it is not limited
2764
+ # to the bootstrap org. The bootstrap org is only used to read config
2765
+ # and to seed cross-org discovery.
2766
+ scheduler = maybe_start_resume_scheduler(
2767
+ lambda org_id: RequestContext(org_id=org_id),
2768
+ bootstrap_org_id=_lifespan_org_id(),
2769
+ )
2770
+ try:
2771
+ yield
2772
+ finally:
2773
+ if scheduler is not None:
2774
+ scheduler.stop()
2775
+
2776
+ app = FastAPI(docs_url="/docs", lifespan=lifespan)
2777
+
2778
+ if require_auth:
2779
+ _add_openapi_security(app)
2780
+
2781
+ @app.get("/meta/version")
2782
+ def get_version_info() -> dict[str, str]:
2783
+ from importlib.metadata import PackageNotFoundError, version
2784
+
2785
+ try:
2786
+ server_version = version("reflexio")
2787
+ except PackageNotFoundError:
2788
+ server_version = "0.0.0-dev"
2789
+ return {
2790
+ "server_version": server_version,
2791
+ "api_version": "v1",
2792
+ "min_client_version": "0.1.0",
2793
+ }
2794
+
2795
+ # Configure rate limiter
2796
+ app.state.limiter = limiter
2797
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore[reportArgumentType]
2798
+
2799
+ # CORS
2800
+ # The locked-down, credentialed allowlist is an enterprise concern: only
2801
+ # hosts that wire in auth (``require_auth=True``) restrict browser origins.
2802
+ # OSS/local runs have no auth and bundle their own docs playground on a
2803
+ # separate port, so they allow any origin (no credentials needed).
2804
+ if require_auth:
2805
+ app.add_middleware(
2806
+ CORSMiddleware,
2807
+ allow_origins=_resolve_cors_origins(),
2808
+ allow_credentials=True,
2809
+ allow_methods=["*"],
2810
+ allow_headers=["*"],
2811
+ )
2812
+ else:
2813
+ app.add_middleware(
2814
+ CORSMiddleware,
2815
+ allow_origins=["*"],
2816
+ allow_credentials=False,
2817
+ allow_methods=["*"],
2818
+ allow_headers=["*"],
2819
+ )
2820
+
2821
+ # Reject oversized requests before they reach endpoint handlers.
2822
+ app.add_middleware(BodySizeLimitMiddleware)
2823
+
2824
+ # Security headers
2825
+ app.add_middleware(SecurityHeadersMiddleware)
2826
+
2827
+ # Timeout middleware
2828
+ app.add_middleware(TimeoutMiddleware)
2829
+
2830
+ # Bot protection
2831
+ app.add_middleware(BotProtectionMiddleware)
2832
+
2833
+ # Correlation ID — added last so it runs outermost (Starlette reverses order)
2834
+ app.add_middleware(CorrelationIdMiddleware)
2835
+
2836
+ # Override get_org_id dependency if custom one provided
2837
+ if get_org_id is not None:
2838
+ app.dependency_overrides[default_get_org_id] = get_org_id
2839
+
2840
+ # When a custom get_org_id is provided together with require_auth,
2841
+ # auth is enforced on every route — mark this app instance so the
2842
+ # token-gated my_config endpoint becomes reachable. Using
2843
+ # ``app.state`` instead of a module-level global keeps the gate
2844
+ # scoped to this FastAPI instance, so multiple apps (e.g. tests,
2845
+ # multi-tenant embeddings) can coexist without leaking state.
2846
+ app.state.my_config_enabled = bool(get_org_id is not None and require_auth)
2847
+
2848
+ # Include core routes
2849
+ app.include_router(core_router)
2850
+
2851
+ # Include stall_state routes
2852
+ app.include_router(stall_state_api.router)
2853
+
2854
+ # Include pending tool call routes
2855
+ app.include_router(pending_tool_call_api.router)
2856
+
2857
+ # Include additional routers
2858
+ for router in additional_routers or []:
2859
+ app.include_router(router)
2860
+
2861
+ # Health/observability endpoint (per-worker metrics for recycling)
2862
+ health_api.install(app)
2863
+
2864
+ return app
2865
+
2866
+
2867
+ # Default standalone app (no auth)
2868
+ app = create_app()