FEATURE: defer creation of bot users (#258)

Also fixes it so users without bot in header can send it messages.

Previous to this change we would seed all bots with database seeds.

This lead to lots of confusion for people who do not enable ai bot.

Instead:

1. We do not seed any bots **until** user enables the ai_bot_enabled setting
2. If it is disabled we will
  a. If no messages were created by bot - delete it
  b. Otherwise we will deactivate account
This commit is contained in:
Sam 2023-10-23 17:00:58 +11:00 committed by GitHub
parent 87c591bbc2
commit 1500308437
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 186 additions and 42 deletions

View File

@ -1,5 +1,5 @@
{{#if this.isAiBotChat}} {{#if this.isAiBotChat}}
<DSection @bodyClass="ai-bot-chat" /> <DSection @bodyClass={{this.aiBotClasses}} />
{{#if this.renderChatWarning}} {{#if this.renderChatWarning}}
<div class="ai-bot-chat-warning">{{i18n <div class="ai-bot-chat-warning">{{i18n
"discourse_ai.ai_bot.pm_warning" "discourse_ai.ai_bot.pm_warning"

View File

@ -1,6 +1,7 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { computed } from "@ember/object"; import { computed } from "@ember/object";
import I18n from "discourse-i18n";
export default class extends Component { export default class extends Component {
@service currentUser; @service currentUser;
@ -14,6 +15,18 @@ export default class extends Component {
return this.siteSettings.ai_bot_enable_chat_warning; return this.siteSettings.ai_bot_enable_chat_warning;
} }
@computed("composerModel.targetRecipients", "composerModel.title")
get aiBotClasses() {
if (
this.composerModel?.title ===
I18n.t("discourse_ai.ai_bot.default_pm_prefix")
) {
return "ai-bot-chat";
} else {
return "ai-bot-pm";
}
}
@computed("composerModel.targetRecipients") @computed("composerModel.targetRecipients")
get isAiBotChat() { get isAiBotChat() {
if ( if (

View File

@ -15,6 +15,18 @@ nav.post-controls .actions button.cancel-streaming {
} }
} }
.ai-bot-pm {
.gpt-persona {
margin-bottom: 5px;
}
#reply-control .composer-fields {
.mini-tag-chooser,
.add-warning {
display: none;
}
}
}
.ai-bot-chat-warning { .ai-bot-chat-warning {
color: var(--tertiary); color: var(--tertiary);
background-color: var(--tertiary-low); background-color: var(--tertiary-low);

View File

@ -1,28 +1,3 @@
# frozen_string_literal: true # frozen_string_literal: true
DiscourseAi::AiBot::EntryPoint::BOTS.each do |id, bot_username| DiscourseAi::AiBot::SiteSettingsExtension.enable_or_disable_ai_bots
# let's not create a bot user if it already exists
# seed seems to be messing with dates on the user
# causing it to look like these bots were created at the
# wrong time
if !User.exists?(id: id)
UserEmail.seed do |ue|
ue.id = id
ue.email = "no_email_#{bot_username}"
ue.primary = true
ue.user_id = id
end
User.seed do |u|
u.id = id
u.name = bot_username.titleize
u.username = UserNameSuggester.suggest(bot_username)
u.password = SecureRandom.hex
u.active = true
u.admin = true
u.moderator = true
u.approved = true
u.trust_level = TrustLevel[4]
end
end
end

View File

@ -8,7 +8,11 @@ module DiscourseAi
GPT4_ID = -110 GPT4_ID = -110
GPT3_5_TURBO_ID = -111 GPT3_5_TURBO_ID = -111
CLAUDE_V2_ID = -112 CLAUDE_V2_ID = -112
BOTS = [[GPT4_ID, "gpt4_bot"], [GPT3_5_TURBO_ID, "gpt3.5_bot"], [CLAUDE_V2_ID, "claude_bot"]] BOTS = [
[GPT4_ID, "gpt4_bot", "gpt-4"],
[GPT3_5_TURBO_ID, "gpt3.5_bot", "gpt-3.5-turbo"],
[CLAUDE_V2_ID, "claude_bot", "claude-2"],
]
def self.map_bot_model_to_user_id(model_name) def self.map_bot_model_to_user_id(model_name)
case model_name case model_name
@ -48,9 +52,16 @@ module DiscourseAi
require_relative "personas/settings_explorer" require_relative "personas/settings_explorer"
require_relative "personas/researcher" require_relative "personas/researcher"
require_relative "personas/creative" require_relative "personas/creative"
require_relative "site_settings_extension"
end end
def inject_into(plugin) def inject_into(plugin)
plugin.on(:site_setting_changed) do |name, _old_value, _new_value|
if name == :ai_bot_enabled_chat_bots || name == :ai_bot_enabled
DiscourseAi::AiBot::SiteSettingsExtension.enable_or_disable_ai_bots
end
end
plugin.register_seedfu_fixtures( plugin.register_seedfu_fixtures(
Rails.root.join("plugins", "discourse-ai", "db", "fixtures", "ai_bot"), Rails.root.join("plugins", "discourse-ai", "db", "fixtures", "ai_bot"),
) )

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module DiscourseAi::AiBot::SiteSettingsExtension
def self.enable_or_disable_ai_bots
enabled_bots = SiteSetting.ai_bot_enabled_chat_bots_map
enabled_bots = [] if !SiteSetting.ai_bot_enabled
DiscourseAi::AiBot::EntryPoint::BOTS.each do |id, bot_name, name|
active = enabled_bots.include?(name)
user = User.find_by(id: id)
if active
if !user
user =
User.new(
id: id,
email: "no_email_#{name}",
name: bot_name.titleize,
username: UserNameSuggester.suggest(bot_name),
active: true,
approved: true,
admin: true,
moderator: true,
trust_level: TrustLevel[4],
)
user.save!(validate: false)
else
user.update!(active: true)
end
elsif !active && user
# will include deleted
has_posts = DB.query_single("SELECT 1 FROM posts WHERE user_id = #{id} LIMIT 1").present?
if has_posts
user.update!(active: false) if user.active
else
user.destroy
end
end
end
end
end

View File

@ -7,6 +7,11 @@ module ::DiscourseAi
User.find(EntryPoint::CLAUDE_V2_ID) User.find(EntryPoint::CLAUDE_V2_ID)
end end
before do
SiteSetting.ai_bot_enabled_chat_bots = "claude-2"
SiteSetting.ai_bot_enabled = true
end
let(:bot) { described_class.new(bot_user) } let(:bot) { described_class.new(bot_user) }
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }

View File

@ -39,7 +39,12 @@ class FakeBot < DiscourseAi::AiBot::Bot
end end
describe FakeBot do describe FakeBot do
fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT4_ID) } before do
SiteSetting.ai_bot_enabled_chat_bots = "gpt-4"
SiteSetting.ai_bot_enabled = true
end
let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT4_ID) }
fab!(:post) { Fabricate(:post, raw: "hello world") } fab!(:post) { Fabricate(:post, raw: "hello world") }
it "can handle command truncation for long messages" do it "can handle command truncation for long messages" do
@ -78,11 +83,16 @@ describe FakeBot do
end end
describe DiscourseAi::AiBot::Bot do describe DiscourseAi::AiBot::Bot do
fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT4_ID) } before do
fab!(:bot) { described_class.as(bot_user) } SiteSetting.ai_bot_enabled_chat_bots = "gpt-4"
SiteSetting.ai_bot_enabled = true
end
let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT4_ID) }
let(:bot) { described_class.as(bot_user) }
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
fab!(:pm) do let!(:pm) do
Fabricate( Fabricate(
:private_message_topic, :private_message_topic,
title: "This is my special PM", title: "This is my special PM",
@ -93,8 +103,8 @@ describe DiscourseAi::AiBot::Bot do
], ],
) )
end end
fab!(:first_post) { Fabricate(:post, topic: pm, user: user, raw: "This is a reply by the user") } let!(:first_post) { Fabricate(:post, topic: pm, user: user, raw: "This is a reply by the user") }
fab!(:second_post) do let!(:second_post) do
Fabricate(:post, topic: pm, user: user, raw: "This is a second reply by the user") Fabricate(:post, topic: pm, user: user, raw: "This is a second reply by the user")
end end

View File

@ -3,9 +3,11 @@
require_relative "../../../../support/openai_completions_inference_stubs" require_relative "../../../../support/openai_completions_inference_stubs"
RSpec.describe DiscourseAi::AiBot::Commands::Command do RSpec.describe DiscourseAi::AiBot::Commands::Command do
fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) } let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) }
let(:command) { DiscourseAi::AiBot::Commands::GoogleCommand.new(bot_user: bot_user, args: nil) } let(:command) { DiscourseAi::AiBot::Commands::GoogleCommand.new(bot_user: bot_user, args: nil) }
before { SiteSetting.ai_bot_enabled = true }
describe "#format_results" do describe "#format_results" do
it "can generate efficient tables of data" do it "can generate efficient tables of data" do
rows = [1, 2, 3, 4, 5] rows = [1, 2, 3, 4, 5]

