ruby_llm 1.16.0 → 2.0.0.rc2

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 (475) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +25 -0
  3. data/README.md +85 -32
  4. data/exe/ruby_llm +8 -0
  5. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  10. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
  11. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
  23. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
  24. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
  25. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
  26. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
  27. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
  28. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
  29. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
  37. data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
  38. data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
  39. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
  40. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
  41. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
  42. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
  43. data/lib/generators/ruby_llm/provider/cli.rb +175 -0
  44. data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
  45. data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
  46. data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
  47. data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
  48. data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
  49. data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
  50. data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
  51. data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
  52. data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
  53. data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
  54. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
  55. data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
  56. data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
  57. data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
  58. data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
  59. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
  60. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
  61. data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
  62. data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
  63. data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
  64. data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
  65. data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
  66. data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
  67. data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
  68. data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
  69. data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
  70. data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
  71. data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
  72. data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
  73. data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
  74. data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
  75. data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
  76. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
  77. data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
  78. data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
  79. data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
  80. data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
  81. data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
  82. data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
  83. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
  84. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
  85. data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
  86. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
  87. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
  88. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
  89. data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
  90. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
  91. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
  92. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
  93. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
  94. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
  95. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
  96. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
  97. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
  98. data/lib/ruby_llm/accounting/usage.rb +245 -0
  99. data/lib/ruby_llm/active_record/acts_as.rb +94 -111
  100. data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
  101. data/lib/ruby_llm/active_record/batch.rb +97 -0
  102. data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
  103. data/lib/ruby_llm/active_record/message_methods.rb +113 -136
  104. data/lib/ruby_llm/active_record/model.rb +135 -0
  105. data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
  106. data/lib/ruby_llm/active_record/tool_call.rb +33 -0
  107. data/lib/ruby_llm/active_record/usage.rb +61 -0
  108. data/lib/ruby_llm/agent.rb +1065 -151
  109. data/lib/ruby_llm/aliases.json +268 -100
  110. data/lib/ruby_llm/attachment.rb +187 -48
  111. data/lib/ruby_llm/batch.rb +432 -0
  112. data/lib/ruby_llm/cached_content.rb +112 -0
  113. data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
  114. data/lib/ruby_llm/chat.rb +1127 -198
  115. data/lib/ruby_llm/chunk.rb +10 -0
  116. data/lib/ruby_llm/citation.rb +105 -0
  117. data/lib/ruby_llm/configuration.rb +261 -24
  118. data/lib/ruby_llm/context.rb +128 -6
  119. data/lib/ruby_llm/cost.rb +217 -80
  120. data/lib/ruby_llm/downloaded_file.rb +33 -0
  121. data/lib/ruby_llm/embedding.rb +121 -17
  122. data/lib/ruby_llm/embedding_request.rb +53 -0
  123. data/lib/ruby_llm/error.rb +159 -23
  124. data/lib/ruby_llm/fallback.rb +133 -0
  125. data/lib/ruby_llm/files/mime_type.rb +97 -0
  126. data/lib/ruby_llm/image.rb +153 -32
  127. data/lib/ruby_llm/message.rb +233 -54
  128. data/lib/ruby_llm/model/modalities.rb +17 -4
  129. data/lib/ruby_llm/model/pricing.rb +24 -5
  130. data/lib/ruby_llm/model/pricing_category.rb +103 -14
  131. data/lib/ruby_llm/model/pricing_tier.rb +55 -15
  132. data/lib/ruby_llm/model.rb +244 -2
  133. data/lib/ruby_llm/models/aliases.rb +41 -0
  134. data/lib/ruby_llm/models/registry.rb +165 -0
  135. data/lib/ruby_llm/models/schema.rb +99 -0
  136. data/lib/ruby_llm/models.json +73261 -33733
  137. data/lib/ruby_llm/models.rb +477 -215
  138. data/lib/ruby_llm/moderation.rb +139 -26
  139. data/lib/ruby_llm/ocr.rb +112 -0
  140. data/lib/ruby_llm/prompt.rb +79 -0
  141. data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
  142. data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
  143. data/lib/ruby_llm/protocol/streaming.rb +230 -0
  144. data/lib/ruby_llm/protocol.rb +662 -0
  145. data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
  146. data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
  147. data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
  148. data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
  149. data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
  150. data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
  151. data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
  152. data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
  153. data/lib/ruby_llm/protocols/anthropic.rb +101 -0
  154. data/lib/ruby_llm/protocols/azure/files.rb +16 -0
  155. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
  156. data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
  157. data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
  158. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
  159. data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
  160. data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
  161. data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
  162. data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
  163. data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
  164. data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
  165. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
  166. data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
  167. data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
  168. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
  169. data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
  170. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
  171. data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
  172. data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
  173. data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
  174. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
  175. data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
  176. data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
  177. data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
  178. data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
  179. data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
  180. data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
  181. data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
  182. data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
  183. data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
  184. data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
  185. data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
  186. data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
  187. data/lib/ruby_llm/protocols/cohere.rb +21 -0
  188. data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
  189. data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
  190. data/lib/ruby_llm/protocols/converse/media.rb +178 -0
  191. data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
  192. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
  193. data/lib/ruby_llm/protocols/converse.rb +54 -0
  194. data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
  195. data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
  196. data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
  197. data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
  198. data/lib/ruby_llm/protocols/deepgram.rb +19 -0
  199. data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
  200. data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
  201. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
  202. data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
  203. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
  204. data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
  205. data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
  206. data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
  207. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
  208. data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
  209. data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
  210. data/lib/ruby_llm/protocols/files.rb +119 -0
  211. data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
  212. data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
  213. data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
  214. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
  215. data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
  216. data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
  217. data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
  218. data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
  219. data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
  220. data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
  221. data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
  222. data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
  223. data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
  224. data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
  225. data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
  226. data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
  227. data/lib/ruby_llm/protocols/gemini.rb +35 -0
  228. data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
  229. data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
  230. data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
  231. data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
  232. data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
  233. data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
  234. data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
  235. data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
  236. data/lib/ruby_llm/protocols/interactions.rb +29 -0
  237. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
  238. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
  239. data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
  240. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
  241. data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
  242. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
  243. data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
  244. data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
  245. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
  246. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
  247. data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
  248. data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
  249. data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
  250. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
  251. data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
  252. data/lib/ruby_llm/protocols/openai/files.rb +42 -0
  253. data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
  254. data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
  255. data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
  256. data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
  257. data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
  258. data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
  259. data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
  260. data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
  261. data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
  262. data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
  263. data/lib/ruby_llm/protocols/responses/media.rb +61 -0
  264. data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
  265. data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
  266. data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
  267. data/lib/ruby_llm/protocols/responses.rb +35 -0
  268. data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
  269. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
  270. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
  271. data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
  272. data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
  273. data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
  274. data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
  275. data/lib/ruby_llm/protocols/xai/files.rb +30 -0
  276. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
  277. data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
  278. data/lib/ruby_llm/provider.rb +560 -128
  279. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
  280. data/lib/ruby_llm/providers/anthropic.rb +4 -6
  281. data/lib/ruby_llm/providers/azure/audio.rb +18 -0
  282. data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
  283. data/lib/ruby_llm/providers/azure/chat.rb +2 -9
  284. data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
  285. data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
  286. data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
  287. data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
  288. data/lib/ruby_llm/providers/azure/images.rb +22 -0
  289. data/lib/ruby_llm/providers/azure/media.rb +5 -14
  290. data/lib/ruby_llm/providers/azure/models.rb +35 -0
  291. data/lib/ruby_llm/providers/azure/responses.rb +26 -0
  292. data/lib/ruby_llm/providers/azure/videos.rb +64 -0
  293. data/lib/ruby_llm/providers/azure.rb +77 -78
  294. data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
  295. data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
  296. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
  297. data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
  298. data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
  299. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
  300. data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
  301. data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
  302. data/lib/ruby_llm/providers/bedrock.rb +216 -45
  303. data/lib/ruby_llm/providers/cohere.rb +31 -0
  304. data/lib/ruby_llm/providers/deepgram.rb +37 -0
  305. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
  306. data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
  307. data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
  308. data/lib/ruby_llm/providers/deepseek.rb +9 -2
  309. data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
  310. data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
  311. data/lib/ruby_llm/providers/gemini.rb +15 -8
  312. data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
  313. data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
  314. data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
  315. data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
  316. data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
  317. data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
  318. data/lib/ruby_llm/providers/gpustack.rb +35 -11
  319. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
  320. data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
  321. data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
  322. data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
  323. data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
  324. data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
  325. data/lib/ruby_llm/providers/mistral/media.rb +6 -18
  326. data/lib/ruby_llm/providers/mistral/models.rb +57 -23
  327. data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
  328. data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
  329. data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
  330. data/lib/ruby_llm/providers/mistral.rb +16 -4
  331. data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
  332. data/lib/ruby_llm/providers/ollama/media.rb +6 -15
  333. data/lib/ruby_llm/providers/ollama/models.rb +50 -9
  334. data/lib/ruby_llm/providers/ollama.rb +9 -8
  335. data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
  336. data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
  337. data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
  338. data/lib/ruby_llm/providers/openai/models.rb +23 -23
  339. data/lib/ruby_llm/providers/openai/responses.rb +13 -0
  340. data/lib/ruby_llm/providers/openai.rb +92 -11
  341. data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
  342. data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
  343. data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
  344. data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
  345. data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
  346. data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
  347. data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
  348. data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
  349. data/lib/ruby_llm/providers/openrouter.rb +78 -20
  350. data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
  351. data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
  352. data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
  353. data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
  354. data/lib/ruby_llm/providers/perplexity.rb +28 -20
  355. data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
  356. data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
  357. data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
  358. data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
  359. data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
  360. data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
  361. data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
  362. data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
  363. data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
  364. data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
  365. data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
  366. data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
  367. data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
  368. data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
  369. data/lib/ruby_llm/providers/vertexai.rb +160 -17
  370. data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
  371. data/lib/ruby_llm/providers/xai/chat.rb +3 -2
  372. data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
  373. data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
  374. data/lib/ruby_llm/providers/xai/images.rb +91 -0
  375. data/lib/ruby_llm/providers/xai/models.rb +32 -36
  376. data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
  377. data/lib/ruby_llm/providers/xai/responses.rb +52 -0
  378. data/lib/ruby_llm/providers/xai/speech.rb +45 -0
  379. data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
  380. data/lib/ruby_llm/providers/xai/videos.rb +87 -0
  381. data/lib/ruby_llm/providers/xai.rb +15 -5
  382. data/lib/ruby_llm/railtie.rb +7 -16
  383. data/lib/ruby_llm/rerank.rb +105 -0
  384. data/lib/ruby_llm/research_job.rb +241 -0
  385. data/lib/ruby_llm/search_results.rb +68 -0
  386. data/lib/ruby_llm/server_tool_call.rb +73 -0
  387. data/lib/ruby_llm/speech.rb +159 -0
  388. data/lib/ruby_llm/speech_chunk.rb +33 -0
  389. data/lib/ruby_llm/support/deprecator.rb +22 -0
  390. data/lib/ruby_llm/support/inspectable.rb +49 -0
  391. data/lib/ruby_llm/support/instrumentation.rb +41 -0
  392. data/lib/ruby_llm/support/utils.rb +147 -0
  393. data/lib/ruby_llm/thinking.rb +127 -20
  394. data/lib/ruby_llm/tokenization.rb +59 -0
  395. data/lib/ruby_llm/tokens.rb +103 -33
  396. data/lib/ruby_llm/tool.rb +266 -91
  397. data/lib/ruby_llm/tool_call.rb +36 -3
  398. data/lib/ruby_llm/tools/server_tools.rb +109 -0
  399. data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
  400. data/lib/ruby_llm/transcription.rb +138 -13
  401. data/lib/ruby_llm/transcription_chunk.rb +68 -0
  402. data/lib/ruby_llm/transport/connection.rb +193 -0
  403. data/lib/ruby_llm/transport/error_middleware.rb +131 -0
  404. data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
  405. data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
  406. data/lib/ruby_llm/uploaded_file.rb +144 -0
  407. data/lib/ruby_llm/version.rb +2 -1
  408. data/lib/ruby_llm/video.rb +136 -0
  409. data/lib/ruby_llm/video_job.rb +150 -0
  410. data/lib/ruby_llm/workflow.rb +91 -0
  411. data/lib/ruby_llm.rb +380 -6
  412. data/lib/tasks/ruby_llm.rake +21 -16
  413. data/skills/rubyllm/SKILL.md +81 -0
  414. data/skills/rubyllm/agents/openai.yaml +4 -0
  415. metadata +339 -97
  416. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  417. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
  418. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  419. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
  420. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
  421. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  422. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  423. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  424. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  425. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  426. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  427. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  428. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  429. data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
  430. data/lib/ruby_llm/active_record/model_methods.rb +0 -82
  431. data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
  432. data/lib/ruby_llm/aliases.rb +0 -41
  433. data/lib/ruby_llm/connection.rb +0 -159
  434. data/lib/ruby_llm/content.rb +0 -91
  435. data/lib/ruby_llm/deprecator.rb +0 -24
  436. data/lib/ruby_llm/error_middleware.rb +0 -81
  437. data/lib/ruby_llm/instrumentation.rb +0 -36
  438. data/lib/ruby_llm/mime_type.rb +0 -96
  439. data/lib/ruby_llm/model/info.rb +0 -164
  440. data/lib/ruby_llm/model_registry.rb +0 -39
  441. data/lib/ruby_llm/models_schema.json +0 -171
  442. data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
  443. data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
  444. data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
  445. data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
  446. data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
  447. data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
  448. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
  449. data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
  450. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
  451. data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
  452. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
  453. data/lib/ruby_llm/providers/gemini/images.rb +0 -47
  454. data/lib/ruby_llm/providers/gemini/models.rb +0 -38
  455. data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
  456. data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
  457. data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
  458. data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
  459. data/lib/ruby_llm/providers/openai/chat.rb +0 -236
  460. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
  461. data/lib/ruby_llm/providers/openai/images.rb +0 -90
  462. data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
  463. data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
  464. data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
  465. data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
  466. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
  467. data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
  468. data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
  469. data/lib/ruby_llm/stream_accumulator.rb +0 -218
  470. data/lib/ruby_llm/streaming.rb +0 -179
  471. data/lib/ruby_llm/tool_concurrency.rb +0 -105
  472. data/lib/ruby_llm/utils.rb +0 -130
  473. data/lib/tasks/models.rake +0 -593
  474. data/lib/tasks/release.rake +0 -94
  475. data/lib/tasks/vcr.rake +0 -124
