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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prescient
4
- VERSION = "0.0.0"
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/prescient.rb CHANGED
@@ -1,8 +1,192 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "prescient/version"
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
- # Your code goes here...
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 "$@"