ruby_llm 1.0.0 → 1.1.0rc1

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +58 -19
  3. data/lib/ruby_llm/active_record/acts_as.rb +46 -7
  4. data/lib/ruby_llm/aliases.json +65 -0
  5. data/lib/ruby_llm/aliases.rb +56 -0
  6. data/lib/ruby_llm/chat.rb +11 -10
  7. data/lib/ruby_llm/configuration.rb +4 -0
  8. data/lib/ruby_llm/error.rb +15 -4
  9. data/lib/ruby_llm/models.json +1489 -283
  10. data/lib/ruby_llm/models.rb +57 -22
  11. data/lib/ruby_llm/provider.rb +44 -41
  12. data/lib/ruby_llm/providers/anthropic/capabilities.rb +8 -9
  13. data/lib/ruby_llm/providers/anthropic/chat.rb +31 -4
  14. data/lib/ruby_llm/providers/anthropic/streaming.rb +12 -6
  15. data/lib/ruby_llm/providers/anthropic.rb +4 -0
  16. data/lib/ruby_llm/providers/bedrock/capabilities.rb +168 -0
  17. data/lib/ruby_llm/providers/bedrock/chat.rb +108 -0
  18. data/lib/ruby_llm/providers/bedrock/models.rb +84 -0
  19. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  20. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +46 -0
  21. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +63 -0
  22. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
  23. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +90 -0
  24. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
  25. data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
  26. data/lib/ruby_llm/providers/bedrock.rb +83 -0
  27. data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
  28. data/lib/ruby_llm/providers/deepseek.rb +5 -0
  29. data/lib/ruby_llm/providers/gemini/capabilities.rb +50 -34
  30. data/lib/ruby_llm/providers/gemini/chat.rb +8 -15
  31. data/lib/ruby_llm/providers/gemini/images.rb +5 -10
  32. data/lib/ruby_llm/providers/gemini/models.rb +0 -8
  33. data/lib/ruby_llm/providers/gemini/streaming.rb +35 -76
  34. data/lib/ruby_llm/providers/gemini/tools.rb +12 -12
  35. data/lib/ruby_llm/providers/gemini.rb +4 -0
  36. data/lib/ruby_llm/providers/openai/capabilities.rb +154 -177
  37. data/lib/ruby_llm/providers/openai/streaming.rb +9 -13
  38. data/lib/ruby_llm/providers/openai.rb +4 -0
  39. data/lib/ruby_llm/streaming.rb +96 -0
  40. data/lib/ruby_llm/tool.rb +15 -7
  41. data/lib/ruby_llm/version.rb +1 -1
  42. data/lib/ruby_llm.rb +8 -3
  43. data/lib/tasks/browser_helper.rb +97 -0
  44. data/lib/tasks/capability_generator.rb +123 -0
  45. data/lib/tasks/capability_scraper.rb +224 -0
  46. data/lib/tasks/cli_helper.rb +22 -0
  47. data/lib/tasks/code_validator.rb +29 -0
  48. data/lib/tasks/model_updater.rb +66 -0
  49. data/lib/tasks/models.rake +28 -197
  50. data/lib/tasks/vcr.rake +97 -0
  51. metadata +42 -19
  52. data/.github/workflows/cicd.yml +0 -109
  53. data/.github/workflows/docs.yml +0 -53
  54. data/.gitignore +0 -58
  55. data/.overcommit.yml +0 -26
  56. data/.rspec +0 -3
  57. data/.rspec_status +0 -50
  58. data/.rubocop.yml +0 -10
  59. data/.yardopts +0 -12
  60. data/Gemfile +0 -32
  61. data/Rakefile +0 -9
  62. data/bin/console +0 -17
  63. data/bin/setup +0 -6
  64. data/ruby_llm.gemspec +0 -43
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c357c02ad6d779fab06975f3e06d6d699fe8860b99f05af4f4f89116c0c02868
4
- data.tar.gz: e1379dcae9b2077b5bde9dd2a1f1a110151db15286ffa3503e9f596f64eb05fb
3
+ metadata.gz: 7f1ca42c438f99a40f6ada6ab87e23377403d9d6c8db6049d54960308b6dd42f
4
+ data.tar.gz: 0467734c40a0a64505b8f4de2eea4b929e2b0c615fedb3fe88d75f8cf20040de
5
5
  SHA512:
