ruby_llm 0.1.0.pre19 → 0.1.0.pre21

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: 230db2439975e0515e1f128a286222c06de3de1b7eff0b77631bc9e4fa2df338
4
- data.tar.gz: 25846959e0c3d2c411d82825077b4ff112bef0aea7910e87f5b3336fd58f8dca
3
+ metadata.gz: 5e413abd8ebadc4f30b5dc080113d9e40405117128bc8156bb7d9b0086f76956
4
+ data.tar.gz: 9a6971f4f5d7f709a4783b9f816589ca2b7e11199d1189bd5c946a8154d2a529
5
5
  SHA512:
6
- metadata.gz: b082f5426e1c0d4ef426df99c5cca9463943b525e113b11aa9d60b799ebe6b0ae77fcc347a06d922b98316b402f8eb9a6a716ddd5e45c26ef86458eb4405d17c
7
- data.tar.gz: c8a8a42ef3e0e8eb676fae0a28a26103c0dd1ff66be9753bb13d448f3114fb05d862fe78eee5f7599b6e2edd28a8d78c4e1fcd64ca87e163cecf1725f73351ff
6
+ metadata.gz: bdc7ad9b3e63755d398cfcc514103532d065a72bbf0eb7219496fc4c278e62b80cede1c9cadc75f3dbdb194cb655fb843d04a77d9860fd5665f5af34419f68eb
7
+ data.tar.gz: c4056980199b29e41993e1f7b092d760328b21e6327c427ad1a9ef09f4e5521482bb9a3cc9c1d0eba3380b18127541b583ebbd7d4780057df1e838c9bba48793
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Carmine Paolino
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -19,9 +19,7 @@ Or install it yourself:
19
19
  gem install ruby_llm
20
20
  ```
21
21
 
22
- ## Quick Start
23
-
24
- RubyLLM makes it dead simple to start chatting with AI models:
22
+ ## Configuration
25
23
 
26
24
  ```ruby
27
25
  require 'ruby_llm'
@@ -31,7 +29,13 @@ RubyLLM.configure do |config|
31
29
  config.openai_api_key = ENV['OPENAI_API_KEY']
32
30
  config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
33
31
  end
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ RubyLLM makes it dead simple to start chatting with AI models:
34
37
 
38
+ ```ruby
35
39
  # Start a conversation
36
40
  chat = RubyLLM.chat
37
41
  chat.ask "What's the best way to learn Ruby?"
@@ -82,16 +86,13 @@ Need vector embeddings for your text? RubyLLM makes it simple:
82
86
 
83
87
  ```ruby
84
88
  # Get embeddings with the default model
85
- vector = RubyLLM.embed "Hello, world!"
89
+ RubyLLM.embed "Hello, world!"
86
90
 
87
91
  # Use a specific model
88
- vector = RubyLLM.embed(
89
- "Ruby is awesome!",
90
- model: "text-embedding-3-large"
91
- )
92
+ RubyLLM.embed "Ruby is awesome!", model: "text-embedding-3-large"
92
93
 
93
94
  # Process multiple texts at once
