Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit 0932253

Browse files
committed
FEATURE: Use Persona's when scanning posts for spam
1 parent 683bb57 commit 0932253

File tree

17 files changed

+375
-143
lines changed

17 files changed

+375
-143
lines changed

app/controllers/discourse_ai/admin/ai_spam_controller.rb

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ def show
1111

1212
def update
1313
initial_settings = AiModerationSetting.spam
14+
15+
initial_data = {
16+
custom_instructions: initial_settings&.data&.dig("custom_instructions"),
17+
llm_model_id: initial_settings&.llm_model_id,
18+
ai_persona_id: initial_settings&.ai_persona_id,
19+
}
20+
1421
initial_custom_instructions = initial_settings&.data&.dig("custom_instructions")
1522
initial_llm_model_id = initial_settings&.llm_model_id
1623

@@ -29,6 +36,22 @@ def update
2936
)
3037
end
3138
end
39+
40+
if allowed_params.key?(:ai_persona_id)
41+
updated_params[:ai_persona_id] = allowed_params[:ai_persona_id]
42+
persona = AiPersona.find_by(id: allowed_params[:ai_persona_id])
43+
if persona.nil? ||
44+
persona.response_format.to_a.none? { |rf|
45+
rf["key"] == "spam" && rf["type"] == "boolean"
46+
}
47+
return(
48+
render_json_error(
49+
I18n.t("discourse_ai.llm.configuration.invalid_persona_response_format"),
50+
status: 422,
51+
)
52+
)
53+
end
54+
end
3255
updated_params[:data] = {
3356
custom_instructions: allowed_params[:custom_instructions],
3457
} if allowed_params.key?(:custom_instructions)
@@ -41,7 +64,7 @@ def update
4164
AiModerationSetting.create!(updated_params.merge(setting_type: :spam))
4265
end
4366

44-
log_ai_spam_update(initial_llm_model_id, initial_custom_instructions, allowed_params)
67+
log_ai_spam_update(initial_data, allowed_params)
4568
end
4669

4770
is_enabled = ActiveModel::Type::Boolean.new.cast(allowed_params[:is_enabled])
@@ -119,9 +142,10 @@ def fix_errors
119142

120143
private
121144

122-
def log_ai_spam_update(initial_llm_model_id, initial_custom_instructions, params)
145+
def log_ai_spam_update(initial_data, params)
123146
changes_to_log = {}
124147

148+
initial_llm_model_id = initial_data[:llm_model_id]
125149
if params.key?(:llm_model_id) && initial_llm_model_id.to_s != params[:llm_model_id].to_s
126150
old_model_name =
127151
LlmModel.find_by(id: initial_llm_model_id)&.display_name || initial_llm_model_id
@@ -131,11 +155,22 @@ def log_ai_spam_update(initial_llm_model_id, initial_custom_instructions, params
131155
changes_to_log[:llm_model_id] = "#{old_model_name}#{new_model_name}"
132156
end
133157

158+
initial_custom_instructions = initial_data[:custom_instructions]
134159
if params.key?(:custom_instructions) &&
135160
initial_custom_instructions != params[:custom_instructions]
136161
changes_to_log[:custom_instructions] = params[:custom_instructions]
137162
end
138163

164+
initial_ai_persona_id = initial_data[:ai_persona_id]
165+
if params.key?(:ai_persona_id) && initial_ai_persona_id.to_s != params[:ai_persona_id].to_s
166+
old_persona_name =
167+
AiPersona.find_by(id: initial_ai_persona_id)&.name || initial_ai_persona_id
168+
new_persona_name =
169+
AiPersona.find_by(id: params[:ai_persona_id])&.name || params[:ai_persona_id]
170+
171+
changes_to_log[:ai_persona_id] = "#{old_persona_name}#{new_persona_name}"
172+
end
173+
139174
if changes_to_log.present?
140175
changes_to_log[:subject] = I18n.t("discourse_ai.spam_detection.logging_subject")
141176
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
@@ -144,7 +179,7 @@ def log_ai_spam_update(initial_llm_model_id, initial_custom_instructions, params
144179
end
145180

146181
def allowed_params
147-
params.permit(:is_enabled, :llm_model_id, :custom_instructions)
182+
params.permit(:is_enabled, :llm_model_id, :custom_instructions, :ai_persona_id)
148183
end
149184

150185
def spam_config

app/models/ai_moderation_setting.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22
class AiModerationSetting < ActiveRecord::Base
33
belongs_to :llm_model
4+
belongs_to :ai_persona
45

56
validates :llm_model_id, presence: true
67
validates :setting_type, presence: true
@@ -19,12 +20,13 @@ def custom_instructions
1920
#
2021
# Table name: ai_moderation_settings
2122
#
22-
# id :bigint not null, primary key
23-
# setting_type :enum not null
24-
# data :jsonb
25-
# llm_model_id :bigint not null
26-
# created_at :datetime not null
27-
# updated_at :datetime not null
23+
# id :bigint not null, primary key
24+
# setting_type :enum not null
25+
# data :jsonb
26+
# llm_model_id :bigint not null
27+
# created_at :datetime not null
28+
# updated_at :datetime not null
29+
# ai_persona_id :bigint default(-31), not null
2830
#
2931
# Indexes
3032
#

app/serializers/ai_spam_serializer.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ class AiSpamSerializer < ApplicationSerializer
88
:stats,
99
:flagging_username,
1010
:spam_score_type,
11-
:spam_scanning_user
11+
:spam_scanning_user,
12+
:ai_persona_id,
13+
:available_personas
1214

1315
def is_enabled
1416
object[:enabled]
@@ -18,6 +20,11 @@ def llm_id
1820
settings&.llm_model&.id
1921
end
2022

23+
def ai_persona_id
24+
settings&.ai_persona&.id ||
25+
DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::SpamDetector]
26+
end
27+
2128
def custom_instructions
2229
settings&.custom_instructions
2330
end
@@ -28,6 +35,12 @@ def available_llms
2835
.map { |hash| { id: hash[:value], name: hash[:name] } }
2936
end
3037

38+
def available_personas
39+
DiscourseAi::Configuration::PersonaEnumerator.values.map do |h|
40+
{ id: h[:value], name: h[:name] }
41+
end
42+
end
43+
3144
def flagging_username
3245
object[:flagging_username]
3346
end

assets/javascripts/discourse/components/ai-spam.gjs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default class AiSpam extends Component {
3535
};
3636
@tracked isEnabled = false;
3737
@tracked selectedLLM = null;
38+
@tracked selectedPersonaId = null;
3839
@tracked customInstructions = "";
3940
@tracked errors = [];
4041

@@ -98,6 +99,7 @@ export default class AiSpam extends Component {
9899
}
99100
this.customInstructions = model.custom_instructions;
100101
this.stats = model.stats;
102+
this.selectedPersonaId = model.ai_persona_id;
101103
}
102104