View File

@ -1,7 +1,9 @@
#frozen_string_literal: true #frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Commands::GoogleCommand do RSpec.describe DiscourseAi::AiBot::Commands::GoogleCommand do
fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) } let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) }
before { SiteSetting.ai_bot_enabled = true }
describe "#process" do describe "#process" do
it "will not explode if there are no results" do it "will not explode if there are no results" do

View File

@ -3,7 +3,9 @@
require_relative "../../../../support/stable_difussion_stubs" require_relative "../../../../support/stable_difussion_stubs"
RSpec.describe DiscourseAi::AiBot::Commands::ImageCommand do RSpec.describe DiscourseAi::AiBot::Commands::ImageCommand do
fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) } let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) }
before { SiteSetting.ai_bot_enabled = true }
describe "#process" do describe "#process" do
it "can generate correct info" do it "can generate correct info" do

View File

@ -1,7 +1,7 @@
#frozen_string_literal: true #frozen_string_literal: true
RSpec.describe DiscourseAi::AiBot::Commands::ReadCommand do RSpec.describe DiscourseAi::AiBot::Commands::ReadCommand do
fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) } let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) }
fab!(:parent_category) { Fabricate(:category, name: "animals") } fab!(:parent_category) { Fabricate(:category, name: "animals") }
fab!(:category) { Fabricate(:category, parent_category: parent_category, name: "amazing-cat") } fab!(:category) { Fabricate(:category, parent_category: parent_category, name: "amazing-cat") }
@ -22,6 +22,8 @@ RSpec.describe DiscourseAi::AiBot::Commands::ReadCommand do
Fabricate(:topic, category: category, tags: [tag_funny, tag_sad, tag_hidden]) Fabricate(:topic, category: category, tags: [tag_funny, tag_sad, tag_hidden])
end end
before { SiteSetting.ai_bot_enabled = true }
describe "#process" do describe "#process" do
it "can read a topic" do it "can read a topic" do
topic_id = topic_with_tags.id topic_id = topic_with_tags.id

