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
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class MCP
5
+ # A tool call that an MCP server runs in the background, as the Tasks
6
+ # extension describes. Declare MCP.extension :tasks, and a server may
7
+ # answer a long call with a task instead of a result.
8
+ #
9
+ # A chat never waits for a task. It pauses the tool call, and
10
+ # Chat#pending_tasks returns the tasks it waits on. #refresh checks on
11
+ # a task once; Chat#complete checks on every one again and resumes the
12
+ # chat once they are done:
13
+ #
14
+ # task = chat.pending_tasks.first
15
+ # task.refresh
16
+ # task.status # => :working
17
+ # task.status_message # => "Rendering page 3 of 12"
18
+ # task.poll_interval # => 5.0
19
+ #
20
+ # MCP#call waits for the task of a tool you call directly.
21
+ class Task
22
+ include Support::Inspectable
23
+
24
+ DONE = %i[completed failed cancelled].freeze
25
+ private_constant :DONE
26
+
27
+ # The task's id on its server.
28
+ attr_reader :id
29
+
30
+ # The ToolCall paused on the task, when it came from a chat.
31
+ attr_reader :tool_call
32
+
33
+ attr_reader :data, :answered # :nodoc:
34
+
35
+ def self.load(mcp, state, tool_call: nil) # :nodoc:
36
+ new(mcp, state['task'], answered: state['answered'], tool_call:)
37
+ end
38
+
39
+ def initialize(mcp, data, answered: nil, tool_call: nil) # :nodoc:
40
+ @mcp = mcp
41
+ @data = data
42
+ @id = data['taskId']
43
+ @answered = Array(answered)
44
+ @tool_call = tool_call
45
+ end
46
+
47
+ # The task's state: +:working+, +:input_required+, +:completed+,
48
+ # +:failed+, or +:cancelled+.
49
+ def status
50
+ data['status'].to_sym
51
+ end
52
+
53
+ # What the server says about the task's state, such as how far it
54
+ # got, or +nil+.
55
+ def status_message
56
+ data['statusMessage']
57
+ end
58
+
59
+ # How many seconds the server asks you to wait before checking on the
60
+ # task again, or +nil+.
61
+ def poll_interval
62
+ data['pollIntervalMs'] / 1000.0 if data['pollIntervalMs']
63
+ end
64
+
65
+ # When the server may forget the task, as a Time, or +nil+ when it
66
+ # keeps the task for good.
67
+ def expires_at
68
+ Time.iso8601(data['createdAt']) + (data['ttlMs'] / 1000.0) if data['ttlMs'] && data['createdAt']
69
+ end
70
+
71
+ # Returns +true+ once the task completed, failed, or was cancelled.
72
+ def done?
73
+ DONE.include?(status)
74
+ end
75
+
76
+ # Returns +true+ when the task finished with a #result.
77
+ def completed?
78
+ status == :completed
79
+ end
80
+
81
+ # Returns +true+ when the task failed with an error.
82
+ def failed?
83
+ status == :failed
84
+ end
85
+
86
+ # Returns +true+ when the task was cancelled before it finished.
87
+ def cancelled?
88
+ status == :cancelled
89
+ end
90
+
91
+ # Returns the MCP::Result once the task #completed?, and +nil+ until
92
+ # then. Raises MCP::Error when the task failed or was cancelled.
93
+ def result
94
+ raise error if failed? || cancelled?
95
+
96
+ Result.new(data['result']) if completed?
97
+ end
98
+
99
+ # Checks on the task once and returns +self+, which now tells whether
100
+ # the task is #done? and how soon to check again. Does nothing once
101
+ # the task is #done?.
102
+ def refresh
103
+ @data = connected.poll_task(id) unless done?
104
+ self
105
+ end
106
+
107
+ # Checks on the task until it is #done?, sleeping #poll_interval
108
+ # seconds in between, and returns +self+. Answers the server's
109
+ # requests for input with MCP.before_input_request callbacks.
110
+ # +timeout+ defaults to the server's MCP.timeout, and the task is
111
+ # cancelled when it runs out or the chat is cancelled.
112
+ #
113
+ # Raises MCP::Error when the task fails or the time runs out, and
114
+ # MCP::InputRequiredError when no callback answers a request.
115
+ def wait(timeout: nil, interval: nil)
116
+ connected.await_task(self, timeout:, interval:)
117
+ end
118
+
119
+ # Asks the server to cancel the task, and returns +self+. The server
120
+ # may still finish it, so check with #refresh.
121
+ def cancel
122
+ connected.cancel_task(id) unless done?
123
+ self
124
+ end
125
+
126
+ def error # :nodoc:
127
+ details = data['error'] || {}
128
+ Error.new(details['message'] || status_message || "Task #{id} was #{status}",
129
+ code: details['code'], data: details['data'])
130
+ end
131
+
132
+ def record_answers(keys) # :nodoc:
133
+ @answered |= keys
134
+ end
135
+
136
+ def to_h # :nodoc:
137
+ { 'task' => data, 'answered' => (answered unless answered.empty?) }.compact
138
+ end
139
+
140
+ private
141
+
142
+ def connected
143
+ @mcp or raise ConfigurationError, "Connect the MCP that runs task #{id} with with_mcp to reach it"
144
+ end
145
+
146
+ def inspect_attributes
147
+ { id:, status:, status_message: }
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class MCP
5
+ # A tool offered by an MCP server. It works anywhere a Tool does: a
6
+ # chat renders its name, description, and schema for the model, and
7
+ # calling it calls the server.
8
+ #
9
+ # tool = linear.tools.first
10
+ # tool.read_only? # => true
11
+ # chat.with_tools(tool)
12
+ #
13
+ # The behavior predicates read the server's annotations. They are hints
14
+ # from the server, so trust them only as far as you trust the server.
15
+ class Tool < RubyLLM::Tool
16
+ include Support::Inspectable
17
+
18
+ # The name the model calls this tool by.
19
+ attr_reader :name
20
+
21
+ # The description the model sees.
22
+ attr_reader :description
23
+
24
+ # The JSON Schema for the arguments the model provides.
25
+ attr_reader :parameters_schema
26
+
27
+ # The tool's name on the server.
28
+ attr_reader :server_name
29
+
30
+ # The tool's +_meta+ as the server sent it: a Hash with String keys,
31
+ # empty when there is none. Extensions keep their own vocabulary
32
+ # there.
33
+ attr_reader :meta
34
+
35
+ # The URI of the tool's UI, the +ui://+ resource an MCP App renders
36
+ # next to the tool's results, or +nil+ for a tool without one. Read
37
+ # it with MCP#resource.
38
+ attr_reader :ui_uri
39
+
40
+ # Who may call the tool, as Symbols: +:model+ when chats offer it to
41
+ # the model, +:app+ when a UI from the same server may call it. Tools
42
+ # say nothing about it unless they belong to an MCP App, which makes
43
+ # them <tt>[:model, :app]</tt>.
44
+ attr_reader :visibility
45
+
46
+ attr_reader :fixed_arguments, :wrap # :nodoc:
47
+
48
+ def initialize(mcp, definition, prefix: nil, as: nil, description: nil, fixed_arguments: {}, wrap: nil) # :nodoc:
49
+ super()
50
+ @mcp = mcp
51
+ @server_name = definition['name']
52
+ @name = (as || [prefix, server_name].compact.join('_')).to_s
53
+ @description = description || definition['description']
54
+ @fixed_arguments = fixed_arguments.transform_keys(&:to_sym)
55
+ @wrap = wrap
56
+ @annotations = definition['annotations'] || {}
57
+ @meta = definition['_meta'] || {}
58
+ @ui_uri = Apps.uri(meta)
59
+ @visibility = Apps.visibility(meta)
60
+ @parameters_schema = model_schema(definition['inputSchema'] || {})
61
+ end
62
+
63
+ # Returns whether the server says the tool only reads.
64
+ def read_only?
65
+ @annotations['readOnlyHint'] == true
66
+ end
67
+
68
+ # Returns whether the tool may destroy or overwrite data. Tools that
69
+ # are not read-only count as destructive unless the server says
70
+ # otherwise.
71
+ def destructive?
72
+ !read_only? && @annotations['destructiveHint'] != false
73
+ end
74
+
75
+ # Returns whether calling the tool again with the same arguments has
76
+ # no further effect.
77
+ def idempotent?
78
+ @annotations['idempotentHint'] == true
79
+ end
80
+
81
+ # Returns whether the tool reaches beyond the server, such as the web.
82
+ def open_world?
83
+ @annotations['openWorldHint'] != false
84
+ end
85
+
86
+ # Returns whether the tool pauses for approval, as declared with
87
+ # MCP.requires_approval.
88
+ def requires_approval?
89
+ @mcp.requires_approval?(self)
90
+ end
91
+
92
+ # Calls the tool on the server and returns its MCP::Result, what the
93
+ # +wrap:+ method made of it, or <tt>{ error: }</tt> when the tool
94
+ # failed. A chat sends the model the result's content. When the
95
+ # server runs the call as a task, returns the MCP::Task without
96
+ # waiting, and a chat pauses the tool call until the task is done.
97
+ # Raises MCP::InputRequiredError when the server needs input that no
98
+ # MCP.before_input_request callback gave.
99
+ def call(**arguments)
100
+ @mcp.run(self, arguments.except(:tool_call))
101
+ end
102
+
103
+ # Resumes a call that paused on input requests, now answered, or on a
104
+ # task, which it checks on once.
105
+ def resume(input, arguments) # :nodoc:
106
+ @mcp.run(self, arguments, input:)
107
+ end
108
+
109
+ # Returns the MCP::Task a call paused on, from its saved state.
110
+ def task(state, tool_call: nil) # :nodoc:
111
+ Task.load(@mcp, state, tool_call:)
112
+ end
113
+
114
+ private
115
+
116
+ def model_schema(schema)
117
+ schema = SchemaDefinition.new(schema:).json_schema
118
+ return schema if fixed_arguments.empty? || !schema.key?('properties')
119
+
120
+ hidden = fixed_arguments.keys.map(&:to_s)
121
+ schema.merge('properties' => schema['properties'].except(*hidden),
122
+ 'required' => Array(schema['required']) - hidden)
123
+ end
124
+
125
+ def inspect_attributes
126
+ { name:, from: (server_name unless server_name == name), read_only: read_only? || nil }
127
+ end
128
+ end
129
+ end
130
+ end