103105
get availableLLMs() {
@@ -133,6 +135,11 @@ export default class AiSpam extends Component {
133135
this.selectedLLM = value;
134136
}
135137

138+
@action
139+
async updatePersona(value) {
140+
this.selectedPersonaId = value;
141+
}
142+
136143
@action
137144
async save() {
138145
try {
@@ -141,6 +148,7 @@ export default class AiSpam extends Component {
141148
data: {
142149
llm_model_id: this.llmId,
143150
custom_instructions: this.customInstructions,
151+
ai_persona_id: this.selectedPersonaId,
144152
},
145153
});
146154
this.toasts.success({
@@ -256,6 +264,18 @@ export default class AiSpam extends Component {
256264
{{/if}}
257265
</div>
258266

267+
<div class="ai-spam__persona">
268+
<label class="ai-spam__persona-label">{{i18n
269+
"discourse_ai.spam.select_persona"
270+
}}</label>
271+
<ComboBox
272+
@value={{this.selectedPersonaId}}
273+
@content={{@model.available_personas}}
274+
@onChange={{this.updatePersona}}
275+
class="ai-spam__persona-selector"
276+
/>
277+
</div>
278+
259279
<div class="ai-spam__instructions">
260280
<label class="ai-spam__instructions-label">
261281
{{i18n "discourse_ai.spam.custom_instructions"}}

assets/stylesheets/modules/llms/common/spam.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424

2525
&__toggle,
2626
&__llm,
27+
&__persona,
2728
&__instructions {
2829
margin-bottom: 1em;
2930
}
3031

3132
&__toggle-label,
3233
&__llm-label,
34+
&__persona-label,
3335
&__instructions-label {
3436
display: block;
3537
margin-bottom: 0.5em;

config/locales/client.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ en:
247247
short_title: "Spam"
248248
title: "Configure spam handling"
249249
select_llm: "Select LLM"
250+
select_persona: "Select persona"
250251
custom_instructions: "Custom instructions"
251252
custom_instructions_help: "Custom instructions specific to your site to help guide the AI in identifying spam, e.g. 'Be more aggressive about scanning posts not in English'."
252253
last_seven_days: "Last 7 days"

config/locales/server.en.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,9 @@ en:
391391
short_text_translator:
392392
name: "Short text translator"
393393
description: "Powers the translation feature by as a generic text translator, used for short texts like category names or tags"
394+
spam_detector:
395+
name: "Spam detector"
396+
description: "Default persona powering our Spam detection feature"
394397

395398
topic_not_found: "Summary unavailable, topic not found!"
396399
summarizing: "Summarizing topic"
@@ -577,6 +580,7 @@ en:
577580
set_llm_first: "Set %{setting} first"
578581
model_unreachable: "We couldn't get a response from this model. Check your settings first."
579582
invalid_seeded_model: "You can't use this model with this feature"
583+
invalid_persona_response_format: "The selected persona must have a response format with a boolean field names \"spam\""
580584
must_select_model: "You must select a LLM first"
581585
endpoints:
582586
not_configured: "%{display_name} (not configured)"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
class AddPersonaToAiModerationSettings < ActiveRecord::Migration[7.2]
3+
def change
4+
add_column :ai_moderation_settings, :ai_persona_id, :bigint, null: false, default: -31
5+
end
6+
end

0 commit comments

Comments
 (0)