ruby_llm 2.0.0 → 2.1.0

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 (284) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -8
  3. data/lib/generators/ruby_llm/generator_helpers.rb +68 -6
  4. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -0
  5. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +36 -3
  6. data/lib/generators/ruby_llm/provider/scaffold.rb +32 -22
  7. data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +1 -5
  8. data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +1 -1
  9. data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +1 -1
  10. data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +1 -1
  11. data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +78 -0
  12. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +6 -163
  13. data/lib/ruby_llm/accounting/usage.rb +86 -49
  14. data/lib/ruby_llm/active_record/acts_as.rb +3 -0
  15. data/lib/ruby_llm/active_record/attachment_helpers.rb +50 -25
  16. data/lib/ruby_llm/active_record/batch.rb +1 -1
  17. data/lib/ruby_llm/active_record/chat_methods.rb +136 -40
  18. data/lib/ruby_llm/active_record/mcp_credential.rb +41 -0
  19. data/lib/ruby_llm/active_record/message_methods.rb +32 -7
  20. data/lib/ruby_llm/active_record/model.rb +1 -1
  21. data/lib/ruby_llm/active_record/provider_file.rb +64 -0
  22. data/lib/ruby_llm/active_record/record.rb +19 -0
  23. data/lib/ruby_llm/active_record/tool_call.rb +1 -1
  24. data/lib/ruby_llm/active_record/usage.rb +53 -3
  25. data/lib/ruby_llm/agent.rb +198 -33
  26. data/lib/ruby_llm/aliases.json +66 -8
  27. data/lib/ruby_llm/attachment.rb +54 -10
  28. data/lib/ruby_llm/batch.rb +26 -3
  29. data/lib/ruby_llm/chat/tool_concurrency.rb +47 -23
  30. data/lib/ruby_llm/chat/tool_search.rb +26 -0
  31. data/lib/ruby_llm/chat.rb +488 -82
  32. data/lib/ruby_llm/choice.rb +38 -0
  33. data/lib/ruby_llm/configuration.rb +36 -11
  34. data/lib/ruby_llm/context.rb +45 -32
  35. data/lib/ruby_llm/cost.rb +42 -1
  36. data/lib/ruby_llm/embedding.rb +9 -2
  37. data/lib/ruby_llm/error.rb +34 -2
  38. data/lib/ruby_llm/evaluation/assertions.rb +40 -0
  39. data/lib/ruby_llm/evaluation/case.rb +52 -0
  40. data/lib/ruby_llm/evaluation/dataset.rb +65 -0
  41. data/lib/ruby_llm/evaluation/evaluator.rb +126 -0
  42. data/lib/ruby_llm/evaluation/evidence.rb +125 -0
  43. data/lib/ruby_llm/evaluation/minitest.rb +26 -0
  44. data/lib/ruby_llm/evaluation/report.rb +103 -0
  45. data/lib/ruby_llm/evaluation/result.rb +75 -0
  46. data/lib/ruby_llm/evaluation/reviewer.rb +20 -0
  47. data/lib/ruby_llm/evaluation/rspec.rb +23 -0
  48. data/lib/ruby_llm/evaluation/runner.rb +93 -0
  49. data/lib/ruby_llm/evaluation/trial.rb +101 -0
  50. data/lib/ruby_llm/evaluation.rb +289 -0
  51. data/lib/ruby_llm/image.rb +12 -4
  52. data/lib/ruby_llm/judge/builder.rb +51 -0
  53. data/lib/ruby_llm/judge/data.rb +43 -0
  54. data/lib/ruby_llm/judge/question.rb +101 -0
  55. data/lib/ruby_llm/judge.rb +179 -0
  56. data/lib/ruby_llm/judgment.rb +125 -0
  57. data/lib/ruby_llm/mcp/apps.rb +25 -0
  58. data/lib/ruby_llm/mcp/client.rb +213 -0
  59. data/lib/ruby_llm/mcp/collection.rb +56 -0
  60. data/lib/ruby_llm/mcp/content.rb +44 -0
  61. data/lib/ruby_llm/mcp/error.rb +27 -0
  62. data/lib/ruby_llm/mcp/http.rb +379 -0
  63. data/lib/ruby_llm/mcp/input_request.rb +118 -0
  64. data/lib/ruby_llm/mcp/input_required_error.rb +29 -0
  65. data/lib/ruby_llm/mcp/listener.rb +168 -0
  66. data/lib/ruby_llm/mcp/oauth/key.rb +78 -0
  67. data/lib/ruby_llm/mcp/oauth/memory_store.rb +28 -0
  68. data/lib/ruby_llm/mcp/oauth/proofs.rb +39 -0
  69. data/lib/ruby_llm/mcp/oauth.rb +653 -0
  70. data/lib/ruby_llm/mcp/param_headers.rb +83 -0
  71. data/lib/ruby_llm/mcp/prompt.rb +71 -0
  72. data/lib/ruby_llm/mcp/resource.rb +84 -0
  73. data/lib/ruby_llm/mcp/resource_template.rb +99 -0
  74. data/lib/ruby_llm/mcp/result.rb +76 -0
  75. data/lib/ruby_llm/mcp/session_expired.rb +10 -0
  76. data/lib/ruby_llm/mcp/stdio.rb +250 -0
  77. data/lib/ruby_llm/mcp/task.rb +151 -0
  78. data/lib/ruby_llm/mcp/tool.rb +130 -0
  79. data/lib/ruby_llm/mcp.rb +1101 -0
  80. data/lib/ruby_llm/message.rb +41 -6
  81. data/lib/ruby_llm/model.rb +2 -1
  82. data/lib/ruby_llm/models/aliases.rb +5 -2
  83. data/lib/ruby_llm/models/lookup.rb +80 -0
  84. data/lib/ruby_llm/models/registry.rb +2 -2
  85. data/lib/ruby_llm/models/schema.rb +1 -1
  86. data/lib/ruby_llm/models.json +23082 -10158
  87. data/lib/ruby_llm/models.rb +20 -69
  88. data/lib/ruby_llm/moderation.rb +7 -2
  89. data/lib/ruby_llm/ocr.rb +7 -2
  90. data/lib/ruby_llm/open_telemetry/attributes.rb +111 -0
  91. data/lib/ruby_llm/open_telemetry.rb +82 -0
  92. data/lib/ruby_llm/probability.rb +30 -0
  93. data/lib/ruby_llm/progress.rb +44 -0
  94. data/lib/ruby_llm/prompt/context.rb +55 -0
  95. data/lib/ruby_llm/prompt.rb +1 -1
  96. data/lib/ruby_llm/protocol/binary_streaming.rb +1 -1
  97. data/lib/ruby_llm/protocol/request_shapes/analysis.rb +190 -0
  98. data/lib/ruby_llm/protocol/request_shapes.rb +183 -0
  99. data/lib/ruby_llm/protocol/stored_uploads.rb +62 -0
  100. data/lib/ruby_llm/protocol/stream_accumulator.rb +9 -25
  101. data/lib/ruby_llm/protocol/streaming.rb +38 -31
  102. data/lib/ruby_llm/protocol.rb +192 -40
  103. data/lib/ruby_llm/protocols/anthropic/chat.rb +39 -32
  104. data/lib/ruby_llm/protocols/anthropic/media.rb +6 -2
  105. data/lib/ruby_llm/protocols/anthropic/models.rb +16 -1
  106. data/lib/ruby_llm/protocols/anthropic/request_shapes.rb +70 -0
  107. data/lib/ruby_llm/protocols/anthropic/streaming.rb +23 -9
  108. data/lib/ruby_llm/protocols/anthropic/tools.rb +43 -0
  109. data/lib/ruby_llm/protocols/anthropic.rb +20 -1
  110. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +9 -17
  111. data/lib/ruby_llm/protocols/bedrock/guardrails.rb +8 -9
  112. data/lib/ruby_llm/protocols/bedrock/rerank.rb +1 -17
  113. data/lib/ruby_llm/protocols/chat_completions/chat.rb +5 -7
  114. data/lib/ruby_llm/protocols/chat_completions/media.rb +34 -8
  115. data/lib/ruby_llm/protocols/chat_completions/request_shapes.rb +121 -0
  116. data/lib/ruby_llm/protocols/chat_completions/streaming.rb +3 -0
  117. data/lib/ruby_llm/protocols/chat_completions.rb +1 -0
  118. data/lib/ruby_llm/protocols/cohere/batch_requests.rb +0 -5
  119. data/lib/ruby_llm/protocols/cohere/batches.rb +3 -7
  120. data/lib/ruby_llm/protocols/cohere/chat.rb +8 -0
  121. data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -4
  122. data/lib/ruby_llm/protocols/cohere/media.rb +6 -2
  123. data/lib/ruby_llm/protocols/cohere/ocr.rb +1 -1
  124. data/lib/ruby_llm/protocols/cohere/streaming.rb +1 -0
  125. data/lib/ruby_llm/protocols/cohere/tools.rb +5 -1
  126. data/lib/ruby_llm/protocols/cohere.rb +1 -0
  127. data/lib/ruby_llm/protocols/converse/chat.rb +22 -131
  128. data/lib/ruby_llm/protocols/converse/media.rb +16 -2
  129. data/lib/ruby_llm/protocols/converse/reasoning.rb +194 -0
  130. data/lib/ruby_llm/protocols/converse/request_shapes.rb +99 -0
  131. data/lib/ruby_llm/protocols/converse/streaming.rb +2 -2
  132. data/lib/ruby_llm/protocols/converse/thinking_stream.rb +2 -1
  133. data/lib/ruby_llm/protocols/converse.rb +3 -5
  134. data/lib/ruby_llm/protocols/deepseek/files.rb +2 -9
  135. data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +1 -7
  136. data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +11 -17
  137. data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +1 -4
  138. data/lib/ruby_llm/protocols/gemini/chat.rb +50 -21
  139. data/lib/ruby_llm/protocols/gemini/embeddings.rb +2 -2
  140. data/lib/ruby_llm/protocols/gemini/files.rb +1 -1
  141. data/lib/ruby_llm/protocols/gemini/images.rb +5 -1
  142. data/lib/ruby_llm/protocols/gemini/media.rb +15 -3
  143. data/lib/ruby_llm/protocols/gemini/request_shapes.rb +111 -0
  144. data/lib/ruby_llm/protocols/gemini/streaming.rb +2 -1
  145. data/lib/ruby_llm/protocols/gemini/tools.rb +17 -11
  146. data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -0
  147. data/lib/ruby_llm/protocols/gemini/videos.rb +7 -2
  148. data/lib/ruby_llm/protocols/gemini.rb +1 -0
  149. data/lib/ruby_llm/protocols/gpustack/videos.rb +7 -9
  150. data/lib/ruby_llm/protocols/interactions/chat.rb +46 -18
  151. data/lib/ruby_llm/protocols/interactions/content.rb +9 -2
  152. data/lib/ruby_llm/protocols/interactions/request_shapes.rb +62 -0
  153. data/lib/ruby_llm/protocols/interactions/streaming.rb +15 -0
  154. data/lib/ruby_llm/protocols/interactions/tools.rb +28 -4
  155. data/lib/ruby_llm/protocols/interactions/transcription.rb +1 -11
  156. data/lib/ruby_llm/protocols/interactions.rb +1 -0
  157. data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +2 -11
  158. data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +2 -2
  159. data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +2 -6
  160. data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +2 -8
  161. data/lib/ruby_llm/protocols/invoke_model.rb +1 -4
  162. data/lib/ruby_llm/protocols/mistral/content.rb +13 -0
  163. data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +11 -1
  164. data/lib/ruby_llm/protocols/mistral/conversations/images.rb +5 -1
  165. data/lib/ruby_llm/protocols/mistral/conversations/request_shapes.rb +45 -0
  166. data/lib/ruby_llm/protocols/mistral/conversations.rb +1 -0
  167. data/lib/ruby_llm/protocols/mistral/multi_completion.rb +11 -1
  168. data/lib/ruby_llm/protocols/openai/decisions.rb +135 -0
  169. data/lib/ruby_llm/protocols/openai/files.rb +0 -7
  170. data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -8
  171. data/lib/ruby_llm/protocols/openrouter/responses.rb +5 -1
  172. data/lib/ruby_llm/protocols/perplexity/agent.rb +116 -0
  173. data/lib/ruby_llm/protocols/perplexity/router.rb +2 -43
  174. data/lib/ruby_llm/protocols/responses/chat.rb +56 -22
  175. data/lib/ruby_llm/protocols/responses/media.rb +11 -7
  176. data/lib/ruby_llm/protocols/responses/request_shapes.rb +70 -0
  177. data/lib/ruby_llm/protocols/responses/streaming.rb +23 -7
  178. data/lib/ruby_llm/protocols/responses/token_counting.rb +2 -2
  179. data/lib/ruby_llm/protocols/responses/tools.rb +27 -0
  180. data/lib/ruby_llm/protocols/responses.rb +1 -0
  181. data/lib/ruby_llm/protocols/system_one/judgments.rb +42 -0
  182. data/lib/ruby_llm/protocols/system_one/models.rb +27 -0
  183. data/lib/ruby_llm/protocols/system_one/responses.rb +55 -0
  184. data/lib/ruby_llm/protocols/system_one.rb +27 -0
  185. data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +2 -2
  186. data/lib/ruby_llm/protocols/vertexai/files.rb +34 -6
  187. data/lib/ruby_llm/protocols/vertexai/ranking.rb +9 -30
  188. data/lib/ruby_llm/protocols/vertexai/research.rb +9 -41
  189. data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
  190. data/lib/ruby_llm/provider.rb +87 -15
  191. data/lib/ruby_llm/providers/anthropic.rb +23 -0
  192. data/lib/ruby_llm/providers/azure/chat.rb +4 -0
  193. data/lib/ruby_llm/providers/azure/images.rb +2 -2
  194. data/lib/ruby_llm/providers/azure/media.rb +8 -3
  195. data/lib/ruby_llm/providers/azure/videos.rb +7 -2
  196. data/lib/ruby_llm/providers/azure.rb +24 -4
  197. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -1
  198. data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +1 -1
  199. data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -3
  200. data/lib/ruby_llm/providers/bedrock/mantle.rb +5 -9
  201. data/lib/ruby_llm/providers/bedrock.rb +38 -6
  202. data/lib/ruby_llm/providers/deepseek/chat.rb +6 -1
  203. data/lib/ruby_llm/providers/deepseek/responses.rb +6 -0
  204. data/lib/ruby_llm/providers/deepseek.rb +4 -0
  205. data/lib/ruby_llm/providers/gemini.rb +4 -0
  206. data/lib/ruby_llm/providers/gpustack/media.rb +8 -5
  207. data/lib/ruby_llm/providers/hetzner/chat.rb +39 -0
  208. data/lib/ruby_llm/providers/hetzner.rb +41 -0
  209. data/lib/ruby_llm/providers/mistral/chat.rb +16 -12
  210. data/lib/ruby_llm/providers/mistral/media.rb +8 -4
  211. data/lib/ruby_llm/providers/mistral/ocr.rb +1 -5
  212. data/lib/ruby_llm/providers/ollama/judgments.rb +26 -0
  213. data/lib/ruby_llm/providers/ollama/media.rb +6 -2
  214. data/lib/ruby_llm/providers/ollama/models.rb +2 -0
  215. data/lib/ruby_llm/providers/ollama.rb +11 -0
  216. data/lib/ruby_llm/providers/openai/capabilities.rb +3 -1
  217. data/lib/ruby_llm/providers/openai.rb +14 -0
  218. data/lib/ruby_llm/providers/openrouter/chat.rb +20 -24
  219. data/lib/ruby_llm/providers/openrouter/embeddings.rb +2 -0
  220. data/lib/ruby_llm/providers/openrouter/media.rb +5 -1
  221. data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -1
  222. data/lib/ruby_llm/providers/openrouter/videos.rb +10 -5
  223. data/lib/ruby_llm/providers/openrouter.rb +4 -0
  224. data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
  225. data/lib/ruby_llm/providers/perplexity/media.rb +5 -11
  226. data/lib/ruby_llm/providers/perplexity/models.rb +8 -4
  227. data/lib/ruby_llm/providers/perplexity.rb +11 -0
  228. data/lib/ruby_llm/providers/typesafe.rb +32 -0
  229. data/lib/ruby_llm/providers/vertexai/credentials.rb +63 -0
  230. data/lib/ruby_llm/providers/vertexai/gemini.rb +2 -2
  231. data/lib/ruby_llm/providers/vertexai/transcription.rb +1 -0
  232. data/lib/ruby_llm/providers/vertexai.rb +27 -28
  233. data/lib/ruby_llm/providers/xai/chat.rb +6 -0
  234. data/lib/ruby_llm/providers/xai/images.rb +9 -10
  235. data/lib/ruby_llm/providers/xai/responses.rb +10 -8
  236. data/lib/ruby_llm/providers/xai/videos.rb +11 -9
  237. data/lib/ruby_llm/providers/xai.rb +4 -0
  238. data/lib/ruby_llm/railtie.rb +11 -0
  239. data/lib/ruby_llm/request_shape/part.rb +117 -0
  240. data/lib/ruby_llm/request_shape/problem.rb +63 -0
  241. data/lib/ruby_llm/request_shape/tool_round.rb +64 -0
  242. data/lib/ruby_llm/request_shape/turn.rb +54 -0
  243. data/lib/ruby_llm/request_shape.rb +160 -0
  244. data/lib/ruby_llm/rerank.rb +9 -2
  245. data/lib/ruby_llm/research_job.rb +21 -5
  246. data/lib/ruby_llm/score.rb +42 -0
  247. data/lib/ruby_llm/search_results.rb +2 -2
  248. data/lib/ruby_llm/server_tool_call.rb +10 -2
  249. data/lib/ruby_llm/speech.rb +8 -1
  250. data/lib/ruby_llm/support/cancellation.rb +39 -0
  251. data/lib/ruby_llm/support/inspectable.rb +3 -2
  252. data/lib/ruby_llm/support/instrumentation.rb +38 -1
  253. data/lib/ruby_llm/support/process_cache.rb +60 -0
  254. data/lib/ruby_llm/support/progress_reporter.rb +33 -0
  255. data/lib/ruby_llm/thinking.rb +2 -1
  256. data/lib/ruby_llm/tokens.rb +57 -14
  257. data/lib/ruby_llm/tool.rb +50 -3
  258. data/lib/ruby_llm/tool_call.rb +17 -2
  259. data/lib/ruby_llm/transcription.rb +10 -3
  260. data/lib/ruby_llm/transport/connection.rb +127 -44
  261. data/lib/ruby_llm/transport/error_middleware.rb +90 -24
  262. data/lib/ruby_llm/transport/event_stream_parser.rb +121 -0
  263. data/lib/ruby_llm/transport/json_response.rb +33 -0
  264. data/lib/ruby_llm/transport/signing_middleware.rb +29 -0
  265. data/lib/ruby_llm/version.rb +1 -1
  266. data/lib/ruby_llm/video.rb +5 -3
  267. data/lib/ruby_llm/video_job.rb +48 -5
  268. data/lib/ruby_llm.rb +106 -5
  269. data/lib/tasks/ruby_llm/evaluations.rb +43 -0
  270. data/lib/tasks/ruby_llm.rake +6 -9
  271. data/skills/rubyllm/SKILL.md +33 -12
  272. metadata +95 -37
  273. data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +0 -34
  274. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +0 -341
  275. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +0 -171
  276. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +0 -197
  277. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +0 -174
  278. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +0 -468
  279. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +0 -101
  280. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +0 -247
  281. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +0 -662
  282. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +0 -251
  283. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +0 -13
  284. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +0 -359