View File

@ -28,6 +28,8 @@ RSpec.describe DiscourseAi::AiBot::Commands::SearchCommand do
Fabricate(:topic, category: category, tags: [tag_funny, tag_sad, tag_hidden]) Fabricate(:topic, category: category, tags: [tag_funny, tag_sad, tag_hidden])
end end
before { SiteSetting.ai_bot_enabled = true }
describe "#process" do describe "#process" do
it "can handle no results" do it "can handle no results" do
post1 = Fabricate(:post, topic: topic_with_tags) post1 = Fabricate(:post, topic: topic_with_tags)

View File

@ -3,7 +3,9 @@
require_relative "../../../../support/openai_completions_inference_stubs" require_relative "../../../../support/openai_completions_inference_stubs"
RSpec.describe DiscourseAi::AiBot::Commands::SummarizeCommand do RSpec.describe DiscourseAi::AiBot::Commands::SummarizeCommand do
fab!(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) } let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID) }
before { SiteSetting.ai_bot_enabled = true }
describe "#process" do describe "#process" do
it "can generate correct info" do it "can generate correct info" do

View File

@ -17,6 +17,8 @@ RSpec.describe DiscourseAi::AiBot::EntryPoint do
end end
before do before do
SiteSetting.ai_bot_enabled_chat_bots = "gpt-4|claude-2"
SiteSetting.ai_bot_enabled = true
SiteSetting.ai_bot_allowed_groups = bot_allowed_group.id SiteSetting.ai_bot_allowed_groups = bot_allowed_group.id
bot_allowed_group.add(admin) bot_allowed_group.add(admin)
end end

View File

@ -7,6 +7,7 @@ RSpec.describe Jobs::CreateAiReply do
before do before do
# got to do this cause we include times in system message # got to do this cause we include times in system message
freeze_time freeze_time
SiteSetting.ai_bot_enabled = true
end end
describe "#execute" do describe "#execute" do
@ -78,6 +79,7 @@ RSpec.describe Jobs::CreateAiReply do
let(:deltas) { claude_response.split(" ").map { |w| "#{w} " } } let(:deltas) { claude_response.split(" ").map { |w| "#{w} " } }
before do before do
SiteSetting.ai_bot_enabled_chat_bots = "claude-2"
bot_user = User.find(DiscourseAi::AiBot::EntryPoint::CLAUDE_V2_ID) bot_user = User.find(DiscourseAi::AiBot::EntryPoint::CLAUDE_V2_ID)
AnthropicCompletionStubs.stub_streamed_response( AnthropicCompletionStubs.stub_streamed_response(

View File

@ -4,6 +4,11 @@ RSpec.describe Jobs::UpdateAiBotPmTitle do
let(:user) { Fabricate(:admin) } let(:user) { Fabricate(:admin) }
let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::CLAUDE_V2_ID) } let(:bot_user) { User.find(DiscourseAi::AiBot::EntryPoint::CLAUDE_V2_ID) }
before do
SiteSetting.ai_bot_enabled_chat_bots = "claude-2"
SiteSetting.ai_bot_enabled = true
end
it "will properly update title on bot PMs" do it "will properly update title on bot PMs" do
SiteSetting.ai_bot_allowed_groups = Group::AUTO_GROUPS[:staff] SiteSetting.ai_bot_allowed_groups = Group::AUTO_GROUPS[:staff]

