llm.rb 4.19.0 → 4.20.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18c28f7b5ec05ad6f9629a6a328be4dd377a6efb43a13c2ac99dd0abf015b1f8
4
- data.tar.gz: a0c03de362af927d090a6bc09b2170ddb7eb5766f26b1f8f657a37bd10d0162e
3
+ metadata.gz: 58f2ff0f8147443face2f3d7c48b249b8aec30de4345fa286f87c622853cb516
4
+ data.tar.gz: 9dba9a0609fff95e141ee5a819ff454a9dbd5ecb9c987a1a3e3b73822431d6d2
5
5
  SHA512:
6
- metadata.gz: c8255c9435bec0a7a06fb427f3a1ade4e0b5ce47f111d33f4cbf1218fce1f89f1438c71980b52c3de7542563690f08bcf54417cb7d427720248a22f8b30a12fb
7
- data.tar.gz: 54aebea9a5f8ac687163d3df3c2c3d31091f8f8d6d96b5ad242199daa8fdfb8e5e17e2b626f7a682158ba9251202362fc9fddaed6c08e6eb8399c8b0509659d8
6
+ metadata.gz: 172de04003136f5b599f5b2c274d9354ca576512bc35e9af85c5672f32bd3ad5f85a8a0b7e60e29c60b8fa7e6bd8d39ed5d23692c60a4b6de0f2c941d542fd41
7
+ data.tar.gz: 9a3ef1da238e38ab51af3a20f235201bca736a41753c9848a589467676a979829dca6c7e5708ee12be344e9e0686ba4652504514b72769ff5d511c5d752dd9f2
data/CHANGELOG.md CHANGED
@@ -2,8 +2,49 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ Changes since `v4.20.1`.
6
+
7
+ ## v4.20.1
8
+
9
+ Changes since `v4.20.0`.
10
+
11
+ This patch release fixes ORM option resolution in the Sequel and
12
+ ActiveRecord wrappers. Symbol-based `provider:` and `context:` hooks now
13
+ resolve correctly, and internal default option constants are referenced
14
+ explicitly instead of relying on nested constant lookup.
15
+
16
+ ### Fix
17
+
18
+ * **Fix symbol-based ORM option hooks for provider and context hashes** <br>
19
+ Make `provider:` and `context:` resolve symbol hooks through the model in
20
+ the Sequel plugin and ActiveRecord wrappers instead of falling back to an
21
+ empty hash.
22
+
23
+ * **Fix ORM wrapper constant lookup for option defaults** <br>
24
+ Qualify internal `EMPTY_HASH` / `DEFAULTS` references in the Sequel plugin
25
+ and ActiveRecord wrappers so option resolution does not depend on nested
26
+ constant lookup quirks.
27
+
28
+ ## v4.20.0
29
+
5
30
  Changes since `v4.19.0`.
6
31
 
32
+ This release adds better support for tagged prompt content. `LLM::Context`
33
+ can now serialize and restore `image_url`, `local_file`, and `remote_file`
34
+ content cleanly, and `LLM::Message` now exposes helpers for inspecting
35
+ tagged image and file attachments.
36
+
37
+ ### Change
38
+
39
+ * **Round-trip tagged prompt objects through `LLM::Context`** <br>
40
+ Teach `LLM::Context` serialization and restore to preserve
41
+ `image_url`, `local_file`, and `remote_file` content across
42
+ `to_json` / `restore`.
43
+
44
+ * **Add attachment helpers to `LLM::Message`** <br>
45
+ Add `image_url?`, `image_urls`, `file?`, and `files` so callers can
46
+ inspect messages for tagged image and file content more directly.
47
+
7
48
  ## v4.19.0
8
49
 
9
50
  Changes since `v4.18.0`.
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <p align="center">
5
5
  <a href="https://0x1eef.github.io/x/llm.rb?rebuild=1"><img src="https://img.shields.io/badge/docs-0x1eef.github.io-blue.svg" alt="RubyDoc"></a>
6
6
  <a href="https://opensource.org/license/0bsd"><img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License"></a>
7
- <a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.19.0-green.svg?" alt="Version"></a>
7
+ <a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.20.1-green.svg?" alt="Version"></a>
8
8
  </p>
9
9
 
10
10
  ## About
@@ -150,8 +150,8 @@ module LLM::ActiveRecord
150
150
  # @return [Hash]
151
151
  def resolve_options(option)
152
152
  case option
153
- when Proc, Hash then resolve_option(option)
154
- else EMPTY_HASH.dup
153
+ when Proc, Symbol, Hash then resolve_option(option)
154
+ else ActsAsAgent::EMPTY_HASH.dup
155
155
  end
156
156
  end
157
157
 
@@ -270,8 +270,8 @@ module LLM::ActiveRecord
270
270
  # @return [Hash]