data/lib/ruby_llm.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'base64'
4
- require 'event_stream_parser'
5
4
  require 'faraday'
6
5
  require 'faraday/multipart'
7
6
  require 'faraday/retry'
@@ -25,14 +24,21 @@ loader.inflector.inflect(
25
24
  'deepseek' => 'DeepSeek',
26
25
  'elevenlabs' => 'ElevenLabs',
27
26
  'gpustack' => 'GPUStack',
27
+ 'hetzner' => 'Hetzner',
28
+ 'http' => 'HTTP',
28
29
  'llm' => 'LLM',
30
+ 'mcp' => 'MCP',
31
+ 'oauth' => 'OAuth',
29
32
  'mistral' => 'Mistral',
30
33
  'ocr' => 'OCR',
31
34
  'openai' => 'OpenAI',
35
+ 'open_telemetry' => 'OpenTelemetry',
32
36
  'openrouter' => 'OpenRouter',
33
37
  'pdf' => 'PDF',
34
38
  'perplexity' => 'Perplexity',
39
+ 'rspec' => 'RSpec',
35
40
  'ruby_llm' => 'RubyLLM',
41
+ 'typesafe' => 'TypeSafe',
36
42
  'vertexai' => 'VertexAI',
37
43
  'xai' => 'XAI'
38
44
  )