@@ -1,88 +1,383 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'erb'
4
3
  require 'forwardable'
5
- require 'pathname'
6
- require 'ruby_llm/schema'
4
+ require 'schematist'
7
5
 
8
6
  module RubyLLM
9
- # Base class for simple, class-configured agents.
7
+ # An Agent is a reusable chat configuration defined as a class. Subclasses
8
+ # declare a model, instructions, tools, and other settings once, then build
9
+ # configured chats wherever they are needed.
10
+ #
11
+ # class SupportAgent < RubyLLM::Agent
12
+ # model "gpt-5.6-luna"
13
+ # instructions "You are a concise support assistant."
14
+ # tools SearchDocs, LookupAccount
15
+ # end
16
+ #
17
+ # SupportAgent.new.ask "How do I reset my API key?"
18
+ #
19
+ # ::chat returns a configured Chat. When ::chat_model names an ActiveRecord
20
+ # chat class, ::create, ::create!, and ::find return configured records of
21
+ # that class instead.
22
+ #
23
+ # Configuration that depends on runtime state goes in blocks or lambdas.
24
+ # They are evaluated when a chat is built, with +chat+ and any declared
25
+ # ::inputs available as methods:
26
+ #
27
+ # class WorkAssistant < RubyLLM::Agent
28
+ # inputs :workspace
29
+ #
30
+ # instructions { "You are helping #{workspace.name}" }
31
+ # end
32
+ #
33
+ # WorkAssistant.chat(workspace: workspace)
34
+ #
35
+ # Agent instances delegate Chat's conversation API (#ask, #complete,
36
+ # #with_tools, and so on) to the wrapped chat, which is available via
37
+ # #chat. Direct transcript replacement stays on the wrapped chat because
38
+ # Rails-backed chat models own their message association. Agents are
39
+ # enumerable over their messages.
10
40
  class Agent
11
41
  extend Forwardable
12
42
  include Enumerable
13
43
 
44
+ DUPED_INHERITED_CONFIG = {
45
+ :@chat_kwargs => {},
46
+ :@tools => [],
47
+ :@server_tools => [],
48
+ :@tool_options => {},
49
+ :@caching => nil,
50
+ :@compaction => nil,
51
+ :@provider_options => {},
52
+ :@headers => {},
53
+ :@input_names => [],
54
+ :@fallbacks => [],
55
+ :@fallback_options => {},
56
+ :@rescue_handlers => [],
57
+ :@instructions => []
58
+ }.freeze
59
+ # Simple value options: a class-level getter/setter macro whose value the
60
+ # agent forwards to the matching Chat#with_* when it builds its chat.
61
+ PASSTHROUGH_OPTIONS = %i[temperature max_output_tokens].freeze
62
+ THINKING_OPTIONS = %i[effort budget display].freeze
63
+
64
+ # The chat operations an agent instance runs through its ::rescue_from
65
+ # handlers. Remaining delegated methods pass through untouched.
66
+ GUARDED_OPERATIONS = %i[ask say ask_later complete generate run_tools step count_tokens compact].freeze
67
+
68
+ # Chat methods that return the wrapped chat so calls can be chained there.
69
+ CHAINABLE_CHAT_DELEGATES = %i[
70
+ with_instructions with_tools with_server_tools with_tool_options with_model
71
+ with_temperature with_max_output_tokens with_thinking with_citations
72
+ with_end_user with_compaction with_caching with_context with_provider_options
73
+ with_headers with_schema with_fallbacks
74
+ before_request before_message after_message before_tool_call after_tool_result
75
+ before_fallback after_fallback
76
+ cancel approve deny cache_until_here
77
+ ].freeze
78
+
79
+ # Chat values and operations whose return values pass through unchanged.
80
+ PASSTHROUGH_CHAT_DELEGATES = %i[
81
+ model provider messages tools server_tools tool_options provider_options headers schema concurrency
82
+ caching citations compaction context end_user fallbacks thinking temperature max_output_tokens
83
+ each complete? cancelled? awaiting_approval? pending_approvals
84
+ add_message add_completion tokens cost render
85
+ ].freeze
86
+
87
+ COPIED_INHERITED_CONFIG = (%i[
88
+ @thinking
89
+ @citations
90
+ @end_user
91
+ @schema
92
+ @context
93
+ @chat_model
94
+ ] + PASSTHROUGH_OPTIONS.map { |option| :"@#{option}" }).freeze
95
+ private_constant :DUPED_INHERITED_CONFIG, :COPIED_INHERITED_CONFIG,
96
+ :PASSTHROUGH_OPTIONS, :THINKING_OPTIONS, :GUARDED_OPERATIONS,
97
+ :CHAINABLE_CHAT_DELEGATES, :PASSTHROUGH_CHAT_DELEGATES
98
+
14
99
  class << self
