llm.rb 0.4.0 → 0.4.2

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: 3d8311b461c49095ba393be6d3456bf7c3032a07e4480f4fd3e4cd9133f2b6e7
4
- data.tar.gz: 5f9ce812a4a27e0982ea5072bca3f0494317ee0274987e57df792cc0e0d2fcfc
3
+ metadata.gz: 2ae98573410bfc74a61f0bf15f811c30800b6567c9e5fc9e4cbe50f4994a200f
4
+ data.tar.gz: 9776839912017f5f3bac8452670c60b3bc9eabecebe11c402458a36b78d958e7
5
5
  SHA512:
6
- metadata.gz: 1d860cabe75a0718e0c06d686127d6d24c65b4dc8967aaf490b892bfad10bf88268791e9138b26e2d1c646a22e2eda7e74ad48e1fa31e9cb68c42de7ec53ac12
7
- data.tar.gz: 8c2e14a316e87560e5d5dc3cdb416d151172ae12e2244bb813ff1bba61c582474607bd6920f82fa5f080e628b2dac319dd20d4f74eff55940f9c88ab7544b327
6
+ metadata.gz: 4252d2d861d409428067415340c2d36f2e75f34f3be9905a576a63455902d1faf4100d8c8c7060e683fe922ff095cc00fd56fd135b9589a746d9de716c66e5e5
7
+ data.tar.gz: 819676961e401aa5e27678e3d36af406362f0a68b0243e1e1f71f5f135286830dd1c009c47c69c26b6789a771da6a1fe72eb023f0f1833c20ffadac37f58ed1b
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Copyright (C) 2025
2
+ Antar Azri <azantar@proton.me>
3
+ 0x1eef <0x1eef@proton.me>
4
+
5
+ Permission to use, copy, modify, and/or distribute this
6
+ software for any purpose with or without fee is hereby
7
+ granted.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS
10
+ ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
11
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
12
+ EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
13
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
14
+ RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
16
+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
17
+ OF THIS SOFTWARE.
data/README.md CHANGED
@@ -133,7 +133,7 @@ The interface is designed so you could drop in any other library in its place:
133
133
  require "llm"
134
134
 
135
135
  llm = LLM.openai(ENV["KEY"])
136
- schema = llm.schema.object({os: llm.schema.string.enum("OpenBSD", "FreeBSD", "NetBSD").required})
136
+ schema = llm.schema.object({os: llm.schema.string.enum("OpenBSD", "FreeBSD", "NetBSD")})
137
137
  bot = LLM::Chat.new(llm, schema:)
138
138
  bot.chat "You secretly love NetBSD", :system
139
139
  bot.chat "What operating system is the best?", :user
@@ -364,7 +364,7 @@ bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n"
364
364
  Generally all providers accept text prompts but some providers can
365
365
  also understand URLs, and various file types (eg images, audio, video,
366
366
  etc). The llm.rb approach to multimodal prompts is to let you pass `URI`
367
- objects to describe links, `LLM::File` / `LLM::Response::File` objects
367
+ objects to describe links, `LLM::File` | `LLM::Response::File` objects
368
368
  to describe files, `String` objects to describe text blobs, or an array
369
369
  of the aforementioned objects to describe multiple objects in a single
370
370
  prompt. Each object is a first class citizen that can be passed directly
@@ -372,9 +372,7 @@ to a prompt.
372
372
 
373
373
  For more depth and examples on how to use the multimodal API, please see
