Class: Prescient::Base Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/prescient/base.rb

Overview

This class is abstract.

Base class for all AI provider implementations

This abstract base class defines the common interface that all AI providers must implement. It provides shared functionality for text processing, context formatting, prompt building, and error handling.

Examples:

Creating a custom provider

class MyProvider < Prescient::Base
  def generate_embedding(text, **options)
    # Implementation here
  end

  def generate_response(prompt, context_items = [], **options)
    # Implementation here
  end

  def health_check
    # Implementation here
  end
end

Author:

  • Claude Code

Since:

  • 1.0.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**options) ⇒ Base

Initialize the provider with configuration options

Parameters:

  • options (Hash)

    Provider-specific configuration options

Options Hash (**options):

  • :api_key (String)

    API key for authenticated providers

  • :url (String)

    Base URL for self-hosted providers

  • :timeout (Integer)

    Request timeout in seconds

  • :prompt_templates (Hash)

    Custom prompt templates

  • :context_configs (Hash)

    Context formatting configurations

Since:

  • 1.0.0



41
42
43
44
# File 'lib/prescient/base.rb', line 41

def initialize(**options)
  @options = options
  validate_configuration!
end

Instance Attribute Details

#optionsHash (readonly)

Returns Configuration options for this provider instance.

Returns:

  • (Hash)

    Configuration options for this provider instance

Since:

  • 1.0.0



31
32
33
# File 'lib/prescient/base.rb', line 31

def options
  @options
end

Instance Method Details

#apply_format_template(template, format_data) ⇒ Object (private)

Apply format template with error handling

Since:

  • 1.0.0



313
314
315
316
317
# File 'lib/prescient/base.rb', line 313

def apply_format_template(template, format_data)
  template % format_data
rescue KeyError
  nil
end

#available?Boolean

Check if the provider is currently available

Returns:

  • (Boolean)

    true if provider is healthy and available

Since:

  • 1.0.0



93
94
95
96
97
# File 'lib/prescient/base.rb', line 93

def available?
  health_check[:status] == 'healthy'
rescue StandardError
  false
end

#build_format_data(item, config) ⇒ Object (private)

Build format data from item fields

Since:

  • 1.0.0



300
301
302
303
304
305
306
307
308
309
310
# File 'lib/prescient/base.rb', line 300

def build_format_data(item, config)
  format_data = {}
  fields_to_check = config[:fields].any? ? config[:fields] : item.keys.map(&:to_s)

  fields_to_check.each do |field|
    value = item[field] || item[field.to_sym]
    format_data[field.to_sym] = value if value
  end

  format_data
end

#build_prompt(query, context_items = []) ⇒ String (protected)

Build formatted prompt from query and context items

Creates a properly formatted prompt using configurable templates, incorporating context items when provided.

Parameters:

  • query (String)

    The user's question or prompt

  • context_items (Array<Hash, String>) (defaults to: [])

    Optional context items

Returns:

  • (String)

    Formatted prompt ready for AI processing

Since:

  • 1.0.0



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/prescient/base.rb', line 203

def build_prompt(query, context_items = [])
  templates = default_prompt_templates.merge(@options[:prompt_templates] || {})
  system_prompt = templates[:system_prompt]

  if context_items.empty?
    templates[:no_context_template] % {
      system_prompt: system_prompt,
      query:         query,
    }
  else
    context_text = context_items.map.with_index(1) { |item, index|
      "#{index}. #{format_context_item(item)}"
    }.join("\n\n")

    templates[:with_context_template] % {
      system_prompt: system_prompt,
      context:       context_text,
      query:         query,
    }
  end
end

#calculate_field_match_score(item_fields, config_fields) ⇒ Object (private)

Calculate field matching score

Since:

  • 1.0.0



362
363
364
365
366
367
# File 'lib/prescient/base.rb', line 362

def calculate_field_match_score(item_fields, config_fields)
  return 0 if config_fields.empty?

  matching_fields = (item_fields & config_fields).size
  matching_fields.to_f / config_fields.size
end

#clean_text(text) ⇒ String (protected)

Clean and preprocess text for AI processing

Removes excess whitespace, normalizes spacing, and enforces length limits suitable for most AI models.

Parameters:

  • text (String, nil)

    The text to clean

Returns:

  • (String)

    Cleaned text, empty string if input was nil/empty

Since:

  • 1.0.0



160
161
162
163
# File 'lib/prescient/base.rb', line 160

def clean_text(text)
  # Limit length for most models
  text.to_s.gsub(/\s+/, ' ').strip.slice(0, 8000)
end

#default_context_configsObject (protected)

Minimal default context configuration - users should define their own contexts

Since:

  • 1.0.0



226
227
228
229
230
231
232
233
234
235
# File 'lib/prescient/base.rb', line 226

def default_context_configs
  {
    # Generic fallback configuration - works with any hash structure
    'default' => {
      fields:           [], # Will be dynamically determined from item keys
      format:           nil, # Will use fallback formatting
      embedding_fields: [], # Will use all string/text fields
    },
  }
