2023-03-07 14:14:39 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2023-03-14 15:03:50 -04:00
|
|
|
module ::DiscourseAi
|
2023-03-07 14:14:39 -05:00
|
|
|
module Inference
|
2023-03-15 16:02:20 -04:00
|
|
|
class OpenAiCompletions
|
2023-04-21 02:54:25 -04:00
|
|
|
TIMEOUT = 60
|
|
|
|
|
2023-06-19 18:45:31 -04:00
|
|
|
class Function
|
|
|
|
attr_reader :name, :description, :parameters, :type
|
|
|
|
|
|
|
|
def initialize(name:, description:, type: nil)
|
|
|
|
@name = name
|
|
|
|
@description = description
|
|
|
|
@type = type || "object"
|
|
|
|
@parameters = []
|
|
|
|
end
|
|
|
|
|
|
|
|
def add_parameter(name:, type:, description:, enum: nil, required: false)
|
|
|
|
@parameters << {
|
|
|
|
name: name,
|
|
|
|
type: type,
|
|
|
|
description: description,
|
|
|
|
enum: enum,
|
|
|
|
required: required,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_json(*args)
|
|
|
|
as_json.to_json(*args)
|
|
|
|
end
|
|
|
|
|
|
|
|
def as_json
|
|
|
|
required_params = []
|
|
|
|
|
|
|
|
properties = {}
|
|
|
|
parameters.each do |parameter|
|
|
|
|
definition = { type: parameter[:type], description: parameter[:description] }
|
|
|
|
definition[:enum] = parameter[:enum] if parameter[:enum]
|
|
|
|
|
|
|
|
required_params << parameter[:name] if parameter[:required]
|
|
|
|
properties[parameter[:name]] = definition
|
|
|
|
end
|
|
|
|
|
|
|
|
params = { type: @type, properties: properties }
|
|
|
|
|
|
|
|
params[:required] = required_params if required_params.present?
|
|
|
|
|
|
|
|
{ name: name, description: description, parameters: params }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-03-22 15:00:28 -04:00
|
|
|
CompletionFailed = Class.new(StandardError)
|
|
|
|
|
2023-04-21 02:54:25 -04:00
|
|
|
def self.perform!(
|
|
|
|
messages,
|
2023-05-11 09:03:03 -04:00
|
|
|
model,
|
2023-04-21 02:54:25 -04:00
|
|
|
temperature: nil,
|
|
|
|
top_p: nil,
|
|
|
|
max_tokens: nil,
|
2023-06-19 18:45:31 -04:00
|
|
|
functions: nil,
|
2023-05-05 14:28:31 -04:00
|
|
|
user_id: nil
|
2023-04-21 02:54:25 -04:00
|
|
|
)
|
2023-06-20 20:39:51 -04:00
|
|
|
url =
|
|
|
|
if model.include?("gpt-4")
|
|
|
|
URI(SiteSetting.ai_openai_gpt4_url)
|
|
|
|
else
|
|
|
|
URI(SiteSetting.ai_openai_gpt35_url)
|
|
|
|
end
|
|
|
|
headers = { "Content-Type" => "application/json" }
|
|
|
|
|
|
|
|
if url.host.include? ("azure")
|
|
|
|
headers["api-key"] = SiteSetting.ai_openai_api_key
|
|
|
|
else
|
|
|
|
headers["Authorization"] = "Bearer #{SiteSetting.ai_openai_api_key}"
|
|
|
|
end
|
2023-05-05 14:28:31 -04:00
|
|
|
|
2023-04-21 02:54:25 -04:00
|
|
|
payload = { model: model, messages: messages }
|
|
|
|
|
|
|
|
payload[:temperature] = temperature if temperature
|
|
|
|
payload[:top_p] = top_p if top_p
|
|
|
|
payload[:max_tokens] = max_tokens if max_tokens
|
2023-06-19 18:45:31 -04:00
|
|
|
payload[:functions] = functions if functions
|
2023-05-05 14:28:31 -04:00
|
|
|
payload[:stream] = true if block_given?
|
2023-03-07 14:14:39 -05:00
|
|
|
|
2023-04-21 02:54:25 -04:00
|
|
|
Net::HTTP.start(
|
|
|
|
url.host,
|
|
|
|
url.port,
|
|
|
|
use_ssl: true,
|
|
|
|
read_timeout: TIMEOUT,
|
|
|
|
open_timeout: TIMEOUT,
|
|
|
|
write_timeout: TIMEOUT,
|
|
|
|
) do |http|
|
|
|
|
request = Net::HTTP::Post.new(url, headers)
|
2023-04-25 21:44:29 -04:00
|
|
|
request_body = payload.to_json
|
|
|
|
request.body = request_body
|
2023-03-20 15:43:51 -04:00
|
|
|
|
2023-05-05 14:28:31 -04:00
|
|
|
http.request(request) do |response|
|
|
|
|
if response.code.to_i != 200
|
|
|
|
Rails.logger.error(
|
|
|
|
"OpenAiCompletions: status: #{response.code.to_i} - body: #{response.body}",
|
|
|
|
)
|
|
|
|
raise CompletionFailed
|
|
|
|
end
|
2023-04-25 21:44:29 -04:00
|
|
|
|
2023-05-05 14:28:31 -04:00
|
|
|
log =
|
|
|
|
AiApiAuditLog.create!(
|
|
|
|
provider_id: AiApiAuditLog::Provider::OpenAI,
|
|
|
|
raw_request_payload: request_body,
|
|
|
|
user_id: user_id,
|
|
|
|
)
|
|
|
|
|
|
|
|
if !block_given?
|
|
|
|
response_body = response.read_body
|
|
|
|
parsed_response = JSON.parse(response_body, symbolize_names: true)
|
|
|
|
|
|
|
|
log.update!(
|
|
|
|
raw_response_payload: response_body,
|
|
|
|
request_tokens: parsed_response.dig(:usage, :prompt_tokens),
|
|
|
|
response_tokens: parsed_response.dig(:usage, :completion_tokens),
|
|
|
|
)
|
|
|
|
return parsed_response
|
|
|
|
end
|
2023-04-21 02:54:25 -04:00
|
|
|
|
2023-05-05 14:28:31 -04:00
|
|
|
begin
|
|
|
|
cancelled = false
|
|
|
|
cancel = lambda { cancelled = true }
|
|
|
|
response_data = +""
|
|
|
|
response_raw = +""
|
2023-04-21 02:54:25 -04:00
|
|
|
|
2023-06-19 18:45:31 -04:00
|
|
|
leftover = ""
|
|
|
|
|
2023-05-05 14:28:31 -04:00
|
|
|
response.read_body do |chunk|
|
|
|
|
if cancelled
|
|
|
|
http.finish
|
|
|
|
return
|
2023-04-25 21:44:29 -04:00
|
|
|
end
|
2023-04-21 02:54:25 -04:00
|
|
|
|
2023-05-05 14:28:31 -04:00
|
|
|
response_raw << chunk
|
|
|
|
|
2023-06-19 18:45:31 -04:00
|
|
|
(leftover + chunk)
|
2023-05-05 14:28:31 -04:00
|
|
|
.split("\n")
|
|
|
|
.each do |line|
|
|
|
|
data = line.split("data: ", 2)[1]
|
|
|
|
next if !data || data == "[DONE]"
|
2023-06-19 18:45:31 -04:00
|
|
|
next if cancelled
|
|
|
|
|
|
|
|
partial = nil
|
|
|
|
begin
|
|
|
|
partial = JSON.parse(data, symbolize_names: true)
|
|
|
|
leftover = ""
|
|
|
|
rescue JSON::ParserError
|
|
|
|
leftover = line
|
|
|
|
end
|
2023-05-05 14:28:31 -04:00
|
|
|
|
2023-06-19 18:45:31 -04:00
|
|
|
if partial
|
2023-05-05 14:28:31 -04:00
|
|
|
response_data << partial.dig(:choices, 0, :delta, :content).to_s
|
2023-06-19 18:45:31 -04:00
|
|
|
response_data << partial.dig(:choices, 0, :delta, :function_call).to_s
|
2023-05-05 14:28:31 -04:00
|
|
|
|
|
|
|
yield partial, cancel
|
|
|
|
end
|
|
|
|
end
|
|
|
|
rescue IOError
|
|
|
|
raise if !cancelled
|
|
|
|
ensure
|
|
|
|
log.update!(
|
|
|
|
raw_response_payload: response_raw,
|
2023-05-15 14:10:42 -04:00
|
|
|
request_tokens:
|
|
|
|
DiscourseAi::Tokenizer::OpenAiTokenizer.size(extract_prompt(messages)),
|
|
|
|
response_tokens: DiscourseAi::Tokenizer::OpenAiTokenizer.size(response_data),
|
2023-05-05 14:28:31 -04:00
|
|
|
)
|
2023-04-21 02:54:25 -04:00
|
|
|
end
|
|
|
|
end
|
2023-05-05 14:28:31 -04:00
|
|
|
end
|
2023-04-21 02:54:25 -04:00
|
|
|
end
|
2023-04-25 21:44:29 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.extract_prompt(messages)
|
|
|
|
messages.map { |message| message[:content] || message["content"] || "" }.join("\n")
|
2023-03-07 14:14:39 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|