ruby_llm 1.0.1 → 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.
- checksums.yaml +4 -4
- data/README.md +28 -12
- data/lib/ruby_llm/active_record/acts_as.rb +46 -7
- data/lib/ruby_llm/aliases.json +65 -0
- data/lib/ruby_llm/aliases.rb +56 -0
- data/lib/ruby_llm/chat.rb +10 -9
- data/lib/ruby_llm/configuration.rb +4 -0
- data/lib/ruby_llm/error.rb +15 -4
- data/lib/ruby_llm/models.json +1163 -303
- data/lib/ruby_llm/models.rb +40 -11
- data/lib/ruby_llm/provider.rb +32 -39
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +8 -9
- data/lib/ruby_llm/providers/anthropic/chat.rb +31 -4
- data/lib/ruby_llm/providers/anthropic/streaming.rb +12 -6
- data/lib/ruby_llm/providers/anthropic.rb +4 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +168 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +108 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +84 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +46 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +90 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
- data/lib/ruby_llm/providers/bedrock.rb +83 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
- data/lib/ruby_llm/providers/deepseek.rb +5 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +50 -34
- data/lib/ruby_llm/providers/gemini/chat.rb +8 -15
- data/lib/ruby_llm/providers/gemini/images.rb +5 -10
- data/lib/ruby_llm/providers/gemini/streaming.rb +35 -76
- data/lib/ruby_llm/providers/gemini/tools.rb +12 -12
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +146 -206
- data/lib/ruby_llm/providers/openai/streaming.rb +9 -13
- data/lib/ruby_llm/providers/openai.rb +4 -0
- data/lib/ruby_llm/streaming.rb +96 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +6 -3
- data/lib/tasks/browser_helper.rb +97 -0
- data/lib/tasks/capability_generator.rb +123 -0
- data/lib/tasks/capability_scraper.rb +224 -0
- data/lib/tasks/cli_helper.rb +22 -0
- data/lib/tasks/code_validator.rb +29 -0
- data/lib/tasks/model_updater.rb +66 -0
- data/lib/tasks/models.rake +28 -193
- data/lib/tasks/vcr.rake +13 -30
- metadata +27 -19
- data/.github/workflows/cicd.yml +0 -158
- data/.github/workflows/docs.yml +0 -53
- data/.gitignore +0 -59
- data/.overcommit.yml +0 -26
- data/.rspec +0 -3
- data/.rubocop.yml +0 -10
- data/.yardopts +0 -12
- data/CONTRIBUTING.md +0 -207
- data/Gemfile +0 -33
- data/Rakefile +0 -9
- data/bin/console +0 -17
- data/bin/setup +0 -6
- data/ruby_llm.gemspec +0 -44
@@ -0,0 +1,97 @@
|
|
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
|
@@ -0,0 +1,123 @@
|
|
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
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ruby_llm'
|
4
|
+
require 'uri'
|
5
|
+
require_relative 'browser_helper'
|
6
|
+
require_relative 'cli_helper'
|
7
|
+
|
8
|
+
class CapabilityScraper # rubocop:disable Metrics/ClassLength,Style/Documentation
|
9
|
+
PROVIDER_DOCS = {
|
10
|
+
openai: {
|
11
|
+
url: 'https://platform.openai.com/docs/models'
|
12
|
+
},
|
13
|
+
anthropic: {
|
14
|
+
url: 'https://docs.anthropic.com/claude/docs/models-overview'
|
15
|
+
},
|
16
|
+
gemini: {
|
17
|
+
url: 'https://ai.google.dev/models/gemini',
|
18
|
+
pricing_url: 'https://ai.google.dev/pricing'
|
19
|
+
},
|
20
|
+
deepseek: {
|
21
|
+
url: 'https://api-docs.deepseek.com/quick_start/pricing'
|
22
|
+
}
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
def self.parse_providers(providers_arg) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
26
|
+
provider_input = (providers_arg || '').downcase.split(',').map(&:strip).reject(&:empty?)
|
27
|
+
|
28
|
+
target_providers = if provider_input.empty? || provider_input.include?('all')
|
29
|
+
PROVIDER_DOCS.keys
|
30
|
+
else
|
31
|
+
provider_input.map(&:to_sym).select { |p| PROVIDER_DOCS.key?(p) }
|
32
|
+
end
|
33
|
+
|
34
|
+
if target_providers.empty?
|
35
|
+
puts "No valid providers. Available: all, #{PROVIDER_DOCS.keys.join(', ')}"
|
36
|
+
exit(1)
|
37
|
+
end
|
38
|
+
|
39
|
+
puts "Targeting providers: #{target_providers.join(', ')}"
|
40
|
+
target_providers
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(providers)
|
44
|
+
@providers = providers
|
45
|
+
@browser_helper = BrowserHelper.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def run # rubocop:disable Metrics/MethodLength
|
49
|
+
@providers.each do |provider_sym|
|
50
|
+
provider_name = provider_sym.to_s
|
51
|
+
puts "\n======== Processing Provider: #{provider_name.upcase} =========="
|
52
|
+
|
53
|
+
docs_info = PROVIDER_DOCS[provider_sym]
|
54
|
+
docs_html = scrape_provider(provider_sym, docs_info)
|
55
|
+
|
56
|
+
next unless docs_html.any? { |_k, v| v && !v.strip.empty? }
|
57
|
+
|
58
|
+
summarize_html_data(docs_html)
|
59
|
+
yield(provider_name, docs_html) if block_given?
|
60
|
+
end
|
61
|
+
ensure
|
62
|
+
@browser_helper&.close
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def scrape_provider(provider_sym, docs_info)
|
68
|
+
if provider_sym == :openai
|
69
|
+
handle_openai_scraping(docs_info[:url])
|
70
|
+
else
|
71
|
+
handle_standard_scraping(provider_sym, docs_info)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_openai_scraping(main_url) # rubocop:disable Metrics/MethodLength
|
76
|
+
main_url_path = extract_path_from_url(main_url)
|
77
|
+
overview_key = generate_key_from_url(main_url, main_url_path)
|
78
|
+
docs_html = {}
|
79
|
+
|
80
|
+
puts "\n--- Interactive Scraping for OpenAI ---"
|
81
|
+
puts " Navigating to main OpenAI models page: #{main_url}..."
|
82
|
+
|
83
|
+
begin
|
84
|
+
@browser_helper.goto(main_url)
|
85
|
+
return docs_html unless @browser_helper.wait_for_page_load
|
86
|
+
|
87
|
+
puts ' Scraping overview page HTML...'
|
88
|
+
overview_html = @browser_helper.get_page_content('OpenAI Overview')
|
89
|
+
docs_html[overview_key] = overview_html if overview_html && !overview_html.empty?
|
90
|
+
|
91
|
+
# Interactive scraping loop
|
92
|
+
interactive_scraping_loop(docs_html, main_url, main_url_path, overview_key)
|
93
|
+
rescue StandardError => e
|
94
|
+
puts "FATAL: Navigation failed: #{e}"
|
95
|
+
end
|
96
|
+
|
97
|
+
docs_html
|
98
|
+
end
|
99
|
+
|
100
|
+
def interactive_scraping_loop(docs_html, main_url, main_url_path, overview_key) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
101
|
+
loop do # rubocop:disable Metrics/BlockLength
|
102
|
+
current_url = begin
|
103
|
+
@browser_helper.current_url
|
104
|
+
rescue StandardError
|
105
|
+
'N/A'
|
106
|
+
end
|
107
|
+
puts "\n--- OpenAI Subpage Scraping ---"
|
108
|
+
puts "Current URL: #{current_url}"
|
109
|
+
puts "Collected Keys: #{docs_html.keys.sort.join(', ')}"
|
110
|
+
puts '---------------------------------'
|
111
|
+
|
112
|
+
puts 'Ready to scrape current page. Options:'
|
113
|
+
puts 's: Scrape current page'
|
114
|
+
puts 'b: Back to overview'
|
115
|
+
puts 'q: Quit scraping'
|
116
|
+
|
117
|
+
choice = CliHelper.get_user_choice('Choose action', %w[s b q])
|
118
|
+
|
119
|
+
case choice
|
120
|
+
when 's'
|
121
|
+
scrape_current_page(docs_html, main_url_path, overview_key)
|
122
|
+
when 'b'
|
123
|
+
puts " Navigating back to overview: #{main_url}..."
|
124
|
+
begin
|
125
|
+
@browser_helper.goto(main_url)
|
126
|
+
@browser_helper.wait_for_page_load
|
127
|
+
rescue StandardError => e
|
128
|
+
puts " Error navigating back: #{e.message}. Navigate manually."
|
129
|
+
end
|
130
|
+
when 'q'
|
131
|
+
puts ' Finished OpenAI scraping.'
|
132
|
+
break
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def scrape_current_page(docs_html, main_url_path, overview_key) # rubocop:disable Metrics/MethodLength
|
138
|
+
current_url = begin
|
139
|
+
@browser_helper.current_url
|
140
|
+
rescue StandardError
|
141
|
+
'N/A'
|
142
|
+
end
|
143
|
+
puts " Scraping content from: #{current_url}"
|
144
|
+
page_html = @browser_helper.get_page_content(current_url)
|
145
|
+
|
146
|
+
if page_html && !page_html.empty?
|
147
|
+
page_key = generate_key_from_url(current_url, main_url_path)
|
148
|
+
if page_key != overview_key || !docs_html.key?(overview_key)
|
149
|
+
docs_html[page_key] = page_html
|
150
|
+
puts " Stored HTML under key: #{page_key}"
|
151
|
+
else
|
152
|
+
puts ' Note: Back on overview page, skipping.'
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
next_choice = CliHelper.get_user_choice('Continue? (y: scrape another, n: quit)', %w[y n])
|
157
|
+
false if next_choice == 'n'
|
158
|
+
end
|
159
|
+
|
160
|
+
def handle_standard_scraping(provider, info)
|
161
|
+
provider_name = provider.to_s
|
162
|
+
docs_html = {}
|
163
|
+
|
164
|
+
puts "\n--- Standard Scraping for #{provider_name.upcase} ---"
|
165
|
+
|
166
|
+
# Main page scraping
|
167
|
+
scrape_url(provider_name, info[:url], "#{provider_name}_main_html", docs_html)
|
168
|
+
|
169
|
+
# Pricing page scraping (if available)
|
170
|
+
if info[:pricing_url] && info[:pricing_url] != info[:url]
|
171
|
+
scrape_url(provider_name, info[:pricing_url], "#{provider_name}_pricing_html", docs_html)
|
172
|
+
end
|
173
|
+
|
174
|
+
docs_html
|
175
|
+
end
|
176
|
+
|
177
|
+
def scrape_url(provider_name, url, key_name, docs_html)
|
178
|
+
puts " Scraping #{key_name}: #{url}..."
|
179
|
+
|
180
|
+
begin
|
181
|
+
@browser_helper.goto(url)
|
182
|
+
return unless @browser_helper.wait_for_page_load
|
183
|
+
|
184
|
+
html_content = @browser_helper.get_page_content("#{provider_name} #{key_name}")
|
185
|
+
docs_html[key_name] = html_content if html_content && !html_content.empty?
|
186
|
+
rescue StandardError => e
|
187
|
+
puts " Error on #{key_name}: #{e.message}"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def extract_path_from_url(url_string)
|
192
|
+
URI(url_string).path
|
193
|
+
rescue StandardError
|
194
|
+
'/docs/models'
|
195
|
+
end
|
196
|
+
|
197
|
+
def generate_key_from_url(url_string, base_url_path = '/docs/models')
|
198
|
+
uri = URI(url_string)
|
199
|
+
path = uri.path.chomp('/')
|
200
|
+
return 'models_overview_html' if path == base_url_path
|
201
|
+
|
202
|
+
key_part = path.split('/').reject(&:empty?).last || 'unknown'
|
203
|
+
"model_#{key_part.gsub(/[^a-z0-9_\-]/i, '_')}_html"
|
204
|
+
rescue StandardError => e
|
205
|
+
puts " Warning: URL parsing failed for: #{url_string} (#{e.message})"
|
206
|
+
"scrape_#{Time.now.to_i}_html"
|
207
|
+
end
|
208
|
+
|
209
|
+
def summarize_html_data(docs_html) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
210
|
+
docs_summary = docs_html.map do |k, v|
|
211
|
+
desc = if v.nil? || v.strip.empty?
|
212
|
+
'(Empty)'
|
213
|
+
elsif v.include?('challenge') || v.include?('Checking')
|
214
|
+
'(Challenge?)'
|
215
|
+
else
|
216
|
+
'(OK)'
|
217
|
+
end
|
218
|
+
"#{k}: ~#{v&.length || 0} chars #{desc}"
|
219
|
+
end.join(', ')
|
220
|
+
|
221
|
+
puts "\n HTML Summary: #{docs_summary}"
|
222
|
+
puts ' Warning: LLM results may be inaccurate.' if docs_summary.include?('(Challenge?)')
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/console'
|
4
|
+
|
5
|
+
class CliHelper # rubocop:disable Style/Documentation
|
6
|
+
def self.get_user_choice(prompt, valid_choices) # rubocop:disable Metrics/MethodLength
|
7
|
+
loop do
|
8
|
+
print "\n#{prompt} [#{valid_choices.join('/')}]: "
|
9
|
+
begin
|
10
|
+
choice = $stdin.getch.downcase
|
11
|
+
puts choice # Echo the character
|
12
|
+
|
13
|
+
return choice if valid_choices.include?(choice)
|
14
|
+
|
15
|
+
puts " Invalid choice '#{choice}'. Valid options: #{valid_choices.join(', ')}"
|
16
|
+
rescue Interrupt
|
17
|
+
puts "\n*** Input interrupted."
|
18
|
+
return 'q'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ripper'
|
4
|
+
|
5
|
+
class CodeValidator # rubocop:disable Style/Documentation
|
6
|
+
# Use Ruby's built-in Ripper to validate Ruby syntax instead of eval
|
7
|
+
def self.validate_syntax(code)
|
8
|
+
# Ripper.sexp returns nil if there's a syntax error
|
9
|
+
result = Ripper.sexp(code)
|
10
|
+
|
11
|
+
if result.nil?
|
12
|
+
puts 'Syntax error in generated code'
|
13
|
+
false
|
14
|
+
else
|
15
|
+
# Additional validation could go here
|
16
|
+
true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.extract_code_from_response(content)
|
21
|
+
match = content.match(/```ruby\s*(.*?)\s*```/m)
|
22
|
+
if match
|
23
|
+
match[1].strip
|
24
|
+
else
|
25
|
+
puts 'No Ruby code block found in response'
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ruby_llm'
|
4
|
+
|
5
|
+
class ModelUpdater # rubocop:disable Style/Documentation
|
6
|
+
def run
|
7
|
+
puts 'Configuring RubyLLM...'
|
8
|
+
configure_from_env
|
9
|
+
|
10
|
+
refresh_models
|
11
|
+
display_model_stats
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def configure_from_env
|
17
|
+
RubyLLM.configure do |config|
|
18
|
+
config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
|
19
|
+
config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
|
20
|
+
config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
|
21
|
+
config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
|
22
|
+
configure_bedrock(config)
|
23
|
+
config.request_timeout = 30
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def configure_bedrock(config)
|
28
|
+
config.bedrock_api_key = ENV.fetch('AWS_ACCESS_KEY_ID', nil)
|
29
|
+
config.bedrock_secret_key = ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
|
30
|
+
config.bedrock_region = ENV.fetch('AWS_REGION', nil)
|
31
|
+
config.bedrock_session_token = ENV.fetch('AWS_SESSION_TOKEN', nil)
|
32
|
+
end
|
33
|
+
|
34
|
+
def refresh_models # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
35
|
+
initial_count = RubyLLM.models.all.size
|
36
|
+
puts "Refreshing models (#{initial_count} cached)..."
|
37
|
+
|
38
|
+
models = RubyLLM.models.refresh!
|
39
|
+
|
40
|
+
if models.all.empty? && initial_count.zero?
|
41
|
+
puts 'Error: Failed to fetch models.'
|
42
|
+
exit(1)
|
43
|
+
elsif models.all.size == initial_count && initial_count.positive?
|
44
|
+
puts 'Warning: Model list unchanged.'
|
45
|
+
else
|
46
|
+
puts "Saving models.json (#{models.all.size} models)"
|
47
|
+
models.save_models
|
48
|
+
end
|
49
|
+
|
50
|
+
@models = models
|
51
|
+
end
|
52
|
+
|
53
|
+
def display_model_stats
|
54
|
+
puts "\nModel count:"
|
55
|
+
provider_counts = @models.all.group_by(&:provider).transform_values(&:count)
|
56
|
+
|
57
|
+
RubyLLM::Provider.providers.each_key do |sym|
|
58
|
+
name = sym.to_s.capitalize
|
59
|
+
count = provider_counts[sym.to_s] || 0
|
60
|
+
status = RubyLLM::Provider.providers[sym].configured? ? '(OK)' : '(SKIP)'
|
61
|
+
puts " #{name}: #{count} models #{status}"
|
62
|
+
end
|
63
|
+
|
64
|
+
puts 'Refresh complete.'
|
65
|
+
end
|
66
|
+
end
|