ruby_llm 1.2.0 → 1.3.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -133
  3. data/lib/ruby_llm/active_record/acts_as.rb +144 -47
  4. data/lib/ruby_llm/aliases.json +187 -17
  5. data/lib/ruby_llm/attachment.rb +164 -0
  6. data/lib/ruby_llm/chat.rb +31 -20
  7. data/lib/ruby_llm/configuration.rb +34 -1
  8. data/lib/ruby_llm/connection.rb +121 -0
  9. data/lib/ruby_llm/content.rb +27 -79
  10. data/lib/ruby_llm/context.rb +30 -0
  11. data/lib/ruby_llm/embedding.rb +13 -5
  12. data/lib/ruby_llm/error.rb +2 -1
  13. data/lib/ruby_llm/image.rb +15 -8
  14. data/lib/ruby_llm/message.rb +14 -6
  15. data/lib/ruby_llm/mime_type.rb +67 -0
  16. data/lib/ruby_llm/model/info.rb +101 -0
  17. data/lib/ruby_llm/model/modalities.rb +22 -0
  18. data/lib/ruby_llm/model/pricing.rb +51 -0
  19. data/lib/ruby_llm/model/pricing_category.rb +48 -0
  20. data/lib/ruby_llm/model/pricing_tier.rb +34 -0
  21. data/lib/ruby_llm/model.rb +7 -0
  22. data/lib/ruby_llm/models.json +26279 -2362
  23. data/lib/ruby_llm/models.rb +95 -14
  24. data/lib/ruby_llm/provider.rb +48 -90
  25. data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
  26. data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
  27. data/lib/ruby_llm/providers/anthropic/media.rb +49 -28
  28. data/lib/ruby_llm/providers/anthropic/models.rb +16 -16
  29. data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
  30. data/lib/ruby_llm/providers/anthropic.rb +3 -3
  31. data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
  32. data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
  33. data/lib/ruby_llm/providers/bedrock/media.rb +59 -0
  34. data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
  35. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
  36. data/lib/ruby_llm/providers/bedrock.rb +14 -25
  37. data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
  38. data/lib/ruby_llm/providers/deepseek.rb +3 -3
  39. data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
  40. data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
  41. data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
  42. data/lib/ruby_llm/providers/gemini/images.rb +4 -3
  43. data/lib/ruby_llm/providers/gemini/media.rb +28 -111
  44. data/lib/ruby_llm/providers/gemini/models.rb +17 -23
  45. data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
  46. data/lib/ruby_llm/providers/gemini.rb +3 -3
  47. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  48. data/lib/ruby_llm/providers/ollama/media.rb +48 -0
  49. data/lib/ruby_llm/providers/ollama.rb +34 -0
  50. data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
  51. data/lib/ruby_llm/providers/openai/chat.rb +6 -4
  52. data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
  53. data/lib/ruby_llm/providers/openai/images.rb +3 -2
  54. data/lib/ruby_llm/providers/openai/media.rb +48 -21
  55. data/lib/ruby_llm/providers/openai/models.rb +17 -18
  56. data/lib/ruby_llm/providers/openai/tools.rb +9 -5
  57. data/lib/ruby_llm/providers/openai.rb +7 -5
  58. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  59. data/lib/ruby_llm/providers/openrouter.rb +31 -0
  60. data/lib/ruby_llm/stream_accumulator.rb +4 -4
  61. data/lib/ruby_llm/streaming.rb +48 -13
  62. data/lib/ruby_llm/utils.rb +27 -0
  63. data/lib/ruby_llm/version.rb +1 -1
  64. data/lib/ruby_llm.rb +15 -5
  65. data/lib/tasks/aliases.rake +235 -0
  66. data/lib/tasks/models_docs.rake +164 -121
  67. data/lib/tasks/models_update.rake +79 -0
  68. data/lib/tasks/release.rake +32 -0
  69. data/lib/tasks/vcr.rake +4 -2
  70. metadata +56 -32
  71. data/lib/ruby_llm/model_info.rb +0 -56
  72. data/lib/tasks/browser_helper.rb +0 -97
  73. data/lib/tasks/capability_generator.rb +0 -123
  74. data/lib/tasks/capability_scraper.rb +0 -224
  75. data/lib/tasks/cli_helper.rb +0 -22
  76. data/lib/tasks/code_validator.rb +0 -29
  77. data/lib/tasks/model_updater.rb +0 -66
  78. data/lib/tasks/models.rake +0 -43
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotenv/load'
4
+ require 'ruby_llm'
5
+
6
+ task default: ['models:update']
7
+
8
+ namespace :models do
9
+ desc 'Update available models from providers (API keys needed)'
10
+ task :update do
11
+ puts 'Configuring RubyLLM...'
12
+ configure_from_env
13
+
14
+ refresh_models
15
+ display_model_stats
16
+ end
17
+ end
18
+
19
+ def configure_from_env
20
+ RubyLLM.configure do |config|
21
+ config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
22
+ config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
23
+ config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
24
+ config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
25
+ config.openrouter_api_key = ENV.fetch('OPENROUTER_API_KEY', nil)
26
+ configure_bedrock(config)
27
+ config.request_timeout = 30
28
+ end
29
+ end
30
+
31
+ def configure_bedrock(config)
32
+ config.bedrock_api_key = ENV.fetch('AWS_ACCESS_KEY_ID', nil)
33
+ config.bedrock_secret_key = ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
34
+ config.bedrock_region = ENV.fetch('AWS_REGION', nil)
35
+ config.bedrock_session_token = ENV.fetch('AWS_SESSION_TOKEN', nil)
36
+ end
37
+
38
+ def refresh_models
39
+ initial_count = RubyLLM.models.all.size
40
+ puts "Refreshing models (#{initial_count} cached)..."
41
+
42
+ models = RubyLLM.models.refresh!
43
+
44
+ if models.all.empty? && initial_count.zero?
45
+ puts 'Error: Failed to fetch models.'
46
+ exit(1)
47
+ elsif models.all.size == initial_count && initial_count.positive?
48
+ puts 'Warning: Model list unchanged.'
49
+ else
50
+ puts "Saving models.json (#{models.all.size} models)"
51
+ models.save_models
52
+ end
53
+
54
+ @models = models
55
+ end
56
+
57
+ def display_model_stats
58
+ puts "\nModel count:"
59
+ provider_counts = @models.all.group_by(&:provider).transform_values(&:count)
60
+
61
+ RubyLLM::Provider.providers.each_key do |sym|
62
+ name = sym.to_s.capitalize
63
+ count = provider_counts[sym.to_s] || 0
64
+ status = status(sym)
65
+ puts " #{name}: #{count} models #{status}"
66
+ end
67
+
68
+ puts 'Refresh complete.'
69
+ end
70
+
71
+ def status(provider_sym)
72
+ if RubyLLM::Provider.providers[provider_sym].local?
73
+ ' (LOCAL - SKIP)'
74
+ elsif RubyLLM::Provider.providers[provider_sym].configured?
75
+ ' (OK)'
76
+ else
77
+ ' (NOT CONFIGURED)'
78
+ end
79
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :release do
4
+ desc 'Verify cassettes are fresh enough for release'
5
+ task :verify_cassettes do
6
+ max_age_days = 1
7
+ cassette_dir = 'spec/fixtures/vcr_cassettes'
8
+ stale_cassettes = []
9
+
10
+ Dir.glob("#{cassette_dir}/**/*.yml").each do |cassette|
11
+ age_days = (Time.now - File.mtime(cassette)) / 86_400
12
+
13
+ next unless age_days > max_age_days
14
+
15
+ stale_cassettes << {
16
+ file: File.basename(cassette),
17
+ age: age_days.round(1)
18
+ }
19
+ end
20
+
21
+ if stale_cassettes.any?
22
+ puts "\n❌ Found stale cassettes (older than #{max_age_days} days):"
23
+ stale_cassettes.each do |c|
24
+ puts " - #{c[:file]} (#{c[:age]} days old)"
25
+ end
26
+ puts "\nRun locally: bundle exec rspec"
27
+ exit 1
28
+ else
29
+ puts "✅ All cassettes are fresh (< #{max_age_days} days old)"
30
+ end
31
+ end
32
+ end
data/lib/tasks/vcr.rake CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dotenv/load'
4
+
3
5
  # Helper functions at the top level
