ujh/fountainpencompanion

View on GitHub
app/models/bots/pen_and_ink_suggestion.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Bots
  class PenAndInkSuggestion < Bots::Base
    LIMIT = 50

    def initialize(user, ink_kind)
      self.user = user
      self.ink_kind = ink_kind
    end

    def run
      remaining_tries = 5
      suggestion = request_suggestion

      loop do
        remaining_tries -= 1
        break if remaining_tries.zero?
        break if suggestion[:ink] && suggestion[:pen]

        Rails.logger.debug("Retrying, as either ink or pen not found")
        # Sometimes it picks two inks. In that case we want to try
        # once more. But only once, as we don't want and endless
        # loop that also costs us money.
        suggestion = request_suggestion
      end

      suggestion
    end

    private

    attr_accessor :user, :ink_kind

    def request_suggestion
      ink = inks.find { |ink| response_message.include?(ink.name) }
      ink ||= inks.find { |ink| response_message.include?(ink.short_name) }
      pen = pens.find { |pen| response_message.include?(pen.name) }

      { message: response_message, ink: ink&.id, pen: pen&.id }
    end

    def prompt
      <<~MESSAGE
        Given the following fountain pens:
        #{pen_data}

        Given the following inks:
        #{ink_data}

        Which combination of ink and fountain pen should I use and why?

        Prefer items that have either never been used, rarely used, or used a long time ago.
        Also suggest items that have been used a lot.
        Make only one suggestion.
        Use markdown syntax for highlighting.
        Do not mention usage and daily usage count if they are zero.
        Use the ink tags and description as part of the reasoning, but do not mention them directly.
      MESSAGE
    end

    def pen_data
      CSV.generate do |csv|
        csv << [
          "fountain pen name",
          "last usage",
          "usage count",
          "daily usage count"
        ]
        pens
          .shuffle
          .take(LIMIT)
          .each do |pen|
            last_usage =
              if pen.last_used_on
                ActionController::Base.helpers.time_ago_in_words(
                  pen.last_used_on
                )
              else
                "never"
              end
            csv << [
              pen.name.inspect,
              last_usage,
              pen.usage_count,
              pen.daily_usage_count
            ]
          end
      end
    end

    def ink_data
      CSV.generate do |csv|
        csv << [
          "ink name",
          "last usage",
          "usage count",
          "daily usage count",
          "tags",
          "description"
        ]

        inks
          .shuffle
          .take(LIMIT)
          .each do |ink|
            last_usage =
              if ink.last_used_on
                ActionController::Base.helpers.time_ago_in_words(
                  ink.last_used_on
                )
              else
                "never"
              end
            csv << [
              ink.name.inspect,
              last_usage,
              ink.usage_count,
              ink.daily_usage_count,
              ink.cluster_tags.join(","),
              ink.cluster_description
            ]
          end
      end
    end

    def pens
      @pens ||=
        user
          .collected_pens
          .active
          .includes(
            :currently_inkeds,
            :usage_records,
            newest_currently_inked: :last_usage
          )
          .reject { |pen| pen.inked? }
    end

    def inks
      @inks ||=
        begin
          rel =
            user.collected_inks.active.includes(
              :currently_inkeds,
              :usage_records,
              micro_cluster: {
                macro_cluster: :brand_cluster
              },
              newest_currently_inked: :last_usage
            )
          rel = rel.where(kind: ink_kind) if ink_kind.present?
          rel
        end
    end
  end
end