llm.rb 3.1.0 → 4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa682f0c6793298daeaac88092cb52f03652cbbbf28adfd6b62f94b8a263f3f3
4
- data.tar.gz: 1fb08983372becef70d866bdc4ee79ee8d8bba55ace5d4be4637a69e91341747
3
+ metadata.gz: cc70b8eb2d7ce82b3959d2b7dc795a89511a1962ed443a5344bb00ef55863033
4
+ data.tar.gz: a9245348fccc085710ae28097b9ce9c0ec9ce8e8f5ea4e23f97a9bde5fc50fee
5
5
  SHA512:
6
- metadata.gz: 720e09be8b25a9fde7d92887636d572edcdbd39a1b3a23ae1f44baaddb9f881c95927f63f16248f3a3d22da1704973f69f51309c487e1c97195175b772499b0d
7
- data.tar.gz: 8cf35f7829b4e66ef002652643779658cf9c8cf8726f8b563eb5ca59ebcfc3a71eeb9b4cc473dfc4556324448855b6733fe3d48a73fb6e70fb91102544eb7061
6
+ metadata.gz: b1a0e67e1d938792da4cf52ff6b05dba568b71c77d28ef18c11510c7f0c37b21d5514f659ae6997193774755aede0bd5af4a1239247fc396b8a4815258723eb6
7
+ data.tar.gz: 87bfee8769ba983ffccef6bfb276922501e8cc68b2b4f2be6857408739b7307403c120de7db1b83c612a6af61e30860366419abb790662feb679ddf6f1234102
data/README.md CHANGED
@@ -13,13 +13,15 @@ tool calling, audio, images, files, and structured outputs.
13
13
 
14
14
  #### REPL
15
15
 
16
- A simple chatbot that maintains a conversation and streams responses in real-time:
16
+ The [LLM::Bot](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Bot.html) class provides
17
+ a session with an LLM provider that maintains conversation history and context across
18
+ multiple requests. The following example implements a simple REPL loop:
17
19
 
18
20
  ```ruby
19
21
  #!/usr/bin/env ruby
20
22
  require "llm"
21
23
 
22
- llm = LLM.openai(key: ENV.fetch("KEY"))
24
+ llm = LLM.openai(key: ENV["KEY"])
23
25
  bot = LLM::Bot.new(llm, stream: $stdout)
24
26
  loop do
25
27
  print "> "
@@ -28,34 +30,12 @@ loop do
28
30
  end
29
31
  ```
30
32
 
31
- #### Prompts
32
-
33
- > ℹ️ **Tip:** Some providers (such as OpenAI) support `system` and `developer`
34
- > roles, but the examples in this README stick to `user` roles since they are
35
- > supported across all providers.
36
-
37
- A prompt builder that produces a chain of messages that can be sent in one request:
38
-
39
- ```ruby
40
- #!/usr/bin/env ruby
41
- require "llm"
42
-
43
- llm = LLM.openai(key: ENV.fetch("KEY"))
44
- bot = LLM::Bot.new(llm)
45
-
46
- prompt = bot.build_prompt do
47
- it.user "Answer concisely."
48
- it.user "Was 2024 a leap year?"
49
- it.user "How many days were in that year?"
50
- end
51
-
52
- res = bot.chat(prompt)
53
- res.choices.each { |m| puts "[#{m.role}] #{m.content}" }
54
- ```
55
-
56
33
  #### Schema
57
34
 
58
- A bot that instructs the LLM to respond in JSON, and according to the given schema:
35
+ The [LLM::Schema](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Schema.html) class provides
36
+ a simple DSL for describing the structure of a response that an LLM emits according
37
+ to a JSON schema. The schema lets a client describe what JSON object an LLM should
38
+ emit, and the LLM will abide by the schema to the best of its ability:
59
39
 