4
6
  def record_all_cassettes(cassette_dir)
5
7
  # Re-record all cassettes
@@ -11,7 +13,7 @@ def record_all_cassettes(cassette_dir)
11
13
  puts 'Done recording. Please review the new cassettes.'
12
14
  end
13
15
 
14
- def record_for_providers(providers, cassette_dir) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
16
+ def record_for_providers(providers, cassette_dir)
15
17
  # Get the list of available providers from RubyLLM itself
16
18
  all_providers = RubyLLM::Provider.providers.keys.map(&:to_s)
17
19
 
@@ -46,7 +48,7 @@ def record_for_providers(providers, cassette_dir) # rubocop:disable Metrics/AbcS
46
48
  puts 'Please review the updated cassettes for sensitive information.'
47
49
  end
48
50
 
49
- def find_matching_cassettes(dir, providers) # rubocop:disable Metrics/MethodLength
51
+ def find_matching_cassettes(dir, providers)
50
52
  cassettes = []
51
53
 
52
54
  Dir.glob("#{dir}/**/*.yml").each do |file|
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-04-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: base64
@@ -42,58 +41,72 @@ dependencies:
42
41
  name: faraday
43
42
  requirement: !ruby/object:Gem::Requirement
44
43
  requirements:
45
- - - "~>"
44
+ - - ">="
46
45
  - !ruby/object:Gem::Version
