ruby_llm 1.2.0 → 1.3.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 +80 -133
- data/lib/ruby_llm/active_record/acts_as.rb +212 -33
- data/lib/ruby_llm/aliases.json +48 -6
- data/lib/ruby_llm/attachments/audio.rb +12 -0
- data/lib/ruby_llm/attachments/image.rb +9 -0
- data/lib/ruby_llm/attachments/pdf.rb +9 -0
- data/lib/ruby_llm/attachments.rb +78 -0
- data/lib/ruby_llm/chat.rb +22 -19
- data/lib/ruby_llm/configuration.rb +30 -1
- data/lib/ruby_llm/connection.rb +95 -0
- data/lib/ruby_llm/content.rb +51 -72
- data/lib/ruby_llm/context.rb +30 -0
- data/lib/ruby_llm/embedding.rb +13 -5
- data/lib/ruby_llm/error.rb +1 -1
- data/lib/ruby_llm/image.rb +13 -5
- data/lib/ruby_llm/message.rb +12 -4
- data/lib/ruby_llm/mime_types.rb +713 -0
- data/lib/ruby_llm/model_info.rb +208 -27
- data/lib/ruby_llm/models.json +25766 -2154
- data/lib/ruby_llm/models.rb +95 -14
- data/lib/ruby_llm/provider.rb +48 -90
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
- data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
- data/lib/ruby_llm/providers/anthropic/media.rb +44 -34
- data/lib/ruby_llm/providers/anthropic/models.rb +15 -15
- data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
- data/lib/ruby_llm/providers/anthropic.rb +3 -3
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
- data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
- data/lib/ruby_llm/providers/bedrock/media.rb +56 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
- data/lib/ruby_llm/providers/bedrock.rb +14 -25
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
- data/lib/ruby_llm/providers/deepseek.rb +3 -3
- data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
- data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
- data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
- data/lib/ruby_llm/providers/gemini/images.rb +2 -2
- data/lib/ruby_llm/providers/gemini/media.rb +39 -110
- data/lib/ruby_llm/providers/gemini/models.rb +16 -22
- data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
- data/lib/ruby_llm/providers/gemini.rb +3 -3
- data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
- data/lib/ruby_llm/providers/ollama/media.rb +44 -0
- data/lib/ruby_llm/providers/ollama.rb +34 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
- data/lib/ruby_llm/providers/openai/chat.rb +6 -4
- data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
- data/lib/ruby_llm/providers/openai/media.rb +38 -21
- data/lib/ruby_llm/providers/openai/models.rb +16 -17
- data/lib/ruby_llm/providers/openai/tools.rb +9 -5
- data/lib/ruby_llm/providers/openai.rb +7 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
- data/lib/ruby_llm/providers/openrouter.rb +31 -0
- data/lib/ruby_llm/stream_accumulator.rb +4 -4
- data/lib/ruby_llm/streaming.rb +3 -3
- data/lib/ruby_llm/utils.rb +22 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +15 -5
- data/lib/tasks/models.rake +69 -33
- data/lib/tasks/models_docs.rake +164 -121
- data/lib/tasks/vcr.rake +4 -2
- metadata +23 -14
- data/lib/tasks/browser_helper.rb +0 -97
- data/lib/tasks/capability_generator.rb +0 -123
- data/lib/tasks/capability_scraper.rb +0 -224
- data/lib/tasks/cli_helper.rb +0 -22
- data/lib/tasks/code_validator.rb +0 -29
- data/lib/tasks/model_updater.rb +0 -66
data/lib/ruby_llm/aliases.json
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
{
|
2
2
|
"claude-3-5-sonnet": {
|
3
3
|
"anthropic": "claude-3-5-sonnet-20241022",
|
4
|
-
"bedrock": "anthropic.claude-3-5-sonnet-20241022-v2:0"
|
4
|
+
"bedrock": "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
5
|
+
"openrouter": "anthropic/claude-3.5-sonnet"
|
5
6
|
},
|
6
7
|
"claude-3-5-haiku": {
|
7
8
|
"anthropic": "claude-3-5-haiku-20241022",
|
8
|
-
"bedrock": "anthropic.claude-3-5-haiku-20241022-v1:0"
|
9
|
+
"bedrock": "anthropic.claude-3-5-haiku-20241022-v1:0",
|
10
|
+
"openrouter": "anthropic/claude-3.5-haiku-20241022"
|
9
11
|
},
|
10
12
|
"claude-3-7-sonnet": {
|
11
13
|
"anthropic": "claude-3-7-sonnet-20250219",
|
12
|
-
"bedrock": "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
|
14
|
+
"bedrock": "us.anthropic.claude-3-7-sonnet-20250219-v1:0",
|
15
|
+
"openrouter": "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
|
13
16
|
},
|
14
17
|
"claude-3-opus": {
|
15
18
|
"anthropic": "claude-3-opus-20240229",
|
@@ -25,14 +28,53 @@
|
|
25
28
|
},
|
26
29
|
"claude-3": {
|
27
30
|
"anthropic": "claude-3-sonnet-20240229",
|
28
|
-
"bedrock": "anthropic.claude-3-sonnet-20240229-v1:0"
|
31
|
+
"bedrock": "anthropic.claude-3-sonnet-20240229-v1:0",
|
32
|
+
"openrouter": "anthropic/claude-3-sonnet"
|
29
33
|
},
|
30
34
|
"claude-2": {
|
31
35
|
"anthropic": "claude-2.0",
|
32
|
-
"bedrock": "anthropic.claude-2.0"
|
36
|
+
"bedrock": "anthropic.claude-2.0",
|
37
|
+
"openrouter": "anthropic/claude-2"
|
33
38
|
},
|
34
39
|
"claude-2-1": {
|
35
40
|
"anthropic": "claude-2.1",
|
36
|
-
"bedrock": "anthropic.claude-2.1"
|
41
|
+
"bedrock": "anthropic.claude-2.1",
|
42
|
+
"openrouter": "anthropic/claude-2.1"
|
43
|
+
},
|
44
|
+
"gpt-4o": {
|
45
|
+
"openai": "gpt-4o",
|
46
|
+
"openrouter": "openai/gpt-4o"
|
47
|
+
},
|
48
|
+
"gpt-4o-mini": {
|
49
|
+
"openai": "gpt-4o-mini",
|
50
|
+
"openrouter": "openai/gpt-4o-mini"
|
51
|
+
},
|
52
|
+
"gpt-4-turbo": {
|
53
|
+
"openai": "gpt-4-turbo",
|
54
|
+
"openrouter": "openai/gpt-4-turbo"
|
55
|
+
},
|
56
|
+
"gemini-1.5-flash": {
|
57
|
+
"gemini": "gemini-1.5-flash",
|
58
|
+
"openrouter": "google/gemini-flash-1.5"
|
59
|
+
},
|
60
|
+
"gemini-1.5-flash-8b": {
|
61
|
+
"gemini": "gemini-1.5-flash-8b",
|
62
|
+
"openrouter": "google/gemini-flash-1.5-8b"
|
63
|
+
},
|
64
|
+
"gemini-1.5-pro": {
|
65
|
+
"gemini": "gemini-1.5-pro",
|
66
|
+
"openrouter": "google/gemini-pro-1.5"
|
67
|
+
},
|
68
|
+
"gemini-2.0-flash": {
|
69
|
+
"gemini": "gemini-2.0-flash",
|
70
|
+
"openrouter": "google/gemini-2.0-flash-001"
|
71
|
+
},
|
72
|
+
"o1": {
|
73
|
+
"openai": "o1",
|
74
|
+
"openrouter": "openai/o1"
|
75
|
+
},
|
76
|
+
"o3-mini": {
|
77
|
+
"openai": "o3-mini",
|
78
|
+
"openrouter": "openai/o3-mini"
|
37
79
|
}
|
38
80
|
}
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# A module for handling attachments and file operations in RubyLLM.
|
5
|
+
module Attachments
|
6
|
+
# Base class for attachments
|
7
|
+
class Base
|
8
|
+
attr_reader :source, :filename
|
9
|
+
|
10
|
+
def initialize(source, filename: nil)
|
11
|
+
@source = source
|
12
|
+
@filename = filename ||
|
13
|
+
(@source.respond_to?(:original_filename) && @source.original_filename) ||
|
14
|
+
(@source.respond_to?(:path) && File.basename(@source.path)) ||
|
15
|
+
(@source.is_a?(String) && File.basename(@source.split('?').first)) || # Basic URL basename
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def url?
|
20
|
+
@source.is_a?(String) && @source.match?(%r{^https?://})
|
21
|
+
end
|
22
|
+
|
23
|
+
def file_path?
|
24
|
+
@source.is_a?(String) && !url?
|
25
|
+
end
|
26
|
+
|
27
|
+
def io_like?
|
28
|
+
@source.respond_to?(:read) && !file_path?
|
29
|
+
end
|
30
|
+
|
31
|
+
def content
|
32
|
+
return @content if defined?(@content) && !@content.nil?
|
33
|
+
|
34
|
+
if url?
|
35
|
+
fetch_content
|
36
|
+
elsif file_path?
|
37
|
+
load_content_from_path
|
38
|
+
elsif io_like?
|
39
|
+
load_content_from_io
|
40
|
+
else
|
41
|
+
RubyLLM.logger.warn "Attachment source is neither a String nor an IO-like object: #{@source}"
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
@content
|
46
|
+
end
|
47
|
+
|
48
|
+
def type
|
49
|
+
self.class.name.demodulize.downcase
|
50
|
+
end
|
51
|
+
|
52
|
+
def encoded
|
53
|
+
Base64.strict_encode64(content)
|
54
|
+
end
|
55
|
+
|
56
|
+
def mime_type
|
57
|
+
RubyLLM::MimeTypes.detect_from_path(@filename)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def fetch_content
|
63
|
+
RubyLLM.logger.debug("Fetching content from URL: #{@source}")
|
64
|
+
response = Faraday.get(@source)
|
65
|
+
@content = response.body if response.success?
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_content_from_path
|
69
|
+
@content = File.read(File.expand_path(@source))
|
70
|
+
end
|
71
|
+
|
72
|
+
def load_content_from_io
|
73
|
+
@source.rewind if source.respond_to? :rewind
|
74
|
+
@content = @source.read
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/ruby_llm/chat.rb
CHANGED
@@ -8,18 +8,20 @@ module RubyLLM
|
|
8
8
|
# chat = RubyLLM.chat
|
9
9
|
# chat.ask "What's the best way to learn Ruby?"
|
10
10
|
# chat.ask "Can you elaborate on that?"
|
11
|
-
class Chat
|
11
|
+
class Chat
|
12
12
|
include Enumerable
|
13
13
|
|
14
14
|
attr_reader :model, :messages, :tools
|
15
15
|
|
16
|
-
def initialize(model: nil, provider: nil, assume_model_exists: false
|
16
|
+
def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
|
17
17
|
if assume_model_exists && !provider
|
18
18
|
raise ArgumentError, 'Provider must be specified if assume_model_exists is true'
|
19
19
|
end
|
20
20
|
|
21
|
-
|
21
|
+
config = context&.config || RubyLLM.config
|
22
|
+
model_id = model || config.default_model
|
22
23
|
with_model(model_id, provider: provider, assume_exists: assume_model_exists)
|
24
|
+
@connection = context ? context.connection_for(@provider) : @provider.connection(config)
|
23
25
|
@temperature = 0.7
|
24
26
|
@messages = []
|
25
27
|
@tools = {}
|
@@ -29,9 +31,9 @@ module RubyLLM
|
|
29
31
|
}
|
30
32
|
end
|
31
33
|
|
32
|
-
def ask(message = nil, with:
|
34
|
+
def ask(message = nil, with: nil, &)
|
33
35
|
add_message role: :user, content: Content.new(message, with)
|
34
|
-
complete(&
|
36
|
+
complete(&)
|
35
37
|
end
|
36
38
|
|
37
39
|
alias say ask
|
@@ -44,7 +46,7 @@ module RubyLLM
|
|
44
46
|
end
|
45
47
|
|
46
48
|
def with_tool(tool)
|
47
|
-
unless @model.supports_functions
|
49
|
+
unless @model.supports_functions?
|
48
50
|
raise UnsupportedFunctionsError, "Model #{@model.id} doesn't support function calling"
|
49
51
|
end
|
50
52
|
|
@@ -58,18 +60,8 @@ module RubyLLM
|
|
58
60
|
self
|
59
61
|
end
|
60
62
|
|
61
|
-
def with_model(model_id, provider: nil, assume_exists: false)
|
62
|
-
|
63
|
-
raise ArgumentError, 'Provider must be specified if assume_exists is true' unless provider
|
64
|
-
|
65
|
-
@provider = Provider.providers[provider.to_sym] || raise(Error, "Unknown provider: #{provider.to_sym}")
|
66
|
-
@model = Struct.new(:id, :provider, :supports_functions, :supports_vision).new(model_id, provider, true, true)
|
67
|
-
RubyLLM.logger.warn "Assuming model '#{model_id}' exists for provider '#{provider}'. " \
|
68
|
-
'Capabilities may not be accurately reflected.'
|
69
|
-
else
|
70
|
-
@model = Models.find model_id, provider
|
71
|
-
@provider = Provider.providers[@model.provider.to_sym] || raise(Error, "Unknown provider: #{@model.provider}")
|
72
|
-
end
|
63
|
+
def with_model(model_id, provider: nil, assume_exists: false)
|
64
|
+
@model, @provider = Models.resolve(model_id, provider:, assume_exists:)
|
73
65
|
self
|
74
66
|
end
|
75
67
|
|
@@ -94,7 +86,14 @@ module RubyLLM
|
|
94
86
|
|
95
87
|
def complete(&)
|
96
88
|
@on[:new_message]&.call
|
97
|
-
response = @provider.complete(
|
89
|
+
response = @provider.complete(
|
90
|
+
messages,
|
91
|
+
tools: @tools,
|
92
|
+
temperature: @temperature,
|
93
|
+
model: @model.id,
|
94
|
+
connection: @connection,
|
95
|
+
&
|
96
|
+
)
|
98
97
|
@on[:end_message]&.call(response)
|
99
98
|
|
100
99
|
add_message response
|
@@ -111,6 +110,10 @@ module RubyLLM
|
|
111
110
|
message
|
112
111
|
end
|
113
112
|
|
113
|
+
def reset_messages!
|
114
|
+
@messages.clear
|
115
|
+
end
|
116
|
+
|
114
117
|
private
|
115
118
|
|
116
119
|
def handle_tool_calls(response, &)
|
@@ -13,6 +13,8 @@ module RubyLLM
|
|
13
13
|
# Provider-specific configuration
|
14
14
|
attr_accessor :openai_api_key,
|
15
15
|
:openai_api_base,
|
16
|
+
:openai_organization_id,
|
17
|
+
:openai_project_id,
|
16
18
|
:anthropic_api_key,
|
17
19
|
:gemini_api_key,
|
18
20
|
:deepseek_api_key,
|
@@ -20,6 +22,8 @@ module RubyLLM
|
|
20
22
|
:bedrock_secret_key,
|
21
23
|
:bedrock_region,
|
22
24
|
:bedrock_session_token,
|
25
|
+
:openrouter_api_key,
|
26
|
+
:ollama_api_base,
|
23
27
|
# Default models
|
24
28
|
:default_model,
|
25
29
|
:default_embedding_model,
|
@@ -29,7 +33,10 @@ module RubyLLM
|
|
29
33
|
:max_retries,
|
30
34
|
:retry_interval,
|
31
35
|
:retry_backoff_factor,
|
32
|
-
:retry_interval_randomness
|
36
|
+
:retry_interval_randomness,
|
37
|
+
# Logging configuration
|
38
|
+
:log_file,
|
39
|
+
:log_level
|
33
40
|
|
34
41
|
def initialize
|
35
42
|
# Connection configuration
|
@@ -43,6 +50,28 @@ module RubyLLM
|
|
43
50
|
@default_model = 'gpt-4.1-nano'
|
44
51
|
@default_embedding_model = 'text-embedding-3-small'
|
45
52
|
@default_image_model = 'dall-e-3'
|
53
|
+
|
54
|
+
# Logging configuration
|
55
|
+
@log_file = $stdout
|
56
|
+
@log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
|
57
|
+
end
|
58
|
+
|
59
|
+
def inspect
|
60
|
+
redacted = lambda do |name, value|
|
61
|
+
if name.match?(/_id|_key|_secret|_token$/)
|
62
|
+
value.nil? ? 'nil' : '[FILTERED]'
|
63
|
+
else
|
64
|
+
value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
inspection = instance_variables.map do |ivar|
|
69
|
+
name = ivar.to_s.delete_prefix('@')
|
70
|
+
value = redacted[name, instance_variable_get(ivar)]
|
71
|
+
"#{name}: #{value}"
|
72
|
+
end.join(', ')
|
73
|
+
|
74
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} #{inspection}>"
|
46
75
|
end
|
47
76
|
end
|
48
77
|
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Connection class for managing API connections to various providers.
|
5
|
+
class Connection
|
6
|
+
attr_reader :provider, :connection, :config
|
7
|
+
|
8
|
+
def initialize(provider, config)
|
9
|
+
@provider = provider
|
10
|
+
@config = config
|
11
|
+
|
12
|
+
ensure_configured!
|
13
|
+
@connection ||= Faraday.new(provider.api_base(@config)) do |faraday|
|
14
|
+
setup_timeout(faraday)
|
15
|
+
setup_logging(faraday)
|
16
|
+
setup_retry(faraday)
|
17
|
+
setup_middleware(faraday)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def post(url, payload, &)
|
22
|
+
body = payload.is_a?(Hash) ? JSON.generate(payload, ascii_only: false) : payload
|
23
|
+
@connection.post url, body do |req|
|
24
|
+
req.headers.merge! @provider.headers(@config) if @provider.respond_to?(:headers)
|
25
|
+
yield req if block_given?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def get(url, &)
|
30
|
+
@connection.get url do |req|
|
31
|
+
req.headers.merge! @provider.headers(@config) if @provider.respond_to?(:headers)
|
32
|
+
yield req if block_given?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def setup_timeout(faraday)
|
39
|
+
faraday.options.timeout = @config.request_timeout
|
40
|
+
end
|
41
|
+
|
42
|
+
def setup_logging(faraday)
|
43
|
+
faraday.response :logger, RubyLLM.logger, bodies: true, response: true,
|
44
|
+
errors: true, headers: false, log_level: :debug do |logger|
|
45
|
+
logger.filter(%r{[A-Za-z0-9+/=]{100,}}, 'data":"[BASE64 DATA]"')
|
46
|
+
logger.filter(/[-\d.e,\s]{100,}/, '[EMBEDDINGS ARRAY]')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def setup_retry(faraday)
|
51
|
+
faraday.request :retry, {
|
52
|
+
max: @config.max_retries,
|
53
|
+
interval: @config.retry_interval,
|
54
|
+
interval_randomness: @config.retry_interval_randomness,
|
55
|
+
backoff_factor: @config.retry_backoff_factor,
|
56
|
+
exceptions: retry_exceptions,
|
57
|
+
retry_statuses: [429, 500, 502, 503, 504, 529]
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def setup_middleware(faraday)
|
62
|
+
faraday.request :json
|
63
|
+
faraday.response :json
|
64
|
+
faraday.adapter Faraday.default_adapter
|
65
|
+
faraday.use :llm_errors, provider: @provider
|
66
|
+
end
|
67
|
+
|
68
|
+
def retry_exceptions
|
69
|
+
[
|
70
|
+
Errno::ETIMEDOUT,
|
71
|
+
Timeout::Error,
|
72
|
+
Faraday::TimeoutError,
|
73
|
+
Faraday::ConnectionFailed,
|
74
|
+
Faraday::RetriableResponse,
|
75
|
+
RubyLLM::RateLimitError,
|
76
|
+
RubyLLM::ServerError,
|
77
|
+
RubyLLM::ServiceUnavailableError,
|
78
|
+
RubyLLM::OverloadedError
|
79
|
+
]
|
80
|
+
end
|
81
|
+
|
82
|
+
def ensure_configured!
|
83
|
+
return if @provider.configured?(@config)
|
84
|
+
|
85
|
+
config_block = <<~RUBY
|
86
|
+
RubyLLM.configure do |config|
|
87
|
+
#{@provider.missing_configs(@config).map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
|
88
|
+
end
|
89
|
+
RUBY
|
90
|
+
|
91
|
+
raise ConfigurationError,
|
92
|
+
"#{@provider.slug} provider is not configured. Add this to your initialization:\n\n#{config_block}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/ruby_llm/content.rb
CHANGED
@@ -5,100 +5,79 @@ module RubyLLM
|
|
5
5
|
# Stores data in a standard internal format, letting providers
|
6
6
|
# handle their own formatting needs.
|
7
7
|
class Content
|
8
|
-
|
9
|
-
@parts = []
|
10
|
-
@parts << { type: 'text', text: text } unless text.nil? || text.empty?
|
8
|
+
attr_reader :text, :attachments
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
def initialize(text = nil, attachments = nil)
|
11
|
+
@text = text
|
12
|
+
@attachments = []
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
process_attachments(attachments)
|
15
|
+
raise ArgumentError, 'Text and attachments cannot be both nil' if @text.nil? && @attachments.empty?
|
16
|
+
end
|
19
17
|
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
def add_image(source)
|
19
|
+
@attachments << Attachments::Image.new(source)
|
20
|
+
self
|
23
21
|
end
|
24
22
|
|
25
|
-
def
|
26
|
-
|
23
|
+
def add_audio(source)
|
24
|
+
@attachments << Attachments::Audio.new(source)
|
25
|
+
self
|
26
|
+
end
|
27
27
|
|
28
|
-
|
28
|
+
def add_pdf(source)
|
29
|
+
@attachments << Attachments::PDF.new(source)
|
30
|
+
self
|
29
31
|
end
|
30
32
|
|
31
33
|
def format
|
32
|
-
|
34
|
+
if @text && @attachments.empty?
|
35
|
+
@text
|
36
|
+
else
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
33
40
|
|
34
|
-
|
41
|
+
# For Rails serialization
|
42
|
+
def as_json
|
43
|
+
hash = { text: @text }
|
44
|
+
unless @attachments.empty?
|
45
|
+
hash[:attachments] = @attachments.map do |a|
|
46
|
+
{ type: a.type, source: a.source }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
hash
|
35
50
|
end
|
36
51
|
|
37
52
|
private
|
38
53
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
return { type: 'image', source: { url: source } } if source.start_with?('http')
|
54
|
+
def process_attachments_hash(attachments)
|
55
|
+
return unless attachments.is_a?(Hash)
|
43
56
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
{
|
48
|
-
type: 'image',
|
49
|
-
source: {
|
50
|
-
type: 'base64',
|
51
|
-
media_type: mime_type,
|
52
|
-
data: data
|
53
|
-
}
|
54
|
-
}
|
57
|
+
Array(attachments[:image]).each { |source| add_image(source) }
|
58
|
+
Array(attachments[:audio]).each { |source| add_audio(source) }
|
59
|
+
Array(attachments[:pdf]).each { |source| add_pdf(source) }
|
55
60
|
end
|
56
61
|
|
57
|
-
def
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
}
|
68
|
-
}
|
69
|
-
end
|
70
|
-
|
71
|
-
def attach_pdf(source)
|
72
|
-
source = File.expand_path(source) unless source.start_with?('http')
|
73
|
-
|
74
|
-
pdf_data = {
|
75
|
-
type: 'pdf',
|
76
|
-
source: source
|
77
|
-
}
|
78
|
-
|
79
|
-
# For local files, validate they exist
|
80
|
-
unless source.start_with?('http')
|
81
|
-
raise Error, "PDF file not found: #{source}" unless File.exist?(source)
|
82
|
-
|
83
|
-
# Preload file content for providers that need it
|
84
|
-
pdf_data[:content] = File.read(source)
|
62
|
+
def process_attachments_array_or_string(attachments)
|
63
|
+
Array(attachments).each do |file|
|
64
|
+
mime_type = RubyLLM::MimeTypes.detect_from_path(file.to_s)
|
65
|
+
if RubyLLM::MimeTypes.image?(mime_type)
|
66
|
+
add_image file
|
67
|
+
elsif RubyLLM::MimeTypes.audio?(mime_type)
|
68
|
+
add_audio file
|
69
|
+
else
|
70
|
+
add_pdf file # Default to PDF for unknown types for now
|
71
|
+
end
|
85
72
|
end
|
86
|
-
|
87
|
-
pdf_data
|
88
73
|
end
|
89
74
|
|
90
|
-
def
|
91
|
-
if
|
92
|
-
|
93
|
-
Base64.strict_encode64(response.body)
|
75
|
+
def process_attachments(attachments)
|
76
|
+
if attachments.is_a?(Hash)
|
77
|
+
process_attachments_hash attachments
|
94
78
|
else
|
95
|
-
|
79
|
+
process_attachments_array_or_string attachments
|
96
80
|
end
|
97
81
|
end
|
98
|
-
|
99
|
-
def mime_type_for(path)
|
100
|
-
ext = File.extname(path).delete('.')
|
101
|
-
"image/#{ext}"
|
102
|
-
end
|
103
82
|
end
|
104
83
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Holds per-call configs
|
5
|
+
class Context
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@config = config
|
10
|
+
@connections = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def chat(*args, **kwargs, &)
|
14
|
+
Chat.new(*args, **kwargs, context: self, &)
|
15
|
+
end
|
16
|
+
|
17
|
+
def embed(*args, **kwargs, &)
|
18
|
+
Embedding.embed(*args, **kwargs, context: self, &)
|
19
|
+
end
|
20
|
+
|
21
|
+
def paint(*args, **kwargs, &)
|
22
|
+
Image.paint(*args, **kwargs, context: self, &)
|
23
|
+
end
|
24
|
+
|
25
|
+
def connection_for(provider_module)
|
26
|
+
slug = provider_module.slug.to_sym
|
27
|
+
@connections[slug] ||= Connection.new(provider_module, @config)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/ruby_llm/embedding.rb
CHANGED
@@ -12,12 +12,20 @@ module RubyLLM
|
|
12
12
|
@input_tokens = input_tokens
|
13
13
|
end
|
14
14
|
|
15
|
-
def self.embed(text,
|
16
|
-
|
17
|
-
|
15
|
+
def self.embed(text, # rubocop:disable Metrics/ParameterLists
|
16
|
+
model: nil,
|
17
|
+
provider: nil,
|
18
|
+
assume_model_exists: false,
|
19
|
+
context: nil,
|
20
|
+
dimensions: nil)
|
21
|
+
config = context&.config || RubyLLM.config
|
22
|
+
model ||= config.default_embedding_model
|
23
|
+
model, provider = Models.resolve(model, provider: provider, assume_exists: assume_model_exists)
|
24
|
+
model_id = model.id
|
18
25
|
|
19
|
-
provider = Provider.for(model_id)
|
20
|
-
|
26
|
+
provider = Provider.for(model_id) if provider.nil?
|
27
|
+
connection = context ? context.connection_for(provider) : provider.connection(config)
|
28
|
+
provider.embed(text, model: model_id, connection:, dimensions:)
|
21
29
|
end
|
22
30
|
end
|
23
31
|
end
|
data/lib/ruby_llm/error.rb
CHANGED
@@ -50,7 +50,7 @@ module RubyLLM
|
|
50
50
|
end
|
51
51
|
|
52
52
|
class << self
|
53
|
-
def parse_error(provider:, response:) # rubocop:disable Metrics/
|
53
|
+
def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
|
54
54
|
message = provider&.parse_error(response)
|
55
55
|
|
56
56
|
case response.status
|
data/lib/ruby_llm/image.rb
CHANGED
@@ -36,12 +36,20 @@ module RubyLLM
|
|
36
36
|
path
|
37
37
|
end
|
38
38
|
|
39
|
-
def self.paint(prompt,
|
40
|
-
|
41
|
-
|
39
|
+
def self.paint(prompt, # rubocop:disable Metrics/ParameterLists
|
40
|
+
model: nil,
|
41
|
+
provider: nil,
|
42
|
+
assume_model_exists: false,
|
43
|
+
size: '1024x1024',
|
44
|
+
context: nil)
|
45
|
+
config = context&.config || RubyLLM.config
|
46
|
+
model ||= config.default_image_model
|
47
|
+
model, provider = Models.resolve(model, provider: provider, assume_exists: assume_model_exists)
|
48
|
+
model_id = model.id
|
42
49
|
|
43
|
-
provider = Provider.for(model_id)
|
44
|
-
|
50
|
+
provider = Provider.for(model_id) if provider.nil?
|
51
|
+
connection = context ? context.connection_for(provider) : provider.connection(config)
|
52
|
+
provider.paint(prompt, model: model_id, size:, connection:)
|
45
53
|
end
|
46
54
|
end
|
47
55
|
end
|