@@ -75,9 +81,14 @@ loader.setup
75
81
  #
76
82
  # Subclass Tool and implement +execute+ to give the model an application
77
83
  # action. Tool.requires_approval pauses execution for a human decision;
78
- # Chat#approve and Chat#deny record it. Chat#with_provider_tools enables
79
- # provider-executed tools such as web search, code execution, and remote MCP.
84
+ # Chat#approve and Chat#deny record it. Tool#progress reports what a
85
+ # running tool is doing to Chat#after_tool_progress as a Progress.
86
+ # Chat#with_provider_tools enables provider-executed tools such as web
87
+ # search, code execution, and remote MCP.
80
88
  # Their calls appear as ServerToolCall values, with Citation values for sources.
89
+ # MCP is a Model Context Protocol client: describe a server to connect to
90
+ # in an MCP class, and Chat#with_mcp gives the model its tools. RubyLLM.mcp
91
+ # connects to one inline.
81
92
  #
82
93
  # Agent defines a reusable configuration with model, instructions, tools,
83
94
  # schema, and runtime inputs. Chat#ask_later, Chat#generate, Chat#run_tools,
@@ -132,6 +143,41 @@ loader.setup
132
143
  # returns a ResearchJob for polling and cancellation. Hosted agent identities
133
144
  # are selected separately from model IDs.
