prescient 0.4.0 → 0.5.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/CHANGELOG.md +22 -0
- data/INTEGRATION_GUIDE.md +12 -1
- data/README.md +177 -12
- data/Rakefile +1 -1
- data/VECTOR_SEARCH_GUIDE.md +7 -3
- data/examples/README.md +2 -1
- data/examples/basic_usage.rb +1 -1
- data/examples/custom_contexts.rb +6 -21
- data/examples/vector_search.rb +69 -305
- data/lib/prescient/cli.rb +196 -4
- data/lib/prescient/configuration_loader.rb +437 -0
- data/lib/prescient/provider/deepseek.rb +139 -0
- data/lib/prescient/provider/gemini.rb +173 -0
- data/lib/prescient/provider/huggingface.rb +1 -0
- data/lib/prescient/provider/mistral.rb +171 -0
- data/lib/prescient/provider/openai.rb +3 -0
- data/lib/prescient/provider/xai.rb +139 -0
- data/lib/prescient/version.rb +1 -1
- data/lib/prescient.rb +116 -22
- data/schema/prescient.configuration.schema.json +153 -0
- data/sig/prescient.rbs +105 -0
- metadata +7 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'httparty'
|
|
4
|
+
|
|
5
|
+
# Google Gemini API provider adapter.
|
|
6
|
+
class Prescient::Provider::Gemini < Prescient::Base
|
|
7
|
+
include HTTParty
|
|
8
|
+
|
|
9
|
+
base_uri 'https://generativelanguage.googleapis.com'
|
|
10
|
+
|
|
11
|
+
def initialize(**options)
|
|
12
|
+
super
|
|
13
|
+
@provider_name = 'Google Gemini'
|
|
14
|
+
self.class.default_timeout(@options[:timeout] || 60)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Generate an embedding through Gemini's embedContent endpoint.
|
|
18
|
+
# @param text [String] Text to embed
|
|
19
|
+
# @return [Array<Float>] Embedding vector
|
|
20
|
+
def generate_embedding(text, **options)
|
|
21
|
+
handle_errors do
|
|
22
|
+
embedding_model = options[:model] || @options[:embedding_model]
|
|
23
|
+
response = self.class.post(
|
|
24
|
+
model_endpoint(embedding_model, 'embedContent'),
|
|
25
|
+
headers: api_headers,
|
|
26
|
+
body: {
|
|
27
|
+
content: { parts: [{ text: clean_text(text) }] },
|
|
28
|
+
}.to_json,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
validate_response!(response, 'embedding generation')
|
|
32
|
+
|
|
33
|
+
embedding = response.parsed_response.dig('embedding', 'values')
|
|
34
|
+
raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding.is_a?(Array)
|
|
35
|
+
|
|
36
|
+
expected_dimensions = @options[:embedding_dimensions]
|
|
37
|
+
expected_dimensions ? validate_embedding_dimensions(embedding, expected_dimensions) : embedding
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Generate a response through Gemini's generateContent endpoint.
|
|
42
|
+
# @param prompt [String] Prompt to send
|
|
43
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
|
44
|
+
# @return [Hash] Normalized response data
|
|
45
|
+
def generate_response(prompt, context_items = [], **options)
|
|
46
|
+
handle_errors do
|
|
47
|
+
model = options[:model] || @options[:chat_model]
|
|
48
|
+
response = self.class.post(
|
|
49
|
+
model_endpoint(model, 'generateContent'),
|
|
50
|
+
headers: api_headers,
|
|
51
|
+
body: {
|
|
52
|
+
contents: [{ role: 'user', parts: [{ text: build_prompt(prompt, context_items) }] }],
|
|
53
|
+
generationConfig: {
|
|
54
|
+
maxOutputTokens: options[:max_tokens] || 2000,
|
|
55
|
+
temperature: options[:temperature] || 0.7,
|
|
56
|
+
topP: options[:top_p] || 0.9,
|
|
57
|
+
},
|
|
58
|
+
}.to_json,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
validate_response!(response, 'text generation')
|
|
62
|
+
|
|
63
|
+
parsed_response = response.parsed_response
|
|
64
|
+
parts = parsed_response.dig('candidates', 0, 'content', 'parts')
|
|
65
|
+
content = Array(parts).filter_map { |part| part['text'] }.join
|
|
66
|
+
raise Prescient::InvalidResponseError, 'No response generated' if content.nil? || content.empty?
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
response: content.strip,
|
|
70
|
+
model: model,
|
|
71
|
+
provider: 'gemini',
|
|
72
|
+
processing_time: nil,
|
|
73
|
+
metadata: {
|
|
74
|
+
usage: parsed_response['usageMetadata'],
|
|
75
|
+
finish_reason: parsed_response.dig('candidates', 0, 'finishReason'),
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check whether the configured Gemini models are available.
|
|
82
|
+
# @return [Hash] Provider health information
|
|
83
|
+
def health_check
|
|
84
|
+
handle_errors do
|
|
85
|
+
response = self.class.get('/v1beta/models', headers: api_headers)
|
|
86
|
+
|
|
87
|
+
if response.success?
|
|
88
|
+
models = response.parsed_response['models'] || []
|
|
89
|
+
embedding_model = find_model(models, @options[:embedding_model], 'embedContent')
|
|
90
|
+
chat_model = find_model(models, @options[:chat_model], 'generateContent')
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
status: 'healthy',
|
|
94
|
+
provider: 'gemini',
|
|
95
|
+
reachable: true,
|
|
96
|
+
models_available: models.map { |model| model['name'].to_s.delete_prefix('models/') },
|
|
97
|
+
embedding_model: { name: @options[:embedding_model], available: embedding_model },
|
|
98
|
+
chat_model: { name: @options[:chat_model], available: chat_model },
|
|
99
|
+
ready: embedding_model && chat_model,
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
{
|
|
103
|
+
status: 'unhealthy',
|
|
104
|
+
provider: 'gemini',
|
|
105
|
+
reachable: true,
|
|
106
|
+
error: "HTTP #{response.code}",
|
|
107
|
+
message: response.message,
|
|
108
|
+
ready: false,
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
rescue Prescient::Error => e
|
|
113
|
+
{
|
|
114
|
+
status: 'unavailable',
|
|
115
|
+
provider: 'gemini',
|
|
116
|
+
reachable: false,
|
|
117
|
+
error: e.class.name,
|
|
118
|
+
message: e.message,
|
|
119
|
+
ready: false,
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# List models available to the configured Gemini API key.
|
|
124
|
+
# @return [Array<Hash>] Model descriptors
|
|
125
|
+
def list_models
|
|
126
|
+
handle_errors do
|
|
127
|
+
response = self.class.get('/v1beta/models', headers: api_headers)
|
|
128
|
+
validate_response!(response, 'model listing')
|
|
129
|
+
|
|
130
|
+
(response.parsed_response['models'] || []).map do |model|
|
|
131
|
+
{
|
|
132
|
+
name: model['name'].to_s.delete_prefix('models/'),
|
|
133
|
+
display_name: model['displayName'],
|
|
134
|
+
supported_generation_modes: model['supportedGenerationMethods'],
|
|
135
|
+
input_token_limit: model['inputTokenLimit'],
|
|
136
|
+
output_token_limit: model['outputTokenLimit'],
|
|
137
|
+
}.compact
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
protected
|
|
143
|
+
|
|
144
|
+
def validate_configuration!
|
|
145
|
+
required_options = [:api_key, :embedding_model, :chat_model]
|
|
146
|
+
missing_options = required_options.select { |option| @options[option].nil? }
|
|
147
|
+
|
|
148
|
+
return unless missing_options.any?
|
|
149
|
+
|
|
150
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def api_headers
|
|
156
|
+
{
|
|
157
|
+
'Content-Type' => 'application/json',
|
|
158
|
+
'x-goog-api-key' => @options[:api_key],
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def model_endpoint(model, operation)
|
|
163
|
+
model_name = model.to_s.delete_prefix('models/')
|
|
164
|
+
"/v1beta/models/#{model_name}:#{operation}"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def find_model(models, model_name, operation)
|
|
168
|
+
models.any? do |model|
|
|
169
|
+
model['name'].to_s.delete_prefix('models/') == model_name &&
|
|
170
|
+
model.fetch('supportedGenerationMethods', []).include?(operation)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'httparty'
|
|
4
|
+
|
|
5
|
+
# Mistral AI API provider adapter.
|
|
6
|
+
class Prescient::Provider::Mistral < Prescient::Base
|
|
7
|
+
include HTTParty
|
|
8
|
+
|
|
9
|
+
base_uri 'https://api.mistral.ai'
|
|
10
|
+
|
|
11
|
+
def initialize(**options)
|
|
12
|
+
super
|
|
13
|
+
self.class.default_timeout(@options[:timeout] || 60)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Generate an embedding through Mistral's embeddings API.
|
|
17
|
+
# @param text [String] Text to embed
|
|
18
|
+
# @return [Array<Float>] Embedding vector
|
|
19
|
+
def generate_embedding(text, **options)
|
|
20
|
+
handle_errors do
|
|
21
|
+
embedding_model = options[:model] || @options[:embedding_model]
|
|
22
|
+
response = self.class.post(
|
|
23
|
+
'/v1/embeddings',
|
|
24
|
+
headers: api_headers,
|
|
25
|
+
body: {
|
|
26
|
+
model: embedding_model,
|
|
27
|
+
input: clean_text(text),
|
|
28
|
+
}.to_json,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
validate_response!(response, 'embedding generation')
|
|
32
|
+
|
|
33
|
+
embedding = response.parsed_response.dig('data', 0, 'embedding')
|
|
34
|
+
raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding.is_a?(Array)
|
|
35
|
+
|
|
36
|
+
expected_dimensions = @options[:embedding_dimensions]
|
|
37
|
+
expected_dimensions ? validate_embedding_dimensions(embedding, expected_dimensions) : embedding
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Generate a response through Mistral's chat completions API.
|
|
42
|
+
# @param prompt [String] Prompt to send
|
|
43
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
|
44
|
+
# @return [Hash] Normalized response data
|
|
45
|
+
def generate_response(prompt, context_items = [], **options)
|
|
46
|
+
handle_errors do
|
|
47
|
+
model = options[:model] || @options[:chat_model]
|
|
48
|
+
response = self.class.post(
|
|
49
|
+
'/v1/chat/completions',
|
|
50
|
+
headers: api_headers,
|
|
51
|
+
body: {
|
|
52
|
+
model: model,
|
|
53
|
+
messages: [{ role: 'user', content: build_prompt(prompt, context_items) }],
|
|
54
|
+
max_tokens: options[:max_tokens] || 2000,
|
|
55
|
+
temperature: options[:temperature] || 0.7,
|
|
56
|
+
top_p: options[:top_p] || 0.9,
|
|
57
|
+
}.to_json,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
validate_response!(response, 'text generation')
|
|
61
|
+
|
|
62
|
+
parsed_response = response.parsed_response
|
|
63
|
+
content = normalize_content(parsed_response.dig('choices', 0, 'message', 'content'))
|
|
64
|
+
raise Prescient::InvalidResponseError, 'No response generated' if content.nil? || content.empty?
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
response: content.strip,
|
|
68
|
+
model: model,
|
|
69
|
+
provider: 'mistral',
|
|
70
|
+
processing_time: nil,
|
|
71
|
+
metadata: {
|
|
72
|
+
usage: parsed_response['usage'],
|
|
73
|
+
finish_reason: parsed_response.dig('choices', 0, 'finish_reason'),
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check whether the configured Mistral models are available.
|
|
80
|
+
# @return [Hash] Provider health information
|
|
81
|
+
def health_check
|
|
82
|
+
handle_errors do
|
|
83
|
+
response = self.class.get('/v1/models', headers: api_headers)
|
|
84
|
+
|
|
85
|
+
if response.success?
|
|
86
|
+
models = response.parsed_response['data'] || []
|
|
87
|
+
embedding_available = model_available?(models, @options[:embedding_model])
|
|
88
|
+
chat_available = model_available?(models, @options[:chat_model])
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
status: 'healthy',
|
|
92
|
+
provider: 'mistral',
|
|
93
|
+
reachable: true,
|
|
94
|
+
models_available: models.map { |model| model['id'] },
|
|
95
|
+
embedding_model: { name: @options[:embedding_model], available: embedding_available },
|
|
96
|
+
chat_model: { name: @options[:chat_model], available: chat_available },
|
|
97
|
+
ready: embedding_available && chat_available,
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
{
|
|
101
|
+
status: 'unhealthy',
|
|
102
|
+
provider: 'mistral',
|
|
103
|
+
reachable: true,
|
|
104
|
+
error: "HTTP #{response.code}",
|
|
105
|
+
message: response.message,
|
|
106
|
+
ready: false,
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
rescue Prescient::Error => e
|
|
111
|
+
{
|
|
112
|
+
status: 'unavailable',
|
|
113
|
+
provider: 'mistral',
|
|
114
|
+
reachable: false,
|
|
115
|
+
error: e.class.name,
|
|
116
|
+
message: e.message,
|
|
117
|
+
ready: false,
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# List models available to the configured Mistral API key.
|
|
122
|
+
# @return [Array<Hash>] Model descriptors
|
|
123
|
+
def list_models
|
|
124
|
+
handle_errors do
|
|
125
|
+
response = self.class.get('/v1/models', headers: api_headers)
|
|
126
|
+
validate_response!(response, 'model listing')
|
|
127
|
+
|
|
128
|
+
(response.parsed_response['data'] || []).map do |model|
|
|
129
|
+
{
|
|
130
|
+
name: model['id'],
|
|
131
|
+
object: model['object'],
|
|
132
|
+
created: model['created'],
|
|
133
|
+
owned_by: model['owned_by'],
|
|
134
|
+
capabilities: model['capabilities'],
|
|
135
|
+
max_context_length: model['max_context_length'],
|
|
136
|
+
}.compact
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
protected
|
|
142
|
+
|
|
143
|
+
def validate_configuration!
|
|
144
|
+
required_options = [:api_key, :embedding_model, :chat_model]
|
|
145
|
+
missing_options = required_options.select { |option| @options[option].nil? }
|
|
146
|
+
|
|
147
|
+
return unless missing_options.any?
|
|
148
|
+
|
|
149
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def api_headers
|
|
155
|
+
{
|
|
156
|
+
'Content-Type' => 'application/json',
|
|
157
|
+
'Authorization' => "Bearer #{@options[:api_key]}",
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def model_available?(models, model_name)
|
|
162
|
+
models.any? { |model| model['id'] == model_name }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def normalize_content(content)
|
|
166
|
+
return content if content.is_a?(String)
|
|
167
|
+
return unless content.is_a?(Array)
|
|
168
|
+
|
|
169
|
+
content.filter_map { |part| part['text'] if part.is_a?(Hash) }.join
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -17,6 +17,7 @@ class Prescient::Provider::OpenAI < Prescient::Base
|
|
|
17
17
|
|
|
18
18
|
def initialize(**options)
|
|
19
19
|
super
|
|
20
|
+
@provider_name = 'OpenAI'
|
|
20
21
|
self.class.default_timeout(@options[:timeout] || 60)
|
|
21
22
|
end
|
|
22
23
|
|
|
@@ -82,6 +83,8 @@ class Prescient::Provider::OpenAI < Prescient::Base
|
|
|
82
83
|
|
|
83
84
|
validate_response!(response, 'text generation')
|
|
84
85
|
|
|
86
|
+
puts "response.parsed_response: #{response.parsed_response.inspect}"
|
|
87
|
+
|
|
85
88
|
content = response.parsed_response.dig('choices', 0, 'message', 'content')
|
|
86
89
|
raise Prescient::InvalidResponseError, 'No response generated' unless content
|
|
87
90
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'httparty'
|
|
4
|
+
|
|
5
|
+
# xAI API provider adapter.
|
|
6
|
+
class Prescient::Provider::XAI < Prescient::Base
|
|
7
|
+
include HTTParty
|
|
8
|
+
|
|
9
|
+
base_uri 'https://api.x.ai'
|
|
10
|
+
|
|
11
|
+
def initialize(**options)
|
|
12
|
+
super
|
|
13
|
+
@provider_name = 'xAI'
|
|
14
|
+
self.class.default_timeout(@options[:timeout] || 60)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# xAI does not expose a standard embeddings API for this adapter.
|
|
18
|
+
# @raise [Prescient::Error] Always, because embeddings are unsupported
|
|
19
|
+
def generate_embedding(_text, **_options)
|
|
20
|
+
raise Prescient::Error, 'xAI provider does not support embeddings.'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Generate a response through xAI's OpenAI-compatible chat API.
|
|
24
|
+
# @param prompt [String] Prompt to send
|
|
25
|
+
# @param context_items [Array<Hash, String>] Optional context items
|
|
26
|
+
# @return [Hash] Normalized response data
|
|
27
|
+
def generate_response(prompt, context_items = [], **options)
|
|
28
|
+
handle_errors do
|
|
29
|
+
model = options[:model] || @options[:chat_model]
|
|
30
|
+
response = self.class.post(
|
|
31
|
+
'/v1/chat/completions',
|
|
32
|
+
headers: api_headers,
|
|
33
|
+
body: {
|
|
34
|
+
model: model,
|
|
35
|
+
messages: [{ role: 'user', content: build_prompt(prompt, context_items) }],
|
|
36
|
+
max_tokens: options[:max_tokens] || 2000,
|
|
37
|
+
temperature: options[:temperature] || 0.7,
|
|
38
|
+
top_p: options[:top_p] || 0.9,
|
|
39
|
+
}.to_json,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
validate_response!(response, 'text generation')
|
|
43
|
+
|
|
44
|
+
parsed_response = response.parsed_response
|
|
45
|
+
content = parsed_response.dig('choices', 0, 'message', 'content')
|
|
46
|
+
raise Prescient::InvalidResponseError, 'No response generated' unless content.is_a?(String) && !content.empty?
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
response: content.strip,
|
|
50
|
+
model: model,
|
|
51
|
+
provider: 'xai',
|
|
52
|
+
processing_time: nil,
|
|
53
|
+
metadata: {
|
|
54
|
+
usage: parsed_response['usage'],
|
|
55
|
+
finish_reason: parsed_response.dig('choices', 0, 'finish_reason'),
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check whether the configured xAI model is available.
|
|
62
|
+
# @return [Hash] Provider health information
|
|
63
|
+
def health_check
|
|
64
|
+
handle_errors do
|
|
65
|
+
response = self.class.get('/v1/models', headers: api_headers)
|
|
66
|
+
|
|
67
|
+
if response.success?
|
|
68
|
+
models = response.parsed_response['data'] || []
|
|
69
|
+
model_available = models.any? { |model| model['id'] == @options[:chat_model] }
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
status: 'healthy',
|
|
73
|
+
provider: 'xai',
|
|
74
|
+
reachable: true,
|
|
75
|
+
models_available: models.map { |model| model['id'] },
|
|
76
|
+
chat_model: { name: @options[:chat_model], available: model_available },
|
|
77
|
+
ready: model_available,
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
{
|
|
81
|
+
status: 'unhealthy',
|
|
82
|
+
provider: 'xai',
|
|
83
|
+
reachable: true,
|
|
84
|
+
error: "HTTP #{response.code}",
|
|
85
|
+
message: response.message,
|
|
86
|
+
ready: false,
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
rescue Prescient::Error => e
|
|
91
|
+
{
|
|
92
|
+
status: 'unavailable',
|
|
93
|
+
provider: 'xai',
|
|
94
|
+
reachable: false,
|
|
95
|
+
error: e.class.name,
|
|
96
|
+
message: e.message,
|
|
97
|
+
ready: false,
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# List models available to the configured xAI API key.
|
|
102
|
+
# @return [Array<Hash>] Model descriptors
|
|
103
|
+
def list_models
|
|
104
|
+
handle_errors do
|
|
105
|
+
response = self.class.get('/v1/models', headers: api_headers)
|
|
106
|
+
validate_response!(response, 'model listing')
|
|
107
|
+
|
|
108
|
+
(response.parsed_response['data'] || []).map do |model|
|
|
109
|
+
{
|
|
110
|
+
name: model['id'],
|
|
111
|
+
object: model['object'],
|
|
112
|
+
created: model['created'],
|
|
113
|
+
owned_by: model['owned_by'],
|
|
114
|
+
context_length: model['context_length'],
|
|
115
|
+
}.compact
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
protected
|
|
121
|
+
|
|
122
|
+
def validate_configuration!
|
|
123
|
+
required_options = [:api_key, :chat_model]
|
|
124
|
+
missing_options = required_options.select { |option| @options[option].nil? }
|
|
125
|
+
|
|
126
|
+
return unless missing_options.any?
|
|
127
|
+
|
|
128
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def api_headers
|
|
134
|
+
{
|
|
135
|
+
'Content-Type' => 'application/json',
|
|
136
|
+
'Authorization' => "Bearer #{@options[:api_key]}",
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/prescient/version.rb
CHANGED
data/lib/prescient.rb
CHANGED
|
@@ -8,6 +8,11 @@ require_relative 'prescient/provider/ollama'
|
|
|
8
8
|
require_relative 'prescient/provider/anthropic'
|
|
9
9
|
require_relative 'prescient/provider/openai'
|
|
10
10
|
require_relative 'prescient/provider/huggingface'
|
|
11
|
+
require_relative 'prescient/provider/gemini'
|
|
12
|
+
require_relative 'prescient/provider/mistral'
|
|
13
|
+
require_relative 'prescient/provider/deepseek'
|
|
14
|
+
require_relative 'prescient/provider/xai'
|
|
15
|
+
require_relative 'prescient/configuration_loader'
|
|
11
16
|
require_relative 'prescient/client'
|
|
12
17
|
require_relative 'prescient/cli'
|
|
13
18
|
|
|
@@ -61,6 +66,28 @@ module Prescient
|
|
|
61
66
|
@_configuration = Configuration.new
|
|
62
67
|
end
|
|
63
68
|
|
|
69
|
+
# Load configuration from a YAML file and replace the current configuration.
|
|
70
|
+
#
|
|
71
|
+
# The loaded configuration starts from the current environment defaults,
|
|
72
|
+
# then applies the YAML file, environment-variable references, and any
|
|
73
|
+
# optional overrides.
|
|
74
|
+
#
|
|
75
|
+
# @param path [String, nil] YAML configuration file path
|
|
76
|
+
# @param env [Hash] Environment variables used while loading configuration
|
|
77
|
+
# @return [Configuration] The loaded configuration
|
|
78
|
+
def self.load_configuration(path = nil, env: ENV)
|
|
79
|
+
effective_path = path || env['PRESCIENT_CONFIG']
|
|
80
|
+
configuration = if effective_path
|
|
81
|
+
ConfigurationLoader.load_file(effective_path, env:)
|
|
82
|
+
else
|
|
83
|
+
Configuration.new.tap do |config|
|
|
84
|
+
configure_default_providers(config, env)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
@_configuration = configuration
|
|
89
|
+
end
|
|
90
|
+
|
|
64
91
|
# Configuration class for managing Prescient settings and providers
|
|
65
92
|
#
|
|
66
93
|
# Handles global settings like timeouts and retry behavior, as well as
|
|
@@ -167,29 +194,62 @@ module Prescient
|
|
|
167
194
|
private
|
|
168
195
|
|
|
169
196
|
def configure_default_providers(config, env)
|
|
170
|
-
config
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
197
|
+
configure_ollama(config, env)
|
|
198
|
+
configure_openai(config, env)
|
|
199
|
+
configure_anthropic(config, env)
|
|
200
|
+
configure_gemini(config, env)
|
|
201
|
+
configure_mistral(config, env)
|
|
202
|
+
configure_deepseek(config, env)
|
|
203
|
+
configure_xai(config, env)
|
|
204
|
+
configure_huggingface(config, env)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def configure_ollama(config, env)
|
|
208
|
+
config.add_provider(
|
|
209
|
+
:ollama,
|
|
210
|
+
Prescient::Provider::Ollama,
|
|
211
|
+
url: env.fetch('OLLAMA_URL', 'http://localhost:11434'),
|
|
212
|
+
embedding_model: env.fetch('OLLAMA_EMBEDDING_MODEL', 'nomic-embed-text'),
|
|
213
|
+
chat_model: env.fetch('OLLAMA_CHAT_MODEL', 'llama3.2:3b'),
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def configure_openai(config, env)
|
|
218
|
+
return unless env['OPENAI_API_KEY']
|
|
219
|
+
|
|
220
|
+
config.add_provider(
|
|
221
|
+
:openai,
|
|
222
|
+
Prescient::Provider::OpenAI,
|
|
223
|
+
api_key: env['OPENAI_API_KEY'],
|
|
224
|
+
embedding_model: env.fetch('OPENAI_EMBEDDING_MODEL', 'text-embedding-3-small'),
|
|
225
|
+
chat_model: env.fetch('OPENAI_CHAT_MODEL', 'gpt-4.1-mini'),
|
|
226
|
+
)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def configure_anthropic(config, env)
|
|
230
|
+
return unless env['ANTHROPIC_API_KEY']
|
|
231
|
+
|
|
232
|
+
config.add_provider(
|
|
233
|
+
:anthropic,
|
|
234
|
+
Prescient::Provider::Anthropic,
|
|
235
|
+
api_key: env['ANTHROPIC_API_KEY'],
|
|
236
|
+
model: env.fetch('ANTHROPIC_MODEL', 'claude-sonnet-4-20250514'),
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def configure_gemini(config, env)
|
|
241
|
+
return unless env['GEMINI_API_KEY']
|
|
242
|
+
|
|
243
|
+
config.add_provider(
|
|
244
|
+
:gemini,
|
|
245
|
+
Prescient::Provider::Gemini,
|
|
246
|
+
api_key: env['GEMINI_API_KEY'],
|
|
247
|
+
embedding_model: env.fetch('GEMINI_EMBEDDING_MODEL', 'gemini-embedding-001'),
|
|
248
|
+
chat_model: env.fetch('GEMINI_CHAT_MODEL', 'gemini-2.5-flash'),
|
|
249
|
+
)
|
|
250
|
+
end
|
|
192
251
|
|
|
252
|
+
def configure_huggingface(config, env)
|
|
193
253
|
return unless env['HUGGINGFACE_API_KEY']
|
|
194
254
|
|
|
195
255
|
config.add_provider(
|
|
@@ -206,6 +266,40 @@ module Prescient
|
|
|
206
266
|
),
|
|
207
267
|
)
|
|
208
268
|
end
|
|
269
|
+
|
|
270
|
+
def configure_deepseek(config, env)
|
|
271
|
+
return unless env['DEEPSEEK_API_KEY']
|
|
272
|
+
|
|
273
|
+
config.add_provider(
|
|
274
|
+
:deepseek,
|
|
275
|
+
Prescient::Provider::DeepSeek,
|
|
276
|
+
api_key: env['DEEPSEEK_API_KEY'],
|
|
277
|
+
chat_model: env.fetch('DEEPSEEK_CHAT_MODEL', 'deepseek-v4-flash'),
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def configure_xai(config, env)
|
|
282
|
+
return unless env['XAI_API_KEY']
|
|
283
|
+
|
|
284
|
+
config.add_provider(
|
|
285
|
+
:xai,
|
|
286
|
+
Prescient::Provider::XAI,
|
|
287
|
+
api_key: env['XAI_API_KEY'],
|
|
288
|
+
chat_model: env.fetch('XAI_CHAT_MODEL', 'grok-4.5'),
|
|
289
|
+
)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def configure_mistral(config, env)
|
|
293
|
+
return unless env['MISTRAL_API_KEY']
|
|
294
|
+
|
|
295
|
+
config.add_provider(
|
|
296
|
+
:mistral,
|
|
297
|
+
Prescient::Provider::Mistral,
|
|
298
|
+
api_key: env['MISTRAL_API_KEY'],
|
|
299
|
+
embedding_model: env.fetch('MISTRAL_EMBEDDING_MODEL', 'mistral-embed'),
|
|
300
|
+
chat_model: env.fetch('MISTRAL_CHAT_MODEL', 'mistral-large-latest'),
|
|
301
|
+
)
|
|
302
|
+
end
|
|
209
303
|
end
|
|
210
304
|
|
|
211
305
|
# Default configuration
|