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_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_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_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_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" ai_stability_api_key: "API key for the stability.ai API"

View File

@ -207,6 +207,18 @@ plugins:
- gpt-3.5-turbo - gpt-3.5-turbo
- gpt-4 - gpt-4
- claude-v1 - 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: ai_helper_add_ai_pm_to_header:
default: true default: true
client: true client: true

View File

@ -133,7 +133,7 @@ module DiscourseAi
[chain_next_response, post] [chain_next_response, post]
end 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? rows = rows.map { |row| yield row } if block_given?
if !column_names if !column_names
@ -154,7 +154,9 @@ module DiscourseAi
# this is not the most efficient format # this is not the most efficient format
# however this is needed cause GPT 3.5 / 4 was steered using JSON # 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 end
protected protected

View File

@ -59,7 +59,7 @@ module DiscourseAi::AiBot::Commands
@last_num_results = parsed.dig("searchInformation", "totalResults").to_i @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"], title: result["title"],
link: result["link"], link: result["link"],

View File

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

View File

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

View File

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

View File

@ -62,7 +62,13 @@ RSpec.describe DiscourseAi::AiBot::Bot do
req_opts: req_opts, 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( OpenAiCompletionsInferenceStubs.stub_streamed_response(
prompt, prompt,
@ -81,7 +87,7 @@ RSpec.describe DiscourseAi::AiBot::Bot do
expect(last.raw).to include("I found nothing") expect(last.raw).to include("I found nothing")
expect(last.post_custom_prompt.custom_prompt).to eq( 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
end end

View File

@ -14,7 +14,8 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do
search = described_class.new(bot_user, post1) search = described_class.new(bot_user, post1)
results = search.process({ query: "order:fake ABDDCDCEDGDG" }.to_json) 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 end
it "supports subfolder properly" do 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) } 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 context "when cleaning usernames" do
it "can properly clean usernames so OpenAI allows it" do it "can properly clean usernames so OpenAI allows it" do
subject.clean_username("test test").should eq("test_test") subject.clean_username("test test").should eq("test_test")