15
- def inherited(subclass)
100
+ def inherited(subclass) # :nodoc:
16
101
  super
17
- subclass.instance_variable_set(:@chat_kwargs, (@chat_kwargs || {}).dup)
18
- subclass.instance_variable_set(:@tools, (@tools || []).dup)
19
- subclass.instance_variable_set(:@instructions, @instructions)
20
- subclass.instance_variable_set(:@temperature, @temperature)
21
- subclass.instance_variable_set(:@thinking, @thinking)
22
- subclass.instance_variable_set(:@params, (@params || {}).dup)
23
- subclass.instance_variable_set(:@headers, (@headers || {}).dup)
24
- subclass.instance_variable_set(:@schema, @schema)
25
- subclass.instance_variable_set(:@context, @context)
26
- subclass.instance_variable_set(:@chat_model, @chat_model)
27
- subclass.instance_variable_set(:@input_names, (@input_names || []).dup)
28
- end
29
-
30
- def model(model_id = nil, **options)
31
- options[:model] = model_id unless model_id.nil?
102
+ copy_inherited_config_to(subclass)
103
+ end
104
+
105
+ # Sets the model used by chats this agent builds. Extra +options+ are
106
+ # forwarded to RubyLLM.chat, including +provider:+ to disambiguate the
107
+ # model and +protocol:+ to override its wire protocol. A block picks
108
+ # the model when the chat is built, with the declared ::inputs
109
+ # available as methods. Called with no arguments, returns the
110
+ # configured chat keywords.
111
+ #
112
+ # model "gpt-5.6-luna"
113
+ # model "gpt-5.6", provider: :openai, protocol: :responses
114
+ # model { quality == :high ? "gpt-5.6" : "gpt-5.6-luna" }
115
+ #
116
+ # The block runs before the chat exists, so it can read inputs but not
117
+ # +chat+.
118
+ def model(model_id = nil, **options, &block)
119
+ return @chat_kwargs || {} if model_id.nil? && options.empty? && !block_given?
120
+
121
+ model_value = block || model_id
122
+ options[:model] = model_value unless model_value.nil?
32
123
  @chat_kwargs = options
33
124
  end
34
125
 
126
+ # Declares the tools for chats this agent builds. A block defers
127
+ # construction until the chat is built. Configure how the model uses
128
+ # them with ::tool_options. Called with no arguments, returns the
129
+ # declared tools.
130
+ #
131
+ # tools SearchDocs, LookupAccount
132
+ # tools { [TodoTool.new(chat: chat)] }
133
+ #
35
134
  def tools(*tools, &block)
36
135
  return @tools || [] if tools.empty? && !block_given?
37
136
 
38
137
  @tools = block_given? ? block : tools.flatten
39
138
  end
40
139
 
41
- def instructions(text = nil, **prompt_locals, &block)
42
- if text.nil? && prompt_locals.empty? && !block_given?
43
- @instructions ||= { prompt: 'instructions', locals: {} }
44
- return @instructions
140
+ # Sets how chats this agent builds use their tools, applied via
141
+ # Chat#with_tool_options. Accepts +choice:+, +calls:+, and
142
+ # +concurrency:+. A block defers evaluation until the chat is built.
143
+ # Called with no arguments, returns the configured options.
144
+ #
145
+ # tool_options choice: :required, calls: :one
146
+ #
147
+ def tool_options(**options, &block)
148
+ return @tool_options || {} if options.empty? && !block_given?
149
+
150
+ @tool_options = block_given? ? block : options
151
+ end
152
+
153
+ # Enables provider-executed tools for chats this agent builds, applied
154
+ # via Chat#with_server_tools. Accepts the same aliases, options, and
155
+ # raw Hashes; a block defers evaluation until the chat is built.
156
+ # Called with no arguments, returns the declared entries.
157
+ #
158
+ # server_tools :web_search
159
+ # server_tools web_search: { allowed_domains: ["ruby-lang.org"] }
160
+ #
161
+ def server_tools(*tools, **tools_with_options, &block)
162
+ return @server_tools || [] if tools.empty? && tools_with_options.empty? && !block_given?
163
+
164
+ @server_tools = block_given? ? block : RubyLLM::Tools::ServerTools.normalize(tools, tools_with_options)
165
+ end
166
+
167
+ # Adds system instructions for chats this agent builds. Accepts a string,
168
+ # a block evaluated when the chat is built, or keyword locals for the
169
+ # agent's conventional prompt template (for a WorkAssistant agent,
170
+ # <tt>app/prompts/work_assistant/instructions.txt.erb</tt>). Multiple
171
+ # declarations are applied in order.
172
+ #
173
+ # instructions "You are a helpful assistant."
174
+ # instructions { "You are helping #{workspace.name}" }
175
+ # instructions display_name: -> { chat.user.display_name_or_email }
176
+ # instructions append: true, persist: false do
177
+ # "Today is #{Date.current}"
178
+ # end
179
+ #
180
+ # A named agent uses its conventional template automatically when it
181
+ # exists, even without calling this method. In Rails mode, declarations
182
+ # persist when the record is created unless <tt>persist: false</tt>;
183
+ # ::find always reapplies them without rewriting history. Called with no
184
+ # arguments, returns the declarations.
185
+ def instructions(text = nil, append: false, persist: true, cache_until_here: false, **prompt_locals, &block)
186
+ return instruction_declarations if text.nil? && prompt_locals.empty? && !block_given?
187
+
188
+ instruction_declarations << {
189
+ value: block || text || { prompt: 'instructions', locals: prompt_locals },
190
+ append: append,
191
+ persist: persist,
192
+ cache_until_here: cache_until_here
193
+ }
194
+ end
195
+
196
+ ##
197
+ # :method: temperature
198
+ # :call-seq: temperature(value = nil)
199
+ #
200
+ # Sets the sampling temperature for chats this agent builds. Called
201
+ # with no argument, returns the configured value.
202
+ #
203
+ # temperature 0.2
204
+
205
+ ##
206
+ # :method: max_output_tokens
207
+ # :call-seq: max_output_tokens(value = nil)
208
+ #
209
+ # Caps the number of tokens chats this agent builds may generate.
210
+ # Called with no argument, returns the configured value.
211
+ #
212
+ # max_output_tokens 1000
213
+
214
+ PASSTHROUGH_OPTIONS.each do |option|
215
+ define_method(option) do |value = nil|
216
+ return instance_variable_get(:"@#{option}") if value.nil?
217
+
218
+ instance_variable_set(:"@#{option}", value)
45
219
  end
220
+ end
46
221
 
47
- @instructions = block || text || { prompt: 'instructions', locals: prompt_locals }
222
+ # Enables thinking for chats this agent builds, applied via
223
+ # Chat#with_thinking. With no options, RubyLLM chooses from the model's
224
+ # registered controls. Accepts keywords or an options Hash. Pass +false+
225
+ # to disable it. Passing +nil+ raises ArgumentError.
226
+ #
227
+ # thinking
228
+ # thinking false
229
+ # thinking effort: :low
230
+ # thinking budget: 10_000
231
+ # thinking display: :summarized
232
+ #
233
+ def thinking(enabled = true, **options) # rubocop:disable Style/OptionalBooleanParameter
234
+ return thinking(**enabled.transform_keys(&:to_sym), **options) if enabled.is_a?(Hash)
235
+
236
+ validate_thinking_options(enabled, options)
237
+ @thinking = enabled ? options : false
48
238
  end
49
239
 
