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
|
@@ -2,33 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
require 'httparty'
|
|
4
4
|
|
|
5
|
+
# Ollama local or hosted API provider adapter.
|
|
5
6
|
class Prescient::Provider::Ollama < Prescient::Base
|
|
6
7
|
include HTTParty
|
|
7
8
|
|
|
8
|
-
EMBEDDING_DIMENSIONS = 768 # nomic-embed-text dimensions
|
|
9
|
-
|
|
10
9
|
def initialize(**options)
|
|
11
10
|
super
|
|
12
11
|
self.class.base_uri(@options[:url])
|
|
13
12
|
self.class.default_timeout(@options[:timeout] || 60)
|
|
14
13
|
end
|
|
15
14
|
|
|
15
|
+
# Generate an embedding through Ollama's `/api/embed` endpoint.
|
|
16
|
+
# @param text [String] Text to embed
|
|
17
|
+
# @return [Array<Float>] Embedding vector from the first returned input item
|
|
16
18
|
def generate_embedding(text, **_options)
|
|
17
19
|
handle_errors do
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
embeddings = fetch_and_parse('post', '/api/embed',
|
|
21
|
+
root_key: 'embeddings',
|
|
22
|
+
headers: { 'Content-Type' => 'application/json' },
|
|
23
|
+
body: {
|
|
24
|
+
model: options[:model] || @options[:embedding_model],
|
|
25
|
+
input: clean_text(text),
|
|
26
|
+
}.to_json)
|
|
27
|
+
|
|
28
|
+
embedding = embeddings.is_a?(Array) ? embeddings.first : nil # : Array[Float]?
|
|
29
|
+
raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding.is_a?(Array)
|
|
30
|
+
|
|
31
|
+
expected_dimensions = @options[:embedding_dimensions]
|
|
32
|
+
if expected_dimensions && embedding.length != expected_dimensions
|
|
33
|
+
raise Prescient::InvalidResponseError,
|
|
34
|
+
"Invalid embedding dimensions: expected #{expected_dimensions}, got #{embedding.length}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
embedding
|
|
29
38
|
end
|
|
30
39
|
end
|
|
31
40
|
|
|
41
|
+
# Generate text through Ollama's generation endpoint.
|
|
42
|
+
# @param prompt [String] Prompt to send
|
|
43
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
|
44
|
+
# @return [Hash] Normalized response data
|
|
32
45
|
def generate_response(prompt, context_items = [], **options)
|
|
33
46
|
handle_errors do
|
|
34
47
|
request_options = prepare_generate_response(prompt, context_items, **options)
|
|
@@ -42,7 +55,7 @@ class Prescient::Provider::Ollama < Prescient::Base
|
|
|
42
55
|
|
|
43
56
|
{
|
|
44
57
|
response: generated_text.strip,
|
|
45
|
-
model: @options[:chat_model],
|
|
58
|
+
model: options[:model] || @options[:chat_model],
|
|
46
59
|
provider: 'ollama',
|
|
47
60
|
processing_time: response.parsed_response['total_duration']&./(1_000_000_000.0),
|
|
48
61
|
metadata: {
|
|
@@ -54,6 +67,12 @@ class Prescient::Provider::Ollama < Prescient::Base
|
|
|
54
67
|
end
|
|
55
68
|
end
|
|
56
69
|
|
|
70
|
+
# Check whether the configured Ollama models are available locally.
|
|
71
|
+
#
|
|
72
|
+
# `reachable` indicates the Ollama API answered successfully. `ready`
|
|
73
|
+
# indicates that both configured models are present in the local model list.
|
|
74
|
+
#
|
|
75
|
+
# @return [Hash] Provider health information
|
|
57
76
|
def health_check
|
|
58
77
|
handle_errors do
|
|
59
78
|
models = available_models
|
|
@@ -63,6 +82,7 @@ class Prescient::Provider::Ollama < Prescient::Base
|
|
|
63
82
|
{
|
|
64
83
|
status: 'healthy',
|
|
65
84
|
provider: 'ollama',
|
|
85
|
+
reachable: true,
|
|
66
86
|
url: @options[:url],
|
|
67
87
|
models_available: models.map { |m| m[:name] },
|
|
68
88
|
embedding_model: {
|
|
@@ -78,14 +98,19 @@ class Prescient::Provider::Ollama < Prescient::Base
|
|
|
78
98
|
end
|
|
79
99
|
rescue Prescient::Error => e
|
|
80
100
|
{
|
|
81
|
-
status:
|
|
82
|
-
provider:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
status: 'unavailable',
|
|
102
|
+
provider: 'ollama',
|
|
103
|
+
reachable: false,
|
|
104
|
+
error: e.class.name,
|
|
105
|
+
message: e.message,
|
|
106
|
+
url: @options[:url],
|
|
107
|
+
ready: false,
|
|
86
108
|
}
|
|
87
109
|
end
|
|
88
110
|
|
|
111
|
+
# List models currently available from Ollama.
|
|
112
|
+
# @return [Array<Hash>] Model descriptors including name, size, digest,
|
|
113
|
+
# modified_at, and booleans for configured embedding/chat roles
|
|
89
114
|
def available_models
|
|
90
115
|
return @_available_models if defined?(@_available_models)
|
|
91
116
|
|
|
@@ -98,6 +123,9 @@ class Prescient::Provider::Ollama < Prescient::Base
|
|
|
98
123
|
end
|
|
99
124
|
end
|
|
100
125
|
|
|
126
|
+
# Pull a model into the Ollama installation.
|
|
127
|
+
# @param model_name [String] Model identifier to download
|
|
128
|
+
# @return [Hash] Pull result
|
|
101
129
|
def pull_model(model_name)
|
|
102
130
|
handle_errors do
|
|
103
131
|
fetch_and_parse('post', '/api/pull',
|
|
@@ -130,7 +158,7 @@ class Prescient::Provider::Ollama < Prescient::Base
|
|
|
130
158
|
{ root_key: 'response',
|
|
131
159
|
headers: { 'Content-Type' => 'application/json' },
|
|
132
160
|
body: {
|
|
133
|
-
model: @options[:chat_model],
|
|
161
|
+
model: options[:model] || @options[:chat_model],
|
|
134
162
|
prompt: formatted_prompt,
|
|
135
163
|
stream: false,
|
|
136
164
|
options: {
|
|
@@ -152,21 +180,4 @@ class Prescient::Provider::Ollama < Prescient::Base
|
|
|
152
180
|
response.parsed_response[root_key]
|
|
153
181
|
end
|
|
154
182
|
|
|
155
|
-
def validate_response!(response, operation)
|
|
156
|
-
return if response.success?
|
|
157
|
-
|
|
158
|
-
case response.code
|
|
159
|
-
when 404
|
|
160
|
-
raise Prescient::ModelNotAvailableError, "Model not available for #{operation}"
|
|
161
|
-
when 429
|
|
162
|
-
raise Prescient::RateLimitError, "Rate limit exceeded for #{operation}"
|
|
163
|
-
when 401, 403
|
|
164
|
-
raise Prescient::AuthenticationError, "Authentication failed for #{operation}"
|
|
165
|
-
when 500..599
|
|
166
|
-
raise Prescient::Error, "Ollama server error during #{operation}: #{response.body}"
|
|
167
|
-
else
|
|
168
|
-
raise Prescient::Error,
|
|
169
|
-
"Ollama request failed for #{operation}: HTTP #{response.code} - #{response.message}"
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
183
|
end
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require 'httparty'
|
|
4
4
|
|
|
5
|
+
# OpenAI API provider adapter.
|
|
5
6
|
class Prescient::Provider::OpenAI < Prescient::Base
|
|
6
7
|
include HTTParty
|
|
7
8
|
|
|
8
9
|
base_uri 'https://api.openai.com'
|
|
9
10
|
|
|
11
|
+
# Known embedding dimensions for OpenAI embedding models.
|
|
10
12
|
EMBEDDING_DIMENSIONS = {
|
|
11
13
|
'text-embedding-3-small' => 1536,
|
|
12
14
|
'text-embedding-3-large' => 3072,
|
|
@@ -18,17 +20,21 @@ class Prescient::Provider::OpenAI < Prescient::Base
|
|
|
18
20
|
self.class.default_timeout(@options[:timeout] || 60)
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
# Generate an embedding through the OpenAI embeddings API.
|
|
24
|
+
# @param text [String] Text to embed
|
|
25
|
+
# @return [Array<Float>] Embedding vector
|
|
26
|
+
def generate_embedding(text, **options)
|
|
22
27
|
handle_errors do
|
|
23
28
|
clean_text_input = clean_text(text)
|
|
24
29
|
|
|
30
|
+
embedding_model = options[:model] || @options[:embedding_model]
|
|
25
31
|
response = self.class.post('/v1/embeddings',
|
|
26
32
|
headers: {
|
|
27
33
|
'Content-Type' => 'application/json',
|
|
28
34
|
'Authorization' => "Bearer #{@options[:api_key]}",
|
|
29
35
|
},
|
|
30
36
|
body: {
|
|
31
|
-
model:
|
|
37
|
+
model: embedding_model,
|
|
32
38
|
input: clean_text_input,
|
|
33
39
|
encoding_format: 'float',
|
|
34
40
|
}.to_json)
|
|
@@ -38,11 +44,20 @@ class Prescient::Provider::OpenAI < Prescient::Base
|
|
|
38
44
|
embedding_data = response.parsed_response.dig('data', 0, 'embedding')
|
|
39
45
|
raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding_data
|
|
40
46
|
|
|
41
|
-
expected_dimensions = EMBEDDING_DIMENSIONS[@options[:
|
|
42
|
-
|
|
47
|
+
expected_dimensions = EMBEDDING_DIMENSIONS[embedding_model] || @options[:embedding_dimensions]
|
|
48
|
+
unless expected_dimensions
|
|
49
|
+
raise Prescient::Error,
|
|
50
|
+
"Embedding dimensions are required for model #{embedding_model}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
validate_embedding_dimensions(embedding_data, expected_dimensions)
|
|
43
54
|
end
|
|
44
55
|
end
|
|
45
56
|
|
|
57
|
+
# Generate a response through the OpenAI chat completions API.
|
|
58
|
+
# @param prompt [String] Prompt to send
|
|
59
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
|
60
|
+
# @return [Hash] Normalized response data
|
|
46
61
|
def generate_response(prompt, context_items = [], **options)
|
|
47
62
|
handle_errors do
|
|
48
63
|
formatted_prompt = build_prompt(prompt, context_items)
|
|
@@ -53,7 +68,7 @@ class Prescient::Provider::OpenAI < Prescient::Base
|
|
|
53
68
|
'Authorization' => "Bearer #{@options[:api_key]}",
|
|
54
69
|
},
|
|
55
70
|
body: {
|
|
56
|
-
model: @options[:chat_model],
|
|
71
|
+
model: options[:model] || @options[:chat_model],
|
|
57
72
|
messages: [
|
|
58
73
|
{
|
|
59
74
|
role: 'user',
|
|
@@ -72,7 +87,7 @@ class Prescient::Provider::OpenAI < Prescient::Base
|
|
|
72
87
|
|
|
73
88
|
{
|
|
74
89
|
response: content.strip,
|
|
75
|
-
model: @options[:chat_model],
|
|
90
|
+
model: options[:model] || @options[:chat_model],
|
|
76
91
|
provider: 'openai',
|
|
77
92
|
processing_time: nil,
|
|
78
93
|
metadata: {
|
|
@@ -83,6 +98,12 @@ class Prescient::Provider::OpenAI < Prescient::Base
|
|
|
83
98
|
end
|
|
84
99
|
end
|
|
85
100
|
|
|
101
|
+
# Check OpenAI model availability via `/v1/models`.
|
|
102
|
+
#
|
|
103
|
+
# `reachable` indicates the API answered successfully. `ready` indicates that
|
|
104
|
+
# both configured models appear in the returned model list.
|
|
105
|
+
#
|
|
106
|
+
# @return [Hash] Provider health information
|
|
86
107
|
def health_check
|
|
87
108
|
handle_errors do
|
|
88
109
|
response = self.class.get('/v1/models',
|
|
@@ -98,6 +119,7 @@ class Prescient::Provider::OpenAI < Prescient::Base
|
|
|
98
119
|
{
|
|
99
120
|
status: 'healthy',
|
|
100
121
|
provider: 'openai',
|
|
122
|
+
reachable: true,
|
|
101
123
|
models_available: models.map { |m| m['id'] },
|
|
102
124
|
embedding_model: {
|
|
103
125
|
name: @options[:embedding_model],
|
|
@@ -111,22 +133,29 @@ class Prescient::Provider::OpenAI < Prescient::Base
|
|
|
111
133
|
}
|
|
112
134
|
else
|
|
113
135
|
{
|
|
114
|
-
status:
|
|
115
|
-
provider:
|
|
116
|
-
|
|
117
|
-
|
|
136
|
+
status: 'unhealthy',
|
|
137
|
+
provider: 'openai',
|
|
138
|
+
reachable: true,
|
|
139
|
+
error: "HTTP #{response.code}",
|
|
140
|
+
message: response.message,
|
|
141
|
+
ready: false,
|
|
118
142
|
}
|
|
119
143
|
end
|
|
120
144
|
end
|
|
121
145
|
rescue Prescient::Error => e
|
|
122
146
|
{
|
|
123
|
-
status:
|
|
124
|
-
provider:
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
status: 'unavailable',
|
|
148
|
+
provider: 'openai',
|
|
149
|
+
reachable: false,
|
|
150
|
+
error: e.class.name,
|
|
151
|
+
message: e.message,
|
|
152
|
+
ready: false,
|
|
127
153
|
}
|
|
128
154
|
end
|
|
129
155
|
|
|
156
|
+
# List models available to the configured OpenAI account.
|
|
157
|
+
#
|
|
158
|
+
# @return [Array<Hash>] Model descriptors
|
|
130
159
|
def list_models
|
|
131
160
|
handle_errors do
|
|
132
161
|
response = self.class.get('/v1/models',
|
|
@@ -157,25 +186,4 @@ class Prescient::Provider::OpenAI < Prescient::Base
|
|
|
157
186
|
raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
|
|
158
187
|
end
|
|
159
188
|
|
|
160
|
-
private
|
|
161
|
-
|
|
162
|
-
def validate_response!(response, operation)
|
|
163
|
-
return if response.success?
|
|
164
|
-
|
|
165
|
-
case response.code
|
|
166
|
-
when 400
|
|
167
|
-
raise Prescient::Error, "Bad request for #{operation}: #{response.body}"
|
|
168
|
-
when 401
|
|
169
|
-
raise Prescient::AuthenticationError, "Authentication failed for #{operation}"
|
|
170
|
-
when 403
|
|
171
|
-
raise Prescient::AuthenticationError, "Forbidden access for #{operation}"
|
|
172
|
-
when 429
|
|
173
|
-
raise Prescient::RateLimitError, "Rate limit exceeded for #{operation}"
|
|
174
|
-
when 500..599
|
|
175
|
-
raise Prescient::Error, "OpenAI server error during #{operation}: #{response.body}"
|
|
176
|
-
else
|
|
177
|
-
raise Prescient::Error,
|
|
178
|
-
"OpenAI request failed for #{operation}: HTTP #{response.code} - #{response.message}"
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
189
|
end
|
data/lib/prescient/version.rb
CHANGED
data/lib/prescient.rb
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'prescient/version'
|
|
4
|
+
require_relative 'prescient/errors'
|
|
5
|
+
require_relative 'prescient/pgvector'
|
|
6
|
+
require_relative 'prescient/base'
|
|
7
|
+
require_relative 'prescient/provider/ollama'
|
|
8
|
+
require_relative 'prescient/provider/anthropic'
|
|
9
|
+
require_relative 'prescient/provider/openai'
|
|
10
|
+
require_relative 'prescient/provider/huggingface'
|
|
11
|
+
require_relative 'prescient/client'
|
|
12
|
+
require_relative 'prescient/cli'
|
|
4
13
|
|
|
5
14
|
# Main Prescient module for AI provider abstraction
|
|
6
15
|
#
|
|
7
16
|
# Prescient provides a unified interface for working with multiple AI providers
|
|
8
|
-
# including Ollama, OpenAI, Anthropic, and
|
|
17
|
+
# including Ollama, OpenAI, Anthropic, and Hugging Face. It supports both
|
|
9
18
|
# embedding generation and text completion with configurable context handling.
|
|
10
19
|
#
|
|
11
20
|
# @example Basic usage
|
|
@@ -20,44 +29,6 @@ require_relative 'prescient/version'
|
|
|
20
29
|
# @example Embedding generation
|
|
21
30
|
# embedding = client.generate_embedding("Some text to embed")
|
|
22
31
|
# puts embedding.length # => 1536 (for OpenAI text-embedding-3-small)
|
|
23
|
-
#
|
|
24
|
-
# @author Claude Code
|
|
25
|
-
# @since 1.0.0
|
|
26
|
-
module Prescient
|
|
27
|
-
# Base error class for all Prescient-specific errors
|
|
28
|
-
class Error < StandardError; end
|
|
29
|
-
|
|
30
|
-
# Raised when there are connection issues with AI providers
|
|
31
|
-
class ConnectionError < Error; end
|
|
32
|
-
|
|
33
|
-
# Raised when API authentication fails
|
|
34
|
-
class AuthenticationError < Error; end
|
|
35
|
-
|
|
36
|
-
# Raised when API rate limits are exceeded
|
|
37
|
-
class RateLimitError < Error; end
|
|
38
|
-
|
|
39
|
-
# Raised when a requested model is not available
|
|
40
|
-
class ModelNotAvailableError < Error; end
|
|
41
|
-
|
|
42
|
-
# Raised when AI provider returns invalid or malformed responses
|
|
43
|
-
class InvalidResponseError < Error; end
|
|
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}.
|
|
49
|
-
module Provider
|
|
50
|
-
# Module for AI provider implementations
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
require_relative 'prescient/base'
|
|
55
|
-
require_relative 'prescient/provider/ollama'
|
|
56
|
-
require_relative 'prescient/provider/anthropic'
|
|
57
|
-
require_relative 'prescient/provider/openai'
|
|
58
|
-
require_relative 'prescient/provider/huggingface'
|
|
59
|
-
require_relative 'prescient/client'
|
|
60
|
-
|
|
61
32
|
module Prescient
|
|
62
33
|
# Configure Prescient with custom settings and providers
|
|
63
34
|
#
|
|
@@ -95,6 +66,9 @@ module Prescient
|
|
|
95
66
|
# Handles global settings like timeouts and retry behavior, as well as
|
|
96
67
|
# provider registration and instantiation.
|
|
97
68
|
class Configuration
|
|
69
|
+
# @return [Array<Symbol>] Built-in provider option keys removed from output
|
|
70
|
+
DEFAULT_SENSITIVE_KEYS = [:api_key, :password, :token, :secret].freeze
|
|
71
|
+
|
|
98
72
|
# @return [Symbol] The default provider to use when none specified
|
|
99
73
|
attr_accessor :default_provider
|
|
100
74
|
|
|
@@ -110,6 +84,9 @@ module Prescient
|
|
|
110
84
|
# @return [Array<Symbol>] List of fallback providers to try when primary fails
|
|
111
85
|
attr_accessor :fallback_providers
|
|
112
86
|
|
|
87
|
+
# @return [Array<Symbol>] Additional keys removed from provider information
|
|
88
|
+
attr_reader :sensitive_keys
|
|
89
|
+
|
|
113
90
|
# @return [Hash] Registered providers configuration
|
|
114
91
|
attr_reader :providers
|
|
115
92
|
|
|
@@ -120,7 +97,18 @@ module Prescient
|
|
|
120
97
|
@retry_attempts = 3
|
|
121
98
|
@retry_delay = 1.0
|
|
122
99
|
@fallback_providers = []
|
|
100
|
+
@sensitive_keys = []
|
|
123
101
|
@providers = {}
|
|
102
|
+
@provider_instances = {} # : Hash[Symbol, untyped]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Configure additional keys to remove from provider information.
|
|
106
|
+
# Built-in sensitive keys are always sanitized.
|
|
107
|
+
#
|
|
108
|
+
# @param keys [Array<Symbol, String>] Additional sensitive option keys
|
|
109
|
+
# @return [Array<Symbol>] Normalized additional keys
|
|
110
|
+
def sensitive_keys=(keys)
|
|
111
|
+
@sensitive_keys = Array(keys).map(&:to_sym).uniq
|
|
124
112
|
end
|
|
125
113
|
|
|
126
114
|
# Register a new AI provider
|
|
@@ -137,12 +125,14 @@ module Prescient
|
|
|
137
125
|
# @example Add OpenAI provider
|
|
138
126
|
# config.add_provider(:openai, Prescient::Provider::OpenAI,
|
|
139
127
|
# api_key: 'sk-...',
|
|
140
|
-
# chat_model: 'gpt-4')
|
|
128
|
+
# chat_model: 'gpt-4.1-mini')
|
|
141
129
|
def add_provider(name, provider_class, **options)
|
|
142
|
-
|
|
130
|
+
provider_name = name.to_sym
|
|
131
|
+
@providers[provider_name] = {
|
|
143
132
|
class: provider_class,
|
|
144
133
|
options: options,
|
|
145
134
|
}
|
|
135
|
+
@provider_instances.delete(provider_name)
|
|
146
136
|
end
|
|
147
137
|
|
|
148
138
|
# Instantiate a provider by name
|
|
@@ -150,15 +140,20 @@ module Prescient
|
|
|
150
140
|
# @param name [Symbol] The provider name
|
|
151
141
|
# @return [Base, nil] Provider instance or nil if not found
|
|
152
142
|
def provider(name)
|
|
153
|
-
|
|
143
|
+
provider_name = name.to_sym
|
|
144
|
+
provider_config = @providers[provider_name]
|
|
154
145
|
return nil unless provider_config
|
|
155
146
|
|
|
156
|
-
provider_config[:
|
|
147
|
+
provider_options = provider_config[:options] # : Hash[Symbol, untyped]
|
|
148
|
+
@provider_instances[provider_name] ||= provider_config[:class].new(**provider_options)
|
|
157
149
|
end
|
|
158
150
|
|
|
159
|
-
# Get list of
|
|
151
|
+
# Get list of providers that currently pass {Prescient::Base#available?}.
|
|
152
|
+
#
|
|
153
|
+
# Providers are included when their health check reports `reachable: true`,
|
|
154
|
+
# or, for legacy adapters, `status == "healthy"`.
|
|
160
155
|
#
|
|
161
|
-
# @return [Array<Symbol>] List of
|
|
156
|
+
# @return [Array<Symbol>] List of reachable provider names
|
|
162
157
|
def available_providers
|
|
163
158
|
@providers.keys.select do |name|
|
|
164
159
|
provider(name)&.available?
|
|
@@ -168,25 +163,53 @@ module Prescient
|
|
|
168
163
|
end
|
|
169
164
|
end
|
|
170
165
|
|
|
166
|
+
class << self
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def configure_default_providers(config, env)
|
|
170
|
+
config.add_provider(:ollama,
|
|
171
|
+
Prescient::Provider::Ollama,
|
|
172
|
+
url: env.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
173
|
+
embedding_model: env.fetch('OLLAMA_EMBEDDING_MODEL', 'nomic-embed-text'),
|
|
174
|
+
chat_model: env.fetch('OLLAMA_CHAT_MODEL', 'llama3.2:3b'))
|
|
175
|
+
if env['OPENAI_API_KEY']
|
|
176
|
+
config.add_provider(
|
|
177
|
+
:openai,
|
|
178
|
+
Prescient::Provider::OpenAI,
|
|
179
|
+
api_key: env['OPENAI_API_KEY'],
|
|
180
|
+
embedding_model: env.fetch('OPENAI_EMBEDDING_MODEL', 'text-embedding-3-small'),
|
|
181
|
+
chat_model: env.fetch('OPENAI_CHAT_MODEL', 'gpt-4.1-mini'),
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
if env['ANTHROPIC_API_KEY']
|
|
185
|
+
config.add_provider(
|
|
186
|
+
:anthropic,
|
|
187
|
+
Prescient::Provider::Anthropic,
|
|
188
|
+
api_key: env['ANTHROPIC_API_KEY'],
|
|
189
|
+
model: env.fetch('ANTHROPIC_MODEL', 'claude-sonnet-4-20250514'),
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
return unless env['HUGGINGFACE_API_KEY']
|
|
194
|
+
|
|
195
|
+
config.add_provider(
|
|
196
|
+
:huggingface,
|
|
197
|
+
Prescient::Provider::HuggingFace,
|
|
198
|
+
api_key: env['HUGGINGFACE_API_KEY'],
|
|
199
|
+
embedding_model: env.fetch(
|
|
200
|
+
'HUGGINGFACE_EMBEDDING_MODEL',
|
|
201
|
+
'sentence-transformers/all-MiniLM-L6-v2',
|
|
202
|
+
),
|
|
203
|
+
chat_model: env.fetch(
|
|
204
|
+
'HUGGINGFACE_CHAT_MODEL',
|
|
205
|
+
'google/gemma-2-2b-it',
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
171
211
|
# Default configuration
|
|
172
212
|
configure do |config|
|
|
173
|
-
config
|
|
174
|
-
url: ENV.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
175
|
-
embedding_model: ENV.fetch('OLLAMA_EMBEDDING_MODEL', 'nomic-embed-text'),
|
|
176
|
-
chat_model: ENV.fetch('OLLAMA_CHAT_MODEL', 'llama3.1:8b'))
|
|
177
|
-
|
|
178
|
-
config.add_provider(:anthropic, Prescient::Provider::Anthropic,
|
|
179
|
-
api_key: ENV.fetch('ANTHROPIC_API_KEY', nil),
|
|
180
|
-
model: ENV.fetch('ANTHROPIC_MODEL', 'claude-3-haiku-20240307'))
|
|
181
|
-
|
|
182
|
-
config.add_provider(:openai, Prescient::Provider::OpenAI,
|
|
183
|
-
api_key: ENV.fetch('OPENAI_API_KEY', nil),
|
|
184
|
-
embedding_model: ENV.fetch('OPENAI_EMBEDDING_MODEL', 'text-embedding-3-small'),
|
|
185
|
-
chat_model: ENV.fetch('OPENAI_CHAT_MODEL', 'gpt-3.5-turbo'))
|
|
186
|
-
|
|
187
|
-
config.add_provider(:huggingface, Prescient::Provider::HuggingFace,
|
|
188
|
-
api_key: ENV.fetch('HUGGINGFACE_API_KEY', nil),
|
|
189
|
-
embedding_model: ENV.fetch('HUGGINGFACE_EMBEDDING_MODEL', 'sentence-transformers/all-MiniLM-L6-v2'),
|
|
190
|
-
chat_model: ENV.fetch('HUGGINGFACE_CHAT_MODEL', 'microsoft/DialoGPT-medium'))
|
|
213
|
+
configure_default_providers(config, ENV)
|
|
191
214
|
end
|
|
192
215
|
end
|
|
@@ -5,7 +5,7 @@ set -e
|
|
|
5
5
|
|
|
6
6
|
OLLAMA_URL=${OLLAMA_URL:-"http://localhost:11434"}
|
|
7
7
|
EMBEDDING_MODEL=${OLLAMA_EMBEDDING_MODEL:-"nomic-embed-text"}
|
|
8
|
-
CHAT_MODEL=${OLLAMA_CHAT_MODEL:-"llama3.
|
|
8
|
+
CHAT_MODEL=${OLLAMA_CHAT_MODEL:-"llama3.2:3b"}
|
|
9
9
|
|
|
10
10
|
echo "🚀 Setting up Ollama models for Prescient gem..."
|
|
11
11
|
echo "Ollama URL: $OLLAMA_URL"
|
|
@@ -74,4 +74,4 @@ main() {
|
|
|
74
74
|
echo " OLLAMA_URL=$OLLAMA_URL ruby examples/custom_contexts.rb"
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
main "$@"
|
|
77
|
+
main "$@"
|