374
374
  the [provider-specific documentation](https://0x1eef.github.io/x/llm.rb/)
375
- for more provider-specific examples &ndash; there can be subtle differences
376
- between providers and even between APIs from the same provider that are
377
- not covered in the README:
375
+ for more provider-specific examples:
378
376
 
379
377
  ```ruby
380
378
  #!/usr/bin/env ruby
@@ -2,9 +2,8 @@
2
2
 
3
3
  class JSON::Schema
4
4
  class Array < Leaf
5
- def initialize(items, **rest)
5
+ def initialize(*items)
6
6
  @items = items
7
- super(**rest)
8
7
  end
9
8
 
10
9
  def to_h
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class JSON::Schema
4
+ ##
5
+ # The {JSON::Schema::Leaf JSON::Schema::Leaf} class is the
6
+ # superclass of all values that can appear in a JSON schema.
7
+ # See the instance methods of {JSON::Schema JSON::Schema} for
8
+ # an example of how to create instances of {JSON::Schema::Leaf JSON::Schema::Leaf}
9
+ # through its subclasses.
4
10
  class Leaf
5
11
  def initialize
6
12
  @description = nil
@@ -9,30 +15,51 @@ class JSON::Schema
9
15
  @required = nil
10
16
  end
11
17
 
18
+ ##
19
+ # Set the description of a leaf
20
+ # @param [String] str The description
21
+ # @return [JSON::Schema::Leaf]
12
22
  def description(str)
13
23
  tap { @description = str }
14
24
  end
15
25
 
26
+ ##
27
+ # Set the default value of a leaf
28
+ # @param [Object] value The default value
29
+ # @return [JSON::Schema::Leaf]
16
30
  def default(value)
17
31
  tap { @default = value }
18
32
  end
19
33
 
34
+ ##
35
+ # Set the allowed values of a leaf
36
+ # @param [Array] values The allowed values
37
+ # @return [JSON::Schema::Leaf]
20
38
  def enum(*values)
21
39
  tap { @enum = values }
22
40
  end
23
41
 
42
+ ##
43
+ # Denote a leaf as required
44
+ # @return [JSON::Schema::Leaf]
24
45
  def required
25
46
  tap { @required = true }
26
47
  end
27
48
 
49
+ ##
50
+ # @return [Hash]
28
51
  def to_h
29
52
  {description: @description, default: @default, enum: @enum}.compact
30
53
  end
31
54
 
55
+ ##
56
+ # @return [String]
32
57
  def to_json(options = {})
33
58
  to_h.to_json(options)
34
59
  end
35
60
 
61
+ ##
62
+ # @return [Boolean]
36
63
  def required?
37
64
  @required
38
65
  end
@@ -0,0 +1,6 @@
1
+ module JSON
2
+ end unless defined?(JSON)
3
+
4
+ class JSON::Schema
5
+ VERSION = "0.1.0"
6
+ end
data/lib/json/schema.rb CHANGED
@@ -3,7 +3,25 @@
3
3
  module JSON
4
4
  end unless defined?(JSON)
5
5
 
6
+ ##
7
+ # The {JSON::Schema JSON::Schema} class represents a JSON schema,
8
+ # and provides methods that let you describe and produce a schema
9
+ # that can be used in various contexts that include the validation
10
+ # and generation of JSON data.
11
+ #
12
+ # @see https://json-schema.org/ JSON Schema Specification
13
+ # @see https://tour.json-schema.org/ JSON Schema Tour
14
+ #
15
+ # @example
16
+ # schema = JSON::Schema.new
17
+ # schema.object({
18
+ # name: schema.string.enum("John", "Jane").required,
19
+ # age: schema.integer.required,
20
+ # hobbies: schema.array(schema.string, schema.null).required,
21
+ # address: schema.object({street: schema.string}).required,
22
+ # })
6
23
  class JSON::Schema
24
+ require_relative "schema/version"
7
25
  require_relative "schema/leaf"
8
26
  require_relative "schema/object"
9
27
  require_relative "schema/array"
@@ -15,25 +33,22 @@ class JSON::Schema
15
33
 
16
34
  ##
17
35
  # Returns an object
18
- # @param properties [Hash] A hash of properties
19
- # @param rest [Hash] Any other options
36
+ # @param [Hash] properties A hash of properties
20
37
  # @return [JSON::Schema::Object]
21
- def object(properties, **rest)
22
- Object.new(properties, **rest)
38
+ def object(properties)
39
+ Object.new(properties)
23
40
  end
24
41
 
25
42
  ##
26
43
  # Returns an array
27
- # @param items [Array] An array of items
28
- # @param rest [Hash] Any other options
44
+ # @param [Array] items An array of items
29
45
  # @return [JSON::Schema::Array]
30
- def array(items, **rest)
31
- Array.new(items, **rest)
46
+ def array(*items)
47
+ Array.new(*items)
32
48
  end
33
49
 
34
50
  ##
35
51
  # Returns a string
36
- # @param rest [Hash] Any other options
37
52
  # @return [JSON::Schema::String]
38
53
  def string(...)
39
54
  String.new(...)
@@ -41,7 +56,6 @@ class JSON::Schema
41
56
 
42
57
  ##
43
58
  # Returns a number
44
- # @param rest [Hash] Any other options
45
59
  # @return [JSON::Schema::Number] a number
46
60
  def number(...)
47
61
  Number.new(...)
@@ -49,7 +63,6 @@ class JSON::Schema
49
63
 
50
64
  ##
51
65
  # Returns an integer
52
- # @param rest [Hash] Any other options
53
66
  # @return [JSON::Schema::Integer]
54
67
  def integer(...)
55
68
  Integer.new(...)
@@ -57,7 +70,6 @@ class JSON::Schema
57
70
 
58
71
  ##
59
72
  # Returns a boolean
60
- # @param rest [Hash] Any other options
61
73
  # @return [JSON::Schema::Boolean]
62
74
  def boolean(...)
63
75
  Boolean.new(...)
@@ -65,7 +77,6 @@ class JSON::Schema
65
77
 
66
78
  ##
67
79
  # Returns null
68
- # @param rest [Hash] Any other options
69
80
  # @return [JSON::Schema::Null]
70
81
  def null(...)
71
82
  Null.new(...)
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.2"
5
5
  end
data/llm.gemspec CHANGED
@@ -21,10 +21,9 @@ Gem::Specification.new do |spec|
21
21
  spec.metadata["source_code_uri"] = "https://github.com/llmrb/llm"
22
22
 
23
23
  spec.files = Dir[
24
- "README.md", "LICENSE.txt",
24
+ "README.md", "LICENSE",
25
25
  "lib/*.rb", "lib/**/*.rb",
26
- "spec/*.rb", "spec/**/*.rb",
27
- "share/llm/models/*.yml", "llm.gemspec"
26
+ "llm.gemspec"
28
27
  ]
29
28
  spec.require_paths = ["lib"]
30
29
 
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: 0.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antar Azri
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-04-29 00:00:00.000000000 Z
12
+ date: 2025-04-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: webmock
@@ -147,6 +147,7 @@ executables: []
147
147
  extensions: []
148
148
  extra_rdoc_files: []
149
149
  files:
150
+ - LICENSE
150
151
  - README.md
151
152
  - lib/json/schema.rb
152
153
  - lib/json/schema/array.rb
@@ -157,6 +158,7 @@ files:
157
158
  - lib/json/schema/number.rb
158
159
  - lib/json/schema/object.rb
159
160
  - lib/json/schema/string.rb
161
+ - lib/json/schema/version.rb
160
162
  - lib/llm.rb
161
163
  - lib/llm/buffer.rb
162
164
  - lib/llm/chat.rb
@@ -213,29 +215,6 @@ files:
213
215
  - lib/llm/utils.rb
214
216
  - lib/llm/version.rb
215
217
  - llm.gemspec
216
- - spec/anthropic/completion_spec.rb
217
- - spec/anthropic/embedding_spec.rb
218
- - spec/anthropic/models_spec.rb
219
- - spec/gemini/completion_spec.rb
220
- - spec/gemini/conversation_spec.rb
221
- - spec/gemini/embedding_spec.rb
222
- - spec/gemini/files_spec.rb
223
- - spec/gemini/images_spec.rb
224
- - spec/gemini/models_spec.rb
225
- - spec/llm/conversation_spec.rb
226
- - spec/ollama/completion_spec.rb
227
- - spec/ollama/conversation_spec.rb
228
- - spec/ollama/embedding_spec.rb
229
- - spec/ollama/models_spec.rb
230
- - spec/openai/audio_spec.rb
231
- - spec/openai/completion_spec.rb
232
- - spec/openai/embedding_spec.rb
233
- - spec/openai/files_spec.rb
234
- - spec/openai/images_spec.rb
235
- - spec/openai/models_spec.rb
236
- - spec/openai/responses_spec.rb
237
- - spec/readme_spec.rb
238
- - spec/setup.rb
239
218
  homepage: https://github.com/llmrb/llm
240
219
  licenses:
241
220
  - 0BSDL
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "setup"
4
-
5
- RSpec.describe "LLM::Anthropic: completions" do
6
- subject(:anthropic) { LLM.anthropic(token) }
7
- let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
8
-
9
- context "when given a successful response",
10
- vcr: {cassette_name: "anthropic/completions/successful_response"} do
11
- subject(:response) { anthropic.complete("Hello, world", :user) }
12
-
13
- it "returns a completion" do
14
- expect(response).to be_a(LLM::Response::Completion)
15
- end
16
-
17
- it "returns a model" do
18
- expect(response.model).to eq("claude-3-5-sonnet-20240620")
19
- end
20
-
21
- it "includes token usage" do
22
- expect(response).to have_attributes(
23
- prompt_tokens: 10,
24
- completion_tokens: 30,
25
- total_tokens: 40
26
- )
27
- end
28
-
29
- context "with a choice" do
30
- subject(:choice) { response.choices[0] }
31
-
32
- it "has choices" do
33
- expect(choice).to have_attributes(
34
- role: "assistant",
35
- content: "Hello! How can I assist you today? Feel free to ask me any questions or let me know if you need help with anything."
36
- )
37
- end
38
-
39
- it "includes the response" do
40
- expect(choice.extra[:response]).to eq(response)
41
- end
42
- end
43
- end
44
-
45
- context "when given a URI to an image",
46
- vcr: {cassette_name: "anthropic/completions/successful_response_uri_image"} do
47
- subject { response.choices[0].content.downcase[0..2] }
48
- let(:response) do
49
- anthropic.complete([
50
- "Is this image the flag of brazil ? ",
51
- "Answer with yes or no. ",
52
- "Nothing else.",
53
- uri
54
- ], :user)
55
- end
56
- let(:uri) { URI("https://upload.wikimedia.org/wikipedia/en/thumb/0/05/Flag_of_Brazil.svg/250px-Flag_of_Brazil.svg.png") }
57
-
58
- it "describes the image" do
59
- is_expected.to eq("yes")
60
- end
61
- end
62
-
63
- context "when given a local reference to an image",
64
- vcr: {cassette_name: "anthropic/completions/successful_response_file_image"} do
65
- subject { response.choices[0].content.downcase[0..2] }
66
- let(:response) do
67
- anthropic.complete([
68
- "Is this image a representation of a blue book ?",
69
- "Answer with yes or no.",
70
- "Nothing else.",
71
- file
72
- ], :user)
73
- end
74
- let(:file) { LLM::File("spec/fixtures/images/bluebook.png") }
75
-
76
- it "describes the image" do
77
- is_expected.to eq("yes")
78
- end
79
- end
80
-
81
- context "when given an unauthorized response",
82
- vcr: {cassette_name: "anthropic/completions/unauthorized_response"} do
83
- subject(:response) { anthropic.complete("Hello", :user) }
84
- let(:token) { "BADTOKEN" }
85
-
86
- it "raises an error" do
87
- expect { response }.to raise_error(LLM::Error::Unauthorized)
88
- end
89
-
90
- it "includes the response" do
91
- response
92
- rescue LLM::Error::Unauthorized => ex
93
- expect(ex.response).to be_kind_of(Net::HTTPResponse)
94
- end
95
- end
96
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "setup"
4
-
5
- RSpec.describe "LLM::Anthropic: embeddings" do
6
- let(:anthropic) { LLM.anthropic(token) }
7
- let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
8
-
9
- context "when given a successful response",
10
- vcr: {cassette_name: "anthropic/embeddings/successful_response"} do
11
- subject(:response) { anthropic.embed("Hello, world", token:) }
12
-
13
- it "returns an embedding" do
14
- expect(response).to be_instance_of(LLM::Response::Embedding)
15
- end
16
-
17
- it "returns a model" do
18
- expect(response.model).to eq("voyage-2")
19
- end
20
-
21
- it "has embeddings" do
22
- expect(response.embeddings).to be_instance_of(Array)
23
- end
24
- end
25
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "setup"
4
-
5
- RSpec.describe "LLM::Anthropic::Models" do
6
- let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
7
- let(:provider) { LLM.anthropic(token) }
8
-
9
- context "when given a successful list operation",
10
- vcr: {cassette_name: "anthropic/models/successful_list"} do
11
- subject { provider.models.all }
12
-
13
- it "is successful" do
14
- is_expected.to be_instance_of(LLM::Response::ModelList)
15
- end
16
-
17
- it "returns a list of models" do
18
- expect(subject.models).to all(be_a(LLM::Model))
19
- end
20
- end
21
- end
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "setup"
4
-
5
- RSpec.describe "LLM::Gemini: completions" do
6
- subject(:gemini) { LLM.gemini(token) }
7
- let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
8
-
9
- context "when given a successful response",
10
- vcr: {cassette_name: "gemini/completions/successful_response"} do
11
- subject(:response) { gemini.complete("Hello!", :user) }
12
-
13
- it "returns a completion" do
14
- expect(response).to be_a(LLM::Response::Completion)
15
- end
16
-
17
- it "returns a model" do
18
- expect(response.model).to eq("gemini-1.5-flash")
19
- end
20
-
21
- it "includes token usage" do
22
- expect(response).to have_attributes(
23
- prompt_tokens: 2,
24
- completion_tokens: 11,
25
- total_tokens: 13
26
- )
27
- end
28
-
29
- context "with a choice" do
30
- subject(:choice) { response.choices[0] }
31
-
32
- it "has choices" do
33
- expect(response).to be_a(LLM::Response::Completion).and have_attributes(
34
- choices: [
35
- have_attributes(
36
- role: "model",
37
- content: "Hello there! How can I help you today?\n"
38
- )
39
- ]
40
- )
41
- end
42
-
43
- it "includes the response" do
44
- expect(choice.extra[:response]).to eq(response)
45
- end
46
- end
47
- end
48
-
49
- context "when given a thread of messages",
50
- vcr: {cassette_name: "gemini/completions/successful_response_thread"} do
51
- subject(:response) do
52
- gemini.complete "What is your name? What age are you?", :user, messages: [
53
- {role: "user", content: "Answer all of my questions"},
54
- {role: "user", content: "Your name is Pablo, you are 25 years old and you are my amigo"}
55
- ]
56
- end
57
-
58
- it "has choices" do
59
- expect(response).to have_attributes(
60
- choices: [
61
- have_attributes(
62
- role: "model",
63
- content: "My name is Pablo, and I am 25 years old. ¡Amigo!\n"
64
- )
65
- ]
66
- )
67
- end
68
- end
69
-
70
- context "when given an unauthorized response",
71
- vcr: {cassette_name: "gemini/completions/unauthorized_response"} do
72
- subject(:response) { gemini.complete("Hello!", :user) }
73
- let(:token) { "BADTOKEN" }
74
-
75
- it "raises an error" do
76
- expect { response }.to raise_error(LLM::Error::Unauthorized)
77
- end
78
-
79
- it "includes a response" do
80
- response
81
- rescue LLM::Error::Unauthorized => ex
82
- expect(ex.response).to be_kind_of(Net::HTTPResponse)
83
- end
84
- end
85
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "setup"
4
-
5
- RSpec.describe "LLM::Chat: gemini" do
6
- let(:described_class) { LLM::Chat }
7
- let(:provider) { LLM.gemini(token) }
8
- let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
9
- let(:conversation) { described_class.new(provider, **params).lazy }
10
-
11
- context "when asked to describe an image",
12
- vcr: {cassette_name: "gemini/conversations/multimodal_response"} do
13
- subject { conversation.last_message }
14
-
15
- let(:params) { {} }
16
- let(:image) { LLM::File("spec/fixtures/images/bluebook.png") }
17
-
18
- before do
19
- conversation.chat(image, :user)
20
- conversation.chat("Describe the image with a short sentance", :user)
21
- end
22
-
23
- it "describes the image" do
24
- is_expected.to have_attributes(
25
- role: "model",
26
- content: "That's a simple illustration of a book " \
27
- "resting on a blue, X-shaped book stand.\n"
28
- )
29
- end
30
- end
31
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "setup"
4
-
5
- RSpec.describe "LLM::OpenAI: embeddings" do
6
- let(:gemini) { LLM.gemini(token) }
7
- let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
8
-
9
- context "when given a successful response",
10
- vcr: {cassette_name: "gemini/embeddings/successful_response"} do
11
- subject(:response) { gemini.embed("Hello, world") }
12
-
13
- it "returns an embedding" do
14
- expect(response).to be_instance_of(LLM::Response::Embedding)
15
- end
16
-
17
- it "returns a model" do
18
- expect(response.model).to eq("text-embedding-004")
19
- end
20
-
21
- it "has embeddings" do
22
- expect(response.embeddings).to be_instance_of(Array)
23
- end
24
- end
25
- end