50
- def temperature(value = nil)
51
- return @temperature if value.nil?
240
+ # Enables context compaction for chats this agent builds, applied via
241
+ # Chat#with_compaction. With no options, the provider's own defaults
242
+ # apply. Pass +false+ to disable it.
243
+ #
244
+ # compaction
245
+ # compaction false
246
+ # compaction at: 50_000
247
+ #
248
+ def compaction(options = {})
249
+ options = {} if options == true
250
+ unless options == false || options.is_a?(Hash)
251
+ raise ArgumentError, 'compaction accepts true, false, or compaction options'
252
+ end
253
+
254
+ @compaction = options
255
+ end
52
256
 
53
- @temperature = value
257
+ # Sets the safety identifier for chats this agent builds, applied via
258
+ # Chat#with_end_user. A block defers evaluation until the
259
+ # chat is built, so the id can come from the agent's inputs. Called
260
+ # with no arguments, returns the configured value.
261
+ #
262
+ # end_user "tenant-42"
263
+ # end_user { workspace.public_id }
264
+ #
265
+ def end_user(value = nil, &block)
266
+ return @end_user if value.nil? && !block_given?
267
+
268
+ @end_user = block || value
54
269
  end
55
270
 
56
- def thinking(effort: nil, budget: nil)
57
- return @thinking if effort.nil? && budget.nil?
271
+ # Enables citations for chats this agent builds, applied via
272
+ # Chat#with_citations. Pass +false+ to disable them.
273
+ #
274
+ # citations
275
+ # citations false
276
+ #
277
+ def citations(value = true) # rubocop:disable Style/OptionalBooleanParameter
278
+ raise ArgumentError, 'citations accepts true or false' unless [true, false].include?(value)
58
279
 
59
- @thinking = { effort: effort, budget: budget }
280
+ @citations = value
60
281
  end
61
282
 
62
- def params(**params, &block)
63
- return @params || {} if params.empty? && !block_given?
283
+ # Enables prompt caching for chats this agent builds, applied via
284
+ # Chat#with_caching. With no options, the provider's default behavior
285
+ # applies. Pass +false+ to stop RubyLLM from sending cache controls. A
286
+ # provider may still cache prompts implicitly. A block defers evaluation
287
+ # until the chat is built. Accepts keywords or an options Hash.
288
+ #
289
+ # caching
290
+ # caching false
291
+ # caching ttl: "1h"
292
+ # caching { { ttl: workspace.cache_ttl } }
293
+ #
294
+ def caching(enabled = true, **options, &block) # rubocop:disable Metrics/PerceivedComplexity, Style/OptionalBooleanParameter
295
+ return caching(**enabled.transform_keys(&:to_sym), **options, &block) if enabled.is_a?(Hash)
296
+
297
+ raise ArgumentError, 'caching accepts false or caching options' unless [true, false].include?(enabled)
298
+ raise ArgumentError, 'caching accepts options or a block, not both' if options.any? && block
299
+ raise ArgumentError, 'caching false does not accept options or a block' if !enabled && (options.any? || block)
300
+
301
+ @caching = block || (enabled ? options : false)
302
+ end
64
303
 
65
- @params = block_given? ? block : params
304
+ # Sets options in the provider's request vocabulary for chats this
305
+ # agent builds, applied via Chat#with_provider_options. A block
306
+ # defers evaluation until the chat is built. Called with no
307
+ # arguments, returns the configured value.
308
+ #
309
+ # provider_options service_tier: "flex"
310
+ #
311
+ def provider_options(**provider_options, &block)
312
+ return @provider_options || {} if provider_options.empty? && !block_given?
313
+
314
+ @provider_options = block_given? ? block : provider_options
66
315
  end
67
316
 
317
+ # Sets custom HTTP headers for chats this agent builds, applied via
318
+ # Chat#with_headers. A block defers evaluation until the chat is
319
+ # built. Called with no arguments, returns the configured value.
68
320
  def headers(**headers, &block)
69
321
  return @headers || {} if headers.empty? && !block_given?
70
322
 
71
323
  @headers = block_given? ? block : headers
72
324
  end
73
325
 
326
+ # Sets the structured output schema for chats this agent builds,
327
+ # applied via Chat#with_schema. Accepts a schema class, a JSON schema
328
+ # hash, or a block. A plain block is built with the Schematist::Schema
329
+ # DSL; a lambda is evaluated when the chat is built. Called with no
330
+ # arguments, returns the configured value.
331
+ #
332
+ # schema PersonSchema
333
+ # schema do
334
+ # string :verdict, enum: ["pass", "revise"]
335
+ # string :feedback
336
+ # end
337
+ #
74
338
  def schema(value = nil, &block)
75
339
  return @schema if value.nil? && !block_given?
76
340
 
77
341
  @schema = block_given? ? block : value
78
342
  end
79
343
 
344
+ # Sets fallback models for chats this agent builds, applied via
345
+ # Chat#with_fallbacks. Called with no arguments, returns the
346
+ # configured models.
347
+ #
348
+ # fallbacks "gpt-4.1-mini", "claude-haiku-4-5"
349
+ # fallbacks "gpt-4.1-mini", on: [RubyLLM::RateLimitError]
350
+ #
351
+ def fallbacks(*models, **options)
352
+ return @fallbacks || [] if models.empty? && options.empty?
353
+ raise ArgumentError, 'To set fallback options, provide at least one fallback model' if models.empty?
354
+
355
+ @fallbacks = models.flatten.compact
356
+ @fallback_options = options
357
+ end
358
+
359
+ def fallback_options
360
+ @fallback_options || {}
361
+ end
362
+
363
+ private :fallback_options
364
+
365
+ # Sets a Context whose configuration chats this agent builds should
366
+ # use, applied via Chat#with_context. Called with no argument, returns
367
+ # the configured context.
80
368
  def context(value = nil)
81
369
  return @context if value.nil?
82
370
 
83
371
  @context = value
84
372
  end
85
373
 
374
+ # Sets the ActiveRecord chat class this agent creates and finds,
375
+ # activating Rails mode (::create, ::create!, ::find, and
376
+ # ::sync_instructions). Accepts the class or its name as a string.
377
+ # Called with no argument, returns the configured value.
378
+ #
379
+ # chat_model Chat
380
+ #
86
381
  def chat_model(value = nil)
87
382
  return @chat_model if value.nil?
88
383
 
@@ -90,31 +385,126 @@ module RubyLLM
90
385
  remove_instance_variable(:@resolved_chat_model) if instance_variable_defined?(:@resolved_chat_model)
91
386
  end
92
387
 
388
+ # Declares named runtime inputs. Matching keyword arguments passed to
389
+ # ::chat, ::create, ::create!, ::find, or ::new become methods inside
390
+ # lazy configuration blocks. Called with no arguments, returns the
391
+ # declared names.
392
+ #
393
+ # inputs :workspace
394
+ #
93
395
  def inputs(*names)
94
396
  return @input_names || [] if names.empty?
95
397
 
96
398
  @input_names = names.flatten.map(&:to_sym)
97
399
  end
98
400
 
99
- def chat_kwargs
401
+ # Registers a handler for exceptions raised by the chat operations of
402
+ # this agent's instances: #ask, #say, #ask_later, #complete,
403
+ # #generate, #run_tools, #step, #count_tokens, and #compact. Name the
404
+ # handler with +with:+ or pass a block; either runs on the agent instance,
405
+ # so the agent's #chat, inputs, and class name are available for instrumentation.
406
+ #
407
+ # class ApplicationAgent < RubyLLM::Agent
408
+ # rescue_from RubyLLM::RateLimitError, Faraday::TimeoutError, with: :handle_transient
409
+ # rescue_from RubyLLM::BadRequestError do |error|
410
+ # error_tracker.notify(error)
411
+ # raise
412
+ # end
413
+ #
414
+ # private
415
+ #
416
+ # def handle_transient(error)
417
+ # metrics.increment("llm.api_error", type: "transient")
418
+ # raise
419
+ # end
420
+ # end
421
+ #
422
+ # Handlers are searched in reverse declaration order, so the last
423
+ # matching one wins. Re-raise inside a handler to let the caller see
424
+ # the exception; otherwise the handler's return value becomes the
425
+ # operation's return value. Exceptions no handler matches are
426
+ # re-raised. Subclasses inherit the handlers declared when they are
427
+ # defined.
428
+ #
429
+ # Exception classes may be named as Strings, which defers constant
430
+ # lookup until an exception is raised.
431
+ def rescue_from(*exception_classes, with: nil, &block)
432
+ raise ArgumentError, 'rescue_from needs a handler: pass with: or a block' unless with || block
433
+ raise ArgumentError, 'rescue_from takes with: or a block, not both' if with && block
434
+
435
+ exception_classes.flatten.each do |exception_class|
436
+ rescue_handlers << [rescue_handler_key(exception_class), with || block]
437
+ end
438
+ end
439
+
440
+ def rescue_handlers # :nodoc:
441
+ @rescue_handlers ||= []
442
+ end
443
+
444
+ def rescue_handler_for(exception) # :nodoc:
445
+ rescue_handlers.reverse_each do |class_name, handler|
446
+ exception_class = rescue_handler_class(class_name)
447
+ return handler if exception_class && exception.is_a?(exception_class)
448
+ end
449
+ nil
450
+ end
451
+
452
+ def chat_kwargs # :nodoc:
100
453
  @chat_kwargs || {}
