| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  | #frozen_string_literal: true | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module DiscourseAi | 
					
						
							|  |  |  |   module AiBot | 
					
						
							|  |  |  |     module Commands | 
					
						
							| 
									
										
										
										
											2023-06-20 08:45:31 +10:00
										 |  |  |       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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |       class Command | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |         CARET = "<!-- caret -->" | 
					
						
							|  |  |  |         PROGRESS_CARET = "<!-- progress -->" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |         class << self | 
					
						
							|  |  |  |           def name | 
					
						
							|  |  |  |             raise NotImplemented | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           def invoked?(cmd_name) | 
					
						
							|  |  |  |             cmd_name == name | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           def desc | 
					
						
							|  |  |  |             raise NotImplemented | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 08:45:31 +10:00
										 |  |  |           def custom_system_message | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           def parameters | 
					
						
							|  |  |  |             raise NotImplemented | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |           end | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |         attr_reader :bot_user | 
					
						
							| 
									
										
										
										
											2023-05-22 12:09:14 +10:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |         def initialize(bot_user:, args:, post: nil, parent_post: nil) | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |           @bot_user = bot_user | 
					
						
							|  |  |  |           @args = args | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |           @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 | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-23 23:08:17 +10:00
										 |  |  |         def bot | 
					
						
							|  |  |  |           @bot ||= DiscourseAi::AiBot::Bot.as(bot_user) | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-29 10:43:58 +10:00
										 |  |  |         def tokenizer | 
					
						
							|  |  |  |           bot.tokenizer | 
					
						
							|  |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |         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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |         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 | 
					
						
							| 
									
										
										
										
											2023-06-20 08:45:31 +10:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |           if !@post | 
					
						
							|  |  |  |             @post = | 
					
						
							| 
									
										
										
										
											2023-06-20 08:45:31 +10:00
										 |  |  |               PostCreator.create!( | 
					
						
							|  |  |  |                 bot_user, | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |                 raw: @placeholder, | 
					
						
							|  |  |  |                 topic_id: @parent_post.topic_id, | 
					
						
							| 
									
										
										
										
											2023-06-20 08:45:31 +10:00
										 |  |  |                 skip_validations: true, | 
					
						
							|  |  |  |                 skip_rate_limiter: true, | 
					
						
							|  |  |  |               ) | 
					
						
							|  |  |  |           else | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |             @post.revise( | 
					
						
							| 
									
										
										
										
											2023-06-20 08:45:31 +10:00
										 |  |  |               bot_user, | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |               { raw: @post.raw + "\n\n" + @placeholder + "\n\n" }, | 
					
						
							| 
									
										
										
										
											2023-06-20 08:45:31 +10:00
										 |  |  |               skip_validations: true, | 
					
						
							|  |  |  |               skip_revision: true, | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |           @post.post_custom_prompt ||= @post.build_post_custom_prompt(custom_prompt: []) | 
					
						
							|  |  |  |           prompt = @post.post_custom_prompt.custom_prompt || [] | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |           parsed_args = JSON.parse(@args).symbolize_keys | 
					
						
							| 
									
										
										
										
											2023-08-04 09:37:58 +10:00
										 |  |  | 
 | 
					
						
							|  |  |  |           prompt << [process(**parsed_args).to_json, self.class.name, "function"] | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |           @post.post_custom_prompt.update!(custom_prompt: prompt) | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 08:45:31 +10:00
										 |  |  |           raw = +(<<~HTML) | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |           <details> | 
					
						
							|  |  |  |             <summary>#{I18n.t("discourse_ai.ai_bot.command_summary.#{self.class.name}")}</summary> | 
					
						
							|  |  |  |             <p> | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |               #{localized_description} | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |             </p>
 | 
					
						
							|  |  |  |           </details>
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           HTML | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           raw << custom_raw if custom_raw.present? | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |           raw = @post.raw.sub(@placeholder, raw) | 
					
						
							| 
									
										
										
										
											2023-05-23 23:08:17 +10:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |           @post.revise(bot_user, { raw: raw }, skip_validations: true, skip_revision: true) | 
					
						
							| 
									
										
										
										
											2023-08-09 16:01:48 +10:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |           if chain_next_response | 
					
						
							| 
									
										
										
										
											2023-08-09 16:01:48 +10:00
										 |  |  |             # somewhat annoying but whitespace was stripped in revise | 
					
						
							|  |  |  |             # so we need to save again | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |             @post.raw = raw | 
					
						
							|  |  |  |             @post.save!(validate: false) | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |           end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |           [chain_next_response, @post] | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-21 17:10:30 +10:00
										 |  |  |         def format_results(rows, column_names = nil, args: nil) | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |           rows = rows&.map { |row| yield row } if block_given? | 
					
						
							| 
									
										
										
										
											2023-05-22 12:09:14 +10:00
										 |  |  | 
 | 
					
						
							|  |  |  |           if !column_names | 
					
						
							|  |  |  |             index = -1
 | 
					
						
							|  |  |  |             column_indexes = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             rows = | 
					
						
							| 
									
										
										
										
											2023-08-14 16:30:12 +10:00
										 |  |  |               rows&.map do |data| | 
					
						
							| 
									
										
										
										
											2023-05-22 12:09:14 +10:00
										 |  |  |                 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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 08:45:31 +10:00
										 |  |  |           # this is not the most efficient format | 
					
						
							|  |  |  |           # however this is needed cause GPT 3.5 / 4 was steered using JSON | 
					
						
							| 
									
										
										
										
											2023-06-21 17:10:30 +10:00
										 |  |  |           result = { column_names: column_names, rows: rows } | 
					
						
							|  |  |  |           result[:args] = args if args | 
					
						
							|  |  |  |           result | 
					
						
							| 
									
										
										
										
											2023-05-22 12:09:14 +10:00
										 |  |  |         end | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-20 17:45:54 +10:00
										 |  |  |         protected | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         attr_reader :bot_user, :args | 
					
						
							|  |  |  |       end | 
					
						
							|  |  |  |     end | 
					
						
							|  |  |  |   end | 
					
						
							|  |  |  | end |