271
271
  def resolve_options(option)
272
272
  case option
273
- when Proc, Hash then resolve_option(option)
274
- else EMPTY_HASH.dup
273
+ when Proc, Symbol, Hash then resolve_option(option)
274
+ else ActsAsLLM::EMPTY_HASH.dup
275
275
  end
276
276
  end
277
277
 
@@ -4,6 +4,32 @@ class LLM::Context
4
4
  ##
5
5
  # @api private
6
6
  module Deserializer
7
+ ##
8
+ # Restore a saved context state
9
+ # @param [String, nil] path
10
+ # The path to a JSON file
11
+ # @param [String, nil] string
12
+ # A raw JSON string
13
+ # @param [Hash, nil] data
14
+ # A parsed context payload
15
+ # @raise [SystemCallError]
16
+ # Might raise a number of SystemCallError subclasses
17
+ # @return [LLM::Context]
18
+ def deserialize(path: nil, string: nil, data: nil)
19
+ ctx = if data
20
+ data
21
+ elsif path.nil? and string.nil?
22
+ raise ArgumentError, "a path, string, or data payload is required"
23
+ elsif path
24
+ LLM.json.load(::File.binread(path))
25
+ else
26
+ LLM.json.load(string)
27
+ end
28
+ @messages.concat [*ctx["messages"]].map { deserialize_message(_1) }
29
+ self
30
+ end
31
+ alias_method :restore, :deserialize
32
+
7
33
  ##
8
34
  # @param [Hash] payload
9
35
  # @return [LLM::Message]
@@ -14,12 +40,36 @@ class LLM::Context
14
40
  usage = payload["usage"]
15
41
  reasoning_content = payload["reasoning_content"]
16
42
  extra = {tool_calls:, original_tool_calls:, tools: @params[:tools], usage:, reasoning_content:}.compact
17
- content = returns.nil? ? payload["content"] : returns
43
+ content = returns.nil? ? deserialize_content(payload["content"]) : returns
18
44
  LLM::Message.new(payload["role"], content, extra)
19
45
  end
20
46
 
21
47
  private
22
48
 
49
+ def deserialize_content(content)
50
+ case content
51
+ when Array
52
+ content.map { deserialize_content(_1) }
53
+ when Hash
54
+ deserialize_object(content)
55
+ else
56
+ content
57
+ end
58
+ end
59
+
60
+ def deserialize_object(object)
61
+ case object["__llm_kind__"]
62
+ when "image_url"
63
+ LLM::Object.from(value: object["value"], kind: :image_url)
64
+ when "local_file"
65
+ LLM::Object.from(value: LLM.File(object["path"]), kind: :local_file)
66
+ when "remote_file"
67
+ LLM::Object.from(value: LLM::Object.from(object["value"] || {}), kind: :remote_file)
68
+ else
69
+ object
70
+ end
71
+ end
72
+
23
73
  def deserialize_tool_calls(items)
24
74
  items ||= []
25
75
  items.empty? ? nil : items
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Context
4
+ ##
5
+ # @api private
6
+ module Serializer
7
+ private
8
+
9
+ def serialize_message(message)
10
+ h = message.to_h
11
+ h[:content] = serialize_content(h[:content])
12
+ h
13
+ end
14
+
15
+ def serialize_content(content)
16
+ case content
17
+ when Array
18
+ content.map { serialize_content(_1) }
19
+ when LLM::Object
20
+ serialize_object(content)
21
+ else
22
+ content
23
+ end
24
+ end
25
+
26
+ def serialize_object(object)
27
+ case object.kind
28
+ when :image_url
29
+ {__llm_kind__: "image_url", value: object.value}
30
+ when :local_file
31
+ {__llm_kind__: "local_file", path: object.value.path}
32
+ when :remote_file
33
+ {__llm_kind__: "remote_file", value: serialize_remote_file(object.value)}
34
+ else
35
+ object.to_h
36
+ end
37
+ end
38
+
39
+ def serialize_remote_file(file)
40
+ {
41
+ "file?" => file.respond_to?(:file?) ? file.file? : true,
42
+ "id" => (file.id if file.respond_to?(:id)),
43
+ "filename" => (file.filename if file.respond_to?(:filename)),
44
+ "mime_type" => (file.mime_type if file.respond_to?(:mime_type)),
45
+ "uri" => (file.uri if file.respond_to?(:uri)),
46
+ "file_type" => (file.file_type if file.respond_to?(:file_type)),
47
+ "name" => (file.name if file.respond_to?(:name)),
48
+ "display_name" => (file.display_name if file.respond_to?(:display_name))
49
+ }.compact
50
+ end
51
+ end
52
+ end
data/lib/llm/context.rb CHANGED
@@ -34,7 +34,9 @@ module LLM
34
34
  # ctx.talk(prompt)
