llm.rb 9.0.0 → 11.0.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +182 -4
  3. data/README.md +194 -42
  4. data/data/anthropic.json +278 -258
  5. data/data/bedrock.json +1288 -1561
  6. data/data/deepseek.json +38 -38
  7. data/data/google.json +656 -579
  8. data/data/openai.json +860 -818
  9. data/data/xai.json +243 -552
  10. data/data/zai.json +168 -168
  11. data/lib/llm/a2a/card/capabilities.rb +41 -0
  12. data/lib/llm/a2a/card/interface.rb +34 -0
  13. data/lib/llm/a2a/card/provider.rb +27 -0
  14. data/lib/llm/a2a/card/skill.rb +68 -0
  15. data/lib/llm/a2a/card.rb +144 -0
  16. data/lib/llm/a2a/error.rb +49 -0
  17. data/lib/llm/a2a/notifications.rb +53 -0
  18. data/lib/llm/a2a/tasks.rb +55 -0
  19. data/lib/llm/a2a/transport/http.rb +131 -0
  20. data/lib/llm/a2a.rb +452 -0
  21. data/lib/llm/active_record/acts_as_agent.rb +20 -9
  22. data/lib/llm/active_record/acts_as_llm.rb +4 -4
  23. data/lib/llm/active_record.rb +1 -6
  24. data/lib/llm/agent.rb +96 -71
  25. data/lib/llm/buffer.rb +1 -2
  26. data/lib/llm/context.rb +77 -50
  27. data/lib/llm/file.rb +7 -0
  28. data/lib/llm/function/call_task.rb +46 -0
  29. data/lib/llm/function.rb +28 -2
  30. data/lib/llm/mcp/transport/http.rb +5 -18
  31. data/lib/llm/mcp/transport/stdio.rb +7 -0
  32. data/lib/llm/mcp.rb +20 -17
  33. data/lib/llm/message.rb +1 -1
  34. data/lib/llm/object/kernel.rb +1 -1
  35. data/lib/llm/provider.rb +9 -9
  36. data/lib/llm/providers/anthropic/stream_parser.rb +2 -2
  37. data/lib/llm/providers/bedrock/stream_parser.rb +2 -2
  38. data/lib/llm/providers/google/stream_parser.rb +2 -2
  39. data/lib/llm/providers/openai/responses/stream_parser.rb +2 -2
  40. data/lib/llm/providers/openai/stream_parser.rb +2 -2
  41. data/lib/llm/response.rb +1 -1
  42. data/lib/llm/schema.rb +11 -0
  43. data/lib/llm/sequel/agent.rb +19 -9
  44. data/lib/llm/sequel/plugin.rb +9 -13
  45. data/lib/llm/stream.rb +11 -36
  46. data/lib/llm/tool/param.rb +1 -8
  47. data/lib/llm/tool.rb +57 -27
  48. data/lib/llm/tracer.rb +1 -1
  49. data/lib/llm/transport/http.rb +1 -1
  50. data/lib/llm/transport/stream_decoder.rb +6 -3
  51. data/lib/llm/transport/utils.rb +35 -0
  52. data/lib/llm/transport.rb +1 -0
  53. data/lib/llm/utils.rb +73 -0
  54. data/lib/llm/version.rb +1 -1
  55. data/lib/llm.rb +24 -4
  56. data/llm.gemspec +16 -1
  57. metadata +29 -5
  58. data/lib/llm/bot.rb +0 -3
  59. data/lib/llm/mcp/transport/http/event_handler.rb +0 -68