47
- version: '2'
46
+ version: 1.10.0
48
47
  type: :runtime
49
48
  prerelease: false
50
49
  version_requirements: !ruby/object:Gem::Requirement
51
50
  requirements:
52
- - - "~>"
51
+ - - ">="
53
52
  - !ruby/object:Gem::Version
54
- version: '2'
53
+ version: 1.10.0
55
54
  - !ruby/object:Gem::Dependency
56
55
  name: faraday-multipart
57
56
  requirement: !ruby/object:Gem::Requirement
58
57
  requirements:
59
- - - "~>"
58
+ - - ">="
60
59
  - !ruby/object:Gem::Version
61
60
  version: '1'
62
61
  type: :runtime
63
62
  prerelease: false
64
63
  version_requirements: !ruby/object:Gem::Requirement
65
64
  requirements:
66
- - - "~>"
65
+ - - ">="
67
66
  - !ruby/object:Gem::Version
68
67
  version: '1'
69
68
  - !ruby/object:Gem::Dependency
70
69
  name: faraday-net_http
71
70
  requirement: !ruby/object:Gem::Requirement
72
71
  requirements:
73
- - - "~>"
72
+ - - ">="
74
73
  - !ruby/object:Gem::Version
75
- version: '3'
74
+ version: '1'
76
75
  type: :runtime
77
76
  prerelease: false
78
77
  version_requirements: !ruby/object:Gem::Requirement
79
78
  requirements:
80
- - - "~>"
79
+ - - ">="
81
80
  - !ruby/object:Gem::Version
82
- version: '3'
81
+ version: '1'
83
82
  - !ruby/object:Gem::Dependency
84
83
  name: faraday-retry
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '1'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '1'
96
+ - !ruby/object:Gem::Dependency
97
+ name: marcel
85
98
  requirement: !ruby/object:Gem::Requirement
86
99
  requirements:
87
100
  - - "~>"
88
101
  - !ruby/object:Gem::Version
89
- version: '2'
102
+ version: '1.0'
90
103
  type: :runtime
