discourse-ai/lib/ai_bot/tools/setting_context.rb
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

161 lines
4.2 KiB
Ruby

#frozen_string_literal: true
module DiscourseAi
module AiBot
module Tools
class SettingContext < Tool
MAX_CONTEXT_TOKENS = 2000
CODE_FILE_EXTENSIONS = "rb,js,gjs,hbs"
class << self
def rg_installed?
if defined?(@rg_installed)
@rg_installed
else
@rg_installed =
begin
Discourse::Utils.execute_command("which", "rg")
true
rescue Discourse::Utils::CommandError
false
end
end
end
def signature
{
name: name,
description:
"Will provide you with full context regarding a particular site setting in Discourse",
parameters: [
{
name: "setting_name",
description: "The name of the site setting we need context for",
type: "string",
required: true,
},
],
}
end
def name
"setting_context"
end
end
def setting_name
parameters[:setting_name]
end
def invoke(_bot_user, llm)
if !self.class.rg_installed?
return(
{
setting_name: setting_name,
context:
"This command requires the rg command line tool to be installed on the server",
}
)
end
if !SiteSetting.has_setting?(setting_name)
{ setting_name: setting_name, context: "This setting does not exist" }
else
description = SiteSetting.description(setting_name)
result = +"# #{setting_name}\n#{description}\n\n"
setting_info =
find_setting_info(setting_name, [Rails.root.join("config", "site_settings.yml").to_s])
if !setting_info
setting_info =
find_setting_info(setting_name, Dir[Rails.root.join("plugins/**/settings.yml")])
end
result << setting_info
result << "\n\n"
%w[lib app plugins].each do |dir|
path = Rails.root.join(dir).to_s
result << Discourse::Utils.execute_command(
"rg",
setting_name,
path,
"-g",
"!**/spec/**",
"-g",
"!**/dist/**",
"-g",
"*.{#{CODE_FILE_EXTENSIONS}}",
"-C",
"10",
"--color",
"never",
"--heading",
"--no-ignore",
chdir: path,
success_status_codes: [0, 1],
)
end
result.gsub!(/^#{Regexp.escape(Rails.root.to_s)}/, "")
result = llm.tokenizer.truncate(result, MAX_CONTEXT_TOKENS)
{ setting_name: setting_name, context: result }
end
end
private
def find_setting_info(name, paths)
path, result = nil
paths.each do |search_path|
result =
Discourse::Utils.execute_command(
"rg",
name,
search_path,
"-g",
"*.{#{CODE_FILE_EXTENSIONS}}",
"-A",
"10",
"--color",
"never",
"--heading",
success_status_codes: [0, 1],
)
if !result.blank?
path = search_path
break
end
end
if result.blank?
nil
else
rows = result.split("\n")
leading_spaces = rows[0].match(/^\s*/)[0].length
filtered = []
rows.each do |row|
if !filtered.blank?
break if row.match(/^\s*/)[0].length <= leading_spaces
end
filtered << row
end
filtered.unshift("#{path}")
filtered.join("\n")
end
end
def description_args
parameters.slice(:setting_name)
end
end
end
end
end