claude-smart 0.2.42 → 0.2.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +1 -1
  3. package/bin/claude-smart.js +2 -2
  4. package/package.json +9 -3
  5. package/plugin/.claude-plugin/plugin.json +9 -3
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +23 -3
  8. package/plugin/pyproject.toml +3 -3
  9. package/plugin/scripts/_lib.sh +91 -0
  10. package/plugin/scripts/backend-service.sh +51 -4
  11. package/plugin/scripts/cli.sh +3 -1
  12. package/plugin/scripts/codex-hook.js +72 -4
  13. package/plugin/scripts/dashboard-build.sh +1 -0
  14. package/plugin/scripts/dashboard-service.sh +1 -0
  15. package/plugin/scripts/ensure-plugin-root.sh +1 -0
  16. package/plugin/scripts/hook_entry.sh +6 -3
  17. package/plugin/scripts/smart-install.sh +3 -2
  18. package/plugin/src/README.md +57 -0
  19. package/plugin/src/claude_smart/context_format.py +11 -12
  20. package/plugin/src/claude_smart/cs_cite.py +26 -12
  21. package/plugin/src/claude_smart/ids.py +13 -5
  22. package/plugin/uv.lock +126 -5
  23. package/plugin/vendor/reflexio/.env.example +62 -0
  24. package/plugin/vendor/reflexio/LICENSE +201 -0
  25. package/plugin/vendor/reflexio/README.md +338 -0
  26. package/plugin/vendor/reflexio/pyproject.toml +274 -0
  27. package/plugin/vendor/reflexio/reflexio/README.md +184 -0
  28. package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
  29. package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
  30. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
  31. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
  32. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
  33. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
  34. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
  35. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
  36. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
  37. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
  38. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
  39. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
  40. package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
  41. package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
  42. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
  43. package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
  44. package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
  45. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +265 -0
  46. package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
  47. package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
  48. package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
  49. package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
  50. package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
  51. package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
  52. package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
  53. package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
  54. package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +964 -0
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
  62. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
  63. package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
  64. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
  65. package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
  66. package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
  67. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
  68. package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
  69. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
  70. package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
  71. package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
  72. package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
  73. package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
  74. package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
  75. package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
  145. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
  146. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
  147. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
  148. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
  149. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
  150. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
  151. package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
  152. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
  153. package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
  154. package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
  155. package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
  156. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
  157. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
  158. package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
  159. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
  160. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
  161. package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
  162. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
  163. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
  164. package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
  165. package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
  166. package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
  167. package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
  168. package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
  169. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
  170. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1112 -0
  171. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
  172. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
  173. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
  174. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
  175. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +768 -0
  176. package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
  177. package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
  178. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
  179. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
  180. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
  181. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
  182. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +333 -0
  183. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
  184. package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
  185. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
  186. package/plugin/vendor/reflexio/reflexio/server/README.md +622 -0
  187. package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
  188. package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
  189. package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
  190. package/plugin/vendor/reflexio/reflexio/server/api.py +2868 -0
  191. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +34 -0
  192. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
  193. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
  194. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
  195. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
  196. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +562 -0
  197. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
  198. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
  199. package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
  200. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
  201. package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
  202. package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
  203. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +359 -0
  204. package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
  205. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1871 -0
  206. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +140 -0
  207. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +479 -0
  208. package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
  209. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
  210. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
  211. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +338 -0
  212. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
  213. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +288 -0
  214. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
  215. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +187 -0
  216. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
  217. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +716 -0
  218. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
  219. package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
  220. package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
  221. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
  222. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
  223. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
  224. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
  225. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
  226. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
  227. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
  228. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
  229. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
  230. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
  231. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
  232. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
  233. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
  234. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
  235. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
  236. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
  237. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
  238. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
  239. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
  240. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
  241. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
  242. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
  243. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
  244. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md +69 -0
  245. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +71 -0
  246. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
  247. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
  248. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +283 -0
  249. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md +234 -0
  250. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +244 -0
  251. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
  252. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
  253. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
  254. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
  255. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
  256. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +137 -0
  257. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
  258. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
  259. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
  260. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
  261. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
  262. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
  263. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
  264. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
  265. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
  266. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
  267. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
  268. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
  269. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
  270. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
  271. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
  272. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
  273. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
  274. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
  275. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
  276. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
  277. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
  278. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
  279. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
  280. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
  281. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
  282. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
  283. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/README.md +58 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
  289. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
  290. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +336 -0
  292. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1668 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
  296. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
  297. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/embedding_text.py +62 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
  311. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
  312. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
  313. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
  318. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +358 -0
  319. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
  320. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
  321. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +535 -0
  322. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
  323. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +779 -0
  324. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
  325. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +94 -0
  326. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
  327. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +702 -0
  328. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
  329. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
  330. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
  331. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +1045 -0
  332. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
  333. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
  334. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
  335. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
  336. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
  337. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
  338. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
  339. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
  340. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
  341. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
  342. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
  343. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
  344. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
  345. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
  346. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
  347. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
  348. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +772 -0
  349. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
  350. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +737 -0
  351. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
  352. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
  353. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
  354. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +803 -0
  355. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
  356. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
  357. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +80 -0
  358. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
  359. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +756 -0
  360. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
  361. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
  362. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
  363. package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
  364. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
  365. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
  366. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
  367. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
  368. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1298 -0
  369. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
  370. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
  371. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
  372. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
  373. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
  374. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
  375. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
  376. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
  377. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
  378. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
  379. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +384 -0
  380. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
  381. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
  382. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
  383. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
  384. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
  385. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
  386. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
  387. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
  388. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
  389. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +572 -0
  390. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
  391. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
  392. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
  393. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
  394. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
  395. package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
  396. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
  397. package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
  398. package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
  399. package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
  400. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
  401. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
  402. package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
