mirror of
https://github.com/discourse/discourse-ai.git
synced 2025-03-09 11:48:47 +00:00
* 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
161 lines
4.2 KiB
Ruby
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
|