134
145
  #
146
+ # == Evaluations
147
+ #
148
+ # Evaluation runs your application against a dataset of cases and grades each
149
+ # result. Implement Evaluation#perform to return a value, Message, Chat, or
150
+ # Agent. Without declared criteria, a model checks each result against the
151
+ # case's +expected_output+. Evaluations can also use an Agent or a Judge as
152
+ # their evaluator, and Ruby assertions. Datasets live under app/evals, named
153
+ # after the class:
154
+ #
155
+ # class SupportEvaluation < RubyLLM::Evaluation
156
+ # def perform(input)
157
+ # SupportAgent.new.ask(input)
158
+ # end
159
+ # end
160
+ # SupportEvaluation.run.passed?
161
+ #
162
+ # Evaluation.run returns an Evaluation::Report with each trial's verdicts,
163
+ # tokens, and cost. The +ruby_llm:eval+ Rake task, Evaluation::RSpec, and
164
+ # Evaluation::Minitest run the same cases from the command line and tests.
165
+ #
166
+ # == Typed judgments
167
+ #
168
+ # Judge defines reusable probability, choice, and score questions. Its +judge+
169
+ # method evaluates supplied text, structured data, or images passed as +with:+
170
+ # and returns a Judgment:
171
+ #
172
+ # class Urgency < RubyLLM::Judge
173
+ # probability :urgent, "Does this need attention today?"
174
+ # end
175
+ # Urgency.judge("Please help today.").urgent.probability
176
+ #
177
+ # RubyLLM.judge accepts question definitions as a Hash. Probability, Choice,
178
+ # and Score answers retain uncertainty; Judgment reports the model, tokens,
179
+ # and cost. Questions share the supplied input and do not retain chat history.
180
+ #
135
181
  # == Batches, usage, and configuration
