claude-smart 0.2.41 → 0.2.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/README.md +1 -1
  3. package/bin/claude-smart.js +86 -48
  4. package/package.json +10 -3
  5. package/plugin/.claude-plugin/plugin.json +9 -3
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +2 -2
  8. package/plugin/dashboard/next.config.ts +9 -1
  9. package/plugin/pyproject.toml +2 -2
  10. package/plugin/scripts/_lib.sh +91 -0
  11. package/plugin/scripts/backend-service.sh +46 -15
  12. package/plugin/scripts/cli.sh +29 -1
  13. package/plugin/scripts/codex-hook.js +72 -4
  14. package/plugin/scripts/dashboard-build.sh +1 -0
  15. package/plugin/scripts/dashboard-service.sh +1 -0
  16. package/plugin/scripts/ensure-plugin-root.sh +7 -14
  17. package/plugin/scripts/hook_entry.sh +1 -0
  18. package/plugin/scripts/smart-install.sh +18 -2
  19. package/plugin/src/claude_smart/cli.py +72 -38
  20. package/plugin/src/claude_smart/context_format.py +11 -12
  21. package/plugin/src/claude_smart/cs_cite.py +26 -12
  22. package/plugin/src/claude_smart/ids.py +13 -5
  23. package/plugin/uv.lock +1 -1
  24. package/plugin/vendor/reflexio/.env.example +53 -0
  25. package/plugin/vendor/reflexio/LICENSE +201 -0
  26. package/plugin/vendor/reflexio/README.md +338 -0
  27. package/plugin/vendor/reflexio/pyproject.toml +271 -0
  28. package/plugin/vendor/reflexio/reflexio/README.md +184 -0
  29. package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
  30. package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
  31. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
  32. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
  33. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
  34. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
  35. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
  36. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
  37. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
  38. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
  39. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
  40. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
  41. package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
  42. package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
  43. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
  44. package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
  45. package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
  46. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +266 -0
  47. package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
  48. package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
  49. package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
  50. package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
  51. package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
  52. package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
  53. package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
  54. package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +961 -0
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
  63. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
  64. package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
  65. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
  66. package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
  67. package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
  68. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
  69. package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
  70. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
  71. package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
  72. package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
  73. package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
  74. package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
  75. package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
  145. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
  146. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
  147. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
  148. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
  149. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
  150. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
  151. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
  152. package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
  153. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
  154. package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
  155. package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
  156. package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
  157. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
  158. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
  159. package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
  160. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
  161. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
  162. package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
  163. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
  164. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
  165. package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
  166. package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
  167. package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
  168. package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
  169. package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
  170. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
  171. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1103 -0
  172. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
  173. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
  174. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
  175. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
  176. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +766 -0
  177. package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
  178. package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
  179. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
  180. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
  181. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
  182. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
  183. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +280 -0
  184. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
  185. package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
  186. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
  187. package/plugin/vendor/reflexio/reflexio/server/README.md +616 -0
  188. package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
  189. package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
  190. package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
  191. package/plugin/vendor/reflexio/reflexio/server/api.py +2714 -0
  192. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
  193. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
  194. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
  195. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
  196. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +540 -0
  197. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
  198. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
  199. package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
  200. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
  201. package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
  202. package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
  203. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +110 -0
  204. package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
  205. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
  206. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
  207. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -0
  208. package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
  209. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
  210. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
  211. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +210 -0
  212. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
  213. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +255 -0
  214. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
  215. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +177 -0
  216. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
  217. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -0
  218. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
  219. package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
  220. package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
  221. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
  222. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
  223. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
  224. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
  225. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
  226. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
  227. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
  228. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
  229. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
  230. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
  231. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
  232. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
  233. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
  234. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
  235. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
  236. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
  237. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
  238. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
  239. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
  240. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
  241. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
  242. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
  243. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
  244. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
  245. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
  246. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +279 -0
  247. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
  248. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
  249. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
  250. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
  251. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
  252. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +127 -0
  253. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
  254. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
  255. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
  256. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
  257. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
  258. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
  259. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
  260. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
  261. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
  262. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
  263. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
  264. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
  265. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
  266. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
  267. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
  268. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
  269. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
  270. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
  271. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
  272. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
  273. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
  274. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
  275. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
  276. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
  277. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
  278. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
  279. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
  281. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
  282. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +340 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1626 -0
  289. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
  290. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
  292. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
  296. package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
  297. package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
  311. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
  312. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +351 -0
  313. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +468 -0
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +777 -0
  318. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
  319. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
  320. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
  321. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -0
  322. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
  323. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
  324. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
  325. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +960 -0
  326. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
  327. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
  328. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
  329. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
  330. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
  331. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
  332. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
  333. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
  334. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
  335. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
  336. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
  337. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
  338. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
  339. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
  340. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
  341. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
  342. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +741 -0
  343. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
  344. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +734 -0
  345. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
  346. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
  347. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
  348. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +800 -0
  349. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
  350. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
  351. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +70 -0
  352. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
  353. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -0
  354. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
  355. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
  356. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
  357. package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
  358. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
  359. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
  360. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
  361. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
  362. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1253 -0
  363. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
  364. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
  365. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
  366. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
  367. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
  368. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
  369. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
  370. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
  371. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
  372. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
  373. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +372 -0
  374. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
  375. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
  376. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
  377. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
  378. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
  379. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
  380. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
  381. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
  382. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
  383. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +568 -0
  384. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
  385. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
  386. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
  387. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
  388. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
  389. package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
  390. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
  391. package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
  392. package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
  393. package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
  394. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
  395. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
  396. package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
