prescient 0.1.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/.rubocop.yml +6 -2
- data/.yardopts +14 -0
- data/CHANGELOG.md +64 -0
- data/CHANGELOG.pdf +0 -0
- data/INTEGRATION_GUIDE.md +363 -0
- data/README.md +96 -38
- data/Rakefile +2 -1
- data/VECTOR_SEARCH_GUIDE.md +42 -39
- data/lib/prescient/base.rb +123 -19
- data/lib/prescient/client.rb +125 -21
- data/lib/prescient/provider/huggingface.rb +1 -3
- data/lib/prescient/version.rb +1 -1
- data/lib/prescient.rb +103 -1
- data/prescient.gemspec +17 -15
- metadata +67 -32
data/lib/prescient/base.rb
CHANGED
@@ -1,26 +1,95 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Base class for all AI provider implementations
|
4
|
+
#
|
5
|
+
# This abstract base class defines the common interface that all AI providers
|
6
|
+
# must implement. It provides shared functionality for text processing, context
|
7
|
+
# formatting, prompt building, and error handling.
|
8
|
+
#
|
9
|
+
# @abstract Subclass and implement {#generate_embedding}, {#generate_response},
|
10
|
+
# {#health_check}, and {#validate_configuration!}
|
11
|
+
#
|
12
|
+
# @example Creating a custom provider
|
13
|
+
# class MyProvider < Prescient::Base
|
14
|
+
# def generate_embedding(text, **options)
|
15
|
+
# # Implementation here
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# def generate_response(prompt, context_items = [], **options)
|
19
|
+
# # Implementation here
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# def health_check
|
23
|
+
# # Implementation here
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# @author Claude Code
|
28
|
+
# @since 1.0.0
|
3
29
|
class Prescient::Base
|
30
|
+
# @return [Hash] Configuration options for this provider instance
|
4
31
|
attr_reader :options
|
5
32
|
|
33
|
+
# Initialize the provider with configuration options
|
34
|
+
#
|
35
|
+
# @param options [Hash] Provider-specific configuration options
|
36
|
+
# @option options [String] :api_key API key for authenticated providers
|
37
|
+
# @option options [String] :url Base URL for self-hosted providers
|
38
|
+
# @option options [Integer] :timeout Request timeout in seconds
|
39
|
+
# @option options [Hash] :prompt_templates Custom prompt templates
|
40
|
+
# @option options [Hash] :context_configs Context formatting configurations
|
6
41
|
def initialize(**options)
|
7
42
|
@options = options
|
8
43
|
validate_configuration!
|
9
44
|
end
|
10
45
|
|
11
|
-
#
|
12
|
-
|
46
|
+
# Generate embeddings for the given text
|
47
|
+
#
|
48
|
+
# This method must be implemented by subclasses to provide embedding
|
49
|
+
# generation functionality.
|
50
|
+
#
|
51
|
+
# @param text [String] The text to generate embeddings for
|
52
|
+
# @param options [Hash] Provider-specific options
|
53
|
+
# @return [Array<Float>] Array of embedding values
|
54
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
55
|
+
# @abstract
|
56
|
+
def generate_embedding(text, **options)
|
13
57
|
raise NotImplementedError, "#{self.class} must implement #generate_embedding"
|
14
58
|
end
|
15
59
|
|
60
|
+
# Generate text response for the given prompt
|
61
|
+
#
|
62
|
+
# This method must be implemented by subclasses to provide text generation
|
63
|
+
# functionality with optional context items.
|
64
|
+
#
|
65
|
+
# @param prompt [String] The prompt to generate a response for
|
66
|
+
# @param context_items [Array<Hash, String>] Optional context items to include
|
67
|
+
# @param options [Hash] Provider-specific generation options
|
68
|
+
# @option options [Float] :temperature Sampling temperature (0.0-2.0)
|
69
|
+
# @option options [Integer] :max_tokens Maximum tokens to generate
|
70
|
+
# @option options [Float] :top_p Nucleus sampling parameter
|
71
|
+
# @return [Hash] Response hash with :response, :model, :provider keys
|
72
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
73
|
+
# @abstract
|
16
74
|
def generate_response(prompt, context_items = [], **options)
|
17
75
|
raise NotImplementedError, "#{self.class} must implement #generate_response"
|
18
76
|
end
|
19
77
|
|
78
|
+
# Check the health and availability of the provider
|
79
|
+
#
|
80
|
+
# This method must be implemented by subclasses to provide health check
|
81
|
+
# functionality.
|
82
|
+
#
|
83
|
+
# @return [Hash] Health status with :status, :provider keys and optional details
|
84
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
85
|
+
# @abstract
|
20
86
|
def health_check
|
21
87
|
raise NotImplementedError, "#{self.class} must implement #health_check"
|
22
88
|
end
|
23
89
|
|
90
|
+
# Check if the provider is currently available
|
91
|
+
#
|
92
|
+
# @return [Boolean] true if provider is healthy and available
|
24
93
|
def available?
|
25
94
|
health_check[:status] == 'healthy'
|
26
95
|
rescue StandardError
|
@@ -29,10 +98,28 @@ class Prescient::Base
|
|
29
98
|
|
30
99
|
protected
|
31
100
|
|
101
|
+
# Validate provider configuration
|
102
|
+
#
|
103
|
+
# Override this method in subclasses to validate required configuration
|
104
|
+
# options and raise appropriate errors for missing or invalid settings.
|
105
|
+
#
|
106
|
+
# @return [void]
|
107
|
+
# @raise [Prescient::Error] If configuration is invalid
|
32
108
|
def validate_configuration!
|
33
109
|
# Override in subclasses to validate required configuration
|
34
110
|
end
|
35
111
|
|
112
|
+
# Handle and standardize errors from provider operations
|
113
|
+
#
|
114
|
+
# Wraps provider-specific operations and converts common exceptions
|
115
|
+
# into standardized Prescient error types while preserving existing
|
116
|
+
# Prescient errors.
|
117
|
+
#
|
118
|
+
# @yield The operation block to execute with error handling
|
119
|
+
# @return [Object] The result of the yielded block
|
120
|
+
# @raise [Prescient::ConnectionError] For network/timeout errors
|
121
|
+
# @raise [Prescient::InvalidResponseError] For JSON parsing errors
|
122
|
+
# @raise [Prescient::Error] For other unexpected errors
|
36
123
|
def handle_errors
|
37
124
|
yield
|
38
125
|
rescue Prescient::Error
|
@@ -48,31 +135,40 @@ class Prescient::Base
|
|
48
135
|
raise Prescient::Error, "Unexpected error: #{e.message}"
|
49
136
|
end
|
50
137
|
|
138
|
+
# Normalize embedding dimensions to match expected size
|
139
|
+
#
|
140
|
+
# Ensures embedding vectors have consistent dimensions by truncating
|
141
|
+
# longer vectors or padding shorter ones with zeros.
|
142
|
+
#
|
143
|
+
# @param embedding [Array<Float>] The embedding vector to normalize
|
144
|
+
# @param target_dimensions [Integer] The desired number of dimensions
|
145
|
+
# @return [Array<Float>, nil] Normalized embedding or nil if input invalid
|
51
146
|
def normalize_embedding(embedding, target_dimensions)
|
52
147
|
return nil unless embedding.is_a?(Array)
|
53
|
-
return embedding if embedding.length
|
148
|
+
return embedding.first(target_dimensions) if embedding.length >= target_dimensions
|
54
149
|
|
55
|
-
|
56
|
-
# Truncate
|
57
|
-
embedding.first(target_dimensions)
|
58
|
-
else
|
59
|
-
# Pad with zeros
|
60
|
-
embedding + Array.new(target_dimensions - embedding.length, 0.0)
|
61
|
-
end
|
150
|
+
embedding + Array.new(target_dimensions - embedding.length, 0.0)
|
62
151
|
end
|
63
152
|
|
153
|
+
# Clean and preprocess text for AI processing
|
154
|
+
#
|
155
|
+
# Removes excess whitespace, normalizes spacing, and enforces length
|
156
|
+
# limits suitable for most AI models.
|
157
|
+
#
|
158
|
+
# @param text [String, nil] The text to clean
|
159
|
+
# @return [String] Cleaned text, empty string if input was nil/empty
|
64
160
|
def clean_text(text)
|
65
|
-
return '' if text.nil? || text.to_s.strip.empty?
|
66
|
-
|
67
|
-
cleaned = text.to_s
|
68
|
-
.strip
|
69
|
-
.gsub(/\s+/, ' ')
|
70
|
-
|
71
161
|
# Limit length for most models
|
72
|
-
|
162
|
+
text.to_s.gsub(/\s+/, ' ').strip.slice(0, 8000)
|
73
163
|
end
|
74
164
|
|
75
|
-
#
|
165
|
+
# Get default prompt templates
|
166
|
+
#
|
167
|
+
# Provides standard templates for system prompts and context handling
|
168
|
+
# that can be overridden via provider options.
|
169
|
+
#
|
170
|
+
# @return [Hash] Hash containing template strings with placeholders
|
171
|
+
# @private
|
76
172
|
def default_prompt_templates
|
77
173
|
{
|
78
174
|
system_prompt: 'You are a helpful AI assistant. Answer questions clearly and accurately.',
|
@@ -96,7 +192,14 @@ class Prescient::Base
|
|
96
192
|
}
|
97
193
|
end
|
98
194
|
|
99
|
-
# Build prompt
|
195
|
+
# Build formatted prompt from query and context items
|
196
|
+
#
|
197
|
+
# Creates a properly formatted prompt using configurable templates,
|
198
|
+
# incorporating context items when provided.
|
199
|
+
#
|
200
|
+
# @param query [String] The user's question or prompt
|
201
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
202
|
+
# @return [String] Formatted prompt ready for AI processing
|
100
203
|
def build_prompt(query, context_items = [])
|
101
204
|
templates = default_prompt_templates.merge(@options[:prompt_templates] || {})
|
102
205
|
system_prompt = templates[:system_prompt]
|
@@ -143,6 +246,7 @@ class Prescient::Base
|
|
143
246
|
# Extract text values from hash, excluding non-textual fields
|
144
247
|
def extract_text_values(item)
|
145
248
|
# Common fields to exclude from embedding text
|
249
|
+
# TODO: configurable fields to exclude aside from the common ones below
|
146
250
|
exclude_fields = ['id', '_id', 'uuid', 'created_at', 'updated_at', 'timestamp', 'version', 'status', 'active']
|
147
251
|
|
148
252
|
item.filter_map { |key, value|
|
data/lib/prescient/client.rb
CHANGED
@@ -1,45 +1,107 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
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
|
4
21
|
class Client
|
22
|
+
# @return [Symbol] The name of the provider being used
|
5
23
|
attr_reader :provider_name
|
24
|
+
|
25
|
+
# @return [Base] The underlying provider instance
|
6
26
|
attr_reader :provider
|
7
27
|
|
8
|
-
|
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)
|
9
34
|
@provider_name = provider_name || Prescient.configuration.default_provider
|
10
35
|
@provider = Prescient.configuration.provider(@provider_name)
|
36
|
+
@enable_fallback = enable_fallback
|
11
37
|
|
12
38
|
raise Prescient::Error, "Provider not found: #{@provider_name}" unless @provider
|
13
39
|
end
|
14
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
|
15
51
|
def generate_embedding(text, **options)
|
16
|
-
|
17
|
-
|
52
|
+
if @enable_fallback
|
53
|
+
with_fallback_handling(:generate_embedding, text, **options)
|
54
|
+
else
|
55
|
+
with_error_handling do
|
18
56
|
@provider.generate_embedding(text, **options)
|
19
|
-
else
|
20
|
-
@provider.generate_embedding(text)
|
21
57
|
end
|
22
58
|
end
|
23
59
|
end
|
24
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
|
25
75
|
def generate_response(prompt, context_items = [], **options)
|
26
|
-
|
27
|
-
|
76
|
+
if @enable_fallback
|
77
|
+
with_fallback_handling(:generate_response, prompt, context_items, **options)
|
78
|
+
else
|
79
|
+
with_error_handling do
|
28
80
|
@provider.generate_response(prompt, context_items, **options)
|
29
|
-
else
|
30
|
-
@provider.generate_response(prompt, context_items)
|
31
81
|
end
|
32
82
|
end
|
33
83
|
end
|
34
84
|
|
85
|
+
# Check the health status of the provider
|
86
|
+
#
|
87
|
+
# @return [Hash] Health status information
|
35
88
|
def health_check
|
36
89
|
@provider.health_check
|
37
90
|
end
|
38
91
|
|
92
|
+
# Check if the provider is currently available
|
93
|
+
#
|
94
|
+
# @return [Boolean] true if provider is healthy and available
|
39
95
|
def available?
|
40
96
|
@provider.available?
|
41
97
|
end
|
42
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
|
43
105
|
def provider_info
|
44
106
|
{
|
45
107
|
name: @provider_name,
|
@@ -50,11 +112,7 @@ module Prescient
|
|
50
112
|
end
|
51
113
|
|
52
114
|
def method_missing(method_name, ...)
|
53
|
-
|
54
|
-
@provider.send(method_name, ...)
|
55
|
-
else
|
56
|
-
super
|
57
|
-
end
|
115
|
+
@provider.respond_to?(method_name) ? @provider.send(method_name, ...) : super
|
58
116
|
end
|
59
117
|
|
60
118
|
def respond_to_missing?(method_name, include_private = false)
|
@@ -63,6 +121,7 @@ module Prescient
|
|
63
121
|
|
64
122
|
private
|
65
123
|
|
124
|
+
# TODO: configurable keys to sanitize
|
66
125
|
def sanitize_options(options)
|
67
126
|
sensitive_keys = [:api_key, :password, :token, :secret]
|
68
127
|
options.reject { |key, _| sensitive_keys.include?(key.to_sym) }
|
@@ -86,22 +145,67 @@ module Prescient
|
|
86
145
|
retry
|
87
146
|
end
|
88
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
|
89
193
|
end
|
90
194
|
|
91
195
|
# Convenience methods for quick access
|
92
|
-
def self.client(provider_name = nil)
|
93
|
-
Client.new(provider_name)
|
196
|
+
def self.client(provider_name = nil, enable_fallback: true)
|
197
|
+
Client.new(provider_name, enable_fallback: enable_fallback)
|
94
198
|
end
|
95
199
|
|
96
|
-
def self.generate_embedding(text, provider: nil, **options)
|
97
|
-
client(provider).generate_embedding(text, **options)
|
200
|
+
def self.generate_embedding(text, provider: nil, enable_fallback: true, **options)
|
201
|
+
client(provider, enable_fallback: enable_fallback).generate_embedding(text, **options)
|
98
202
|
end
|
99
203
|
|
100
|
-
def self.generate_response(prompt, context_items = [], provider: nil, **options)
|
101
|
-
client(provider).generate_response(prompt, context_items, **options)
|
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)
|
102
206
|
end
|
103
207
|
|
104
208
|
def self.health_check(provider: nil)
|
105
|
-
client(provider).health_check
|
209
|
+
client(provider, enable_fallback: false).health_check
|
106
210
|
end
|
107
211
|
end
|
@@ -157,9 +157,7 @@ class Prescient::Provider::HuggingFace < Prescient::Base
|
|
157
157
|
protected
|
158
158
|
|
159
159
|
def validate_configuration!
|
160
|
-
|
161
|
-
missing_options = required_options.select { |opt| @options[opt].nil? }
|
162
|
-
|
160
|
+
missing_options = [:api_key, :embedding_model, :chat_model].select { |opt| @options[opt].nil? }
|
163
161
|
return unless missing_options.any?
|
164
162
|
|
165
163
|
raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
|
data/lib/prescient/version.rb
CHANGED
data/lib/prescient.rb
CHANGED
@@ -2,14 +2,50 @@
|
|
2
2
|
|
3
3
|
require_relative 'prescient/version'
|
4
4
|
|
5
|
+
# Main Prescient module for AI provider abstraction
|
6
|
+
#
|
7
|
+
# Prescient provides a unified interface for working with multiple AI providers
|
8
|
+
# including Ollama, OpenAI, Anthropic, and HuggingFace. It supports both
|
9
|
+
# embedding generation and text completion with configurable context handling.
|
10
|
+
#
|
11
|
+
# @example Basic usage
|
12
|
+
# Prescient.configure do |config|
|
13
|
+
# config.add_provider(:openai, Prescient::Provider::OpenAI,
|
14
|
+
# api_key: 'your-api-key')
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# client = Prescient.client(:openai)
|
18
|
+
# response = client.generate_response("Hello, world!")
|
19
|
+
#
|
20
|
+
# @example Embedding generation
|
21
|
+
# embedding = client.generate_embedding("Some text to embed")
|
22
|
+
# puts embedding.length # => 1536 (for OpenAI text-embedding-3-small)
|
23
|
+
#
|
24
|
+
# @author Claude Code
|
25
|
+
# @since 1.0.0
|
5
26
|
module Prescient
|
27
|
+
# Base error class for all Prescient-specific errors
|
6
28
|
class Error < StandardError; end
|
29
|
+
|
30
|
+
# Raised when there are connection issues with AI providers
|
7
31
|
class ConnectionError < Error; end
|
32
|
+
|
33
|
+
# Raised when API authentication fails
|
8
34
|
class AuthenticationError < Error; end
|
35
|
+
|
36
|
+
# Raised when API rate limits are exceeded
|
9
37
|
class RateLimitError < Error; end
|
38
|
+
|
39
|
+
# Raised when a requested model is not available
|
10
40
|
class ModelNotAvailableError < Error; end
|
41
|
+
|
42
|
+
# Raised when AI provider returns invalid or malformed responses
|
11
43
|
class InvalidResponseError < Error; end
|
12
44
|
|
45
|
+
# Container module for AI provider implementations
|
46
|
+
#
|
47
|
+
# All provider classes should be defined within this module and inherit
|
48
|
+
# from {Prescient::Base}.
|
13
49
|
module Provider
|
14
50
|
# Module for AI provider implementations
|
15
51
|
end
|
@@ -23,34 +59,85 @@ require_relative 'prescient/provider/huggingface'
|
|
23
59
|
require_relative 'prescient/client'
|
24
60
|
|
25
61
|
module Prescient
|
26
|
-
# Configure
|
62
|
+
# Configure Prescient with custom settings and providers
|
63
|
+
#
|
64
|
+
# @example Configure with custom provider
|
65
|
+
# Prescient.configure do |config|
|
66
|
+
# config.default_provider = :openai
|
67
|
+
# config.timeout = 60
|
68
|
+
# config.add_provider(:openai, Prescient::Provider::OpenAI,
|
69
|
+
# api_key: 'your-key')
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# @yield [config] Configuration block
|
73
|
+
# @yieldparam config [Configuration] The configuration object
|
74
|
+
# @return [void]
|
27
75
|
def self.configure
|
28
76
|
yield(configuration)
|
29
77
|
end
|
30
78
|
|
79
|
+
# Get the current configuration instance
|
80
|
+
#
|
81
|
+
# @return [Configuration] The current configuration
|
31
82
|
def self.configuration
|
32
83
|
@_configuration ||= Configuration.new
|
33
84
|
end
|
34
85
|
|
86
|
+
# Reset configuration to defaults
|
87
|
+
#
|
88
|
+
# @return [Configuration] New configuration instance
|
35
89
|
def self.reset_configuration!
|
36
90
|
@_configuration = Configuration.new
|
37
91
|
end
|
38
92
|
|
93
|
+
# Configuration class for managing Prescient settings and providers
|
94
|
+
#
|
95
|
+
# Handles global settings like timeouts and retry behavior, as well as
|
96
|
+
# provider registration and instantiation.
|
39
97
|
class Configuration
|
98
|
+
# @return [Symbol] The default provider to use when none specified
|
40
99
|
attr_accessor :default_provider
|
100
|
+
|
101
|
+
# @return [Integer] Default timeout in seconds for API requests
|
41
102
|
attr_accessor :timeout
|
103
|
+
|
104
|
+
# @return [Integer] Number of retry attempts for failed requests
|
42
105
|
attr_accessor :retry_attempts
|
106
|
+
|
107
|
+
# @return [Float] Delay between retry attempts in seconds
|
43
108
|
attr_accessor :retry_delay
|
109
|
+
|
110
|
+
# @return [Array<Symbol>] List of fallback providers to try when primary fails
|
111
|
+
attr_accessor :fallback_providers
|
112
|
+
|
113
|
+
# @return [Hash] Registered providers configuration
|
44
114
|
attr_reader :providers
|
45
115
|
|
116
|
+
# Initialize configuration with default values
|
46
117
|
def initialize
|
47
118
|
@default_provider = :ollama
|
48
119
|
@timeout = 30
|
49
120
|
@retry_attempts = 3
|
50
121
|
@retry_delay = 1.0
|
122
|
+
@fallback_providers = []
|
51
123
|
@providers = {}
|
52
124
|
end
|
53
125
|
|
126
|
+
# Register a new AI provider
|
127
|
+
#
|
128
|
+
# @param name [Symbol] Unique identifier for the provider
|
129
|
+
# @param provider_class [Class] Provider class that inherits from Base
|
130
|
+
# @param options [Hash] Configuration options for the provider
|
131
|
+
# @option options [String] :api_key API key for authenticated providers
|
132
|
+
# @option options [String] :url Base URL for self-hosted providers
|
133
|
+
# @option options [String] :model, :chat_model Model name for text generation
|
134
|
+
# @option options [String] :embedding_model Model name for embeddings
|
135
|
+
# @return [void]
|
136
|
+
#
|
137
|
+
# @example Add OpenAI provider
|
138
|
+
# config.add_provider(:openai, Prescient::Provider::OpenAI,
|
139
|
+
# api_key: 'sk-...',
|
140
|
+
# chat_model: 'gpt-4')
|
54
141
|
def add_provider(name, provider_class, **options)
|
55
142
|
@providers[name.to_sym] = {
|
56
143
|
class: provider_class,
|
@@ -58,12 +145,27 @@ module Prescient
|
|
58
145
|
}
|
59
146
|
end
|
60
147
|
|
148
|
+
# Instantiate a provider by name
|
149
|
+
#
|
150
|
+
# @param name [Symbol] The provider name
|
151
|
+
# @return [Base, nil] Provider instance or nil if not found
|
61
152
|
def provider(name)
|
62
153
|
provider_config = @providers[name.to_sym]
|
63
154
|
return nil unless provider_config
|
64
155
|
|
65
156
|
provider_config[:class].new(**provider_config[:options])
|
66
157
|
end
|
158
|
+
|
159
|
+
# Get list of available providers (those that are configured and healthy)
|
160
|
+
#
|
161
|
+
# @return [Array<Symbol>] List of available provider names
|
162
|
+
def available_providers
|
163
|
+
@providers.keys.select do |name|
|
164
|
+
provider(name)&.available?
|
165
|
+
rescue StandardError
|
166
|
+
false
|
167
|
+
end
|
168
|
+
end
|
67
169
|
end
|
68
170
|
|
69
171
|
# Default configuration
|
data/prescient.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.description = "Prescient provides a unified interface for AI providers including local Ollama, Anthropic Claude, OpenAI GPT, and HuggingFace models. Built for AI applications with error handling, health monitoring, and provider switching."
|
13
13
|
spec.homepage = "https://github.com/yourcompany/prescient"
|
14
14
|
spec.license = "MIT"
|
15
|
-
spec.required_ruby_version = ">= 3.
|
15
|
+
spec.required_ruby_version = ">= 3.1.0"
|
16
16
|
|
17
17
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
18
|
spec.metadata["homepage_uri"] = spec.homepage
|
@@ -31,21 +31,23 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.require_paths = ["lib"]
|
32
32
|
|
33
33
|
# Runtime dependencies
|
34
|
-
spec.add_dependency "httparty", "~> 0.
|
35
|
-
|
34
|
+
spec.add_dependency "httparty", "~> 0.23.1"
|
35
|
+
|
36
36
|
# Optional dependencies for vector database integration
|
37
|
-
spec.add_development_dependency "pg", "~> 1.
|
37
|
+
spec.add_development_dependency "pg", "~> 1.6" # PostgreSQL adapter for pgvector integration
|
38
38
|
|
39
39
|
# Development dependencies
|
40
|
-
spec.add_development_dependency "minitest", "~> 5.
|
41
|
-
spec.add_development_dependency "mocha", "~> 2.
|
42
|
-
spec.add_development_dependency "webmock", "~> 3.
|
43
|
-
spec.add_development_dependency "vcr", "~> 6.
|
44
|
-
spec.add_development_dependency "rubocop", "~> 1.
|
45
|
-
spec.add_development_dependency "rubocop-minitest", "~> 0.
|
46
|
-
spec.add_development_dependency "rubocop-performance", "~> 1.
|
47
|
-
spec.add_development_dependency "rubocop-rake", "~> 0.
|
48
|
-
spec.add_development_dependency "simplecov", "~> 0.22"
|
49
|
-
spec.add_development_dependency "rake", "~> 13.
|
50
|
-
spec.add_development_dependency "irb"
|
40
|
+
spec.add_development_dependency "minitest", "~> 5.25"
|
41
|
+
spec.add_development_dependency "mocha", "~> 2.7"
|
42
|
+
spec.add_development_dependency "webmock", "~> 3.25"
|
43
|
+
spec.add_development_dependency "vcr", "~> 6.3"
|
44
|
+
spec.add_development_dependency "rubocop", "~> 1.79"
|
45
|
+
spec.add_development_dependency "rubocop-minitest", "~> 0.38.1"
|
46
|
+
spec.add_development_dependency "rubocop-performance", "~> 1.25"
|
47
|
+
spec.add_development_dependency "rubocop-rake", "~> 0.7.1"
|
48
|
+
spec.add_development_dependency "simplecov", "~> 0.22.0"
|
49
|
+
spec.add_development_dependency "rake", "~> 13.3"
|
50
|
+
spec.add_development_dependency "irb", "~> 1.15"
|
51
|
+
spec.add_development_dependency "yard", "~> 0.9.37"
|
52
|
+
spec.add_development_dependency "kramdown", "~> 2.5"
|
51
53
|
end
|