summaryrefslogtreecommitdiff
path: root/components/providers/openai.rb
blob: ce6fb3309db2308da04210c98b3086dcbd434027 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
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
85
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
# frozen_string_literal: true

require 'openai'

require_relative './base'
require_relative '../crypto'

module NanoBot
  module Components
    module Providers
      class OpenAI < Base
        DEFAULT_ADDRESS = 'https://api.openai.com'

        CHAT_SETTINGS = %i[
          model stream temperature top_p n stop max_tokens
          presence_penalty frequency_penalty logit_bias
        ].freeze

        attr_reader :settings

        def initialize(settings, credentials, environment: {})
          @settings = settings
          @credentials = credentials
          @environment = environment

          uri_base = if @credentials[:address].nil? || @credentials[:address].to_s.strip.empty?
                       "#{DEFAULT_ADDRESS}/"
                     else
                       "#{@credentials[:address].to_s.sub(%r{/$}, '')}/"
                     end

          @client = ::OpenAI::Client.new(uri_base:, access_token: @credentials[:'access-token'])
        end

        def stream(input)
          provider = @settings.key?(:stream) ? @settings[:stream] : true
          interface = input[:interface].key?(:stream) ? input[:interface][:stream] : true

          provider && interface
        end

        def evaluate(input, &block)
          messages = input[:history].map do |event|
            { role: event[:who] == 'user' ? 'user' : 'assistant',
              content: event[:message] }
          end

          %i[instruction backdrop directive].each do |key|
            next unless input[:behavior][key]

            messages.prepend(
              { role: key == :directive ? 'system' : 'user',
                content: input[:behavior][key] }
            )
          end

          payload = { user: OpenAI.end_user(@settings, @environment), messages: }

          CHAT_SETTINGS.each do |key|
            payload[key] = @settings[key] if @settings.key?(key)
          end

          payload.delete(:logit_bias) if payload.key?(:logit_bias) && payload[:logit_bias].nil?

          if stream(input)
            content = ''

            payload[:stream] = proc do |chunk, _bytesize|
              partial = chunk.dig('choices', 0, 'delta', 'content')
              if partial
                content += partial
                block.call({ who: 'AI', message: partial }, false)
              end

              block.call({ who: 'AI', message: content }, true) if chunk.dig('choices', 0, 'finish_reason')
            end

            @client.chat(parameters: payload)
          else
            result = @client.chat(parameters: payload)

            raise StandardError, result['error'] if result['error']

            block.call({ who: 'AI', message: result.dig('choices', 0, 'message', 'content') }, true)
          end
        end

        def self.end_user(settings, environment)
          user = ENV.fetch('NANO_BOTS_END_USER', nil)

          user = settings[:user] if !settings[:user].nil? && !settings[:user].to_s.strip.empty?

          candidate = environment && (
            environment['NANO_BOTS_END_USER'] ||
            environment[:NANO_BOTS_END_USER]
          )

          user = candidate if !candidate.nil? && !candidate.to_s.strip.empty?

          user = if user.nil? || user.to_s.strip.empty?
                   'unknown'
                 else
                   user.to_s.strip
                 end

          Crypto.encrypt(user, soft: true)
        end
      end
    end
  end
end