Roman Rizzi f9d7d7f5f0
DEV: AI bot migration to the Llm pattern. (#343)
* DEV: AI bot migration to the Llm pattern.

We added tool and conversation context support to the Llm service in discourse-ai#366, meaning we met all the conditions to migrate this module.

This PR migrates to the new pattern, meaning adding a new bot now requires minimal effort as long as the service supports it. On top of this, we introduce the concept of a "Playground" to separate the PM-specific bits from the completion, allowing us to use the bot in other contexts like chat in the future. Commands are called tools, and we simplified all the placeholder logic to perform updates in a single place, making the flow more one-wayish.

* Followup fixes based on testing

* Cleanup unused inference code

* FIX: text-based tools could be in the middle of a sentence

* GPT-4-turbo support

* Use new LLM API
2024-01-04 10:44:07 -03:00

125 lines
2.8 KiB
Ruby

# frozen_string_literal: true
module DiscourseAi
module AiBot
module Tools
class Tool
class << self
def signature
raise NotImplemented
end
def name
raise NotImplemented
end
def accepted_options
[]
end
def option(name, type:)
Option.new(tool: self, name: name, type: type)
end
def help
I18n.t("discourse_ai.ai_bot.command_help.#{signature[:name]}")
end
def custom_system_message
nil
end
end
attr_accessor :custom_raw
def initialize(parameters, tool_call_id: "", persona_options: {})
@parameters = parameters
@tool_call_id = tool_call_id
@persona_options = persona_options
end
attr_reader :parameters, :tool_call_id
def name
self.class.name
end
def summary
I18n.t("discourse_ai.ai_bot.command_summary.#{name}")
end
def details
I18n.t("discourse_ai.ai_bot.command_description.#{name}", description_args)
end
def help
I18n.t("discourse_ai.ai_bot.command_help.#{name}")
end
def options
self
.class
.accepted_options
.reduce(HashWithIndifferentAccess.new) do |memo, option|
val = @persona_options[option.name]
memo[option.name] = val if val
memo
end
end
def chain_next_response?
true
end
def standalone?
false
end
def low_cost?
false
end
protected
def accepted_options
[]
end
def option(name, type:)
Option.new(tool: self, name: name, type: type)
end
def description_args
{}
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
end
end
end
end