diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 5315a258..6199caf9 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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" diff --git a/config/settings.yml b/config/settings.yml index 13eca12d..557c11c8 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -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 diff --git a/lib/modules/ai_bot/commands/command.rb b/lib/modules/ai_bot/commands/command.rb index e923d7e6..916238a9 100644 --- a/lib/modules/ai_bot/commands/command.rb +++ b/lib/modules/ai_bot/commands/command.rb @@ -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 diff --git a/lib/modules/ai_bot/commands/google_command.rb b/lib/modules/ai_bot/commands/google_command.rb index 4aeffce9..697b6c80 100644 --- a/lib/modules/ai_bot/commands/google_command.rb +++ b/lib/modules/ai_bot/commands/google_command.rb @@ -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"], diff --git a/lib/modules/ai_bot/commands/search_command.rb b/lib/modules/ai_bot/commands/search_command.rb index 7441d17d..09ceb92f 100644 --- a/lib/modules/ai_bot/commands/search_command.rb +++ b/lib/modules/ai_bot/commands/search_command.rb @@ -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, diff --git a/lib/modules/ai_bot/commands/time_command.rb b/lib/modules/ai_bot/commands/time_command.rb index e3d4e0da..e5cb8aa8 100644 --- a/lib/modules/ai_bot/commands/time_command.rb +++ b/lib/modules/ai_bot/commands/time_command.rb @@ -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 diff --git a/lib/modules/ai_bot/open_ai_bot.rb b/lib/modules/ai_bot/open_ai_bot.rb index 0337c747..1b4735d3 100644 --- a/lib/modules/ai_bot/open_ai_bot.rb +++ b/lib/modules/ai_bot/open_ai_bot.rb @@ -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) diff --git a/spec/lib/modules/ai_bot/bot_spec.rb b/spec/lib/modules/ai_bot/bot_spec.rb index 124fad99..289beb5f 100644 --- a/spec/lib/modules/ai_bot/bot_spec.rb +++ b/spec/lib/modules/ai_bot/bot_spec.rb @@ -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 diff --git a/spec/lib/modules/ai_bot/commands/search_command_spec.rb b/spec/lib/modules/ai_bot/commands/search_command_spec.rb index deb55a22..13e3e998 100644 --- a/spec/lib/modules/ai_bot/commands/search_command_spec.rb +++ b/spec/lib/modules/ai_bot/commands/search_command_spec.rb @@ -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 diff --git a/spec/lib/modules/ai_bot/commands/time_command_spec.rb b/spec/lib/modules/ai_bot/commands/time_command_spec.rb new file mode 100644 index 00000000..58b672ce --- /dev/null +++ b/spec/lib/modules/ai_bot/commands/time_command_spec.rb @@ -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 diff --git a/spec/lib/modules/ai_bot/open_ai_bot_spec.rb b/spec/lib/modules/ai_bot/open_ai_bot_spec.rb index b4e2be2f..54a01032 100644 --- a/spec/lib/modules/ai_bot/open_ai_bot_spec.rb +++ b/spec/lib/modules/ai_bot/open_ai_bot_spec.rb @@ -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")