136
182
  #
137
183
  # RubyLLM.batch submits staged chats or EmbeddingRequest objects for
@@ -142,6 +188,10 @@ loader.setup
142
188
  # Tokens and Cost report usage and pricing. Chat totals include retries
143
189
  # and attempts that produced no message. Provider-reported costs take
144
190
  # precedence over estimates; unknown usage and prices remain +nil+.
191
+ # RubyLLM.with_usage_owner attributes the usage of operations to a record.
192
+ # OpenTelemetry.enable enables optional tracing alongside existing subscribers.
193
+ # Your application configures the OpenTelemetry SDK and exporters.
194
+ #
145
195
  # RubyLLM.workflow groups instrumentation from ordinary Ruby code into
146
196
  # named Workflow steps.
147
197
  #
@@ -149,7 +199,8 @@ loader.setup
149
199
  # isolated settings for a request or tenant. Models finds, filters, and
150
200
  # describes the model catalog. Provider supplies endpoints, authentication,
151
201
  # and protocol selection; Protocol implements request and response formats.
152
- # Error subclasses normalize provider failures.
202
+ # Error subclasses normalize provider failures, and Error#request_shape
203
+ # describes a refused conversation request as a RequestShape.
153
204
  #
154
205
  # == Rails integration
155
206
  #
@@ -160,7 +211,8 @@ loader.setup
160
211
  # Approvals and cancellation survive requests and processes.
161
212
  #
162
213
  # Your application owns chats and messages; RubyLLM owns usage, tool calls,
163
- # models, and batches. Agent can create and reload your chat records through
214
+ # models, batches, MCP credentials, and provider files. ActiveRecord::Record configures their shared database
215
+ # connection. Agent can create and reload your chat records through
164
216
  # Agent.chat_model. Individual operations also work directly in Rails
165
217
  # services and jobs.
166
218
  module RubyLLM
@@ -208,6 +260,23 @@ module RubyLLM
208
260
  Workflow.new(name, id:, metadata:, config: config).run(&)
209
261
  end
210
262
 
263
+ # Attributes the usage of every operation inside the block to +owner+,
264
+ # such as a user or an account record, and returns the block's value.
265
+ # In Rails, the usage ledger stores the owner alongside any chat and
266
+ # message associations; everywhere, the +usage.ruby_llm+ event carries
267
+ # it as +owner+. An operation's own +owner:+ keyword wins, and nested blocks
268
+ # restore the outer owner when they end. Each fiber and thread keeps its
269
+ # own owner, and fibers and threads started inside the block inherit it.
270
+ #
271
+ # RubyLLM.with_usage_owner(current_user) do
272
+ # RubyLLM.transcribe("memo.m4a")
273
+ # RubyLLM.paint("a paper boat in the rain")
274
+ # end
275
+ #
276
+ def with_usage_owner(owner, &)
277
+ Accounting::Usage.with_owner(owner, &)
278
+ end
279
+
211
280
  # Creates a Chat conversation. Arguments are forwarded to Chat.new:
212
281
  # +model:+, +provider:+, +protocol:+, +assume_model_exists:+, and
213
282
  # +context:+. With no arguments, uses the configured default model.
@@ -221,6 +290,25 @@ module RubyLLM
221
290
  Chat.new(...)
222
291
  end
223
292
 
293
+ # Connects to a Model Context Protocol server without writing an MCP
294
+ # class. Pass +url:+ for a Streamable HTTP server, +command:+ for a
295
+ # local server that speaks over stdio, or +transport:+ and +name:+ for
296
+ # a server reached any other way, as MCP.transport describes. Also
297
+ # accepts +name:+, +bearer_token:+, +headers:+, +env:+, +directory:+,
298
+ # +timeout:+, +prefix:+, +input_requests:+, +log_level:+,
299
+ # +extensions:+, which takes a name or a Hash of names to the settings
300
+ # of MCP.extension, and +oauth:+, which takes +true+ or the options of
301
+ # MCP.oauth.
302
+ #
303
+ # docs = RubyLLM.mcp(url: "https://learn.microsoft.com/api/mcp")
304
+ # files = RubyLLM.mcp(command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "."])
305
+ # RubyLLM.mcp(url: server.endpoint, prefix: "mcp_#{server.id}", oauth: { owner: server })
306
+ #
307
+ # Returns an MCP.
308
+ def mcp(...)
309
+ MCP.define(...).new
310
+ end
311
+
224
312
  # Counts the tokens +text+ would consume as a single user message,
225
313
  # without requesting a completion. Builds a minimal Chat and delegates
226
314
  # to Chat#count_tokens. Returns an Integer.
@@ -287,6 +375,17 @@ module RubyLLM
287
375
  Moderation.moderate(...)
288
376
  end
289
377
 
378
+ # Judges text, structured data, or images against typed questions and
379
+ # returns a Judgment. Accepts the same arguments as Judge.judge. Subclass
380
+ # Judge to define reusable questions with probability, choice, and score.
381
+ # Uses Configuration#default_judgment_model unless a model is supplied.
382
+ #
383
+ # RubyLLM.judge("Please help today",
384
+ # questions: { urgent: { type: :probability, instructions: "Is this urgent?" } })
385
+ def judge(...)
386
+ Judge.judge(...)
387
+ end
388
+
290
389
  # Runs a hosted research task and returns its report as a Message.
291
390
  # Requires explicit +provider:+ and +agent:+. See ResearchJob.research.
292
391
  def research(...)
@@ -480,12 +579,14 @@ RubyLLM::Provider.register :deepseek, RubyLLM::Providers::DeepSeek
480
579
  RubyLLM::Provider.register :elevenlabs, RubyLLM::Providers::ElevenLabs
481
580
  RubyLLM::Provider.register :gemini, RubyLLM::Providers::Gemini
482
581
  RubyLLM::Provider.register :gpustack, RubyLLM::Providers::GPUStack
582
+ RubyLLM::Provider.register :hetzner, RubyLLM::Providers::Hetzner
483
583
  RubyLLM::Provider.register :mistral, RubyLLM::Providers::Mistral