data/lib/llm/a2a.rb ADDED
@@ -0,0 +1,452 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The {LLM::A2A} class provides access to agents that implement the
5
+ # Agent2Agent (A2A) Protocol. A2A defines a standard way for
6
+ # independent AI agents to discover each other's capabilities,
7
+ # negotiate interaction modalities, and collaborate on tasks.
8
+ #
9
+ # In llm.rb, {LLM::A2A} supports both HTTP+JSON/REST and JSON-RPC 2.0
10
+ # protocol bindings and focuses on discovering agent skills that can be
11
+ # used through {LLM::Context} and {LLM::Agent}.
12
+ #
13
+ # Requests can be made concurrently and responses are matched by task id.
14
+ #
15
+ # @example REST binding (default)
16
+ # a2a = LLM::A2A.rest(url: "https://agent.example.com")
17
+ # card = a2a.card
18
+ # puts card.skills.map(&:name)
19
+ # task = a2a.send_message("What is the weather in Tokyo?").task
20
+ # a2a.tasks.get(task.id)
21
+ #
22
+ # @example JSON-RPC binding
23
+ # a2a = LLM::A2A.jsonrpc(url: "https://agent.example.com")
24
+ #
25
+ # @example Using skills as tools in a context
26
+ # llm = LLM.openai(key: ENV["KEY"])
27
+ # a2a = LLM::A2A.rest(url: "https://agent.example.com")
28
+ # ctx = LLM::Context.new(llm, tools: a2a.skills)
29
+ # ctx.talk("Analyze this data using the remote agent.")
30
+ # ctx.talk(ctx.wait(:call)) while ctx.functions?
31
+ class LLM::A2A
32
+ require_relative "a2a/card"
33
+ require_relative "a2a/error"
34
+ require_relative "a2a/tasks"
35
+ require_relative "a2a/notifications"
36
+ require_relative "a2a/transport/http"
37
+
38
+ ##
39
+ # @param [Symbol] binding
40
+ # The protocol binding to use. One of `:rest` (HTTP+JSON/REST) or
41
+ # `:jsonrpc` (JSON-RPC 2.0). Defaults to `:rest`.
42
+ # @param [Object] transport
43
+ # The transport used to communicate with the remote A2A agent
44
+ # @param [String] base_path
45
+ # Optional base path prefix for REST endpoints
46
+ # @param [String] protocol_version
47
+ # The expected A2A protocol version. Defaults to `"1.0"`.
48
+ # @return [LLM::A2A]
49
+ def initialize(transport:, binding: :rest, base_path: "", protocol_version: "1.0")
50
+ @binding = binding
51
+ @base_path = LLM::Utils.normalize_base_path(base_path)
52
+ @protocol_version = protocol_version
53
+ @transport = transport
54
+ end
55
+
56
+ ##
57
+ # Builds an A2A client over HTTP.
58
+ # @param [String] url
59
+ # The base URL of the A2A agent (e.g., "https://agent.example.com")
60
+ # @param [Hash<String, String>] headers
61
+ # Extra HTTP headers to include in requests (e.g., Authorization)
62
+ # @param [Integer, nil] timeout
63
+ # The timeout in seconds for HTTP requests
64
+ # @param [LLM::Transport, Class, nil] transport
65
+ # Optional override with any {LLM::Transport} instance or subclass
66
+ # @param [Symbol] binding
67
+ # The protocol binding to use. One of `:rest` or `:jsonrpc`
68
+ # @param [String] base_path
69
+ # Optional base path prefix for REST endpoints
70
+ # @param [String] protocol_version
71
+ # The expected A2A protocol version. Defaults to `"1.0"`.
72
+ # @return [LLM::A2A]
73
+ def self.http(url:, headers: {}, timeout: 30, transport: nil, binding: :rest, base_path: "", protocol_version: "1.0")
74
+ new(
75
+ binding:,
76
+ base_path:,
77
+ protocol_version:,
78
+ transport: Transport::HTTP.new(
79
+ url:,
80
+ headers:,
81
+ timeout:,
82
+ transport:,
83
+ protocol_version:
84
+ )
85
+ )
86
+ end
87
+
88
+ ##
89
+ # Builds an A2A client over HTTP+JSON/REST.
90
+ # @param [String] url
91
+ # @param [Hash<String, String>] headers
92
+ # @param [Integer, nil] timeout
93
+ # @param [LLM::Transport, Class, nil] transport
94
+ # @return [LLM::A2A]
95
+ def self.rest(url:, headers: {}, timeout: 30, transport: nil, base_path: "", protocol_version: "1.0")
96
+ http(
97
+ url:,
98
+ headers:,
99
+ timeout:,
100
+ transport:,
101
+ binding: :rest,
102
+ base_path:,
103
+ protocol_version:
104
+ )
105
+ end
106
+
107
+ ##
108
+ # Builds an A2A client over HTTP+JSON with JSON-RPC 2.0.
109
+ # @param [String] url
110
+ # @param [Hash<String, String>] headers
111
+ # @param [Integer, nil] timeout
112
+ # @param [LLM::Transport, Class, nil] transport
113
+ # @return [LLM::A2A]
114
+ def self.jsonrpc(url:, headers: {}, timeout: 30, transport: nil, base_path: "", protocol_version: "1.0")
115
+ http(
116
+ url:,
117
+ headers:,
118
+ timeout:,
119
+ transport:,
120
+ binding: :jsonrpc,
121
+ base_path:,
122
+ protocol_version:
123
+ )
124
+ end
125
+
126
+ ##
127
+ # Returns the active protocol binding.
128
+ # @return [Symbol]
129
+ attr_reader :binding
130
+
131
+ ##
132
+ # Returns the remote agent card.
133
+ #
134
+ # The agent card is fetched from `/.well-known/agent-card.json` and
135
+ # cached for the lifetime of this client instance.
136
+ # @return [LLM::A2A::Card]
137
+ def card
138
+ return @card if defined?(@card)
139
+ @card = LLM::A2A::Card.new(transport.get("/.well-known/agent-card.json"))
140
+ end
141
+ alias_method :agent_card, :card
142
+
143
+ ##
144
+ # Returns the agent's skills adapted as callable tools.
145
+ #
146
+ # Each skill in the agent card is mapped to an {LLM::Tool} subclass
147
+ # that wraps a {#send_message} call. When the tool is called, it
148
+ # sends a message to the remote agent and returns the task artifacts
149
+ # as the result.
150
+ # @return [Array<Class<LLM::Tool>>]
151
+ def skills
152
+ @skills ||= card.skills.map { LLM::Tool.a2a(self, _1) }
153
+ end
154
+ alias_method :tools, :skills
155
+
156
+ ##
157
+ # Returns task-oriented A2A operations.
158
+ # @return [LLM::A2A::Tasks]
159
+ def tasks
160
+ @tasks ||= LLM::A2A::Tasks.new(self)
161
+ end
162
+
163
+ ##
164
+ # Returns push notification configuration operations.
165
+ # @return [LLM::A2A::Notifications]
166
+ def notifications
167
+ @notifications ||= LLM::A2A::Notifications.new(self)
168
+ end
169
+
170
+ ##
171
+ # Sends a message to the agent and returns the response.
172
+ # @param [String] text The message text to send
173
+ # @param [Hash] config
174
+ # Optional configuration (accepted_output_modes, return_immediately)
175
+ # @param [Hash, nil] metadata
176
+ # Optional metadata to attach to the request
177
+ # @return [LLM::Object] The task or message response
178
+ def send_message(text, configuration = {}, metadata: nil)
179
+ body = build_request(
180
+ "SendMessage",
181
+ message: {role: "ROLE_USER", parts: [{text:}], messageId: SecureRandom.uuid},
182
+ configuration:,
183
+ metadata:
184
+ )
185
+ execute_request(body)
186
+ end
187
+
188
+ ##
189
+ # Sends a streaming message to the agent.
190
+ #
191
+ # The block is called for each {LLM::Object} event in the stream
192
+ # (Task, Message, TaskStatusUpdateEvent, TaskArtifactUpdateEvent).
193
+ # @param [String] text The message text to send
194
+ # @param [Hash] config Optional configuration
195
+ # @yieldparam [LLM::Object] event A stream event
196
+ # @return [void]
197
+ def send_streaming_message(text, configuration = {}, &on_event)
198
+ body = build_request(
199
+ "SendStreamingMessage",
200
+ message: {role: "ROLE_USER", parts: [{text:}], messageId: SecureRandom.uuid},
201
+ configuration:
202
+ )
203
+ execute_stream(body, &on_event)
204
+ end
205
+
206
+ ##
207
+ # Gets the current state of a task.
208
+ # @param [String] task_id The task ID to retrieve
209
+ # @param [Integer, nil] history_length
210
+ # Optional limit on recent messages to include
211
+ # @return [LLM::Object]
212
+ def get_task(task_id, history_length: nil)
213
+ case @binding
214
+ when :rest
215
+ path = rest_path("/tasks/#{task_id}")
216
+ path = "#{path}?historyLength=#{history_length}" if history_length
217
+ res = transport.get(path)
218
+ when :jsonrpc
219
+ body = build_request("GetTask", id: task_id, historyLength: history_length)
220
+ res = transport.post("/", body)
221
+ else
222
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
223
+ end
224
+ LLM::Object.from(res)
225
+ end
226
+
227
+ ##
228
+ # Cancels a task in progress.
229
+ # @param [String] task_id The task ID to cancel
230
+ # @param [Hash, nil] metadata Optional metadata to attach to the request
231
+ # @return [LLM::Object]
232
+ def cancel_task(task_id, metadata: nil)
233
+ body = build_request("CancelTask", id: task_id, metadata:)
234
+ case @binding
235
+ when :rest
236
+ res = transport.post(rest_path("/tasks/#{task_id}:cancel"), body)
237
+ when :jsonrpc
238
+ res = transport.post("/", body)
239
+ else
240
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
241
+ end
242
+ LLM::Object.from(res)
243
+ end
244
+
245
+ ##
246
+ # Subscribes to streaming updates for an existing task.
247
+ # @param [String] task_id The task ID to subscribe to
248
+ # @yieldparam [LLM::Object] event A stream event
249
+ # @return [void]
250
+ def subscribe_to_task(task_id, &on_event)
251
+ case @binding
252
+ when :rest
253
+ transport.get_stream(rest_path("/tasks/#{task_id}:subscribe")) { on_event&.call(LLM::Object.from(_1)) }
254
+ when :jsonrpc
255
+ body = build_request("SubscribeToTask", id: task_id)
256
+ transport.post_stream("/", body) { on_event&.call(LLM::Object.from(_1)) }
257
+ else
258
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
259
+ end
260
+ end
261
+
262
+ ##
263
+ # Lists tasks with optional filtering.
264
+ # @param [String, nil] context_id Optional context ID to filter by
265
+ # @param [String, nil] status Optional task state to filter by
266
+ # @param [Integer, nil] history_length Optional limit on recent messages to include
267
+ # @param [String, nil] status_timestamp_after Optional lower bound for status timestamp filtering
268
+ # @param [Boolean, nil] include_artifacts Whether to include task artifacts
269
+ # @param [Integer] page_size Maximum number of tasks to return
270
+ # @param [String, nil] page_token Pagination cursor
271
+ # @return [LLM::Object]
272
+ def list_tasks(context_id: nil, status: nil, history_length: nil, status_timestamp_after: nil,
273
+ include_artifacts: nil, page_size: 20, page_token: nil)
274
+ case @binding
275
+ when :rest
276
+ params = {}
277
+ params[:contextId] = context_id if context_id
278
+ params[:status] = status if status
279
+ params[:historyLength] = history_length if history_length
280
+ params[:statusTimestampAfter] = status_timestamp_after if status_timestamp_after
281
+ params[:includeArtifacts] = include_artifacts unless include_artifacts.nil?
282
+ params[:pageSize] = page_size if page_size
283
+ params[:pageToken] = page_token if page_token
284
+ query = URI.encode_www_form(params)
285
+ path = rest_path("/tasks")
286
+ path = "#{path}?#{query}" unless query.empty?
287
+ res = transport.get(path)
288
+ when :jsonrpc
289
+ body = build_request("ListTasks", contextId: context_id, status: status,
290
+ historyLength: history_length,
291
+ statusTimestampAfter: status_timestamp_after,
292
+ includeArtifacts: include_artifacts,
293
+ pageSize: page_size, pageToken: page_token)
294
+ res = transport.post("/", body)
295
+ else
296
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
297
+ end
298
+ LLM::Object.from(res)
299
+ end
300
+
301
+ ##
302
+ # Creates a push notification configuration for a task.
303
+ # @param [String] task_id The parent task ID
304
+ # @param [String] url The callback URL
305
+ # @param [String, nil] token Optional token to include with notifications
306
+ # @param [Hash, nil] authentication Optional authentication information
307
+ # @param [String, nil] id Optional configuration ID
308
+ # @return [LLM::Object]
309
+ def create_task_push_notification_config(task_id, url:, token: nil, authentication: nil, id: nil)
310
+ body = build_request("CreateTaskPushNotificationConfig", taskId: task_id, url:, token:, authentication:, id:)
311
+ case @binding
312
+ when :rest
313
+ res = transport.post(rest_path("/tasks/#{task_id}/pushNotificationConfigs"), body)
314
+ when :jsonrpc
315
+ res = transport.post("/", body)
316
+ else
317
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
318
+ end
319
+ LLM::Object.from(res)
320
+ end
321
+
322
+ ##
323
+ # Retrieves a push notification configuration for a task.
324
+ # @param [String] task_id The parent task ID
325
+ # @param [String] id The configuration ID
326
+ # @return [LLM::Object]
327
+ def get_task_push_notification_config(task_id, id)
328
+ case @binding
329
+ when :rest
330
+ res = transport.get(rest_path("/tasks/#{task_id}/pushNotificationConfigs/#{id}"))
331
+ when :jsonrpc
332
+ body = build_request("GetTaskPushNotificationConfig", taskId: task_id, id:)
333
+ res = transport.post("/", body)
334
+ else
335
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
336
+ end
337
+ LLM::Object.from(res)
338
+ end
339
+
340
+ ##
341
+ # Lists push notification configurations for a task.
342
+ # @param [String] task_id The parent task ID
343
+ # @param [Integer, nil] page_size Maximum number of configurations to return
344
+ # @param [String, nil] page_token Pagination cursor
345
+ # @return [LLM::Object]
346
+ def list_task_push_notification_configs(task_id, page_size: nil, page_token: nil)
347
+ case @binding
348
+ when :rest
349
+ params = {}
350
+ params[:pageSize] = page_size if page_size
351
+ params[:pageToken] = page_token if page_token
352
+ query = URI.encode_www_form(params)
353
+ path = rest_path("/tasks/#{task_id}/pushNotificationConfigs")
354
+ path = "#{path}?#{query}" unless query.empty?
355
+ res = transport.get(path)
356
+ when :jsonrpc
357
+ body = build_request("ListTaskPushNotificationConfigs", taskId: task_id, pageSize: page_size, pageToken: page_token)
358
+ res = transport.post("/", body)
359
+ else
360
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
361
+ end
362
+ LLM::Object.from(res)
363
+ end
364
+
365
+ ##
366
+ # Deletes a push notification configuration for a task.
367
+ # @param [String] task_id The parent task ID
368
+ # @param [String] id The configuration ID
369
+ # @return [LLM::Object]
370
+ def delete_task_push_notification_config(task_id, id)
371
+ case @binding
372
+ when :rest
373
+ res = transport.delete(rest_path("/tasks/#{task_id}/pushNotificationConfigs/#{id}"))
374
+ when :jsonrpc
375
+ body = build_request("DeleteTaskPushNotificationConfig", taskId: task_id, id:)
376
+ res = transport.post("/", body)
377
+ else
378
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
379
+ end
380
+ LLM::Object.from(res)
381
+ end
382
+
383
+ ##
384
+ # Returns the authenticated extended agent card.
385
+ # @return [LLM::A2A::Card]
386
+ def extended_card
387
+ case @binding
388
+ when :rest
389
+ res = transport.get(rest_path("/extendedAgentCard"))
390
+ when :jsonrpc
391
+ body = build_request("GetExtendedAgentCard")
392
+ res = transport.post("/", body)
393
+ else
394
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
395
+ end
396
+ LLM::A2A::Card.new(res)
397
+ end
398
+ alias_method :get_extended_agent_card, :extended_card
399
+
400
+ ##
401
+ # @return [String]
402
+ def inspect
403
+ "#<#{LLM::Utils.object_id(self)} @binding=#{@binding.inspect}>"
404
+ end
405
+
406
+ private
407
+
408
+ attr_reader :transport
409
+
410
+ def build_request(method, **params)
411
+ case @binding
412
+ when :rest
413
+ params
414
+ when :jsonrpc
415
+ {jsonrpc: "2.0", method:, params: params.compact, id: SecureRandom.uuid}
416
+ else
417
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
418
+ end
419
+ end
420
+
421
+ def execute_request(body)
422
+ res = case @binding
423
+ when :rest
424
+ transport.post(rest_path("/message:send"), body)
425
+ when :jsonrpc
426
+ res = transport.post("/", body)
427
+ if res["error"]
428
+ raise LLM::A2A::Error.new(res["error"]["message"], res["error"]["code"])
429
+ end
430
+ res["result"] || res
431
+ else
432
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
433
+ end
434
+ LLM::Object.from(res)
435
+ end
436
+
437
+ def execute_stream(body, &on_event)
438
+ case @binding
439
+ when :rest
440
+ transport.post_stream(rest_path("/message:stream"), body) { on_event&.call(LLM::Object.from(_1)) }
441
+ when :jsonrpc
442
+ transport.post_stream("/", body) { on_event&.call(LLM::Object.from(_1)) }
443
+ else
444
+ raise LLM::A2A::Error, "Invalid A2A binding: #{@binding.inspect}"
445
+ end
446
+ end
447
+
448
+ def rest_path(path)
449
+ return path if @base_path.empty?
450
+ "#{@base_path}#{path}"
451
+ end
452
+ end
@@ -11,19 +11,24 @@ module LLM::ActiveRecord
11
11
  # class and forwarded to an internal agent subclass.
