mirror of
				https://github.com/discourse/discourse-ai.git
				synced 2025-11-04 00:18:39 +00:00 
			
		
		
		
	This command can be used to extract information about a discourse site setting directly from source. To operate it needs the rg binary in the container.
		
			
				
	
	
		
			209 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			209 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
#frozen_string_literal: true
 | 
						|
 | 
						|
module DiscourseAi
 | 
						|
  module AiBot
 | 
						|
    module Commands
 | 
						|
      class Parameter
 | 
						|
        attr_reader :name, :description, :type, :enum, :required
 | 
						|
        def initialize(name:, description:, type:, enum: nil, required: false)
 | 
						|
          @name = name
 | 
						|
          @description = description
 | 
						|
          @type = type
 | 
						|
          @enum = enum
 | 
						|
          @required = required
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      class Command
 | 
						|
        CARET = "<!-- caret -->"
 | 
						|
        PROGRESS_CARET = "<!-- progress -->"
 | 
						|
 | 
						|
        class << self
 | 
						|
          def name
 | 
						|
            raise NotImplemented
 | 
						|
          end
 | 
						|
 | 
						|
          def invoked?(cmd_name)
 | 
						|
            cmd_name == name
 | 
						|
          end
 | 
						|
 | 
						|
          def desc
 | 
						|
            raise NotImplemented
 | 
						|
          end
 | 
						|
 | 
						|
          def custom_system_message
 | 
						|
          end
 | 
						|
 | 
						|
          def parameters
 | 
						|
            raise NotImplemented
 | 
						|
          end
 | 
						|
        end
 | 
						|
 | 
						|
        attr_reader :bot_user
 | 
						|
 | 
						|
        def initialize(bot_user:, args:, post: nil, parent_post: nil)
 | 
						|
          @bot_user = bot_user
 | 
						|
          @args = args
 | 
						|
          @post = post
 | 
						|
          @parent_post = parent_post
 | 
						|
 | 
						|
          @placeholder = +(<<~HTML).strip
 | 
						|
            <details>
 | 
						|
              <summary>#{I18n.t("discourse_ai.ai_bot.command_summary.#{self.class.name}")}</summary>
 | 
						|
              <p>
 | 
						|
                #{CARET}
 | 
						|
              </p>
 | 
						|
            </details>
 | 
						|
            #{PROGRESS_CARET}
 | 
						|
          HTML
 | 
						|
 | 
						|
          @invoked = false
 | 
						|
        end
 | 
						|
 | 
						|
        def bot
 | 
						|
          @bot ||= DiscourseAi::AiBot::Bot.as(bot_user)
 | 
						|
        end
 | 
						|
 | 
						|
        def tokenizer
 | 
						|
          bot.tokenizer
 | 
						|
        end
 | 
						|
 | 
						|
        def standalone?
 | 
						|
          false
 | 
						|
        end
 | 
						|
 | 
						|
        def low_cost?
 | 
						|
          false
 | 
						|
        end
 | 
						|
 | 
						|
        def result_name
 | 
						|
          raise NotImplemented
 | 
						|
        end
 | 
						|
 | 
						|
        def name
 | 
						|
          raise NotImplemented
 | 
						|
        end
 | 
						|
 | 
						|
        def process(post)
 | 
						|
          raise NotImplemented
 | 
						|
        end
 | 
						|
 | 
						|
        def description_args
 | 
						|
          {}
 | 
						|
        end
 | 
						|
 | 
						|
        def custom_raw
 | 
						|
        end
 | 
						|
 | 
						|
        def chain_next_response
 | 
						|
          true
 | 
						|
        end
 | 
						|
 | 
						|
        def show_progress(text, progress_caret: false)
 | 
						|
          # during tests we may have none
 | 
						|
          caret = progress_caret ? PROGRESS_CARET : CARET
 | 
						|
          new_placeholder = @placeholder.sub(caret, text + caret)
 | 
						|
          raw = @post.raw.sub(@placeholder, new_placeholder)
 | 
						|
          @placeholder = new_placeholder
 | 
						|
 | 
						|
          @post.revise(bot_user, { raw: raw }, skip_validations: true, skip_revision: true)
 | 
						|
        end
 | 
						|
 | 
						|
        def localized_description
 | 
						|
          I18n.t(
 | 
						|
            "discourse_ai.ai_bot.command_description.#{self.class.name}",
 | 
						|
            self.description_args,
 | 
						|
          )
 | 
						|
        end
 | 
						|
 | 
						|
        def invoke!
 | 
						|
          raise StandardError.new("Command can only be invoked once!") if @invoked
 | 
						|
 | 
						|
          @invoked = true
 | 
						|
 | 
						|
          if !@post
 | 
						|
            @post =
 | 
						|
              PostCreator.create!(
 | 
						|
                bot_user,
 | 
						|
                raw: @placeholder,
 | 
						|
                topic_id: @parent_post.topic_id,
 | 
						|
                skip_validations: true,
 | 
						|
                skip_rate_limiter: true,
 | 
						|
              )
 | 
						|
          else
 | 
						|
            @post.revise(
 | 
						|
              bot_user,
 | 
						|
              { raw: @post.raw + "\n\n" + @placeholder + "\n\n" },
 | 
						|
              skip_validations: true,
 | 
						|
              skip_revision: true,
 | 
						|
            )
 | 
						|
          end
 | 
						|
 | 
						|
          @post.post_custom_prompt ||= @post.build_post_custom_prompt(custom_prompt: [])
 | 
						|
          prompt = @post.post_custom_prompt.custom_prompt || []
 | 
						|
 | 
						|
          parsed_args = JSON.parse(@args).symbolize_keys
 | 
						|
 | 
						|
          prompt << [process(**parsed_args).to_json, self.class.name, "function"]
 | 
						|
          @post.post_custom_prompt.update!(custom_prompt: prompt)
 | 
						|
 | 
						|
          raw = +(<<~HTML)
 | 
						|
          <details>
 | 
						|
            <summary>#{I18n.t("discourse_ai.ai_bot.command_summary.#{self.class.name}")}</summary>
 | 
						|
            <p>
 | 
						|
              #{localized_description}
 | 
						|
            </p>
 | 
						|
          </details>
 | 
						|
 | 
						|
          HTML
 | 
						|
 | 
						|
          raw << custom_raw if custom_raw.present?
 | 
						|
 | 
						|
          raw = @post.raw.sub(@placeholder, raw)
 | 
						|
 | 
						|
          @post.revise(bot_user, { raw: raw }, skip_validations: true, skip_revision: true)
 | 
						|
 | 
						|
          if chain_next_response
 | 
						|
            # somewhat annoying but whitespace was stripped in revise
 | 
						|
            # so we need to save again
 | 
						|
            @post.raw = raw
 | 
						|
            @post.save!(validate: false)
 | 
						|
          end
 | 
						|
 | 
						|
          [chain_next_response, @post]
 | 
						|
        end
 | 
						|
 | 
						|
        def format_results(rows, column_names = nil, args: nil)
 | 
						|
          rows = rows&.map { |row| yield row } if block_given?
 | 
						|
 | 
						|
          if !column_names
 | 
						|
            index = -1
 | 
						|
            column_indexes = {}
 | 
						|
 | 
						|
            rows =
 | 
						|
              rows&.map do |data|
 | 
						|
                new_row = []
 | 
						|
                data.each do |key, value|
 | 
						|
                  found_index = column_indexes[key.to_s] ||= (index += 1)
 | 
						|
                  new_row[found_index] = value
 | 
						|
                end
 | 
						|
                new_row
 | 
						|
              end
 | 
						|
            column_names = column_indexes.keys
 | 
						|
          end
 | 
						|
 | 
						|
          # this is not the most efficient format
 | 
						|
          # however this is needed cause GPT 3.5 / 4 was steered using JSON
 | 
						|
          result = { column_names: column_names, rows: rows }
 | 
						|
          result[:args] = args if args
 | 
						|
          result
 | 
						|
        end
 | 
						|
 | 
						|
        protected
 | 
						|
 | 
						|
        attr_reader :bot_user, :args
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |