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
41 changes: 38 additions & 3 deletions app/controllers/discourse_ai/admin/ai_spam_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ def show

def update
initial_settings = AiModerationSetting.spam

initial_data = {
custom_instructions: initial_settings&.data&.dig("custom_instructions"),
llm_model_id: initial_settings&.llm_model_id,
ai_persona_id: initial_settings&.ai_persona_id,
}

initial_custom_instructions = initial_settings&.data&.dig("custom_instructions")
initial_llm_model_id = initial_settings&.llm_model_id

Expand All @@ -29,6 +36,22 @@ def update
)
end
end

if allowed_params.key?(:ai_persona_id)
updated_params[:ai_persona_id] = allowed_params[:ai_persona_id]
persona = AiPersona.find_by(id: allowed_params[:ai_persona_id])
if persona.nil? ||
persona.response_format.to_a.none? { |rf|
rf["key"] == "spam" && rf["type"] == "boolean"
}
return(
render_json_error(
I18n.t("discourse_ai.llm.configuration.invalid_persona_response_format"),
status: 422,
)
)
end
end
updated_params[:data] = {
custom_instructions: allowed_params[:custom_instructions],
} if allowed_params.key?(:custom_instructions)
Expand All @@ -41,7 +64,7 @@ def update
AiModerationSetting.create!(updated_params.merge(setting_type: :spam))
end

log_ai_spam_update(initial_llm_model_id, initial_custom_instructions, allowed_params)
log_ai_spam_update(initial_data, allowed_params)
end

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

private

def log_ai_spam_update(initial_llm_model_id, initial_custom_instructions, params)
def log_ai_spam_update(initial_data, params)
changes_to_log = {}

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

initial_custom_instructions = initial_data[:custom_instructions]
if params.key?(:custom_instructions) &&
initial_custom_instructions != params[:custom_instructions]
changes_to_log[:custom_instructions] = params[:custom_instructions]
end

initial_ai_persona_id = initial_data[:ai_persona_id]
if params.key?(:ai_persona_id) && initial_ai_persona_id.to_s != params[:ai_persona_id].to_s
old_persona_name =
AiPersona.find_by(id: initial_ai_persona_id)&.name || initial_ai_persona_id
new_persona_name =
AiPersona.find_by(id: params[:ai_persona_id])&.name || params[:ai_persona_id]

changes_to_log[:ai_persona_id] = "#{old_persona_name} → #{new_persona_name}"
end

if changes_to_log.present?
changes_to_log[:subject] = I18n.t("discourse_ai.spam_detection.logging_subject")
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
Expand All @@ -144,7 +179,7 @@ def log_ai_spam_update(initial_llm_model_id, initial_custom_instructions, params
end

def allowed_params
params.permit(:is_enabled, :llm_model_id, :custom_instructions)
params.permit(:is_enabled, :llm_model_id, :custom_instructions, :ai_persona_id)
end

def spam_config
Expand Down
14 changes: 8 additions & 6 deletions app/models/ai_moderation_setting.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class AiModerationSetting < ActiveRecord::Base
belongs_to :llm_model
belongs_to :ai_persona

validates :llm_model_id, presence: true
validates :setting_type, presence: true
Expand All @@ -19,12 +20,13 @@ def custom_instructions
#
# Table name: ai_moderation_settings
#
# id :bigint not null, primary key
# setting_type :enum not null
# data :jsonb
# llm_model_id :bigint not null
# created_at :datetime not null
# updated_at :datetime not null
# id :bigint not null, primary key
# setting_type :enum not null
# data :jsonb
# llm_model_id :bigint not null
# created_at :datetime not null
# updated_at :datetime not null
# ai_persona_id :bigint default(-31), not null
#
# Indexes
#
Expand Down
15 changes: 14 additions & 1 deletion app/serializers/ai_spam_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ class AiSpamSerializer < ApplicationSerializer
:stats,
:flagging_username,
:spam_score_type,
:spam_scanning_user
:spam_scanning_user,
:ai_persona_id,
:available_personas

def is_enabled
object[:enabled]
Expand All @@ -18,6 +20,11 @@ def llm_id
settings&.llm_model&.id
end

def ai_persona_id
settings&.ai_persona&.id ||
DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::SpamDetector]
end

