# frozen_string_literal: true module DiscourseAi::Completions class OpenAiMessageProcessor attr_reader :prompt_tokens, :completion_tokens, :cached_tokens def initialize(partial_tool_calls: false) @tool = nil @tool_arguments = +"" @prompt_tokens = nil @completion_tokens = nil @cached_tokens = nil @partial_tool_calls = partial_tool_calls end def process_message(json) result = [] tool_calls = json.dig(:choices, 0, :message, :tool_calls) message = json.dig(:choices, 0, :message, :content) result << message if message.present? if tool_calls.present? tool_calls.each do |tool_call| id = tool_call.dig(:id) name = tool_call.dig(:function, :name) arguments = tool_call.dig(:function, :arguments) parameters = arguments.present? ? JSON.parse(arguments, symbolize_names: true) : {} result << ToolCall.new(id: id, name: name, parameters: parameters) end end update_usage(json) result end def process_streamed_message(json) rval = nil tool_calls = json.dig(:choices, 0, :delta, :tool_calls) content = json.dig(:choices, 0, :delta, :content) finished_tools = json.dig(:choices, 0, :finish_reason) || tool_calls == [] if tool_calls.present? id = tool_calls.dig(0, :id) name = tool_calls.dig(0, :function, :name) arguments = tool_calls.dig(0, :function, :arguments) # TODO: multiple tool support may require index #index = tool_calls[0].dig(:index) if id.present? && @tool && @tool.id != id process_arguments rval = @tool @tool = nil end if id.present? && name.present? @tool_arguments = +"" @tool = ToolCall.new(id: id, name: name) @streaming_parser = ToolCallProgressTracker.new(self) if @partial_tool_calls end @tool_arguments << arguments.to_s @streaming_parser << arguments.to_s if @streaming_parser && !arguments.to_s.empty? rval = current_tool_progress if !rval elsif finished_tools && @tool parsed_args = JSON.parse(@tool_arguments, symbolize_names: true) @tool.parameters = parsed_args @tool.partial = false rval = @tool @tool = nil elsif !content.to_s.empty? # we don't want to strip empty content like "\n", do not use present? rval = content end update_usage(json) rval end def notify_progress(key, value) if @tool @tool.partial = true @tool.parameters[key.to_sym] = value @has_new_data = true end end def current_tool_progress if @has_new_data @has_new_data = false @tool else nil end end def finish rval = [] if @tool process_arguments rval << @tool @tool = nil end rval end private def process_arguments if @tool_arguments.present? parsed_args = JSON.parse(@tool_arguments, symbolize_names: true) @tool.parameters = parsed_args @tool_arguments = nil end end def update_usage(json) @prompt_tokens ||= json.dig(:usage, :prompt_tokens) @completion_tokens ||= json.dig(:usage, :completion_tokens) @cached_tokens ||= json.dig(:usage, :prompt_tokens_details, :cached_tokens) end end end