end

#default_prompt_templatesHash (protected)

Get default prompt templates

Provides standard templates for system prompts and context handling that can be overridden via provider options.

Returns:

  • (Hash)

    Hash containing template strings with placeholders

Since:

  • 1.0.0



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/prescient/base.rb', line 172

def default_prompt_templates
  {
    system_prompt:         'You are a helpful AI assistant. Answer questions clearly and accurately.',
    no_context_template:   "      %<system_prompt>s\n\n      Question: %<query>s\n\n      Please provide a helpful response based on your knowledge.\n    TEMPLATE\n    with_context_template: <<~TEMPLATE.strip,\n      %<system_prompt>s Use the following context to answer the question. If the context doesn't contain relevant information, say so clearly.\n\n      Context:\n      %<context>s\n\n      Question: %<query>s\n\n      Please provide a helpful response based on the context above.\n    TEMPLATE\n  }\nend\n".strip,

#detect_context_type(item) ⇒ Object (private)

Detect context type from item structure

Since:

  • 1.0.0



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/prescient/base.rb', line 320

def detect_context_type(item)
  return 'default' unless item.is_a?(Hash)

  # Check for explicit type fields (user-defined)
  return item['type'].to_s if item['type']
  return item['context_type'].to_s if item['context_type']
  return item['model_type'].to_s.downcase if item['model_type']

  # If no explicit type and user has configured contexts, try to match
  context_configs = @options[:context_configs] || {}
  return match_context_by_fields(item, context_configs) if context_configs.any?

  # Default fallback
  'default'
end

#extract_configured_fields(item, config) ⇒ Object (private)

Extract fields configured for embeddings

Since:

  • 1.0.0



282
283
284
285
286
# File 'lib/prescient/base.rb', line 282

def extract_configured_fields(item, config)
  return nil unless config[:embedding_fields]&.any?

  config[:embedding_fields].filter_map { |field| item[field] || item[field.to_sym] }
end

#extract_embedding_text(item, context_type = nil) ⇒ Object (protected)

Extract text for embedding generation based on context configuration

Since:

  • 1.0.0



238
239
240
241
242
243
244
# File 'lib/prescient/base.rb', line 238

def extract_embedding_text(item, context_type = nil)
  return item.to_s unless item.is_a?(Hash)

  config = resolve_context_config(item, context_type)
  text_values = extract_configured_fields(item, config) || extract_text_values(item)
  text_values.join(' ').strip
end

#extract_text_values(item) ⇒ Object (protected)

Extract text values from hash, excluding non-textual fields

Since:

  • 1.0.0



247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/prescient/base.rb', line 247

def extract_text_values(item)
  # Common fields to exclude from embedding text
  # TODO: configurable fields to exclude aside from the common ones below
  exclude_fields = ['id', '_id', 'uuid', 'created_at', 'updated_at', 'timestamp', 'version', 'status', 'active']

  item.filter_map { |key, value|
    next if exclude_fields.include?(key.to_s.downcase)
    next unless value.is_a?(String) || value.is_a?(Numeric)
    next if value.to_s.strip.empty?

    value.to_s
  }
end

#fallback_format_hash(item, format_data = nil) ⇒ Object (private)

Fallback formatting for hash items

Since:

  • 1.0.0



370
371
372
373
# File 'lib/prescient/base.rb', line 370

def fallback_format_hash(item, format_data = nil)
  # Fallback: join key-value pairs
  (format_data || item).map { |k, v| "#{k}: #{v}" }.join(', ')
end

#find_best_field_match(item_fields, context_configs) ⇒ Object (private)

Find the best matching context configuration

Since:

  • 1.0.0



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/prescient/base.rb', line 344

def find_best_field_match(item_fields, context_configs)
  best_match = nil
  best_score = 0

  context_configs.each do |context_type, config|
    next unless config[:fields]&.any?

    score = calculate_field_match_score(item_fields, config[:fields])
    next unless score >= 0.5 && score > best_score

    best_match = context_type
    best_score = score
  end

  best_match
end

#format_context_item(item) ⇒ Object (protected)

Generic context item formatting using configurable contexts

Since:

  • 1.0.0



262
263
264
265
266
267
268
# File 'lib/prescient/base.rb', line 262

def format_context_item(item)
  case item
  when Hash then format_hash_item(item)
  when String then item
  else item.to_s
  end
end

#format_hash_item(item) ⇒ Object (private)

Format a hash item using context configuration

Since:

  • 1.0.0



289
290
291
292
293
294
295
296
297
# File 'lib/prescient/base.rb', line 289

def format_hash_item(item)
  config = resolve_context_config(item, nil)
  return fallback_format_hash(item) unless config[:format]

  format_data = build_format_data(item, config)
  return fallback_format_hash(item) unless format_data.any?

  apply_format_template(config[:format], format_data) || fallback_format_hash(item)
end

#generate_embedding(text, **options) ⇒ Array<Float>

This method is abstract.

Generate embeddings for the given text

This method must be implemented by subclasses to provide embedding generation functionality.

Parameters:

  • text (String)

    The text to generate embeddings for

  • options (Hash)

    Provider-specific options

Returns:

  • (Array<Float>)

    Array of embedding values

Raises:

  • (NotImplementedError)

    If not implemented by subclass

Since:

  • 1.0.0



56
57
58
# File 'lib/prescient/base.rb', line 56

def generate_embedding(text, **options)
  raise NotImplementedError, "#{self.class} must implement #generate_embedding"
end

#generate_response(prompt, context_items = [], **options) ⇒ Hash

This method is abstract.

Generate text response for the given prompt

This method must be implemented by subclasses to provide text generation functionality with optional context items.

Parameters:

  • prompt (String)

    The prompt to generate a response for

  • context_items (Array<Hash, String>) (defaults to: [])

    Optional context items to include

  • options (Hash)

    Provider-specific generation options

Options Hash (**options):

  • :temperature (Float)

    Sampling temperature (0.0-2.0)

  • :max_tokens (Integer)

    Maximum tokens to generate

  • :top_p (Float)

    Nucleus sampling parameter

Returns:

  • (Hash)

    Response hash with :response, :model, :provider keys

Raises:

  • (NotImplementedError)

    If not implemented by subclass

Since:

  • 1.0.0



74
75
76
# File 'lib/prescient/base.rb', line 74

def generate_response(prompt, context_items = [], **options)
  raise NotImplementedError, "#{self.class} must implement #generate_response"
end

#handle_errors { ... } ⇒ Object (protected)

Handle and standardize errors from provider operations

Wraps provider-specific operations and converts common exceptions into standardized Prescient error types while preserving existing Prescient errors.

Yields:

  • The operation block to execute with error handling

Returns:

  • (Object)

    The result of the yielded block

Raises:

Since:

  • 1.0.0



123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/prescient/base.rb', line 123

def handle_errors
  yield
rescue Prescient::Error
  # Re-raise Prescient errors without wrapping
  raise
rescue Net::ReadTimeout, Net::OpenTimeout => e
  raise Prescient::ConnectionError, "Request timeout: #{e.message}"
rescue Net::HTTPError => e
  raise Prescient::ConnectionError, "HTTP error: #{e.message}"
rescue JSON::ParserError => e
  raise Prescient::InvalidResponseError, "Invalid JSON response: #{e.message}"
rescue StandardError => e
  raise Prescient::Error, "Unexpected error: #{e.message}"
end

#health_checkHash

This method is abstract.

Check the health and availability of the provider

This method must be implemented by subclasses to provide health check functionality.

Returns:

  • (Hash)

    Health status with :status, :provider keys and optional details

Raises:

  • (NotImplementedError)

    If not implemented by subclass

Since:

  • 1.0.0



86
87
88
# File 'lib/prescient/base.rb', line 86

def health_check
  raise NotImplementedError, "#{self.class} must implement #health_check"
end

#match_context_by_fields(item, context_configs) ⇒ Object (private)

Match context type based on configured field patterns

Since:

  • 1.0.0



337
338
339
340
341
# File 'lib/prescient/base.rb', line 337

def match_context_by_fields(item, context_configs)
  item_fields = item.keys.map(&:to_s)
  best_match = find_best_field_match(item_fields, context_configs)
  best_match || 'default'
end

#normalize_embedding(embedding, target_dimensions) ⇒ Array<Float>? (protected)

Normalize embedding dimensions to match expected size

Ensures embedding vectors have consistent dimensions by truncating longer vectors or padding shorter ones with zeros.

Parameters:

  • embedding (Array<Float>)

    The embedding vector to normalize

  • target_dimensions (Integer)

    The desired number of dimensions

Returns:

  • (Array<Float>, nil)

    Normalized embedding or nil if input invalid

Since:

  • 1.0.0



146
147
148
149
150
151
# File 'lib/prescient/base.rb', line 146

def normalize_embedding(embedding, target_dimensions)
  return nil unless embedding.is_a?(Array)
  return embedding.first(target_dimensions) if embedding.length >= target_dimensions

  embedding + Array.new(target_dimensions - embedding.length, 0.0)
end

#resolve_context_config(item, context_type) ⇒ Object (private)

Resolve context configuration for an item

Since:

  • 1.0.0



273
274
275
276
277
278
279
# File 'lib/prescient/base.rb', line 273

def resolve_context_config(item, context_type)
  context_configs = default_context_configs.merge(@options[:context_configs] || {})
  return context_configs['default'] if context_configs.empty?

  detected_type = context_type || detect_context_type(item)
  context_configs[detected_type] || context_configs['default']
end

#validate_configuration!void (protected)

This method returns an undefined value.

Validate provider configuration

Override this method in subclasses to validate required configuration options and raise appropriate errors for missing or invalid settings.

Raises:

Since:

  • 1.0.0



108
109
110
# File 'lib/prescient/base.rb', line 108

def validate_configuration!
  # Override in subclasses to validate required configuration
end