101
454
  end
102
455
 
456
+ def resolved_chat_kwargs(inputs: {}) # :nodoc:
457
+ kwargs = chat_kwargs
458
+ return kwargs unless kwargs[:model].is_a?(Proc)
459
+
460
+ kwargs.merge(model: evaluate(kwargs[:model], runtime_context(chat: nil, inputs: inputs)))
461
+ end
462
+
463
+ def build_chat(inputs:, options:) # :nodoc:
464
+ (context || RubyLLM).chat(**resolved_chat_kwargs(inputs:), **options)
465
+ end
466
+
467
+ # Builds a Chat configured with this agent's declarations and returns
468
+ # it. Keywords matching declared ::inputs become runtime inputs; the
469
+ # rest are forwarded to RubyLLM.chat.
470
+ #
471
+ # chat = WorkAssistant.chat
472
+ # chat.ask "Hello"
473
+ #
103
474
  def chat(**kwargs)
104
475
  input_values, chat_options = partition_inputs(kwargs)
105
- chat = RubyLLM.chat(**chat_kwargs, **chat_options)
476
+ chat = build_chat(inputs: input_values, options: chat_options)
106
477
  apply_configuration(chat, input_values:, persist_instructions: true)
107
478
  chat
108
479
  end
109
480
 
481
+ # Creates a ::chat_model record, applies this agent's configuration to
482
+ # it, and returns it. Keywords matching declared ::inputs become
483
+ # runtime inputs; the rest are forwarded to the model's +create+.
484
+ #
485
+ # chat = WorkAssistant.create(user: current_user)
486
+ #
487
+ # Raises ArgumentError if ::chat_model is not configured.
110
488
  def create(**kwargs)
111
489
  with_rails_chat_record(:create, **kwargs)
112
490
  end
113
491
 
492
+ # Like ::create, but calls the model's <tt>create!</tt>, raising if
493
+ # the record is invalid.
494
+ #
495
+ # chat = WorkAssistant.create!(user: current_user)
496
+ #
114
497
  def create!(**kwargs)
115
498
  with_rails_chat_record(:create!, **kwargs)
116
499
  end
117
500
 
501
+ # Finds the ::chat_model record with +id+ and applies this agent's
502
+ # configuration at runtime, without persisting instructions. Returns
503
+ # the record.
504
+ #
505
+ # chat = WorkAssistant.find(params[:id])
506
+ #
507
+ # Raises ArgumentError if ::chat_model is not configured.
118
508
  def find(id, **kwargs)
119
509
  raise ArgumentError, 'chat_model must be configured to use find' unless resolved_chat_model
120
510
 
@@ -125,113 +515,225 @@ module RubyLLM
125
515
  record
126
516
  end
127
517
 
128
- # Mutates persisted instructions on the configured chat record.
129
- def sync_instructions!(chat_or_id, **kwargs)
130
- raise ArgumentError, 'chat_model must be configured to use sync_instructions!' unless resolved_chat_model
518
+ # Re-renders this agent's instructions and persists them on the given
519
+ # ::chat_model record (or the record found by that id). Keywords
520
+ # matching declared ::inputs become runtime inputs. Returns the
521
+ # record.
522
+ #
523
+ # WorkAssistant.sync_instructions(chat)
524
+ #
525
+ # Raises ArgumentError if ::chat_model is not configured.
526
+ def sync_instructions(chat_or_id, **kwargs)
527
+ raise ArgumentError, 'chat_model must be configured to use sync_instructions' unless resolved_chat_model
131
528
 
132
529
  input_values, = partition_inputs(kwargs)
133
530
  record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
134
531
  apply_assume_model_exists(record)
532
+ apply_protocol(record)
533
+ apply_context(record)
135
534
  runtime = runtime_context(chat: record, inputs: input_values)
136
- instructions_value = resolved_instructions_value(record, runtime, inputs: input_values)
137
- return record if instructions_value.nil?
138
-
139
- record.with_instructions(instructions_value)
535
+ apply_instructions(
536
+ record,
537
+ runtime,
538
+ inputs: input_values,
539
+ persist: true,
540
+ persistent_only: true
541
+ )
140
542
  record
141
543
  end
142
544
 
143
- def render_prompt(name, chat:, inputs:, locals:)
144
- path = prompt_path_for(name)
145
- unless File.exist?(path)
146
- raise RubyLLM::PromptNotFoundError,
147
- "Prompt file not found for #{self}: #{path}. Create the file or use inline instructions."
545
+ def render_prompt(name, chat:, inputs:, locals:) # :nodoc:
546
+ resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
547
+ RubyLLM.render_prompt("#{prompt_agent_path}/#{name}", **resolved_locals)
548
+ end
549
+
550
+ def partition_inputs(kwargs) # :nodoc:
551
+ input_values = {}
552
+ chat_options = {}
553
+
554
+ kwargs.each do |key, value|
555
+ symbolized_key = key.to_sym
556
+ if inputs.include?(symbolized_key)
557
+ input_values[symbolized_key] = value
558
+ else
559
+ chat_options[symbolized_key] = value
560
+ end
148
561
  end
149
562
 
150
- resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
151
- ERB.new(File.read(path)).result_with_hash(resolved_locals)
563
+ [input_values, chat_options]
564
+ end
565
+
566
+ def apply_configuration(chat, input_values:, persist_instructions:) # :nodoc:
567
+ runtime = runtime_context(chat:, inputs: input_values)
568
+ apply_chat_options(chat)
569
+ apply_context(chat)
570
+ apply_instructions(chat, runtime, inputs: input_values, persist: persist_instructions)
571
+ apply_tools(chat, runtime)
572
+ apply_passthrough_options(chat)
573
+ apply_thinking(chat)
574
+ apply_citations(chat)
575
+ apply_end_user(chat, runtime)
576
+ apply_caching(chat, runtime)
577
+ apply_compaction(chat)
578
+ apply_provider_options(chat, runtime)
579
+ apply_headers(chat, runtime)
580
+ apply_schema(chat, runtime)
581
+ apply_fallbacks(chat)
152
582
  end
153
583
 
154
584
  private
155
585
 
586
+ def validate_thinking_options(enabled, options)
587
+ raise ArgumentError, 'thinking accepts false or thinking options' unless [true, false].include?(enabled)
588
+ raise ArgumentError, 'thinking false does not accept options' if !enabled && options.any?
589
+ raise ArgumentError, 'thinking options cannot be nil; use thinking false to disable' if options.value?(nil)
590
+
591
+ unsupported = options.keys - THINKING_OPTIONS
592
+ return if unsupported.empty?
593
+
594
+ raise ArgumentError, "thinking accepts #{THINKING_OPTIONS.join(', ')}, got #{unsupported.join(', ')}"
595
+ end
596
+
597
+ def rescue_handler_key(exception_class)
598
+ case exception_class
599
+ when Module then exception_class.name || exception_class
600
+ when String then exception_class
601
+ else raise ArgumentError, "#{exception_class.inspect} is not an exception class or its name"
602
+ end
603
+ end
604
+
605
+ def rescue_handler_class(class_name)
606
+ return class_name if class_name.is_a?(Module)
607
+
608
+ Object.const_get(class_name)
609
+ rescue NameError
610
+ nil
611
+ end
612
+
613
+ def copy_inherited_config_to(subclass)
614
+ DUPED_INHERITED_CONFIG.each do |ivar, default|
615
+ value = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : default
616
+ subclass.instance_variable_set(ivar, value.respond_to?(:dup) ? value.dup : value)
617
+ end
618
+
619
+ COPIED_INHERITED_CONFIG.each do |ivar|
620
+ subclass.instance_variable_set(ivar, instance_variable_get(ivar))
621
+ end
622
+ end
623
+
156
624
  def with_rails_chat_record(method_name, **kwargs)
157
625
  raise ArgumentError, 'chat_model must be configured to use create/create!' unless resolved_chat_model
158
626
 
159
627
  input_values, chat_options = partition_inputs(kwargs)
160
- record = resolved_chat_model.public_send(method_name, **chat_kwargs, **chat_options)
628
+ record = resolved_chat_model.public_send(
629
+ method_name, **resolved_chat_kwargs(inputs: input_values), **chat_options
630
+ )
161
631
  apply_configuration(record, input_values:, persist_instructions: true) if record
162
632
  record
163
633
  end
164
634
 
165
- def apply_configuration(chat_object, input_values:, persist_instructions:)
166
- runtime = runtime_context(chat: chat_object, inputs: input_values)
167
- llm_chat = llm_chat_for(chat_object)
635
+ def apply_context(chat)
636
+ chat.with_context(context) if context
637
+ end
638
+
639
+ def apply_instructions(chat, runtime, inputs:, persist:, persistent_only: false)
640
+ instructions_config.each do |declaration|
641
+ next if persistent_only && !declaration[:persist]
168
642
 
