FEATURE: add ai_bot_enabled_chat commands and tune search (#94)

* FEATURE: add ai_bot_enabled_chat commands and tune search

This allows admins to disable/enable GPT command integrations.

Also hones search results which were looping cause the result did not denote
the failure properly (it lost context)

* include more context for google command
include more context for time command

* type
This commit is contained in:
Sam 2023-06-21 17:10:30 +10:00 committed by GitHub
parent d1ab79e82f
commit a028309cbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 83 additions and 17 deletions

View File

@ -61,6 +61,7 @@ en:
ai_bot_enabled: "Enable the AI Bot module."
ai_bot_allowed_groups: "When the GPT Bot has access to the PM, it will reply to members of these groups."
ai_bot_enabled_chat_bots: "Available models to act as an AI Bot"
ai_bot_enabled_chat_commands: "Available GPT integrations used to provide external functionality to the model. Only works with GPT-4 and GPT-3.5"
ai_helper_add_ai_pm_to_header: "Display a button in the header to start a PM with a AI Bot"
ai_stability_api_key: "API key for the stability.ai API"

View File

@ -207,6 +207,18 @@ plugins:
- gpt-3.5-turbo
- gpt-4
- claude-v1
ai_bot_enabled_chat_commands:
type: list
default: "categories|google|image|search|tags|time"
client: true
choices:
- categories
- google
- image
- search
- summarize
- tags
- time
ai_helper_add_ai_pm_to_header:
default: true
client: true

View File

@ -133,7 +133,7 @@ module DiscourseAi
[chain_next_response, post]
end
def format_results(rows, column_names = nil)
def format_results(rows, column_names = nil, args: nil)
rows = rows.map { |row| yield row } if block_given?
if !column_names
@ -154,7 +154,9 @@ module DiscourseAi
# this is not the most efficient format
# however this is needed cause GPT 3.5 / 4 was steered using JSON
{ column_names: column_names, rows: rows }
result = { column_names: column_names, rows: rows }
result[:args] = args if args
result
end
protected

View File

@ -59,7 +59,7 @@ module DiscourseAi::AiBot::Commands
@last_num_results = parsed.dig("searchInformation", "totalResults").to_i
format_results(results) do |result|
format_results(results, args: json_data) do |result|
{
title: result["title"],
link: result["link"],

View File

@ -15,7 +15,7 @@ module DiscourseAi::AiBot::Commands
[
Parameter.new(
name: "search_query",
description: "Search query to run against the discourse instance",
description: "Search query (correct bad spelling, remove connector words!)",
type: "string",
),
Parameter.new(
@ -89,8 +89,8 @@ module DiscourseAi::AiBot::Commands
}
end
def process(search_string)
parsed = JSON.parse(search_string)
def process(search_args)
parsed = JSON.parse(search_args)
limit = nil
@ -127,9 +127,9 @@ module DiscourseAi::AiBot::Commands
@last_num_results = posts.length
if posts.blank?
[]
{ args: search_args, rows: [], instruction: "nothing was found, expand your search" }
else
format_results(posts) do |post|
format_results(posts, args: search_args) do |post|
{
title: post.topic.title,
url: Discourse.base_path + post.url,

View File

@ -8,14 +8,14 @@ module DiscourseAi::AiBot::Commands
end
def desc
"!time RUBY_COMPATIBLE_TIMEZONE - will generate the time in a timezone"
"Will generate the time in a timezone"
end
def parameters
[
Parameter.new(
name: "timezone",
description: "Ruby compatible timezone",
description: "ALWAYS supply a Ruby compatible timezone",
type: "string",
required: true,
),
@ -45,7 +45,7 @@ module DiscourseAi::AiBot::Commands
@last_timezone = timezone
@last_time = time.to_s
time.to_s
{ args: args, time: time.to_s }
end
end
end

View File

@ -46,11 +46,12 @@ module DiscourseAi
temperature: temperature,
top_p: top_p,
max_tokens: max_tokens,
functions: available_functions,
) { |key, old_value, new_value| new_value.nil? ? old_value : new_value }
model = model_for(low_cost: prefer_low_cost)
params[:functions] = available_functions if available_functions.present?
DiscourseAi::Inference::OpenAiCompletions.perform!(prompt, model, **params, &blk)
end
@ -87,12 +88,14 @@ module DiscourseAi
end
def available_commands
# note: Summarize command is not ready yet, leave it out for now
@cmds ||=
return @cmds if @cmds
all_commands =
[
Commands::CategoriesCommand,
Commands::TimeCommand,
Commands::SearchCommand,
Commands::SummarizeCommand,
].tap do |cmds|
cmds << Commands::TagsCommand if SiteSetting.tagging_enabled
cmds << Commands::ImageCommand if SiteSetting.ai_stability_api_key.present?
@ -101,6 +104,9 @@ module DiscourseAi
cmds << Commands::GoogleCommand
end
end
allowed_commands = SiteSetting.ai_bot_enabled_chat_commands.split("|")
@cmds = all_commands.filter { |klass| allowed_commands.include?(klass.name) }
end
def model_for(low_cost: false)

View File

@ -62,7 +62,13 @@ RSpec.describe DiscourseAi::AiBot::Bot do
req_opts: req_opts,
)
prompt << { role: "function", content: "[]", name: "search" }
result =
DiscourseAi::AiBot::Commands::SearchCommand
.new(nil, nil)
.process({ query: "test search" }.to_json)
.to_json
prompt << { role: "function", content: result, name: "search" }
OpenAiCompletionsInferenceStubs.stub_streamed_response(
prompt,
@ -81,7 +87,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do
expect(last.raw).to include("I found nothing")
expect(last.post_custom_prompt.custom_prompt).to eq(
[["[]", "search", "function"], ["I found nothing, sorry", bot_user.username]],
[[result, "search", "function"], ["I found nothing, sorry", bot_user.username]],
)
end
end

View File

@ -14,7 +14,8 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do
search = described_class.new(bot_user, post1)
results = search.process({ query: "order:fake ABDDCDCEDGDG" }.to_json)
expect(results).to eq([])
expect(results[:args]).to eq("{\"query\":\"order:fake ABDDCDCEDGDG\"}")
expect(results[:rows]).to eq([])
end
it "supports subfolder properly" do

View File

@ -0,0 +1,17 @@
#frozen_string_literal: true
require_relative "../../../../support/openai_completions_inference_stubs"
RSpec.describe DiscourseAi::AiBot::Commands::TimeCommand do
describe "#process" do
it "can generate correct info" do
freeze_time
args = { timezone: "America/Los_Angeles" }.to_json
info = DiscourseAi::AiBot::Commands::TimeCommand.new(nil, nil).process(args)
expect(info).to eq({ args: args, time: Time.now.in_time_zone("America/Los_Angeles").to_s })
expect(info.to_s).not_to include("not_here")
end
end
end

View File

@ -14,6 +14,27 @@ RSpec.describe DiscourseAi::AiBot::OpenAiBot do
subject { described_class.new(bot_user) }
context "when changing available commands" do
it "contains all commands by default" do
# this will break as we add commands, but it is important as a sanity check
SiteSetting.ai_stability_api_key = "test"
SiteSetting.ai_google_custom_search_api_key = "test"
SiteSetting.ai_google_custom_search_cx = "test"
expect(subject.available_commands.length).to eq(6)
expect(subject.available_commands.length).to eq(
SiteSetting.ai_bot_enabled_chat_commands.split("|").length,
)
end
it "can properly filter out commands" do
SiteSetting.ai_bot_enabled_chat_commands = "time|tags"
expect(subject.available_commands.length).to eq(2)
expect(subject.available_commands).to eq(
[DiscourseAi::AiBot::Commands::TimeCommand, DiscourseAi::AiBot::Commands::TagsCommand],
)
end
end
context "when cleaning usernames" do
it "can properly clean usernames so OpenAI allows it" do
subject.clean_username("test test").should eq("test_test")