6
- metadata.gz: 131ffdba032ec5844b1cd893ab3c712ee0a821faedc640ea4a2affc399f8c497a85b7a4c4e98daf13b7b7f52a1905e9135da4e94fd13967b96f47b02cd42a524
7
- data.tar.gz: 75415cab8eba778d7b5d7918309dea8ea0e48bca1aca0ef13214835aec8ee9993bd943ff08efd827c5577636c6aa60ad998a6c81eac973e7cadd0061ce428fa2
6
+ metadata.gz: ed886fb15081ce27309c0e583223e62adf171907f0f047344763e9eae5ede4dbb96657bb1b69dde9e68bc1ed56acedb93717273067236e40104dddcc09886f33
7
+ data.tar.gz: 7f2aa9c50cfd1a525273256ff014712944c976229f6b90fe338decaf2525bcb82f19dec187d14795ce0891a7fceadb5c048905c8afa8066b0ddbe37d54c48edd
data/README.md CHANGED
@@ -2,13 +2,16 @@
2
2
 
3
3
  A delightful Ruby way to work with AI. No configuration madness, no complex callbacks, no handler hell – just beautiful, expressive Ruby code.
4
4
 
5
- <div style="display: flex; align-items: center; flex-wrap: wrap; gap: 4px;">
5
+ <div style="display: flex; align-items: center; flex-wrap: wrap; margin-bottom: 1em">
6
6
  <img src="https://upload.wikimedia.org/wikipedia/commons/4/4d/OpenAI_Logo.svg" alt="OpenAI" height="40" width="120">
7
- &nbsp;&nbsp;&nbsp;&nbsp;
7
+ &nbsp;&nbsp;
8
8
  <img src="https://upload.wikimedia.org/wikipedia/commons/7/78/Anthropic_logo.svg" alt="Anthropic" height="40" width="120">
9
- &nbsp;&nbsp;&nbsp;&nbsp;
9
+ &nbsp;&nbsp;
10
10
  <img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Google_Gemini_logo.svg" alt="Google" height="40" width="120">
11
- &nbsp;&nbsp;&nbsp;&nbsp;
11
+ &nbsp;&nbsp;
12
+ <img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/bedrock-color.svg" alt="Bedrock" height="40">
13
+ <img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/bedrock-text.svg" alt="Bedrock" height="40" width="120">
14
+ &nbsp;&nbsp;
12
15
  <img src="https://upload.wikimedia.org/wikipedia/commons/e/ec/DeepSeek_logo.svg" alt="DeepSeek" height="40" width="120">
13
16
  </div>
14
17
 
@@ -27,7 +30,7 @@ RubyLLM fixes all that. One beautiful API for everything. One consistent format.
27
30
 
28
31
  ## Features
29
32
 
30
- - 💬 **Chat** with OpenAI, Anthropic, Gemini, and DeepSeek models
33
+ - 💬 **Chat** with OpenAI, Anthropic, Gemini, AWS Bedrock Anthropic, and DeepSeek models
31
34
  - 👁️ **Vision and Audio** understanding
32
35
  - 📄 **PDF Analysis** for analyzing documents
33
36
  - 🖼️ **Image generation** with DALL-E and other providers
@@ -52,6 +55,11 @@ chat.ask "Describe this meeting", with: { audio: "meeting.wav" }
52
55
  # Analyze documents
53
56
  chat.ask "Summarize this document", with: { pdf: "contract.pdf" }
54
57
 
58
+ # Stream responses in real-time
59
+ chat.ask "Tell me a story about a Ruby programmer" do |chunk|
60
+ print chunk.content
61
+ end
62
+
55
63
  # Generate images
56
64
  RubyLLM.paint "a sunset over mountains in watercolor style"
57
65
 
@@ -59,16 +67,22 @@ RubyLLM.paint "a sunset over mountains in watercolor style"
59
67
  RubyLLM.embed "Ruby is elegant and expressive"
60
68
 
61
69
  # Let AI use your code
62
- class Calculator < RubyLLM::Tool
63
- description "Performs calculations"
64
- param :expression, type: :string, desc: "Math expression to evaluate"
65
-
66
- def execute(expression:)
67
- eval(expression).to_s
70
+ class Weather < RubyLLM::Tool
71
+ description "Gets current weather for a location"
72
+ param :latitude, desc: "Latitude (e.g., 52.5200)"
73
+ param :longitude, desc: "Longitude (e.g., 13.4050)"
74
+
75
+ def execute(latitude:, longitude:)
76
+ url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
77
+
78
+ response = Faraday.get(url)
79
+ data = JSON.parse(response.body)
80
+ rescue => e
81
+ { error: e.message }
68
82
  end
69
83
  end
70
84
 
71
- chat.with_tool(Calculator).ask "What's 123 * 456?"
85
+ chat.with_tool(Weather).ask "What's the weather in Berlin? (52.5200, 13.4050)"
72
86
  ```
73
87
 
74
88
  ## Installation
@@ -88,10 +102,16 @@ Configure with your API keys:
88
102
 
89
103
  ```ruby