169
- apply_context(llm_chat)
170
- apply_instructions(chat_object, runtime, inputs: input_values, persist: persist_instructions)
171
- apply_tools(llm_chat, runtime)
172
- apply_temperature(llm_chat)
173
- apply_thinking(llm_chat)
174
- apply_params(llm_chat, runtime)
175
- apply_headers(llm_chat, runtime)
176
- apply_schema(llm_chat, runtime)
643
+ value = resolved_instruction_value(declaration, chat, runtime, inputs:)
644
+ next if blank_instruction?(value)
645
+
646
+ options = {
647
+ append: declaration[:append],
648
+ cache_until_here: declaration[:cache_until_here]
649
+ }
650
+ options[:persist] = persist && declaration[:persist] if rails_chat_record?(chat)
651
+ chat.with_instructions(value, **options)
652
+ end
177
653
  end
178
654
 
179
- def apply_context(llm_chat)
180
- llm_chat.with_context(context) if context
655
+ # An empty prompt file, such as the one the agent generator writes,
656
+ # means no instructions rather than an empty system message.
657
+ def blank_instruction?(value)
658
+ value.nil? || (value.respond_to?(:strip) && value.strip.empty?)
181
659
  end
182
660
 
183
- def apply_instructions(chat_object, runtime, inputs:, persist:)
184
- value = resolved_instructions_value(chat_object, runtime, inputs:)
185
- return if value.nil?
661
+ def apply_tools(chat, runtime)
662
+ tools_to_apply = Array(evaluate(tools, runtime)).compact
663
+ chat.with_tools(*tools_to_apply) if tools_to_apply.any?
186
664
 
187
- target = instruction_target(chat_object, persist:)
188
- return target.with_runtime_instructions(value) if use_runtime_instructions?(target, persist:)
665
+ options = evaluate(tool_options, runtime)
666
+ chat.with_tool_options(**options) if options && !options.empty?
189
667
 
190
- target.with_instructions(value)
668
+ server_tools_to_apply = Array(evaluate(server_tools, runtime)).compact
669
+ chat.with_server_tools(*server_tools_to_apply) if server_tools_to_apply.any?
191
670
  end
192
671
 
193
- def apply_tools(llm_chat, runtime)
194
- tools_to_apply = Array(evaluate(tools, runtime))
195
- llm_chat.with_tools(*tools_to_apply) unless tools_to_apply.empty?
672
+ def apply_passthrough_options(chat)
673
+ PASSTHROUGH_OPTIONS.each do |option|
674
+ value = instance_variable_get(:"@#{option}")
675
+ chat.public_send(:"with_#{option}", value) unless value.nil?
676
+ end
196
677
  end
197
678
 
198
- def apply_temperature(llm_chat)
199
- llm_chat.with_temperature(temperature) unless temperature.nil?
679
+ def apply_thinking(chat)
680
+ return if @thinking.nil?
681
+
682
+ @thinking == false ? chat.with_thinking(false) : chat.with_thinking(**@thinking)
200
683
  end
201
684
 
202
- def apply_thinking(llm_chat)
203
- llm_chat.with_thinking(**thinking) if thinking
685
+ def apply_citations(chat)
686
+ chat.with_citations(@citations) unless @citations.nil?
204
687
  end
205
688
 
206
- def apply_params(llm_chat, runtime)
207
- value = evaluate(params, runtime)
208
- llm_chat.with_params(**value) if value && !value.empty?
689
+ def apply_end_user(chat, runtime)
690
+ value = evaluate(end_user, runtime)
691
+ chat.with_end_user(value) unless value.nil?
209
692
  end
210
693
 
211
- def apply_headers(llm_chat, runtime)
694
+ def apply_caching(chat, runtime)
695
+ value = evaluate(@caching, runtime)
696
+ return if value.nil?
697
+
698
+ value == false ? chat.with_caching(false) : chat.with_caching(**value)
699
+ end
700
+
701
+ def apply_compaction(chat)
702
+ return if @compaction.nil?
703
+
704
+ @compaction == false ? chat.with_compaction(false) : chat.with_compaction(**@compaction)
705
+ end
706
+
707
+ def apply_provider_options(chat, runtime)
708
+ value = evaluate(provider_options, runtime)
709
+ chat.with_provider_options(**value) if value && !value.empty?
710
+ end
711
+
712
+ def apply_headers(chat, runtime)
212
713
  value = evaluate(headers, runtime)
213
- llm_chat.with_headers(**value) if value && !value.empty?
714
+ chat.with_headers(**value) if value && !value.empty?
214
715
  end
215
716
 
216
- def apply_schema(llm_chat, runtime)
717
+ def apply_schema(chat, runtime)
217
718
  value = resolved_schema_value(runtime)
218
- llm_chat.with_schema(value) if value
719
+ chat.with_schema(value) if value
720
+ end
721
+
722
+ def apply_fallbacks(chat)
723
+ chat.with_fallbacks(*fallbacks, **fallback_options) if fallbacks.any?
219
724
  end
220
725
 
221
726
  def resolved_schema_value(runtime)
222
727
  value = schema
223
728
  return value unless value.is_a?(Proc)
729
+ return evaluate(value, runtime) if value.lambda?
224
730
 
225
- evaluate(value, runtime)
226
- rescue NoMethodError => e
227
- raise unless e.receiver.equal?(runtime)
228
-
229
- RubyLLM::Schema.create(&value)
731
+ Schematist::Schema.create(&value)
230
732
  end
231
733
 
232
- def llm_chat_for(chat_object)
233
- apply_assume_model_exists(chat_object)
234
- chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
734
+ def apply_chat_options(chat)
735
+ apply_assume_model_exists(chat)
736
+ apply_protocol(chat)
235
737
  end
236
738
 
237
739
  def apply_assume_model_exists(chat_object)
@@ -242,12 +744,20 @@ module RubyLLM
242
744
  chat_object.assume_model_exists = chat_kwargs[:assume_model_exists]
243
745
  end
244
746
 
747
+ def apply_protocol(chat_object)
748
+ return unless chat_kwargs.key?(:protocol) &&
749
+ resolved_chat_model &&
750
+ chat_object.is_a?(resolved_chat_model)
751
+
752
+ chat_object.protocol = chat_kwargs[:protocol]
753
+ end
754
+
245
755
  def evaluate(value, runtime)
246
756
  value.is_a?(Proc) ? runtime.instance_exec(&value) : value
247
757
  end
248
758
 
249
- def resolved_instructions_value(chat_object, runtime, inputs:)
250
- value = evaluate(@instructions, runtime)
759
+ def resolved_instruction_value(declaration, chat_object, runtime, inputs:)
760
+ value = evaluate(declaration[:value], runtime)
251
761
  return value unless prompt_instruction?(value)
252
762
 
253
763
  runtime.prompt(
@@ -256,26 +766,32 @@ module RubyLLM
256
766
  )
257
767
  end
258
768
 
259
- def prompt_instruction?(value)
260
- value.is_a?(Hash) && value[:prompt]
769
+ def instructions_config
770
+ return instruction_declarations if instruction_declarations.any?
771
+ return [] unless default_instructions_prompt_exists?
772
+
773
+ [{
774
+ value: { prompt: 'instructions', locals: {} },
775
+ append: false,
776
+ persist: true,
777
+ cache_until_here: false
778
+ }]
261
779
  end
262
780
 
263
- def instruction_target(chat_object, persist:)
264
- if persist || !chat_object.respond_to?(:to_llm)
265
- chat_object
266
- else
267
- runtime_instruction_target(chat_object)
268
- end
781
+ def instruction_declarations
782
+ @instruction_declarations ||= []
269
783
  end
270
784
 
271
- def runtime_instruction_target(chat_object)
272
- return chat_object if chat_object.respond_to?(:with_runtime_instructions)
785
+ def rails_chat_record?(chat)
786
+ resolved_chat_model && chat.is_a?(resolved_chat_model)
787
+ end
273
788
 
274
- chat_object.to_llm
789
+ def default_instructions_prompt_exists?
790
+ name && File.exist?(Prompt.new("#{prompt_agent_path}/instructions").path)
275
791
  end
276
792
 
277
- def use_runtime_instructions?(target, persist:)
278
- !persist && target.respond_to?(:with_runtime_instructions)
793
+ def prompt_instruction?(value)
794
+ value.is_a?(Hash) && value[:prompt]
279
795
  end
280
796
 
281
797
  def resolve_prompt_locals(locals, runtime:, chat:, inputs:)
@@ -286,22 +802,6 @@ module RubyLLM
286
802
  base.merge(evaluated)
287
803
  end
288
804
 
289
- def partition_inputs(kwargs)
290
- input_values = {}
291
- chat_options = {}
292
-
293
- kwargs.each do |key, value|
294
- symbolized_key = key.to_sym
295
- if inputs.include?(symbolized_key)
296
- input_values[symbolized_key] = value
297
- else
298
- chat_options[symbolized_key] = value
299
- end
300
- end
301
-
302
- [input_values, chat_options]
303
- end
304
-
305
805
  def runtime_context(chat:, inputs:)
306
806
  agent_class = self
307
807
  Object.new.tap do |runtime|
@@ -316,27 +816,9 @@ module RubyLLM
316
816
  end
317
817
  end
318
818
 
