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 +4 -4
- data/LICENSE +21 -0
- data/README.md +31 -17
- data/lib/ruby_llm/error.rb +54 -0
- data/lib/ruby_llm/provider.rb +1 -1
- data/lib/ruby_llm/providers/anthropic.rb +4 -0
- data/lib/ruby_llm/providers/openai.rb +19 -3
- data/lib/ruby_llm/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e413abd8ebadc4f30b5dc080113d9e40405117128bc8156bb7d9b0086f76956
|
4
|
+
data.tar.gz: 9a6971f4f5d7f709a4783b9f816589ca2b7e11199d1189bd5c946a8154d2a529
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
##
|
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
|
-
|
89
|
+
RubyLLM.embed "Hello, world!"
|
86
90
|
|
87
91
|
# Use a specific model
|
88
|
-
|
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
|
-
|
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!
|
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
|
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
|
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!
|
345
|
-
chat.chat.with_tool
|
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
|
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)
|
data/lib/ruby_llm/provider.rb
CHANGED
@@ -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
|
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
|
@@ -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
|
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
|
data/lib/ruby_llm/version.rb
CHANGED
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.
|
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-
|
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
|