60
40
  ```ruby
61
41
  #!/usr/bin/env ruby
@@ -67,20 +47,19 @@ class Estimation < LLM::Schema
67
47
  property :notes, String, "Short notes", optional: true
68
48
  end
69
49
 
70
- llm = LLM.openai(key: ENV.fetch("KEY"))
50
+ llm = LLM.openai(key: ENV["KEY"])
71
51
  bot = LLM::Bot.new(llm, schema: Estimation)
72
- img = llm.images.create(prompt: "A man in his 30s")
73
- res = bot.chat bot.image_url(img.urls.first)
74
- data = res.choices.find(&:assistant?).content!
75
-
76
- puts "age: #{data["age"]}"
77
- puts "confidence: #{data["confidence"]}"
78
- puts "notes: #{data["notes"]}" if data["notes"]
52
+ bot.chat("Estimate age and confidence for a man in his 30s.")
79
53
  ```
80
54
 
81
55
  #### Tools
82
56
 
83
- A bot equipped with a tool that is capable of running system commands:
57
+ The [LLM::Tool](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Tool.html) class lets you
58
+ define callable tools for the model. Each tool is described to the LLM as a function
59
+ it can invoke to fetch information or perform an action. The model decides when to
60
+ call tools based on the conversation; when it does, llm.rb runs the tool and sends
61
+ the result back on the next request. The following example implements a simple tool
62
+ that runs shell commands:
84
63
 
85
64
  ```ruby
86
65
  #!/usr/bin/env ruby
@@ -96,17 +75,57 @@ class System < LLM::Tool
96
75
  end
97
76
  end
98
77
 
99
- llm = LLM.openai(key: ENV.fetch("KEY"))
100
- bot = LLM::Bot.new(llm, tools: [System])
78
+ llm = LLM.openai(key: ENV["KEY"])
79
+ bot = LLM::Bot.new(llm, tools: [System])
80
+ bot.chat("Run `date`.")
81
+ bot.chat(bot.functions.map(&:call)) # report return value to the LLM
82
+ ```
101
83
 