484
584
  RubyLLM::Provider.register :ollama, RubyLLM::Providers::Ollama
485
585
  RubyLLM::Provider.register :ollama_cloud, RubyLLM::Providers::OllamaCloud
486
586
  RubyLLM::Provider.register :openai, RubyLLM::Providers::OpenAI
487
587
  RubyLLM::Provider.register :openrouter, RubyLLM::Providers::OpenRouter
488
588
  RubyLLM::Provider.register :perplexity, RubyLLM::Providers::Perplexity
589
+ RubyLLM::Provider.register :typesafe, RubyLLM::Providers::TypeSafe
489
590
  RubyLLM::Provider.register :vertexai, RubyLLM::Providers::VertexAI
490
591
  RubyLLM::Provider.register :xai, RubyLLM::Providers::XAI
491
592
 
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module RubyLLM
6
+ module Tasks
7
+ module Evaluations # :nodoc:
8
+ module_function
9
+
10
+ def run(name = nil, only: nil)
11
+ files = discover(name)
12
+ repetitions = Integer(ENV.fetch('EVAL_REPETITIONS', '1'))
13
+ reports = files.map do |file, class_name|
14
+ require file.to_s
15
+ evaluation = Object.const_get(class_name)
16
+ raise ArgumentError, "#{class_name} must inherit from RubyLLM::Evaluation" unless evaluation < Evaluation
17
+
18
+ report = evaluation.run(only:, repetitions:)
19
+ path = Pathname.new(ENV.fetch('EVAL_OUTPUT', 'tmp/evaluations')).join("#{class_name.gsub('::', '/')}.json")
20
+ FileUtils.mkdir_p(path.dirname)
21
+ report.save(path)
22
+ puts report
23
+ puts "Report: #{path}"
24
+ report
25
+ end
26
+ abort 'Evaluations failed' unless reports.all?(&:passed?)
27
+ end
28
+
29
+ def discover(name)
30
+ root = Prompt.root.parent.join('evals')
31
+ inflector = Zeitwerk::Inflector.new
32
+ files = root.glob('**/*_evaluation.rb').sort.to_h do |file|
33
+ parts = file.relative_path_from(root).to_s.delete_suffix('.rb').split('/')
34
+ [file, parts.map { |part| inflector.camelize(part, file.to_s) }.join('::')]
35
+ end
36
+ files.select! { |_file, class_name| class_name == name } if name
37
+ raise ArgumentError, "No evaluations found in #{root}#{" matching #{name}" if name}" if files.empty?
38
+
39
+ files
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,16 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :ruby_llm do
4
- namespace :upgrade do
5
- %i[rollback resume finalize].each do |action|
6
- desc "#{action.to_s.capitalize} a copy-mode RubyLLM upgrade with affected processes stopped"
7
- task action => :environment do
8
- require 'generators/ruby_llm/upgrade/upgrade_migration'
4
+ desc 'Run app/evals evaluations (optionally select an evaluation and case)'
5
+ task :eval, %i[evaluation case] do |_task, args|
6
+ Rake::Task[:environment].invoke if Rake::Task.task_defined?(:environment)
7
+ require 'ruby_llm'
8
+ require 'tasks/ruby_llm/evaluations'
9
9
 
10
- RubyLLM::Generators::UpgradeMigration.for.public_send(action)
11
- puts "RubyLLM copy upgrade: #{action} completed"
12
- end
13
- end
10
+ RubyLLM::Tasks::Evaluations.run(args[:evaluation], only: args[:case])
14
11
  end
15
12
 
16
13
  desc 'Load the selected model registry into the database'
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: rubyllm
3
- description: Build and maintain Ruby or Rails applications with the RubyLLM AI framework. Use for chats, agents, tools, structured output, media generation, transcription, OCR, moderation, embeddings, reranking, Rails integration, and RubyLLM upgrades; not for contributing to the framework itself.
3
+ description: Build and maintain Ruby or Rails applications with the RubyLLM AI framework. Use for chats, agents, tools, MCP servers, structured output, judgments, evaluations, media generation, transcription, OCR, moderation, embeddings, reranking, tracing, Rails integration, and RubyLLM upgrades; not for contributing to the framework itself.
4
4
  license: MIT
5
5
  metadata:
6
- api_version: "2.0"
6
+ api_version: "2.1"
7
7
  ---
8
8
 
9
9
  # Build with RubyLLM
10
10
 
11
- Use the application's installed RubyLLM version and existing conventions. This skill describes the 2.0 API. Read `Gemfile.lock`, or inspect the installed gem before choosing an API:
11
+ Use the application's installed RubyLLM version and existing conventions. This skill describes the 2.x API; features marked 2.1 need RubyLLM 2.1 or later. Read `Gemfile.lock`, or inspect the installed gem before choosing an API:
12
12
 
