ruby_llm-voyage 0.1.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.
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm'
4
+ require 'ruby_llm/voyage/version'
5
+ require 'ruby_llm/voyage/results'
6
+ require 'ruby_llm/voyage/vector_decoder'
7
+ require 'ruby_llm/providers/voyage'
8
+
9
+ # RubyLLM framework namespace.
10
+ module RubyLLM
11
+ # Voyage retrieval APIs for RubyLLM: embeddings, reranking, contextualized
12
+ # chunk embeddings, and files.
13
+ module Voyage
14
+ # Generates embeddings with a Voyage text embedding model.
15
+ #
16
+ # This facade exposes Voyage-specific options that are not first-class
17
+ # keywords on +RubyLLM.embed+ in RubyLLM 1.x. It returns the same
18
+ # {RubyLLM::Embedding} result type and emits the same instrumentation event.
19
+ #
20
+ # @param text [String, Array<String>] one input or a batch of inputs
21
+ # @param model [String, nil] Voyage model ID, such as +voyage-4-lite+;
22
+ # defaults to +config.default_embedding_model+
23
+ # @param dimensions [Integer, nil] requested output dimensions; defaults
24
+ # to +config.voyage_default_output_dimension+
25
+ # @param input_type [String, Symbol, nil] +query+, +document+, or +nil+
26
+ # @param truncation [Boolean, nil] whether Voyage may truncate long inputs
27
+ # @param output_dtype [String, Symbol, nil] +float+, +int8+, +uint8+,
28
+ # +binary+, +ubinary+, or +nil+
29
+ # @param encoding_format [String, Symbol, nil] +base64+ or +nil+
30
+ # @param provider_options [Hash] trusted fields merged into the Voyage
31
+ # request after named options
32
+ # @param context [RubyLLM::Context, nil] isolated RubyLLM configuration
33
+ # @return [RubyLLM::Embedding] vectors, model ID, and input-token usage
34
+ # @raise [RubyLLM::ConfigurationError] when the Voyage API key is missing
35
+ # @raise [RubyLLM::Error] when Voyage returns an unsuccessful response
36
+ #
37
+ # @example Embed a retrieval query
38
+ # embedding = RubyLLM::Voyage.embed(
39
+ # "How do I configure SSO?",
40
+ # model: "voyage-4-lite",
41
+ # input_type: :query,
42
+ # dimensions: 512
43
+ # )
44
+ # embedding.vectors #=> Array<Float>
45
+ def self.embed(text, model: nil, dimensions: nil, input_type: UNSET, truncation: UNSET, output_dtype: UNSET,
46
+ encoding_format: UNSET, provider_options: {}, context: nil)
47
+ validate_input!(:text, text)
48
+ config = context&.config || RubyLLM.config
49
+ model ||= config.default_embedding_model
50
+ provider = Providers::Voyage.new(config)
51
+ payload = {
52
+ provider: provider.slug,
53
+ model: model,
54
+ input: text,
55
+ dimensions: dimensions,
56
+ input_type: input_type,
57
+ truncation: truncation,
58
+ output_dtype: output_dtype,
59
+ encoding_format: encoding_format
60
+ }.reject { |_key, value| value.equal?(UNSET) }
61
+
62
+ RubyLLM.instrument('embedding.ruby_llm', payload, config:) do |event|
63
+ result = provider.embed(
64
+ text,
65
+ model:,
66
+ dimensions:,
67
+ input_type:,
68
+ truncation:,
69
+ output_dtype:,
70
+ encoding_format:,
71
+ provider_options:
72
+ )
73
+ event[:result] = result
74
+ event[:response_model] = result.model
75
+ event[:input_tokens] = result.input_tokens
76
+ result
77
+ end
78
+ end
79
+
80
+ # Embeds search text for the query side of retrieval.
81
+ #
82
+ # Equivalent to {.embed} with +input_type: 'query'+. Pair with
83
+ # {.embed_documents} so Voyage optimizes both sides of retrieval, and
84
+ # configure +default_embedding_model+ and +voyage_default_output_dimension+ once
85
+ # to keep call sites small.
86
+ #
87
+ # @param text [String, Array<String>] one search input or a batch
88
+ # @param options [Hash] the same options as {.embed}, except +input_type+
89
+ # @return [RubyLLM::Embedding]
90
+ # @raise [ArgumentError] when +input_type+ is passed
91
+ #
92
+ # @example
93
+ # RubyLLM::Voyage.embed_query('How do I configure SSO?').vectors
94
+ def self.embed_query(text, **options)
95
+ embed_side(text, 'query', __method__, options)
96
+ end
97
+
98
+ # Embeds content for the document side of retrieval.
99
+ #
100
+ # Equivalent to {.embed} with +input_type: 'document'+. Accepts one text
101
+ # or a batch of up to 1,000.
102
+ #
103
+ # @param texts [String, Array<String>] content to index
104
+ # @param options [Hash] the same options as {.embed}, except +input_type+
105
+ # @return [RubyLLM::Embedding]
106
+ # @raise [ArgumentError] when +input_type+ is passed
107
+ #
108
+ # @example
109
+ # RubyLLM::Voyage.embed_documents(articles.map(&:content)).vectors
110
+ def self.embed_documents(texts, **options)
111
+ embed_side(texts, 'document', __method__, options)
112
+ end
113
+
114
+ # Reranks documents for a query with a Voyage reranker.
115
+ # @param query [String]
116
+ # @param documents [Array<String>]
117
+ # @param model [String]
118
+ # @param top_k [Integer, nil]
119
+ # @param return_documents [Boolean]
120
+ # @param truncation [Boolean]
121
+ # @param provider_options [Hash]
122
+ # @param context [RubyLLM::Context, nil]
123
+ # @return [RubyLLM::Voyage::Reranking]
124
+ def self.rerank(query:, documents:, model: 'rerank-2.5-lite', top_k: nil, return_documents: UNSET,
125
+ truncation: UNSET, provider_options: {}, context: nil)
126
+ validate_input!(:query, query)
127
+ validate_input!(:documents, documents)
128
+ config = context&.config || RubyLLM.config
129
+ provider = Providers::Voyage.new(config)
130
+ payload = { model: model, query: query, document_count: documents.length }
131
+ RubyLLM.instrument('rerank.voyage', payload, config:) do |event|
132
+ result = provider.rerank(
133
+ query:, documents:, model:, top_k:, return_documents:, truncation:, provider_options:
134
+ )
135
+ event[:result] = result
136
+ event[:response_model] = result.model
137
+ event[:input_tokens] = result.input_tokens
138
+ result
139
+ end
140
+ end
141
+
142
+ # Generates context-aware embeddings for grouped document chunks.
143
+ # @param inputs [Array<String>, Array<Array<String>>]
144
+ # @param model [String]
145
+ # @param input_type [String, Symbol, nil]
146
+ # @param dimensions [Integer, nil] defaults to
147
+ # +config.voyage_default_output_dimension+
148
+ # @param output_dtype [String, Symbol, nil]
149
+ # @param encoding_format [String, Symbol, nil]
150
+ # @param auto_chunk [Boolean, nil]
151
+ # @param chunk_size [Integer, nil]
152
+ # @param chunk_overlap [Integer, nil]
153
+ # @param provider_options [Hash]
154
+ # @param context [RubyLLM::Context, nil]
155
+ # @return [RubyLLM::Voyage::ContextualizedEmbedding]
156
+ def self.contextualized_embed(inputs, model: 'voyage-context-4', input_type: UNSET, dimensions: nil,
157
+ output_dtype: UNSET, encoding_format: UNSET, auto_chunk: nil, chunk_size: nil,
158
+ chunk_overlap: nil, provider_options: {}, context: nil)
159
+ validate_input!(:inputs, inputs)
160
+ config = context&.config || RubyLLM.config
161
+ provider = Providers::Voyage.new(config)
162
+ payload = { model: model, input_count: inputs.length, input_type: input_type, auto_chunk: auto_chunk }
163
+ .reject { |_key, value| value.equal?(UNSET) }
164
+ RubyLLM.instrument('contextualized_embedding.voyage', payload, config:) do |event|
165
+ result = provider.contextualized_embed(
166
+ inputs, model:, input_type:, dimensions:, output_dtype:, encoding_format:, auto_chunk:, chunk_size:,
167
+ chunk_overlap:, provider_options:
168
+ )
169
+ event[:result] = result
170
+ event[:response_model] = result.model
171
+ event[:input_tokens] = result.input_tokens
172
+ result
173
+ end
174
+ end
175
+
176
+ # Uploads a JSONL file to Voyage.
177
+ # @param path [String, Pathname]
178
+ # @param purpose [String]
179
+ # @param context [RubyLLM::Context, nil]
180
+ # @return [RubyLLM::Voyage::VoyageFile]
181
+ def self.upload_file(path, purpose: 'batch', context: nil)
182
+ provider_for(context).upload_file(path, purpose:)
183
+ end
184
+
185
+ # Retrieves metadata for one Voyage file.
186
+ # @param file_id [String]
187
+ # @param context [RubyLLM::Context, nil]
188
+ # @return [RubyLLM::Voyage::VoyageFile]
189
+ def self.retrieve_file(file_id, context: nil)
190
+ provider_for(context).retrieve_file(file_id)
191
+ end
192
+
193
+ # Lists Voyage files with cursor pagination.
194
+ # @param purpose [String, nil]
195
+ # @param limit [Integer, nil]
196
+ # @param order [String, Symbol, nil]
197
+ # @param after [String, nil]
198
+ # @param context [RubyLLM::Context, nil]
199
+ # @return [RubyLLM::Voyage::Page]
200
+ def self.list_files(purpose: nil, limit: nil, order: nil, after: nil, context: nil)
201
+ provider_for(context).list_files(purpose:, limit:, order: order&.to_s, after:)
202
+ end
203
+
204
+ # Downloads a Voyage file's raw contents.
205
+ # @param file_id [String]
206
+ # @param context [RubyLLM::Context, nil]
207
+ # @return [String]
208
+ def self.file_content(file_id, context: nil)
209
+ provider_for(context).file_content(file_id)
210
+ end
211
+
212
+ # Deletes one or more Voyage files atomically.
213
+ # @param file_ids [Array<String>]
214
+ # @param context [RubyLLM::Context, nil]
215
+ # @return [RubyLLM::Voyage::FileDeletion]
216
+ def self.delete_files(file_ids, context: nil)
217
+ provider_for(context).delete_files(file_ids)
218
+ end
219
+
220
+ class << self
221
+ # Alias of {.retrieve_file}, matching the +find_file+ naming used by
222
+ # RubyLLM's provider file API.
223
+ alias find_file retrieve_file
224
+
225
+ # Alias of {.file_content}, matching the +download_file+ naming used by
226
+ # RubyLLM's provider file API.
227
+ alias download_file file_content
228
+ end
229
+
230
+ # Builds a configured Voyage provider for resource operations.
231
+ # @api private
232
+ def self.provider_for(context)
233
+ Providers::Voyage.new(context&.config || RubyLLM.config)
234
+ end
235
+ private_class_method :provider_for
236
+
237
+ # Fails fast on inputs Voyage would reject, before any network request.
238
+ # @api private
239
+ def self.validate_input!(name, value)
240
+ raise ArgumentError, "#{name} cannot be nil" if value.nil?
241
+ raise ArgumentError, "#{name} cannot be empty" if value.respond_to?(:empty?) && value.empty?
242
+ end
243
+ private_class_method :validate_input!
244
+
245
+ # Embeds one retrieval side with a fixed input type.
246
+ # @api private
247
+ def self.embed_side(text, input_type, method_name, options)
248
+ if options.key?(:input_type)
249
+ raise ArgumentError, "#{method_name} sets input_type: '#{input_type}'; use embed to pass another input_type"
250
+ end
251
+
252
+ embed(text, input_type: input_type, **options)
253
+ end
254
+ private_class_method :embed_side
255
+ end
256
+ end
257
+
258
+ RubyLLM::Provider.register :voyage, RubyLLM::Providers::Voyage
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm'
4
+ require 'ruby_llm/voyage'
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_llm-voyage
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Insley
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ruby_llm
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.16'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '1.16'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
32
+ description: A RubyLLM extension for Voyage embeddings, reranking, contextualized
33
+ chunks, and files.
34
+ email:
35
+ - dan@insley.ca
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files:
39
+ - CHANGELOG.md
40
+ - LICENSE.txt
41
+ - README.md
42
+ files:
43
+ - CHANGELOG.md
44
+ - CONTRIBUTING.md
45
+ - LICENSE.txt
46
+ - README.md
47
+ - SECURITY.md
48
+ - docs/RELEASING.md
49
+ - lib/ruby_llm-voyage.rb
50
+ - lib/ruby_llm/providers/voyage.rb
51
+ - lib/ruby_llm/providers/voyage/contextualized_embeddings.rb
52
+ - lib/ruby_llm/providers/voyage/embeddings.rb
53
+ - lib/ruby_llm/providers/voyage/files.rb
54
+ - lib/ruby_llm/providers/voyage/reranking.rb
55
+ - lib/ruby_llm/voyage.rb
56
+ - lib/ruby_llm/voyage/results.rb
57
+ - lib/ruby_llm/voyage/vector_decoder.rb
58
+ - lib/ruby_llm/voyage/version.rb
59
+ homepage: https://github.com/dinsley/ruby_llm-voyage
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ homepage_uri: https://github.com/dinsley/ruby_llm-voyage
64
+ source_code_uri: https://github.com/dinsley/ruby_llm-voyage/tree/v0.1.0
65
+ changelog_uri: https://github.com/dinsley/ruby_llm-voyage/blob/main/CHANGELOG.md
66
+ bug_tracker_uri: https://github.com/dinsley/ruby_llm-voyage/issues
67
+ documentation_uri: https://www.rubydoc.info/gems/ruby_llm-voyage/0.1.0
68
+ allowed_push_host: https://rubygems.org
69
+ rubygems_mfa_required: 'true'
70
+ rdoc_options:
71
+ - "--main"
72
+ - README.md
73
+ - "--markup"
74
+ - markdown
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 3.1.3
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 4.0.10
89
+ specification_version: 4
90
+ summary: Voyage retrieval APIs for RubyLLM
91
+ test_files: []