102
- prompt = bot.build_prompt do
103
- it.user "You can run safe shell commands."
104
- it.user "Run `date`."
84
+ #### Agents
85
+
86
+ The [LLM::Agent](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Agent.html)
87
+ class provides a class-level DSL for defining reusable, preconfigured
88
+ assistants with defaults for model, tools, schema, and instructions.
89
+ Instructions are injected only on the first request, and unlike
90
+ [LLM::Bot](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Bot.html),
91
+ an [LLM::Agent](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Agent.html)
92
+ will automatically call tools when needed:
93
+
94
+ ```ruby
95
+ #!/usr/bin/env ruby
96
+ require "llm"
97
+
98
+ class SystemAdmin < LLM::Agent
99
+ model "gpt-4.1"
100
+ instructions "You are a Linux system admin"
101
+ tools Shell
102
+ schema Result
105
103
  end
106
104
 
105
+ llm = LLM.openai(key: ENV["KEY"])
106
+ agent = SystemAdmin.new(llm)
107
+ res = agent.chat("Run 'date'")
108
+ ```
109
+
110
+ #### Prompts
111
+
112
+ The [LLM::Bot#build_prompt](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Bot.html#build_prompt-instance_method)
113
+ method provides a simple DSL for building a chain of messages that
114
+ can be sent in a single request. A conversation with an LLM consists
115
+ of messages that have a role (eg system, user), and content:
116
+
117
+ ```ruby
118
+ #!/usr/bin/env ruby
119
+ require "llm"
120
+
121
+ llm = LLM.openai(key: ENV["KEY"])
122
+ bot = LLM::Bot.new(llm)
123
+ prompt = bot.build_prompt do
124
+ it.system "Answer concisely."
125
+ it.user "Was 2024 a leap year?"
126
+ it.user "How many days were in that year?"
127
+ end
107
128
  bot.chat(prompt)
108
- bot.chat(bot.functions.map(&:call))
109
- bot.messages.select(&:assistant?).each { |m| puts "[#{m.role}] #{m.content}" }
110
129
  ```
111
130
 
112
131
  ## Features
@@ -120,6 +139,7 @@ bot.messages.select(&:assistant?).each { |m| puts "[#{m.role}] #{m.content}" }
120
139
  #### Chat, Agents
121
140
  - 🧠 Stateless + stateful chat (completions + responses)
122
141
  - 🤖 Tool calling / function execution
142
+ - 🔁 Agent tool-call auto-execution (bounded)
123
143
  - 🗂️ JSON Schema structured output
124
144
  - 📡 Streaming responses
125
145
 
@@ -320,7 +340,7 @@ end
320
340
  llm = LLM.openai(key: ENV["KEY"])
321
341
  bot = LLM::Bot.new(llm, schema: Player)
322
342
  prompt = bot.build_prompt do
323
- it.user "The player's name is Sam and their position is (7, 12)."
343
+ it.system "The player's name is Sam and their position is (7, 12)."
324
344
  it.user "Return the player's name and position"
325
345
  end
326
346
 
data/lib/llm/agent.rb ADDED
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ ##
5
+ # {LLM::Agent LLM::Agent} provides a class-level DSL for defining
6
+ # reusable, preconfigured assistants with defaults for model,
7
+ # tools, schema, and instructions.
8
+ #
9
+ # @note
10
+ # Unlike {LLM::Bot LLM::Bot}, this class will automatically run
11
+ # tool calls for you.
12
+ #
13
+ # @note
14
+ # Instructions are injected only on the first request.
15
+ #
16
+ # @note
17
+ # This idea originally came from RubyLLM and was adapted to llm.rb.
18
+ #
19
+ # @example
20
+ # class SystemAdmin < LLM::Agent
21
+ # model "gpt-4.1-nano"
22
+ # instructions "You are a Linux system admin"
23
+ # tools Shell
24
+ # schema Result
25
+ # end
26
+ #
27
+ # llm = LLM.openai(key: ENV["KEY"])
28
+ # agent = SystemAdmin.new(llm)
29
+ # agent.chat("Run 'date'")
30
+ class Agent
31
+ ##
32
+ # Set or get the default model
33
+ # @param [String, nil] model
34
+ # The model identifier
35
+ # @return [String, nil]
36
+ # Returns the current model when no argument is provided
37
+ def self.model(model = nil)
38
+ return @model if model.nil?
39
+ @model = model
40
+ end
41
+
42
+ ##
43
+ # Set or get the default tools
44
+ # @param [Array<LLM::Function>, nil] tools
45
+ # One or more tools
46
+ # @return [Array<LLM::Function>]
47
+ # Returns the current tools when no argument is provided
48
+ def self.tools(*tools)
49
+ return @tools || [] if tools.empty?
50
+ @tools = tools.flatten
51
+ end
52
+
53
+ ##
54
+ # Set or get the default schema
55
+ # @param [#to_json, nil] schema
56
+ # The schema
57
+ # @return [#to_json, nil]
58
+ # Returns the current schema when no argument is provided
59
+ def self.schema(schema = nil)
60
+ return @schema if schema.nil?
61
+ @schema = schema
62
+ end
63
+
64
+ ##
65
+ # Set or get the default instructions
66
+ # @param [String, nil] instructions
67
+ # The system instructions
68
+ # @return [String, nil]
69
+ # Returns the current instructions when no argument is provided
70
+ def self.instructions(instructions = nil)
71
+ return @instructions if instructions.nil?
72
+ @instructions = instructions
73
+ end
74
+
75
+ ##
76
+ # @param [LLM::Provider] provider
77
+ # A provider
78
+ # @param [Hash] params
79
+ # The parameters to maintain throughout the conversation.
80
+ # Any parameter the provider supports can be included and
81
+ # not only those listed here.
82
+ # @option params [String] :model Defaults to the provider's default model
83
+ # @option params [Array<LLM::Function>, nil] :tools Defaults to nil
84
+ # @option params [#to_json, nil] :schema Defaults to nil
85
+ def initialize(provider, params = {})
86
+ defaults = {model: self.class.model, tools: self.class.tools, schema: self.class.schema}.compact
87
+ @provider = provider
88
+ @bot = LLM::Bot.new(provider, defaults.merge(params))
89
+ @instructions_applied = false
90
+ end
91
+
92
+ ##
93
+ # Maintain a conversation via the chat completions API.
94
+ # This method immediately sends a request to the LLM and returns the response.
95
+ #
96
+ # @param prompt (see LLM::Provider#complete)
97
+ # @param [Hash] params The params passed to the provider, including optional :stream, :tools, :schema etc.
98
+ # @option params [Integer] :max_tool_rounds The maxinum number of tool call iterations (default 10)
99
+ # @return [LLM::Response] Returns the LLM's response for this turn.
100
+ # @example
101
+ # llm = LLM.openai(key: ENV["KEY"])
102
+ # agent = LLM::Agent.new(llm)
103
+ # response = agent.chat("Hello, what is your name?")
104
+ # puts response.choices[0].content
105
+ def chat(prompt, params = {})
106
+ i, max = 0, Integer(params.delete(:max_tool_rounds) || 10)
107
+ res = @bot.chat(apply_instructions(prompt), params)
108
+ until @bot.functions.empty?
109
+ raise LLM::ToolLoopError, "pending tool calls remain" if i >= max
110
+ res = @bot.chat @bot.functions.map(&:call), params
111
+ i += 1
112
+ end
113
+ @instructions_applied = true
114
+ res
115
+ end
116
+
117
+ ##
118
+ # Maintain a conversation via the responses API.
119
+ # This method immediately sends a request to the LLM and returns the response.
120
+ #
121
+ # @note Not all LLM providers support this API
122
+ # @param prompt (see LLM::Provider#complete)
123
+ # @param [Hash] params The params passed to the provider, including optional :stream, :tools, :schema etc.
124
+ # @option params [Integer] :max_tool_rounds The maxinum number of tool call iterations (default 10)
125
+ # @return [LLM::Response] Returns the LLM's response for this turn.
126
+ # @example
127
+ # llm = LLM.openai(key: ENV["KEY"])
128
+ # agent = LLM::Agent.new(llm)
129
+ # res = agent.respond("What is the capital of France?")
130
+ # puts res.output_text
131
+ def respond(prompt, params = {})
132
+ i, max = 0, Integer(params.delete(:max_tool_rounds) || 10)
133
+ res = @bot.respond(apply_instructions(prompt), params)
134
+ until @bot.functions.empty?
135
+ raise LLM::ToolLoopError, "pending tool calls remain" if i >= max
136
+ res = @bot.respond @bot.functions.map(&:call), params
137
+ i += 1
138
+ end
139
+ @instructions_applied = true
140
+ res
141
+ end
142
+
143
+ ##
144
+ # @return [LLM::Buffer<LLM::Message>]
145
+ def messages
146
+ @bot.messages
147
+ end
148
+
149
+ ##
150
+ # @return [Array<LLM::Function>]
151
+ def functions
152
+ @bot.functions
153
+ end
154
+
155
+ ##
156
+ # @return [LLM::Object]
157
+ def usage
158
+ @bot.usage
159
+ end
160
+
161
+ ##
162
+ # @return [LLM::Builder]
163
+ def build_prompt(&)
164
+ @bot.build_prompt(&)
165
+ end
166
+
167
+ ##
168
+ # @param [String] url
169
+ # The URL
170
+ # @return [LLM::Object]
171
+ # Returns a tagged object
172
+ def image_url(url)
173
+ @bot.image_url(url)
174
+ end
175
+
176
+ ##
177
+ # @param [String] path
178
+ # The path
179
+ # @return [LLM::Object]
180
+ # Returns a tagged object
181
+ def local_file(path)
182
+ @bot.local_file(path)
183
+ end
184
+
185
+ ##
186
+ # @param [LLM::Response] res
187
+ # The response
188
+ # @return [LLM::Object]
189
+ # Returns a tagged object
190
+ def remote_file(res)
191
+ @bot.remote_file(res)
192
+ end
193
+
194
+ private
195
+
196
+ def apply_instructions(prompt)
197
+ instr = self.class.instructions
198
+ return prompt unless instr
199
+ if LLM::Builder === prompt
200
+ messages = prompt.to_a
201
+ builder = LLM::Builder.new(@provider) do |builder|
202
+ builder.system instr unless @instructions_applied
203
+ messages.each { |msg| builder.chat(msg.content, role: msg.role) }
204
+ end
205
+ builder.tap(&:call)
206
+ else
207
+ build_prompt do
208
+ _1.system instr unless @instructions_applied
209
+ _1.user prompt
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
data/lib/llm/bot.rb CHANGED
@@ -131,7 +131,7 @@ module LLM
131
131
  # end
132
132
  # bot.chat(prompt)
133
133
  def build_prompt(&)
134
- LLM::Builder.new(&).tap(&:call)
134
+ LLM::Builder.new(@provider, &).tap(&:call)
135
135
  end
136
136
 
137
137
  ##
data/lib/llm/builder.rb CHANGED
@@ -4,6 +4,9 @@
4
4
  # The {LLM::Builder LLM::Builder} class can build a collection
5
5
  # of messages that can be sent in a single request.
6
6
  #
7
+ # @note
8
+ # This API is not meant to be used directly.
9
+ #
7
10
  # @example
8
11
  # llm = LLM.openai(key: ENV["KEY"])
9
12
  # bot = LLM::Bot.new(llm)
@@ -16,7 +19,8 @@ class LLM::Builder
16
19
  ##
17
20
  # @param [Proc] evaluator
18
21
  # The evaluator
19
- def initialize(&evaluator)
22
+ def initialize(provider, &evaluator)
23
+ @provider = provider
20
24
  @buffer = []
21
25
  @evaluator = evaluator
22
26
  end
@@ -33,7 +37,13 @@ class LLM::Builder
33
37
  # @param [Symbol] role
34
38
  # The role (eg user, system)
35
39
  # @return [void]
36
- def chat(content, role: :user)
40
+ def chat(content, role: @provider.user_role)
41
+ role = case role.to_sym
42
+ when :system then @provider.system_role
43
+ when :user then @provider.user_role
44
+ when :developer then @provider.developer_role
45
+ else role
46
+ end
37
47
  @buffer << LLM::Message.new(role, content)
38
48
  end
39
49
 
@@ -42,7 +52,7 @@ class LLM::Builder
42
52
  # The message content
43
53
  # @return [void]
44
54
  def user(content)
45
- chat(content, role: :user)
55
+ chat(content, role: @provider.user_role)
46
56
  end
47
57
 
48
58
  ##
@@ -50,7 +60,15 @@ class LLM::Builder
50
60
  # The message content
51
61
  # @return [void]
52
62
  def system(content)
53
- chat(content, role: :system)
63
+ chat(content, role: @provider.system_role)
64
+ end
65
+
66
+ ##
67
+ # @param [String] content
68
+ # The message content
69
+ # @return [void]
70
+ def developer(content)
71
+ chat(content, role: @provider.developer_role)
54
72
  end
55
73
 
56
74
  ##
data/lib/llm/error.rb CHANGED
@@ -35,10 +35,6 @@ module LLM
35
35
  # HTTPServerError
36
36
  ServerError = Class.new(Error)
37
37
 
38
- ##
39
- # When no images are found in a response
40
- NoImageError = Class.new(Error)
41
-
42
38
  ##
43
39
  # When an given an input object that is not understood
44
40
  FormatError = Class.new(Error)
@@ -54,4 +50,8 @@ module LLM
54
50
  ##
55
51
  # When the context window is exceeded
56
52
  ContextWindowError = Class.new(InvalidRequestError)
53
+
54
+ ##
55
+ # When stuck in a tool call loop
56
+ ToolLoopError = Class.new(Error)
57
57
  end
data/lib/llm/provider.rb CHANGED
@@ -45,7 +45,7 @@ class LLM::Provider
45
45
  # @return [String]
46
46
  # @note The secret key is redacted in inspect for security reasons
47
47
  def inspect
48
- "#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @http=#{@http.inspect}>"
48
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @client=#{@client.inspect}>"
49
49
  end
50
50
 
51
51
  ##
@@ -234,6 +234,24 @@ class LLM::Provider
234
234
  raise NotImplementedError
235
235
  end
236
236
 
237
+ ##
238
+ # @return [Symbol]
239
+ def user_role
240
+ :user
241
+ end
242
+
243
+ ##
244
+ # @return [Symbol]
245
+ def system_role
246
+ :system
247
+ end
248
+
249
+ ##
250
+ # @return [Symbol]
251
+ def developer_role
252
+ :developer
253
+ end
254
+
237
255
  private
238
256
 
239
257
  attr_reader :client, :base_uri, :host, :port, :timeout, :ssl
@@ -3,14 +3,12 @@
3
3
  class LLM::Gemini
4
4
  ##
5
5
  # The {LLM::Gemini::Images LLM::Gemini::Images} class provides an images
6
- # object for interacting with [Gemini's images API](https://ai.google.dev/gemini-api/docs/image-generation).
7
- # Please note that unlike OpenAI, which can return either URLs or base64-encoded strings,
8
- # Gemini's images API will always return an image as a base64 encoded string that
9
- # can be decoded into binary.
6
+ # object for interacting with Google's Imagen text-to-image models via the
7
+ # Imagen API: https://ai.google.dev/gemini-api/docs/imagen
8
+ #
10
9
  # @example
11
10
  # #!/usr/bin/env ruby
12
11
  # require "llm"
13
- #
14
12
  # llm = LLM.gemini(key: ENV["KEY"])
15
13
  # res = llm.images.create prompt: "A dog on a rocket to the moon"
16
14
  # IO.copy_stream res.images[0], "rocket.png"
@@ -31,21 +29,30 @@ class LLM::Gemini
31
29
  # llm = LLM.gemini(key: ENV["KEY"])
32
30
  # res = llm.images.create prompt: "A dog on a rocket to the moon"
33
31
  # IO.copy_stream res.images[0], "rocket.png"
34
- # @see https://ai.google.dev/gemini-api/docs/image-generation Gemini docs
32
+ # @see https://ai.google.dev/gemini-api/docs/imagen Imagen docs
35
33
  # @param [String] prompt The prompt
36
- # @param [Hash] params Other parameters (see Gemini docs)
34
+ # @param [Integer] n The number of images to generate
35
+ # @param [String] image_size The size of the image ("1K", "2K", etc.)
36
+ # @param [String] aspect_ratio The aspect ratio of the image ("1:1", "16:9", etc.)
37
+ # @param [String] person_generation Allow the model to generate images of people ("dont_allow", "allow_adult", "allow_all")
38
+ # @param [String] model The model to use
39
+ # @param [Hash] params Other parameters (see Imagen docs)
37
40
  # @raise (see LLM::Provider#request)
38
- # @raise [LLM::NoImageError] when no images are returned
39
41
  # @return [LLM::Response]
40
- def create(prompt:, model: "gemini-2.5-flash-image", **params)
41
- req = Net::HTTP::Post.new("/v1beta/models/#{model}:generateContent?key=#{key}", headers)
42
+ def create(prompt:, n: 1, image_size: nil, aspect_ratio: nil, person_generation: nil, model: "imagen-4.0-generate-001", **params)
43
+ req = Net::HTTP::Post.new("/v1beta/models/#{model}:predict?key=#{key}", headers)
42
44
  body = LLM.json.dump({
43
- contents: [{parts: [{text: create_prompt}, {text: prompt}]}],
44
- generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
45
- }.merge!(params))
45
+ parameters: {
46
+ sampleCount: n,
47
+ imageSize: image_size,
48
+ aspectRatio: aspect_ratio,
49
+ personGeneration: person_generation
50
+ }.compact.merge!(params),
51
+ instances: [{prompt:}]
52
+ })
46
53
  req.body = body