View File

@ -14,6 +14,11 @@ RSpec.describe DiscourseAi::AiBot::OpenAiBot do
subject { described_class.new(bot_user) } subject { described_class.new(bot_user) }
before do
SiteSetting.ai_bot_enabled_chat_bots = "gpt-4"
SiteSetting.ai_bot_enabled = true
end
context "when changing available commands" do context "when changing available commands" do
it "contains all commands by default" do it "contains all commands by default" do
# this will break as we add commands, but it is important as a sanity check # this will break as we add commands, but it is important as a sanity check
@ -69,11 +74,11 @@ RSpec.describe DiscourseAi::AiBot::OpenAiBot do
end end
context "when the topic has multiple posts" do context "when the topic has multiple posts" do
fab!(:post_1) { Fabricate(:post, topic: topic, raw: post_body(1), post_number: 1) } let!(:post_1) { Fabricate(:post, topic: topic, raw: post_body(1), post_number: 1) }
fab!(:post_2) do let!(:post_2) do
Fabricate(:post, topic: topic, user: bot_user, raw: post_body(2), post_number: 2) Fabricate(:post, topic: topic, user: bot_user, raw: post_body(2), post_number: 2)
end end
fab!(:post_3) { Fabricate(:post, topic: topic, raw: post_body(3), post_number: 3) } let!(:post_3) { Fabricate(:post, topic: topic, raw: post_body(3), post_number: 3) }
it "includes them in the prompt respecting the post number order" do it "includes them in the prompt respecting the post number order" do
prompt_messages = subject.bot_prompt_with_topic_context(post_3) prompt_messages = subject.bot_prompt_with_topic_context(post_3)

View File

@ -0,0 +1,50 @@
#frozen_string_literal: true
describe DiscourseAi::AiBot::SiteSettingsExtension do
it "correctly creates/deletes bot accounts as needed" do
SiteSetting.ai_bot_enabled = true
SiteSetting.ai_bot_enabled_chat_bots = "gpt-4"
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::GPT4_ID)).to eq(true)
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID)).to eq(false)
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::CLAUDE_V2_ID)).to eq(false)
SiteSetting.ai_bot_enabled_chat_bots = "gpt-3.5-turbo"
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::GPT4_ID)).to eq(false)
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID)).to eq(true)
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::CLAUDE_V2_ID)).to eq(false)
SiteSetting.ai_bot_enabled_chat_bots = "gpt-3.5-turbo|claude-2"
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::GPT4_ID)).to eq(false)
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID)).to eq(true)
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::CLAUDE_V2_ID)).to eq(true)
SiteSetting.ai_bot_enabled = false
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::GPT4_ID)).to eq(false)
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID)).to eq(false)
expect(User.exists?(id: DiscourseAi::AiBot::EntryPoint::CLAUDE_V2_ID)).to eq(false)
end
it "leaves accounts around if they have any posts" do
SiteSetting.ai_bot_enabled = true
SiteSetting.ai_bot_enabled_chat_bots = "gpt-4"
user = User.find(DiscourseAi::AiBot::EntryPoint::GPT4_ID)
create_post(user: user, raw: "this is a test post")
user.reload
SiteSetting.ai_bot_enabled = false
user.reload
expect(user.active).to eq(false)
SiteSetting.ai_bot_enabled = true
user.reload
expect(user.active).to eq(true)
end
end

View File

@ -30,6 +30,7 @@ RSpec.describe DiscourseAi::AiBot::BotController do
describe "#show_bot_username" do describe "#show_bot_username" do
it "returns the username_lower of the selected bot" do it "returns the username_lower of the selected bot" do
SiteSetting.ai_bot_enabled = true
gpt_3_5_bot = "gpt-3.5-turbo" gpt_3_5_bot = "gpt-3.5-turbo"
expected_username = User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID).username_lower expected_username = User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID).username_lower