prescient 0.2.0 → 0.4.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +0 -2
- data/.yardopts +3 -6
- data/CHANGELOG.md +53 -4
- data/INTEGRATION_GUIDE.md +27 -29
- data/LICENSE.txt +1 -1
- data/README.md +217 -74
- data/Rakefile +82 -7
- data/Steepfile +17 -0
- data/VECTOR_SEARCH_GUIDE.md +32 -8
- data/docker-compose.yml +3 -5
- data/examples/README.md +45 -0
- data/examples/basic_usage.rb +2 -2
- data/examples/custom_contexts.rb +6 -6
- data/examples/custom_prompts.rb +5 -5
- data/examples/vector_search.rb +2 -2
- data/exe/prescient +7 -0
- data/lib/prescient/base.rb +81 -26
- data/lib/prescient/cli.rb +287 -0
- data/lib/prescient/client.rb +85 -39
- data/lib/prescient/errors.rb +51 -0
- data/lib/prescient/pgvector.rb +194 -0
- data/lib/prescient/provider/anthropic.rb +57 -56
- data/lib/prescient/provider/huggingface.rb +79 -78
- data/lib/prescient/provider/ollama.rb +48 -37
- data/lib/prescient/provider/openai.rb +43 -35
- data/lib/prescient/version.rb +2 -1
- data/lib/prescient.rb +86 -63
- data/scripts/setup-ollama-models.sh +2 -2
- data/sig/prescient.rbs +233 -1
- metadata +25 -218
- data/CHANGELOG.pdf +0 -0
- data/prescient.gemspec +0 -53
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
# Command-line interface for common Prescient operations.
|
|
7
|
+
class Prescient::CLI
|
|
8
|
+
# Supported output formats.
|
|
9
|
+
# @return [Array<String>] Output format names
|
|
10
|
+
FORMATS = ['text', 'json'].freeze
|
|
11
|
+
|
|
12
|
+
# Raised when command-line arguments are invalid or incomplete.
|
|
13
|
+
class UsageError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Run the CLI and return a process exit status.
|
|
16
|
+
#
|
|
17
|
+
# @param arguments [Array<String>] Command-line arguments
|
|
18
|
+
# @param input [IO] Input stream used for stdin prompts
|
|
19
|
+
# @param output [IO] Output stream for command results
|
|
20
|
+
# @param errors [IO] Output stream for diagnostics
|
|
21
|
+
# @return [Integer] Process exit status
|
|
22
|
+
def self.run(arguments, input: $stdin, output: $stdout, errors: $stderr)
|
|
23
|
+
new(arguments, input:, output:, errors:).run
|
|
24
|
+
rescue UsageError, OptionParser::ParseError => e
|
|
25
|
+
errors.puts "prescient: #{e.message}"
|
|
26
|
+
2
|
|
27
|
+
rescue Prescient::Error => e
|
|
28
|
+
errors.puts "prescient: #{e.message}"
|
|
29
|
+
1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Initialize a CLI runner with injectable streams.
|
|
33
|
+
#
|
|
34
|
+
# @param arguments [Array<String>] Command-line arguments
|
|
35
|
+
# @param input [IO] Input stream used for stdin prompts
|
|
36
|
+
# @param output [IO] Output stream for command results
|
|
37
|
+
# @param errors [IO] Output stream for diagnostics
|
|
38
|
+
def initialize(arguments, input:, output:, errors:)
|
|
39
|
+
@arguments = arguments.dup
|
|
40
|
+
@input = input
|
|
41
|
+
@output = output
|
|
42
|
+
@errors = errors
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run
|
|
46
|
+
command = @arguments.shift
|
|
47
|
+
return print_help(2) unless command
|
|
48
|
+
|
|
49
|
+
case command
|
|
50
|
+
when 'providers' then providers
|
|
51
|
+
when 'health' then health
|
|
52
|
+
when 'generate' then generate
|
|
53
|
+
when 'embed' then embed
|
|
54
|
+
when 'config' then config
|
|
55
|
+
when 'help', '--help', '-h' then print_help(0)
|
|
56
|
+
else
|
|
57
|
+
raise UsageError, "unknown command #{command.inspect}; run 'prescient help'"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def providers
|
|
64
|
+
options = parse_options('List configured providers')
|
|
65
|
+
return options if options.is_a?(Integer)
|
|
66
|
+
|
|
67
|
+
provider_list = Prescient.configuration.providers.map { |name, registration|
|
|
68
|
+
{ name: name.to_s, class: registration[:class].name }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if options[:format] == 'json'
|
|
72
|
+
print_json(providers: provider_list)
|
|
73
|
+
else
|
|
74
|
+
provider_list.each { |provider| @output.puts "#{provider[:name]}\t#{provider[:class]}" }
|
|
75
|
+
end
|
|
76
|
+
0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def health
|
|
80
|
+
options = parse_options('Check provider health')
|
|
81
|
+
return options if options.is_a?(Integer)
|
|
82
|
+
|
|
83
|
+
names = options[:provider] ? [options[:provider].to_sym] : Prescient.configuration.providers.keys
|
|
84
|
+
raise UsageError, 'no providers are configured' if names.empty?
|
|
85
|
+
|
|
86
|
+
results = names.to_h { |name| [name.to_s, Prescient.health_check(provider: name)] }
|
|
87
|
+
output_health(results, options[:format])
|
|
88
|
+
results.values.all? { |result| result[:reachable] != false } ? 0 : 1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def generate
|
|
92
|
+
options = parse_options('Generate a text response', fallback: true)
|
|
93
|
+
return options if options.is_a?(Integer)
|
|
94
|
+
|
|
95
|
+
prompt = read_text(options[:arguments], 'prompt')
|
|
96
|
+
client = client_for(options)
|
|
97
|
+
response = client.generate_response(prompt, **model_options(options))
|
|
98
|
+
|
|
99
|
+
options[:format] == 'json' ? print_json(response) : @output.puts(response[:response])
|
|
100
|
+
0
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def embed
|
|
104
|
+
options = parse_options('Generate an embedding', fallback: true)
|
|
105
|
+
return options if options.is_a?(Integer)
|
|
106
|
+
|
|
107
|
+
text = read_text(options[:arguments], 'text')
|
|
108
|
+
client = client_for(options)
|
|
109
|
+
embedding = client.generate_embedding(text, **model_options(options))
|
|
110
|
+
|
|
111
|
+
if options[:format] == 'json'
|
|
112
|
+
print_json(embedding: embedding, dimensions: embedding.length, provider: client.provider_name.to_s)
|
|
113
|
+
else
|
|
114
|
+
@output.puts JSON.generate(embedding)
|
|
115
|
+
end
|
|
116
|
+
0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def config
|
|
120
|
+
subcommand = @arguments.shift
|
|
121
|
+
raise UsageError, "unknown config command #{subcommand.inspect}" unless subcommand == 'validate'
|
|
122
|
+
|
|
123
|
+
options = parse_options('Validate the current configuration')
|
|
124
|
+
return options if options.is_a?(Integer)
|
|
125
|
+
|
|
126
|
+
validate_configuration
|
|
127
|
+
if options[:format] == 'json'
|
|
128
|
+
print_json(valid: true, providers: Prescient.configuration.providers.keys.map(&:to_s))
|
|
129
|
+
else
|
|
130
|
+
@output.puts 'configuration valid'
|
|
131
|
+
end
|
|
132
|
+
0
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def validate_configuration
|
|
136
|
+
configuration = Prescient.configuration
|
|
137
|
+
unless configuration.provider(configuration.default_provider)
|
|
138
|
+
raise Prescient::Error, 'default provider is not configured'
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
configuration.providers.each_key { |name| configuration.provider(name) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def parse_options(description, fallback: false)
|
|
145
|
+
options = { format: 'text', fallback: fallback }
|
|
146
|
+
parser = OptionParser.new do |parser|
|
|
147
|
+
parser.banner = "Usage: prescient #{@arguments.first || 'command'} [options]"
|
|
148
|
+
parser.separator description
|
|
149
|
+
add_common_options(parser, options)
|
|
150
|
+
parser.on('--no-fallback', 'Disable provider fallback') { options[:fallback] = false } if fallback
|
|
151
|
+
parser.on('-h', '--help', 'Show command help') do
|
|
152
|
+
@output.puts parser
|
|
153
|
+
throw :help_shown, 0
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
result = catch(:help_shown) { parse_arguments(parser) }
|
|
158
|
+
return result unless result.nil?
|
|
159
|
+
|
|
160
|
+
options[:arguments] = @arguments
|
|
161
|
+
options
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def add_common_options(parser, options)
|
|
165
|
+
parser.on('--format FORMAT', FORMATS, "Output format (#{FORMATS.join(', ')})") do |value|
|
|
166
|
+
options[:format] = value
|
|
167
|
+
end
|
|
168
|
+
parser.on('--provider NAME', 'Use a specific provider') do |value|
|
|
169
|
+
options[:provider] = value
|
|
170
|
+
end
|
|
171
|
+
add_model_options(parser, options)
|
|
172
|
+
add_credential_options(parser, options)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Parse command arguments and return nil when parsing completes.
|
|
176
|
+
#
|
|
177
|
+
# @param parser [OptionParser] Configured command option parser
|
|
178
|
+
# @return [nil]
|
|
179
|
+
def parse_arguments(parser)
|
|
180
|
+
parser.parse!(@arguments)
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def model_options(options)
|
|
185
|
+
options[:model] ? { model: options[:model] } : {}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def client_for(options)
|
|
189
|
+
validate_override_options(options)
|
|
190
|
+
Prescient.client(
|
|
191
|
+
options[:provider]&.to_sym,
|
|
192
|
+
enable_fallback: options[:fallback],
|
|
193
|
+
provider_options: provider_options(options),
|
|
194
|
+
)
|
|
195
|
+
rescue KeyError => e
|
|
196
|
+
raise UsageError, "environment variable not set: #{e.key}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def add_model_options(parser, options)
|
|
200
|
+
parser.on('--model NAME', 'Override the configured model') do |value|
|
|
201
|
+
options[:model] = value
|
|
202
|
+
end
|
|
203
|
+
parser.on('--embedding-model NAME', 'Override the embedding model') do |value|
|
|
204
|
+
options[:embedding_model] = value
|
|
205
|
+
end
|
|
206
|
+
parser.on('--chat-model NAME', 'Override the chat model') do |value|
|
|
207
|
+
options[:chat_model] = value
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def add_credential_options(parser, options)
|
|
212
|
+
parser.on('--api-key KEY', 'Use an API key for this operation') do |value|
|
|
213
|
+
options[:api_key] = value
|
|
214
|
+
end
|
|
215
|
+
parser.on('--api-key-env NAME', 'Read the API key from this environment variable') do |value|
|
|
216
|
+
options[:api_key_env] = value
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def validate_override_options(options)
|
|
221
|
+
if options[:model] && (options[:embedding_model] || options[:chat_model])
|
|
222
|
+
raise UsageError, '--model cannot be combined with --embedding-model or --chat-model'
|
|
223
|
+
end
|
|
224
|
+
return unless options[:api_key] && options[:api_key_env]
|
|
225
|
+
|
|
226
|
+
raise UsageError, '--api-key cannot be combined with --api-key-env'
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def provider_options(options)
|
|
230
|
+
{
|
|
231
|
+
api_key: api_key_override(options),
|
|
232
|
+
embedding_model: options[:embedding_model],
|
|
233
|
+
chat_model: options[:chat_model],
|
|
234
|
+
}.compact
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def api_key_override(options)
|
|
238
|
+
return options[:api_key] if options[:api_key]
|
|
239
|
+
return ENV.fetch(options[:api_key_env]) if options[:api_key_env]
|
|
240
|
+
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def output_health(results, format)
|
|
245
|
+
if format == 'json'
|
|
246
|
+
print_json(results)
|
|
247
|
+
else
|
|
248
|
+
results.each do |name, result|
|
|
249
|
+
@output.puts '%<name>-12s %<status>s' % { name: name, status: result[:status] || 'unknown' }
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def read_text(arguments, label)
|
|
255
|
+
return arguments.join(' ') unless arguments.empty?
|
|
256
|
+
return @input.read unless @input.tty?
|
|
257
|
+
|
|
258
|
+
raise UsageError, "missing #{label}; provide it as an argument or through stdin"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def print_json(value)
|
|
262
|
+
@output.puts JSON.generate(value)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def print_help(status)
|
|
266
|
+
@output.puts <<~HELP
|
|
267
|
+
Usage: prescient COMMAND [options]
|
|
268
|
+
|
|
269
|
+
Commands:
|
|
270
|
+
providers List configured providers
|
|
271
|
+
health Check provider health
|
|
272
|
+
generate TEXT Generate a text response
|
|
273
|
+
embed TEXT Generate an embedding
|
|
274
|
+
config validate Validate the current configuration
|
|
275
|
+
|
|
276
|
+
Options:
|
|
277
|
+
--provider NAME Select a provider
|
|
278
|
+
--model NAME Override the selected operation's model
|
|
279
|
+
--chat-model NAME Override the chat model
|
|
280
|
+
--embedding-model NAME Override the embedding model
|
|
281
|
+
--api-key KEY Use an API key for the operation
|
|
282
|
+
--api-key-env NAME Read the API key from an environment variable
|
|
283
|
+
--format FORMAT Use text or json output
|
|
284
|
+
HELP
|
|
285
|
+
status
|
|
286
|
+
end
|
|
287
|
+
end
|
data/lib/prescient/client.rb
CHANGED
|
@@ -16,26 +16,25 @@ module Prescient
|
|
|
16
16
|
# client = Prescient::Client.new # Uses configured default
|
|
17
17
|
# puts client.provider_name # => :ollama (or configured default)
|
|
18
18
|
#
|
|
19
|
-
# @author Claude Code
|
|
20
|
-
# @since 1.0.0
|
|
21
19
|
class Client
|
|
22
20
|
# @return [Symbol] The name of the provider being used
|
|
23
21
|
attr_reader :provider_name
|
|
24
22
|
|
|
25
|
-
# @return [Base] The underlying provider instance
|
|
23
|
+
# @return [Prescient::Base] The underlying provider instance
|
|
26
24
|
attr_reader :provider
|
|
27
25
|
|
|
28
26
|
# Initialize a new client with the specified provider
|
|
29
27
|
#
|
|
30
28
|
# @param provider_name [Symbol, nil] Name of provider to use, or nil for default
|
|
31
29
|
# @param enable_fallback [Boolean] Whether to enable automatic fallback to other providers
|
|
30
|
+
# @param provider_options [Hash] Temporary options for the selected provider
|
|
32
31
|
# @raise [Prescient::Error] If the specified provider is not configured
|
|
33
|
-
def initialize(provider_name = nil, enable_fallback: true)
|
|
32
|
+
def initialize(provider_name = nil, enable_fallback: true, provider_options: {})
|
|
34
33
|
@provider_name = provider_name || Prescient.configuration.default_provider
|
|
35
|
-
@provider =
|
|
34
|
+
@provider = provider_with_options(@provider_name, provider_options)
|
|
36
35
|
@enable_fallback = enable_fallback
|
|
37
36
|
|
|
38
|
-
raise Prescient::Error, "Provider not
|
|
37
|
+
raise Prescient::Error, "Provider not configured: #{@provider_name}" unless @provider
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
# Generate embeddings for the given text
|
|
@@ -84,14 +83,14 @@ module Prescient
|
|
|
84
83
|
|
|
85
84
|
# Check the health status of the provider
|
|
86
85
|
#
|
|
87
|
-
# @return [Hash] Health status information
|
|
86
|
+
# @return [Hash] Health status information from the selected provider
|
|
88
87
|
def health_check
|
|
89
88
|
@provider.health_check
|
|
90
89
|
end
|
|
91
90
|
|
|
92
91
|
# Check if the provider is currently available
|
|
93
92
|
#
|
|
94
|
-
# @return [Boolean] true if provider
|
|
93
|
+
# @return [Boolean] true if the provider currently passes its availability check
|
|
95
94
|
def available?
|
|
96
95
|
@provider.available?
|
|
97
96
|
end
|
|
@@ -101,7 +100,8 @@ module Prescient
|
|
|
101
100
|
# Returns details about the provider including its availability
|
|
102
101
|
# and configuration options (with sensitive data removed).
|
|
103
102
|
#
|
|
104
|
-
# @return [Hash] Provider information including :name, :class, :available,
|
|
103
|
+
# @return [Hash] Provider information including :name, :class, :available,
|
|
104
|
+
# and recursively sanitized :options
|
|
105
105
|
def provider_info
|
|
106
106
|
{
|
|
107
107
|
name: @provider_name,
|
|
@@ -111,20 +111,35 @@ module Prescient
|
|
|
111
111
|
}
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def sanitize_options(options)
|
|
117
|
+
sensitive_keys = Prescient::Configuration::DEFAULT_SENSITIVE_KEYS + Prescient.configuration.sensitive_keys
|
|
118
|
+
|
|
119
|
+
case options
|
|
120
|
+
when Hash
|
|
121
|
+
sanitized = {} # : Hash[untyped, untyped]
|
|
122
|
+
options.each do |key, value|
|
|
123
|
+
next if key.respond_to?(:to_sym) && sensitive_keys.include?(key.to_sym)
|
|
117
124
|
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
sanitized[key] = sanitize_options(value)
|
|
126
|
+
end
|
|
127
|
+
sanitized
|
|
128
|
+
when Array
|
|
129
|
+
options.map { |value| sanitize_options(value) }
|
|
130
|
+
else
|
|
131
|
+
options
|
|
132
|
+
end
|
|
120
133
|
end
|
|
121
134
|
|
|
122
|
-
|
|
135
|
+
def provider_with_options(provider_name, provider_options)
|
|
136
|
+
return Prescient.configuration.provider(provider_name) if provider_options.empty?
|
|
123
137
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
options
|
|
138
|
+
registration = Prescient.configuration.providers[provider_name.to_sym]
|
|
139
|
+
return unless registration
|
|
140
|
+
|
|
141
|
+
registration_options = registration[:options] # : Hash[Symbol, untyped]
|
|
142
|
+
registration[:class].new(**registration_options, **provider_options)
|
|
128
143
|
end
|
|
129
144
|
|
|
130
145
|
def with_error_handling
|
|
@@ -150,24 +165,17 @@ module Prescient
|
|
|
150
165
|
last_error = nil
|
|
151
166
|
|
|
152
167
|
providers_to_try.each_with_index do |provider_name, index|
|
|
153
|
-
|
|
154
|
-
provider = if index.zero? && provider_name == @provider_name
|
|
155
|
-
@provider
|
|
156
|
-
else
|
|
157
|
-
Prescient.configuration.provider(provider_name)
|
|
158
|
-
end
|
|
168
|
+
provider = provider_for(provider_name, index)
|
|
159
169
|
next unless provider
|
|
160
170
|
|
|
161
|
-
#
|
|
162
|
-
next unless provider.available?
|
|
163
|
-
|
|
164
|
-
# Use retry logic for each provider
|
|
171
|
+
# Use the provider operation as the availability probe.
|
|
165
172
|
return with_error_handling do
|
|
166
173
|
provider.send(method_name, *args, **options)
|
|
167
174
|
end
|
|
168
175
|
rescue Prescient::Error => e
|
|
176
|
+
raise e unless fallback_eligible?(e)
|
|
177
|
+
|
|
169
178
|
last_error = e
|
|
170
|
-
# Log the error and continue to next provider
|
|
171
179
|
next
|
|
172
180
|
end
|
|
173
181
|
|
|
@@ -175,36 +183,74 @@ module Prescient
|
|
|
175
183
|
raise last_error || Prescient::Error.new("No available providers for #{method_name}")
|
|
176
184
|
end
|
|
177
185
|
|
|
186
|
+
def provider_for(provider_name, index)
|
|
187
|
+
return @provider if index.zero? && provider_name == @provider_name
|
|
188
|
+
|
|
189
|
+
Prescient.configuration.provider(provider_name)
|
|
190
|
+
end
|
|
191
|
+
|
|
178
192
|
def providers_to_try
|
|
179
193
|
providers = [@provider_name]
|
|
180
194
|
|
|
181
195
|
# Add configured fallback providers
|
|
182
196
|
fallback_providers = Prescient.configuration.fallback_providers
|
|
183
|
-
if fallback_providers && !fallback_providers.empty?
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
additional_providers = if fallback_providers && !fallback_providers.empty?
|
|
198
|
+
fallback_providers.reject { |p| p == @provider_name }
|
|
199
|
+
else
|
|
200
|
+
# If no explicit fallbacks are configured, probe all configured providers
|
|
201
|
+
Prescient.configuration.providers.keys.reject { |p| p == @provider_name }
|
|
202
|
+
end
|
|
203
|
+
providers += additional_providers
|
|
190
204
|
|
|
191
205
|
providers.uniq
|
|
192
206
|
end
|
|
207
|
+
|
|
208
|
+
def fallback_eligible?(error)
|
|
209
|
+
[
|
|
210
|
+
Prescient::ConnectionError,
|
|
211
|
+
Prescient::RateLimitError,
|
|
212
|
+
Prescient::ModelNotAvailableError,
|
|
213
|
+
Prescient::ProviderError,
|
|
214
|
+
].any? { |error_class| error.is_a?(error_class) }
|
|
215
|
+
end
|
|
193
216
|
end
|
|
194
217
|
|
|
195
218
|
# Convenience methods for quick access
|
|
196
|
-
|
|
197
|
-
|
|
219
|
+
#
|
|
220
|
+
# @param provider_name [Symbol, nil] Provider to use, or the configured default
|
|
221
|
+
# @param enable_fallback [Boolean] Whether provider fallback is enabled
|
|
222
|
+
# @param provider_options [Hash] Temporary options for the selected provider
|
|
223
|
+
# @return [Client] A configured client instance
|
|
224
|
+
def self.client(provider_name = nil, enable_fallback: true, provider_options: {})
|
|
225
|
+
Client.new(provider_name, enable_fallback: enable_fallback, provider_options: provider_options)
|
|
198
226
|
end
|
|
199
227
|
|
|
228
|
+
# Generate an embedding through a configured provider.
|
|
229
|
+
#
|
|
230
|
+
# @param text [String] Text to embed
|
|
231
|
+
# @param provider [Symbol, nil] Provider to use
|
|
232
|
+
# @param enable_fallback [Boolean] Whether provider fallback is enabled
|
|
233
|
+
# @return [Array<Float>] Embedding vector
|
|
200
234
|
def self.generate_embedding(text, provider: nil, enable_fallback: true, **options)
|
|
201
235
|
client(provider, enable_fallback: enable_fallback).generate_embedding(text, **options)
|
|
202
236
|
end
|
|
203
237
|
|
|
238
|
+
# Generate a response through a configured provider.
|
|
239
|
+
#
|
|
240
|
+
# @param prompt [String] Prompt to send
|
|
241
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
|
242
|
+
# @param provider [Symbol, nil] Provider to use
|
|
243
|
+
# @param enable_fallback [Boolean] Whether provider fallback is enabled
|
|
244
|
+
# @return [Hash] Normalized provider response with :response, :model, :provider
|
|
245
|
+
# and optional metadata
|
|
204
246
|
def self.generate_response(prompt, context_items = [], provider: nil, enable_fallback: true, **options)
|
|
205
247
|
client(provider, enable_fallback: enable_fallback).generate_response(prompt, context_items, **options)
|
|
206
248
|
end
|
|
207
249
|
|
|
250
|
+
# Return the health status of a configured provider.
|
|
251
|
+
#
|
|
252
|
+
# @param provider [Symbol, nil] Provider to check
|
|
253
|
+
# @return [Hash] Provider health information
|
|
208
254
|
def self.health_check(provider: nil)
|
|
209
255
|
client(provider, enable_fallback: false).health_check
|
|
210
256
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prescient
|
|
4
|
+
# Base error class for all Prescient-specific errors
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
attr_reader :provider
|
|
7
|
+
attr_reader :operation
|
|
8
|
+
attr_reader :status
|
|
9
|
+
|
|
10
|
+
def initialize(message = nil, provider: nil, operation: nil, status: nil)
|
|
11
|
+
super(message)
|
|
12
|
+
|
|
13
|
+
@provider = provider
|
|
14
|
+
@operation = operation
|
|
15
|
+
@status = status
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Raised when there are connection issues with AI providers
|
|
20
|
+
class ConnectionError < Error; end
|
|
21
|
+
|
|
22
|
+
# Raised when API authentication fails
|
|
23
|
+
class AuthenticationError < Error; end
|
|
24
|
+
|
|
25
|
+
# Raised when API rate limits are exceeded
|
|
26
|
+
class RateLimitError < Error; end
|
|
27
|
+
|
|
28
|
+
# Raised when a requested model is not available
|
|
29
|
+
class ModelNotAvailableError < Error; end
|
|
30
|
+
|
|
31
|
+
# Raised when AI provider returns invalid or malformed responses
|
|
32
|
+
class InvalidResponseError < Error; end
|
|
33
|
+
|
|
34
|
+
# Raised when a vector cannot be stored or searched safely
|
|
35
|
+
class InvalidVectorError < Error; end
|
|
36
|
+
|
|
37
|
+
# Raised when an AI provider reports a transient service-side failure
|
|
38
|
+
class ProviderError < Error; end
|
|
39
|
+
|
|
40
|
+
# Container module for AI provider implementations
|
|
41
|
+
#
|
|
42
|
+
# All provider classes should be defined within this module and inherit
|
|
43
|
+
# from {Prescient::Base}.
|
|
44
|
+
module Provider
|
|
45
|
+
# Module for AI provider implementations
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Namespace for optional PostgreSQL pgvector integration
|
|
49
|
+
module Pgvector
|
|
50
|
+
end
|
|
51
|
+
end
|