0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-09 21:45:25 +08:00
discourse/plugins/discourse-ai/lib/automation.rb
Régis Hanol 97c241a718
FIX: Preserve safe formatting in reviewable reasons (#42164)
Previously, reviewable reasons from custom flag producers were treated
as inline text, and pre-escaping prevented Markdown and line-break
formatting from being recognized.

This change cooks a constrained Markdown subset at serialization so
every producer shares one sanitization boundary, renders cooked block
HTML directly, and keeps watched-word matches literal because their
Markdown metacharacters are data rather than formatting.
2026-07-30 17:07:08 +02: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: 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