Class: Prescient::Provider::OpenAI

Inherits:
Base
  • Object
show all
Includes:
HTTParty
Defined in:
lib/prescient/provider/openai.rb

Overview

Since:

  • 1.0.0

Constant Summary collapse

EMBEDDING_DIMENSIONS =

Since:

  • 1.0.0

{
  'text-embedding-3-small' => 1536,
  'text-embedding-3-large' => 3072,
  'text-embedding-ada-002' => 1536,
}.freeze

Instance Attribute Summary

Attributes inherited from Base

#options

Instance Method Summary collapse

Methods inherited from Base

#apply_format_template, #available?, #build_format_data, #build_prompt, #calculate_field_match_score, #clean_text, #default_context_configs, #default_prompt_templates, #detect_context_type, #extract_configured_fields, #extract_embedding_text, #extract_text_values, #fallback_format_hash, #find_best_field_match, #format_context_item, #format_hash_item, #handle_errors, #match_context_by_fields, #normalize_embedding, #resolve_context_config

Constructor Details

#initialize(**options) ⇒ OpenAI

Returns a new instance of OpenAI.

Since:

  • 1.0.0



16
17
18
19
# File 'lib/prescient/provider/openai.rb', line 16

def initialize(**options)
  super
  self.class.default_timeout(@options[:timeout] || 60)
end

Instance Method Details

#generate_embedding(text, **_options) ⇒ Object

Since:

  • 1.0.0



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/prescient/provider/openai.rb', line 21

def generate_embedding(text, **_options)
  handle_errors do
    clean_text_input = clean_text(text)

    response = self.class.post('/v1/embeddings',
                               headers: {
                                 'Content-Type'  => 'application/json',
                                 'Authorization' => "Bearer #{@options[:api_key]}",
                               },
                               body:    {
                                 model:           @options[:embedding_model],
                                 input:           clean_text_input,
                                 encoding_format: 'float',
                               }.to_json)

    validate_response!(response, 'embedding generation')

    embedding_data = response.parsed_response.dig('data', 0, 'embedding')
    raise Prescient::InvalidResponseError, 'No embedding returned' unless embedding_data

    expected_dimensions = EMBEDDING_DIMENSIONS[@options[:embedding_model]] || 1536
    normalize_embedding(embedding_data, expected_dimensions)
  end
end

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

Since:

  • 1.0.0



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/prescient/provider/openai.rb', line 46

def generate_response(prompt, context_items = [], **options)
  handle_errors do
    formatted_prompt = build_prompt(prompt, context_items)

    response = self.class.post('/v1/chat/completions',
                               headers: {
                                 'Content-Type'  => 'application/json',
                                 'Authorization' => "Bearer #{@options[:api_key]}",
                               },
                               body:    {
                                 model:       @options[:chat_model],
                                 messages:    [
                                   {
                                     role:    'user',
                                     content: formatted_prompt,
                                   },
                                 ],
                                 max_tokens:  options[:max_tokens] || 2000,
                                 temperature: options[:temperature] || 0.7,
                                 top_p:       options[:top_p] || 0.9,
                               }.to_json)

    validate_response!(response, 'text generation')

    content = response.parsed_response.dig('choices', 0, 'message', 'content')
    raise Prescient::InvalidResponseError, 'No response generated' unless content

    {
      response:        content.strip,
      model:           @options[:chat_model],
      provider:        'openai',
      processing_time: nil,
      metadata:        {
        usage:         response.parsed_response['usage'],
        finish_reason: response.parsed_response.dig('choices', 0, 'finish_reason'),
      },
    }
  end
end

#health_checkObject

Since:

  • 1.0.0



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/prescient/provider/openai.rb', line 86

def health_check
  handle_errors do
    response = self.class.get('/v1/models',
                              headers: {
                                'Authorization' => "Bearer #{@options[:api_key]}",
                              })

    if response.success?
      models = response.parsed_response['data'] || []
      embedding_available = models.any? { |m| m['id'] == @options[:embedding_model] }
      chat_available = models.any? { |m| m['id'] == @options[:chat_model] }

      {
        status:           'healthy',
        provider:         'openai',
        models_available: models.map { |m| m['id'] },
        embedding_model:  {
          name:      @options[:embedding_model],
          available: embedding_available,
        },
        chat_model:       {
          name:      @options[:chat_model],
          available: chat_available,
        },
        ready:            embedding_available && chat_available,
      }
    else
      {
        status:   'unhealthy',
        provider: 'openai',
        error:    "HTTP #{response.code}",
        message:  response.message,
      }
    end
  end
rescue Prescient::Error => e
  {
    status:   'unavailable',
    provider: 'openai',
    error:    e.class.name,
    message:  e.message,
  }
end

#list_modelsObject

Since:

  • 1.0.0



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/prescient/provider/openai.rb', line 130

def list_models
  handle_errors do
    response = self.class.get('/v1/models',
                              headers: {
                                'Authorization' => "Bearer #{@options[:api_key]}",
                              })
    validate_response!(response, 'model listing')

    models = response.parsed_response['data'] || []
    models.map do |model|
      {
        name:     model['id'],
        created:  model['created'],
        owned_by: model['owned_by'],
      }
    end
  end
end

#validate_configuration!Object (protected)

Raises:

Since:

  • 1.0.0



151
152
153
154
155
156
157
158
# File 'lib/prescient/provider/openai.rb', line 151

def validate_configuration!
  required_options = [:api_key, :embedding_model, :chat_model]
  missing_options = required_options.select { |opt| @options[opt].nil? }

  return unless missing_options.any?

  raise Prescient::Error, "Missing required options: #{missing_options.join(', ')}"
end

#validate_response!(response, operation) ⇒ Object (private)

Since:

  • 1.0.0



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/prescient/provider/openai.rb', line 162

def validate_response!(response, operation)
  return if response.success?

  case response.code
  when 400
    raise Prescient::Error, "Bad request for #{operation}: #{response.body}"
  when 401
    raise Prescient::AuthenticationError, "Authentication failed for #{operation}"
  when 403
    raise Prescient::AuthenticationError, "Forbidden access for #{operation}"
  when 429
    raise Prescient::RateLimitError, "Rate limit exceeded for #{operation}"
  when 500..599
    raise Prescient::Error, "OpenAI server error during #{operation}: #{response.body}"
  else
    raise Prescient::Error,
          "OpenAI request failed for #{operation}: HTTP #{response.code} - #{response.message}"
  end
end