diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 68efd113..5eebaf66 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -100,7 +100,7 @@ en: command_description: time: "Time in %{timezone} is %{time}" summarize: "Summarized %{title}" - image: "Prompt: %{prompt}" + image: "%{prompt}" categories: one: "Found %{count} category" other: "Found %{count} categories" @@ -115,6 +115,6 @@ en: other: "Found %{count} results for '%{query}'" summarization: - configuration_hint: + configuration_hint: one: "Configure the `%{setting}` setting first." other: "Configure these settings first: %{settings}" diff --git a/lib/modules/ai_bot/bot.rb b/lib/modules/ai_bot/bot.rb index 329877c7..edb34bb0 100644 --- a/lib/modules/ai_bot/bot.rb +++ b/lib/modules/ai_bot/bot.rb @@ -25,7 +25,7 @@ module DiscourseAi attr_reader :bot_user BOT_NOT_FOUND = Class.new(StandardError) - MAX_COMPLETIONS = 3 + MAX_COMPLETIONS = 6 def self.as(bot_user) available_bots = [DiscourseAi::AiBot::OpenAiBot, DiscourseAi::AiBot::AnthropicBot] @@ -79,6 +79,7 @@ module DiscourseAi end redis_stream_key = nil + partial_reply = +"" reply = +(bot_reply_post ? bot_reply_post.raw.dup : "") start = Time.now @@ -87,7 +88,9 @@ module DiscourseAi functions = Functions.new submit_prompt(prompt, prefer_low_cost: prefer_low_cost) do |partial, cancel| - reply << get_delta(partial, context) + current_delta = get_delta(partial, context) + partial_reply << current_delta + reply << current_delta populate_functions(partial, functions) if redis_stream_key && !Discourse.redis.get(redis_stream_key) @@ -135,7 +138,8 @@ module DiscourseAi bot_reply_post.post_custom_prompt ||= post.build_post_custom_prompt(custom_prompt: []) prompt = post.post_custom_prompt.custom_prompt || [] - prompt << build_message(bot_user.username, reply) + prompt << [partial_reply, bot_user.username] + post.post_custom_prompt.update!(custom_prompt: prompt) end diff --git a/lib/modules/ai_bot/commands/image_command.rb b/lib/modules/ai_bot/commands/image_command.rb index 6a0bcdc6..b1be713f 100644 --- a/lib/modules/ai_bot/commands/image_command.rb +++ b/lib/modules/ai_bot/commands/image_command.rb @@ -36,7 +36,7 @@ module DiscourseAi::AiBot::Commands end def description_args - { prompt: @last_prompt || 0 } + { prompt: @last_prompt } end def chain_next_response diff --git a/lib/modules/ai_bot/commands/search_command.rb b/lib/modules/ai_bot/commands/search_command.rb index 284d35e8..7441d17d 100644 --- a/lib/modules/ai_bot/commands/search_command.rb +++ b/lib/modules/ai_bot/commands/search_command.rb @@ -20,7 +20,8 @@ module DiscourseAi::AiBot::Commands ), Parameter.new( name: "user", - description: "Filter search results to this username", + description: + "Filter search results to this username (only include if user explicitly asks to filter by user)", type: "string", ), Parameter.new( @@ -31,7 +32,8 @@ module DiscourseAi::AiBot::Commands ), Parameter.new( name: "limit", - description: "limit number of results returned", + description: + "limit number of results returned (generally prefer to just keep to default)", type: "integer", ), Parameter.new( diff --git a/lib/modules/ai_bot/open_ai_bot.rb b/lib/modules/ai_bot/open_ai_bot.rb index 36dd7dc2..0337c747 100644 --- a/lib/modules/ai_bot/open_ai_bot.rb +++ b/lib/modules/ai_bot/open_ai_bot.rb @@ -87,12 +87,12 @@ module DiscourseAi end def available_commands + # note: Summarize command is not ready yet, leave it out for now @cmds ||= [ 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? @@ -108,6 +108,16 @@ module DiscourseAi "gpt-3.5-turbo-16k" end + def clean_username(username) + if username.match?(/\0[a-zA-Z0-9_-]{1,64}\z/) + username + else + # not the best in the world, but this is what we have to work with + # if sites enable unicode usernames this can get messy + username.gsub(/[^a-zA-Z0-9_-]/, "_")[0..63] + end + end + private def populate_functions(partial, functions) @@ -133,9 +143,9 @@ module DiscourseAi if function result[:name] = poster_username - elsif !system && poster_username != bot_user.username + elsif !system && poster_username != bot_user.username && poster_username.present? # Open AI restrict name to 64 chars and only A-Za-z._ (work around) - result[:content] = "#{poster_username}: #{content}" + result[:name] = clean_username(poster_username) end result diff --git a/spec/lib/modules/ai_bot/bot_spec.rb b/spec/lib/modules/ai_bot/bot_spec.rb index 4fb735ea..124fad99 100644 --- a/spec/lib/modules/ai_bot/bot_spec.rb +++ b/spec/lib/modules/ai_bot/bot_spec.rb @@ -80,7 +80,9 @@ RSpec.describe DiscourseAi::AiBot::Bot do expect(last.raw).not_to include("translation missing") expect(last.raw).to include("I found nothing") - expect(last.post_custom_prompt.custom_prompt.to_s).to include("I found nothing") + expect(last.post_custom_prompt.custom_prompt).to eq( + [["[]", "search", "function"], ["I found nothing, sorry", bot_user.username]], + ) end end diff --git a/spec/lib/modules/ai_bot/jobs/regular/create_ai_reply_spec.rb b/spec/lib/modules/ai_bot/jobs/regular/create_ai_reply_spec.rb index ea889326..b5cece38 100644 --- a/spec/lib/modules/ai_bot/jobs/regular/create_ai_reply_spec.rb +++ b/spec/lib/modules/ai_bot/jobs/regular/create_ai_reply_spec.rb @@ -4,6 +4,11 @@ require_relative "../../../../../support/openai_completions_inference_stubs" require_relative "../../../../../support/anthropic_completion_stubs" RSpec.describe Jobs::CreateAiReply do + before do + # got to do this cause we include times in system message + freeze_time + end + describe "#execute" do fab!(:topic) { Fabricate(:topic) } fab!(:post) { Fabricate(:post, topic: topic) } 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 90d82471..b4e2be2f 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,14 @@ RSpec.describe DiscourseAi::AiBot::OpenAiBot do subject { described_class.new(bot_user) } + context "when cleaning usernames" 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") + subject.clean_username("test๐Ÿ˜€test").should eq("test_test") + end + end + context "when the topic has one post" do fab!(:post_1) { Fabricate(:post, topic: topic, raw: post_body(1), post_number: 1) } @@ -23,7 +31,8 @@ RSpec.describe DiscourseAi::AiBot::OpenAiBot do post_1_message = prompt_messages[-1] expect(post_1_message[:role]).to eq("user") - expect(post_1_message[:content]).to eq("#{post_1.user.username}: #{post_body(1)}") + expect(post_1_message[:content]).to eq(post_body(1)) + expect(post_1_message[:name]).to eq(post_1.user.username) end end @@ -51,13 +60,15 @@ RSpec.describe DiscourseAi::AiBot::OpenAiBot do # negative cause we may have grounding prompts expect(prompt_messages[-3][:role]).to eq("user") - expect(prompt_messages[-3][:content]).to eq("#{post_1.username}: #{post_body(1)}") + expect(prompt_messages[-3][:content]).to eq(post_body(1)) + expect(prompt_messages[-3][:name]).to eq(post_1.username) expect(prompt_messages[-2][:role]).to eq("assistant") expect(prompt_messages[-2][:content]).to eq(post_body(2)) expect(prompt_messages[-1][:role]).to eq("user") - expect(prompt_messages[-1][:content]).to eq("#{post_3.username}: #{post_body(3)}") + expect(prompt_messages[-1][:content]).to eq(post_body(3)) + expect(prompt_messages[-1][:name]).to eq(post_3.username) end end end