13
13
  ```bash
14
14
  bundle exec ruby -rruby_llm -e 'puts RubyLLM::VERSION'
@@ -19,7 +19,7 @@ For a 1.x application, use its version's documentation and keep the requested ch
19
19
 
20
20
  ## Choose the API for the task
21
21
 
22
- RubyLLM has two public API families: conversations use chats, messages, tools, agents, structured output, streaming, and loop control; individual operations use `paint`, `animate`, `speak`, `transcribe`, `ocr`, `moderate`, `embed`, and `rerank`. Individual operations return typed results without requiring a chat. Some stream or have an asynchronous lifecycle.
22
+ RubyLLM has two public API families: conversations use chats, messages, tools, agents, structured output, streaming, and loop control; individual operations use `paint`, `animate`, `speak`, `transcribe`, `ocr`, `moderate`, `embed`, `rerank`, `judge`, `tokenize`, and `research`. Individual operations return typed results without requiring a chat. Some stream or have an asynchronous lifecycle.
23
23
 
24
24
  Providers supply service endpoints, authentication, catalogs, and protocol selection. Protocols translate requests and responses. Both API families use this integration layer and shared services for model selection, configuration, accounting, and instrumentation. Build against the public API and let RubyLLM handle the service formats.
25
25
 
@@ -27,7 +27,7 @@ Rails integration adds persistence, attachments, streaming UI support, jobs, and
27
27
 
28
28
  ## Find the relevant API
29
29
 
30
- The installed gem's public method signatures and RDoc describe the code the application actually runs. Use them when online documentation differs. Find the matching version through [RubyLLM's documentation](https://rubyllm.com/). During the 2.0 release-candidate period, the 2.0 guides and Markdown API reference are under [rubyllm.com/next/](https://rubyllm.com/next/).
30
+ The installed gem's public method signatures and RDoc describe the code the application actually runs. Use them when online documentation differs. Find the matching version through [RubyLLM's documentation](https://rubyllm.com/). The root site documents the latest stable release. Unreleased changes on `main` are under [rubyllm.com/next/](https://rubyllm.com/next/), and 1.x under [rubyllm.com/v1/](https://rubyllm.com/v1/).
31
31
 
32
32
  Read the relevant page instead of loading the entire documentation site:
33
33
 
@@ -37,44 +37,65 @@ Read the relevant page instead of loading the entire documentation site:
37
37
  | Configure a provider or select a model | `configuration/`, `configuration-providers/`, `models/` |
38
38
  | Chat, stream, or attach files | `chat/`, `streaming/`, `attachments/` |
39
39
  | Declare tools or require approval | `tools/`, `tool-parameters/`, `tool-execution/` |
40
+ | Use tools from MCP servers (2.1) | `mcp/` |
41
+ | Use provider-hosted tools such as web search | `provider-tools/`, `citations/` |
40
42
  | Parse structured output | `structured-output/` |
41
43
  | Build reusable agents | `agents/`, `prompt-rendering/` |
44
+ | Enable thinking or prompt caching | `thinking/`, `prompt-caching/` |
42
45
  | Generate images or video | `image-generation/`, `video-generation/` |
43
46
  | Generate speech or transcribe audio | `text-to-speech/`, `audio-transcription/` |
44
47
  | Extract document text or moderate content | `ocr/`, `moderation/` |
45
- | Embed text or rank search results | `embeddings/`, `rerank/` |
46
- | Persist chats, attach files, or stream with Hotwire | `rails/`, `rails-persistence/`, `rails-streaming/`, `rails-generators/` |
48
+ | Embed text or rank search results | `embeddings/`, `rerank/`, `rag/` |
49
+ | Classify, score, or decide with typed answers (2.1) | `judgments/` |
50
+ | Measure answer quality against a dataset (2.1) | `evaluations/`, `evaluation-datasets/`, `evaluation-evaluators/`, `evaluation-running/`, `evaluation-examples/` |
51
+ | Count tokens, upload provider files, or run hosted research | `tokenization/`, `files/`, `hosted-research/` |
52
+ | Persist chats, attach files, or stream with Hotwire | `rails/`, `rails-persistence/`, `rails-streaming/`, `rails-generators/`, `rails-advanced-config/` |
47
53
  | Resume agents in background jobs | `durable-agents/` |
48
54
  | Track spend or handle failures | `cost-and-usage-tracking/`, `error-handling/` |
55
+ | Observe calls, group workflows, or trace with OpenTelemetry | `instrumentation/`, `opentelemetry/` (2.1) |
49
56
  | Submit batches or drive the conversation loop | `batches/`, `agentic-workflows/` |
50
57
  | Upgrade an existing application | `upgrading/` |
51
58
 
52
59
  Markdown guides use the same name with `.md`, such as `chat.md`. The API reference starts at `api/RubyLLM.md`; class references include `api/RubyLLM/Chat.md` and `api/RubyLLM/Agent.md`. If fetching documentation is unavailable, inspect the installed source and identify any remaining uncertainty.
53
60
 
54
- ## Use the 2.0 API consistently
61
+ ## Use the 2.x API consistently
55
62
 
56
63
  - Configure credentials through `RubyLLM.configure`, following the application's secret storage. Select an existing configured provider. Verify explicit model IDs with `RubyLLM.models.find(id, provider: ...)`; do not invent IDs or use `assume_model_exists` to hide a typo.
57
64
  - Start with `RubyLLM.chat` for a conversation. Set options with chainable `with_*` methods. Use `RubyLLM::Agent` when instructions, tools, or options should be reused. Ordinary Ruby methods and jobs compose workflows.
58
65
  - Use individual operations directly for media, document processing, moderation, embeddings, and reranking. Read their typed results and use `save` on generated images, video, and speech. Check each operation's provider, model, and credentials; a configured chat provider may not offer every operation. `animate` waits for completion; use `animate_later` when the application needs to manage the job.
59
66
  - `ask` runs the conversation and returns a message. `ask_later` stages input and returns the chat. For explicit loop control, use `generate`, `run_tools`, `step`, and `complete?`. Approval can park the loop before completion; inspect `awaiting_approval?` and resume after `approve` or `deny`.
60
67
  - Read text through `message.content` and structured output through `message.parsed`. Declare schemas with `Schematist::Schema` or the schema DSL. Do not assume `content` is a parsed Hash.
61
- - Subclass `RubyLLM::Tool`. Use `description`, `parameter`, or a `parameters` block, and keyword arguments on `execute`. Register tools with `with_tools`; configure choice and concurrency with `with_tool_options`. Keep application authorization in the tool or underlying service. Use `requires_approval` when the product needs a user's decision before execution.
68
+ - Subclass `RubyLLM::Tool`. Use `description`, `parameter`, or a `parameters` block, and keyword arguments on `execute`. Register tools with `with_tools`; configure choice and concurrency with `with_tool_options`. Keep application authorization in the tool or underlying service. Use `requires_approval` when the product needs a user's decision before execution. With large tool sets, pass `defer: true` to `with_tools` or `with_mcp` (or declare `defer` on the class) so the model searches for tools instead of receiving them all (2.1).
62
69
  - Use `with_thinking`, `with_caching`, `with_compaction`, and `with_citations` without arguments to enable them and `false` to disable them. They reject `nil`. Read the relevant guide for supported options and model requirements.
70
+ - Connect MCP servers by subclassing `RubyLLM::MCP` (`url` or `command`, then `only` and `requires_approval` to shape tools) and attach them with `chat.with_mcp(...)` or the agent `mcp` macro (2.1). Do not add a separate MCP client gem.
71
+ - For a yes/no probability, a choice, or a score about application data, subclass `RubyLLM::Judge` and call `.judge(input)` instead of parsing free text from a chat (2.1). Backends are TypeSafe's Jev, OpenAI Decisions, and local decision models such as Clef with `provider: :ollama`.
72
+ - Use `with_fallbacks` for alternate models, `with_provider_tools` for provider-hosted tools, and `progress` inside a long-running tool with `after_tool_progress` on the chat (2.1) to report it.
73
+ - Use `RubyLLM.context` for per-tenant credentials or endpoints, and an agent `context` block when the configuration depends on the agent's inputs (2.1).
63
74
  - Use shared RubyLLM options when available. Reserve `provider_options` for provider-specific request fields. Do not add a second provider SDK for functionality RubyLLM already supplies.
64
- - Read tokens through `message.tokens.input`, `.output`, `.cache_read`, `.cache_write`, and `.thinking`; read cost through `message.cost.total`. Unknown cost is `nil`, not zero. The ledger includes individual attempts; a successful answer can include earlier failed attempts.
75
+ - Read tokens through `message.tokens.input`, `.output`, `.cache_read`, `.cache_write`, and `.thinking`; read cost through `message.cost.total`. Unknown cost is `nil`, not zero. The ledger includes individual attempts; a successful answer can include earlier failed attempts. Attribute usage with `owner:` on individual operations or `RubyLLM.with_usage_owner` (2.1). Persisted chat usage keeps both its chat and owner associations.
76
+ - Group related calls with `RubyLLM.workflow` and `workflow.step` for instrumentation. To send traces to an existing OpenTelemetry setup, call `RubyLLM::OpenTelemetry.enable` after configuring the SDK (2.1). RubyLLM never configures the SDK or exporters, and does not export prompts or responses.
65
77
  - RubyLLM enums are Symbols: `finish_reason == :stop`, thinking effort `:high`. Provider slugs and model IDs remain Strings.
66
78
 
67
79
  ## Integrate with Rails
68
80
 
69
81
  For a new integration, use `bin/rails generate ruby_llm:install`, review its migrations, and follow the application's migration workflow. Load packaged model data with `bin/rails ruby_llm:load_models`. `RubyLLM.models.refresh` performs a network refresh and persists the result.
70
82
 
71
- Applications own their chat and message models, declared with `acts_as_chat` and `acts_as_message`. RubyLLM owns the supporting model, tool-call, usage, and batch tables. Do not generate application `Model` or `ToolCall` classes from 1.x examples. Pass custom chat and message mappings to the generators when the application uses different names.
83
+ Applications own their chat and message models, declared with `acts_as_chat` and `acts_as_message`. RubyLLM owns the supporting model, tool-call, usage, and batch tables, plus MCP credential and provider file tables from 2.1. Do not generate application `Model` or `ToolCall` classes from 1.x examples. Pass custom chat and message mappings to the generators when the application uses different names.
72
84
 
73
85
  Use the same chat API on persisted records. Persisted `ask` returns the application's message record. Use the existing job backend for background work; retrieve and configure agents through their documented persistence API when a job resumes in another process. Prompt templates live under `app/prompts` by convention.
74
86
 
75
87
  Pass Active Storage attachments through `with:` and retain the message attachment association set up by the install generator. For a Hotwire UI, use the Rails streaming guide or `ruby_llm:chat_ui` generator: persisted messages provide the targets for Turbo Streams. Individual operations work directly in Rails services and jobs with the same methods as plain Ruby.
76
88
 
77
- For an upgrade, read the complete matching upgrade guide before editing migrations. The 2.0 cutover requires preparation, backfill, finish, and application-specific reconciliation before affected activity resumes. Legacy-column cleanup is a later phase. Checkpoints make a stopped backfill resumable; retained columns do not make a 1.x code rollback safe. Rehearse against an isolated snapshot and respect the application's deployment and recovery process.
89
+ For an upgrade, read the complete matching upgrade guide before editing migrations. Each release's upgrade covers only the changes since the previous release, so upgrade one release at a time; a 1.x application completes the 2.0 upgrade first. The 2.0 cutover requires preparation, backfill, finish, and application-specific reconciliation before affected activity resumes. Legacy-column cleanup is a later phase. Checkpoints make a stopped backfill resumable; retained columns do not make a 1.x code rollback safe. Rehearse against an isolated snapshot and respect the application's deployment and recovery process.
90
+
91
+ ## Evaluate AI behavior
92
+
93
+ Specs check code; evaluations check how often the model gets the answer right (2.1). Put a `RubyLLM::Evaluation` subclass and its dataset in `app/evals`, named after the class (`support_evaluation.rb` and `support_evaluation.yml`). `perform(input)` runs the application and returns a string, Message, Chat, or Agent. Each case has `name`, `inputs`, and usually `expected_output`.
94
+
95
+ - Without declared criteria, a model checks each answer against `expected_output`, which every case must then supply. `evaluation :name, "statement"` declares criteria and replaces that default; `evaluation :correctness` without a description keeps it.
96
+ - `assertions` uses Minitest assertions with `output`, `tool_calls`, `messages`, `input`, `expected_output`, and `metadata`. Assertions do not turn off grading; `evaluator false` does. Outside Rails, assertions need `gem "minitest"`.
97
+ - Choose the grader with `evaluator model: ...`, an Agent class, or a Judge class with `evaluation :name, minimum: 0.8`.
98
+ - Run with `bin/rails "ruby_llm:eval[SupportEvaluation]"`, `SupportEvaluation.run`, or `evaluates SupportEvaluation` in RSpec or Minitest after extending `RubyLLM::Evaluation::RSpec` or `RubyLLM::Evaluation::Minitest`. Model grading costs money on every run, so keep graded evaluations out of the default test run unless the application records requests.
78
99
 
79
100
  ## Verify the application change
80
101