12
12
  module ActsAsAgent
13
13
  module ClassMethods
14
- def model(model = nil)
15
- return agent.model if model.nil?
16
- agent.model(model)
14
+ def model(model = nil, &block)
15
+ return agent.model if model.nil? && !block
16
+ agent.model(model, &block)
17
17
  end
18
18
 
19
- def tools(*tools)
20
- return agent.tools if tools.empty?
21
- agent.tools(*tools)
19
+ def tools(*tools, &block)
20
+ return agent.tools if tools.empty? && !block
21
+ agent.tools(*tools, &block)
22
22
  end
23
23
 
24
- def schema(schema = nil)
25
- return agent.schema if schema.nil?
26
- agent.schema(schema)
24
+ def skills(*skills, &block)
25
+ return agent.skills if skills.empty? && !block
26
+ agent.skills(*skills, &block)
27
+ end
28
+
29
+ def schema(schema = nil, &block)
30
+ return agent.schema if schema.nil? && !block
31
+ agent.schema(schema, &block)
27
32
  end
28
33
 
29
34
  def instructions(instructions = nil)
@@ -36,6 +41,11 @@ module LLM::ActiveRecord
36
41
  agent.concurrency(concurrency)
37
42
  end
38
43
 
44
+ def confirm(*tool_names, &block)
45
+ return agent.confirm if tool_names.empty? && !block
46
+ agent.confirm(*tool_names, &block)
47
+ end
48
+
39
49
  def tracer(tracer = nil, &block)