90
104
  RubyLLM.configure do |config|
91
- config.openai_api_key = ENV['OPENAI_API_KEY']
92
- config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
93
- config.gemini_api_key = ENV['GEMINI_API_KEY']
94
- config.deepseek_api_key = ENV['DEEPSEEK_API_KEY'] # Optional
105
+ config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
106
+ config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
107
+ config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
108
+ config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
109
+
110
+ # Bedrock
111
+ config.bedrock_api_key = ENV.fetch('AWS_ACCESS_KEY_ID', nil)
112
+ config.bedrock_secret_key = ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
113
+ config.bedrock_region = ENV.fetch('AWS_REGION', nil)
114
+ config.bedrock_session_token = ENV.fetch('AWS_SESSION_TOKEN', nil)
95
115
  end
96
116
  ```
97
117
 
@@ -115,6 +135,9 @@ chat.ask "Tell me a story about a Ruby programmer" do |chunk|
115
135
  print chunk.content
116
136
  end
117
137
 
138
+ # Set personality or behavior with instructions (aka system prompts) - available from 1.1.0
139
+ chat.with_instructions "You are a friendly Ruby expert who loves to help beginners"
140
+
118
141
  # Understand content in multiple forms
119
142
  chat.ask "Compare these diagrams", with: { image: ["diagram1.png", "diagram2.png"] }
120
143
  chat.ask "Summarize this document", with: { pdf: "contract.pdf" }
@@ -145,8 +168,12 @@ class ToolCall < ApplicationRecord
145
168
  acts_as_tool_call
146
169
  end
147
170
 
148
- # In your controller
149
- chat = Chat.create!(model_id: "gpt-4o-mini")
171
+ # In a background job
172
+ chat = Chat.create! model_id: "gpt-4o-mini"
173
+
174
+ # Set personality or behavior with instructions (aka system prompts) - they're persisted too! - available from 1.1.0
175
+ chat.with_instructions "You are a friendly Ruby expert who loves to help beginners"
176
+
150
177
  chat.ask("What's your favorite Ruby gem?") do |chunk|
151
178
  Turbo::StreamsChannel.broadcast_append_to(
152
179
  chat,
@@ -182,6 +209,18 @@ chat.with_tool(Search).ask "Find documents about Ruby 3.3 features"
182
209
 
183
210
  Check out the guides at https://rubyllm.com for deeper dives into conversations with tools, streaming responses, embedding generations, and more.
184
211
 
212
+ ## Contributing
213
+
214
+ We welcome contributions to RubyLLM!
215
+
216
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions on how to:
217
+ - Run the test suite
218
+ - Add new features
219
+ - Update documentation
220
+ - Re-record VCR cassettes when needed
221
+
222
+ We appreciate your help making RubyLLM better!
223
+
185
224
  ## License
186
225
 
187
- Released under the MIT License.
226
+ Released under the MIT License.
@@ -9,7 +9,7 @@ module RubyLLM
9
9
  extend ActiveSupport::Concern
10
10
 
11
11
  class_methods do # rubocop:disable Metrics/BlockLength
12
- def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall') # rubocop:disable Metrics/MethodLength
12
+ def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall')
13
13
  include ChatMethods
14
14
 
15
15
  @message_class = message_class.to_s
@@ -21,12 +21,6 @@ module RubyLLM
21
21
  dependent: :destroy
22
22
 
23
23
  delegate :complete,
24
- :with_tool,
25
- :with_tools,
26
- :with_model,
27
- :with_temperature,
28
- :on_new_message,
29
- :on_end_message,
30
24
  :add_message,
31
25
  to: :to_llm
32
26
  end
@@ -85,6 +79,51 @@ module RubyLLM
85
79
  .on_end_message { |msg| persist_message_completion(msg) }
86
80
  end
87
81
 
82
+ def with_instructions(instructions, replace: false)
83
+ transaction do
84
+ # If replace is true, remove existing system messages
85
+ messages.where(role: :system).destroy_all if replace
86
+
87
+ # Create the new system message
88
+ messages.create!(
89
+ role: :system,
90
+ content: instructions
91
+ )
92
+ end
93
+ to_llm.with_instructions(instructions)
94
+ self
95
+ end
96
+
97
+ def with_tool(tool)
98
+ to_llm.with_tool(tool)
99
+ self
100
+ end
101
+
102
+ def with_tools(*tools)
103
+ to_llm.with_tools(*tools)
104
+ self
105
+ end
106
+
107
+ def with_model(model_id, provider: nil)
108
+ to_llm.with_model(model_id, provider: provider)
109
+ self
110
+ end
111
+
112
+ def with_temperature(temperature)
113
+ to_llm.with_temperature(temperature)
114
+ self
115
+ end
116
+
117
+ def on_new_message(&)
118
+ to_llm.on_new_message(&)
119
+ self
120
+ end
121
+
122
+ def on_end_message(&)
123
+ to_llm.on_end_message(&)
124
+ self
125
+ end
126
+
88
127
  def ask(message, &)
89
128
  message = { role: :user, content: message }
90
129
  messages.create!(**message)
@@ -0,0 +1,65 @@
1
+ {
2
+ "claude-3-5-sonnet": {
3
+ "anthropic": "claude-3-5-sonnet-20241022",
4
+ "bedrock": "anthropic.claude-3-5-sonnet-20241022-v2:0"
5
+ },
6
+ "claude-3-5-haiku": {
7
+ "anthropic": "claude-3-5-haiku-20241022",
8
+ "bedrock": "anthropic.claude-3-5-haiku-20241022-v1:0"
9
+ },
10
+ "claude-3-7-sonnet": {
11
+ "anthropic": "claude-3-7-sonnet-20250219",
12
+ "bedrock": "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
13
+ },
14
+ "claude-3-opus": {
15
+ "anthropic": "claude-3-opus-20240229",
16
+ "bedrock": "anthropic.claude-3-opus-20240229-v1:0"
17
+ },
18
+ "claude-3-sonnet": {
19
+ "anthropic": "claude-3-sonnet-20240229",
20
+ "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0"
21
+ },
22
+ "claude-3-haiku": {
23
+ "anthropic": "claude-3-haiku-20240307",
24
+ "bedrock": "anthropic.claude-3-haiku-20240307-v1:0"
25
+ },
26
+ "claude-3": {
27
+ "anthropic": "claude-3-sonnet-20240229",
28
+ "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0"
29
+ },
30
+ "claude-2": {
31
+ "anthropic": "claude-2.0",
32
+ "bedrock": "anthropic.claude-2.0"
33
+ },
34
+ "claude-2-1": {
35
+ "anthropic": "claude-2.1",
36
+ "bedrock": "anthropic.claude-2.1"
37
+ },
38
+ "gpt-4o": {
39
+ "openai": "gpt-4o-2024-11-20"
40
+ },
41
+ "gpt-4o-mini": {
42
+ "openai": "gpt-4o-mini-2024-07-18"
43
+ },
44
+ "gpt-4-turbo": {
45
+ "openai": "gpt-4-turbo-2024-04-09"
46
+ },
47
+ "gemini-1.5-flash": {
48
+ "gemini": "gemini-1.5-flash-002"
49
+ },
50
+ "gemini-1.5-flash-8b": {
51
+ "gemini": "gemini-1.5-flash-8b-001"
52
+ },
53
+ "gemini-1.5-pro": {
54
+ "gemini": "gemini-1.5-pro-002"
55
+ },
56
+ "gemini-2.0-flash": {
57
+ "gemini": "gemini-2.0-flash-001"
58
+ },
59
+ "o1": {
60
+ "openai": "o1-2024-12-17"
61
+ },
62
+ "o3-mini": {
63
+ "openai": "o3-mini-2025-01-31"
64
+ }
65
+ }
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Manages model aliases, allowing users to reference models by simpler names
5
+ # that map to specific model versions across different providers.
6
+ #
7
+ # Aliases are defined in aliases.json and follow the format:
8
+ # {
9
+ # "simple-name": {
10
+ # "provider1": "specific-version-for-provider1",
11
+ # "provider2": "specific-version-for-provider2"
12
+ # }
13
+ # }
14
+ class Aliases
15
+ class << self
16
+ # Resolves a model ID to its provider-specific version
17
+ #
18
+ # @param model_id [String] the model identifier or alias
19
+ # @param provider_slug [String, Symbol, nil] optional provider to resolve for
20
+ # @return [String] the resolved model ID or the original if no alias exists
21
+ def resolve(model_id, provider = nil)
22
+ return model_id unless aliases[model_id]
23
+
24
+ if provider
25
+ aliases[model_id][provider.to_s] || model_id
26
+ else
27
+ # Get native provider's version
28
+ aliases[model_id].values.first || model_id
29
+ end
30
+ end
31
+
32
+ # Returns the loaded aliases mapping
33
+ # @return [Hash] the aliases mapping
34
+ def aliases
35
+ @aliases ||= load_aliases
36
+ end
37
+
38
+ # Loads aliases from the JSON file
39
+ # @return [Hash] the loaded aliases
40
+ def load_aliases
41
+ file_path = File.expand_path('aliases.json', __dir__)
42
+ if File.exist?(file_path)
43
+ JSON.parse(File.read(file_path))
44
+ else
45
+ {}
46
+ end
47
+ end
48
+
49
+ # Reloads aliases from disk
50
+ # @return [Hash] the reloaded aliases
51
+ def reload!
52
+ @aliases = load_aliases
53
+ end
54
+ end
55
+ end
56
+ end
data/lib/ruby_llm/chat.rb CHANGED
@@ -13,10 +13,10 @@ module RubyLLM
13
13
 
14
14
  attr_reader :model, :messages, :tools
15
15
 
16
- def initialize(model: nil)
16
+ def initialize(model: nil, provider: nil)
17
17
  model_id = model || RubyLLM.config.default_model
18
- self.model = model_id
19
- @temperature = @model.metadata['family'] == 'o1' ? 1 : 0.7
18
+ with_model(model_id, provider: provider)
19
+ @temperature = 0.7
20
20
  @messages = []
21
21
  @tools = {}
22
22
  @on = {
@@ -32,6 +32,11 @@ module RubyLLM
32
32
 
33
33
  alias say ask
34
34
 
35
+ def with_instructions(instructions)
36
+ add_message role: :system, content: instructions
37
+ self
38
+ end
39
+
35
40
  def with_tool(tool)
36
41
  unless @model.supports_functions
37
42
  raise UnsupportedFunctionsError, "Model #{@model.id} doesn't support function calling"
@@ -47,13 +52,9 @@ module RubyLLM
47
52
  self
48
53
  end
49
54
 
50
- def model=(model_id)
51
- @model = Models.find model_id
52
- @provider = Models.provider_for model_id
53
- end
54
-
55
- def with_model(model_id)
56
- self.model = model_id
55
+ def with_model(model_id, provider: nil)
56
+ @model = Models.find model_id, provider
57
+ @provider = Provider.providers[@model.provider.to_sym] || raise(Error, "Unknown provider: #{@model.provider}")
57
58
  self
58
59
  end
59
60
 
@@ -14,6 +14,10 @@ module RubyLLM
14
14
  :anthropic_api_key,
15
15
  :gemini_api_key,
16
16
  :deepseek_api_key,
17
+ :bedrock_api_key,
18
+ :bedrock_secret_key,
19
+ :bedrock_region,
20
+ :bedrock_session_token,
17
21
  :default_model,
18
22
  :default_embedding_model,
19
23
  :default_image_model,
@@ -19,15 +19,21 @@ module RubyLLM
19
19
  end
20
20
  end
21
21
 
22
- class ModelNotFoundError < StandardError; end
22
+ # Error classes for non-HTTP errors
23
+ class ConfigurationError < StandardError; end
23
24
  class InvalidRoleError < StandardError; end
25
+ class ModelNotFoundError < StandardError; end
24
26
  class UnsupportedFunctionsError < StandardError; end
25
- class UnauthorizedError < Error; end
26
- class PaymentRequiredError < Error; end
27
- class ServiceUnavailableError < Error; end
27
+
28
+ # Error classes for different HTTP status codes
28
29
  class BadRequestError < Error; end
30
+ class ForbiddenError < Error; end
31
+ class OverloadedError < Error; end
32
+ class PaymentRequiredError < Error; end
29
33
  class RateLimitError < Error; end
30
34
  class ServerError < Error; end
35
+ class ServiceUnavailableError < Error; end
36
+ class UnauthorizedError < Error; end
31
37
 
32
38
  # Faraday middleware that maps provider-specific API errors to RubyLLM errors.
33
39
  # Uses provider's parse_error method to extract meaningful error messages.
@@ -56,12 +62,17 @@ module RubyLLM
56
62
  raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
57
63
  when 402
58
64
  raise PaymentRequiredError.new(response, message || 'Payment required - please top up your account')
65
+ when 403
66
+ raise ForbiddenError.new(response,
67
+ message || 'Forbidden - you do not have permission to access this resource')
59
68
  when 429
60
69
  raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
61
70
  when 500
62
71
  raise ServerError.new(response, message || 'API server error - please try again')
63
72
  when 502..503
64
73
  raise ServiceUnavailableError.new(response, message || 'API server unavailable - please try again later')
74
+ when 529
75
+ raise OverloadedError.new(response, message || 'Service overloaded - please try again later')
65
76
  else
66
77
  raise Error.new(response, message || 'An unknown error occurred')
67
78
  end