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:
parent
d1ab79e82f
commit
a028309cbd
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue