FEATURE: allow @mentioning an ai bot in a channel (#602)

if a persona is mentionable and allows chat allow it to be mentioned in a chat channel
This commit is contained in:
Sam 2024-05-07 10:30:39 +10:00 committed by GitHub
parent 37a2db5223
commit 88c7427fab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 94 additions and 27 deletions

View File

@ -11,28 +11,40 @@ module DiscourseAi
REQUIRE_TITLE_UPDATE = "discourse-ai-title-update" REQUIRE_TITLE_UPDATE = "discourse-ai-title-update"
def self.schedule_chat_reply(message, channel, user, context) def self.find_chat_persona(message, channel, user)
if channel.direct_message_channel? if channel.direct_message_channel?
allowed_user_ids = channel.allowed_user_ids AiPersona.allowed_chat.find do |p|
p[:user_id].in?(channel.allowed_user_ids) && (user.group_ids & p[:allowed_group_ids])
return if AiPersona.allowed_chat.any? { |m| m[:user_id] == user.id } end
else
persona = # let's defer on the parse if there is no @ in the message
AiPersona.allowed_chat.find do |p| if message.message.include?("@")
p[:user_id].in?(allowed_user_ids) && (user.group_ids & p[:allowed_group_ids]) mentions = message.parsed_mentions.parsed_direct_mentions
if mentions.present?
AiPersona.allowed_chat.find do |p|
p[:username].in?(mentions) && (user.group_ids & p[:allowed_group_ids])
end
end end
if persona
::Jobs.enqueue(
:create_ai_chat_reply,
channel_id: channel.id,
message_id: message.id,
persona_id: persona[:id],
)
end end
end end
end end
def self.schedule_chat_reply(message, channel, user, context)
return if !SiteSetting.ai_bot_enabled
return if AiPersona.allowed_chat.blank?
return if AiPersona.allowed_chat.any? { |m| m[:user_id] == user.id }
persona = find_chat_persona(message, channel, user)
return if !persona
::Jobs.enqueue(
:create_ai_chat_reply,
channel_id: channel.id,
message_id: message.id,
persona_id: persona[:id],
)
end
def self.is_bot_user_id?(user_id) def self.is_bot_user_id?(user_id)
# this will catch everything and avoid any feedback loops # this will catch everything and avoid any feedback loops
# we could get feedback loops between say discobot and ai-bot or third party plugins # we could get feedback loops between say discobot and ai-bot or third party plugins
@ -209,20 +221,26 @@ module DiscourseAi
def chat_context(message, channel, persona_user) def chat_context(message, channel, persona_user)
has_vision = bot.persona.class.vision_enabled has_vision = bot.persona.class.vision_enabled
if !message.thread_id messages = nil
hash = { type: :user, content: message.message }
hash[:upload_ids] = message.uploads.map(&:id) if has_vision && message.uploads.present?
return [hash]
end
max_messages = 40 max_messages = 40
if bot.persona.class.respond_to?(:max_context_posts) if bot.persona.class.respond_to?(:max_context_posts)
max_messages = bot.persona.class.max_context_posts || 40 max_messages = bot.persona.class.max_context_posts || 40
end end
# I would like to use a guardian however membership for if !message.thread_id && channel.direct_message_channel?
# persona_user is far in future messages = [message]
thread_messages = elsif !channel.direct_message_channel? && !message.thread_id
messages =
Chat::Message
.where(chat_channel_id: channel.id, thread_id: nil)
.order(id: :desc)
.limit(max_messages)
.to_a
.reverse
end
messages ||=
ChatSDK::Thread.last_messages( ChatSDK::Thread.last_messages(
thread_id: message.thread_id, thread_id: message.thread_id,
guardian: Discourse.system_user.guardian, guardian: Discourse.system_user.guardian,
@ -231,7 +249,7 @@ module DiscourseAi
builder = DiscourseAi::Completions::PromptMessagesBuilder.new builder = DiscourseAi::Completions::PromptMessagesBuilder.new
thread_messages.each do |m| messages.each do |m|
if available_bot_user_ids.include?(m.user_id) if available_bot_user_ids.include?(m.user_id)
builder.push(type: :model, content: m.message) builder.push(type: :model, content: m.message)
else else
@ -277,7 +295,8 @@ module DiscourseAi
channel_id: channel.id, channel_id: channel.id,
guardian: guardian, guardian: guardian,
in_reply_to_id: message.id, in_reply_to_id: message.id,
force_thread: message.thread_id.nil?, force_thread: message.thread_id.nil? && channel.direct_message_channel?,
enforce_membership: !channel.direct_message_channel?,
) )
ChatSDK::Message.start_stream(message_id: reply.id, guardian: guardian) ChatSDK::Message.start_stream(message_id: reply.id, guardian: guardian)
else else

View File

@ -131,7 +131,54 @@ RSpec.describe DiscourseAi::AiBot::Playground do
persona persona
end end
context "with chat" do context "with chat channels" do
fab!(:channel) { Fabricate(:chat_channel) }
fab!(:membership) do
Fabricate(:user_chat_channel_membership, user: user, chat_channel: channel)
end
let(:guardian) { Guardian.new(user) }
before do
SiteSetting.ai_bot_enabled = true
SiteSetting.chat_allowed_groups = "#{Group::AUTO_GROUPS[:trust_level_0]}"
Group.refresh_automatic_groups!
persona.update!(allow_chat: true, mentionable: true, default_llm: "anthropic:claude-3-opus")
end
it "should reply to a mention if properly enabled" do
prompts = nil
ChatSDK::Message.create(
channel_id: channel.id,
raw: "This is a story about stuff",
guardian: guardian,
)
DiscourseAi::Completions::Llm.with_prepared_responses(["world"]) do |_, _, _prompts|
ChatSDK::Message.create(
channel_id: channel.id,
raw: "Hello @#{persona.user.username}",
guardian: guardian,
)
prompts = _prompts
end
expect(prompts.length).to eq(1)
prompt = prompts[0]
expect(prompt.messages.length).to eq(2)
expect(prompt.messages[1][:content]).to include("story about stuff")
expect(prompt.messages[1][:content]).to include("Hello")
last_message = Chat::Message.where(chat_channel_id: channel.id).order("id desc").first
expect(last_message.message).to eq("world")
end
end
context "with chat dms" do
fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [user, persona.user]) } fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [user, persona.user]) }
before do before do
@ -142,6 +189,7 @@ RSpec.describe DiscourseAi::AiBot::Playground do
mentionable: false, mentionable: false,
default_llm: "anthropic:claude-3-opus", default_llm: "anthropic:claude-3-opus",
) )
SiteSetting.ai_bot_enabled = true
end end
let(:guardian) { Guardian.new(user) } let(:guardian) { Guardian.new(user) }