prescient 0.0.0 → 0.2.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/.env.example +37 -0
- data/.rubocop.yml +330 -0
- data/.yardopts +14 -0
- data/CHANGELOG.md +64 -0
- data/CHANGELOG.pdf +0 -0
- data/Dockerfile.example +41 -0
- data/INTEGRATION_GUIDE.md +363 -0
- data/README.md +917 -13
- data/Rakefile +26 -3
- data/VECTOR_SEARCH_GUIDE.md +453 -0
- data/db/init/01_enable_pgvector.sql +30 -0
- data/db/init/02_create_schema.sql +108 -0
- data/db/init/03_create_indexes.sql +96 -0
- data/db/init/04_insert_sample_data.sql +121 -0
- data/db/migrate/001_create_prescient_tables.rb +158 -0
- data/docker-compose.yml +153 -0
- data/examples/basic_usage.rb +123 -0
- data/examples/custom_contexts.rb +355 -0
- data/examples/custom_prompts.rb +212 -0
- data/examples/vector_search.rb +330 -0
- data/lib/prescient/base.rb +374 -0
- data/lib/prescient/client.rb +211 -0
- data/lib/prescient/provider/anthropic.rb +146 -0
- data/lib/prescient/provider/huggingface.rb +200 -0
- data/lib/prescient/provider/ollama.rb +172 -0
- data/lib/prescient/provider/openai.rb +181 -0
- data/lib/prescient/version.rb +1 -1
- data/lib/prescient.rb +186 -2
- data/prescient.gemspec +53 -0
- data/scripts/setup-ollama-models.sh +77 -0
- metadata +252 -14
- data/.vscode/settings.json +0 -1
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prescient
|
4
|
+
# Client class for interacting with AI providers
|
5
|
+
#
|
6
|
+
# The Client provides a high-level interface for working with AI providers,
|
7
|
+
# handling error recovery, retries, and method delegation. It acts as a
|
8
|
+
# facade over the configured providers.
|
9
|
+
#
|
10
|
+
# @example Basic usage
|
11
|
+
# client = Prescient::Client.new(:openai)
|
12
|
+
# response = client.generate_response("Hello, world!")
|
13
|
+
# embedding = client.generate_embedding("Text to embed")
|
14
|
+
#
|
15
|
+
# @example Using default provider
|
16
|
+
# client = Prescient::Client.new # Uses configured default
|
17
|
+
# puts client.provider_name # => :ollama (or configured default)
|
18
|
+
#
|
19
|
+
# @author Claude Code
|
20
|
+
# @since 1.0.0
|
21
|
+
class Client
|
22
|
+
# @return [Symbol] The name of the provider being used
|
23
|
+
attr_reader :provider_name
|
24
|
+
|
25
|
+
# @return [Base] The underlying provider instance
|
26
|
+
attr_reader :provider
|
27
|
+
|
28
|
+
# Initialize a new client with the specified provider
|
29
|
+
#
|
30
|
+
# @param provider_name [Symbol, nil] Name of provider to use, or nil for default
|
31
|
+
# @param enable_fallback [Boolean] Whether to enable automatic fallback to other providers
|
32
|
+
# @raise [Prescient::Error] If the specified provider is not configured
|
33
|
+
def initialize(provider_name = nil, enable_fallback: true)
|
34
|
+
@provider_name = provider_name || Prescient.configuration.default_provider
|
35
|
+
@provider = Prescient.configuration.provider(@provider_name)
|
36
|
+
@enable_fallback = enable_fallback
|
37
|
+
|
38
|
+
raise Prescient::Error, "Provider not found: #{@provider_name}" unless @provider
|
39
|
+
end
|
40
|
+
|
41
|
+
# Generate embeddings for the given text
|
42
|
+
#
|
43
|
+
# Delegates to the underlying provider with automatic retry logic
|
44
|
+
# for transient failures. If fallback is enabled, tries other providers
|
45
|
+
# on persistent failures.
|
46
|
+
#
|
47
|
+
# @param text [String] The text to generate embeddings for
|
48
|
+
# @param options [Hash] Provider-specific options
|
49
|
+
# @return [Array<Float>] Array of embedding values
|
50
|
+
# @raise [Prescient::Error] If embedding generation fails on all providers
|
51
|
+
def generate_embedding(text, **options)
|
52
|
+
if @enable_fallback
|
53
|
+
with_fallback_handling(:generate_embedding, text, **options)
|
54
|
+
else
|
55
|
+
with_error_handling do
|
56
|
+
@provider.generate_embedding(text, **options)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Generate text response for the given prompt
|
62
|
+
#
|
63
|
+
# Delegates to the underlying provider with automatic retry logic
|
64
|
+
# for transient failures. Supports optional context items for RAG.
|
65
|
+
# If fallback is enabled, tries other providers on persistent failures.
|
66
|
+
#
|
67
|
+
# @param prompt [String] The prompt to generate a response for
|
68
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
69
|
+
# @param options [Hash] Provider-specific generation options
|
70
|
+
# @option options [Float] :temperature Sampling temperature (0.0-2.0)
|
71
|
+
# @option options [Integer] :max_tokens Maximum tokens to generate
|
72
|
+
# @option options [Float] :top_p Nucleus sampling parameter
|
73
|
+
# @return [Hash] Response hash with :response, :model, :provider keys
|
74
|
+
# @raise [Prescient::Error] If response generation fails on all providers
|
75
|
+
def generate_response(prompt, context_items = [], **options)
|
76
|
+
if @enable_fallback
|
77
|
+
with_fallback_handling(:generate_response, prompt, context_items, **options)
|
78
|
+
else
|
79
|
+
with_error_handling do
|
80
|
+
@provider.generate_response(prompt, context_items, **options)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Check the health status of the provider
|
86
|
+
#
|
87
|
+
# @return [Hash] Health status information
|
88
|
+
def health_check
|
89
|
+
@provider.health_check
|
90
|
+
end
|
91
|
+
|
92
|
+
# Check if the provider is currently available
|
93
|
+
#
|
94
|
+
# @return [Boolean] true if provider is healthy and available
|
95
|
+
def available?
|
96
|
+
@provider.available?
|
97
|
+
end
|
98
|
+
|
99
|
+
# Get comprehensive information about the provider
|
100
|
+
#
|
101
|
+
# Returns details about the provider including its availability
|
102
|
+
# and configuration options (with sensitive data removed).
|
103
|
+
#
|
104
|
+
# @return [Hash] Provider information including :name, :class, :available, :options
|
105
|
+
def provider_info
|
106
|
+
{
|
107
|
+
name: @provider_name,
|
108
|
+
class: @provider.class.name.split('::').last,
|
109
|
+
available: available?,
|
110
|
+
options: sanitize_options(@provider.options),
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
def method_missing(method_name, ...)
|
115
|
+
@provider.respond_to?(method_name) ? @provider.send(method_name, ...) : super
|
116
|
+
end
|
117
|
+
|
118
|
+
def respond_to_missing?(method_name, include_private = false)
|
119
|
+
@provider.respond_to?(method_name, include_private) || super
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# TODO: configurable keys to sanitize
|
125
|
+
def sanitize_options(options)
|
126
|
+
sensitive_keys = [:api_key, :password, :token, :secret]
|
127
|
+
options.reject { |key, _| sensitive_keys.include?(key.to_sym) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def with_error_handling
|
131
|
+
retries = 0
|
132
|
+
begin
|
133
|
+
yield
|
134
|
+
rescue Prescient::RateLimitError => e
|
135
|
+
raise e unless retries < Prescient.configuration.retry_attempts
|
136
|
+
|
137
|
+
retries += 1
|
138
|
+
sleep(Prescient.configuration.retry_delay * retries)
|
139
|
+
retry
|
140
|
+
rescue Prescient::ConnectionError => e
|
141
|
+
raise e unless retries < Prescient.configuration.retry_attempts
|
142
|
+
|
143
|
+
retries += 1
|
144
|
+
sleep(Prescient.configuration.retry_delay)
|
145
|
+
retry
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def with_fallback_handling(method_name, *args, **options)
|
150
|
+
last_error = nil
|
151
|
+
|
152
|
+
providers_to_try.each_with_index do |provider_name, index|
|
153
|
+
# Use existing provider instance for primary provider, create new ones for fallbacks
|
154
|
+
provider = if index.zero? && provider_name == @provider_name
|
155
|
+
@provider
|
156
|
+
else
|
157
|
+
Prescient.configuration.provider(provider_name)
|
158
|
+
end
|
159
|
+
next unless provider
|
160
|
+
|
161
|
+
# Check if provider is available before trying
|
162
|
+
next unless provider.available?
|
163
|
+
|
164
|
+
# Use retry logic for each provider
|
165
|
+
return with_error_handling do
|
166
|
+
provider.send(method_name, *args, **options)
|
167
|
+
end
|
168
|
+
rescue Prescient::Error => e
|
169
|
+
last_error = e
|
170
|
+
# Log the error and continue to next provider
|
171
|
+
next
|
172
|
+
end
|
173
|
+
|
174
|
+
# If we get here, all providers failed
|
175
|
+
raise last_error || Prescient::Error.new("No available providers for #{method_name}")
|
176
|
+
end
|
177
|
+
|
178
|
+
def providers_to_try
|
179
|
+
providers = [@provider_name]
|
180
|
+
|
181
|
+
# Add configured fallback providers
|
182
|
+
fallback_providers = Prescient.configuration.fallback_providers
|
183
|
+
if fallback_providers && !fallback_providers.empty?
|
184
|
+
providers += fallback_providers.reject { |p| p == @provider_name }
|
185
|
+
else
|
186
|
+
# If no explicit fallbacks configured, try all available providers
|
187
|
+
available = Prescient.configuration.available_providers
|
188
|
+
providers += available.reject { |p| p == @provider_name }
|
189
|
+
end
|
190
|
+
|
191
|
+
providers.uniq
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Convenience methods for quick access
|
196
|
+
def self.client(provider_name = nil, enable_fallback: true)
|
197
|
+
Client.new(provider_name, enable_fallback: enable_fallback)
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.generate_embedding(text, provider: nil, enable_fallback: true, **options)
|
201
|
+
client(provider, enable_fallback: enable_fallback).generate_embedding(text, **options)
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.generate_response(prompt, context_items = [], provider: nil, enable_fallback: true, **options)
|
205
|
+
client(provider, enable_fallback: enable_fallback).generate_response(prompt, context_items, **options)
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.health_check(provider: nil)
|
209
|
+
client(provider, enable_fallback: false).health_check
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
class Prescient::Provider::Anthropic < Prescient::Base
|
6
|
+
include HTTParty
|
7
|
+
|
8
|
+
base_uri 'https://api.anthropic.com'
|
9
|
+
|
10
|
+
def initialize(**options)
|
11
|
+
super
|
12
|
+
self.class.default_timeout(@options[:timeout] || 60)
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate_embedding(_text, **_options)
|
16
|
+
# Anthropic doesn't provide embedding API, raise error
|
17
|
+
raise Prescient::Error,
|
18
|
+
'Anthropic provider does not support embeddings. Use OpenAI or HuggingFace for embeddings.'
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate_response(prompt, context_items = [], **options)
|
22
|
+
handle_errors do
|
23
|
+
formatted_prompt = build_prompt(prompt, context_items)
|
24
|
+
|
25
|
+
response = self.class.post('/v1/messages',
|
26
|
+
headers: {
|
27
|
+
'Content-Type' => 'application/json',
|
28
|
+
'x-api-key' => @options[:api_key],
|
29
|
+
'anthropic-version' => '2023-06-01',
|
30
|
+
},
|
31
|
+
body: {
|
32
|
+
model: @options[:model],
|
33
|
+
max_tokens: options[:max_tokens] || 2000,
|
34
|
+
temperature: options[:temperature] || 0.7,
|
35
|
+
messages: [
|
36
|
+
{
|
37
|
+
role: 'user',
|
38
|
+
content: formatted_prompt,
|
39
|
+
},
|
40
|
+
],
|
41
|
+
}.to_json)
|
42
|
+
|
43
|
+
validate_response!(response, 'text generation')
|
44
|
+
|
45
|
+
content = response.parsed_response.dig('content', 0, 'text')
|
46
|
+
raise Prescient::InvalidResponseError, 'No response generated' unless content
|
47
|
+
|
48
|
+
{
|
49
|
+
response: content.strip,
|
50
|
+
model: @options[:model],
|
51
|
+
provider: 'anthropic',
|
52
|
+
processing_time: nil,
|
53
|
+
metadata: {
|
54
|
+
usage: response.parsed_response['usage'],
|
55
|
+
},
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def health_check
|
61
|
+
handle_errors do
|
62
|
+
# Test with a simple message
|
63
|
+
response = self.class.post('/v1/messages',
|
64
|
+
headers: {
|
65
|
+
'Content-Type' => 'application/json',
|
66
|
+
'x-api-key' => @options[:api_key],
|
67
|
+
'anthropic-version' => '2023-06-01',
|
68
|
+
},
|
69
|
+
body: {
|
70
|
+
model: @options[:model],
|
71
|
+
max_tokens: 10,
|
72
|
+
messages: [
|
73
|
+
{
|
74
|
+
role: 'user',
|
75
|
+
content: 'Test',
|
76
|
+
},
|
77
|
+
],
|
78
|
+
}.to_json)
|
79
|
+
|
80
|
+
if response.success?
|
81
|
+
{
|
82
|
+
status: 'healthy',
|
83
|
+
provider: 'anthropic',
|
84
|
+
model: @options[:model],
|
85
|
+
ready: true,
|
86
|
+
}
|
87
|
+
else
|
88
|
+
{
|
89
|
+
status: 'unhealthy',
|
90
|
+
provider: 'anthropic',
|
91
|
+
error: "HTTP #{response.code}",
|
92
|
+
message: response.message,
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
rescue Prescient::ConnectionError => e
|
97
|
+
{
|
98
|
+
status: 'unavailable',
|
99
|
+
provider: 'anthropic',
|
100
|
+
error: e.class.name,
|
101
|
+
message: e.message,
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
def list_models
|
106
|
+
# Anthropic doesn't provide a models list API
|
107
|
+
[
|
108
|
+
{ name: 'claude-3-haiku-20240307', type: 'text' },
|
109
|
+
{ name: 'claude-3-sonnet-20240229', type: 'text' },
|
110
|
+
{ name: 'claude-3-opus-20240229', type: 'text' },
|
111
|
+
]
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
|
116
|
+
def validate_configuration!
|
117
|
+
required_options = [:api_key, :model]
|
118
|
+
missing_options = required_options.select { |opt| @options[opt].nil? }
|
119
|
+
|
120
|
+
return unless missing_options.any?
|
121
|
+
|
122
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def validate_response!(response, operation)
|
128
|
+
return if response.success?
|
129
|
+
|
130
|
+
case response.code
|
131
|
+
when 400
|
132
|
+
raise Prescient::Error, "Bad request for #{operation}: #{response.body}"
|
133
|
+
when 401
|
134
|
+
raise Prescient::AuthenticationError, "Authentication failed for #{operation}"
|
135
|
+
when 403
|
136
|
+
raise Prescient::AuthenticationError, "Forbidden access for #{operation}"
|
137
|
+
when 429
|
138
|
+
raise Prescient::RateLimitError, "Rate limit exceeded for #{operation}"
|
139
|
+
when 500..599
|
140
|
+
raise Prescient::Error, "Anthropic server error during #{operation}: #{response.body}"
|
141
|
+
else
|
142
|
+
raise Prescient::Error,
|
143
|
+
"Anthropic request failed for #{operation}: HTTP #{response.code} - #{response.message}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
class Prescient::Provider::HuggingFace < Prescient::Base
|
6
|
+
include HTTParty
|
7
|
+
|
8
|
+
base_uri 'https://api-inference.huggingface.co'
|
9
|
+
|
10
|
+
EMBEDDING_DIMENSIONS = {
|
11
|
+
'sentence-transformers/all-MiniLM-L6-v2' => 384,
|
12
|
+
'sentence-transformers/all-mpnet-base-v2' => 768,
|
13
|
+
'sentence-transformers/all-roberta-large-v1' => 1024,
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize(**options)
|
17
|
+
super
|
18
|
+
self.class.default_timeout(@options[:timeout] || 60)
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate_embedding(text, **_options)
|
22
|
+
handle_errors do
|
23
|
+
clean_text_input = clean_text(text)
|
24
|
+
|
25
|
+
response = self.class.post("/pipeline/feature-extraction/#{@options[:embedding_model]}",
|
26
|
+
headers: {
|
27
|
+
'Content-Type' => 'application/json',
|
28
|
+
'Authorization' => "Bearer #{@options[:api_key]}",
|
29
|
+
},
|
30
|
+
body: {
|
31
|
+
inputs: clean_text_input,
|
32
|
+
options: {
|
33
|
+
wait_for_model: true,
|
34
|
+
},
|
35
|
+
}.to_json)
|
36
|
+
|
37
|
+
validate_response!(response, 'embedding generation')
|
38
|
+
|
39
|
+
# HuggingFace returns embeddings as nested arrays, get the first one
|
40
|
+
embedding_data = response.parsed_response
|
41
|
+
embedding_data = embedding_data.first if embedding_data.is_a?(Array) && embedding_data.first.is_a?(Array)
|
42
|
+
|
43
|
+
raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding_data.is_a?(Array)
|
44
|
+
|
45
|
+
expected_dimensions = EMBEDDING_DIMENSIONS[@options[:embedding_model]] || 384
|
46
|
+
normalize_embedding(embedding_data, expected_dimensions)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_response(prompt, context_items = [], **options)
|
51
|
+
handle_errors do
|
52
|
+
formatted_prompt = build_prompt(prompt, context_items)
|
53
|
+
|
54
|
+
response = self.class.post("/models/#{@options[:chat_model]}",
|
55
|
+
headers: {
|
56
|
+
'Content-Type' => 'application/json',
|
57
|
+
'Authorization' => "Bearer #{@options[:api_key]}",
|
58
|
+
},
|
59
|
+
body: {
|
60
|
+
inputs: formatted_prompt,
|
61
|
+
parameters: {
|
62
|
+
max_new_tokens: options[:max_tokens] || 2000,
|
63
|
+
temperature: options[:temperature] || 0.7,
|
64
|
+
top_p: options[:top_p] || 0.9,
|
65
|
+
return_full_text: false,
|
66
|
+
},
|
67
|
+
options: {
|
68
|
+
wait_for_model: true,
|
69
|
+
},
|
70
|
+
}.to_json)
|
71
|
+
|
72
|
+
validate_response!(response, 'text generation')
|
73
|
+
|
74
|
+
# HuggingFace returns different formats depending on the model
|
75
|
+
generated_text = nil
|
76
|
+
parsed_response = response.parsed_response
|
77
|
+
|
78
|
+
if parsed_response.is_a?(Array) && parsed_response.first.is_a?(Hash)
|
79
|
+
generated_text = parsed_response.first['generated_text']
|
80
|
+
elsif parsed_response.is_a?(Hash)
|
81
|
+
generated_text = parsed_response['generated_text'] || parsed_response['text']
|
82
|
+
end
|
83
|
+
|
84
|
+
raise Prescient::InvalidResponseError, 'No response generated' unless generated_text
|
85
|
+
|
86
|
+
{
|
87
|
+
response: generated_text.strip,
|
88
|
+
model: @options[:chat_model],
|
89
|
+
provider: 'huggingface',
|
90
|
+
processing_time: nil,
|
91
|
+
metadata: {},
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def health_check
|
97
|
+
handle_errors do
|
98
|
+
# Test embedding model
|
99
|
+
embedding_response = self.class.post("/pipeline/feature-extraction/#{@options[:embedding_model]}",
|
100
|
+
headers: {
|
101
|
+
'Authorization' => "Bearer #{@options[:api_key]}",
|
102
|
+
},
|
103
|
+
body: { inputs: 'test' }.to_json)
|
104
|
+
|
105
|
+
# Test chat model
|
106
|
+
chat_response = self.class.post("/models/#{@options[:chat_model]}",
|
107
|
+
headers: {
|
108
|
+
'Authorization' => "Bearer #{@options[:api_key]}",
|
109
|
+
},
|
110
|
+
body: {
|
111
|
+
inputs: 'test',
|
112
|
+
parameters: { max_new_tokens: 5 },
|
113
|
+
}.to_json)
|
114
|
+
|
115
|
+
embedding_healthy = embedding_response.success?
|
116
|
+
chat_healthy = chat_response.success?
|
117
|
+
|
118
|
+
{
|
119
|
+
status: embedding_healthy && chat_healthy ? 'healthy' : 'partial',
|
120
|
+
provider: 'huggingface',
|
121
|
+
embedding_model: {
|
122
|
+
name: @options[:embedding_model],
|
123
|
+
available: embedding_healthy,
|
124
|
+
},
|
125
|
+
chat_model: {
|
126
|
+
name: @options[:chat_model],
|
127
|
+
available: chat_healthy,
|
128
|
+
},
|
129
|
+
ready: embedding_healthy && chat_healthy,
|
130
|
+
}
|
131
|
+
end
|
132
|
+
rescue Prescient::ConnectionError => e
|
133
|
+
{
|
134
|
+
status: 'unavailable',
|
135
|
+
provider: 'huggingface',
|
136
|
+
error: e.class.name,
|
137
|
+
message: e.message,
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def list_models
|
142
|
+
# HuggingFace doesn't provide a simple API to list all models
|
143
|
+
# Return the configured models
|
144
|
+
[
|
145
|
+
{
|
146
|
+
name: @options[:embedding_model],
|
147
|
+
type: 'embedding',
|
148
|
+
dimensions: EMBEDDING_DIMENSIONS[@options[:embedding_model]],
|
149
|
+
},
|
150
|
+
{
|
151
|
+
name: @options[:chat_model],
|
152
|
+
type: 'text-generation',
|
153
|
+
},
|
154
|
+
]
|
155
|
+
end
|
156
|
+
|
157
|
+
protected
|
158
|
+
|
159
|
+
def validate_configuration!
|
160
|
+
missing_options = [:api_key, :embedding_model, :chat_model].select { |opt| @options[opt].nil? }
|
161
|
+
return unless missing_options.any?
|
162
|
+
|
163
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def validate_response!(response, operation)
|
169
|
+
return if response.success?
|
170
|
+
|
171
|
+
case response.code
|
172
|
+
when 400
|
173
|
+
raise Prescient::Error, "Bad request for #{operation}: #{response.body}"
|
174
|
+
when 401
|
175
|
+
raise Prescient::AuthenticationError, "Authentication failed for #{operation}"
|
176
|
+
when 403
|
177
|
+
raise Prescient::AuthenticationError, "Forbidden access for #{operation}"
|
178
|
+
when 429
|
179
|
+
raise Prescient::RateLimitError, "Rate limit exceeded for #{operation}"
|
180
|
+
when 503
|
181
|
+
# HuggingFace model loading
|
182
|
+
error_body = begin
|
183
|
+
response.parsed_response
|
184
|
+
rescue StandardError
|
185
|
+
response.body
|
186
|
+
end
|
187
|
+
if error_body.is_a?(Hash) && error_body['error']&.include?('loading')
|
188
|
+
raise Prescient::Error, 'Model is loading, please try again later'
|
189
|
+
end
|
190
|
+
|
191
|
+
raise Prescient::Error, "HuggingFace service unavailable for #{operation}"
|
192
|
+
|
193
|
+
when 500..599
|
194
|
+
raise Prescient::Error, "HuggingFace server error during #{operation}: #{response.body}"
|
195
|
+
else
|
196
|
+
raise Prescient::Error,
|
197
|
+
"HuggingFace request failed for #{operation}: HTTP #{response.code} - #{response.message}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|