91
104
  prerelease: false
92
105
  version_requirements: !ruby/object:Gem::Requirement
93
106
  requirements:
94
107
  - - "~>"
95
108
  - !ruby/object:Gem::Version
96
- version: '2'
109
+ version: '1.0'
97
110
  - !ruby/object:Gem::Dependency
98
111
  name: zeitwerk
99
112
  requirement: !ruby/object:Gem::Requirement
@@ -108,11 +121,12 @@ dependencies:
108
121
  - - "~>"
109
122
  - !ruby/object:Gem::Version
110
123
  version: '2'
111
- description: A delightful Ruby way to work with AI. Chat in text, analyze and generate
112
- images, understand audio, and use tools through a unified interface to OpenAI, Anthropic,
113
- Google, AWS Bedrock Anthropic, and DeepSeek. Built for developer happiness with
114
- automatic token counting, proper streaming, and Rails integration. No wrapping your
115
- head around multiple APIs - just clean Ruby code that works.
124
+ description: A delightful Ruby way to work with AI. Tired of juggling different SDKs?
125
+ RubyLLM provides one beautiful, Ruby-like interface for OpenAI, Anthropic, Gemini,
126
+ Bedrock, OpenRouter, DeepSeek, Ollama, and any OpenAI-compatible API. Chat (with
127
+ text, images, audio, PDFs), generate images, create embeddings, use tools (function
128
+ calling), stream responses, and integrate with Rails effortlessly. Minimal dependencies,
129
+ maximum developer happiness - just clean Ruby code that works.
116
130
  email:
117
131
  - carmine@paolino.me
118
132
  executables: []
@@ -125,15 +139,24 @@ files:
125
139
  - lib/ruby_llm/active_record/acts_as.rb
126
140
  - lib/ruby_llm/aliases.json
127
141
  - lib/ruby_llm/aliases.rb
142
+ - lib/ruby_llm/attachment.rb
128
143
  - lib/ruby_llm/chat.rb
129
144
  - lib/ruby_llm/chunk.rb
130
145
  - lib/ruby_llm/configuration.rb
146
+ - lib/ruby_llm/connection.rb
131
147
  - lib/ruby_llm/content.rb
148
+ - lib/ruby_llm/context.rb
132
149
  - lib/ruby_llm/embedding.rb
133
150
  - lib/ruby_llm/error.rb
134
151
  - lib/ruby_llm/image.rb
135
152
  - lib/ruby_llm/message.rb
136
- - lib/ruby_llm/model_info.rb
153
+ - lib/ruby_llm/mime_type.rb
154
+ - lib/ruby_llm/model.rb
155
+ - lib/ruby_llm/model/info.rb
156
+ - lib/ruby_llm/model/modalities.rb
157
+ - lib/ruby_llm/model/pricing.rb
158
+ - lib/ruby_llm/model/pricing_category.rb
159
+ - lib/ruby_llm/model/pricing_tier.rb
137
160
  - lib/ruby_llm/models.json
138
161
  - lib/ruby_llm/models.rb
139
162
  - lib/ruby_llm/provider.rb
@@ -148,6 +171,7 @@ files:
148
171
  - lib/ruby_llm/providers/bedrock.rb
149
172
  - lib/ruby_llm/providers/bedrock/capabilities.rb
150
173
  - lib/ruby_llm/providers/bedrock/chat.rb
174
+ - lib/ruby_llm/providers/bedrock/media.rb
151
175
  - lib/ruby_llm/providers/bedrock/models.rb
152
176
  - lib/ruby_llm/providers/bedrock/signing.rb
153
177
  - lib/ruby_llm/providers/bedrock/streaming.rb
@@ -168,6 +192,9 @@ files:
168
192
  - lib/ruby_llm/providers/gemini/models.rb
169
193
  - lib/ruby_llm/providers/gemini/streaming.rb
170
194
  - lib/ruby_llm/providers/gemini/tools.rb
