Sam 7edb57c005
DEV: simplify command framework (#125)
The command framework had some confusing dispatching where it would dispatch
JSON blobs, this meant there was lots of parsing required in every command

The refactor handles transforming the args prior to dispatch which makes
consuming far simpler

This is also general prep to supporting some basic command framework in other
llms.
2023-08-04 09:37:58 +10:00

171 lines
4.1 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
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, :args
def initialize(bot_user, args)
@bot_user = bot_user
@args = args
end
def bot
@bot ||= DiscourseAi::AiBot::Bot.as(bot_user)
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 invoke_and_attach_result_to(post, parent_post)
placeholder = (<<~HTML).strip
<details>
<summary>#{I18n.t("discourse_ai.ai_bot.command_summary.#{self.class.name}")}</summary>
</details>
HTML
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>
#{I18n.t("discourse_ai.ai_bot.command_description.#{self.class.name}", self.description_args)}
</p>
</details>
HTML
raw << custom_raw if custom_raw.present?
raw = post.raw.sub(placeholder, raw)
if chain_next_response
post.raw = raw
post.save!(validate: false)
else
post.revise(bot_user, { raw: raw }, skip_validations: true, skip_revision: true)
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