94
- vectors = RubyLLM.embed([
95
+ RubyLLM.embed([
95
96
  "First document",
96
97
  "Second document",
97
98
  "Third document"
@@ -172,6 +173,21 @@ chat.ask "What's 123 * 456?"
172
173
  # D, -- RubyLLM: Tool calculator returned: "56088"
173
174
  ```
174
175
 
176
+ ## Error Handling
177
+
178
+ RubyLLM wraps provider errors in clear Ruby exceptions:
179
+
180
+ ```ruby
181
+ begin
182
+ chat = RubyLLM.chat
183
+ chat.ask "Hello world!"
184
+ rescue RubyLLM::UnauthorizedError
185
+ puts "Check your API credentials"
186
+ rescue RubyLLM::BadRequestError => e
187
+ puts "Something went wrong: #{e.message}"
188
+ end
189
+ ```
190
+
175
191
  ## Rails Integration
176
192
 
177
193
  RubyLLM comes with built-in Rails support that makes it dead simple to persist your chats and messages. Just create your tables and hook it up:
@@ -242,14 +258,14 @@ That's it! Now you can use chats straight from your models:
242
258
 
243
259
  ```ruby
244
260
  # Create a new chat
245
- chat = Chat.create!(model_id: "gpt-4")
261
+ chat = Chat.create! model_id: "gpt-4o-mini"
246
262
 
247
263
  # Ask questions - messages are automatically saved
248
264
  chat.ask "What's the weather in Paris?"
249
265
 
250
266
  # Stream responses in real-time
251
267
  chat.ask "Tell me a story" do |chunk|
252
- broadcast_chunk(chunk)
268
+ broadcast_chunk chunk
253
269
  end
254
270
 
255
271
  # Everything is persisted automatically
@@ -307,7 +323,7 @@ The persistence works seamlessly with background jobs:
307
323
  ```ruby
308
324
  class ChatJob < ApplicationJob
309
325
  def perform(chat_id, message)
310
- chat = Chat.find(chat_id)
326
+ chat = Chat.find chat_id
311
327
 
312
328
  chat.ask(message) do |chunk|
313
329
  # Optional: Broadcast chunks for real-time updates
@@ -341,8 +357,8 @@ class WeatherTool < RubyLLM::Tool
341
357
  end
342
358
 
343
359
  # Use tools with your persisted chats
344
- chat = Chat.create!(model_id: "gpt-4")
345
- chat.chat.with_tool(WeatherTool.new)
360
+ chat = Chat.create! model_id: "gpt-4"
361
+ chat.chat.with_tool WeatherTool.new
346
362
 
347
363
  # Ask about weather - tool usage is automatically saved
348
364
  chat.ask "What's the weather in Paris?"
@@ -352,8 +368,6 @@ pp chat.messages.map(&:role)
352
368
  #=> [:user, :assistant, :tool, :assistant]
353
369
  ```
354
370
 
355
- Looking for more examples? Check out the [example Rails app](https://github.com/example/ruby_llm_rails) showing these patterns in action!
356
-
357
371
  ## Development
358
372
 
359
373
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt.
@@ -364,4 +378,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/crmne/
364
378
 
365
379
  ## License
366
380
 
367
- Released under the MIT License. See LICENSE.txt for details.
381
+ Released under the MIT License. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Custom error class that wraps API errors from different providers
5
+ # into a consistent format with helpful error messages.
6
+ #
7
+ # Example:
8
+ # begin
9
+ # chat.ask "What's 2+2?"
10
+ # rescue RubyLLM::Error => e
11
+ # puts "Couldn't chat with AI: #{e.message}"
12
+ # end
13
+ class Error < StandardError
14
+ attr_reader :response
15
+
16
+ def initialize(response = nil, message = nil)
17
+ @response = response
18
+ super(message || response&.body.to_s)
19
+ end
20
+ end
21
+
22
+ class UnauthorizedError < Error; end
23
+ class BadRequestError < Error; end
24
+ class RateLimitError < Error; end
25
+ class ServerError < Error; end
26
+
27
+ # Faraday middleware that maps provider-specific API errors to RubyLLM errors.
28
+ # Uses provider's parse_error method to extract meaningful error messages.
29
+ class ErrorMiddleware < Faraday::Middleware
30
+ def initialize(app, provider: nil)
31
+ super(app)
32
+ @provider = provider
33
+ end
34
+
35
+ def call(env) # rubocop:disable Metrics/MethodLength
36
+ @app.call(env).on_complete do |response|
37
+ message = @provider&.parse_error(response)
38
+
39
+ case response.status
40
+ when 400
41
+ raise BadRequestError.new(response, message)
42
+ when 401
43
+ raise UnauthorizedError.new(response, 'Invalid API key - check your credentials')
44
+ when 429
45
+ raise RateLimitError.new(response, 'Rate limit exceeded - please wait a moment')
46
+ when 500..599
47
+ raise ServerError.new(response, 'API server error - please try again')
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ Faraday::Middleware.register_middleware(llm_errors: RubyLLM::ErrorMiddleware)
@@ -69,7 +69,7 @@ module RubyLLM
69
69
  f.request :json
70
70
  f.response :json
71
71
  f.adapter Faraday.default_adapter
72
- f.use Faraday::Response::RaiseError
72
+ f.use :llm_errors, provider: self
73
73
  f.response :logger, RubyLLM.logger, { headers: false, bodies: true, errors: true, log_level: :debug }
74
74
  end
75
75
  end
@@ -7,6 +7,10 @@ module RubyLLM
7
7
  class Anthropic # rubocop:disable Metrics/ClassLength
8
8
  include Provider
9
9
 
10
+ def parse_error(response)
11
+ JSON.parse(response.body).dig('error', 'message')
12
+ end
13
+
10
14
  private
11
15
 
12
16
  def api_base
@@ -8,6 +8,10 @@ module RubyLLM
8
8
  class OpenAI # rubocop:disable Metrics/ClassLength
9
9
  include Provider
10
10
 
11
+ def parse_error(response)
12
+ JSON.parse(response.body).dig('error', 'message')
13
+ end
14
+
11
15
  private
12
16
 
13
17
  def api_base
@@ -43,13 +47,14 @@ module RubyLLM
43
47
  payload[:tools] = tools.map { |_, tool| tool_for(tool) }
44
48
  payload[:tool_choice] = 'auto'
45
49
  end
50
+ payload[:stream_options] = { include_usage: true } if stream
46
51
  end
47
52
  end
48
53
 
49
54
  def format_messages(messages)
50
55
  messages.map do |msg|
51
56
  {
52
- role: msg.role.to_s,
57
+ role: format_role(msg.role),
53
58
  content: msg.content,
54
59
  tool_calls: format_tool_calls(msg.tool_calls),
55
60
  tool_call_id: msg.tool_call_id
@@ -57,6 +62,15 @@ module RubyLLM
57
62
  end
58
63
  end
59
64
 
65
+ def format_role(role)
66
+ case role
67
+ when :system
68
+ 'developer'
69
+ else
70
+ role.to_s
71
+ end
72
+ end
73
+
60
74
  def build_embedding_payload(text, model:)
61
75
  {
62
76
  model: model,
@@ -156,14 +170,16 @@ module RubyLLM
156
170
  end.compact
157
171
  end
158
172
 
159
- def handle_stream(&block)
173
+ def handle_stream(&block) # rubocop:disable Metrics/MethodLength
160
174
  to_json_stream do |data|
161
175
  block.call(
162
176
  Chunk.new(
163
177
  role: :assistant,
164
178
  model_id: data['model'],
165
179
  content: data.dig('choices', 0, 'delta', 'content'),
166
- tool_calls: parse_tool_calls(data.dig('choices', 0, 'delta', 'tool_calls'), parse_arguments: false)
180
+ tool_calls: parse_tool_calls(data.dig('choices', 0, 'delta', 'tool_calls'), parse_arguments: false),
181
+ input_tokens: data.dig('usage', 'prompt_tokens'),
182
+ output_tokens: data.dig('usage', 'completion_tokens')
167
183
  )
168
184
  )
169
185
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '0.1.0.pre19'
4
+ VERSION = '0.1.0.pre21'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre19
4
+ version: 0.1.0.pre21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-06 00:00:00.000000000 Z
11
+ date: 2025-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: event_stream_parser
@@ -339,6 +339,7 @@ files:
339
339
  - ".rspec"
340
340
  - ".rubocop.yml"
341
341
  - Gemfile
342
+ - LICENSE
342
343
  - README.md
343
344
  - Rakefile
344
345
  - bin/console
@@ -349,6 +350,7 @@ files:
349
350
  - lib/ruby_llm/chunk.rb
350
351
  - lib/ruby_llm/configuration.rb
351
352
  - lib/ruby_llm/embedding.rb
353
+ - lib/ruby_llm/error.rb
352
354
  - lib/ruby_llm/message.rb
353
355
  - lib/ruby_llm/model_capabilities/anthropic.rb
354
356
  - lib/ruby_llm/model_capabilities/openai.rb