47
54
  res = execute(request: req)
48
- validate ResponseAdapter.adapt(res, type: :image)
55
+ ResponseAdapter.adapt(res, type: :image)
49
56
  end
50
57
 
51
58
  ##
@@ -59,19 +66,10 @@ class LLM::Gemini
59
66
  # @param [String] prompt The prompt
60
67
  # @param [Hash] params Other parameters (see Gemini docs)
61
68
  # @raise (see LLM::Provider#request)
62
- # @raise [LLM::NoImageError] when no images are returned
63
69
  # @note (see LLM::Gemini::Images#create)
64
70
  # @return [LLM::Response]
65
71
  def edit(image:, prompt:, model: "gemini-2.5-flash-image", **params)
66
- req = Net::HTTP::Post.new("/v1beta/models/#{model}:generateContent?key=#{key}", headers)
67
- image = LLM::Object.from(value: LLM.File(image), kind: :local_file)
68
- body = LLM.json.dump({
69
- contents: [{parts: [{text: edit_prompt}, {text: prompt}, adapter.adapt_content(image)]}],
70
- generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
71
- }.merge!(params)).b
72
- set_body_stream(req, StringIO.new(body))
73
- res = execute(request: req)
74
- validate ResponseAdapter.adapt(res, type: :image)
72
+ raise NotImplementedError, "image editing is not yet supported by Gemini"
75
73
  end
