FEATURE: LLM mentions and auto silence (#949)
* FEATURE: allow mentioning an LLM mid conversation to switch This is a edgecase feature that allow you to start a conversation in a PM with LLM1 and then use LLM2 to evaluation or continue the conversation * FEATURE: allow auto silencing of spam accounts New rule can also allow for silencing an account automatically This can prevent spammers from creating additional posts.
This commit is contained in:
parent
6c25718a7f
commit
616b990894
|
@ -4,6 +4,7 @@ en:
|
||||||
flag_types:
|
flag_types:
|
||||||
review: "Add post to review queue"
|
review: "Add post to review queue"
|
||||||
spam: "Flag as spam and hide post"
|
spam: "Flag as spam and hide post"
|
||||||
|
spam_silence: "Flag as spam, hide post and silence user"
|
||||||
scriptables:
|
scriptables:
|
||||||
llm_triage:
|
llm_triage:
|
||||||
title: Triage posts using AI
|
title: Triage posts using AI
|
||||||
|
|
|
@ -78,11 +78,16 @@ module DiscourseAi
|
||||||
bot_user = nil
|
bot_user = nil
|
||||||
mentioned = nil
|
mentioned = nil
|
||||||
|
|
||||||
all_llm_user_ids = LlmModel.joins(:user).pluck("users.id")
|
all_llm_users =
|
||||||
|
LlmModel
|
||||||
|
.where(enabled_chat_bot: true)
|
||||||
|
.joins(:user)
|
||||||
|
.pluck("users.id", "users.username_lower")
|
||||||
|
|
||||||
if post.topic.private_message?
|
if post.topic.private_message?
|
||||||
# this is an edge case, you started a PM with a different bot
|
# this is an edge case, you started a PM with a different bot
|
||||||
bot_user = post.topic.topic_allowed_users.where(user_id: all_llm_user_ids).first&.user
|
bot_user =
|
||||||
|
post.topic.topic_allowed_users.where(user_id: all_llm_users.map(&:first)).first&.user
|
||||||
bot_user ||=
|
bot_user ||=
|
||||||
post
|
post
|
||||||
.topic
|
.topic
|
||||||
|
@ -92,14 +97,17 @@ module DiscourseAi
|
||||||
&.user
|
&.user
|
||||||
end
|
end
|
||||||
|
|
||||||
if mentionables.present?
|
mentions = nil
|
||||||
|
if mentionables.present? || (bot_user && post.topic.private_message?)
|
||||||
mentions = post.mentions.map(&:downcase)
|
mentions = post.mentions.map(&:downcase)
|
||||||
|
|
||||||
# in case we are replying to a post by a bot
|
# in case we are replying to a post by a bot
|
||||||
if post.reply_to_post_number && post.reply_to_post&.user
|
if post.reply_to_post_number && post.reply_to_post&.user
|
||||||
mentions << post.reply_to_post.user.username_lower
|
mentions << post.reply_to_post.user.username_lower
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if mentionables.present?
|
||||||
mentioned = mentionables.find { |mentionable| mentions.include?(mentionable[:username]) }
|
mentioned = mentionables.find { |mentionable| mentions.include?(mentionable[:username]) }
|
||||||
|
|
||||||
# direct PM to mentionable
|
# direct PM to mentionable
|
||||||
|
@ -117,7 +125,9 @@ module DiscourseAi
|
||||||
end
|
end
|
||||||
|
|
||||||
if bot_user
|
if bot_user
|
||||||
persona_id = mentioned&.dig(:id) || post.topic.custom_fields["ai_persona_id"]
|
topic_persona_id = post.topic.custom_fields["ai_persona_id"]
|
||||||
|
persona_id = mentioned&.dig(:id) || topic_persona_id
|
||||||
|
|
||||||
persona = nil
|
persona = nil
|
||||||
|
|
||||||
if persona_id
|
if persona_id
|
||||||
|
@ -130,6 +140,19 @@ module DiscourseAi
|
||||||
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, name: persona_name)
|
DiscourseAi::AiBot::Personas::Persona.find_by(user: post.user, name: persona_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# edge case, llm was mentioned in an ai persona conversation
|
||||||
|
if persona_id == topic_persona_id.to_i && post.topic.private_message? && persona &&
|
||||||
|
all_llm_users.present?
|
||||||
|
if !persona.force_default_llm && mentions.present?
|
||||||
|
mentioned_llm_user_id, _ =
|
||||||
|
all_llm_users.find { |id, username| mentions.include?(username) }
|
||||||
|
|
||||||
|
if mentioned_llm_user_id
|
||||||
|
bot_user = User.find_by(id: mentioned_llm_user_id) || bot_user
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
persona ||= DiscourseAi::AiBot::Personas::General
|
persona ||= DiscourseAi::AiBot::Personas::General
|
||||||
|
|
||||||
bot_user = User.find(persona.user_id) if persona && persona.force_default_llm
|
bot_user = User.find(persona.user_id) if persona && persona.force_default_llm
|
||||||
|
|
|
@ -6,6 +6,10 @@ module DiscourseAi
|
||||||
[
|
[
|
||||||
{ id: "review", translated_name: I18n.t("discourse_automation.ai.flag_types.review") },
|
{ id: "review", translated_name: I18n.t("discourse_automation.ai.flag_types.review") },
|
||||||
{ id: "spam", translated_name: I18n.t("discourse_automation.ai.flag_types.spam") },
|
{ id: "spam", translated_name: I18n.t("discourse_automation.ai.flag_types.spam") },
|
||||||
|
{
|
||||||
|
id: "spam_silence",
|
||||||
|
translated_name: I18n.t("discourse_automation.ai.flag_types.spam_silence"),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
def self.available_models
|
def self.available_models
|
||||||
|
|
|
@ -83,7 +83,7 @@ module DiscourseAi
|
||||||
.sub("%%AUTOMATION_ID%%", automation&.id.to_s)
|
.sub("%%AUTOMATION_ID%%", automation&.id.to_s)
|
||||||
.sub("%%AUTOMATION_NAME%%", automation&.name.to_s)
|
.sub("%%AUTOMATION_NAME%%", automation&.name.to_s)
|
||||||
|
|
||||||
if flag_type == :spam
|
if flag_type == :spam || flag_type == :spam_silence
|
||||||
PostActionCreator.new(
|
PostActionCreator.new(
|
||||||
Discourse.system_user,
|
Discourse.system_user,
|
||||||
post,
|
post,
|
||||||
|
@ -91,6 +91,8 @@ module DiscourseAi
|
||||||
message: score_reason,
|
message: score_reason,
|
||||||
queue_for_review: true,
|
queue_for_review: true,
|
||||||
).perform
|
).perform
|
||||||
|
|
||||||
|
SpamRule::AutoSilence.new(post.user, post).silence_user if flag_type == :spam_silence
|
||||||
else
|
else
|
||||||
reviewable =
|
reviewable =
|
||||||
ReviewablePost.needs_review!(target: post, created_by: Discourse.system_user)
|
ReviewablePost.needs_review!(target: post, created_by: Discourse.system_user)
|
||||||
|
|
|
@ -622,6 +622,65 @@ RSpec.describe DiscourseAi::AiBot::Playground do
|
||||||
expect(post.topic.posts.last.post_number).to eq(1)
|
expect(post.topic.posts.last.post_number).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "allows swapping a llm mid conversation using a mention" do
|
||||||
|
SiteSetting.ai_bot_enabled = true
|
||||||
|
|
||||||
|
post = nil
|
||||||
|
DiscourseAi::Completions::Llm.with_prepared_responses(
|
||||||
|
["Yes I can", "Magic Title"],
|
||||||
|
llm: "custom:#{claude_2.id}",
|
||||||
|
) do
|
||||||
|
post =
|
||||||
|
create_post(
|
||||||
|
title: "I just made a PM",
|
||||||
|
raw: "Hey there #{persona.user.username}, can you help me?",
|
||||||
|
target_usernames: "#{user.username},#{persona.user.username}",
|
||||||
|
archetype: Archetype.private_message,
|
||||||
|
user: admin,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
post.topic.custom_fields["ai_persona_id"] = persona.id
|
||||||
|
post.topic.save_custom_fields
|
||||||
|
|
||||||
|
llm2 = Fabricate(:llm_model, enabled_chat_bot: true)
|
||||||
|
|
||||||
|
llm2.toggle_companion_user
|
||||||
|
|
||||||
|
DiscourseAi::Completions::Llm.with_prepared_responses(
|
||||||
|
["Hi from bot two"],
|
||||||
|
llm: "custom:#{llm2.id}",
|
||||||
|
) do
|
||||||
|
create_post(
|
||||||
|
user: admin,
|
||||||
|
raw: "hi @#{llm2.user.username.capitalize} how are you",
|
||||||
|
topic_id: post.topic_id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_post = post.topic.reload.posts.order("id desc").first
|
||||||
|
expect(last_post.raw).to eq("Hi from bot two")
|
||||||
|
expect(last_post.user_id).to eq(persona.user_id)
|
||||||
|
|
||||||
|
# tether llm, so it can no longer be switched
|
||||||
|
persona.update!(force_default_llm: true, default_llm: "custom:#{claude_2.id}")
|
||||||
|
|
||||||
|
DiscourseAi::Completions::Llm.with_prepared_responses(
|
||||||
|
["Hi from bot one"],
|
||||||
|
llm: "custom:#{claude_2.id}",
|
||||||
|
) do
|
||||||
|
create_post(
|
||||||
|
user: admin,
|
||||||
|
raw: "hi @#{llm2.user.username.capitalize} how are you",
|
||||||
|
topic_id: post.topic_id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_post = post.topic.reload.posts.order("id desc").first
|
||||||
|
expect(last_post.raw).to eq("Hi from bot one")
|
||||||
|
expect(last_post.user_id).to eq(persona.user_id)
|
||||||
|
end
|
||||||
|
|
||||||
it "allows PMing a persona even when no particular bots are enabled" do
|
it "allows PMing a persona even when no particular bots are enabled" do
|
||||||
SiteSetting.ai_bot_enabled = true
|
SiteSetting.ai_bot_enabled = true
|
||||||
toggle_enabled_bots(bots: [])
|
toggle_enabled_bots(bots: [])
|
||||||
|
|
|
@ -110,6 +110,24 @@ describe DiscourseAi::Automation::LlmTriage do
|
||||||
expect(post.topic.reload.visible).to eq(false)
|
expect(post.topic.reload.visible).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "can handle spam+silence flags" do
|
||||||
|
DiscourseAi::Completions::Llm.with_prepared_responses(["bad"]) do
|
||||||
|
triage(
|
||||||
|
post: post,
|
||||||
|
model: "custom:#{llm_model.id}",
|
||||||
|
system_prompt: "test %%POST%%",
|
||||||
|
search_for_text: "bad",
|
||||||
|
flag_post: true,
|
||||||
|
flag_type: :spam_silence,
|
||||||
|
automation: nil,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(post.reload).to be_hidden
|
||||||
|
expect(post.topic.reload.visible).to eq(false)
|
||||||
|
expect(post.user.silenced?).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
it "can handle garbled output from LLM" do
|
it "can handle garbled output from LLM" do
|
||||||
DiscourseAi::Completions::Llm.with_prepared_responses(["Bad.\n\nYo"]) do
|
DiscourseAi::Completions::Llm.with_prepared_responses(["Bad.\n\nYo"]) do
|
||||||
triage(
|
triage(
|
||||||
|
|
Loading…
Reference in New Issue