@@ -0,0 +1,756 @@
1
+ """
2
+ Utils for service layer
3
+ """
4
+
5
+ import ast
6
+ import json
7
+ import logging
8
+ import os
9
+ import re
10
+ from dataclasses import dataclass
11
+ from datetime import UTC, datetime
12
+ from typing import Any
13
+
14
+ import tiktoken
15
+
16
+ from reflexio.cli.log_format import LLM_IO_LOG_FILE, next_llm_entry_id
17
+ from reflexio.models.api_schema.internal_schema import RequestInteractionDataModel
18
+ from reflexio.models.api_schema.service_schemas import (
19
+ Interaction,
20
+ UserActionType,
21
+ )
22
+ from reflexio.server import LLM_PROMPT_LEVEL
23
+ from reflexio.server.prompt.prompt_manager import PromptManager
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Custom log level for model responses (between INFO=20 and WARNING=30)
28
+ # Already registered in server/__init__.py; import the numeric constant only.
29
+ MODEL_RESPONSE_LEVEL = 25
30
+
31
+ # Per-interaction content token budget when formatting interactions into an
32
+ # extraction/evaluation prompt. Read from REFLEXIO_MAX_INTERACTION_CONTENT_TOKENS;
33
+ # a value <= 0 disables slicing. See _resolve_max_interaction_content_tokens.
34
+ DEFAULT_MAX_INTERACTION_CONTENT_TOKENS = 512
35
+ _MAX_INTERACTION_CONTENT_TOKENS_ENV = "REFLEXIO_MAX_INTERACTION_CONTENT_TOKENS"
36
+ # Inserted between the head and tail of a sliced interaction so the LLM can see
37
+ # that the middle of the content was elided.
38
+ _CONTENT_TRUNCATION_MARKER = " …[truncated] "
39
+ # Encoder used purely as a length heuristic for slicing. tiktoken does not know
40
+ # the real tokenizer for non-OpenAI providers, but cl100k_base is a consistent,
41
+ # good-enough proxy for budgeting prompt content (mirrors the embedding path).
42
+ _CONTENT_ENCODING_NAME = "cl100k_base"
43
+ _content_token_encoding: tiktoken.Encoding | None = None
44
+ # Guards against logging the same malformed env value on every call.
45
+ _INVALID_MAX_TOKENS_WARNED = False
46
+
47
+
48
+ def _get_content_token_encoding() -> tiktoken.Encoding:
49
+ """Return a process-cached tiktoken encoder for content slicing."""
50
+ global _content_token_encoding
51
+ if _content_token_encoding is None:
52
+ _content_token_encoding = tiktoken.get_encoding(_CONTENT_ENCODING_NAME)
53
+ return _content_token_encoding
54
+
55
+
56
+ def _resolve_max_interaction_content_tokens() -> int | None:
57
+ """Resolve the per-interaction content token budget from the environment.
58
+
59
+ Returns:
60
+ int | None: The positive token budget, or None when slicing is
61
+ disabled (env value <= 0). Unset or malformed values fall back to
62
+ ``DEFAULT_MAX_INTERACTION_CONTENT_TOKENS``.
63
+ """
64
+ global _INVALID_MAX_TOKENS_WARNED
65
+ raw = os.getenv(_MAX_INTERACTION_CONTENT_TOKENS_ENV, "").strip()
66
+ if not raw:
67
+ return DEFAULT_MAX_INTERACTION_CONTENT_TOKENS
68
+ try:
69
+ value = int(raw)
70
+ except ValueError:
71
+ if not _INVALID_MAX_TOKENS_WARNED:
72
+ logger.warning(
73
+ "Invalid %s=%r; falling back to default %d",
74
+ _MAX_INTERACTION_CONTENT_TOKENS_ENV,
75
+ raw,
76
+ DEFAULT_MAX_INTERACTION_CONTENT_TOKENS,
77
+ )
78
+ _INVALID_MAX_TOKENS_WARNED = True
79
+ return DEFAULT_MAX_INTERACTION_CONTENT_TOKENS
80
+ return value if value > 0 else None
81
+
82
+
83
+ def slice_content_by_tokens(content: str, max_tokens: int | None) -> str:
84
+ """Slice interaction content to a token budget, keeping head and tail.
85
+
86
+ When the content exceeds ``max_tokens`` tokens, keep the first ``max_tokens
87
+ // 2`` tokens and the last ``max_tokens - max_tokens // 2`` tokens, joined by
88
+ a truncation marker so the elision is visible to the LLM. Content within
89
+ budget (or when ``max_tokens`` is None / content is empty) is returned
90
+ unchanged.
91
+
92
+ Args:
93
+ content (str): The interaction content to slice.
94
+ max_tokens (int | None): Token budget, or None to disable slicing.
95
+
96
+ Returns:
97
+ str: The original or head+marker+tail-sliced content.
98
+ """
99
+ if max_tokens is None or not content:
100
+ return content
101
+ encoding = _get_content_token_encoding()
102
+ tokens = encoding.encode(content)
103
+ if len(tokens) <= max_tokens:
104
+ return content
105
+ head = max_tokens // 2
106
+ tail = max_tokens - head
107
+ return (
108
+ f"{encoding.decode(tokens[:head])}"
109
+ f"{_CONTENT_TRUNCATION_MARKER}"
110
+ f"{encoding.decode(tokens[-tail:])}"
111
+ )
112
+
113
+
114
+ def _format_response_for_logging(response: Any) -> Any:
115
+ """Render ``ToolCallingChatResponse`` with pretty tool_calls; pass others through.
116
+
117
+ The dataclass's ``__repr__`` (which ``%s`` formatting falls back to)
118
+ prints each tool_call as an opaque object handle
119
+ (``<ChatCompletionMessageToolCall object at 0x…>``), erasing the
120
+ tool name + arguments the model emitted. This helper detects that
121
+ one case and renders a multi-line human-readable form using the
122
+ same ``_format_tool_calls`` helper the request-side formatter uses.
123
+
124
+ All other response types (strings, Pydantic ``BaseModel`` instances
125
+ from classic extractors / deduplicators / aggregators) fall through
126
+ unchanged so the existing log shape is preserved.
127
+
128
+ Lazy-imports ``ToolCallingChatResponse`` to avoid a circular
129
+ ``service_utils`` ↔ ``litellm_client`` dependency at module load.
130
+ """
131
+ try:
132
+ from reflexio.server.llm.litellm_client import ToolCallingChatResponse
133
+ except Exception: # noqa: BLE001 - fall back gracefully if the import fails
134
+ return response
135
+
136
+ if not isinstance(response, ToolCallingChatResponse):
137
+ return response
138
+
139
+ lines = [
140
+ f"ToolCallingChatResponse(finish_reason={response.finish_reason!r}):",
141
+ f" content: {response.content!r}",
142
+ ]
143
+ if response.tool_calls:
144
+ lines.extend(_format_tool_calls(response.tool_calls))
145
+ else:
146
+ lines.append(" tool_calls: []")
147
+ return "\n".join(lines)
148
+
149
+
150
+ def log_model_response(
151
+ target_logger: logging.Logger, label: str, response: Any
152
+ ) -> None:
153
+ """
154
+ Log an LLM model response. Full response goes to the log file at LLM_PROMPT level.
155
+ A one-line summary goes to the console at MODEL_RESPONSE level.
156
+
157
+ Args:
158
+ target_logger (logging.Logger): The logger instance to use
159
+ label (str): Descriptive label for the response (e.g. "Profile updates model response")
160
+ response (Any): The model response to log
161
+ """
162
+ entry_id = next_llm_entry_id()
163
+ # Special-case ToolCallingChatResponse so tool_calls render as
164
+ # id/name/arguments instead of opaque ``<… object at 0x…>`` handles.
165
+ formatted = _format_response_for_logging(response)
166
+ # Full response to llm_io.log only (level 15 < INFO 20, so console ignores it)
167
+ target_logger.log(
168
+ LLM_PROMPT_LEVEL,
169
+ "[#%d] %s: %s",
170
+ entry_id,
171
+ label,
172
+ formatted,
173
+ extra={"entry_id": entry_id, "label": label},
174
+ )
175
+ # One-line summary to console
176
+ response_type = type(response).__name__
177
+ target_logger.log(
178
+ MODEL_RESPONSE_LEVEL,
179
+ "[MODEL] %s: %s — %s [#%d]",
180
+ label,
181
+ response_type,
182
+ LLM_IO_LOG_FILE,
183
+ entry_id,
184
+ )
185
+
186
+
187
+ def log_llm_messages(
188
+ target_logger: logging.Logger,
189
+ label: str,
190
+ messages: list[dict[str, Any]],
191
+ ) -> None:
192
+ """
193
+ Log LLM prompt messages. Full content goes to the log file at LLM_PROMPT level.
194
+ A one-line summary goes to the console at INFO level.
195
+
196
+ Args:
197
+ target_logger (logging.Logger): The logger instance to use
198
+ label (str): Descriptive label (e.g. "Profile extraction")
199
+ messages (list[dict[str, Any]]): The LLM messages to log
200
+ """
201
+ entry_id = next_llm_entry_id()
202
+ # Full messages to llm_io.log only
203
+ formatted = format_messages_for_logging(messages)
204
+ target_logger.log(
205
+ LLM_PROMPT_LEVEL,
206
+ "%s messages:\n%s",
207
+ label,
208
+ formatted,
209
+ extra={"entry_id": entry_id, "label": label},
210
+ )
211
+ # Summary to console
212
+ total_chars = sum(len(str(msg.get("content", ""))) for msg in messages)
213
+ target_logger.info(
214
+ "[LLM] %s: %d msgs, ~%d chars — %s [#%d]",
215
+ label,
216
+ len(messages),
217
+ total_chars,
218
+ LLM_IO_LOG_FILE,
219
+ entry_id,
220
+ )
221
+
222
+
223
+ @dataclass
224
+ class PromptConfig:
225
+ """Configuration for a prompt to be rendered.
226
+
227
+ Attributes:
228
+ prompt_id: The ID of the prompt template to render
229
+ variables: Dictionary of variables to pass to the prompt template
230
+ """
231
+
232
+ prompt_id: str
233
+ variables: dict[str, any] # type: ignore[reportGeneralTypeIssues]
234
+
235
+
236
+ @dataclass
237
+ class MessageConstructionConfig:
238
+ """Configuration for constructing LLM messages from interactions.
239
+
240
+ Attributes:
241
+ prompt_manager: The prompt manager to use for rendering prompts
242
+ system_prompt_config: Configuration for the system message (before interactions)
243
+ user_prompt_config: Configuration for the user message (after interactions)
244
+ """
245
+
246
+ prompt_manager: PromptManager
247
+ system_prompt_config: PromptConfig | None = None
248
+ user_prompt_config: PromptConfig | None = None
249
+
250
+
251
+ def format_interactions_to_history_string(interactions: list[Interaction]) -> str:
252
+ """
253
+ Format a list of interactions into a single string representing the interaction history.
254
+
255
+ Each interaction is formatted as:
256
+ - Text content: "{role}: {content}"
257
+ - User actions: "{role}: {action} {action_description}"
258
+
259
+ Args:
260
+ interactions (list[Interaction]): List of interactions to format
261
+
262
+ Returns:
263
+ str: A formatted string representing the interaction history, with interactions separated by newlines.
264
+ Returns empty string if no interactions are provided.
265
+
266
+ Example:
267
+ >>> interactions = [
268
+ ... Interaction(role="user", content="I love sushi", user_action=UserActionType.NONE),
269
+ ... Interaction(role="user", content="", user_action=UserActionType.CLICK, user_action_description="menu item")
270
+ ... ]
271
+ >>> result = format_interactions_to_history_string(interactions)
272
+ >>> print(result)
273
+ user: I love sushi
274
+ user: click menu item
275
+ """
276
+ max_content_tokens = _resolve_max_interaction_content_tokens()
277
+ formatted_interactions = []
278
+ for interaction in interactions:
279
+ # Add text content with tools_used prefix if present
280
+ if interaction.content:
281
+ content = slice_content_by_tokens(interaction.content, max_content_tokens)
282
+ if interaction.tools_used:
283
+ tool_prefix = " ".join(
284
+ f"[used tool: {t.tool_name}({json.dumps(t.tool_data)})]"
285
+ for t in interaction.tools_used
286
+ )
287
+ formatted_interactions.append(
288
+ f"{interaction.role}: ```{tool_prefix} {content}```"
289
+ )
290
+ else:
291
+ formatted_interactions.append(f"{interaction.role}: ```{content}```")
292
+
293
+ # Add user action
294
+ if interaction.user_action != UserActionType.NONE:
295
+ formatted_interactions.append(
296
+ f"{interaction.role}: ```{interaction.user_action.value} {interaction.user_action_description}```"
297
+ )
298
+
299
+ return "\n".join(formatted_interactions)
300
+
301
+
302
+ def format_sessions_to_history_string(
303
+ sessions: list[RequestInteractionDataModel],
304
+ ) -> str:
305
+ """
306
+ Format interactions grouped by session into a string.
307
+
308
+ All RequestInteractionDataModel objects with the same session_id are consolidated
309
+ under a single header. Within each consolidated group, interactions are ordered by
310
+ their id in ascending order (smaller to bigger).
311
+
312
+ Args:
313
+ sessions (list[RequestInteractionDataModel]): List of request interaction data models to format
314
+
315
+ Returns:
316
+ str: A formatted string with interactions grouped by session.
317
+ Returns empty string if no sessions are provided.
318
+
319
+ Example:
320
+ >>> # Given sessions with interactions (multiple requests in same session)
321
+ >>> result = format_sessions_to_history_string(sessions)
322
+ >>> print(result)
323
+ === Session: session_1 ===
324
+ user: Hello, I need help
325
+ assistant: How can I assist you?
326
+ user: Thanks for the help
327
+ assistant: You're welcome!
328
+
329
+ === Session: session_2 ===
330
+ user: I love sushi
331
+ assistant: That's great!
332
+ """
333
+ if not sessions:
334
+ return ""
335
+
336
+ # Group all RequestInteractionDataModel objects by their session_id
337
+ grouped_by_name: dict[str, list[RequestInteractionDataModel]] = {}
338
+ for request_interaction in sessions:
339
+ if request_interaction.session_id not in grouped_by_name:
340
+ grouped_by_name[request_interaction.session_id] = []
341
+ grouped_by_name[request_interaction.session_id].append(request_interaction)
342
+
343
+ # Sort each group's requests by created_at timestamp
344
+ for group_name in grouped_by_name:
345
+ grouped_by_name[group_name] = sorted(
346
+ grouped_by_name[group_name], key=lambda g: g.request.created_at
347
+ )
348
+
349
+ # Sort group names by the earliest request timestamp in each group
350
+ sorted_group_names = sorted(
351
+ grouped_by_name.keys(),
352
+ key=lambda name: grouped_by_name[name][0].request.created_at,
353
+ )
354
+
355
+ formatted_groups = []
356
+ for group_name in sorted_group_names:
357
+ # Format header with session name AND its earliest interaction date.
358
+ # Without the date, downstream extraction agents have no anchor for
359
+ # resolving relative-time references in the conversation
360
+ # ("X weeks ago", "yesterday", "two days before the wedding") —
361
+ # they fall back to real-world `now()` and encode every event as
362
+ # today's date, breaking temporal-reasoning queries.
363
+ #
364
+ # We use the earliest *interaction* timestamp, not request.created_at,
365
+ # because Request.created_at defaults to `now()` on construction —
366
+ # only interactions reliably carry the conversation's true wall-clock
367
+ # time when the publisher provides it.
368
+ all_ts: list[int] = [
369
+ i.created_at
370
+ for ri in grouped_by_name[group_name]
371
+ for i in ri.interactions
372
+ if i.created_at
373
+ ]
374
+ first_ts = min(all_ts) if all_ts else 0
375
+ if first_ts:
376
+ try:
377
+ session_date_iso = datetime.fromtimestamp(first_ts, tz=UTC).strftime(
378
+ "%Y-%m-%d"
379
+ )
380
+ group_header = (
381
+ f"=== Session: {group_name} (date: {session_date_iso}) ==="
382
+ )
383
+ except (OverflowError, OSError, ValueError):
384
+ group_header = f"=== Session: {group_name} ==="
385
+ else:
386
+ group_header = f"=== Session: {group_name} ==="
387
+
388
+ # Combine all interactions from all requests in this session
389
+ all_interactions = []
390
+ for request_interaction in grouped_by_name[group_name]:
391
+ all_interactions.extend(request_interaction.interactions)
392
+
393
+ # Sort interactions by id ascending (smaller to bigger)
394
+ all_interactions = sorted(all_interactions, key=lambda i: i.interaction_id)
395
+
396
+ # Format combined interactions
397
+ group_interactions = format_interactions_to_history_string(all_interactions)
398
+
399
+ # Combine header and interactions
400
+ formatted_groups.append(f"{group_header}\n{group_interactions}")
401
+
402
+ return "\n\n".join(formatted_groups)
403
+
404
+
405
+ def extract_interactions_from_request_interaction_data_models(
406
+ request_interaction_data_models: list[RequestInteractionDataModel],
407
+ ) -> list[Interaction]:
408
+ """
409
+ Extract a flat list of interactions from request interaction groups.
410
+
411
+ This is useful for backward compatibility with services that still expect
412
+ a flat list of interactions.
413
+
414
+ Args:
415
+ request_interaction_data_models (list[RequestInteractionDataModel]): List of request interaction data models
416
+
417
+ Returns:
418
+ list[Interaction]: Flat list of all interactions from all groups
419
+ """
420
+ interactions = []
421
+ for request_interaction_data_model in request_interaction_data_models:
422
+ interactions.extend(request_interaction_data_model.interactions)
423
+ return interactions
424
+
425
+
426
+ def construct_messages_from_interactions(
427
+ interactions: list[Interaction],
428
+ config: MessageConstructionConfig,
429
+ ) -> list[dict]:
430
+ """
431
+ Construct a list of LLM messages from interactions with custom prompts.
432
+
433
+ This function creates a structured message sequence:
434
+ 1. Optional system message (using system_prompt_config)
435
+ 2. Single user message containing:
436
+ - Formatted interactions (text content and user actions)
437
+ - User prompt (if configured)
438
+ - Last interaction's image (if present)
439
+
440
+ Args:
441
+ interactions: List of interactions to convert into messages
442
+ config: Configuration for message construction including prompt configs
443
+
444
+ Returns:
445
+ list[dict]: List of messages ready to be sent to the LLM API.
446
+ Each message is a dict with 'role' and 'content' keys.
447
+
448
+ Example:
449
+ >>> system_config = PromptConfig(
450
+ ... prompt_id="profile_update_instruction_start",
451
+ ... variables={"agent_context_prompt": "...", "context_prompt": "..."}
452
+ ... )
453
+ >>> user_config = PromptConfig(
454
+ ... prompt_id="profile_update_main",
455
+ ... variables={"extraction_definition_prompt": "...", "existing_profiles": "..."}
456
+ ... )
457
+ >>> config = MessageConstructionConfig(
458
+ ... prompt_manager=prompt_manager,
459
+ ... system_prompt_config=system_config,
460
+ ... user_prompt_config=user_config
461
+ ... )
462
+ >>> messages = construct_messages_from_interactions(interactions, config)
463
+ """
464
+ messages = []
465
+
466
+ # Add system message if configured
467
+ if config.system_prompt_config:
468
+ system_content = config.prompt_manager.render_prompt(
469
+ config.system_prompt_config.prompt_id,
470
+ config.system_prompt_config.variables,
471
+ )
472
+ messages.append({"role": "system", "content": system_content})
473
+
474
+ # Build combined user message content
475
+ combined_content = []
476
+
477
+ # Add user prompt if configured
478
+ if config.user_prompt_config:
479
+ user_prompt_content = config.prompt_manager.render_prompt(
480
+ config.user_prompt_config.prompt_id,
481
+ config.user_prompt_config.variables,
482
+ )
483
+ combined_content.append({"type": "text", "text": user_prompt_content})
484
+
485
+ # Add last interaction's image (if present)
486
+ if interactions:
487
+ last_interaction = interactions[-1]
488
+
489
+ # Add image URL (priority over base64)
490
+ if last_interaction.interacted_image_url:
491
+ combined_content.append(
492
+ {
493
+ "type": "image_url",
494
+ "image_url": {"url": last_interaction.interacted_image_url},
495
+ }
496
+ )
497
+ # Add base64-encoded image if no URL
498
+ elif last_interaction.image_encoding:
499
+ combined_content.append(
500
+ {
501
+ "type": "image_url",
502
+ "image_url": {
503
+ "url": f"data:image/jpeg;base64,{last_interaction.image_encoding}"
504
+ },
505
+ }
506
+ )
507
+
508
+ # Add combined user message if there's any content
509
+ if combined_content:
510
+ # Flatten to plain string when all blocks are text-only (no images).
511
+ # Content-block format combined with structured output (response_format)
512
+ # causes significantly slower OpenAI API responses and timeouts.
513
+ all_text = all(block.get("type") == "text" for block in combined_content)
514
+ if all_text:
515
+ content = "\n\n".join(block["text"] for block in combined_content)
516
+ else:
517
+ content = combined_content
518
+ messages.append({"role": "user", "content": content})
519
+
520
+ return messages
521
+
522
+
523
+ def extract_json_from_string(text: str) -> dict:
524
+ """
525
+ Extract JSON from a string, handling both JSON-style and Python-style booleans.
526
+
527
+ This function attempts to extract JSON from text in the following order:
528
+ 1. From code blocks (```json...```)
529
+ 2. From content between first { and last }
530
+
531
+ It also handles Python-style boolean values (True/False/None) by converting
532
+ them to JSON-style (true/false/null) before parsing, and falls back to
533
+ Python literal parsing for dict-like responses that use single quotes.
534
+
535
+ Args:
536
+ text (str): string to extract JSON from
537
+
538
+ Returns:
539
+ dict: JSON object, or empty dict if parsing fails
540
+ """
541
+
542
+ def normalize_json_string(json_str: str) -> str:
543
+ """Convert Python-style syntax to JSON-friendly syntax.
544
+
545
+ Handles:
546
+ - Python booleans (True/False/None) -> JSON (true/false/null)
547
+ """
548
+ # Replace Python boolean/null values with JSON equivalents
549
+ # Use word boundaries to avoid replacing parts of strings
550
+ json_str = re.sub(r"\bTrue\b", "true", json_str)
551
+ json_str = re.sub(r"\bFalse\b", "false", json_str)
552
+ json_str = re.sub(r"\bNone\b", "null", json_str)
553
+
554
+ return json_str # noqa: RET504
555
+
556
+ def fix_unescaped_inner_quotes(json_str: str) -> str:
557
+ """
558
+ Attempt to fix common cases where apostrophes are returned as double quotes.
559
+
560
+ Many LLMs occasionally substitute `customer's` with `customer"s`, leaving the JSON
561
+ invalid because the double quote isn't escaped. We treat double quotes that sit
562
+ between two word characters as apostrophes.
563
+ """
564
+ return re.sub(r"(?<=\w)\"(?=\w)", "'", json_str)
565
+
566
+ def parse_json_candidate(json_str: str) -> tuple[dict | None, str | None]:
567
+ """
568
+ Try to parse a JSON candidate string using multiple strategies.
569
+
570
+ Strategies:
571
+ 1. Direct json.loads
572
+ 2. json.loads after normalizing Python syntax
573
+ 3. ast.literal_eval as a fallback for Python-style dicts
574
+ """
575
+ candidates = [json_str]
576
+ last_error: str | None = None
577
+
578
+ normalized_json_str = normalize_json_string(json_str)
579
+ if normalized_json_str != json_str:
580
+ candidates.append(normalized_json_str)
581
+
582
+ repaired_json_str = fix_unescaped_inner_quotes(normalized_json_str)
583
+ if repaired_json_str not in candidates:
584
+ candidates.append(repaired_json_str)
585
+
586
+ for candidate in candidates:
587
+ try:
588
+ parsed = json.loads(candidate)
589
+ if isinstance(parsed, dict):
590
+ return parsed, None
591
+ except json.JSONDecodeError as err: # noqa: PERF203
592
+ last_error = str(err)
593
+ continue
594
+
595
+ for candidate in candidates:
596
+ try:
597
+ parsed = ast.literal_eval(candidate)
598
+ if isinstance(parsed, dict):
599
+ return parsed, None
600
+ except (ValueError, SyntaxError) as err: # noqa: PERF203
601
+ last_error = str(err)
602
+ continue
603
+
604
+ return None, last_error
605
+
606
+ # Pattern to match content between triple backticks and json keyword
607
+ # Allow optional whitespace around json keyword and content
608
+ pattern = r"```json\s*(.*?)\s*```"
609
+
610
+ # Find the match using regex
611
+ match = re.search(pattern, text, re.DOTALL)
612
+
613
+ if match:
614
+ json_str = match.group(1)
615
+ parsed_json, error_message = parse_json_candidate(json_str)
616
+ if parsed_json is not None:
617
+ return parsed_json
618
+ if error_message:
619
+ logger.warning("Failed to parse JSON from code block: %s", error_message)
620
+
621
+ # Try to find JSON content between first { and last }
622
+ start_idx = text.find("{")
623
+ end_idx = text.rfind("}")
624
+ if start_idx != -1 and end_idx != -1:
625
+ json_str = text[start_idx : end_idx + 1]
626
+ parsed_json, error_message = parse_json_candidate(json_str)
627
+ if parsed_json is not None:
628
+ return parsed_json
629
+ if error_message:
630
+ logger.warning("Failed to parse JSON from braces: %s", error_message)
631
+
632
+ return {}
633
+
634
+
635
+ def _format_tool_calls(tool_calls: list[Any]) -> list[str]:
636
+ """Render an assistant message's ``tool_calls`` list for the log.
637
+
638
+ Accepts either the OpenAI SDK object shape (with ``.function.name`` /
639
+ ``.function.arguments`` attrs) or the dict shape that pass-through
640
+ serialisation may produce. Returns one indented line per call with the
641
+ tool_call_id, the tool name, and the parsed arguments — so the log
642
+ reader can correlate each tool_call with its tool-role response.
643
+ """
644
+ lines: list[str] = [" tool_calls:"]
645
+ for tc in tool_calls:
646
+ # Extract id, name, arguments from either attribute or mapping shape.
647
+ tc_id = getattr(tc, "id", None) or (
648
+ tc.get("id") if isinstance(tc, dict) else None
649
+ )
650
+ fn = getattr(tc, "function", None)
651
+ if fn is not None:
652
+ name = getattr(fn, "name", None)
653
+ args_raw = getattr(fn, "arguments", None)
654
+ elif isinstance(tc, dict):
655
+ fn_dict = tc.get("function", {}) or {}
656
+ name = fn_dict.get("name") if isinstance(fn_dict, dict) else None
657
+ args_raw = fn_dict.get("arguments") if isinstance(fn_dict, dict) else None
658
+ else:
659
+ name = None
660
+ args_raw = None
661
+
662
+ # arguments comes through as a JSON string from the provider — parse
663
+ # for readability, fall back to raw text on malformed JSON.
664
+ parsed_args: Any
665
+ if isinstance(args_raw, str):
666
+ try:
667
+ parsed_args = json.loads(args_raw)
668
+ except json.JSONDecodeError:
669
+ parsed_args = args_raw
670
+ else:
671
+ parsed_args = args_raw
672
+
673
+ lines.append(f" - id: {tc_id}")
674
+ lines.append(f" name: {name}")
675
+ # Logging path must never raise — fall back to repr() on
676
+ # non-serializable argument objects (datetime, sets, custom
677
+ # types, etc.) so a logging call can't take down a request.
678
+ try:
679
+ rendered_args = json.dumps(parsed_args)
680
+ except (TypeError, ValueError):
681
+ rendered_args = repr(parsed_args)
682
+ lines.append(f" arguments: {rendered_args}")
683
+ return lines
684
+
685
+
686
+ def format_messages_for_logging(messages: list[dict[str, Any]]) -> str:
687
+ """
688
+ Format messages for logging with proper newlines in text content.
689
+
690
+ Args:
691
+ messages: List of message dictionaries with role and content
692
+
693
+ Returns:
694
+ str: Formatted string representation of messages with newlines preserved
695
+ """
696
+ formatted_parts = []
697
+ for i, msg in enumerate(messages):
698
+ formatted_parts.append(f"Message {i + 1}:")
699
+ formatted_parts.append(f" role: {msg.get('role', 'unknown')}")
700
+
701
+ # Tool-role messages carry a ``tool_call_id`` that correlates them
702
+ # back to the assistant's emitted call — render it so readers can
703
+ # reconstruct which response answered which call.
704
+ tool_call_id = msg.get("tool_call_id")
705
+ if tool_call_id is not None:
706
+ formatted_parts.append(f" tool_call_id: {tool_call_id}")
707
+
708
+ content = msg.get("content", "")
709
+
710
+ if isinstance(content, str):
711
+ # Simple string content - preserve newlines
712
+ formatted_parts.append(" content:")
713
+ # Indent each line of content
714
+ formatted_parts.extend(f" {line}" for line in content.split("\n"))
715
+ elif isinstance(content, list):
716
+ # Multimodal content (list of objects)
717
+ formatted_parts.append(" content:")
718
+ for item in content:
719
+ if isinstance(item, dict):
720
+ item_type = item.get("type", "unknown")
721
+ if item_type == "text":
722
+ text_content = item.get("text", "")
723
+ formatted_parts.append(f" type: {item_type}")
724
+ formatted_parts.append(" text:")
725
+ # Indent each line of text
726
+ formatted_parts.extend(
727
+ f" {line}" for line in text_content.split("\n")
728
+ )
729
+ else:
730
+ # For non-text content, use JSON representation
731
+ formatted_parts.append(f" {json.dumps(item, indent=4)}")
732
+ else:
733
+ formatted_parts.append(f" {json.dumps(item, indent=4)}")
734
+ else:
735
+ # Fallback to JSON for other types
736
+ formatted_parts.append(f" content: {json.dumps(content, indent=4)}")
737
+
738
+ # Assistant messages with tool_calls must render the call list —
739
+ # otherwise the log shows ``content: null`` with no visibility into
740
+ # which tools the model invoked. Classic extraction doesn't use
741
+ # tool-calling, but the agentic pipeline relies on it heavily.
742
+ tool_calls = msg.get("tool_calls")
743
+ if tool_calls:
744
+ formatted_parts.extend(_format_tool_calls(tool_calls))
745
+
746
+ formatted_parts.append("") # Empty line between messages
747
+
748
+ return "\n".join(formatted_parts)
749
+
750
+
751
+ # Example usage
752
+ if __name__ == "__main__":
753
+ test_string = """'Based on the existing profiles (which are currently empty) and the new interaction indicating a preference for sushi, the update would involve adding a new profile that reflects this preference. Here's the JSON format for the updates:\n\n```json\n{\n "add_profile": ["I like sushi"],\n "delete_profile": []\n}\n```'"""
754
+
755
+ result = extract_json_from_string(test_string)
756
+ print("Extracted JSON:", result)