76
74
 
77
75
  ##
@@ -91,36 +89,6 @@ class LLM::Gemini
91
89
  @provider.instance_variable_get(:@key)
92
90
  end
93
91
 
94
- def create_prompt
95
- <<~PROMPT
96
- ## Context
97
- Your task is to generate one or more image(s) based on the user's instructions.
98
- The user will provide you with text only.
99
-
100
- ## Instructions
101
- 1. The model *MUST* generate image(s) based on the user text alone.
102
- 2. The model *MUST NOT* generate anything else.
103
- PROMPT
104
- end
105
-
106
- def edit_prompt
107
- <<~PROMPT
108
- ## Context
109
- Your task is to edit the provided image based on the user's instructions.
110
- The user will provide you with both text and an image.
111
-
112
- ## Instructions
113
- 1. The model *MUST* edit the provided image based on the user's instructions
114
- 2. The model *MUST NOT* generate a new image.
115
- 3. The model *MUST NOT* generate anything else.
116
- PROMPT
117
- end
118
-
119
- def validate(res)
120
- return res unless res.images.empty?
121
- raise LLM::NoImageError.new { _1.response = res.res }, "no images found in response"
122
- end
123
-
124
92
  [:headers, :execute, :set_body_stream].each do |m|
125
93
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
126
94
  end
