Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/models/ai_spam_log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class AiSpamLog < ActiveRecord::Base
# payload :string(20000) default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# error :string(3000)
#
# Indexes
#
Expand Down
6 changes: 6 additions & 0 deletions db/migrate/20250211021037_add_error_to_ai_spam_log.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true
class AddErrorToAiSpamLog < ActiveRecord::Migration[7.2]
def change
add_column :ai_spam_logs, :error, :string, limit: 3000
end
end
38 changes: 24 additions & 14 deletions lib/ai_moderation/spam_scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -394,22 +394,32 @@ def self.handle_spam(post, log)
queue_for_review: true,
).perform

log.update!(reviewable: result.reviewable)
# Currently in core re-flagging something that is already flagged as spam
# is not supported, long term we may want to support this but in the meantime
# we should not be silencing/hiding if the PostActionCreator fails.
if result.success?
log.update!(reviewable: result.reviewable)

reason = I18n.t("discourse_ai.spam_detection.silence_reason", url: url)
silencer =
UserSilencer.new(
post.user,
flagging_user,
message: :too_many_spam_flags,
post_id: post.id,
reason: reason,
keep_posts: true,
)
silencer.silence

reason = I18n.t("discourse_ai.spam_detection.silence_reason", url: url)
silencer =
UserSilencer.new(
post.user,
flagging_user,
message: :too_many_spam_flags,
post_id: post.id,
reason: reason,
keep_posts: true,
# silencer will not hide tl1 posts, so we do this here
hide_post(post)
else
log.update!(
error:
"unable to flag post as spam, post action failed for post #{post.id} with error: '#{result.errors.full_messages.join(", ").truncate(3000)}'",
)
silencer.silence

# silencer will not hide tl1 posts, so we do this here
hide_post(post)
end
end

def self.hide_post(post)
Expand Down
25 changes: 17 additions & 8 deletions lib/automation/llm_triage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,24 @@ def self.handle(
.sub("%%AUTOMATION_NAME%%", automation&.name.to_s)

if flag_type == :spam || flag_type == :spam_silence
PostActionCreator.new(
Discourse.system_user,
post,
PostActionType.types[:spam],
message: score_reason,
queue_for_review: true,
).perform
result =
PostActionCreator.new(
Discourse.system_user,
post,
PostActionType.types[:spam],
message: score_reason,
queue_for_review: true,
).perform

SpamRule::AutoSilence.new(post.user, post).silence_user if flag_type == :spam_silence
if flag_type == :spam_silence
if result.success?
SpamRule::AutoSilence.new(post.user, post).silence_user
else
Rails.logger.warn(
"llm_triage: unable to flag post as spam, post action failed for #{post.id} with error: '#{result.errors.full_messages.join(",").truncate(3000)}'",
)
end
end
else
reviewable =
ReviewablePost.needs_review!(target: post, created_by: Discourse.system_user)
Expand Down
28 changes: 26 additions & 2 deletions spec/lib/modules/ai_moderation/spam_scanner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@

before { Jobs.run_immediately! }

it "Can correctly run tests" do
it "can correctly run tests" do
prompts = nil
result =
DiscourseAi::Completions::Llm.with_prepared_responses(
Expand All @@ -240,7 +240,7 @@
expect(result[:is_spam]).to eq(false)
end

it "Correctly handles spam scanning" do
it "correctly handles spam scanning" do
expect(described_class.flagging_user.id).not_to eq(Discourse.system_user.id)

# flag post for scanning
Expand Down Expand Up @@ -288,6 +288,30 @@
expect(post.topic.reload.visible).to eq(true)
expect(post.user.reload.silenced?).to eq(false)
end

it "does not silence the user or hide the post when a flag cannot be created" do
post = post_with_uploaded_image
Fabricate(
:post_action,
post: post,
user: described_class.flagging_user,
post_action_type_id: PostActionType.types[:spam],
)

described_class.new_post(post)

DiscourseAi::Completions::Llm.with_prepared_responses(["spam"]) do |_, _, _prompts|
# force a rebake so we actually scan
post.rebake!
end

log = AiSpamLog.find_by(post: post)

expect(log.reviewable).to be_nil
expect(log.error).to match(/unable to flag post as spam/)
expect(post.user.reload).not_to be_silenced
expect(post.topic.reload).to be_visible
end
end

it "includes location information and email in context" do
Expand Down
22 changes: 22 additions & 0 deletions spec/lib/modules/automation/llm_triage_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,28 @@ def triage(**args)
expect(post.user.silenced?).to eq(true)
end

it "does not silence the user if the flag fails" do
Fabricate(
:post_action,
post: post,
user: Discourse.system_user,
post_action_type_id: PostActionType.types[:spam],
)
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.user.reload).not_to be_silenced
end

it "can handle garbled output from LLM" do
DiscourseAi::Completions::Llm.with_prepared_responses(["Bad.\n\nYo"]) do
triage(
Expand Down