app/models/ruby_conversations/message_builder.rb



# frozen_string_literal: true

module RubyConversations
  # Builds Message objects with their prompts and inputs based on Prompt templates.
  class MessageBuilder
    def initialize(conversation)
      @conversation = conversation
    end

    def build_from_single_prompt(name, description: nil, inputs: {})
      prompt = Prompt.find_by_name!(name)
      validate_inputs!(prompt, inputs)

      interpolated_message = prompt.interpolate(inputs)
      message = build_message(interpolated_message, description)
      build_prompt_association(message, prompt, inputs)

      message
    end

    def build_from_user_message(raw_message, description: nil)
      message = build_message(raw_message, description)
      build_message_prompt(message, Prompt.new(name: 'user_message', role: 'user', message: raw_message))
      message
    end

    def build_from_multiple_prompts(prompt_inputs, description: nil)
      message = build_message('', description)

      prompt_inputs.each do |prompt_name, inputs|
        process_single_prompt_input(message, prompt_name, inputs)
      end

      message.request = message.request.strip
      message
    end

    def process_single_prompt_input(message, prompt_name, inputs)
      prompt = Prompt.find_by!(name: prompt_name)
      validate_inputs!(prompt, inputs)

      interpolated_message = prompt.interpolate(inputs)
      message.request += "#{interpolated_message}\n\n"

      message_prompt = build_message_prompt(message, prompt)
      build_inputs_for_prompt(message_prompt, inputs)
    end

    private

    def validate_inputs!(prompt, inputs)
      return unless prompt.valid_placeholders.present?

      missing_inputs = calculate_missing_inputs(prompt, inputs)

      errors = []
      errors << "#{prompt.name}: Missing required inputs: #{missing_inputs.join(', ')}" if missing_inputs.any?
      raise ArgumentError, errors.join("\n") if errors.any?
    end

    def build_prompt_association(message, prompt, inputs)
      message_prompt = build_message_prompt(message, prompt)
      build_inputs_for_prompt(message_prompt, inputs)
    end

    def build_message_prompt(message, prompt)
      prompt_attrs = base_prompt_attrs(prompt)
      prompt_attrs[:message] = message
      prompt_attrs[:name] = prompt.name
      prompt_attrs[:role] = prompt.role
      prompt_attrs[:llm] = prompt.llm
      prompt = RubyConversations::MessagePrompt.new(prompt_attrs)
      message.message_prompts << prompt
      prompt
    end

    def build_inputs_for_prompt(message_prompt, inputs)
      inputs.each do |input_name, value|
        input_attrs = base_input_attrs(input_name, value)
        input_attrs[:message_prompt] = message_prompt
        input = RubyConversations::MessageInput.new(input_attrs)
        message_prompt.message_inputs << input
      end
    end

    def calculate_missing_inputs(prompt, inputs)
      required_placeholders = prompt.valid_placeholders.split(',').map(&:strip)
      provided_keys = inputs.keys.map(&:to_s)
      required_placeholders - provided_keys
    end

    def build_message(request, description)
      message_attrs = base_message_attrs(request, description)
      message = RubyConversations::Message.new(message_attrs)
      @conversation.messages << message
      message
    end

    def base_prompt_attrs(prompt)
      {
        prompt_version_id: prompt.latest_version_id,
        draft: prompt.message
      }
    end

    def base_message_attrs(request, description)
      {
        request: request,
        change_description: description,
        model_identifier: @conversation.model_identifier,
        llm: @conversation.llm,
        tool: @conversation.tool
      }
    end

    def base_input_attrs(input_name, value)
      {
        placeholder_name: input_name.to_s,
        value: value.to_s
      }
    end
  end
end