llm.rb 10.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.
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)
@@ -121,6 +126,7 @@ module LLM::ActiveRecord
121
126
  when :json, :jsonb then ctx.restore(data:)
122
127
  else raise ArgumentError, "Unknown format: #{options[:format].inspect}"
123
128
  end
129
+ ctx
124
130
  end
125
131
  end
126
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
  ##
data/lib/llm/agent.rb CHANGED
@@ -221,9 +221,14 @@ module LLM
221
221
  # response = agent.talk("Hello, what is your name?")
222
222
  # puts response.choices[0].content
223
223
  def talk(prompt, params = {})
224
- run_loop(prompt, params)
224
+ run_loop(prompt, params, :talk)
225
+ end
226
+
227
+ ##
228
+ # @see LLM::Context#ask
229
+ def ask(prompt, params = {})
230
+ run_loop(prompt, params, :ask)
225
231
  end
226
- alias_method :chat, :talk
227
232
 
228
233
  ##
229
234
  # @return [LLM::Buffer<LLM::Message>]
@@ -366,13 +371,13 @@ module LLM
366
371
  ##
367
372
  # @return [String]
368
373
  def to_json(...)
369
- to_h.to_json(...)
374
+ LLM.json.dump(to_h, ...)
370
375
  end
371
376
 
372
377
  ##
373
378
  # @return [String]
374
379
  def inspect
375
- "#<#{self.class.name}:0x#{object_id.to_s(16)} " \
380
+ "#<#{LLM::Utils.object_id(self)} " \
376
381
  "@llm=#{@llm.class}, @mode=#{mode.inspect}, @messages=#{messages.inspect}>"
377
382
  end
378
383
 
@@ -451,22 +456,23 @@ module LLM
451
456
  ##
452
457
  # Runs the tool loop
453
458
  # @api private
454
- def run_loop(prompt, params)
459
+ def run_loop(prompt, params, target)
455
460
  run = proc do
461
+ talk = @ctx.method(target)
456
462
  max = params.key?(:tool_attempts) ? params.delete(:tool_attempts) : 25
457
463
  max = Integer(max) if max
458
464
  stream = params[:stream] || @ctx.params[:stream]
459
465
  stream.extra[:concurrency] = concurrency if LLM::Stream === stream
460
- res = @ctx.talk(apply_instructions(prompt), params)
466
+ res = talk.call(apply_instructions(prompt), params)
461
467
  while @ctx.functions?
462
468
  if max
463
469
  max.times do
464
470
  break unless @ctx.functions?
465
- res = @ctx.talk(call_functions, params)
471
+ res = talk.call(call_functions, params)
466
472
  end
467
- res = @ctx.talk(@ctx.functions.map(&:rate_limit), params) if @ctx.functions?
473
+ res = talk.call(@ctx.functions.map(&:rate_limit), params) if @ctx.functions?
468
474
  else
469
- res = @ctx.talk(call_functions, params)
475
+ res = talk.call(call_functions, params)
470
476
  end
471
477
  end
472
478
  res
data/lib/llm/buffer.rb CHANGED
@@ -97,8 +97,7 @@ module LLM
97
97
  ##
98
98
  # @return [String]
99
99
  def inspect
100
- "#<#{self.class.name}:0x#{object_id.to_s(16)} " \
101
- "message_count=#{@messages.size}>"
100
+ "#<#{LLM::Utils.object_id(self)} message_count=#{@messages.size}>"
102
101
  end
103
102
 
104
103
  ##
data/lib/llm/context.rb CHANGED
@@ -201,12 +201,37 @@ module LLM
201
201
  @messages.concat [res.choices[-1]]
202
202
  res
203
203
  end
204
- alias_method :chat, :talk
204
+
205
+ ##
206
+ # Ask a question and return the content string directly.
207
+ # Accepts `with:` for file attachments and a block for streaming.
208
+ # This interface is compatible with RubyLLM's `ask` method.
209
+ # @param [String] prompt
210
+ # @param [Hash] options
211
+ # @option options [String, Array<String>, nil] :with
212
+ # File path(s) to attach
213
+ # @option options [#<<, LLM::Stream, nil] :stream
214
+ # A stream target
215
+ # @yield [String] content chunks when streaming
216
+ # @return [LLM::Response]
217
+ def ask(prompt, options = {}, &block)
218
+ options = {with: nil, stream: nil}.merge!(options || {})
219
+ with, stream = options.values_at(:with, :stream)
220
+ prompt = with ? [prompt, [*with].map { local_file(_1) }] : prompt
221
+ target = if block
222
+ blk = block.dup
223
+ blk.singleton_class.alias_method(:<<, :call)
224
+ blk
225
+ else
226
+ stream
227
+ end
228
+ target ? talk(prompt, stream: target) : talk(prompt)
229
+ end
205
230
 
206
231
  ##
207
232
  # @return [String]
208
233
  def inspect
209
- "#<#{self.class.name}:0x#{object_id.to_s(16)} " \
234
+ "#<#{LLM::Utils.object_id(self)} " \
210
235
  "@llm=#{@llm.class}, @mode=#{@mode.inspect}, @params=#{@params.inspect}, " \
211
236
  "@messages=#{@messages.inspect}>"
212
237
  end
@@ -414,7 +439,7 @@ module LLM
414
439
  ##
415
440
  # @return [String]
416
441
  def to_json(...)
417
- to_h.to_json(...)
442
+ LLM.json.dump(to_h, ...)
418
443
  end
419
444
 
420
445
  ##
@@ -475,8 +500,9 @@ module LLM
475
500
  # Returns the bound stream queue, if available.
476
501
  # @api private
477
502
  def queue
478
- return @queue if @queue
479
- stream.queue if LLM::Stream === stream
503
+ [@queue, stream&.queue].compact.first
504
+ rescue NoMethodError
505
+ nil
480
506
  end
481
507
 
482
508
  ##
data/lib/llm/file.rb CHANGED
@@ -43,6 +43,13 @@ class LLM::File
43
43
  mime_type == "application/pdf"
44
44
  end
45
45
 
46
+ ##
47
+ # @return [Boolean]
48
+ # Returns true if the file exists on disk
49
+ def exist?
50
+ File.exist?(path)
51
+ end
52
+
46
53
  ##
47
54
  # @return [Integer]
48
55
  # Returns the size of the file in bytes
data/lib/llm/function.rb CHANGED
@@ -182,7 +182,7 @@ class LLM::Function
182
182
  def define(klass = nil, &b)
183
183
  @runner = klass || b
184
184
  end
185
- alias_method :register, :define
185
+ alias_method :def, :define
186
186
 
187
187
  ##
188
188
  # Call the function