def custom_instructions
settings&.custom_instructions
end
Expand All @@ -28,6 +35,12 @@ def available_llms
.map { |hash| { id: hash[:value], name: hash[:name] } }
end

def available_personas
DiscourseAi::Configuration::PersonaEnumerator.values.map do |h|
{ id: h[:value], name: h[:name] }
end
end

def flagging_username
object[:flagging_username]
end
Expand Down
20 changes: 20 additions & 0 deletions assets/javascripts/discourse/components/ai-spam.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default class AiSpam extends Component {
};
@tracked isEnabled = false;
@tracked selectedLLM = null;
@tracked selectedPersonaId = null;
@tracked customInstructions = "";
@tracked errors = [];

Expand Down Expand Up @@ -98,6 +99,7 @@ export default class AiSpam extends Component {
}
this.customInstructions = model.custom_instructions;
this.stats = model.stats;
this.selectedPersonaId = model.ai_persona_id;
}

get availableLLMs() {
Expand Down Expand Up @@ -133,6 +135,11 @@ export default class AiSpam extends Component {
this.selectedLLM = value;
}

@action
async updatePersona(value) {
this.selectedPersonaId = value;
}

@action
async save() {
try {
Expand All @@ -141,6 +148,7 @@ export default class AiSpam extends Component {
data: {
llm_model_id: this.llmId,
custom_instructions: this.customInstructions,
ai_persona_id: this.selectedPersonaId,
},
});
this.toasts.success({
Expand Down Expand Up @@ -256,6 +264,18 @@ export default class AiSpam extends Component {
{{/if}}
</div>

<div class="ai-spam__persona">
<label class="ai-spam__persona-label">{{i18n
"discourse_ai.spam.select_persona"
}}</label>
<ComboBox
@value={{this.selectedPersonaId}}
@content={{@model.available_personas}}
@onChange={{this.updatePersona}}
class="ai-spam__persona-selector"
/>
</div>

<div class="ai-spam__instructions">
<label class="ai-spam__instructions-label">
{{i18n "discourse_ai.spam.custom_instructions"}}
Expand Down
2 changes: 2 additions & 0 deletions assets/stylesheets/modules/llms/common/spam.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@

&__toggle,
&__llm,
&__persona,
&__instructions {
margin-bottom: 1em;
}

&__toggle-label,
&__llm-label,
&__persona-label,
&__instructions-label {
display: block;
margin-bottom: 0.5em;
Expand Down
1 change: 1 addition & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ en:
short_title: "Spam"
title: "Configure spam handling"
select_llm: "Select LLM"
select_persona: "Select persona"
custom_instructions: "Custom instructions"
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'."
last_seven_days: "Last 7 days"
Expand Down
4 changes: 4 additions & 0 deletions config/locales/server.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,9 @@ en:
short_text_translator:
name: "Short text translator"
description: "Powers the translation feature by as a generic text translator, used for short texts like category names or tags"
spam_detector:
name: "Spam detector"
description: "Default persona powering our Spam detection feature"

topic_not_found: "Summary unavailable, topic not found!"
summarizing: "Summarizing topic"
Expand Down Expand Up @@ -577,6 +580,7 @@ en:
set_llm_first: "Set %{setting} first"
model_unreachable: "We couldn't get a response from this model. Check your settings first."
invalid_seeded_model: "You can't use this model with this feature"
invalid_persona_response_format: "The selected persona must have a response format with a boolean field names \"spam\""
must_select_model: "You must select a LLM first"
endpoints:
not_configured: "%{display_name} (not configured)"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true
class AddPersonaToAiModerationSettings < ActiveRecord::Migration[7.2]
def change
add_column :ai_moderation_settings, :ai_persona_id, :bigint, null: false, default: -31
end
end
Loading
Loading