@@ -5,13 +5,9 @@ module LLM::Gemini::ResponseAdapter
5
5
  ##
6
6
  # @return [Array<StringIO>]
7
7
  def images
8
- candidates.flat_map do |candidate|
9
- parts = candidate&.dig("content", "parts") || []
10
- parts.filter_map do
11
- data = _1.dig("inlineData", "data")
12
- next unless data
13
- StringIO.new(data.unpack1("m0"))
14
- end
8
+ (body.predictions || []).map do
9
+ b64 = _1["bytesBase64Encoded"]
10
+ StringIO.new(b64.unpack1("m0"))
15
11
  end
16
12
  end
17
13
 
@@ -22,10 +18,5 @@ module LLM::Gemini::ResponseAdapter
22
18
  # will always return an empty array.
23
19
  # @return [Array<String>]
24
20
  def urls = []
25
-
26
- ##
27
- # Returns one or more candidates, or an empty array
28
- # @return [Array<Hash>]
29
- def candidates = body.candidates || []
30
21
  end
31
22
  end
@@ -43,7 +43,7 @@ class LLM::Gemini
43
43
 
44
44
  def merge_candidates!(deltas)
45
45
  deltas.each do |delta|
46
- index = delta["index"]
46
+ index = delta["index"].to_i
47
47
  @body["candidates"][index] ||= {"content" => {"parts" => []}}