40
50
  return agent.tracer if tracer.nil? && !block
41
51
  agent.tracer(tracer, &block)
@@ -116,6 +126,7 @@ module LLM::ActiveRecord
116
126
  when :json, :jsonb then ctx.restore(data:)
117
127
  else raise ArgumentError, "Unknown format: #{options[:format].inspect}"
118
128
  end
129
+ ctx
119
130
  end
120
131
  end
121
132
  end
@@ -59,12 +59,12 @@ module LLM::ActiveRecord
59
59
  end
60
60
 
61
61
  ##
62
- # Continues the stored context through the Responses API and flushes it.
63
- # @see LLM::Context#respond
62
+ # Continues the stored context with new input and flushes it.
63
+ # @see LLM::Context#ask
64
64
  # @return [LLM::Response]
65
- def respond(...)
65
+ def ask(...)
66
66
  options = self.class.llm_plugin_options
67
- ctx.respond(...).tap { Utils.save!(self, ctx, options) }
67
+ ctx.ask(...).tap { Utils.save!(self, ctx, options) }
68
68
  end
69
69
 
70
70
  ##
@@ -20,12 +20,7 @@ module LLM::ActiveRecord
20
20
  # Resolves a single configured option against a model instance.
21
21
  # @return [Object]
22
22
  def self.resolve_option(obj, option)
23
- case option
24
- when Proc then obj.instance_exec(&option)
25
- when Symbol then obj.send(option)
26
- when Hash then option.dup
27
- else option
28
- end
23
+ LLM::Utils.resolve_option(obj, option)
29
24
  end
30
25
 
31
26
  ##