319
- def prompt_path_for(name)
320
- filename = name.to_s
321
- filename += '.txt.erb' unless filename.end_with?('.txt.erb')
322
- prompt_root.join(prompt_agent_path, filename)
323
- end
324
-
325
819
  def prompt_agent_path
326
820
  class_name = name || 'agent'
327
- class_name.gsub('::', '/')
328
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
329
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
330
- .tr('-', '_')
331
- .downcase
332
- end
333
-
334
- def prompt_root
335
- if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
336
- Rails.root.join('app/prompts')
337
- else
338
- Pathname.new(Dir.pwd).join('app/prompts')
339
- end
821
+ Support::Utils.underscore(class_name.gsub('::', '/')).tr('-', '_')
340
822
  end
341
823
 
342
824
  def resolved_chat_model
@@ -349,19 +831,451 @@ module RubyLLM
349
831
  end
350
832
  end
351
833
 
834
+ # Returns a new agent wrapping +chat:+, or wrapping a newly built chat
835
+ # when +chat:+ is +nil+. Applies the agent's configuration either way.
836
+ # Keywords matching declared inputs (and the +inputs:+ hash) become
837
+ # runtime inputs; the rest are forwarded to RubyLLM.chat when the agent
838
+ # builds its own chat. Pass <tt>persist_instructions: false</tt> to
839
+ # apply instructions at runtime only, without persisting them on a
840
+ # Rails-backed record.
841
+ #
842
+ # agent = WorkAssistant.new
843
+ # agent.ask "Hello"
844
+ #
845
+ # record = Chat.find(params[:id])
846
+ # WorkAssistant.new(chat: record)
847
+ #
352
848
  def initialize(chat: nil, inputs: nil, persist_instructions: true, **kwargs)
353
- input_values, chat_options = self.class.send(:partition_inputs, kwargs)
354
- @chat = chat || RubyLLM.chat(**self.class.chat_kwargs, **chat_options)
355
- self.class.send(:apply_configuration, @chat, input_values: input_values.merge(inputs || {}),
356
- persist_instructions:)
849
+ input_values, chat_options = self.class.partition_inputs(kwargs)
850
+ input_values = input_values.merge(inputs || {})
851
+ @chat = chat || self.class.build_chat(inputs: input_values, options: chat_options)
852
+ self.class.apply_configuration(@chat, input_values:, persist_instructions:)
357
853
  end
358
854
 
855
+ # The wrapped Chat, or the chat record in Rails mode.
359
856
  attr_reader :chat
360
857
 
