Class: Spectre::Openai::Completions
- Inherits:
-
Object
- Object
- Spectre::Openai::Completions
- Defined in:
- lib/spectre/openai/completions.rb
Constant Summary collapse
- API_URL =
'https://api.openai.com/v1/chat/completions'- DEFAULT_MODEL =
'gpt-4o-mini'- DEFAULT_TIMEOUT =
60
Class Method Summary collapse
-
.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args) ⇒ Hash
Class method to generate a completion based on user messages and optional tools.
-
.generate_body(messages, model, json_schema, max_tokens, tools, forwarded) ⇒ Hash
Helper method to generate the request body.
-
.handle_response(response) ⇒ Hash
Handles the API response, raising errors for specific cases and returning structured content otherwise.
-
.validate_messages!(messages) ⇒ Object
Validate the structure and content of the messages array.
Class Method Details
.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args) ⇒ Hash
Class method to generate a completion based on user messages and optional tools
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/spectre/openai/completions.rb', line 25 def self.create(messages:, model: DEFAULT_MODEL, json_schema: nil, tools: nil, **args) api_key = Spectre.openai_configuration.api_key raise APIKeyNotConfiguredError, "API key is not configured" unless api_key () uri = URI(API_URL) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.read_timeout = args.fetch(:read_timeout, DEFAULT_TIMEOUT) http.open_timeout = args.fetch(:open_timeout, DEFAULT_TIMEOUT) request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{api_key}" }) max_tokens = args[:max_tokens] # Forward extra args (like temperature) into the body, excluding control/network keys forwarded = args.reject { |k, _| [:read_timeout, :open_timeout, :max_tokens].include?(k) } request.body = generate_body(, model, json_schema, max_tokens, tools, forwarded).to_json response = http.request(request) unless response.is_a?(Net::HTTPSuccess) raise "OpenAI API Error: #{response.code} - #{response.message}: #{response.body}" end parsed_response = JSON.parse(response.body) handle_response(parsed_response) rescue JSON::ParserError => e raise "JSON Parse Error: #{e.message}" end |
.generate_body(messages, model, json_schema, max_tokens, tools, forwarded) ⇒ Hash
Helper method to generate the request body
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/spectre/openai/completions.rb', line 88 def self.generate_body(, model, json_schema, max_tokens, tools, forwarded) body = { model: model, messages: } body[:max_tokens] = max_tokens if max_tokens body[:response_format] = { type: 'json_schema', json_schema: json_schema } if json_schema body[:tools] = tools if tools # Add the tools to the request body if provided # Merge any extra forwarded options (e.g., temperature, top_p) if forwarded && !forwarded.empty? body.merge!(forwarded.transform_keys(&:to_sym)) end body end |
.handle_response(response) ⇒ Hash
Handles the API response, raising errors for specific cases and returning structured content otherwise
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/spectre/openai/completions.rb', line 110 def self.handle_response(response) = response.dig('choices', 0, 'message') finish_reason = response.dig('choices', 0, 'finish_reason') # Check if the response contains a refusal if ['refusal'] raise "Refusal: #{message['refusal']}" end # Check if the finish reason is "length", indicating incomplete response if finish_reason == "length" raise "Incomplete response: The completion was cut off due to token limit." end # Check if the finish reason is "content_filter", indicating policy violations if finish_reason == "content_filter" raise "Content filtered: The model's output was blocked due to policy violations." end # Check if the model made a function call if finish_reason == "function_call" || finish_reason == "tool_calls" return { tool_calls: ['tool_calls'], content: ['content'] } end # If the response finished normally, return the content if finish_reason == "stop" return { content: ['content'] } end # Handle unexpected finish reasons raise "Unexpected finish_reason: #{finish_reason}" end |
.validate_messages!(messages) ⇒ Object
Validate the structure and content of the messages array.
66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/spectre/openai/completions.rb', line 66 def self.() # Check if messages is an array of hashes. # This ensures that the input is in the correct format for message processing. unless .is_a?(Array) && .all? { |msg| msg.is_a?(Hash) } raise ArgumentError, "Messages must be an array of message hashes." end # Check if the array is empty. # This prevents requests with no messages, which would be invalid. if .empty? raise ArgumentError, "Messages cannot be empty." end end |