@@ -0,0 +1,2613 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import time
5
+ import uuid
6
+ import warnings
7
+ from collections.abc import Callable, Coroutine, Sequence
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from datetime import UTC, datetime
10
+ from typing import Any, TypeVar
11
+ from urllib.parse import urljoin
12
+
13
+ import aiohttp
14
+ import requests
15
+
16
+ from reflexio.defaults import DEFAULT_AGENT_VERSION
17
+ from reflexio.models.api_schema.retriever_schema import (
18
+ ConversationTurn,
19
+ GetAgentPlaybooksRequest,
20
+ GetAgentPlaybooksViewResponse,
21
+ GetAgentSuccessEvaluationResultsRequest,
22
+ GetEvaluationResultsViewResponse,
23
+ GetInteractionsRequest,
24
+ GetInteractionsViewResponse,
25
+ GetProfilesViewResponse,
26
+ GetRequestsRequest,
27
+ GetRequestsViewResponse,
28
+ GetUserPlaybooksRequest,
29
+ GetUserPlaybooksViewResponse,
30
+ GetUserProfilesRequest,
31
+ ProfileChangeLogViewResponse,
32
+ RerankUserProfilesRequest,
33
+ SearchAgentPlaybookRequest,
34
+ SearchAgentPlaybooksViewResponse,
35
+ SearchInteractionRequest,
36
+ SearchInteractionsViewResponse,
37
+ SearchProfilesViewResponse,
38
+ SearchUserPlaybookRequest,
39
+ SearchUserPlaybooksViewResponse,
40
+ SearchUserProfileRequest,
41
+ StorageStatsResponse,
42
+ UnifiedSearchRequest,
43
+ UnifiedSearchViewResponse,
44
+ UpdateAgentPlaybookRequest,
45
+ UpdateAgentPlaybookResponse,
46
+ UpdatePlaybookStatusRequest,
47
+ UpdatePlaybookStatusResponse,
48
+ UpdateUserPlaybookRequest,
49
+ UpdateUserPlaybookResponse,
50
+ )
51
+ from reflexio.models.config_schema import SearchMode
52
+
53
+ IS_TEST_ENV = os.environ.get("IS_TEST_ENV", "false").strip() == "true"
54
+
55
+ BACKEND_URL = "http://127.0.0.1:8000" if IS_TEST_ENV else "https://www.reflexio.ai/"
56
+
57
+ from reflexio.models.api_schema.domain.entities import (
58
+ UpgradeProfilesRequest,
59
+ UpgradeProfilesResponse,
60
+ UpgradeUserPlaybooksRequest,
61
+ UpgradeUserPlaybooksResponse,
62
+ )
63
+ from reflexio.models.api_schema.pending_tool_call_schema import (
64
+ PendingToolCallListResponse,
65
+ PendingToolCallResponse,
66
+ )
67
+ from reflexio.models.api_schema.service_schemas import (
68
+ AddAgentPlaybookRequest,
69
+ AddAgentPlaybookResponse,
70
+ AddUserPlaybookRequest,
71
+ AddUserPlaybookResponse,
72
+ AddUserProfileRequest,
73
+ AddUserProfileResponse,
74
+ AgentPlaybook,
75
+ BulkDeleteResponse,
76
+ ClearUserDataRequest,
77
+ ClearUserDataResponse,
78
+ DeleteAgentPlaybookRequest,
79
+ DeleteAgentPlaybookResponse,
80
+ DeleteAgentPlaybooksByIdsRequest,
81
+ DeleteProfilesByIdsRequest,
82
+ DeleteRequestRequest,
83
+ DeleteRequestResponse,
84
+ DeleteRequestsByIdsRequest,
85
+ DeleteSessionRequest,
86
+ DeleteSessionResponse,
87
+ DeleteUserInteractionRequest,
88
+ DeleteUserInteractionResponse,
89
+ DeleteUserPlaybookRequest,
90
+ DeleteUserPlaybookResponse,
91
+ DeleteUserPlaybooksByIdsRequest,
92
+ DeleteUserProfileRequest,
93
+ DeleteUserProfileResponse,
94
+ GetOperationStatusResponse,
95
+ InteractionData,
96
+ ManualPlaybookGenerationRequest,
97
+ ManualProfileGenerationRequest,
98
+ MyConfigResponse,
99
+ OperationStatus,
100
+ PlaybookStatus,
101
+ PublishUserInteractionRequest,
102
+ PublishUserInteractionResponse,
103
+ RerunPlaybookGenerationRequest,
104
+ RerunPlaybookGenerationResponse,
105
+ RerunProfileGenerationRequest,
106
+ RerunProfileGenerationResponse,
107
+ RunPlaybookAggregationRequest,
108
+ RunPlaybookAggregationResponse,
109
+ Status,
110
+ UserPlaybook,
111
+ UserProfile,
112
+ WhoamiResponse,
113
+ )
114
+ from reflexio.models.api_schema.stall_state_schema import (
115
+ MarkNotifiedResponse,
116
+ StallStateResponse,
117
+ )
118
+ from reflexio.models.config_schema import Config
119
+
120
+ from .cache import InMemoryCache
121
+
122
+ T = TypeVar("T")
123
+
124
+ logger = logging.getLogger(__name__)
125
+
126
+
127
+ class ReflexioAPIError(Exception):
128
+ """Raised when the API returns a successful HTTP status but the body
129
+ cannot be decoded as JSON.
130
+
131
+ This is the "your REFLEXIO_URL points at a non-API host / a proxy
132
+ returned an HTML error page with status 200" class of failure. The
133
+ error message includes the URL, status code, Content-Type, and a
134
+ truncated body preview so the user can see at a glance what went
135
+ wrong. The CLI's ``handle_errors`` decorator renders this as a
136
+ structured ``CliError`` with ``error_type="api"``.
137
+ """
138
+
139
+
140
+ class ReflexioClient:
141
+ """Client for interacting with the Reflexio API."""
142
+
143
+ # Shared thread pool for all instances to maximize efficiency
144
+ _thread_pool = ThreadPoolExecutor(max_workers=4, thread_name_prefix="reflexio")
145
+
146
+ def __init__(
147
+ self, api_key: str = "", url_endpoint: str = "", timeout: int = 300
148
+ ) -> None:
149
+ """Initialize the Reflexio client.
150
+
151
+ Args:
152
+ api_key (str): API key for authentication. Falls back to REFLEXIO_API_KEY env var.
153
+ url_endpoint (str): Base URL for the API. Falls back to REFLEXIO_API_URL env var,
154
+ then to the default backend URL.
155
+ timeout (int): Default request timeout in seconds (default 300)
156
+ """
157
+ self.base_url = (
158
+ url_endpoint or os.environ.get("REFLEXIO_API_URL", "") or BACKEND_URL
159
+ )
160
+ self.api_key = api_key or os.environ.get("REFLEXIO_API_KEY", "")
161
+ self.timeout = timeout
162
+ self.session = requests.Session()
163
+ # Treat any API 3xx as an error instead of silently following.
164
+ # ``requests`` default would demote POST→GET on 302, which has
165
+ # historically turned misconfigured base URLs (e.g. pointing at a
166
+ # marketing site that 302s to its www subdomain) into silent
167
+ # publish losses: the POST became a GET to a marketing page that
168
+ # returned HTML 200, which then crashed ``response.json()``.
169
+ self.session.max_redirects = 0
170
+ self._cache = InMemoryCache()
171
+
172
+ def _get_auth_headers(self) -> dict:
173
+ """Get authentication headers with Bearer token if api_key is configured.
174
+
175
+ Returns:
176
+ dict: Headers with Authorization bearer token, or empty dict if no api_key
177
+ """
178
+ if self.api_key:
179
+ return {
180
+ "Authorization": f"Bearer {self.api_key}",
181
+ "Content-Type": "application/json",
182
+ }
183
+ return {}
184
+
185
+ def _get_headers(self) -> dict:
186
+ """Get default headers for API requests.
187
+
188
+ Returns:
189
+ dict: Headers with content-type and optional authorization
190
+ """
191
+ headers = {"Content-Type": "application/json"}
192
+ if self.api_key:
193
+ headers["Authorization"] = f"Bearer {self.api_key}"
194
+ return headers
195
+
196
+ def _convert_to_model(self, data: dict | object, model_class: type[T]) -> T:
197
+ """Convert dict to model instance if needed.
198
+
199
+ Args:
200
+ data: Either a dict or already an instance of model_class
201
+ model_class: The target class to convert to
202
+
203
+ Returns:
204
+ An instance of model_class
205
+ """
206
+ if isinstance(data, dict):
207
+ return model_class(**data)
208
+ return data # type: ignore[reportReturnType]
209
+
210
+ def _build_request(
211
+ self,
212
+ request: T | dict | None,
213
+ model_class: type[T],
214
+ **kwargs: Any,
215
+ ) -> T:
216
+ """Build request object from request param or kwargs.
217
+
218
+ Args:
219
+ request: Optional request object or dict
220
+ model_class: The request class to instantiate
221
+ **kwargs: Field values to use if request is None
222
+
223
+ Returns:
224
+ An instance of model_class
225
+ """
226
+ if request is not None:
227
+ return self._convert_to_model(request, model_class) # type: ignore[reportReturnType]
228
+ # Filter out None values and build from kwargs
229
+ filtered_kwargs = {k: v for k, v in kwargs.items() if v is not None}
230
+ return model_class(**filtered_kwargs)
231
+
232
+ def _fire_and_forget(
233
+ self,
234
+ async_func: Callable[..., Coroutine[Any, Any, Any]],
235
+ *args: Any,
236
+ **kwargs: Any,
237
+ ) -> None:
238
+ """Execute an async request in fire-and-forget mode.
239
+
240
+ Args:
241
+ async_func: Asynchronous function to call
242
+ *args: Positional arguments to pass to the function
243
+ **kwargs: Keyword arguments to pass to the function
244
+ """
245
+ try:
246
+ loop = asyncio.get_running_loop()
247
+ loop.create_task(async_func(*args, **kwargs))
248
+ except RuntimeError:
249
+ self._thread_pool.submit(lambda: asyncio.run(async_func(*args, **kwargs)))
250
+
251
+ async def _make_async_request(
252
+ self, method: str, endpoint: str, headers: dict | None = None, **kwargs: Any
253
+ ) -> Any:
254
+ """Make an async HTTP request to the API."""
255
+ url = urljoin(self.base_url, endpoint)
256
+
257
+ # Merge auth headers with any provided headers
258
+ request_headers = self._get_headers()
259
+ if headers:
260
+ request_headers.update(headers)
261
+
262
+ async with aiohttp.ClientSession() as async_session:
263
+ response = await async_session.request(
264
+ method, url, headers=request_headers, **kwargs
265
+ )
266
+ response.raise_for_status()
267
+ return await response.json()
268
+
269
+ def _make_request(
270
+ self, method: str, endpoint: str, headers: dict | None = None, **kwargs: Any
271
+ ) -> Any:
272
+ """Make an HTTP request to the API and decode the JSON response.
273
+
274
+ Beyond ``requests.raise_for_status()`` this also guards against
275
+ the "2xx with non-JSON body" failure mode — typically a
276
+ misconfigured base URL pointing at a marketing/proxy host that
277
+ returns an HTML error or redirect page with status 200. Those
278
+ cases produce a ``ReflexioAPIError`` with the URL + body
279
+ preview instead of an opaque ``JSONDecodeError`` deep in the
280
+ stack.
281
+
282
+ Args:
283
+ method: HTTP method (GET, POST, DELETE).
284
+ endpoint: API endpoint path.
285
+ headers: Additional headers to include in the request.
286
+ **kwargs: Extra kwargs passed through to ``requests``.
287
+
288
+ Returns:
289
+ dict: Decoded JSON response, or ``{}`` for empty 2xx
290
+ bodies (e.g. 204 No Content).
291
+
292
+ Raises:
293
+ requests.HTTPError: For 4xx/5xx status codes.
294
+ ReflexioAPIError: For 2xx responses whose body isn't JSON.
295
+ """
296
+ url = urljoin(self.base_url, endpoint)
297
+
298
+ # Merge auth headers with any provided headers
299
+ request_headers = self._get_headers()
300
+ if headers:
301
+ request_headers.update(headers)
302
+
303
+ self.session.headers.update(request_headers)
304
+ kwargs.setdefault("timeout", self.timeout)
305
+ response = self.session.request(method, url, **kwargs)
306
+ response.raise_for_status()
307
+
308
+ # Empty body on a successful response (e.g. 204 No Content).
309
+ # Only treat genuine empty bytes as "no content" — MagicMock
310
+ # tests leave ``content`` as a MagicMock and should fall through.
311
+ content = response.content
312
+ if isinstance(content, (bytes, bytearray)) and not content:
313
+ return {}
314
+
315
+ # Content-Type guard is only enforced when the header is a real
316
+ # string. Test fixtures that mock ``Session.request`` without
317
+ # wiring headers return MagicMock values here and should keep
318
+ # going straight to ``.json()`` for backward compatibility.
319
+ content_type_raw = response.headers.get("Content-Type", "")
320
+ if isinstance(content_type_raw, str) and content_type_raw:
321
+ content_type = content_type_raw.lower()
322
+ if "json" not in content_type:
323
+ body_preview = (
324
+ response.text[:200] if isinstance(response.text, str) else ""
325
+ )
326
+ raise ReflexioAPIError(
327
+ f"Expected JSON from {method} {url} but got "
328
+ f"Content-Type={content_type} "
329
+ f"(status {response.status_code}). "
330
+ f"Body preview: {body_preview!r}. "
331
+ "Is REFLEXIO_URL pointing at the API host?"
332
+ )
333
+ try:
334
+ return response.json()
335
+ except requests.exceptions.JSONDecodeError as exc:
336
+ body_preview = response.text[:200] if isinstance(response.text, str) else ""
337
+ raise ReflexioAPIError(
338
+ f"{method} {url} returned status {response.status_code} but "
339
+ f"the body is not valid JSON: {exc}. "
340
+ f"Body preview: {body_preview!r}. "
341
+ "Is REFLEXIO_URL pointing at the API host?"
342
+ ) from exc
343
+
344
+ def _publish_interaction_sync(
345
+ self,
346
+ request: PublishUserInteractionRequest,
347
+ wait_for_response: bool = False,
348
+ ) -> PublishUserInteractionResponse:
349
+ """Internal sync method to publish interaction.
350
+
351
+ Args:
352
+ request (PublishUserInteractionRequest): The publish request
353
+ wait_for_response (bool): If True, server processes synchronously and returns real result
354
+ """
355
+ params = {"wait_for_response": "true"} if wait_for_response else None
356
+ response = self._make_request(
357
+ "POST",
358
+ "/api/publish_interaction",
359
+ json=request.model_dump(),
360
+ params=params,
361
+ )
362
+ return PublishUserInteractionResponse(**response)
363
+
364
+ async def _publish_interaction_async(
365
+ self,
366
+ request: PublishUserInteractionRequest,
367
+ wait_for_response: bool = False,
368
+ ) -> PublishUserInteractionResponse:
369
+ """Internal async method to publish interaction.
370
+
371
+ Args:
372
+ request (PublishUserInteractionRequest): The publish request
373
+ wait_for_response (bool): If True, server processes synchronously and returns real result
374
+ """
375
+ params = {"wait_for_response": "true"} if wait_for_response else None
376
+ response = await self._make_async_request(
377
+ "POST",
378
+ "/api/publish_interaction",
379
+ json=request.model_dump(),
380
+ params=params,
381
+ )
382
+ return PublishUserInteractionResponse(**response)
383
+
384
+ def publish_interaction(
385
+ self,
386
+ user_id: str,
387
+ interactions: Sequence[InteractionData | dict],
388
+ source: str = "",
389
+ agent_version: str = DEFAULT_AGENT_VERSION,
390
+ session_id: str | None = None,
391
+ wait_for_response: bool = False,
392
+ skip_aggregation: bool = False,
393
+ force_extraction: bool = False,
394
+ override_learning_stall: bool = False,
395
+ metadata: dict[str, Any] | None = None,
396
+ ) -> PublishUserInteractionResponse:
397
+ """Publish user interactions.
398
+
399
+ Always blocks on the HTTP round-trip so the caller can see
400
+ 4xx/5xx, JSON-decode errors, and network errors. The
401
+ ``wait_for_response`` parameter controls whether the **server**
402
+ processes extraction synchronously; the client-side POST is
403
+ always synchronous.
404
+
405
+ In server-async mode (``wait_for_response=False``), the server
406
+ returns 200 as soon as it has registered a BackgroundTask —
407
+ typically ~100 ms. That's fine to block on from the CLI and
408
+ eliminates a prior fragility where CLI fire-and-forget relied
409
+ on a thread pool whose atexit handler wouldn't run under
410
+ SIGTERM, so publishes from a Claude Code subagent could be
411
+ silently lost.
412
+
413
+ Library users who need a truly non-blocking call can submit
414
+ the request through ``_fire_and_forget`` directly.
415
+
416
+ Args:
417
+ user_id: The user ID.
418
+ interactions: List of interaction data.
419
+ source: The source of the interaction.
420
+ agent_version: The agent version.
421
+ session_id: Optional session ID for grouping requests.
422
+ wait_for_response: If True, the **server** waits for
423
+ extraction to complete before returning (longer HTTP
424
+ call, response includes real profile/playbook counts).
425
+ If False, the server returns immediately after queuing
426
+ extraction. The client blocks on the HTTP round-trip
427
+ in both cases.
428
+ skip_aggregation: If True, extract profiles/playbooks but
429
+ skip aggregation to agent playbooks.
430
+ force_extraction: If True, bypass all extraction gates
431
+ (stride_size, cheap pre-filter, LLM should_run) and
432
+ always run extractors.
433
+ override_learning_stall: If True, run extraction even when
434
+ Reflexio has recorded a provider auth/billing stall. Keep
435
+ this False for automatic hook publishes; use it only for an
436
+ explicit retry after reauth or limit reset.
437
+ metadata: Optional per-request annotations stamped by the
438
+ customer. Mirrored onto the stored ``Request`` row so the
439
+ eval pipeline can read them back. Conventional key:
440
+ ``reflexio_retrieval_enabled`` (bool) for F2 group
441
+ assignment. Values must be JSON-encodable.
442
+
443
+ Returns:
444
+ PublishUserInteractionResponse: Server response. In
445
+ ``wait_for_response=False`` mode this is a bare
446
+ acknowledgement ("Interaction queued for processing")
447
+ without extraction counts; in ``wait_for_response=True``
448
+ mode it includes request_id, storage routing, and
449
+ deltas.
450
+ """
451
+ interaction_data_list = [
452
+ (
453
+ InteractionData(**interaction_request)
454
+ if isinstance(interaction_request, dict)
455
+ else interaction_request
456
+ )
457
+ for interaction_request in interactions
458
+ ]
459
+ request = PublishUserInteractionRequest(
460
+ session_id=session_id,
461
+ user_id=user_id,
462
+ interaction_data_list=interaction_data_list,
463
+ source=source,
464
+ agent_version=agent_version,
465
+ skip_aggregation=skip_aggregation,
466
+ force_extraction=force_extraction,
467
+ override_learning_stall=override_learning_stall,
468
+ metadata=metadata or {},
469
+ )
470
+ result = self._publish_interaction_sync(
471
+ request, wait_for_response=wait_for_response
472
+ )
473
+ self._cache.invalidate("get_profiles")
474
+ self._cache.invalidate("get_agent_playbooks")
475
+ return result
476
+
477
+ def search_interactions(
478
+ self,
479
+ request: SearchInteractionRequest | dict | None = None,
480
+ *,
481
+ user_id: str | None = None,
482
+ request_id: str | None = None,
483
+ query: str | None = None,
484
+ start_time: datetime | None = None,
485
+ end_time: datetime | None = None,
486
+ top_k: int | None = None,
487
+ most_recent_k: int | None = None,
488
+ search_mode: SearchMode | None = None,
489
+ ) -> SearchInteractionsViewResponse:
490
+ """Search for user interactions.
491
+
492
+ Args:
493
+ request (Optional[SearchInteractionRequest]): The search request object (alternative to kwargs)
494
+ user_id (str): The user ID to search for
495
+ request_id (Optional[str]): Filter by specific request ID
496
+ query (Optional[str]): Search query string
497
+ start_time (Optional[datetime]): Filter by start time
498
+ end_time (Optional[datetime]): Filter by end time
499
+ top_k (Optional[int]): Maximum number of results to return
500
+ most_recent_k (Optional[int]): Return most recent k interactions
501
+
502
+ Returns:
503
+ SearchInteractionsViewResponse: Response containing matching interactions
504
+ """
505
+ req = self._build_request(
506
+ request,
507
+ SearchInteractionRequest,
508
+ user_id=user_id,
509
+ request_id=request_id,
510
+ query=query,
511
+ start_time=start_time,
512
+ end_time=end_time,
513
+ top_k=top_k,
514
+ most_recent_k=most_recent_k,
515
+ search_mode=search_mode,
516
+ )
517
+ response = self._make_request(
518
+ "POST",
519
+ "/api/search_interactions",
520
+ json=req.model_dump(),
521
+ )
522
+ return SearchInteractionsViewResponse(**response)
523
+
524
+ def search_user_profiles(
525
+ self,
526
+ request: SearchUserProfileRequest | dict | None = None,
527
+ *,
528
+ user_id: str | None = None,
529
+ generated_from_request_id: str | None = None,
530
+ query: str | None = None,
531
+ start_time: datetime | None = None,
532
+ end_time: datetime | None = None,
533
+ top_k: int | None = None,
534
+ source: str | None = None,
535
+ custom_feature: str | None = None,
536
+ extractor_name: str | None = None,
537
+ threshold: float | None = None,
538
+ enable_reformulation: bool | None = None,
539
+ search_mode: SearchMode | None = None,
540
+ ) -> SearchProfilesViewResponse:
541
+ """Search for user profiles.
542
+
543
+ Args:
544
+ request (Optional[SearchUserProfileRequest]): The search request object (alternative to kwargs)
545
+ user_id (str): The user ID to search for
546
+ generated_from_request_id (Optional[str]): Filter by request ID that generated the profile
547
+ query (Optional[str]): Search query string
548
+ start_time (Optional[datetime]): Filter by start time
549
+ end_time (Optional[datetime]): Filter by end time
550
+ top_k (Optional[int]): Maximum number of results to return (default: 10)
551
+ source (Optional[str]): Filter by source
552
+ custom_feature (Optional[str]): Filter by custom feature
553
+ extractor_name (Optional[str]): Deprecated compatibility field. Accepted but ignored.
554
+ threshold (Optional[float]): Similarity threshold (default: 0.7)
555
+ enable_reformulation (Optional[bool]): Enable LLM query reformulation (default: False)
556
+
557
+ Returns:
558
+ SearchProfilesViewResponse: Response containing matching profiles
559
+ """
560
+ req = self._build_request(
561
+ request,
562
+ SearchUserProfileRequest,
563
+ user_id=user_id,
564
+ generated_from_request_id=generated_from_request_id,
565
+ query=query,
566
+ start_time=start_time,
567
+ end_time=end_time,
568
+ top_k=top_k,
569
+ source=source,
570
+ custom_feature=custom_feature,
571
+ extractor_name=extractor_name,
572
+ threshold=threshold,
573
+ enable_reformulation=enable_reformulation,
574
+ search_mode=search_mode,
575
+ )
576
+ response = self._make_request(
577
+ "POST", "/api/search_profiles", json=req.model_dump()
578
+ )
579
+ return SearchProfilesViewResponse(**response)
580
+
581
+ # NOTE: keep signature in sync with search_user_profiles
582
+ def search_profiles(
583
+ self,
584
+ request: SearchUserProfileRequest | dict | None = None,
585
+ *,
586
+ user_id: str | None = None,
587
+ generated_from_request_id: str | None = None,
588
+ query: str | None = None,
589
+ start_time: datetime | None = None,
590
+ end_time: datetime | None = None,
591
+ top_k: int | None = None,
592
+ source: str | None = None,
593
+ custom_feature: str | None = None,
594
+ extractor_name: str | None = None,
595
+ threshold: float | None = None,
596
+ enable_reformulation: bool | None = None,
597
+ search_mode: SearchMode | None = None,
598
+ ) -> SearchProfilesViewResponse:
599
+ """Deprecated alias for :meth:`search_user_profiles`. Will be removed in a future release."""
600
+ warnings.warn(
601
+ "ReflexioClient.search_profiles() is deprecated; use "
602
+ "search_user_profiles() instead.",
603
+ DeprecationWarning,
604
+ stacklevel=2,
605
+ )
606
+ return self.search_user_profiles(
607
+ request,
608
+ user_id=user_id,
609
+ generated_from_request_id=generated_from_request_id,
610
+ query=query,
611
+ start_time=start_time,
612
+ end_time=end_time,
613
+ top_k=top_k,
614
+ source=source,
615
+ custom_feature=custom_feature,
616
+ extractor_name=extractor_name,
617
+ threshold=threshold,
618
+ enable_reformulation=enable_reformulation,
619
+ search_mode=search_mode,
620
+ )
621
+
622
+ def rerank_user_profiles(
623
+ self,
624
+ request: RerankUserProfilesRequest | dict | None = None,
625
+ *,
626
+ user_id: str | None = None,
627
+ query: str | None = None,
628
+ profile_ids: list[str] | None = None,
629
+ top_k: int | None = None,
630
+ ) -> SearchProfilesViewResponse:
631
+ """Rerank a list of profile ids by query relevance using a cross-encoder.
632
+
633
+ The server fetches each candidate's full content (filtered by
634
+ ``user_id``), scores ``(query, content)`` pairs with a CPU
635
+ cross-encoder, and returns the top_k profiles sorted by descending
636
+ score. Profile ids that don't exist for the user are silently dropped.
637
+
638
+ Args:
639
+ request (Optional[RerankUserProfilesRequest]): The rerank request
640
+ object (alternative to kwargs)
641
+ user_id (Optional[str]): The user whose profiles to rerank
642
+ query (Optional[str]): The reranking query
643
+ profile_ids (Optional[list[str]]): Candidate profile ids to score
644
+ top_k (Optional[int]): Maximum profiles to return (default: 10)
645
+
646
+ Returns:
647
+ SearchProfilesViewResponse: Reranked profiles, top_k entries
648
+ """
649
+ req = self._build_request(
650
+ request,
651
+ RerankUserProfilesRequest,
652
+ user_id=user_id,
653
+ query=query,
654
+ profile_ids=profile_ids,
655
+ top_k=top_k,
656
+ )
657
+ response = self._make_request(
658
+ "POST", "/api/rerank_user_profiles", json=req.model_dump()
659
+ )
660
+ return SearchProfilesViewResponse(**response)
661
+
662
+ def storage_stats(
663
+ self,
664
+ user_id: str,
665
+ ) -> StorageStatsResponse:
666
+ """Get a quick count of how many profiles/playbooks the user has.
667
+
668
+ Returns counts and the modified-time range across every status —
669
+ useful for sizing ``top_k`` before retrieval.
670
+
671
+ Args:
672
+ user_id (str): The user to inspect.
673
+
674
+ Returns:
675
+ StorageStatsResponse: Counts and timestamp range for the user.
676
+ """
677
+ response = self._make_request(
678
+ "GET", "/api/storage_stats", params={"user_id": user_id}
679
+ )
680
+ return StorageStatsResponse(**response)
681
+
682
+ def search_user_playbooks(
683
+ self,
684
+ request: SearchUserPlaybookRequest | dict | None = None,
685
+ *,
686
+ query: str | None = None,
687
+ user_id: str | None = None,
688
+ agent_version: str | None = None,
689
+ playbook_name: str | None = None,
690
+ start_time: datetime | None = None,
691
+ end_time: datetime | None = None,
692
+ status_filter: list[Status | None] | None = None,
693
+ top_k: int | None = None,
694
+ threshold: float | None = None,
695
+ enable_reformulation: bool | None = None,
696
+ search_mode: SearchMode | None = None,
697
+ ) -> SearchUserPlaybooksViewResponse:
698
+ """Search for user playbooks with semantic/text search and filtering.
699
+
700
+ Args:
701
+ request (Optional[SearchUserPlaybookRequest]): The search request object (alternative to kwargs)
702
+ query (Optional[str]): Query for semantic/text search
703
+ user_id (Optional[str]): Filter by user (via request_id linkage to requests table)
704
+ agent_version (Optional[str]): Filter by agent version
705
+ playbook_name (Optional[str]): Filter by playbook name
706
+ start_time (Optional[datetime]): Start time for created_at filter
707
+ end_time (Optional[datetime]): End time for created_at filter
708
+ status_filter (Optional[list[Optional[Status]]]): Filter by status (None for CURRENT, PENDING, ARCHIVED)
709
+ top_k (Optional[int]): Maximum number of results to return (default: 10)
710
+ threshold (Optional[float]): Similarity threshold for vector search (default: 0.4)
711
+ enable_reformulation (Optional[bool]): Enable LLM query reformulation (default: False)
712
+
713
+ Returns:
714
+ SearchUserPlaybooksViewResponse: Response containing matching user playbooks
715
+ """
716
+ req = self._build_request(
717
+ request,
718
+ SearchUserPlaybookRequest,
719
+ query=query,
720
+ user_id=user_id,
721
+ agent_version=agent_version,
722
+ playbook_name=playbook_name,
723
+ start_time=start_time,
724
+ end_time=end_time,
725
+ status_filter=status_filter,
726
+ top_k=top_k,
727
+ threshold=threshold,
728
+ enable_reformulation=enable_reformulation,
729
+ search_mode=search_mode,
730
+ )
731
+ response = self._make_request(
732
+ "POST", "/api/search_user_playbooks", json=req.model_dump()
733
+ )
734
+ return SearchUserPlaybooksViewResponse(**response)
735
+
736
+ def search_agent_playbooks(
737
+ self,
738
+ request: SearchAgentPlaybookRequest | dict | None = None,
739
+ *,
740
+ query: str | None = None,
741
+ agent_version: str | None = None,
742
+ playbook_name: str | None = None,
743
+ start_time: datetime | None = None,
744
+ end_time: datetime | None = None,
745
+ status_filter: list[Status | None] | None = None,
746
+ playbook_status_filter: PlaybookStatus | None = None,
747
+ top_k: int | None = None,
748
+ threshold: float | None = None,
749
+ enable_reformulation: bool | None = None,
750
+ search_mode: SearchMode | None = None,
751
+ ) -> SearchAgentPlaybooksViewResponse:
752
+ """Search for agent playbooks with semantic/text search and filtering.
753
+
754
+ Args:
755
+ request (Optional[SearchAgentPlaybookRequest]): The search request object (alternative to kwargs)
756
+ query (Optional[str]): Query for semantic/text search
757
+ agent_version (Optional[str]): Filter by agent version
758
+ playbook_name (Optional[str]): Filter by playbook name
759
+ start_time (Optional[datetime]): Start time for created_at filter
760
+ end_time (Optional[datetime]): End time for created_at filter
761
+ status_filter (Optional[list[Optional[Status]]]): Filter by status (None for CURRENT, PENDING, ARCHIVED)
762
+ playbook_status_filter (Optional[PlaybookStatus]): Filter by playbook status (PENDING, APPROVED, REJECTED)
763
+ top_k (Optional[int]): Maximum number of results to return (default: 10)
764
+ threshold (Optional[float]): Similarity threshold for vector search (default: 0.4)
765
+ enable_reformulation (Optional[bool]): Enable LLM query reformulation (default: False)
766
+
767
+ Returns:
768
+ SearchAgentPlaybooksViewResponse: Response containing matching agent playbooks
769
+ """
770
+ req = self._build_request(
771
+ request,
772
+ SearchAgentPlaybookRequest,
773
+ query=query,
774
+ agent_version=agent_version,
775
+ playbook_name=playbook_name,
776
+ start_time=start_time,
777
+ end_time=end_time,
778
+ status_filter=status_filter,
779
+ playbook_status_filter=playbook_status_filter,
780
+ top_k=top_k,
781
+ threshold=threshold,
782
+ enable_reformulation=enable_reformulation,
783
+ search_mode=search_mode,
784
+ )
785
+ response = self._make_request(
786
+ "POST", "/api/search_agent_playbooks", json=req.model_dump()
787
+ )
788
+ return SearchAgentPlaybooksViewResponse(**response)
789
+
790
+ def _delete_profile_sync(
791
+ self, request: DeleteUserProfileRequest
792
+ ) -> DeleteUserProfileResponse:
793
+ """Internal sync method to delete profile."""
794
+ response = self._make_request(
795
+ "DELETE",
796
+ "/api/delete_profile",
797
+ json=request.model_dump(),
798
+ )
799
+ return DeleteUserProfileResponse(**response)
800
+
801
+ async def _delete_profile_async(
802
+ self, request: DeleteUserProfileRequest
803
+ ) -> DeleteUserProfileResponse:
804
+ """Internal async method to delete profile."""
805
+ response = await self._make_async_request(
806
+ "DELETE",
807
+ "/api/delete_profile",
808
+ json=request.model_dump(),
809
+ )
810
+ return DeleteUserProfileResponse(**response)
811
+
812
+ def delete_profile(
813
+ self,
814
+ user_id: str,
815
+ profile_id: str = "",
816
+ search_query: str = "",
817
+ wait_for_response: bool = False,
818
+ ) -> DeleteUserProfileResponse | None:
819
+ """Delete user profiles.
820
+
821
+ This method is optimized for resource efficiency:
822
+ - In async contexts (e.g., FastAPI): Uses existing event loop (most efficient)
823
+ - In sync contexts: Uses shared thread pool (avoids thread creation overhead)
824
+
825
+ Args:
826
+ user_id (str): The user ID
827
+ profile_id (str, optional): Specific profile ID to delete
828
+ search_query (str, optional): Query to match profiles for deletion
829
+ wait_for_response (bool, optional): If True, wait for response. If False, send request without waiting. Defaults to False.
830
+
831
+ Returns:
832
+ Optional[DeleteUserProfileResponse]: Response containing success status and message if wait_for_response=True, None otherwise
833
+ """
834
+ request = DeleteUserProfileRequest(
835
+ user_id=user_id,
836
+ profile_id=profile_id,
837
+ search_query=search_query,
838
+ )
839
+
840
+ self._cache.invalidate("get_profiles")
841
+ if wait_for_response:
842
+ # Synchronous blocking call
843
+ return self._delete_profile_sync(request)
844
+ # Non-blocking fire-and-forget
845
+ self._fire_and_forget(self._delete_profile_async, request)
846
+ return None
847
+
848
+ def _delete_interaction_sync(
849
+ self, request: DeleteUserInteractionRequest
850
+ ) -> DeleteUserInteractionResponse:
851
+ """Internal sync method to delete interaction."""
852
+ response = self._make_request(
853
+ "DELETE",
854
+ "/api/delete_interaction",
855
+ json=request.model_dump(),
856
+ )
857
+ return DeleteUserInteractionResponse(**response)
858
+
859
+ async def _delete_interaction_async(
860
+ self, request: DeleteUserInteractionRequest
861
+ ) -> DeleteUserInteractionResponse:
862
+ """Internal async method to delete interaction."""
863
+ response = await self._make_async_request(
864
+ "DELETE",
865
+ "/api/delete_interaction",
866
+ json=request.model_dump(),
867
+ )
868
+ return DeleteUserInteractionResponse(**response)
869
+
870
+ def delete_interaction(
871
+ self, user_id: str, interaction_id: int, wait_for_response: bool = False
872
+ ) -> DeleteUserInteractionResponse | None:
873
+ """Delete a user interaction.
874
+
875
+ This method is optimized for resource efficiency:
876
+ - In async contexts (e.g., FastAPI): Uses existing event loop (most efficient)
877
+ - In sync contexts: Uses shared thread pool (avoids thread creation overhead)
878
+
879
+ Args:
880
+ user_id (str): The user ID
881
+ interaction_id (int): The interaction ID to delete
882
+ wait_for_response (bool, optional): If True, wait for response. If False, send request without waiting. Defaults to False.
883
+
884
+ Returns:
885
+ Optional[DeleteUserInteractionResponse]: Response containing success status and message if wait_for_response=True, None otherwise
886
+ """
887
+ request = DeleteUserInteractionRequest(
888
+ user_id=user_id,
889
+ interaction_id=interaction_id,
890
+ )
891
+
892
+ if wait_for_response:
893
+ # Synchronous blocking call
894
+ return self._delete_interaction_sync(request)
895
+ # Non-blocking fire-and-forget
896
+ self._fire_and_forget(self._delete_interaction_async, request)
897
+ return None
898
+
899
+ def _delete_request_sync(
900
+ self, request: DeleteRequestRequest
901
+ ) -> DeleteRequestResponse:
902
+ """Internal sync method to delete request."""
903
+ response = self._make_request(
904
+ "DELETE",
905
+ "/api/delete_request",
906
+ json=request.model_dump(),
907
+ )
908
+ return DeleteRequestResponse(**response)
909
+
910
+ async def _delete_request_async(
911
+ self, request: DeleteRequestRequest
912
+ ) -> DeleteRequestResponse:
913
+ """Internal async method to delete request."""
914
+ response = await self._make_async_request(
915
+ "DELETE",
916
+ "/api/delete_request",
917
+ json=request.model_dump(),
918
+ )
919
+ return DeleteRequestResponse(**response)
920
+
921
+ def delete_request(
922
+ self, request_id: str, wait_for_response: bool = False
923
+ ) -> DeleteRequestResponse | None:
924
+ """Delete a request and all its associated interactions.
925
+
926
+ This method is optimized for resource efficiency:
927
+ - In async contexts (e.g., FastAPI): Uses existing event loop (most efficient)
928
+ - In sync contexts: Uses shared thread pool (avoids thread creation overhead)
929
+
930
+ Args:
931
+ request_id (str): The request ID to delete
932
+ wait_for_response (bool, optional): If True, wait for response. If False, send request without waiting. Defaults to False.
933
+
934
+ Returns:
935
+ Optional[DeleteRequestResponse]: Response containing success status and message if wait_for_response=True, None otherwise
936
+ """
937
+ request = DeleteRequestRequest(request_id=request_id)
938
+
939
+ if wait_for_response:
940
+ # Synchronous blocking call
941
+ return self._delete_request_sync(request)
942
+ # Non-blocking fire-and-forget
943
+ self._fire_and_forget(self._delete_request_async, request)
944
+ return None
945
+
946
+ def _delete_session_sync(
947
+ self, request: DeleteSessionRequest
948
+ ) -> DeleteSessionResponse:
949
+ """Internal sync method to delete session."""
950
+ response = self._make_request(
951
+ "DELETE",
952
+ "/api/delete_session",
953
+ json=request.model_dump(),
954
+ )
955
+ return DeleteSessionResponse(**response)
956
+
957
+ async def _delete_session_async(
958
+ self, request: DeleteSessionRequest
959
+ ) -> DeleteSessionResponse:
960
+ """Internal async method to delete session."""
961
+ response = await self._make_async_request(
962
+ "DELETE",
963
+ "/api/delete_session",
964
+ json=request.model_dump(),
965
+ )
966
+ return DeleteSessionResponse(**response)
967
+
968
+ def delete_session(
969
+ self, session_id: str, wait_for_response: bool = False
970
+ ) -> DeleteSessionResponse | None:
971
+ """Delete all requests and interactions in a session.
972
+
973
+ This method is optimized for resource efficiency:
974
+ - In async contexts (e.g., FastAPI): Uses existing event loop (most efficient)
975
+ - In sync contexts: Uses shared thread pool (avoids thread creation overhead)
976
+
977
+ Args:
978
+ session_id (str): The session ID to delete
979
+ wait_for_response (bool, optional): If True, wait for response. If False, send request without waiting. Defaults to False.
980
+
981
+ Returns:
982
+ Optional[DeleteSessionResponse]: Response containing success status, message, and deleted count if wait_for_response=True, None otherwise
983
+ """
984
+ request = DeleteSessionRequest(session_id=session_id)
985
+
986
+ if wait_for_response:
987
+ # Synchronous blocking call
988
+ return self._delete_session_sync(request)
989
+ # Non-blocking fire-and-forget
990
+ self._fire_and_forget(self._delete_session_async, request)
991
+ return None
992
+
993
+ def _delete_agent_playbook_sync(
994
+ self, request: DeleteAgentPlaybookRequest
995
+ ) -> DeleteAgentPlaybookResponse:
996
+ """Internal sync method to delete agent playbook."""
997
+ response = self._make_request(
998
+ "DELETE",
999
+ "/api/delete_agent_playbook",
1000
+ json=request.model_dump(),
1001
+ )
1002
+ return DeleteAgentPlaybookResponse(**response)
1003
+
1004
+ async def _delete_agent_playbook_async(
1005
+ self, request: DeleteAgentPlaybookRequest
1006
+ ) -> DeleteAgentPlaybookResponse:
1007
+ """Internal async method to delete agent playbook."""
1008
+ response = await self._make_async_request(
1009
+ "DELETE",
1010
+ "/api/delete_agent_playbook",
1011
+ json=request.model_dump(),
1012
+ )
1013
+ return DeleteAgentPlaybookResponse(**response)
1014
+
1015
+ def delete_agent_playbook(
1016
+ self, agent_playbook_id: int, wait_for_response: bool = False
1017
+ ) -> DeleteAgentPlaybookResponse | None:
1018
+ """Delete an agent playbook by ID.
1019
+
1020
+ This method is optimized for resource efficiency:
1021
+ - In async contexts (e.g., FastAPI): Uses existing event loop (most efficient)
1022
+ - In sync contexts: Uses shared thread pool (avoids thread creation overhead)
1023
+
1024
+ Args:
1025
+ agent_playbook_id (int): The agent playbook ID to delete
1026
+ wait_for_response (bool, optional): If True, wait for response. If False, send request without waiting. Defaults to False.
1027
+
1028
+ Returns:
1029
+ Optional[DeleteAgentPlaybookResponse]: Response containing success status and message if wait_for_response=True, None otherwise
1030
+ """
1031
+ request = DeleteAgentPlaybookRequest(agent_playbook_id=agent_playbook_id)
1032
+
1033
+ self._cache.invalidate("get_agent_playbooks")
1034
+ if wait_for_response:
1035
+ # Synchronous blocking call
1036
+ return self._delete_agent_playbook_sync(request)
1037
+ # Non-blocking fire-and-forget
1038
+ self._fire_and_forget(self._delete_agent_playbook_async, request)
1039
+ return None
1040
+
1041
+ def _delete_user_playbook_sync(
1042
+ self, request: DeleteUserPlaybookRequest
1043
+ ) -> DeleteUserPlaybookResponse:
1044
+ """Internal sync method to delete user playbook."""
1045
+ response = self._make_request(
1046
+ "DELETE",
1047
+ "/api/delete_user_playbook",
1048
+ json=request.model_dump(),
1049
+ )
1050
+ return DeleteUserPlaybookResponse(**response)
1051
+
1052
+ async def _delete_user_playbook_async(
1053
+ self, request: DeleteUserPlaybookRequest
1054
+ ) -> DeleteUserPlaybookResponse:
1055
+ """Internal async method to delete user playbook."""
1056
+ response = await self._make_async_request(
1057
+ "DELETE",
1058
+ "/api/delete_user_playbook",
1059
+ json=request.model_dump(),
1060
+ )
1061
+ return DeleteUserPlaybookResponse(**response)
1062
+
1063
+ def delete_user_playbook(
1064
+ self, user_playbook_id: int, wait_for_response: bool = False
1065
+ ) -> DeleteUserPlaybookResponse | None:
1066
+ """Delete a user playbook by ID.
1067
+
1068
+ This method is optimized for resource efficiency:
1069
+ - In async contexts (e.g., FastAPI): Uses existing event loop (most efficient)
1070
+ - In sync contexts: Uses shared thread pool (avoids thread creation overhead)
1071
+
1072
+ Args:
1073
+ user_playbook_id (int): The user playbook ID to delete
1074
+ wait_for_response (bool, optional): If True, wait for response. If False, send request without waiting. Defaults to False.
1075
+
1076
+ Returns:
1077
+ Optional[DeleteUserPlaybookResponse]: Response containing success status and message if wait_for_response=True, None otherwise
1078
+ """
1079
+ request = DeleteUserPlaybookRequest(user_playbook_id=user_playbook_id)
1080
+
1081
+ if wait_for_response:
1082
+ # Synchronous blocking call
1083
+ return self._delete_user_playbook_sync(request)
1084
+ # Non-blocking fire-and-forget
1085
+ self._fire_and_forget(self._delete_user_playbook_async, request)
1086
+ return None
1087
+
1088
+ def get_profile_change_log(self) -> ProfileChangeLogViewResponse:
1089
+ """Get profile change log.
1090
+
1091
+ Returns:
1092
+ ProfileChangeLogViewResponse: Response containing profile change log
1093
+ """
1094
+ response = self._make_request("GET", "/api/profile_change_log")
1095
+ return ProfileChangeLogViewResponse(**response)
1096
+
1097
+ def get_interactions(
1098
+ self,
1099
+ request: GetInteractionsRequest | dict | None = None,
1100
+ *,
1101
+ user_id: str | None = None,
1102
+ start_time: datetime | None = None,
1103
+ end_time: datetime | None = None,
1104
+ top_k: int | None = None,
1105
+ ) -> GetInteractionsViewResponse:
1106
+ """Get user interactions.
1107
+
1108
+ Args:
1109
+ request (Optional[GetInteractionsRequest]): The list request object (alternative to kwargs)
1110
+ user_id (str): The user ID to get interactions for
1111
+ start_time (Optional[datetime]): Filter by start time
1112
+ end_time (Optional[datetime]): Filter by end time
1113
+ top_k (Optional[int]): Maximum number of results to return (default: 30)
1114
+
1115
+ Returns:
1116
+ GetInteractionsViewResponse: Response containing list of interactions
1117
+ """
1118
+ req = self._build_request(
1119
+ request,
1120
+ GetInteractionsRequest,
1121
+ user_id=user_id,
1122
+ start_time=start_time,
1123
+ end_time=end_time,
1124
+ top_k=top_k,
1125
+ )
1126
+ response = self._make_request(
1127
+ "POST",
1128
+ "/api/get_interactions",
1129
+ json=req.model_dump(),
1130
+ )
1131
+ return GetInteractionsViewResponse(**response)
1132
+
1133
+ def get_profiles(
1134
+ self,
1135
+ request: GetUserProfilesRequest | dict | None = None,
1136
+ force_refresh: bool = False,
1137
+ *,
1138
+ user_id: str | None = None,
1139
+ start_time: datetime | None = None,
1140
+ end_time: datetime | None = None,
1141
+ top_k: int | None = None,
1142
+ status_filter: list[Status | str | None] | None = None,
1143
+ ) -> GetProfilesViewResponse:
1144
+ """Get user profiles.
1145
+
1146
+ Args:
1147
+ request (Optional[GetUserProfilesRequest]): The list request object (alternative to kwargs)
1148
+ force_refresh (bool, optional): If True, bypass cache and fetch fresh data. Defaults to False.
1149
+ user_id (str): The user ID to get profiles for
1150
+ start_time (Optional[datetime]): Filter by start time
1151
+ end_time (Optional[datetime]): Filter by end time
1152
+ top_k (Optional[int]): Maximum number of results to return (default: 30)
1153
+ status_filter (Optional[list[Optional[Union[Status, str]]]]): Filter by profile status. Accepts Status enum or string values (e.g., "archived", "pending").
1154
+
1155
+ Returns:
1156
+ GetProfilesViewResponse: Response containing list of profiles
1157
+ """
1158
+ # Convert string status values to Status enum
1159
+ converted_status_filter = None
1160
+ if status_filter is not None:
1161
+ converted_status_filter = []
1162
+ for status in status_filter:
1163
+ if status is None:
1164
+ converted_status_filter.append(None)
1165
+ elif isinstance(status, str):
1166
+ converted_status_filter.append(Status(status))
1167
+ else:
1168
+ converted_status_filter.append(status)
1169
+
1170
+ req = self._build_request(
1171
+ request,
1172
+ GetUserProfilesRequest,
1173
+ user_id=user_id,
1174
+ start_time=start_time,
1175
+ end_time=end_time,
1176
+ top_k=top_k,
1177
+ status_filter=converted_status_filter,
1178
+ )
1179
+
1180
+ # Check cache if not forcing refresh
1181
+ if not force_refresh:
1182
+ cached_result = self._cache.get(
1183
+ "get_profiles",
1184
+ user_id=req.user_id,
1185
+ start_time=req.start_time,
1186
+ end_time=req.end_time,
1187
+ top_k=req.top_k,
1188
+ status_filter=req.status_filter,
1189
+ )
1190
+ if cached_result is not None:
1191
+ return cached_result
1192
+
1193
+ # Make API call
1194
+ response = self._make_request(
1195
+ "POST",
1196
+ "/api/get_profiles",
1197
+ json=req.model_dump(),
1198
+ )
1199
+ result = GetProfilesViewResponse(**response)
1200
+
1201
+ # Store in cache
1202
+ self._cache.set(
1203
+ "get_profiles",
1204
+ result,
1205
+ user_id=req.user_id,
1206
+ start_time=req.start_time,
1207
+ end_time=req.end_time,
1208
+ top_k=req.top_k,
1209
+ status_filter=req.status_filter,
1210
+ )
1211
+
1212
+ return result
1213
+
1214
+ def get_all_interactions(
1215
+ self,
1216
+ limit: int = 100,
1217
+ ) -> GetInteractionsViewResponse:
1218
+ """Get all user interactions across all users.
1219
+
1220
+ Args:
1221
+ limit (int, optional): Maximum number of interactions to return. Defaults to 100.
1222
+
1223
+ Returns:
1224
+ GetInteractionsViewResponse: Response containing all user interactions
1225
+ """
1226
+ response = self._make_request(
1227
+ "GET",
1228
+ f"/api/get_all_interactions?limit={limit}",
1229
+ )
1230
+ return GetInteractionsViewResponse(**response)
1231
+
1232
+ def get_all_profiles(
1233
+ self,
1234
+ limit: int = 100,
1235
+ status_filter: str | None = None,
1236
+ ) -> GetProfilesViewResponse:
1237
+ """Get all user profiles across all users.
1238
+
1239
+ Args:
1240
+ limit (int, optional): Maximum number of profiles to return. Defaults to 100.
1241
+ status_filter (str, optional): Filter by profile status. Accepts
1242
+ ``"current"``, ``"pending"``, or ``"archived"``. If ``None``
1243
+ (the default), profiles with any status are returned.
1244
+
1245
+ Returns:
1246
+ GetProfilesViewResponse: Response containing all user profiles
1247
+ """
1248
+ from urllib.parse import urlencode
1249
+
1250
+ params: dict[str, str | int] = {"limit": limit}
1251
+ if status_filter:
1252
+ params["status_filter"] = status_filter
1253
+ response = self._make_request(
1254
+ "GET",
1255
+ f"/api/get_all_profiles?{urlencode(params)}",
1256
+ )
1257
+ return GetProfilesViewResponse(**response)
1258
+
1259
+ def set_config(self, config: Config | dict) -> dict:
1260
+ """Set configuration for the organization.
1261
+
1262
+ Args:
1263
+ config (Union[Config, dict]): The configuration to set
1264
+
1265
+ Returns:
1266
+ dict: Response containing success status and message
1267
+ """
1268
+ config = self._convert_to_model(config, Config) # type: ignore[reportAssignmentType]
1269
+ return self._make_request(
1270
+ "POST",
1271
+ "/api/set_config",
1272
+ json=config.model_dump(), # type: ignore[reportAttributeAccessIssue]
1273
+ )
1274
+
1275
+ def update_config(self, partial: dict) -> dict:
1276
+ """Apply a partial (PATCH-style) update to the org config.
1277
+
1278
+ Unlike :meth:`set_config`, this does NOT round-trip the payload
1279
+ through ``Config(**...)`` client-side, so partial updates like
1280
+ ``{"shadow_mode_enabled": true}`` succeed without needing
1281
+ the caller to also re-send required fields like ``storage_config``.
1282
+ The server fetches the existing config and shallow-merges
1283
+ atomically — there is no client-side read-modify-write race.
1284
+
1285
+ Nested objects (e.g. ``storage_config``, ``llm_config``) are
1286
+ replaced wholesale; deep merging is intentionally not supported.
1287
+
1288
+ Args:
1289
+ partial: Top-level fields to overlay on the existing config.
1290
+ Unknown keys are dropped server-side by Pydantic.
1291
+
1292
+ Returns:
1293
+ dict: ``{"success": bool, "msg": str}`` from the server.
1294
+
1295
+ Raises:
1296
+ TypeError: If *partial* is not a ``dict``.
1297
+ """
1298
+ if not isinstance(partial, dict):
1299
+ raise TypeError(
1300
+ f"update_config requires a dict, got {type(partial).__name__}"
1301
+ )
1302
+ return self._make_request("POST", "/api/update_config", json=partial)
1303
+
1304
+ def get_config(self) -> Config:
1305
+ """Get configuration for the organization.
1306
+
1307
+ Returns:
1308
+ Config: The current configuration
1309
+ """
1310
+ response = self._make_request(
1311
+ "GET",
1312
+ "/api/get_config",
1313
+ )
1314
+ return Config(**response)
1315
+
1316
+ def invalidate_cache(self, org_id: str | None = None) -> dict:
1317
+ """Explicitly evict the server-side per-org Reflexio cache entry.
1318
+
1319
+ Useful when the running config has been mutated through a
1320
+ channel the server can't observe (sibling-replica writes,
1321
+ direct DB updates, hand-edited config files on backends that
1322
+ don't support cheap version probing). Phase 1 mtime-based
1323
+ auto-invalidation and Phase 3 DB-version probing cover most
1324
+ cases automatically; this is the manual escape hatch.
1325
+
1326
+ Cross-org invalidation is intentionally not supported: the
1327
+ endpoint only ever invalidates the caller's own org. The
1328
+ ``org_id`` argument is a verification token — when provided
1329
+ it must match the token's authenticated org or the server
1330
+ rejects with 403.
1331
+
1332
+ Args:
1333
+ org_id: Optional org_id to verify against the caller's
1334
+ authenticated identity. Omit to let the server
1335
+ resolve it from the auth header.
1336
+
1337
+ Returns:
1338
+ dict: ``{"invalidated": bool, "org_id": str}``. The
1339
+ ``invalidated`` flag is False when nothing was cached for
1340
+ the org (still a successful no-op).
1341
+ """
1342
+ body: dict[str, str] = {}
1343
+ if org_id is not None:
1344
+ body["org_id"] = org_id
1345
+ return self._make_request("POST", "/api/admin/cache/invalidate", json=body)
1346
+
1347
+ def get_user_playbooks(
1348
+ self,
1349
+ request: GetUserPlaybooksRequest | dict | None = None,
1350
+ *,
1351
+ limit: int | None = None,
1352
+ user_id: str | None = None,
1353
+ playbook_name: str | None = None,
1354
+ agent_version: str | None = None,
1355
+ status_filter: list[Status | None] | None = None,
1356
+ ) -> GetUserPlaybooksViewResponse:
1357
+ """Get user playbooks.
1358
+
1359
+ Args:
1360
+ request (Optional[GetUserPlaybooksRequest]): The get request object (alternative to kwargs)
1361
+ limit (Optional[int]): Maximum number of results to return (default: 100)
1362
+ user_id (Optional[str]): Filter by user ID
1363
+ playbook_name (Optional[str]): Filter by playbook name
1364
+ agent_version (Optional[str]): Filter by agent version
1365
+ status_filter (Optional[list[Optional[Status]]]): Filter by status
1366
+
1367
+ Returns:
1368
+ GetUserPlaybooksViewResponse: Response containing user playbooks
1369
+ """
1370
+ req = self._build_request(
1371
+ request,
1372
+ GetUserPlaybooksRequest,
1373
+ limit=limit,
1374
+ user_id=user_id,
1375
+ playbook_name=playbook_name,
1376
+ agent_version=agent_version,
1377
+ status_filter=status_filter,
1378
+ )
1379
+ response = self._make_request(
1380
+ "POST",
1381
+ "/api/get_user_playbooks",
1382
+ json=req.model_dump(),
1383
+ )
1384
+ return GetUserPlaybooksViewResponse(**response)
1385
+
1386
+ def add_user_playbook(
1387
+ self,
1388
+ user_playbooks: list[UserPlaybook | dict],
1389
+ ) -> AddUserPlaybookResponse:
1390
+ """Add user playbooks directly to storage.
1391
+
1392
+ Args:
1393
+ user_playbooks (list[Union[UserPlaybook, dict]]): List of user playbooks to add.
1394
+ Each user playbook should contain:
1395
+ - agent_version (str): Required. The agent version.
1396
+ - request_id (str): Required. The request ID.
1397
+ - content (str): Required. The playbook content.
1398
+ - playbook_name (str): Optional. The playbook name/category.
1399
+
1400
+ Returns:
1401
+ AddUserPlaybookResponse: Response containing success status, message, and added count.
1402
+ """
1403
+ # Convert dicts to UserPlaybook objects if needed
1404
+ user_playbook_list = [
1405
+ UserPlaybook(**rf) if isinstance(rf, dict) else rf for rf in user_playbooks
1406
+ ]
1407
+ request = AddUserPlaybookRequest(user_playbooks=user_playbook_list)
1408
+ response = self._make_request(
1409
+ "POST",
1410
+ "/api/add_user_playbook",
1411
+ json=request.model_dump(),
1412
+ )
1413
+ return AddUserPlaybookResponse(**response)
1414
+
1415
+ def add_agent_playbooks(
1416
+ self,
1417
+ agent_playbooks: list[AgentPlaybook | dict],
1418
+ ) -> AddAgentPlaybookResponse:
1419
+ """Add agent playbooks directly to storage.
1420
+
1421
+ Args:
1422
+ agent_playbooks (list[Union[AgentPlaybook, dict]]): List of agent playbooks to add.
1423
+ Each agent playbook should contain:
1424
+ - agent_version (str): Required. The agent version.
1425
+ - content (str): Required. The playbook content.
1426
+ - playbook_status (PlaybookStatus): Required. The playbook approval status.
1427
+ - playbook_metadata (str): Required. Metadata about the playbook.
1428
+ - playbook_name (str): Optional. The playbook name/category.
1429
+
1430
+ Returns:
1431
+ AddAgentPlaybookResponse: Response containing success status, message, and added count.
1432
+ """
1433
+ # Convert dicts to AgentPlaybook objects if needed
1434
+ agent_playbook_list = [
1435
+ AgentPlaybook(**fb) if isinstance(fb, dict) else fb
1436
+ for fb in agent_playbooks
1437
+ ]
1438
+ request = AddAgentPlaybookRequest(agent_playbooks=agent_playbook_list)
1439
+ response = self._make_request(
1440
+ "POST",
1441
+ "/api/add_agent_playbook",
1442
+ json=request.model_dump(),
1443
+ )
1444
+ return AddAgentPlaybookResponse(**response)
1445
+
1446
+ @staticmethod
1447
+ def _coerce_user_profile(profile: UserProfile | dict) -> UserProfile:
1448
+ """Coerce a dict into a ``UserProfile``, filling in missing required fields.
1449
+
1450
+ Required fields on ``UserProfile`` that callers commonly omit
1451
+ (``profile_id``, ``last_modified_timestamp``, ``generated_from_request_id``)
1452
+ are filled in with sensible client-side defaults so that a minimal dict
1453
+ like ``{"user_id": "u", "content": "x"}`` validates successfully.
1454
+
1455
+ Args:
1456
+ profile (Union[UserProfile, dict]): The profile to coerce. If it is
1457
+ already a ``UserProfile`` it is returned unchanged.
1458
+
1459
+ Returns:
1460
+ UserProfile: A fully-populated ``UserProfile`` instance.
1461
+ """
1462
+ if isinstance(profile, UserProfile):
1463
+ return profile
1464
+ data = dict(profile)
1465
+ data.setdefault("profile_id", f"cli-{uuid.uuid4().hex[:12]}")
1466
+ data.setdefault("last_modified_timestamp", int(datetime.now(UTC).timestamp()))
1467
+ data.setdefault("generated_from_request_id", "cli-manual")
1468
+ data.setdefault("source", "cli-manual")
1469
+ return UserProfile(**data)
1470
+
1471
+ def add_user_profile(
1472
+ self,
1473
+ user_profiles: list[UserProfile | dict],
1474
+ ) -> AddUserProfileResponse:
1475
+ """Add user profiles directly to storage, bypassing inference.
1476
+
1477
+ Mirror of :meth:`add_user_playbook` for the profile resource.
1478
+ Useful for seeding a known fact about the user (testing,
1479
+ migration, manual fact injection) without going through the
1480
+ interaction-based generation pipeline. The server populates
1481
+ the embedding automatically.
1482
+
1483
+ Args:
1484
+ user_profiles (list[Union[UserProfile, dict]]): List of user profiles to add.
1485
+ Each profile must contain at least:
1486
+ - user_id (str): The user the profile belongs to.
1487
+ - content (str): The profile content (used for embedding).
1488
+ When passing dicts, missing required fields are auto-populated
1489
+ client-side with sensible defaults: ``profile_id`` becomes
1490
+ ``f"cli-{uuid4.hex[:12]}"``, ``last_modified_timestamp`` is set to
1491
+ ``int(datetime.now(UTC).timestamp())``, and
1492
+ ``generated_from_request_id`` defaults to ``"cli-manual"``.
1493
+
1494
+ Returns:
1495
+ AddUserProfileResponse: Response containing success status, message, and added count.
1496
+ """
1497
+ user_profile_list = [self._coerce_user_profile(p) for p in user_profiles]
1498
+ request = AddUserProfileRequest(user_profiles=user_profile_list)
1499
+ response = self._make_request(
1500
+ "POST",
1501
+ "/api/add_user_profile",
1502
+ json=request.model_dump(),
1503
+ )
1504
+ return AddUserProfileResponse(**response)
1505
+
1506
+ def update_user_playbook(
1507
+ self,
1508
+ user_playbook_id: int,
1509
+ *,
1510
+ playbook_name: str | None = None,
1511
+ content: str | None = None,
1512
+ trigger: str | None = None,
1513
+ rationale: str | None = None,
1514
+ ) -> UpdateUserPlaybookResponse:
1515
+ """Update editable fields of a user playbook in place.
1516
+
1517
+ Pass only the fields you want to change. Fields left as
1518
+ ``None`` are not touched on the server side.
1519
+
1520
+ Args:
1521
+ user_playbook_id (int): The ID of the user playbook to update.
1522
+ playbook_name (Optional[str]): New playbook category name.
1523
+ content (Optional[str]): New content text.
1524
+ trigger (Optional[str]): New trigger condition.
1525
+ rationale (Optional[str]): New rationale text.
1526
+
1527
+ Returns:
1528
+ UpdateUserPlaybookResponse: Response containing success status and message.
1529
+ """
1530
+ request = UpdateUserPlaybookRequest(
1531
+ user_playbook_id=user_playbook_id,
1532
+ playbook_name=playbook_name,
1533
+ content=content,
1534
+ trigger=trigger,
1535
+ rationale=rationale,
1536
+ )
1537
+ response = self._make_request(
1538
+ "PUT",
1539
+ "/api/update_user_playbook",
1540
+ json=request.model_dump(),
1541
+ )
1542
+ return UpdateUserPlaybookResponse(**response)
1543
+
1544
+ def update_agent_playbook(
1545
+ self,
1546
+ agent_playbook_id: int,
1547
+ *,
1548
+ playbook_name: str | None = None,
1549
+ content: str | None = None,
1550
+ trigger: str | None = None,
1551
+ rationale: str | None = None,
1552
+ playbook_status: PlaybookStatus | None = None,
1553
+ ) -> UpdateAgentPlaybookResponse:
1554
+ """Update editable fields of an agent playbook in place.
1555
+
1556
+ Pass only the fields you want to change. To change ONLY the
1557
+ approval status, prefer :meth:`update_agent_playbook_status` —
1558
+ it has tighter semantics and a single-purpose endpoint.
1559
+
1560
+ Args:
1561
+ agent_playbook_id (int): The ID of the agent playbook to update.
1562
+ playbook_name (Optional[str]): New playbook category name.
1563
+ content (Optional[str]): New content text.
1564
+ trigger (Optional[str]): New trigger condition.
1565
+ rationale (Optional[str]): New rationale text.
1566
+ playbook_status (Optional[PlaybookStatus]): New approval status.
1567
+
1568
+ Returns:
1569
+ UpdateAgentPlaybookResponse: Response containing success status and message.
1570
+ """
1571
+ request = UpdateAgentPlaybookRequest(
1572
+ agent_playbook_id=agent_playbook_id,
1573
+ playbook_name=playbook_name,
1574
+ content=content,
1575
+ trigger=trigger,
1576
+ rationale=rationale,
1577
+ playbook_status=playbook_status,
1578
+ )
1579
+ response = self._make_request(
1580
+ "PUT",
1581
+ "/api/update_agent_playbook",
1582
+ json=request.model_dump(),
1583
+ )
1584
+ self._cache.invalidate("get_agent_playbooks")
1585
+ return UpdateAgentPlaybookResponse(**response)
1586
+
1587
+ def update_agent_playbook_status(
1588
+ self,
1589
+ agent_playbook_id: int,
1590
+ *,
1591
+ playbook_status: PlaybookStatus,
1592
+ ) -> UpdatePlaybookStatusResponse:
1593
+ """Update only the approval status of an agent playbook.
1594
+
1595
+ This is the dedicated endpoint for the approval workflow
1596
+ (approve / pending / reject). Use it instead of
1597
+ :meth:`update_agent_playbook` when the only change is the
1598
+ ``playbook_status`` — the server enforces tighter validation
1599
+ and writes a smaller change log.
1600
+
1601
+ Args:
1602
+ agent_playbook_id (int): The ID of the agent playbook.
1603
+ playbook_status (PlaybookStatus): New approval status
1604
+ (``PENDING``, ``APPROVED``, or ``REJECTED``).
1605
+
1606
+ Returns:
1607
+ UpdatePlaybookStatusResponse: Response containing success status and message.
1608
+ """
1609
+ request = UpdatePlaybookStatusRequest(
1610
+ agent_playbook_id=agent_playbook_id,
1611
+ playbook_status=playbook_status,
1612
+ )
1613
+ response = self._make_request(
1614
+ "PUT",
1615
+ "/api/update_agent_playbook_status",
1616
+ json=request.model_dump(),
1617
+ )
1618
+ self._cache.invalidate("get_agent_playbooks")
1619
+ return UpdatePlaybookStatusResponse(**response)
1620
+
1621
+ def get_agent_playbooks(
1622
+ self,
1623
+ request: GetAgentPlaybooksRequest | dict | None = None,
1624
+ force_refresh: bool = False,
1625
+ *,
1626
+ limit: int | None = None,
1627
+ playbook_name: str | None = None,
1628
+ agent_version: str | None = None,
1629
+ status_filter: list[Status | None] | None = None,
1630
+ playbook_status_filter: PlaybookStatus | None = None,
1631
+ ) -> GetAgentPlaybooksViewResponse:
1632
+ """Get agent playbooks.
1633
+
1634
+ Args:
1635
+ request (Optional[GetAgentPlaybooksRequest]): The get request object (alternative to kwargs)
1636
+ force_refresh (bool, optional): If True, bypass cache and fetch fresh data. Defaults to False.
1637
+ limit (Optional[int]): Maximum number of results to return (default: 100)
1638
+ playbook_name (Optional[str]): Filter by playbook name
1639
+ agent_version (Optional[str]): Filter by agent version
1640
+ status_filter (Optional[list[Optional[Status]]]): Filter by status
1641
+ playbook_status_filter (Optional[PlaybookStatus]): Filter by playbook status (default: APPROVED)
1642
+
1643
+ Returns:
1644
+ GetAgentPlaybooksViewResponse: Response containing agent playbooks
1645
+ """
1646
+ req = self._build_request(
1647
+ request,
1648
+ GetAgentPlaybooksRequest,
1649
+ limit=limit,
1650
+ playbook_name=playbook_name,
1651
+ agent_version=agent_version,
1652
+ status_filter=status_filter,
1653
+ playbook_status_filter=playbook_status_filter,
1654
+ )
1655
+
1656
+ # Check cache if not forcing refresh
1657
+ if not force_refresh:
1658
+ cached_result = self._cache.get(
1659
+ "get_agent_playbooks",
1660
+ limit=req.limit,
1661
+ playbook_name=req.playbook_name,
1662
+ status_filter=req.status_filter,
1663
+ playbook_status_filter=req.playbook_status_filter,
1664
+ )
1665
+ if cached_result is not None:
1666
+ return cached_result
1667
+
1668
+ # Make API call
1669
+ response = self._make_request(
1670
+ "POST",
1671
+ "/api/get_agent_playbooks",
1672
+ json=req.model_dump(),
1673
+ )
1674
+ result = GetAgentPlaybooksViewResponse(**response)
1675
+
1676
+ # Store in cache
1677
+ self._cache.set(
1678
+ "get_agent_playbooks",
1679
+ result,
1680
+ limit=req.limit,
1681
+ playbook_name=req.playbook_name,
1682
+ status_filter=req.status_filter,
1683
+ playbook_status_filter=req.playbook_status_filter,
1684
+ )
1685
+
1686
+ return result
1687
+
1688
+ def get_requests(
1689
+ self,
1690
+ request: GetRequestsRequest | dict | None = None,
1691
+ *,
1692
+ user_id: str | None = None,
1693
+ request_id: str | None = None,
1694
+ start_time: datetime | None = None,
1695
+ end_time: datetime | None = None,
1696
+ top_k: int | None = None,
1697
+ ) -> GetRequestsViewResponse:
1698
+ """Get requests with their associated interactions, grouped by session.
1699
+
1700
+ Args:
1701
+ request (Optional[GetRequestsRequest]): The get request object (alternative to kwargs)
1702
+ user_id (Optional[str]): Filter by user ID
1703
+ request_id (Optional[str]): Filter by request ID
1704
+ start_time (Optional[datetime]): Filter by start time
1705
+ end_time (Optional[datetime]): Filter by end time
1706
+ top_k (Optional[int]): Maximum number of results to return (default: 30)
1707
+
1708
+ Returns:
1709
+ GetRequestsViewResponse: Response containing requests grouped by session with their interactions
1710
+ """
1711
+ req = self._build_request(
1712
+ request,
1713
+ GetRequestsRequest,
1714
+ user_id=user_id,
1715
+ request_id=request_id,
1716
+ start_time=start_time,
1717
+ end_time=end_time,
1718
+ top_k=top_k,
1719
+ )
1720
+ response = self._make_request(
1721
+ "POST",
1722
+ "/api/get_requests",
1723
+ json=req.model_dump(),
1724
+ )
1725
+ return GetRequestsViewResponse(**response)
1726
+
1727
+ def get_agent_success_evaluation_results(
1728
+ self,
1729
+ request: GetAgentSuccessEvaluationResultsRequest | dict | None = None,
1730
+ *,
1731
+ limit: int | None = None,
1732
+ agent_version: str | None = None,
1733
+ ) -> GetEvaluationResultsViewResponse:
1734
+ """Get agent success evaluation results.
1735
+
1736
+ Args:
1737
+ request (Optional[GetAgentSuccessEvaluationResultsRequest]): The get request object (alternative to kwargs)
1738
+ limit (Optional[int]): Maximum number of results to return (default: 100)
1739
+ agent_version (Optional[str]): Filter by agent version
1740
+
1741
+ Returns:
1742
+ GetEvaluationResultsViewResponse: Response containing agent success evaluation results
1743
+ """
1744
+ req = self._build_request(
1745
+ request,
1746
+ GetAgentSuccessEvaluationResultsRequest,
1747
+ limit=limit,
1748
+ agent_version=agent_version,
1749
+ )
1750
+ response = self._make_request(
1751
+ "POST",
1752
+ "/api/get_agent_success_evaluation_results",
1753
+ json=req.model_dump(),
1754
+ )
1755
+ return GetEvaluationResultsViewResponse(**response)
1756
+
1757
+ def _poll_operation_status(
1758
+ self,
1759
+ service_name: str,
1760
+ poll_interval: float = 3.0,
1761
+ max_wait: float = 600.0,
1762
+ min_started_at: int | None = None,
1763
+ ) -> GetOperationStatusResponse:
1764
+ """
1765
+ Poll the operation status endpoint until the operation completes, fails, or is cancelled.
1766
+
1767
+ Args:
1768
+ service_name: The service name to poll (e.g. "profile_generation", "playbook_generation")
1769
+ poll_interval: Seconds between polls
1770
+ max_wait: Maximum seconds to wait before raising TimeoutError
1771
+ min_started_at: Optional Unix timestamp. Ignore stale operation
1772
+ states that started before this timestamp.
1773
+
1774
+ Returns:
1775
+ GetOperationStatusResponse: Final operation status
1776
+ """
1777
+ start = time.monotonic()
1778
+ while True:
1779
+ try:
1780
+ response = self._make_request(
1781
+ "GET",
1782
+ "/api/get_operation_status",
1783
+ params={"service_name": service_name},
1784
+ )
1785
+ except Exception as e:
1786
+ logger.warning("Failed to poll operation status: %s", e)
1787
+ elapsed = time.monotonic() - start
1788
+ if elapsed + poll_interval > max_wait:
1789
+ raise TimeoutError(
1790
+ f"Operation '{service_name}' did not complete within {max_wait}s"
1791
+ ) from e
1792
+ time.sleep(poll_interval)
1793
+ continue
1794
+ status_response = GetOperationStatusResponse(**response)
1795
+ op = status_response.operation_status
1796
+ if op and min_started_at is not None and op.started_at < min_started_at:
1797
+ op = None
1798
+ if op and op.status in (
1799
+ OperationStatus.COMPLETED,
1800
+ OperationStatus.FAILED,
1801
+ OperationStatus.CANCELLED,
1802
+ ):
1803
+ return status_response
1804
+ elapsed = time.monotonic() - start
1805
+ if elapsed + poll_interval > max_wait:
1806
+ raise TimeoutError(
1807
+ f"Operation '{service_name}' did not complete within {max_wait}s"
1808
+ )
1809
+ time.sleep(poll_interval)
1810
+
1811
+ def _rerun_profile_generation_sync(
1812
+ self, request: RerunProfileGenerationRequest
1813
+ ) -> RerunProfileGenerationResponse:
1814
+ """Internal sync method to rerun profile generation.
1815
+
1816
+ Submits the request, then polls operation status until completion.
1817
+ """
1818
+ submitted_at = int(time.time())
1819
+ response = self._make_request(
1820
+ "POST",
1821
+ "/api/rerun_profile_generation",
1822
+ json=request.model_dump(),
1823
+ )
1824
+ initial = RerunProfileGenerationResponse(**response)
1825
+ if not initial.success:
1826
+ return initial
1827
+
1828
+ # Poll until the background task completes
1829
+ try:
1830
+ status_response = self._poll_operation_status(
1831
+ "profile_generation", min_started_at=submitted_at
1832
+ )
1833
+ op = status_response.operation_status
1834
+ if op and op.status == OperationStatus.COMPLETED:
1835
+ return RerunProfileGenerationResponse(
1836
+ success=True,
1837
+ msg="Profile generation completed",
1838
+ profiles_generated=op.processed_users,
1839
+ )
1840
+ if op and op.status == OperationStatus.FAILED:
1841
+ return RerunProfileGenerationResponse(
1842
+ success=False,
1843
+ msg=op.error_message or "Profile generation failed",
1844
+ )
1845
+ return RerunProfileGenerationResponse(
1846
+ success=False,
1847
+ msg="Profile generation was cancelled",
1848
+ )
1849
+ except (TimeoutError, Exception) as e:
1850
+ logger.warning("Error while polling profile generation status: %s", e)
1851
+ return initial
1852
+
1853
+ async def _rerun_profile_generation_async(
1854
+ self, request: RerunProfileGenerationRequest
1855
+ ) -> RerunProfileGenerationResponse:
1856
+ """Internal async method to rerun profile generation."""
1857
+ response = await self._make_async_request(
1858
+ "POST",
1859
+ "/api/rerun_profile_generation",
1860
+ json=request.model_dump(),
1861
+ )
1862
+ return RerunProfileGenerationResponse(**response)
1863
+
1864
+ def rerun_profile_generation(
1865
+ self,
1866
+ request: RerunProfileGenerationRequest | dict | None = None,
1867
+ wait_for_response: bool = False,
1868
+ *,
1869
+ user_id: str | None = None,
1870
+ start_time: datetime | None = None,
1871
+ end_time: datetime | None = None,
1872
+ source: str | None = None,
1873
+ extractor_names: list[str] | None = None,
1874
+ ) -> RerunProfileGenerationResponse | None:
1875
+ """Rerun profile generation for users.
1876
+
1877
+ This method is optimized for resource efficiency:
1878
+ - In async contexts (e.g., FastAPI): Uses existing event loop (most efficient)
1879
+ - In sync contexts: Uses shared thread pool (avoids thread creation overhead)
1880
+
1881
+ Args:
1882
+ request (Optional[RerunProfileGenerationRequest]): The rerun request object (alternative to kwargs)
1883
+ wait_for_response (bool, optional): If True, wait for response. If False, send request without waiting. Defaults to False.
1884
+ user_id (Optional[str]): Specific user ID to rerun for. If None, runs for all users.
1885
+ start_time (Optional[datetime]): Filter interactions by start time.
1886
+ end_time (Optional[datetime]): Filter interactions by end time.
1887
+ source (Optional[str]): Filter interactions by source.
1888
+ extractor_names (Optional[list[str]]): Deprecated compatibility field. Accepted but ignored.
1889
+
1890
+ Returns:
1891
+ Optional[RerunProfileGenerationResponse]: Response containing success status, message, profiles_generated count, and operation_id if wait_for_response=True, None otherwise.
1892
+ """
1893
+ req = self._build_request(
1894
+ request,
1895
+ RerunProfileGenerationRequest,
1896
+ user_id=user_id,
1897
+ start_time=start_time,
1898
+ end_time=end_time,
1899
+ source=source,
1900
+ extractor_names=extractor_names,
1901
+ )
1902
+
1903
+ if wait_for_response:
1904
+ return self._rerun_profile_generation_sync(req)
1905
+ self._fire_and_forget(self._rerun_profile_generation_async, req)
1906
+ return None
1907
+
1908
+ def upgrade_profiles(
1909
+ self,
1910
+ *,
1911
+ user_id: str | None = None,
1912
+ only_affected_users: bool = True,
1913
+ ) -> UpgradeProfilesResponse:
1914
+ """Promote PENDING profiles to CURRENT, archive old CURRENT, delete old ARCHIVED.
1915
+
1916
+ Args:
1917
+ user_id: Specific user ID to upgrade. If None, upgrades all users.
1918
+ only_affected_users: If True, only upgrade users who have pending profiles.
1919
+
1920
+ Returns:
1921
+ UpgradeProfilesResponse: Counts of archived, promoted, and deleted profiles.
1922
+ """
1923
+ req = UpgradeProfilesRequest(
1924
+ user_id=user_id,
1925
+ only_affected_users=only_affected_users,
1926
+ )
1927
+ response = self._make_request(
1928
+ "POST",
1929
+ "/api/upgrade_all_profiles",
1930
+ json=req.model_dump(),
1931
+ )
1932
+ return UpgradeProfilesResponse(**response)
1933
+
1934
+ def upgrade_user_playbooks(
1935
+ self,
1936
+ *,
1937
+ agent_version: str | None = None,
1938
+ playbook_name: str | None = None,
1939
+ ) -> UpgradeUserPlaybooksResponse:
1940
+ """Promote PENDING user playbooks to CURRENT, archive old CURRENT, delete old ARCHIVED.
1941
+
1942
+ Args:
1943
+ agent_version: Filter by agent version. If None, upgrades all versions.
1944
+ playbook_name: Filter by playbook name. If None, upgrades all playbooks.
1945
+
1946
+ Returns:
1947
+ UpgradeUserPlaybooksResponse: Counts of archived, promoted, and deleted playbooks.
1948
+ """
1949
+ req = UpgradeUserPlaybooksRequest(
1950
+ agent_version=agent_version,
1951
+ playbook_name=playbook_name,
1952
+ )
1953
+ response = self._make_request(
1954
+ "POST",
1955
+ "/api/upgrade_all_user_playbooks",
1956
+ json=req.model_dump(),
1957
+ )
1958
+ return UpgradeUserPlaybooksResponse(**response)
1959
+
1960
+ async def _manual_profile_generation_async(
1961
+ self, request: ManualProfileGenerationRequest
1962
+ ) -> None:
1963
+ """Internal async method for manual profile generation."""
1964
+ await self._make_async_request(
1965
+ "POST",
1966
+ "/api/manual_profile_generation",
1967
+ json=request.model_dump(),
1968
+ )
1969
+
1970
+ def manual_profile_generation(
1971
+ self,
1972
+ request: ManualProfileGenerationRequest | dict | None = None,
1973
+ *,
1974
+ user_id: str | None = None,
1975
+ source: str | None = None,
1976
+ extractor_names: list[str] | None = None,
1977
+ ) -> None:
1978
+ """Manually trigger profile generation with window-sized interactions (fire-and-forget).
1979
+
1980
+ Unlike rerun_profile_generation which uses ALL interactions and outputs PENDING status,
1981
+ this method uses window-sized interactions (from window_size config) and
1982
+ outputs profiles with CURRENT status.
1983
+
1984
+ This is a fire-and-forget operation that runs asynchronously in the background.
1985
+
1986
+ Args:
1987
+ request (Optional[ManualProfileGenerationRequest]): The request object (alternative to kwargs)
1988
+ user_id (Optional[str]): Specific user ID to generate for. If None, generates for all users.
1989
+ source (Optional[str]): Filter interactions by source.
1990
+ extractor_names (Optional[list[str]]): Deprecated compatibility field. Accepted but ignored.
1991
+
1992
+ Returns:
1993
+ None: This method always returns None (fire-and-forget).
1994
+ """
1995
+ req = self._build_request(
1996
+ request,
1997
+ ManualProfileGenerationRequest,
1998
+ user_id=user_id,
1999
+ source=source,
2000
+ extractor_names=extractor_names,
2001
+ )
2002
+ self._fire_and_forget(self._manual_profile_generation_async, req)
2003
+
2004
+ def _rerun_playbook_generation_sync(
2005
+ self, request: RerunPlaybookGenerationRequest
2006
+ ) -> RerunPlaybookGenerationResponse:
2007
+ """Internal sync method to rerun playbook generation.
2008
+
2009
+ Submits the request, then polls operation status until completion.
2010
+ """
2011
+ submitted_at = int(time.time())
2012
+ response = self._make_request(
2013
+ "POST",
2014
+ "/api/rerun_playbook_generation",
2015
+ json=request.model_dump(),
2016
+ )
2017
+ initial = RerunPlaybookGenerationResponse(**response)
2018
+ if not initial.success:
2019
+ return initial
2020
+
2021
+ # Poll until the background task completes
2022
+ try:
2023
+ status_response = self._poll_operation_status(
2024
+ "playbook_generation", min_started_at=submitted_at
2025
+ )
2026
+ op = status_response.operation_status
2027
+ if op and op.status == OperationStatus.COMPLETED:
2028
+ return RerunPlaybookGenerationResponse(
2029
+ success=True,
2030
+ msg="Playbook generation completed",
2031
+ playbooks_generated=op.processed_users,
2032
+ )
2033
+ if op and op.status == OperationStatus.FAILED:
2034
+ return RerunPlaybookGenerationResponse(
2035
+ success=False,
2036
+ msg=op.error_message or "Playbook generation failed",
2037
+ )
2038
+ return RerunPlaybookGenerationResponse(
2039
+ success=False,
2040
+ msg="Playbook generation was cancelled",
2041
+ )
2042
+ except (TimeoutError, Exception) as e:
2043
+ logger.warning("Error while polling playbook generation status: %s", e)
2044
+ return initial
2045
+
2046
+ async def _rerun_playbook_generation_async(
2047
+ self, request: RerunPlaybookGenerationRequest
2048
+ ) -> RerunPlaybookGenerationResponse:
2049
+ """Internal async method to rerun playbook generation."""
2050
+ response = await self._make_async_request(
2051
+ "POST",
2052
+ "/api/rerun_playbook_generation",
2053
+ json=request.model_dump(),
2054
+ )
2055
+ return RerunPlaybookGenerationResponse(**response)
2056
+
2057
+ def rerun_playbook_generation(
2058
+ self,
2059
+ request: RerunPlaybookGenerationRequest | dict | None = None,
2060
+ wait_for_response: bool = False,
2061
+ *,
2062
+ agent_version: str | None = None,
2063
+ start_time: datetime | None = None,
2064
+ end_time: datetime | None = None,
2065
+ playbook_name: str | None = None,
2066
+ ) -> RerunPlaybookGenerationResponse | None:
2067
+ """Rerun playbook generation for an agent version.
2068
+
2069
+ This method is optimized for resource efficiency:
2070
+ - In async contexts (e.g., FastAPI): Uses existing event loop (most efficient)
2071
+ - In sync contexts: Uses shared thread pool (avoids thread creation overhead)
2072
+
2073
+ Args:
2074
+ request (Optional[RerunPlaybookGenerationRequest]): The rerun request object (alternative to kwargs)
2075
+ wait_for_response (bool, optional): If True, wait for response. If False, send request without waiting. Defaults to False.
2076
+ agent_version (str): Required. The agent version to evaluate.
2077
+ start_time (Optional[datetime]): Filter by start time.
2078
+ end_time (Optional[datetime]): Filter by end time.
2079
+ playbook_name (Optional[str]): Deprecated compatibility field. Accepted but ignored.
2080
+
2081
+ Returns:
2082
+ Optional[RerunPlaybookGenerationResponse]: Response containing success status, message, playbooks_generated count, and operation_id if wait_for_response=True, None otherwise.
2083
+ """
2084
+ req = self._build_request(
2085
+ request,
2086
+ RerunPlaybookGenerationRequest,
2087
+ agent_version=agent_version,
2088
+ start_time=start_time,
2089
+ end_time=end_time,
2090
+ playbook_name=playbook_name,
2091
+ )
2092
+
2093
+ if wait_for_response:
2094
+ return self._rerun_playbook_generation_sync(req)
2095
+ self._fire_and_forget(self._rerun_playbook_generation_async, req)
2096
+ return None
2097
+
2098
+ async def _manual_playbook_generation_async(
2099
+ self, request: ManualPlaybookGenerationRequest
2100
+ ) -> None:
2101
+ """Internal async method for manual playbook generation."""
2102
+ await self._make_async_request(
2103
+ "POST",
2104
+ "/api/manual_playbook_generation",
2105
+ json=request.model_dump(),
2106
+ )
2107
+
2108
+ def manual_playbook_generation(
2109
+ self,
2110
+ request: ManualPlaybookGenerationRequest | dict | None = None,
2111
+ *,
2112
+ agent_version: str | None = None,
2113
+ source: str | None = None,
2114
+ playbook_name: str | None = None,
2115
+ ) -> None:
2116
+ """Manually trigger playbook generation with window-sized interactions (fire-and-forget).
2117
+
2118
+ Unlike rerun_playbook_generation which uses ALL interactions and outputs PENDING status,
2119
+ this method uses window-sized interactions (from window_size config) and
2120
+ outputs playbooks with CURRENT status.
2121
+
2122
+ This is a fire-and-forget operation that runs asynchronously in the background.
2123
+
2124
+ Args:
2125
+ request (Optional[ManualPlaybookGenerationRequest]): The request object (alternative to kwargs)
2126
+ agent_version (str): Required. The agent version to evaluate.
2127
+ source (Optional[str]): Filter interactions by source.
2128
+ playbook_name (Optional[str]): Deprecated compatibility field. Accepted but ignored.
2129
+
2130
+ Returns:
2131
+ None: This method always returns None (fire-and-forget).
2132
+ """
2133
+ req = self._build_request(
2134
+ request,
2135
+ ManualPlaybookGenerationRequest,
2136
+ agent_version=agent_version,
2137
+ source=source,
2138
+ playbook_name=playbook_name,
2139
+ )
2140
+ self._fire_and_forget(self._manual_playbook_generation_async, req)
2141
+
2142
+ def _run_playbook_aggregation_sync(
2143
+ self, request: RunPlaybookAggregationRequest
2144
+ ) -> RunPlaybookAggregationResponse:
2145
+ """Internal sync method to run playbook aggregation."""
2146
+ response = self._make_request(
2147
+ "POST",
2148
+ "/api/run_playbook_aggregation",
2149
+ json=request.model_dump(),
2150
+ )
2151
+ return RunPlaybookAggregationResponse(**response)
2152
+
2153
+ async def _run_playbook_aggregation_async(
2154
+ self, request: RunPlaybookAggregationRequest
2155
+ ) -> RunPlaybookAggregationResponse:
2156
+ """Internal async method to run playbook aggregation."""
2157
+ response = await self._make_async_request(
2158
+ "POST",
2159
+ "/api/run_playbook_aggregation",
2160
+ json=request.model_dump(),
2161
+ )
2162
+ return RunPlaybookAggregationResponse(**response)
2163
+
2164
+ def run_playbook_aggregation(
2165
+ self,
2166
+ request: RunPlaybookAggregationRequest | dict | None = None,
2167
+ wait_for_response: bool = False,
2168
+ *,
2169
+ agent_version: str | None = None,
2170
+ playbook_name: str | None = None,
2171
+ ) -> RunPlaybookAggregationResponse | None:
2172
+ """Run playbook aggregation to cluster similar user playbooks.
2173
+
2174
+ This method is optimized for resource efficiency:
2175
+ - In async contexts (e.g., FastAPI): Uses existing event loop (most efficient)
2176
+ - In sync contexts: Uses shared thread pool (avoids thread creation overhead)
2177
+
2178
+ Args:
2179
+ request (Optional[RunPlaybookAggregationRequest]): The aggregation request object (alternative to kwargs)
2180
+ wait_for_response (bool, optional): If True, wait for response. If False, send request without waiting. Defaults to False.
2181
+ agent_version (str): Required. The agent version.
2182
+ playbook_name (Optional[str]): Deprecated compatibility field. Accepted but ignored.
2183
+
2184
+ Returns:
2185
+ Optional[RunPlaybookAggregationResponse]: Response containing success status and message if wait_for_response=True, None otherwise.
2186
+ """
2187
+ req = self._build_request(
2188
+ request,
2189
+ RunPlaybookAggregationRequest,
2190
+ agent_version=agent_version,
2191
+ playbook_name=playbook_name,
2192
+ )
2193
+
2194
+ if wait_for_response:
2195
+ return self._run_playbook_aggregation_sync(req)
2196
+ self._fire_and_forget(self._run_playbook_aggregation_async, req)
2197
+ return None
2198
+
2199
+ def search(
2200
+ self,
2201
+ request: UnifiedSearchRequest | dict | None = None,
2202
+ *,
2203
+ query: str | None = None,
2204
+ top_k: int | None = None,
2205
+ threshold: float | None = None,
2206
+ agent_version: str | None = None,
2207
+ playbook_name: str | None = None,
2208
+ user_id: str | None = None,
2209
+ entity_types: list[str] | None = None,
2210
+ agent_playbook_status_filter: list[PlaybookStatus | str] | None = None,
2211
+ enable_reformulation: bool | None = None,
2212
+ enable_agent_answer: bool | None = None,
2213
+ conversation_history: list[ConversationTurn] | list[dict] | None = None,
2214
+ search_mode: SearchMode | str | None = None,
2215
+ ) -> UnifiedSearchViewResponse:
2216
+ """Search across all entity types (profiles, agent playbooks, user playbooks).
2217
+
2218
+ Runs query reformulation and searches all entity types in parallel.
2219
+ Query reformulation is controlled per-request via the enable_reformulation parameter.
2220
+
2221
+ Args:
2222
+ request (Optional[UnifiedSearchRequest]): The search request object (alternative to kwargs)
2223
+ query (str): Search query text
2224
+ top_k (Optional[int]): Maximum results per entity type (default: 5)
2225
+ threshold (Optional[float]): Similarity threshold for vector search (default: 0.3)
2226
+ agent_version (Optional[str]): Filter by agent version (agent_playbooks, user_playbooks)
2227
+ playbook_name (Optional[str]): Filter by playbook name (agent_playbooks, user_playbooks)
2228
+ user_id (Optional[str]): Filter by user ID (profiles, user_playbooks)
2229
+ entity_types (Optional[list[str]]): Entity types to search. Valid values:
2230
+ "profiles", "user_playbooks", "agent_playbooks".
2231
+ agent_playbook_status_filter (Optional[list[Union[PlaybookStatus, str]]]):
2232
+ Agent-playbook approval statuses to include.
2233
+ enable_reformulation (Optional[bool]): Enable LLM query reformulation (default: False)
2234
+ enable_agent_answer (Optional[bool]): Enable agentic answer synthesis when
2235
+ the configured search backend supports it (default: False).
2236
+ conversation_history (Optional[list[ConversationTurn] | list[dict]]): Prior conversation turns for context-aware query reformulation. Accepts ConversationTurn objects or dicts with "role" and "content" keys.
2237
+ search_mode (Optional[SearchMode | str]): Search mode to use. Accepts SearchMode enum or string value ("vector", "fts", "hybrid").
2238
+
2239
+ Returns:
2240
+ UnifiedSearchViewResponse: Combined search results from all entity types
2241
+ """
2242
+ req = self._build_request(
2243
+ request,
2244
+ UnifiedSearchRequest,
2245
+ query=query,
2246
+ top_k=top_k,
2247
+ threshold=threshold,
2248
+ agent_version=agent_version,
2249
+ playbook_name=playbook_name,
2250
+ user_id=user_id,
2251
+ entity_types=entity_types,
2252
+ agent_playbook_status_filter=agent_playbook_status_filter,
2253
+ enable_reformulation=enable_reformulation,
2254
+ enable_agent_answer=enable_agent_answer,
2255
+ conversation_history=conversation_history,
2256
+ search_mode=search_mode,
2257
+ )
2258
+ response = self._make_request("POST", "/api/search", json=req.model_dump())
2259
+ return UnifiedSearchViewResponse(**response)
2260
+
2261
+ # =========================================================================
2262
+ # Bulk Delete Operations
2263
+ # =========================================================================
2264
+
2265
+ def delete_requests_by_ids(self, request_ids: list[str]) -> BulkDeleteResponse:
2266
+ """Delete multiple requests by their IDs.
2267
+
2268
+ Args:
2269
+ request_ids (list[str]): List of request IDs to delete
2270
+
2271
+ Returns:
2272
+ BulkDeleteResponse: Response containing success status and deleted count
2273
+ """
2274
+ req = DeleteRequestsByIdsRequest(request_ids=request_ids)
2275
+ response = self._make_request(
2276
+ "DELETE", "/api/delete_requests_by_ids", json=req.model_dump()
2277
+ )
2278
+ return BulkDeleteResponse(**response)
2279
+
2280
+ def delete_profiles_by_ids(self, profile_ids: list[str]) -> BulkDeleteResponse:
2281
+ """Delete multiple profiles by their IDs.
2282
+
2283
+ Args:
2284
+ profile_ids (list[str]): List of profile IDs to delete
2285
+
2286
+ Returns:
2287
+ BulkDeleteResponse: Response containing success status and deleted count
2288
+ """
2289
+ req = DeleteProfilesByIdsRequest(profile_ids=profile_ids)
2290
+ response = self._make_request(
2291
+ "DELETE", "/api/delete_profiles_by_ids", json=req.model_dump()
2292
+ )
2293
+ self._cache.invalidate("get_profiles")
2294
+ return BulkDeleteResponse(**response)
2295
+
2296
+ def delete_agent_playbooks_by_ids(
2297
+ self, agent_playbook_ids: list[int]
2298
+ ) -> BulkDeleteResponse:
2299
+ """Delete multiple agent playbooks by their IDs.
2300
+
2301
+ Args:
2302
+ agent_playbook_ids (list[int]): List of agent playbook IDs to delete
2303
+
2304
+ Returns:
2305
+ BulkDeleteResponse: Response containing success status and deleted count
2306
+ """
2307
+ req = DeleteAgentPlaybooksByIdsRequest(agent_playbook_ids=agent_playbook_ids)
2308
+ response = self._make_request(
2309
+ "DELETE", "/api/delete_agent_playbooks_by_ids", json=req.model_dump()
2310
+ )
2311
+ self._cache.invalidate("get_agent_playbooks")
2312
+ return BulkDeleteResponse(**response)
2313
+
2314
+ def delete_user_playbooks_by_ids(
2315
+ self, user_playbook_ids: list[int]
2316
+ ) -> BulkDeleteResponse:
2317
+ """Delete multiple user playbooks by their IDs.
2318
+
2319
+ Args:
2320
+ user_playbook_ids (list[int]): List of user playbook IDs to delete
2321
+
2322
+ Returns:
2323
+ BulkDeleteResponse: Response containing success status and deleted count
2324
+ """
2325
+ req = DeleteUserPlaybooksByIdsRequest(user_playbook_ids=user_playbook_ids)
2326
+ response = self._make_request(
2327
+ "DELETE", "/api/delete_user_playbooks_by_ids", json=req.model_dump()
2328
+ )
2329
+ return BulkDeleteResponse(**response)
2330
+
2331
+ def whoami(self) -> WhoamiResponse:
2332
+ """Return the server's view of the caller's org and storage routing.
2333
+
2334
+ Returns:
2335
+ WhoamiResponse: Masked summary of org ID and resolved storage.
2336
+ Never contains raw credentials — safe to print.
2337
+ """
2338
+ response = self._make_request("GET", "/api/whoami")
2339
+ return WhoamiResponse(**response)
2340
+
2341
+ def get_my_config(self) -> MyConfigResponse:
2342
+ """Return raw storage credentials for the caller's org.
2343
+
2344
+ Used by ``reflexio config storage`` to let users inspect their
2345
+ per-org server-side storage credentials.
2346
+
2347
+ Returns:
2348
+ MyConfigResponse: Unmasked storage config dict, or
2349
+ ``success=False`` when no storage is configured
2350
+ server-side or the endpoint is disabled.
2351
+ """
2352
+ response = self._make_request("GET", "/api/my_config")
2353
+ return MyConfigResponse(**response)
2354
+
2355
+ def delete_all_interactions(self) -> BulkDeleteResponse:
2356
+ """Delete all requests and their associated interactions.
2357
+
2358
+ Returns:
2359
+ BulkDeleteResponse: Response containing success status and deleted count
2360
+ """
2361
+ response = self._make_request("DELETE", "/api/delete_all_interactions")
2362
+ self._cache.clear()
2363
+ return BulkDeleteResponse(**response)
2364
+
2365
+ def delete_all_profiles(self) -> BulkDeleteResponse:
2366
+ """Delete all profiles.
2367
+
2368
+ Returns:
2369
+ BulkDeleteResponse: Response containing success status and deleted count
2370
+ """
2371
+ response = self._make_request("DELETE", "/api/delete_all_profiles")
2372
+ self._cache.invalidate("get_profiles")
2373
+ return BulkDeleteResponse(**response)
2374
+
2375
+ def delete_all_playbooks(self) -> BulkDeleteResponse:
2376
+ """Delete all playbooks (both user and agent).
2377
+
2378
+ Cascading variant — wipes both playbook stores. For per-entity
2379
+ semantics use :meth:`delete_all_user_playbooks` (user only) or
2380
+ :meth:`delete_all_agent_playbooks` (agent only).
2381
+
2382
+ Returns:
2383
+ BulkDeleteResponse: Response containing success status and deleted count
2384
+ """
2385
+ response = self._make_request("DELETE", "/api/delete_all_playbooks")
2386
+ self._cache.clear()
2387
+ return BulkDeleteResponse(**response)
2388
+
2389
+ def delete_all_user_playbooks(self) -> BulkDeleteResponse:
2390
+ """Delete all user playbooks (user only, not agent).
2391
+
2392
+ Returns:
2393
+ BulkDeleteResponse: Response containing success status and deleted count
2394
+ """
2395
+ response = self._make_request("DELETE", "/api/delete_all_user_playbooks")
2396
+ return BulkDeleteResponse(**response) # user playbooks not cached
2397
+
2398
+ def delete_all_agent_playbooks(self) -> BulkDeleteResponse:
2399
+ """Delete all agent playbooks (agent only, not user).
2400
+
2401
+ Returns:
2402
+ BulkDeleteResponse: Response containing success status and deleted count
2403
+ """
2404
+ response = self._make_request("DELETE", "/api/delete_all_agent_playbooks")
2405
+ self._cache.invalidate("get_agent_playbooks")
2406
+ return BulkDeleteResponse(**response)
2407
+
2408
+ def get_stall_state(self) -> StallStateResponse:
2409
+ """Read the current learning-stall state from the server.
2410
+
2411
+ Returns:
2412
+ StallStateResponse: ``stalled=False`` when clean.
2413
+ """
2414
+ response = self._make_request("GET", "/stall_state")
2415
+ return StallStateResponse.model_validate(response)
2416
+
2417
+ def mark_stall_notified(self) -> MarkNotifiedResponse:
2418
+ """Idempotently flip ``notified_in_cc=1`` on the current stall row.
2419
+
2420
+ Returns:
2421
+ MarkNotifiedResponse: New ``notified_in_cc`` value.
2422
+ """
2423
+ response = self._make_request("POST", "/stall_state/notified")
2424
+ return MarkNotifiedResponse.model_validate(response)
2425
+
2426
+ # ===============================
2427
+ # Pending tool calls (human questions)
2428
+ # ===============================
2429
+ # The resumable extraction agent can pause and emit an ``ask_human``
2430
+ # pending tool call when it needs a human to answer a question before it
2431
+ # can finish. These methods let a user list those questions, answer them
2432
+ # to unblock the agent, edit a prior answer, mark a question N/A, or
2433
+ # cancel it. See notebooks/08_resumable_agent_human_questions.ipynb.
2434
+
2435
+ def list_pending_tool_calls(
2436
+ self, status: str | None = None, limit: int = 100
2437
+ ) -> PendingToolCallListResponse:
2438
+ """List pending tool calls (e.g. ``ask_human`` questions) for the org.
2439
+
2440
+ Args:
2441
+ status (str | None): Optional status filter. One of ``"pending"``,
2442
+ ``"resolved"``, ``"expired"``, ``"superseded"``, or
2443
+ ``"cancelled"``. When None, all statuses are returned.
2444
+ limit (int): Maximum number of results to return (1-500, default 100).
2445
+
2446
+ Returns:
2447
+ PendingToolCallListResponse: The matching pending tool calls.
2448
+ """
2449
+ params: dict[str, Any] = {"limit": limit}
2450
+ if status is not None:
2451
+ params["status"] = status
2452
+ response = self._make_request("GET", "/api/pending_tool_calls", params=params)
2453
+ return PendingToolCallListResponse.model_validate(response)
2454
+
2455
+ def get_pending_tool_call(
2456
+ self, pending_tool_call_id: str
2457
+ ) -> PendingToolCallResponse:
2458
+ """Fetch a single pending tool call by id.
2459
+
2460
+ Args:
2461
+ pending_tool_call_id (str): The id of the pending tool call.
2462
+
2463
+ Returns:
2464
+ PendingToolCallResponse: The pending tool call.
2465
+ """
2466
+ response = self._make_request(
2467
+ "GET", f"/api/pending_tool_calls/{pending_tool_call_id}"
2468
+ )
2469
+ return PendingToolCallResponse.model_validate(response)
2470
+
2471
+ def resolve_pending_tool_call(
2472
+ self,
2473
+ pending_tool_call_id: str,
2474
+ *,
2475
+ result: dict[str, Any] | None = None,
2476
+ answer: str | None = None,
2477
+ valid_for_seconds: int | None = None,
2478
+ ) -> PendingToolCallResponse:
2479
+ """Resolve a pending tool call, unblocking dependent agent runs.
2480
+
2481
+ Provide exactly one of ``result`` or ``answer``. ``answer`` is a
2482
+ convenience for the common ``ask_human`` case and is wrapped as
2483
+ ``{"answer": answer}``; use ``result`` to send an arbitrary tool
2484
+ result payload.
2485
+
2486
+ Args:
2487
+ pending_tool_call_id (str): The id of the pending tool call.
2488
+ result (dict[str, Any] | None): Raw tool result payload.
2489
+ answer (str | None): Human answer text (wrapped as ``{"answer": ...}``).
2490
+ valid_for_seconds (int | None): How long the resolved answer
2491
+ stays valid for answer reuse. Server default when None.
2492
+
2493
+ Returns:
2494
+ PendingToolCallResponse: The resolved pending tool call.
2495
+
2496
+ Raises:
2497
+ ValueError: If both or neither of ``result`` and ``answer`` are set.
2498
+ """
2499
+ if (result is None) == (answer is None):
2500
+ raise ValueError("Provide exactly one of 'result' or 'answer' to resolve.")
2501
+ payload: dict[str, Any] = {
2502
+ "result": result if result is not None else {"answer": answer}
2503
+ }
2504
+ if valid_for_seconds is not None:
2505
+ payload["valid_for_seconds"] = valid_for_seconds
2506
+ response = self._make_request(
2507
+ "POST",
2508
+ f"/api/pending_tool_calls/{pending_tool_call_id}/resolve",
2509
+ json=payload,
2510
+ )
2511
+ return PendingToolCallResponse.model_validate(response)
2512
+
2513
+ def update_pending_tool_call_answer(
2514
+ self,
2515
+ pending_tool_call_id: str,
2516
+ answer: str,
2517
+ *,
2518
+ valid_for_seconds: int | None = None,
2519
+ ) -> PendingToolCallResponse:
2520
+ """Edit the answer of an already-resolved ``ask_human`` question.
2521
+
2522
+ Editing schedules another resume of dependent agent runs with the new
2523
+ answer.
2524
+
2525
+ Args:
2526
+ pending_tool_call_id (str): The id of the pending tool call.
2527
+ answer (str): The new (non-empty) answer text.
2528
+ valid_for_seconds (int | None): How long the edited answer stays
2529
+ valid for answer reuse. Server default when None.
2530
+
2531
+ Returns:
2532
+ PendingToolCallResponse: The updated pending tool call.
2533
+ """
2534
+ payload: dict[str, Any] = {"answer": answer}
2535
+ if valid_for_seconds is not None:
2536
+ payload["valid_for_seconds"] = valid_for_seconds
2537
+ response = self._make_request(
2538
+ "PATCH",
2539
+ f"/api/pending_tool_calls/{pending_tool_call_id}/answer",
2540
+ json=payload,
2541
+ )
2542
+ return PendingToolCallResponse.model_validate(response)
2543
+
2544
+ def mark_pending_tool_call_not_applicable(
2545
+ self,
2546
+ pending_tool_call_id: str,
2547
+ *,
2548
+ valid_for_seconds: int | None = None,
2549
+ ) -> PendingToolCallResponse:
2550
+ """Mark an ``ask_human`` question as not applicable.
2551
+
2552
+ Resolves the question with the standard "user does not have
2553
+ information" result so dependent agent runs can proceed.
2554
+
2555
+ Args:
2556
+ pending_tool_call_id (str): The id of the pending tool call.
2557
+ valid_for_seconds (int | None): How long the result stays valid
2558
+ for answer reuse. Server default when None.
2559
+
2560
+ Returns:
2561
+ PendingToolCallResponse: The resolved (N/A) pending tool call.
2562
+ """
2563
+ payload: dict[str, Any] = {}
2564
+ if valid_for_seconds is not None:
2565
+ payload["valid_for_seconds"] = valid_for_seconds
2566
+ response = self._make_request(
2567
+ "POST",
2568
+ f"/api/pending_tool_calls/{pending_tool_call_id}/not_applicable",
2569
+ json=payload,
2570
+ )
2571
+ return PendingToolCallResponse.model_validate(response)
2572
+
2573
+ def cancel_pending_tool_call(
2574
+ self, pending_tool_call_id: str
2575
+ ) -> PendingToolCallResponse:
2576
+ """Cancel a pending tool call without answering it.
2577
+
2578
+ Only pending (unanswered) tool calls can be cancelled.
2579
+
2580
+ Args:
2581
+ pending_tool_call_id (str): The id of the pending tool call.
2582
+
2583
+ Returns:
2584
+ PendingToolCallResponse: The cancelled pending tool call.
2585
+ """
2586
+ response = self._make_request(
2587
+ "POST", f"/api/pending_tool_calls/{pending_tool_call_id}/cancel"
2588
+ )
2589
+ return PendingToolCallResponse.model_validate(response)
2590
+
2591
+ def clear_user_data(self, user_id: str) -> ClearUserDataResponse:
2592
+ """Delete all rows scoped to a single ``user_id``.
2593
+
2594
+ Wipes the user's interactions, user playbooks, profiles, and
2595
+ requests on the server. Does NOT touch agent playbooks — they
2596
+ are intentionally shared cross-project. Used by paired-protocol
2597
+ harnesses (e.g. SWE-bench) to isolate per-task data on a shared
2598
+ backend without one task's clear-all nuking another in-flight
2599
+ task's rows.
2600
+
2601
+ Args:
2602
+ user_id (str): The user id whose data should be cleared.
2603
+
2604
+ Returns:
2605
+ ClearUserDataResponse: Per-entity deletion counts.
2606
+ """
2607
+ req = ClearUserDataRequest(user_id=user_id)
2608
+ response = self._make_request(
2609
+ "POST", "/api/clear_user_data", json=req.model_dump()
2610
+ )
2611
+ # Nuclear — clear everything that could reference this user.
2612
+ self._cache.clear()
2613
+ return ClearUserDataResponse(**response)