361
- def_delegators :chat, :model, :messages, :tools, :params, :headers, :schema, :ask, :say, :with_tool, :with_tools,
362
- :with_model, :with_temperature, :with_thinking, :with_context, :with_params, :with_headers,
363
- :with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :before_message,
364
- :after_message, :before_tool_call, :after_tool_result, :each, :complete, :add_message,
365
- :reset_messages!, :cost
858
+ # Agent instances delegate the Chat conversation API to the wrapped
859
+ # #chat without adapting its return values.
860
+ def_delegators :chat, *CHAINABLE_CHAT_DELEGATES, *PASSTHROUGH_CHAT_DELEGATES
861
+
862
+ ##
863
+ # :method: with_instructions
864
+ # :call-seq: with_instructions(instructions, append: false, cache_until_here: false)
865
+ #
866
+ # Delegates to Chat#with_instructions. See that method for arguments and return values.
867
+
868
+ ##
869
+ # :method: with_tools
870
+ # :call-seq: with_tools(*tools)
871
+ #
872
+ # Delegates to Chat#with_tools. See that method for arguments and return values.
873
+
874
+ ##
875
+ # :method: with_server_tools
876
+ # :call-seq: with_server_tools(*tools, **tools_with_options)
877
+ #
878
+ # Delegates to Chat#with_server_tools. See that method for arguments and return values.
879
+
880
+ ##
881
+ # :method: with_tool_options
882
+ # :call-seq: with_tool_options(**options)
883
+ #
884
+ # Delegates to Chat#with_tool_options. See that method for arguments and return values.
885
+
886
+ ##
887
+ # :method: with_model
888
+ # :call-seq: with_model(model_id, provider: nil, protocol: nil, assume_model_exists: false)
889
+ #
890
+ # Delegates to Chat#with_model. See that method for arguments and return values.
891
+
892
+ ##
893
+ # :method: with_temperature
894
+ # :call-seq: with_temperature(temperature)
895
+ #
896
+ # Delegates to Chat#with_temperature. See that method for arguments and return values.
897
+
898
+ ##
899
+ # :method: with_max_output_tokens
900
+ # :call-seq: with_max_output_tokens(max_output_tokens)
901
+ #
902
+ # Delegates to Chat#with_max_output_tokens. See that method for arguments and return values.
903
+
904
+ ##
905
+ # :method: with_thinking
906
+ # :call-seq: with_thinking(enabled = true, **options)
907
+ #
908
+ # Delegates to Chat#with_thinking. See that method for arguments and return values.
909
+
910
+ ##
911
+ # :method: with_citations
912
+ # :call-seq: with_citations(enabled = true)
913
+ #
914
+ # Delegates to Chat#with_citations. See that method for arguments and return values.
915
+
916
+ ##
917
+ # :method: with_end_user
918
+ # :call-seq: with_end_user(end_user)
919
+ #
920
+ # Delegates to Chat#with_end_user. See that method for arguments and return values.
921
+
922
+ ##
923
+ # :method: with_compaction
924
+ # :call-seq: with_compaction(options = {})
925
+ #
926
+ # Delegates to Chat#with_compaction. See that method for arguments and return values.
927
+
928
+ ##
929
+ # :method: with_caching
930
+ # :call-seq: with_caching(options = {})
931
+ #
932
+ # Delegates to Chat#with_caching. See that method for arguments and return values.
933
+
934
+ ##
935
+ # :method: with_context
936
+ # :call-seq: with_context(context)
937
+ #
938
+ # Delegates to Chat#with_context. See that method for arguments and return values.
939
+
940
+ ##
941
+ # :method: with_provider_options
942
+ # :call-seq: with_provider_options(provider_options)
943
+ #
944
+ # Delegates to Chat#with_provider_options. See that method for arguments and return values.
945
+
946
+ ##
947
+ # :method: with_headers
948
+ # :call-seq: with_headers(headers)
949
+ #
950
+ # Delegates to Chat#with_headers. See that method for arguments and return values.
951
+
952
+ ##
953
+ # :method: with_schema
954
+ # :call-seq: with_schema(schema)
955
+ #
956
+ # Delegates to Chat#with_schema. See that method for arguments and return values.
957
+
958
+ ##
959
+ # :method: with_fallbacks
960
+ # :call-seq: with_fallbacks(*models, on: Fallback::DEFAULT_ERRORS)
961
+ #
962
+ # Delegates to Chat#with_fallbacks. See that method for arguments and return values.
963
+
964
+ ##
965
+ # :method: before_request
966
+ # :call-seq: before_request(&block)
967
+ #
968
+ # Delegates to Chat#before_request. See that method for arguments and return values.
969
+
970
+ ##
971
+ # :method: before_message
972
+ # :call-seq: before_message(&block)
973
+ #
974
+ # Delegates to Chat#before_message. See that method for arguments and return values.
975
+
976
+ ##
977
+ # :method: after_message
978
+ # :call-seq: after_message(&block)
979
+ #
980
+ # Delegates to Chat#after_message. See that method for arguments and return values.
981
+
982
+ ##
983
+ # :method: before_tool_call
984
+ # :call-seq: before_tool_call(&block)
985
+ #
986
+ # Delegates to Chat#before_tool_call. See that method for arguments and return values.
987
+
988
+ ##
989
+ # :method: after_tool_result
990
+ # :call-seq: after_tool_result(&block)
991
+ #
992
+ # Delegates to Chat#after_tool_result. See that method for arguments and return values.
993
+
994
+ ##
995
+ # :method: before_fallback
996
+ # :call-seq: before_fallback(&block)
997
+ #
998
+ # Delegates to Chat#before_fallback. See that method for arguments and return values.
999
+
1000
+ ##
1001
+ # :method: after_fallback
1002
+ # :call-seq: after_fallback(&block)
1003
+ #
1004
+ # Delegates to Chat#after_fallback. See that method for arguments and return values.
1005
+
1006
+ ##
1007
+ # :method: cancel
1008
+ # :call-seq: cancel()
1009
+ #
1010
+ # Delegates to Chat#cancel. See that method for arguments and return values.
1011
+
1012
+ ##
1013
+ # :method: approve
1014
+ # :call-seq: approve(tool_call)
1015
+ #
1016
+ # Delegates to Chat#approve. See that method for arguments and return values.
1017
+
1018
+ ##
1019
+ # :method: deny
1020
+ # :call-seq: deny(tool_call)
1021
+ #
1022
+ # Delegates to Chat#deny. See that method for arguments and return values.
1023
+
1024
+ ##
1025
+ # :method: cache_until_here
1026
+ # :call-seq: cache_until_here()
1027
+ #
1028
+ # Delegates to Chat#cache_until_here. See that method for arguments and return values.
1029
+
1030
+ ##
1031
+ # :method: model
1032
+ # :call-seq: model
1033
+ #
1034
+ # Delegates to Chat#model. See that method for arguments and return values.
1035
+
1036
+ ##
1037
+ # :method: provider
1038
+ # :call-seq: provider
1039
+ #
1040
+ # Delegates to Chat#provider. See that method for arguments and return values.
1041
+
1042
+ ##
1043
+ # :method: messages
1044
+ # :call-seq: messages
1045
+ #
1046
+ # Delegates to Chat#messages. See that method for arguments and return values.
1047
+
1048
+ ##
1049
+ # :method: tools
1050
+ # :call-seq: tools
1051
+ #
1052
+ # Delegates to Chat#tools. See that method for arguments and return values.
1053
+
1054
+ ##
1055
+ # :method: server_tools
1056
+ # :call-seq: server_tools
1057
+ #
1058
+ # Delegates to Chat#server_tools. See that method for arguments and return values.
1059
+
1060
+ ##
1061
+ # :method: tool_options
1062
+ # :call-seq: tool_options()
1063
+ #
1064
+ # Delegates to Chat#tool_options. See that method for arguments and return values.
1065
+
1066
+ ##
1067
+ # :method: provider_options
1068
+ # :call-seq: provider_options
1069
+ #
1070
+ # Delegates to Chat#provider_options. See that method for arguments and return values.
1071
+
1072
+ ##
1073
+ # :method: headers
1074
+ # :call-seq: headers
1075
+ #
1076
+ # Delegates to Chat#headers. See that method for arguments and return values.
1077
+
1078
+ ##
1079
+ # :method: schema
1080
+ # :call-seq: schema
1081
+ #
1082
+ # Delegates to Chat#schema. See that method for arguments and return values.
1083
+
1084
+ ##
1085
+ # :method: concurrency
1086
+ # :call-seq: concurrency
1087
+ #
1088
+ # Delegates to Chat#concurrency. See that method for arguments and return values.
1089
+
1090
+ ##
1091
+ # :method: caching
1092
+ # :call-seq: caching
1093
+ #
1094
+ # Delegates to Chat#caching. See that method for arguments and return values.
1095
+
1096
+ ##
1097
+ # :method: citations
1098
+ # :call-seq: citations
1099
+ #
1100
+ # Delegates to Chat#citations. See that method for arguments and return values.
1101
+
1102
+ ##
1103
+ # :method: compaction
1104
+ # :call-seq: compaction
1105
+ #
1106
+ # Delegates to Chat#compaction. See that method for arguments and return values.
1107
+
1108
+ ##
1109
+ # :method: context
1110
+ # :call-seq: context
1111
+ #
1112
+ # Delegates to Chat#context. See that method for arguments and return values.
1113
+
1114
+ ##
1115
+ # :method: end_user
1116
+ # :call-seq: end_user
1117
+ #
1118
+ # Delegates to Chat#end_user. See that method for arguments and return values.
1119
+
1120
+ ##
1121
+ # :method: fallbacks
1122
+ # :call-seq: fallbacks
1123
+ #
1124
+ # Delegates to Chat#fallbacks. See that method for arguments and return values.
1125
+
1126
+ ##
1127
+ # :method: thinking
1128
+ # :call-seq: thinking()
1129
+ #
1130
+ # Delegates to Chat#thinking. See that method for arguments and return values.
1131
+
1132
+ ##
1133
+ # :method: temperature
1134
+ # :call-seq: temperature
1135
+ #
1136
+ # Returns Chat#temperature from the wrapped chat.
1137
+
1138
+ ##
1139
+ # :method: max_output_tokens
1140
+ # :call-seq: max_output_tokens
1141
+ #
1142
+ # Returns Chat#max_output_tokens from the wrapped chat.
1143
+
1144
+ ##
1145
+ # :method: each
1146
+ # :call-seq: each(&block)
1147
+ #
1148
+ # Delegates to Chat#each. See that method for arguments and return values.
1149
+
1150
+ ##
1151
+ # :method: complete?
1152
+ # :call-seq: complete?()
1153
+ #
1154
+ # Delegates to Chat#complete?. See that method for arguments and return values.
1155
+
1156
+ ##
1157
+ # :method: cancelled?
1158
+ # :call-seq: cancelled?()
1159
+ #
1160
+ # Delegates to Chat#cancelled?. See that method for arguments and return values.
1161
+
1162
+ ##
1163
+ # :method: awaiting_approval?
1164
+ # :call-seq: awaiting_approval?()
1165
+ #
1166
+ # Delegates to Chat#awaiting_approval?. See that method for arguments and return values.
1167
+
1168
+ ##
1169
+ # :method: pending_approvals
1170
+ # :call-seq: pending_approvals()
1171
+ #
1172
+ # Delegates to Chat#pending_approvals. See that method for arguments and return values.
1173
+
1174
+ ##
1175
+ # :method: add_message
1176
+ # :call-seq: add_message(message_or_attributes)
1177
+ #
1178
+ # Delegates to Chat#add_message. See that method for arguments and return values.
1179
+
1180
+ ##
1181
+ # :method: tokens
1182
+ # :call-seq: tokens()
1183
+ #
1184
+ # Delegates to Chat#tokens. See that method for arguments and return values.
1185
+
1186
+ ##
1187
+ # :method: cost
1188
+ # :call-seq: cost()
1189
+ #
1190
+ # Delegates to Chat#cost. See that method for arguments and return values.
1191
+
1192
+ ##
1193
+ # :method: render
1194
+ # :call-seq: render()
1195
+ #
1196
+ # Delegates to Chat#render. See that method for arguments and return values.
1197
+
1198
+ ##
1199
+ # :method: ask
1200
+ # :call-seq: ask(message = nil, with: nil, &block)
1201
+ #
1202
+ # Delegates to Chat#ask, routing exceptions through the handlers
1203
+ # declared with ::rescue_from.
1204
+
1205
+ ##
1206
+ # :method: complete
1207
+ # :call-seq: complete(&block)
1208
+ #
1209
+ # Delegates to Chat#complete, routing exceptions through the handlers
1210
+ # declared with ::rescue_from.
1211
+
1212
+ ##
1213
+ # :method: say
1214
+ # :call-seq: say(message = nil, with: nil, &block)
1215
+ #
1216
+ # Delegates to Chat#say, the alias for Chat#ask, with ::rescue_from handling.
1217
+
1218
+ ##
1219
+ # :method: ask_later
1220
+ # :call-seq: ask_later(message = nil, with: nil)
1221
+ #
1222
+ # Stages a message through Chat#ask_later without calling the provider.
1223
+ # Returns the wrapped chat. Exceptions use ::rescue_from handlers.
1224
+
1225
+ ##
1226
+ # :method: generate
1227
+ # :call-seq: generate(&block)
1228
+ #
1229
+ # Generates one response through Chat#generate without executing tools.
1230
+ # Returns a Message. Exceptions use ::rescue_from handlers.
1231
+
1232
+ ##
1233
+ # :method: run_tools
1234
+ # :call-seq: run_tools
1235
+ #
1236
+ # Runs pending tools through Chat#run_tools, respecting approval decisions.
1237
+ # Returns the wrapped chat. Exceptions use ::rescue_from handlers.
1238
+
1239
+ ##
1240
+ # :method: step
1241
+ # :call-seq: step(&block)
1242
+ #
1243
+ # Advances the conversation through Chat#step. Returns a Message or
1244
+ # +nil+ when no progress is possible. Exceptions use ::rescue_from handlers.
1245
+
1246
+ ##
1247
+ # :method: count_tokens
1248
+ # :call-seq: count_tokens(message = nil)
1249
+ #
1250
+ # Counts request tokens through Chat#count_tokens without generating a
1251
+ # response. Returns an Integer. Exceptions use ::rescue_from handlers.
1252
+
1253
+ ##
1254
+ # :method: compact
1255
+ # :call-seq: compact
1256
+ #
1257
+ # Compacts the model context through Chat#compact and returns its Message.
1258
+ # Exceptions use ::rescue_from handlers.
1259
+
1260
+ GUARDED_OPERATIONS.each do |operation|
1261
+ define_method(operation) do |*args, **kwargs, &block|
1262
+ chat.public_send(operation, *args, **kwargs, &block)
1263
+ rescue StandardError => e
1264
+ rescue_with_handler(e)
1265
+ end
1266
+ end
1267
+
1268
+ # Runs the ::rescue_from handler matching +exception+ and returns its
1269
+ # value, or re-raises when no handler matches. The chat operations call
1270
+ # this for you.
1271
+ def rescue_with_handler(exception)
1272
+ handler = self.class.rescue_handler_for(exception)
1273
+ raise exception unless handler
1274
+
1275
+ return instance_exec(exception, &handler) unless handler.is_a?(Symbol)
1276
+
1277
+ handler_method = method(handler)
1278
+ handler_method.arity.zero? ? handler_method.call : handler_method.call(exception)
1279
+ end
366
1280
  end
367
1281
  end