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}}
<DSection @bodyClass="ai-bot-chat" />
<DSection @bodyClass={{this.aiBotClasses}} />
{{#if this.renderChatWarning}}
<div class="ai-bot-chat-warning">{{i18n
"discourse_ai.ai_bot.pm_warning"

View File

@ -1,6 +1,7 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import { computed } from "@ember/object";
import I18n from "discourse-i18n";
export default class extends Component {
@service currentUser;
@ -14,6 +15,18 @@ export default class extends Component {
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")
get isAiBotChat() {
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 {
color: var(--tertiary);
background-color: var(--tertiary-low);

View File

@ -1,28 +1,3 @@
# frozen_string_literal: true
DiscourseAi::AiBot::EntryPoint::BOTS.each do |id, bot_username|
# 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
DiscourseAi::AiBot::SiteSettingsExtension.enable_or_disable_ai_bots

View File

@ -8,7 +8,11 @@ module DiscourseAi
GPT4_ID = -110
GPT3_5_TURBO_ID = -111
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)
case model_name
@ -48,9 +52,16 @@ module DiscourseAi
require_relative "personas/settings_explorer"
require_relative "personas/researcher"
require_relative "personas/creative"
require_relative "site_settings_extension"
end
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(
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)
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(:post) { Fabricate(:post) }

View File

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

View File

@ -3,9 +3,11 @@
require_relative "../../../../support/openai_completions_inference_stubs"
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) }
before { SiteSetting.ai_bot_enabled = true }
describe "#format_results" do
it "can generate efficient tables of data" do
rows = [1, 2, 3, 4, 5]

View File

@ -1,7 +1,9 @@
#frozen_string_literal: true
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
it "will not explode if there are no results" do

View File

@ -3,7 +3,9 @@
require_relative "../../../../support/stable_difussion_stubs"
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
it "can generate correct info" do

View File

@ -1,7 +1,7 @@
#frozen_string_literal: true
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!(: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])
end
before { SiteSetting.ai_bot_enabled = true }
describe "#process" do
it "can read a topic" do
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])
end
before { SiteSetting.ai_bot_enabled = true }
describe "#process" do
it "can handle no results" do
post1 = Fabricate(:post, topic: topic_with_tags)

View File

@ -3,7 +3,9 @@
require_relative "../../../../support/openai_completions_inference_stubs"
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
it "can generate correct info" do

View File

@ -17,6 +17,8 @@ RSpec.describe DiscourseAi::AiBot::EntryPoint do
end
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
bot_allowed_group.add(admin)
end

View File

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

View File

@ -4,6 +4,11 @@ RSpec.describe Jobs::UpdateAiBotPmTitle do
let(:user) { Fabricate(:admin) }
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
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) }
before do
SiteSetting.ai_bot_enabled_chat_bots = "gpt-4"
SiteSetting.ai_bot_enabled = true
end
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
@ -69,11 +74,11 @@ RSpec.describe DiscourseAi::AiBot::OpenAiBot do
end
context "when the topic has multiple posts" do
fab!(:post_1) { Fabricate(:post, topic: topic, raw: post_body(1), post_number: 1) }
fab!(:post_2) do
let!(:post_1) { Fabricate(:post, topic: topic, raw: post_body(1), post_number: 1) }
let!(:post_2) do
Fabricate(:post, topic: topic, user: bot_user, raw: post_body(2), post_number: 2)
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
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
it "returns the username_lower of the selected bot" do
SiteSetting.ai_bot_enabled = true
gpt_3_5_bot = "gpt-3.5-turbo"
expected_username = User.find(DiscourseAi::AiBot::EntryPoint::GPT3_5_TURBO_ID).username_lower