48
48
  candidate = @body["candidates"][index]
49
49
  delta.each do |key, value|
@@ -81,6 +81,8 @@ class LLM::Gemini
81
81
  parts << delta
82
82
  elsif delta["fileData"]
83
83
  parts << delta
84
+ else
85
+ parts << delta
84
86
  end
85
87
  end
86
88
  end
@@ -103,12 +103,6 @@ module LLM
103
103
  LLM::Gemini::Models.new(self)
104
104
  end
105
105
 
106
- ##
107
- # @return (see LLM::Provider#assistant_role)
108
- def assistant_role
109
- "model"
110
- end
111
-
112
106
  ##
113
107
  # Returns the default model for chat completions
114
108
  # @see https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash gemini-2.5-flash
@@ -141,6 +135,33 @@ module LLM
141
135
  ResponseAdapter.adapt(complete(query, tools: [server_tools[:google_search]]), type: :web_search)
142
136
  end
143
137
 
138
+ ##
139
+ # @return [Symbol]
140
+ # Returns the providers user role
141
+ def user_role
142
+ :user
143
+ end
144
+
145
+ ##
146
+ # @return [Symbol]
147
+ # Returns the providers system role
148
+ def system_role
149
+ :user
150
+ end
151
+
152
+ ##
153
+ # @return [Symbol]
154
+ # Returns the providers developer role
155
+ def developer_role
156
+ :user
157
+ end
158
+
159
+ ##
160
+ # @return (see LLM::Provider#assistant_role)
161
+ def assistant_role
162
+ "model"
163
+ end
164
+
144
165
  private
