lib/lammy/openai.rb
# frozen_string_literal: true require 'openai' require 'hashie' require 'base64' module Lammy # Use the OpenAI API's Ruby library class OpenAI MODELS = [ /^gpt-.*$/, /^chatgpt-.*$/, /^o\d+(?:-.*)?$/, /^(?:davinci|babbage)-002$/ ].freeze EMBEDDINGS = %w[ text-embedding-3-small text-embedding-3-large text-embedding-ada-002 ].freeze attr_reader :settings def initialize(settings) @settings = settings end # Generate a response with support for structured output def chat(user_message, system_message = nil, stream = nil) schema = schema(settings) messages = messages(user_message, system_message) request = client.chat( parameters: { model: settings[:model] || Lammy.configuration.model, response_format: schema, messages: messages, stream: stream ? ->(chunk) { stream.call(stream_content(chunk)) } : nil }.compact ) return stream if stream response = request.dig('choices', 0, 'message', 'content') content = schema ? ::Hashie::Mash.new(JSON.parse(response)) : response array?(schema) ? content.items : content end # OpenAI’s text embeddings measure the relatedness of text strings. An embedding is a vector of floating point # numbers. The distance between two vectors measures their relatedness. Small distances suggest high relatedness # and large distances suggest low relatedness. def embeddings(chunks) responses = chunks.map do |chunk| response = client.embeddings( parameters: { model: settings[:model], dimensions: settings[:dimensions], input: chunk } ) response.dig('data', 0, 'embedding') end responses.one? ? responses.first : responses end private def schema(settings) return unless settings[:schema] { 'type' => 'json_schema', 'json_schema' => { 'name' => 'schema', 'schema' => settings[:schema].merge('additionalProperties' => false) } } end def messages(user_message, system_message) return user_message if user_message.is_a?(Array) [ system_message ? L.system(system_message) : nil, vision(L.user(user_message)) ].compact end def vision(message) image = message[:_image] base = message.except(:_image) return base unless image messages = [ { 'type' => 'text', 'text' => message[:content] }, { 'type' => 'image_url', 'image_url' => { 'url' => "data:image/jpeg;base64,#{Base64.encode64(image)}" } } ] base.merge(content: messages) end def array?(schema) schema.is_a?(Hash) && schema.dig('json_schema', 'schema', 'properties', 'items', 'type') == 'array' end def stream_content(chunk) chunk.dig('choices', 0, 'delta', 'content') end def client return settings[:client] if settings[:client] return Lammy.configuration.client if Lammy.configuration.client @client ||= ::OpenAI::Client.new( access_token: ENV.fetch('OPENAI_ACCESS_TOKEN') ) end end end