0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-05 23:04:22 +08:00
discourse/plugins/discourse-ai/lib/translation.rb
Natalie Tay 7c89948f1a
UX: Convert AI translation's max days ago setting to a specific date setting instead (#42082)
Related:
https://meta.discourse.org/t/request-for-a-date-picker-in-ai-translate-settings/405828

The backfill translation setting was "max age days". This resulted in a
running date, which meant the older topics couldn't get updated if the
date keeps running from today.
2026-07-28 18:27:34 +08:00

218 lines
6.6 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseAi
module Translation
def self.enabled?
SiteSetting.discourse_ai_enabled && SiteSetting.ai_translation_enabled && has_llm_model? &&
SiteSetting.content_localization_supported_locales.present?
end
def self.locales
SiteSetting.content_localization_locales
end
def self.supported_locale_bases_cte
<<~SQL
supported AS MATERIALIZED (
SELECT COALESCE(
array_agg(
DISTINCT split_part(lower(replace(locale, '-', '_')), '_', 1)
) FILTER (WHERE locale IS NOT NULL),
ARRAY[]::text[]
) AS bases
FROM unnest(ARRAY[:supported_locales]::text[]) configured(locale)
)
SQL
end
def self.has_llm_model?
agent_ids = [
SiteSetting.ai_translation_locale_detector_agent,
SiteSetting.ai_translation_post_raw_translator_agent,
SiteSetting.ai_translation_topic_title_translator_agent,
SiteSetting.ai_translation_short_text_translator_agent,
]
agent_default_llms =
AiAgent
.all_agents(enabled_only: false)
.select { |p| agent_ids.include?(p.id) }
.map(&:default_llm_id)
default_llm_model = SiteSetting.ai_default_llm_model
if agent_default_llms.any?(&:blank?) && default_llm_model.blank?
false
else
true
end
end
def self.backfill_enabled?(target: nil)
enabled? && SiteSetting.ai_translation_backfill_hourly_rate > 0 &&
(![Post, Topic].include?(target) || backfill_start_at.present?)
end
def self.backfill_start_at
value = SiteSetting.ai_translation_backfill_start_date
return if value.blank?
date = Date.iso8601(value)
Time.utc(date.year, date.month, date.day)
end
def self.category_ids
SiteSetting.ai_translation_categories.to_s.split("|").filter_map { |id| id.presence&.to_i }
end
def self.category_ids_with_subcategories
category_ids.flat_map { |category_id| Category.subcategory_ids(category_id) }.uniq
end
def self.category_scope_cache_key
ids =
case SiteSetting.ai_translation_category_scope
when "include", "exclude"
category_ids_with_subcategories
else
category_ids
end
"#{SiteSetting.ai_translation_category_scope}:#{ids.sort.join("|")}"
end
def self.category_scope_condition(category_column:)
case SiteSetting.ai_translation_category_scope
when "all"
["#{category_column} IS NOT NULL", {}]
when "include"
ids = category_ids_with_subcategories
return "1 = 0", {} if ids.blank?
["#{category_column} IN (:category_ids)", { category_ids: ids }]
when "include_strict"
ids = category_ids
return "1 = 0", {} if ids.blank?
["#{category_column} IN (:category_ids)", { category_ids: ids }]
when "exclude"
ids = category_ids_with_subcategories
return "#{category_column} IS NOT NULL", {} if ids.blank?
[
"#{category_column} IS NOT NULL AND #{category_column} NOT IN (:category_ids)",
{ category_ids: ids },
]
when "exclude_strict"
ids = category_ids
return "#{category_column} IS NOT NULL", {} if ids.blank?
[
"#{category_column} IS NOT NULL AND #{category_column} NOT IN (:category_ids)",
{ category_ids: ids },
]
else
[
"#{category_column} IS NOT NULL AND EXISTS (
SELECT 1 FROM categories category_scope_categories
WHERE category_scope_categories.id = #{category_column}
AND category_scope_categories.read_restricted = false
)",
{},
]
end
end
def self.category_allowed?(category)
category_id = category.respond_to?(:id) ? category.id : category
return false if category_id.blank?
case SiteSetting.ai_translation_category_scope
when "all"
true
when "include"
category_ids_with_subcategories.include?(category_id)
when "include_strict"
category_ids.include?(category_id)
when "exclude"
!category_ids_with_subcategories.include?(category_id)
when "exclude_strict"
!category_ids.include?(category_id)
else
if category.respond_to?(:read_restricted)
!category.read_restricted
else
Category.where(id: category_id, read_restricted: false).exists?
end
end
end
def self.llm_model_for_agent(agent_id)
return nil if agent_id.blank?
ai_agent = AiAgent.find_by_id_from_cache(agent_id)
return nil if ai_agent.blank?
agent_klass = ai_agent.class_instance
BaseTranslator.preferred_llm_model(agent_klass)
end
def self.credits_available_for_agent_ids?(agent_ids)
return true if agent_ids.blank?
models = agent_ids.map { |agent_id| llm_model_for_agent(agent_id) }.compact.uniq
return true if models.empty?
models.all? { |model| LlmCreditAllocation.credits_available?(model) }
end
def self.credits_available_for_post_detection?
credits_available_for_agent_ids?(
[
SiteSetting.ai_translation_locale_detector_agent,
SiteSetting.ai_translation_post_raw_translator_agent,
],
)
end
def self.credits_available_for_topic_detection?
credits_available_for_agent_ids?(
[
SiteSetting.ai_translation_locale_detector_agent,
SiteSetting.ai_translation_topic_title_translator_agent,
SiteSetting.ai_translation_post_raw_translator_agent,
],
)
end
def self.credits_available_for_post_localization?
credits_available_for_agent_ids?([SiteSetting.ai_translation_post_raw_translator_agent])
end
def self.credits_available_for_topic_localization?
credits_available_for_agent_ids?(
[
SiteSetting.ai_translation_topic_title_translator_agent,
SiteSetting.ai_translation_post_raw_translator_agent,
],
)
end
def self.credits_available_for_category_localization?
credits_available_for_agent_ids?(
[
SiteSetting.ai_translation_short_text_translator_agent,
SiteSetting.ai_translation_post_raw_translator_agent,
],
)
end
def self.credits_available_for_tag_localization?
credits_available_for_agent_ids?([SiteSetting.ai_translation_short_text_translator_agent])
end
def self.credits_available_for_sidebar_localization?
credits_available_for_agent_ids?([SiteSetting.ai_translation_short_text_translator_agent])
end
end
end