35
35
  # ctx.messages.each { |m| puts "[#{m.role}] #{m.content}" }
36
36
  class Context
37
+ require_relative "context/serializer"
37
38
  require_relative "context/deserializer"
39
+ include Serializer
38
40
  include Deserializer
39
41
 
40
42
  ##
@@ -279,7 +281,7 @@ module LLM
279
281
  ##
280
282
  # @return [Hash]
281
283
  def to_h
282
- {schema_version: 1, model:, messages:}
284
+ {schema_version: 1, model:, messages: @messages.map { serialize_message(_1) }}
283
285
  end
284
286
 
285
287
  ##
@@ -299,36 +301,10 @@ module LLM
299
301
  # Might raise a number of SystemCallError subclasses
300
302
  # @return [void]
301
303
  def serialize(path:)
302
- ::File.binwrite path, LLM.json.dump(self)
304
+ ::File.binwrite path, LLM.json.dump(to_h)
303
305
  end
304
306
  alias_method :save, :serialize
305
307
 
306
- ##
307
- # Restore a saved context state
308
- # @param [String, nil] path
309
- # The path to a JSON file
310
- # @param [String, nil] string
311
- # A raw JSON string
312
- # @param [Hash, nil] data
313
- # A parsed context payload
314
- # @raise [SystemCallError]
315
- # Might raise a number of SystemCallError subclasses
316
- # @return [LLM::Context]
317
- def deserialize(path: nil, string: nil, data: nil)
318
- ctx = if data
319
- data
320
- elsif path.nil? and string.nil?
321
- raise ArgumentError, "a path, string, or data payload is required"
322
- elsif path
323
- LLM.json.load(::File.binread(path))
324
- else
325
- LLM.json.load(string)
326
- end
327
- @messages.concat [*ctx["messages"]].map { deserialize_message(_1) }
328
- self
329
- end
330
- alias_method :restore, :deserialize
331
-
332
308
  ##
333
309
  # @return [LLM::Cost]
334
310
  # Returns an _approximate_ cost for a given context
data/lib/llm/message.rb CHANGED
@@ -74,6 +74,36 @@ module LLM
74
74
  extra.reasoning_content
75
75
  end
76
76
 
77
+ ##
78
+ # Returns true when a message contains an image URL
79
+ # @return [Boolean]
80
+ def image_url?
81
+ image_urls.any?
82
+ end
83
+
84
+ ##
85
+ # Returns image URL content items from the message
86
+ # @return [Array<LLM::Object>]
87
+ def image_urls
88
+ content_items.select { LLM::Object === _1 && _1.kind == :image_url }
89
+ end
90
+
91
+ ##
92
+ # Returns true when a message contains a local or remote file
93
+ # @return [Boolean]
94
+ def file?
95
+ files.any?
96
+ end
97
+
98
+ ##
99
+ # Returns local and remote file content items from the message
100
+ # @return [Array<LLM::Object>]
101
+ def files
102
+ content_items.select do
103
+ LLM::Object === _1 && [:local_file, :remote_file].include?(_1.kind)
104
+ end
105
+ end
106
+
77
107
  ##
78
108
  # @return [Array<LLM::Function>]
79
109
  def functions
@@ -178,5 +208,9 @@ module LLM
178
208
  tools = extra.tools || response&.__tools__ || []
179
209
  tools.map { _1.respond_to?(:function) ? _1.function : _1 }
180
210
  end
211
+
212
+ def content_items
213
+ Array(content)
214
+ end
181
215
  end
182
216
  end
@@ -79,7 +79,7 @@ module LLM::Sequel
79
79
  ##
80
80
  # @return [Hash]
81
81
  def llm_plugin_options
82
- @llm_plugin_options || DEFAULTS
82
+ @llm_plugin_options || Plugin::DEFAULTS
83
83
  end
84
84
  end
85
85
 
@@ -287,8 +287,8 @@ module LLM::Sequel
287
287
  # @return [Hash]
288
288
  def resolve_options(option)
289
289
  case option
290
- when Proc, Hash then resolve_option(option)
291
- else EMPTY_HASH.dup
290
+ when Proc, Symbol, Hash then resolve_option(option)
291
+ else Plugin::EMPTY_HASH.dup
292
292
  end
293
293
  end
294
294
 
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "4.19.0"
4
+ VERSION = "4.20.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.19.0
4
+ version: 4.20.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antar Azri
@@ -231,6 +231,7 @@ files:
231
231
  - lib/llm/buffer.rb
232
232
  - lib/llm/context.rb
233
233
  - lib/llm/context/deserializer.rb
234
+ - lib/llm/context/serializer.rb
234
235
  - lib/llm/contract.rb
235
236
  - lib/llm/contract/completion.rb
236
237
  - lib/llm/cost.rb