145
166
 
146
167
  def headers
@@ -41,7 +41,7 @@ class LLM::OpenAI
41
41
  index = choice["index"]
42
42
  if @body["choices"][index]
43
43
  target_message = @body["choices"][index]["message"]
44
- delta = choice["delta"]
44
+ delta = choice["delta"] || {}
45
45
  delta.each do |key, value|
46
46
  if key == "content"
47
47
  target_message[key] ||= +""
@@ -56,7 +56,7 @@ class LLM::OpenAI
56
56
  else
57
57
  message_hash = {"role" => "assistant"}
58
58
  @body["choices"][index] = {"message" => message_hash}
59
- choice["delta"].each do |key, value|
59
+ (choice["delta"] || {}).each do |key, value|
60
60
  if key == "content"
61
61
  @io << value if @io.respond_to?(:<<)
62
62
  message_hash[key] = value
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "3.1.0"
4
+ VERSION = "4.1.0"
5
5
  end
data/lib/llm.rb CHANGED
@@ -18,6 +18,7 @@ module LLM
18
18
  require_relative "llm/file"
19
19
  require_relative "llm/provider"
20
20
  require_relative "llm/bot"
21
+ require_relative "llm/agent"
21
22
  require_relative "llm/buffer"
22
23
  require_relative "llm/function"
23
24
  require_relative "llm/eventstream"
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: 3.1.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antar Azri
@@ -178,6 +178,7 @@ files:
178
178
  - LICENSE
179
179
  - README.md
180
180
  - lib/llm.rb
181
+ - lib/llm/agent.rb
181
182
  - lib/llm/bot.rb
182
183
  - lib/llm/buffer.rb
183
184
  - lib/llm/builder.rb