195
+ - lib/ruby_llm/providers/ollama.rb
196
+ - lib/ruby_llm/providers/ollama/chat.rb
197
+ - lib/ruby_llm/providers/ollama/media.rb
171
198
  - lib/ruby_llm/providers/openai.rb
172
199
  - lib/ruby_llm/providers/openai/capabilities.rb
173
200
  - lib/ruby_llm/providers/openai/chat.rb
@@ -177,20 +204,19 @@ files:
177
204
  - lib/ruby_llm/providers/openai/models.rb
178
205
  - lib/ruby_llm/providers/openai/streaming.rb
179
206
  - lib/ruby_llm/providers/openai/tools.rb
207
+ - lib/ruby_llm/providers/openrouter.rb
208
+ - lib/ruby_llm/providers/openrouter/models.rb
180
209
  - lib/ruby_llm/railtie.rb
181
210
  - lib/ruby_llm/stream_accumulator.rb
182
211
  - lib/ruby_llm/streaming.rb
183
212
  - lib/ruby_llm/tool.rb
184
213
  - lib/ruby_llm/tool_call.rb
214
+ - lib/ruby_llm/utils.rb
185
215
  - lib/ruby_llm/version.rb
186
- - lib/tasks/browser_helper.rb
187
- - lib/tasks/capability_generator.rb
188
- - lib/tasks/capability_scraper.rb
189
- - lib/tasks/cli_helper.rb
190
- - lib/tasks/code_validator.rb
191
- - lib/tasks/model_updater.rb
192
- - lib/tasks/models.rake
216
+ - lib/tasks/aliases.rake
193
217
  - lib/tasks/models_docs.rake
218
+ - lib/tasks/models_update.rake
219
+ - lib/tasks/release.rake
194
220
  - lib/tasks/vcr.rake
195
221
  homepage: https://rubyllm.com
196
222
  licenses:
@@ -202,7 +228,6 @@ metadata:
202
228
  documentation_uri: https://rubyllm.com
203
229
  bug_tracker_uri: https://github.com/crmne/ruby_llm/issues
204
230
  rubygems_mfa_required: 'true'
205
- post_install_message:
206
231
  rdoc_options: []
207
232
  require_paths:
208
233
  - lib
@@ -217,8 +242,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
217
242
  - !ruby/object:Gem::Version
218
243
  version: '0'
219
244
  requirements: []
220
- rubygems_version: 3.5.22
221
- signing_key:
245
+ rubygems_version: 3.6.7
222
246
  specification_version: 4
