0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 17:53:55 +08:00
discourse/plugins/discourse-ai/lib/automation.rb
Krzysztof Kotlarek 3709009232
FIX: Avoid duplicate AI triage reviewables for spam flags (#41043)
Previously, AI triage could create both a ReviewablePost and a
ReviewableFlaggedPost for the same post when a spam flag followed an
existing review flag.

This change promotes the existing reviewable before spam flagging and
separates the score reason from the model response, so moderators see
one review item with clearer context.

Demo:
<img width="1070" height="942" alt="Screenshot 2026-06-19 at 2 12 10 pm"
src="https://github.com/user-attachments/assets/a55bfc5f-98df-44ba-aa6f-28a523269055"
/>
2026-06-22 13:01:42 +08:00

141 lines
4.3 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseAi
module Automation
def self.spam_based_flag_types
%w[spam spam_silence]
end
def self.flag_post_reason(
reason:,
llm_response: nil,
automation_id: nil,
automation_name: nil,
base_path: Discourse.base_path
)
if automation_context?(automation_id, automation_name)
I18n.t(
"discourse_automation.scriptables.llm_triage.flagged_post",
score_reason:
spam_score_reason(
automation_id: automation_id,
automation_name: automation_name,
base_path: base_path,
),
response: flag_post_response(reason: reason, llm_response: llm_response),
)
else
I18n.t("discourse_ai.ai_bot.flag_post.reason", reason: reason)
end
end
def self.spam_score_reason(
automation_id: nil,
automation_name: nil,
base_path: Discourse.base_path
)
if automation_context?(automation_id, automation_name)
I18n.t(
"discourse_automation.scriptables.llm_triage.flagged_post_score_reason",
base_path: base_path,
automation_id: automation_id.to_s,
automation_name: ERB::Util.html_escape(automation_name.to_s),
)
else
I18n.t("discourse_ai.ai_bot.flag_post.score_reason")
end
end
def self.spam_post_action_message(
reason:,
llm_response: nil,
automation_id: nil,
automation_name: nil,
base_path: Discourse.base_path
)
if automation_context?(automation_id, automation_name)
flag_post_response(reason: reason, llm_response: llm_response)
else
flag_post_reason(
reason: reason,
llm_response: llm_response,
automation_id: automation_id,
automation_name: automation_name,
base_path: base_path,
)
end
end
def self.flag_post_response(reason:, llm_response: nil)
I18n.t(
"discourse_automation.scriptables.llm_triage.flagged_post_response",
llm_response: ERB::Util.html_escape(llm_response.presence || reason),
)
end
def self.flag_types
[
{ id: "review", translated_name: I18n.t("discourse_automation.ai.flag_types.review") },
{
id: "review_hide",
translated_name: I18n.t("discourse_automation.ai.flag_types.review_hide"),
},
{
id: "review_delete",
translated_name: I18n.t("discourse_automation.ai.flag_types.review_delete"),
},
{
id: "review_delete_silence",
translated_name: I18n.t("discourse_automation.ai.flag_types.review_delete_silence"),
},
{ 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
def self.available_custom_tools
available_tools(
scope: AiTool.where(enabled: true).where("parameters = '[]'::jsonb"),
description_field: :description,
)
end
def self.available_tools_all
available_tools(description_field: :summary)
end
def self.available_tools(scope: AiTool.where(enabled: true), description_field: :description)
scope
.pluck(:id, :name, description_field)
.map { |id, name, desc| { id: id, translated_name: name, description: desc } }
end
def self.available_models
DB.query_hash(<<~SQL).each { |value_h| value_h["id"] = "#{value_h["id"]}" }
SELECT display_name AS translated_name, id AS id
FROM llm_models
SQL
end
def self.available_agent_choices(require_user: true, require_default_llm: true)
relation = AiAgent.includes(:user)
relation = relation.where.not(user_id: nil) if require_user
relation = relation.where.not(default_llm: nil) if require_default_llm
relation.map do |agent|
phash = { id: agent.id, translated_name: agent.name, description: agent.name }
phash[:description] += " (#{agent&.user&.username})" if require_user
phash
end
end
def self.automation_context?(automation_id, automation_name)
automation_id.present? && automation_name.present?
end
private_class_method :automation_context?, :flag_post_response
end
end