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,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
class Prescient::Provider::Ollama < Prescient::Base
|
6
|
+
include HTTParty
|
7
|
+
|
8
|
+
EMBEDDING_DIMENSIONS = 768 # nomic-embed-text dimensions
|
9
|
+
|
10
|
+
def initialize(**options)
|
11
|
+
super
|
12
|
+
self.class.base_uri(@options[:url])
|
13
|
+
self.class.default_timeout(@options[:timeout] || 60)
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_embedding(text, **_options)
|
17
|
+
handle_errors do
|
18
|
+
embedding = fetch_and_parse('post', '/api/embeddings',
|
19
|
+
root_key: 'embedding',
|
20
|
+
headers: { 'Content-Type' => 'application/json' },
|
21
|
+
body: {
|
22
|
+
model: @options[:embedding_model],
|
23
|
+
prompt: clean_text(text),
|
24
|
+
}.to_json)
|
25
|
+
|
26
|
+
raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding
|
27
|
+
|
28
|
+
normalize_embedding(embedding, EMBEDDING_DIMENSIONS)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_response(prompt, context_items = [], **options)
|
33
|
+
handle_errors do
|
34
|
+
request_options = prepare_generate_response(prompt, context_items, **options)
|
35
|
+
|
36
|
+
# Make the request and store both text and full response
|
37
|
+
response = self.class.post('/api/generate', **request_options)
|
38
|
+
validate_response!(response, 'POST /api/generate')
|
39
|
+
|
40
|
+
generated_text = response.parsed_response['response']
|
41
|
+
raise Prescient::InvalidResponseError, 'No response generated' unless generated_text
|
42
|
+
|
43
|
+
{
|
44
|
+
response: generated_text.strip,
|
45
|
+
model: @options[:chat_model],
|
46
|
+
provider: 'ollama',
|
47
|
+
processing_time: response.parsed_response['total_duration']&./(1_000_000_000.0),
|
48
|
+
metadata: {
|
49
|
+
eval_count: response.parsed_response['eval_count'],
|
50
|
+
eval_duration: response.parsed_response['eval_duration'],
|
51
|
+
prompt_eval_count: response.parsed_response['prompt_eval_count'],
|
52
|
+
},
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def health_check
|
58
|
+
handle_errors do
|
59
|
+
models = available_models
|
60
|
+
embedding_available = models.any? { |m| m[:embedding] }
|
61
|
+
chat_available = models.any? { |m| m[:chat] }
|
62
|
+
|
63
|
+
{
|
64
|
+
status: 'healthy',
|
65
|
+
provider: 'ollama',
|
66
|
+
url: @options[:url],
|
67
|
+
models_available: models.map { |m| m[:name] },
|
68
|
+
embedding_model: {
|
69
|
+
name: @options[:embedding_model],
|
70
|
+
available: embedding_available,
|
71
|
+
},
|
72
|
+
chat_model: {
|
73
|
+
name: @options[:chat_model],
|
74
|
+
available: chat_available,
|
75
|
+
},
|
76
|
+
ready: embedding_available && chat_available,
|
77
|
+
}
|
78
|
+
end
|
79
|
+
rescue Prescient::Error => e
|
80
|
+
{
|
81
|
+
status: 'unavailable',
|
82
|
+
provider: 'ollama',
|
83
|
+
error: e.class.name,
|
84
|
+
message: e.message,
|
85
|
+
url: @options[:url],
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
def available_models
|
90
|
+
return @_available_models if defined?(@_available_models)
|
91
|
+
|
92
|
+
handle_errors do
|
93
|
+
@_available_models = (fetch_and_parse('get', '/api/tags', root_key: 'models') || []).map { |model|
|
94
|
+
{ embedding: model['name'] == @options[:embedding_model],
|
95
|
+
chat: model['name'] == @options[:chat_model],
|
96
|
+
name: model['name'], size: model['size'], modified_at: model['modified_at'], digest: model['digest'] }
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def pull_model(model_name)
|
102
|
+
handle_errors do
|
103
|
+
fetch_and_parse('post', '/api/pull',
|
104
|
+
headers: { 'Content-Type' => 'application/json' },
|
105
|
+
body: { name: model_name }.to_json,
|
106
|
+
timeout: 300) # 5 minutes for model download
|
107
|
+
{
|
108
|
+
success: true,
|
109
|
+
model: model_name,
|
110
|
+
message: "Model #{model_name} pulled successfully",
|
111
|
+
}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
protected
|
116
|
+
|
117
|
+
def validate_configuration!
|
118
|
+
required_options = [:url, :embedding_model, :chat_model]
|
119
|
+
missing_options = required_options.select { |opt| @options[opt].nil? }
|
120
|
+
|
121
|
+
return unless missing_options.any?
|
122
|
+
|
123
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def prepare_generate_response(prompt, context_items = [], **options)
|
129
|
+
formatted_prompt = build_prompt(prompt, context_items)
|
130
|
+
{ root_key: 'response',
|
131
|
+
headers: { 'Content-Type' => 'application/json' },
|
132
|
+
body: {
|
133
|
+
model: @options[:chat_model],
|
134
|
+
prompt: formatted_prompt,
|
135
|
+
stream: false,
|
136
|
+
options: {
|
137
|
+
num_predict: options[:max_tokens] || 2000,
|
138
|
+
temperature: options[:temperature] || 0.7,
|
139
|
+
top_p: options[:top_p] || 0.9,
|
140
|
+
},
|
141
|
+
}.to_json }
|
142
|
+
end
|
143
|
+
|
144
|
+
def fetch_and_parse(htt_verb, endpoint, **options)
|
145
|
+
options = options.dup
|
146
|
+
root_key = options.delete(:root_key)
|
147
|
+
|
148
|
+
response = self.class.send(htt_verb, endpoint, **options)
|
149
|
+
validate_response!(response, "#{htt_verb.upcase} #{endpoint}")
|
150
|
+
return unless root_key
|
151
|
+
|
152
|
+
response.parsed_response[root_key]
|
153
|
+
end
|
154
|
+
|
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
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
class Prescient::Provider::OpenAI < Prescient::Base
|
6
|
+
include HTTParty
|
7
|
+
|
8
|
+
base_uri 'https://api.openai.com'
|
9
|
+
|
10
|
+
EMBEDDING_DIMENSIONS = {
|
11
|
+
'text-embedding-3-small' => 1536,
|
12
|
+
'text-embedding-3-large' => 3072,
|
13
|
+
'text-embedding-ada-002' => 1536,
|
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('/v1/embeddings',
|
26
|
+
headers: {
|
27
|
+
'Content-Type' => 'application/json',
|
28
|
+
'Authorization' => "Bearer #{@options[:api_key]}",
|
29
|
+
},
|
30
|
+
body: {
|
31
|
+
model: @options[:embedding_model],
|
32
|
+
input: clean_text_input,
|
33
|
+
encoding_format: 'float',
|
34
|
+
}.to_json)
|
35
|
+
|
36
|
+
validate_response!(response, 'embedding generation')
|
37
|
+
|
38
|
+
embedding_data = response.parsed_response.dig('data', 0, 'embedding')
|
39
|
+
raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding_data
|
40
|
+
|
41
|
+
expected_dimensions = EMBEDDING_DIMENSIONS[@options[:embedding_model]] || 1536
|
42
|
+
normalize_embedding(embedding_data, expected_dimensions)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_response(prompt, context_items = [], **options)
|
47
|
+
handle_errors do
|
48
|
+
formatted_prompt = build_prompt(prompt, context_items)
|
49
|
+
|
50
|
+
response = self.class.post('/v1/chat/completions',
|
51
|
+
headers: {
|
52
|
+
'Content-Type' => 'application/json',
|
53
|
+
'Authorization' => "Bearer #{@options[:api_key]}",
|
54
|
+
},
|
55
|
+
body: {
|
56
|
+
model: @options[:chat_model],
|
57
|
+
messages: [
|
58
|
+
{
|
59
|
+
role: 'user',
|
60
|
+
content: formatted_prompt,
|
61
|
+
},
|
62
|
+
],
|
63
|
+
max_tokens: options[:max_tokens] || 2000,
|
64
|
+
temperature: options[:temperature] || 0.7,
|
65
|
+
top_p: options[:top_p] || 0.9,
|
66
|
+
}.to_json)
|
67
|
+
|
68
|
+
validate_response!(response, 'text generation')
|
69
|
+
|
70
|
+
content = response.parsed_response.dig('choices', 0, 'message', 'content')
|
71
|
+
raise Prescient::InvalidResponseError, 'No response generated' unless content
|
72
|
+
|
73
|
+
{
|
74
|
+
response: content.strip,
|
75
|
+
model: @options[:chat_model],
|
76
|
+
provider: 'openai',
|
77
|
+
processing_time: nil,
|
78
|
+
metadata: {
|
79
|
+
usage: response.parsed_response['usage'],
|
80
|
+
finish_reason: response.parsed_response.dig('choices', 0, 'finish_reason'),
|
81
|
+
},
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def health_check
|
87
|
+
handle_errors do
|
88
|
+
response = self.class.get('/v1/models',
|
89
|
+
headers: {
|
90
|
+
'Authorization' => "Bearer #{@options[:api_key]}",
|
91
|
+
})
|
92
|
+
|
93
|
+
if response.success?
|
94
|
+
models = response.parsed_response['data'] || []
|
95
|
+
embedding_available = models.any? { |m| m['id'] == @options[:embedding_model] }
|
96
|
+
chat_available = models.any? { |m| m['id'] == @options[:chat_model] }
|
97
|
+
|
98
|
+
{
|
99
|
+
status: 'healthy',
|
100
|
+
provider: 'openai',
|
101
|
+
models_available: models.map { |m| m['id'] },
|
102
|
+
embedding_model: {
|
103
|
+
name: @options[:embedding_model],
|
104
|
+
available: embedding_available,
|
105
|
+
},
|
106
|
+
chat_model: {
|
107
|
+
name: @options[:chat_model],
|
108
|
+
available: chat_available,
|
109
|
+
},
|
110
|
+
ready: embedding_available && chat_available,
|
111
|
+
}
|
112
|
+
else
|
113
|
+
{
|
114
|
+
status: 'unhealthy',
|
115
|
+
provider: 'openai',
|
116
|
+
error: "HTTP #{response.code}",
|
117
|
+
message: response.message,
|
118
|
+
}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
rescue Prescient::Error => e
|
122
|
+
{
|
123
|
+
status: 'unavailable',
|
124
|
+
provider: 'openai',
|
125
|
+
error: e.class.name,
|
126
|
+
message: e.message,
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def list_models
|
131
|
+
handle_errors do
|
132
|
+
response = self.class.get('/v1/models',
|
133
|
+
headers: {
|
134
|
+
'Authorization' => "Bearer #{@options[:api_key]}",
|
135
|
+
})
|
136
|
+
validate_response!(response, 'model listing')
|
137
|
+
|
138
|
+
models = response.parsed_response['data'] || []
|
139
|
+
models.map do |model|
|
140
|
+
{
|
141
|
+
name: model['id'],
|
142
|
+
created: model['created'],
|
143
|
+
owned_by: model['owned_by'],
|
144
|
+
}
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
protected
|
150
|
+
|
151
|
+
def validate_configuration!
|
152
|
+
required_options = [:api_key, :embedding_model, :chat_model]
|
153
|
+
missing_options = required_options.select { |opt| @options[opt].nil? }
|
154
|
+
|
155
|
+
return unless missing_options.any?
|
156
|
+
|
157
|
+
raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
|
158
|
+
end
|
159
|
+
|
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
|
+
end
|
data/lib/prescient/version.rb
CHANGED
data/lib/prescient.rb
CHANGED
@@ -1,8 +1,192 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
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
|
7
|
-
|
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
|
+
module Prescient
|
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]
|
75
|
+
def self.configure
|
76
|
+
yield(configuration)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get the current configuration instance
|
80
|
+
#
|
81
|
+
# @return [Configuration] The current configuration
|
82
|
+
def self.configuration
|
83
|
+
@_configuration ||= Configuration.new
|
84
|
+
end
|
85
|
+
|
86
|
+
# Reset configuration to defaults
|
87
|
+
#
|
88
|
+
# @return [Configuration] New configuration instance
|
89
|
+
def self.reset_configuration!
|
90
|
+
@_configuration = Configuration.new
|
91
|
+
end
|
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.
|
97
|
+
class Configuration
|
98
|
+
# @return [Symbol] The default provider to use when none specified
|
99
|
+
attr_accessor :default_provider
|
100
|
+
|
101
|
+
# @return [Integer] Default timeout in seconds for API requests
|
102
|
+
attr_accessor :timeout
|
103
|
+
|
104
|
+
# @return [Integer] Number of retry attempts for failed requests
|
105
|
+
attr_accessor :retry_attempts
|
106
|
+
|
107
|
+
# @return [Float] Delay between retry attempts in seconds
|
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
|
114
|
+
attr_reader :providers
|
115
|
+
|
116
|
+
# Initialize configuration with default values
|
117
|
+
def initialize
|
118
|
+
@default_provider = :ollama
|
119
|
+
@timeout = 30
|
120
|
+
@retry_attempts = 3
|
121
|
+
@retry_delay = 1.0
|
122
|
+
@fallback_providers = []
|
123
|
+
@providers = {}
|
124
|
+
end
|
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')
|
141
|
+
def add_provider(name, provider_class, **options)
|
142
|
+
@providers[name.to_sym] = {
|
143
|
+
class: provider_class,
|
144
|
+
options: options,
|
145
|
+
}
|
146
|
+
end
|
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
|
152
|
+
def provider(name)
|
153
|
+
provider_config = @providers[name.to_sym]
|
154
|
+
return nil unless provider_config
|
155
|
+
|
156
|
+
provider_config[:class].new(**provider_config[:options])
|
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
|
169
|
+
end
|
170
|
+
|
171
|
+
# Default configuration
|
172
|
+
configure do |config|
|
173
|
+
config.add_provider(:ollama, Prescient::Provider::Ollama,
|
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'))
|
191
|
+
end
|
8
192
|
end
|
data/prescient.gemspec
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/prescient/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "prescient"
|
7
|
+
spec.version = Prescient::VERSION
|
8
|
+
spec.authors = ["Ken C. Demanawa"]
|
9
|
+
spec.email = ["kenneth.c.demanawa@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Prescient AI provider abstraction for Ruby applications"
|
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
|
+
spec.homepage = "https://github.com/yourcompany/prescient"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 3.1.0"
|
16
|
+
|
17
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = "https://github.com/yourcompany/prescient"
|
20
|
+
spec.metadata["changelog_uri"] = "https://github.com/yourcompany/prescient/blob/main/CHANGELOG.md"
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(File.expand_path(f) == __FILE__) ||
|
26
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
# Runtime dependencies
|
34
|
+
spec.add_dependency "httparty", "~> 0.23.1"
|
35
|
+
|
36
|
+
# Optional dependencies for vector database integration
|
37
|
+
spec.add_development_dependency "pg", "~> 1.6" # PostgreSQL adapter for pgvector integration
|
38
|
+
|
39
|
+
# Development dependencies
|
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"
|
53
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# Setup script for pulling required Ollama models for Prescient gem
|
3
|
+
|
4
|
+
set -e
|
5
|
+
|
6
|
+
OLLAMA_URL=${OLLAMA_URL:-"http://localhost:11434"}
|
7
|
+
EMBEDDING_MODEL=${OLLAMA_EMBEDDING_MODEL:-"nomic-embed-text"}
|
8
|
+
CHAT_MODEL=${OLLAMA_CHAT_MODEL:-"llama3.1:8b"}
|
9
|
+
|
10
|
+
echo "🚀 Setting up Ollama models for Prescient gem..."
|
11
|
+
echo "Ollama URL: $OLLAMA_URL"
|
12
|
+
|
13
|
+
# Function to check if Ollama is ready
|
14
|
+
wait_for_ollama() {
|
15
|
+
echo "⏳ Waiting for Ollama to be ready..."
|
16
|
+
local max_attempts=30
|
17
|
+
local attempt=1
|
18
|
+
|
19
|
+
while [ $attempt -le $max_attempts ]; do
|
20
|
+
if curl -s "$OLLAMA_URL/api/tags" > /dev/null 2>&1; then
|
21
|
+
echo "✅ Ollama is ready!"
|
22
|
+
return 0
|
23
|
+
fi
|
24
|
+
|
25
|
+
echo "Attempt $attempt/$max_attempts - Ollama not ready yet..."
|
26
|
+
sleep 2
|
27
|
+
((attempt++))
|
28
|
+
done
|
29
|
+
|
30
|
+
echo "❌ Ollama failed to start within expected time"
|
31
|
+
exit 1
|
32
|
+
}
|
33
|
+
|
34
|
+
# Function to pull a model
|
35
|
+
pull_model() {
|
36
|
+
local model_name=$1
|
37
|
+
echo "📦 Pulling model: $model_name"
|
38
|
+
|
39
|
+
if curl -s -X POST "$OLLAMA_URL/api/pull" \
|
40
|
+
-H "Content-Type: application/json" \
|
41
|
+
-d "{\"name\": \"$model_name\"}" | grep -q "success"; then
|
42
|
+
echo "✅ Successfully pulled $model_name"
|
43
|
+
else
|
44
|
+
echo "⚠️ Model pull initiated for $model_name (this may take a while)"
|
45
|
+
# Wait a bit and check if model appears in list
|
46
|
+
sleep 5
|
47
|
+
fi
|
48
|
+
}
|
49
|
+
|
50
|
+
# Function to list available models
|
51
|
+
list_models() {
|
52
|
+
echo "📋 Available models:"
|
53
|
+
curl -s "$OLLAMA_URL/api/tags" | jq -r '.models[]?.name // empty' 2>/dev/null || echo "Unable to list models"
|
54
|
+
}
|
55
|
+
|
56
|
+
# Main execution
|
57
|
+
main() {
|
58
|
+
wait_for_ollama
|
59
|
+
|
60
|
+
echo "🔧 Current models:"
|
61
|
+
list_models
|
62
|
+
|
63
|
+
echo "📥 Pulling required models..."
|
64
|
+
pull_model "$EMBEDDING_MODEL"
|
65
|
+
pull_model "$CHAT_MODEL"
|
66
|
+
|
67
|
+
echo "✨ Model setup complete!"
|
68
|
+
echo "📋 Final model list:"
|
69
|
+
list_models
|
70
|
+
|
71
|
+
echo ""
|
72
|
+
echo "🎉 Ollama is ready for use with Prescient gem!"
|
73
|
+
echo "💡 You can now run the examples with:"
|
74
|
+
echo " OLLAMA_URL=$OLLAMA_URL ruby examples/custom_contexts.rb"
|
75
|
+
}
|
76
|
+
|
77
|
+
main "$@"
|