223
- summary: Beautiful Ruby interface to modern AI
247
+ summary: A single delightful Ruby way to work with AI.
224
248
  test_files: []
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'time'
4
-
5
- module RubyLLM
6
- # Information about an AI model's capabilities, pricing, and metadata.
7
- # Used by the Models registry to help developers choose the right model
8
- # for their needs.
9
- #
10
- # Example:
11
- # model = RubyLLM.models.find('gpt-4')
12
- # model.supports_vision? # => true
13
- # model.supports_functions? # => true
14
- # model.input_price_per_million # => 30.0
15
- class ModelInfo
16
- attr_reader :id, :created_at, :display_name, :provider, :metadata,
17
- :context_window, :max_tokens, :supports_vision, :supports_functions,
18
- :supports_json_mode, :input_price_per_million, :output_price_per_million, :type, :family
19
-
20
- def initialize(data) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
21
- @id = data[:id]
22
- @created_at = data[:created_at].is_a?(String) ? Time.parse(data[:created_at]) : data[:created_at]
23
- @display_name = data[:display_name]
24
- @provider = data[:provider]
25
- @context_window = data[:context_window]
26
- @max_tokens = data[:max_tokens]
27
- @type = data[:type]
28
- @family = data[:family]
29
- @supports_vision = data[:supports_vision]
30
- @supports_functions = data[:supports_functions]
31
- @supports_json_mode = data[:supports_json_mode]
32
- @input_price_per_million = data[:input_price_per_million]
33
- @output_price_per_million = data[:output_price_per_million]
34
- @metadata = data[:metadata] || {}
35
- end
36
-
37
- def to_h # rubocop:disable Metrics/MethodLength
38
- {
39
- id: id,
40
- created_at: created_at&.iso8601,
41
- display_name: display_name,
42
- provider: provider,
43
- context_window: context_window,
44
- max_tokens: max_tokens,
45
- type: type,
46
- family: family,
47
- supports_vision: supports_vision,
48
- supports_functions: supports_functions,
49
- supports_json_mode: supports_json_mode,
50
- input_price_per_million: input_price_per_million,
51
- output_price_per_million: output_price_per_million,
52
- metadata: metadata
53
- }
54
- end
55
- end
56
- end
@@ -1,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'ferrum'
4
- require_relative 'cli_helper'
5
-
6
- class BrowserHelper # rubocop:disable Style/Documentation
7
- REALISTIC_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' # rubocop:disable Layout/LineLength
8
-
9
- def initialize
10
- @browser = create_browser
11
- end
12
-
13
- def goto(url)
14
- @browser.goto(url)
15
- end
16
-
17
- def current_url
18
- @browser.page.url
19
- rescue StandardError
20
- 'N/A'
21
- end
22
-
23
- def get_page_content(context = 'current page') # rubocop:disable Metrics/MethodLength
24
- puts " Extracting HTML for #{context}..."
25
-
26
- begin
27
- sleep(1.0) # Small delay for page stability
28
- html = @browser.body
29
-
30
- if html && !html.empty?
31
- puts " Extracted ~#{html.length} chars of HTML"
32
- puts ' WARNING: Challenge page detected' if html.match?(/challenge-platform|Checking site/)
33
- html
34
- else
35
- puts ' Warning: Empty content returned'
36
- ''
37
- end
38
- rescue StandardError => e
39
- puts " Error getting HTML: #{e.class} - #{e.message}"
40
- ''
41
- end
42
- end
43
-
44
- def wait_for_page_load
45
- handle_cloudflare_challenge
46
- end
47
-
48
- def close
49
- puts "\nClosing browser..."
50
- @browser.quit
51
- rescue StandardError => e
52
- puts " Warning: Error closing browser: #{e.message}"
53
- end
54
-
55
- private
56
-
57
- def create_browser
58
- puts ' Initializing browser for manual interaction...'
59
-
60
- Ferrum::Browser.new(
61
- window_size: [1366, 768],
62
- headless: false,
63
- browser_options: browser_options,
64
- timeout: 120,
65
- process_timeout: 120,
66
- pending_connection_errors: false
67
- )
68
- end
69
-
70
- def browser_options
71
- {
72
- 'user-agent' => REALISTIC_USER_AGENT,
73
- 'disable-gpu' => nil,
74
- 'no-sandbox' => nil,
75
- 'disable-blink-features' => 'AutomationControlled',
76
- 'disable-infobars' => nil,
77
- 'start-maximized' => nil
78
- }
79
- end
80
-
81
- def handle_cloudflare_challenge # rubocop:disable Metrics/MethodLength
82
- puts "\nWaiting for Cloudflare challenge resolution..."
83
- puts 'c: Challenge solved'
84
- puts 'q: Quit/Skip'
85
-
86
- choice = CliHelper.get_user_choice('Confirm when ready', %w[c q])
87
- return false if choice == 'q'
88
-
89
- begin
90
- @browser.page.target_id
91
- true
92
- rescue StandardError
93
- puts 'Browser check failed after challenge'
94
- false
95
- end
96
- end
97
- end
@@ -1,123 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'ruby_llm'
4
- require 'json'
5
- require_relative 'code_validator'
6
-
7
- class CapabilityGenerator # rubocop:disable Style/Documentation
8
- def initialize(provider_name, docs_html)
9
- @provider_name = provider_name
10
- @docs_html = docs_html
11
- @processed_html = process_html(docs_html)
12
- end
13
-
14
- def generate_and_save # rubocop:disable Metrics/MethodLength
15
- puts " Starting code generation for #{@provider_name}..."
16
-
17
- existing_path = File.expand_path("../../lib/ruby_llm/providers/#{@provider_name}/capabilities.rb", __dir__)
18
- unless File.exist?(existing_path)
19
- puts " Skipping: No file at #{existing_path}"
20
- return
21
- end
22
-
23
- existing_code = File.read(existing_path)
24
- puts ' Read existing code'
25
-
26
- generated_code = generate_capabilities(existing_code)
27
-
28
- if generated_code
29
- puts " Writing updated code to #{existing_path}..."
30
- File.write(existing_path, generated_code)
31
- puts " Updated #{@provider_name}"
32
-
33
- verify_code_with_models_update
34
- else
35
- puts " Failed to generate valid code for #{@provider_name}"
36
- end
37
- end
38
-
39
- private
40
-
41
- def process_html(docs_html) # rubocop:disable Metrics/MethodLength
42
- docs_html.transform_values do |html|
43
- next '' if html.nil? || html.empty?
44
-
45
- # Extract just the main content areas, skip scripts, styles, etc
46
- main_content = html.scan(%r{<main.*?>.*?</main>}m).first ||
47
- html.scan(%r{<article.*?>.*?</article>}m).first ||
48
- html.scan(%r{<div class="content.*?>.*?</div>}m).first
49
-
50
- if main_content
51
- # Further clean up the content
52
- main_content.gsub(%r{<script.*?>.*?</script>}m, '')
53
- .gsub(%r{<style.*?>.*?</style>}m, '')
54
- .gsub(/<!--.*?-->/m, '')
55
- .gsub(/\s+/, ' ')
56
- .strip
57
- else
58
- ''
59
- end
60
- end
61
- end
62
-
63
- def generate_capabilities(existing_code) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
64
- max_retries = 3
65
- retries = 0
66
-
67
- loop do # rubocop:disable Metrics/BlockLength
68
- puts " Attempt #{retries + 1}/#{max_retries}..."
69
-
70
- begin
71
- gemini = RubyLLM.chat(model: 'gemini-1.5-pro-latest')
72
- .with_temperature(0.1)
73
-
74
- docs_json = JSON.pretty_generate(@processed_html)
75
-
76
- prompt = <<~PROMPT
77
- Update RubyLLM::Providers::#{@provider_name.capitalize}::Capabilities module.
78
- Only use the provided HTML content and existing code structure.
79
- Focus on updating values while preserving the module structure.
80
-
81
- Existing code to maintain structure:
82
- ```ruby
83
- #{existing_code}
84
- ```
85
-
86
- HTML content to extract new values from:
87
- ```json
88
- #{docs_json}
89
- ```
90
-
91
- Return ONLY the complete Ruby code within ```ruby ``` tags.
92
- PROMPT
93
-
94
- response = gemini.ask(prompt)
95
- generated_code = CodeValidator.extract_code_from_response(response.content)
96
-
97
- return generated_code if generated_code && CodeValidator.validate_syntax(generated_code)
98
- rescue RubyLLM::BadRequestError => e
99
- puts " Error: #{e.message}"
100
- # Try with even less content if we hit token limits
101
- @processed_html = @processed_html.transform_values { |html| html[0..10_000] }
102
- rescue StandardError => e
103
- puts " Error: #{e.class} - #{e.message}"
104
- end
105
-
106
- retries += 1
107
- break if retries >= max_retries
108
- end
109
-
110
- nil
111
- end
112
-
113
- def verify_code_with_models_update
114
- puts ' Verifying with models:update...'
115
- begin
116
- Rake::Task['models:update'].reenable
117
- Rake::Task['models:update'].invoke
118
- puts ' Verification successful'
119
- rescue StandardError => e
120
- puts " Verification failed: #{e.message}"
121
- end
122
- end
123
- end