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