Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
059da39a4a |
@@ -50,7 +50,7 @@ module DiscourseAi
|
||||
)
|
||||
end
|
||||
|
||||
SiteSetting.set_and_log("ai_spam_detection_enabled", is_enabled, current_user)
|
||||
SiteSetting.ai_spam_detection_enabled = is_enabled
|
||||
end
|
||||
|
||||
render json: AiSpamSerializer.new(spam_config, root: false)
|
||||
|
||||
@@ -42,21 +42,6 @@ module DiscourseAi
|
||||
<style>
|
||||
#{artifact.css}
|
||||
</style>
|
||||
<script>
|
||||
window._discourse_user_data = {
|
||||
#{current_user ? "username: #{current_user.username.to_json}" : "username: null"}
|
||||
};
|
||||
window.discourseArtifactReady = new Promise(resolve => {
|
||||
window._resolveArtifactData = resolve;
|
||||
});
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.type === 'discourse-artifact-data') {
|
||||
window.discourseArtifactData = event.data.dataset || {};
|
||||
Object.assign(window.discourseArtifactData, window._discourse_user_data);
|
||||
window._resolveArtifactData(window.discourseArtifactData);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
#{artifact.html}
|
||||
@@ -89,19 +74,6 @@ module DiscourseAi
|
||||
</head>
|
||||
<body>
|
||||
<iframe sandbox="allow-scripts allow-forms" height="100%" width="100%" srcdoc="#{ERB::Util.html_escape(untrusted_html)}" frameborder="0"></iframe>
|
||||
<script>
|
||||
document.querySelector('iframe').addEventListener('load', function() {
|
||||
try {
|
||||
const iframeWindow = this.contentWindow;
|
||||
const message = { type: 'discourse-artifact-data', dataset: {} };
|
||||
|
||||
if (window.frameElement && window.frameElement.dataset) {
|
||||
Object.assign(message.dataset, window.frameElement.dataset);
|
||||
}
|
||||
iframeWindow.postMessage(message, '*');
|
||||
} catch (e) { console.error('Error passing data to artifact:', e); }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
@@ -8,7 +8,7 @@ module DiscourseAi
|
||||
before_action :require_site_settings!
|
||||
|
||||
skip_before_action :preload_json, :check_xhr, only: %i[show asset]
|
||||
skip_before_action :redirect_to_login_if_required, :verify_authenticity_token, only: %i[asset]
|
||||
skip_before_action :verify_authenticity_token, only: ["asset"]
|
||||
|
||||
def create
|
||||
ensure_allowed_create!
|
||||
|
||||
@@ -124,10 +124,6 @@ module DiscourseAi
|
||||
raise Discourse::InvalidParameters.new(:custom_prompt) if params[:custom_prompt].blank?
|
||||
end
|
||||
|
||||
# to stream we must have an appropriate client_id
|
||||
# otherwise we may end up streaming the data to the wrong client
|
||||
raise Discourse::InvalidParameters.new(:client_id) if params[:client_id].blank?
|
||||
|
||||
if location == "composer"
|
||||
Jobs.enqueue(
|
||||
:stream_composer_helper,
|
||||
@@ -136,7 +132,6 @@ module DiscourseAi
|
||||
prompt: prompt.name,
|
||||
custom_prompt: params[:custom_prompt],
|
||||
force_default_locale: params[:force_default_locale] || false,
|
||||
client_id: params[:client_id],
|
||||
)
|
||||
else
|
||||
post_id = get_post_param!
|
||||
@@ -151,7 +146,6 @@ module DiscourseAi
|
||||
text: text,
|
||||
prompt: prompt.name,
|
||||
custom_prompt: params[:custom_prompt],
|
||||
client_id: params[:client_id],
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ module ::Jobs
|
||||
|
||||
bot = DiscourseAi::Personas::Bot.as(bot_user, persona: persona.new)
|
||||
|
||||
DiscourseAi::AiBot::Playground.new(bot).reply_to(post, feature_name: "bot")
|
||||
DiscourseAi::AiBot::Playground.new(bot).reply_to(post)
|
||||
rescue DiscourseAi::Personas::Bot::BOT_NOT_FOUND
|
||||
Rails.logger.warn(
|
||||
"Bot not found for post #{post.id} - perhaps persona was deleted or bot was disabled",
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class DetectTranslatePost < ::Jobs::Base
|
||||
sidekiq_options retry: false
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
return if args[:post_id].blank?
|
||||
|
||||
post = Post.find_by(id: args[:post_id])
|
||||
return if post.blank? || post.raw.blank? || post.deleted_at.present? || post.user_id <= 0
|
||||
|
||||
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||
topic = post.topic
|
||||
if topic.blank? || topic.category&.read_restricted? ||
|
||||
topic.archetype == Archetype.private_message
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
detected_locale = DiscourseAi::Translation::PostLocaleDetector.detect_locale(post)
|
||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||
# this job is non-critical
|
||||
# the backfill job will handle failures
|
||||
return
|
||||
end
|
||||
|
||||
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||
return if locales.blank?
|
||||
|
||||
locales.each do |locale|
|
||||
next if locale == detected_locale
|
||||
|
||||
begin
|
||||
DiscourseAi::Translation::PostLocalizer.localize(post, locale)
|
||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||
# do nothing, there are too many sporadic lookup failures
|
||||
rescue => e
|
||||
DiscourseAi::Translation::VerboseLogger.log(
|
||||
"Failed to translate post #{post.id} to #{locale}: #{e.message}",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
MessageBus.publish("/topic/#{post.topic_id}", type: :localized, id: post.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,49 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class DetectTranslateTopic < ::Jobs::Base
|
||||
sidekiq_options retry: false
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
return if args[:topic_id].blank?
|
||||
|
||||
topic = Topic.find_by(id: args[:topic_id])
|
||||
if topic.blank? || topic.title.blank? || topic.deleted_at.present? || topic.user_id <= 0
|
||||
return
|
||||
end
|
||||
|
||||
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||
return if topic.category&.read_restricted?
|
||||
end
|
||||
|
||||
begin
|
||||
detected_locale = DiscourseAi::Translation::TopicLocaleDetector.detect_locale(topic)
|
||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||
# this job is non-critical
|
||||
# the backfill job will handle failures
|
||||
return
|
||||
end
|
||||
|
||||
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||
return if locales.blank?
|
||||
|
||||
locales.each do |locale|
|
||||
next if locale == detected_locale
|
||||
|
||||
begin
|
||||
DiscourseAi::Translation::TopicLocalizer.localize(topic, locale)
|
||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||
# do nothing, there are too many sporadic lookup failures
|
||||
rescue => e
|
||||
DiscourseAi::Translation::VerboseLogger.log(
|
||||
"Failed to translate topic #{topic.id} to #{locale}: #{e.message}",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
MessageBus.publish("/topic/#{topic.id}", type: :localized, id: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,70 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class GenerateInferredConcepts < ::Jobs::Base
|
||||
sidekiq_options queue: "low"
|
||||
|
||||
# Process items to generate new concepts
|
||||
#
|
||||
# @param args [Hash] Contains job arguments
|
||||
# @option args [String] :item_type Required - Type of items to process ('topics' or 'posts')
|
||||
# @option args [Array<Integer>] :item_ids Required - List of item IDs to process
|
||||
# @option args [Integer] :batch_size (100) Number of items to process in each batch
|
||||
# @option args [Boolean] :match_only (false) Only match against existing concepts without generating new ones
|
||||
def execute(args = {})
|
||||
return if args[:item_ids].blank? || args[:item_type].blank?
|
||||
|
||||
if %w[topics posts].exclude?(args[:item_type])
|
||||
Rails.logger.error("Invalid item_type for GenerateInferredConcepts: #{args[:item_type]}")
|
||||
return
|
||||
end
|
||||
|
||||
# Process items in smaller batches to avoid memory issues
|
||||
batch_size = args[:batch_size] || 100
|
||||
|
||||
# Get the list of item IDs
|
||||
item_ids = args[:item_ids]
|
||||
match_only = args[:match_only] || false
|
||||
|
||||
# Process items in batches
|
||||
item_ids.each_slice(batch_size) do |batch_item_ids|
|
||||
process_batch(batch_item_ids, args[:item_type], match_only)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_batch(item_ids, item_type, match_only)
|
||||
klass = item_type.singularize.classify.constantize
|
||||
items = klass.where(id: item_ids)
|
||||
manager = DiscourseAi::InferredConcepts::Manager.new
|
||||
|
||||
items.each do |item|
|
||||
begin
|
||||
process_item(item, item_type, match_only, manager)
|
||||
rescue => e
|
||||
Rails.logger.error(
|
||||
"Error generating concepts from #{item_type.singularize} #{item.id}: #{e.message}\n#{e.backtrace.join("\n")}",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def process_item(item, item_type, match_only, manager)
|
||||
# Use the Manager method that handles both identifying and creating concepts
|
||||
if match_only
|
||||
if item_type == "topics"
|
||||
manager.match_topic_to_concepts(item)
|
||||
else # posts
|
||||
manager.match_post_to_concepts(item)
|
||||
end
|
||||
else
|
||||
if item_type == "topics"
|
||||
manager.generate_concepts_from_topic(item)
|
||||
else # posts
|
||||
manager.generate_concepts_from_post(item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class LocalizeCategories < ::Jobs::Base
|
||||
cluster_concurrency 1
|
||||
sidekiq_options retry: false
|
||||
|
||||
BATCH_SIZE = 50
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
|
||||
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||
return if locales.blank?
|
||||
|
||||
cat_id = args[:from_category_id] || Category.order(:id).first&.id
|
||||
last_id = nil
|
||||
|
||||
categories = Category.where("id >= ?", cat_id).order(:id).limit(BATCH_SIZE)
|
||||
return if categories.empty?
|
||||
|
||||
categories.each do |category|
|
||||
if SiteSetting.ai_translation_backfill_limit_to_public_content && category.read_restricted?
|
||||
last_id = category.id
|
||||
next
|
||||
end
|
||||
|
||||
CategoryLocalization.transaction do
|
||||
locales.each do |locale|
|
||||
next if CategoryLocalization.exists?(category_id: category.id, locale: locale)
|
||||
begin
|
||||
DiscourseAi::Translation::CategoryLocalizer.localize(category, locale)
|
||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||
# do nothing, there are too many sporadic lookup failures
|
||||
rescue => e
|
||||
DiscourseAi::Translation::VerboseLogger.log(
|
||||
"Failed to translate category #{category.id} to #{locale}: #{e.message}",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
last_id = category.id
|
||||
end
|
||||
|
||||
if categories.size == BATCH_SIZE
|
||||
Jobs.enqueue_in(10.seconds, :localize_categories, from_category_id: last_id + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,68 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class LocalizePosts < ::Jobs::Base
|
||||
cluster_concurrency 1
|
||||
sidekiq_options retry: false
|
||||
|
||||
BATCH_SIZE = 50
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
|
||||
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||
return if locales.blank?
|
||||
|
||||
limit = args[:limit] || BATCH_SIZE
|
||||
|
||||
locales.each do |locale|
|
||||
posts =
|
||||
Post
|
||||
.joins(
|
||||
"LEFT JOIN post_localizations pl ON pl.post_id = posts.id AND pl.locale = #{ActiveRecord::Base.connection.quote(locale)}",
|
||||
)
|
||||
.where(deleted_at: nil)
|
||||
.where("posts.user_id > 0")
|
||||
.where.not(raw: [nil, ""])
|
||||
.where.not(locale: nil)
|
||||
.where.not(locale: locale)
|
||||
.where("pl.id IS NULL")
|
||||
|
||||
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||
posts =
|
||||
posts
|
||||
.joins(:topic)
|
||||
.where(topics: { category_id: Category.where(read_restricted: false).select(:id) })
|
||||
.where.not(topics: { archetype: Archetype.private_message })
|
||||
end
|
||||
|
||||
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||
posts =
|
||||
posts.where(
|
||||
"posts.created_at > ?",
|
||||
SiteSetting.ai_translation_backfill_max_age_days.days.ago,
|
||||
)
|
||||
end
|
||||
|
||||
posts = posts.order(updated_at: :desc).limit(limit)
|
||||
|
||||
next if posts.empty?
|
||||
|
||||
posts.each do |post|
|
||||
begin
|
||||
DiscourseAi::Translation::PostLocalizer.localize(post, locale)
|
||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||
# do nothing, there are too many sporadic lookup failures
|
||||
rescue => e
|
||||
DiscourseAi::Translation::VerboseLogger.log(
|
||||
"Failed to translate post #{post.id} to #{locale}: #{e.message}",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
DiscourseAi::Translation::VerboseLogger.log("Translated #{posts.size} posts to #{locale}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,63 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class LocalizeTopics < ::Jobs::Base
|
||||
cluster_concurrency 1
|
||||
sidekiq_options retry: false
|
||||
|
||||
BATCH_SIZE = 50
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
|
||||
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||
return if locales.blank?
|
||||
|
||||
limit = args[:limit] || BATCH_SIZE
|
||||
|
||||
locales.each do |locale|
|
||||
topics =
|
||||
Topic
|
||||
.joins(
|
||||
"LEFT JOIN topic_localizations tl ON tl.topic_id = topics.id AND tl.locale = #{ActiveRecord::Base.connection.quote(locale)}",
|
||||
)
|
||||
.where(deleted_at: nil)
|
||||
.where("topics.user_id > 0")
|
||||
.where.not(locale: nil)
|
||||
.where.not(locale: locale)
|
||||
.where("tl.id IS NULL")
|
||||
|
||||
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||
topics = topics.where(category_id: Category.where(read_restricted: false).select(:id))
|
||||
end
|
||||
|
||||
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||
topics =
|
||||
topics.where(
|
||||
"topics.created_at > ?",
|
||||
SiteSetting.ai_translation_backfill_max_age_days.days.ago,
|
||||
)
|
||||
end
|
||||
|
||||
topics = topics.order(updated_at: :desc).limit(limit)
|
||||
|
||||
next if topics.empty?
|
||||
|
||||
topics.each do |topic|
|
||||
begin
|
||||
DiscourseAi::Translation::TopicLocalizer.localize(topic, locale)
|
||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||
# do nothing, there are too many sporadic lookup failures
|
||||
rescue => e
|
||||
DiscourseAi::Translation::VerboseLogger.log(
|
||||
"Failed to translate topic #{topic.id} to #{locale}: #{e.message}",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
DiscourseAi::Translation::VerboseLogger.log("Translated #{topics.size} topics to #{locale}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,7 +8,6 @@ module Jobs
|
||||
return unless args[:prompt]
|
||||
return unless user = User.find_by(id: args[:user_id])
|
||||
return unless args[:text]
|
||||
return unless args[:client_id]
|
||||
|
||||
prompt = CompletionPrompt.enabled_by_name(args[:prompt])
|
||||
|
||||
@@ -22,7 +21,6 @@ module Jobs
|
||||
user,
|
||||
"/discourse-ai/ai-helper/stream_composer_suggestion",
|
||||
force_default_locale: args[:force_default_locale],
|
||||
client_id: args[:client_id],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class CategoryLocalizationBackfill < ::Jobs::Scheduled
|
||||
every 12.hours
|
||||
cluster_concurrency 1
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||
|
||||
Jobs.enqueue(:localize_categories)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,87 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class GenerateConceptsFromPopularItems < ::Jobs::Scheduled
|
||||
every 1.day
|
||||
|
||||
# This job runs daily and generates new concepts from popular topics and posts
|
||||
# It selects items based on engagement metrics and generates concepts from their content
|
||||
def execute(_args)
|
||||
return unless SiteSetting.inferred_concepts_enabled
|
||||
|
||||
process_popular_topics
|
||||
process_popular_posts
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_popular_topics
|
||||
# Find candidate topics that are popular and don't have concepts yet
|
||||
manager = DiscourseAi::InferredConcepts::Manager.new
|
||||
candidates =
|
||||
manager.find_candidate_topics(
|
||||
limit: SiteSetting.inferred_concepts_daily_topics_limit || 20,
|
||||
min_posts: SiteSetting.inferred_concepts_min_posts || 5,
|
||||
min_likes: SiteSetting.inferred_concepts_min_likes || 10,
|
||||
min_views: SiteSetting.inferred_concepts_min_views || 100,
|
||||
created_after: SiteSetting.inferred_concepts_lookback_days.days.ago,
|
||||
)
|
||||
|
||||
return if candidates.blank?
|
||||
|
||||
# Process candidate topics - first generate concepts, then match
|
||||
Jobs.enqueue(
|
||||
:generate_inferred_concepts,
|
||||
item_type: "topics",
|
||||
item_ids: candidates.map(&:id),
|
||||
batch_size: 10,
|
||||
)
|
||||
|
||||
if SiteSetting.inferred_concepts_background_match
|
||||
# Schedule a follow-up job to match existing concepts
|
||||
Jobs.enqueue_in(
|
||||
1.hour,
|
||||
:generate_inferred_concepts,
|
||||
item_type: "topics",
|
||||
item_ids: candidates.map(&:id),
|
||||
batch_size: 10,
|
||||
match_only: true,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def process_popular_posts
|
||||
# Find candidate posts that are popular and don't have concepts yet
|
||||
manager = DiscourseAi::InferredConcepts::Manager.new
|
||||
candidates =
|
||||
manager.find_candidate_posts(
|
||||
limit: SiteSetting.inferred_concepts_daily_posts_limit || 30,
|
||||
min_likes: SiteSetting.inferred_concepts_post_min_likes || 5,
|
||||
exclude_first_posts: true,
|
||||
created_after: SiteSetting.inferred_concepts_lookback_days.days.ago,
|
||||
)
|
||||
|
||||
return if candidates.blank?
|
||||
|
||||
# Process candidate posts - first generate concepts, then match
|
||||
Jobs.enqueue(
|
||||
:generate_inferred_concepts,
|
||||
item_type: "posts",
|
||||
item_ids: candidates.map(&:id),
|
||||
batch_size: 10,
|
||||
)
|
||||
|
||||
if SiteSetting.inferred_concepts_background_match
|
||||
# Schedule a follow-up job to match against existing concepts
|
||||
Jobs.enqueue_in(
|
||||
1.hour,
|
||||
:generate_inferred_concepts,
|
||||
item_type: "posts",
|
||||
item_ids: candidates.map(&:id),
|
||||
batch_size: 10,
|
||||
match_only: true,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class PostLocalizationBackfill < ::Jobs::Scheduled
|
||||
every 5.minutes
|
||||
cluster_concurrency 1
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
|
||||
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||
|
||||
Jobs.enqueue(:localize_posts, limit: SiteSetting.ai_translation_backfill_rate)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,56 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class PostsLocaleDetectionBackfill < ::Jobs::Scheduled
|
||||
every 5.minutes
|
||||
sidekiq_options retry: false
|
||||
cluster_concurrency 1
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||
|
||||
posts =
|
||||
Post
|
||||
.where(locale: nil)
|
||||
.where(deleted_at: nil)
|
||||
.where("posts.user_id > 0")
|
||||
.where.not(raw: [nil, ""])
|
||||
|
||||
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||
public_categories = Category.where(read_restricted: false).pluck(:id)
|
||||
posts =
|
||||
posts
|
||||
.joins(:topic)
|
||||
.where(topics: { category_id: public_categories })
|
||||
.where(topics: { archetype: "regular" })
|
||||
end
|
||||
|
||||
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||
posts =
|
||||
posts.where(
|
||||
"posts.created_at > ?",
|
||||
SiteSetting.ai_translation_backfill_max_age_days.days.ago,
|
||||
)
|
||||
end
|
||||
|
||||
posts = posts.order(updated_at: :desc).limit(SiteSetting.ai_translation_backfill_rate)
|
||||
return if posts.empty?
|
||||
|
||||
posts.each do |post|
|
||||
begin
|
||||
DiscourseAi::Translation::PostLocaleDetector.detect_locale(post)
|
||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||
# do nothing, there are too many sporadic lookup failures
|
||||
rescue => e
|
||||
DiscourseAi::Translation::VerboseLogger.log(
|
||||
"Failed to detect post #{post.id}'s locale: #{e.message}",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
DiscourseAi::Translation::VerboseLogger.log("Detected #{posts.size} post locales")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class TopicLocalizationBackfill < ::Jobs::Scheduled
|
||||
every 5.minutes
|
||||
cluster_concurrency 1
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
|
||||
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||
|
||||
Jobs.enqueue(:localize_topics, limit: SiteSetting.ai_translation_backfill_rate)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class TopicsLocaleDetectionBackfill < ::Jobs::Scheduled
|
||||
every 5.minutes
|
||||
sidekiq_options retry: false
|
||||
cluster_concurrency 1
|
||||
|
||||
def execute(args)
|
||||
return if !SiteSetting.discourse_ai_enabled
|
||||
return if !SiteSetting.ai_translation_enabled
|
||||
limit = SiteSetting.ai_translation_backfill_rate
|
||||
return if limit == 0
|
||||
|
||||
topics = Topic.where(locale: nil, deleted_at: nil).where("topics.user_id > 0")
|
||||
|
||||
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||
topics = topics.where(category_id: Category.where(read_restricted: false).select(:id))
|
||||
end
|
||||
|
||||
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||
topics =
|
||||
topics.where(
|
||||
"topics.created_at > ?",
|
||||
SiteSetting.ai_translation_backfill_max_age_days.days.ago,
|
||||
)
|
||||
end
|
||||
|
||||
topics = topics.order(updated_at: :desc).limit(limit)
|
||||
return if topics.empty?
|
||||
|
||||
topics.each do |topic|
|
||||
begin
|
||||
DiscourseAi::Translation::TopicLocaleDetector.detect_locale(topic)
|
||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||
# do nothing, there are too many sporadic lookup failures
|
||||
rescue => e
|
||||
DiscourseAi::Translation::VerboseLogger.log(
|
||||
"Failed to detect topic #{topic.id}'s locale: #{e.message}",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
DiscourseAi::Translation::VerboseLogger.log("Detected #{topics.size} topic locales")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -266,7 +266,6 @@ class AiPersona < ActiveRecord::Base
|
||||
define_method(:top_p) { @ai_persona&.top_p }
|
||||
define_method(:system_prompt) { @ai_persona&.system_prompt || "You are a helpful bot." }
|
||||
define_method(:uploads) { @ai_persona&.uploads }
|
||||
define_method(:response_format) { @ai_persona&.response_format }
|
||||
define_method(:examples) { @ai_persona&.examples }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,8 +22,6 @@ class AiTool < ActiveRecord::Base
|
||||
message: I18n.t("discourse_ai.tools.name.characters"),
|
||||
}
|
||||
|
||||
validate :validate_parameters_enum
|
||||
|
||||
def signature
|
||||
{
|
||||
name: function_call_name,
|
||||
@@ -59,30 +57,6 @@ class AiTool < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def validate_parameters_enum
|
||||
return unless parameters.is_a?(Array)
|
||||
|
||||
parameters.each_with_index do |param, index|
|
||||
next if !param.is_a?(Hash) || !param.key?("enum")
|
||||
enum_values = param["enum"]
|
||||
|
||||
if enum_values.empty?
|
||||
errors.add(
|
||||
:parameters,
|
||||
"Parameter '#{param["name"]}' at index #{index}: enum cannot be empty",
|
||||
)
|
||||
next
|
||||
end
|
||||
|
||||
if enum_values.uniq.length != enum_values.length
|
||||
errors.add(
|
||||
:parameters,
|
||||
"Parameter '#{param["name"]}' at index #{index}: enum values must be unique",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.preamble
|
||||
<<~JS
|
||||
/**
|
||||
@@ -168,7 +142,6 @@ class AiTool < ActiveRecord::Base
|
||||
* base_64_content (string): Base64 encoded content of the file.
|
||||
* Returns: { id: number, url: string, short_url: string } - Details of the created upload record.
|
||||
*
|
||||
* upload.getUrl(shortUrl): Given a short URL, eg upload://12345, returns the full CDN friendly URL of the upload.
|
||||
* 5. chain
|
||||
* Controls the execution flow.
|
||||
*
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InferredConcept < ActiveRecord::Base
|
||||
has_many :inferred_concept_topics
|
||||
has_many :topics, through: :inferred_concept_topics
|
||||
|
||||
has_many :inferred_concept_posts
|
||||
has_many :posts, through: :inferred_concept_posts
|
||||
|
||||
validates :name, presence: true, uniqueness: true
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: inferred_concepts
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_inferred_concepts_on_name (name) UNIQUE
|
||||
#
|
||||
@@ -1,25 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InferredConceptPost < ActiveRecord::Base
|
||||
belongs_to :inferred_concept
|
||||
belongs_to :post
|
||||
|
||||
validates :inferred_concept_id, presence: true
|
||||
validates :post_id, presence: true
|
||||
validates :inferred_concept_id, uniqueness: { scope: :post_id }
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: inferred_concept_posts
|
||||
#
|
||||
# inferred_concept_id :bigint
|
||||
# post_id :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_inferred_concept_posts_on_inferred_concept_id (inferred_concept_id)
|
||||
# index_inferred_concept_posts_uniqueness (post_id,inferred_concept_id) UNIQUE
|
||||
#
|
||||
@@ -1,25 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InferredConceptTopic < ActiveRecord::Base
|
||||
belongs_to :inferred_concept
|
||||
belongs_to :topic
|
||||
|
||||
validates :inferred_concept_id, presence: true
|
||||
validates :topic_id, presence: true
|
||||
validates :inferred_concept_id, uniqueness: { scope: :topic_id }
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: inferred_concept_topics
|
||||
#
|
||||
# inferred_concept_id :bigint
|
||||
# topic_id :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_inferred_concept_topics_on_inferred_concept_id (inferred_concept_id)
|
||||
# index_inferred_concept_topics_uniqueness (topic_id,inferred_concept_id) UNIQUE
|
||||
#
|
||||
@@ -1,34 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AiInferredConceptPostSerializer < ApplicationSerializer
|
||||
attributes :id,
|
||||
:post_number,
|
||||
:topic_id,
|
||||
:topic_title,
|
||||
:username,
|
||||
:avatar_template,
|
||||
:created_at,
|
||||
:updated_at,
|
||||
:excerpt,
|
||||
:truncated,
|
||||
:inferred_concepts
|
||||
|
||||
def avatar_template
|
||||
User.avatar_template(object.username, object.uploaded_avatar_id)
|
||||
end
|
||||
|
||||
def excerpt
|
||||
Post.excerpt(object.cooked)
|
||||
end
|
||||
|
||||
def truncated
|
||||
object.cooked.length > SiteSetting.post_excerpt_maxlength
|
||||
end
|
||||
|
||||
def inferred_concepts
|
||||
ActiveModel::ArraySerializer.new(
|
||||
object.inferred_concepts,
|
||||
each_serializer: InferredConceptSerializer,
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InferredConceptSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :created_at, :updated_at
|
||||
end
|
||||
@@ -1,7 +1,6 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import htmlClass from "discourse/helpers/html-class";
|
||||
@@ -89,15 +88,6 @@ export default class AiArtifactComponent extends Component {
|
||||
}`;
|
||||
}
|
||||
|
||||
@action
|
||||
setDataAttributes(element) {
|
||||
if (this.args.dataAttributes) {
|
||||
Object.entries(this.args.dataAttributes).forEach(([key, value]) => {
|
||||
element.setAttribute(key, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.expanded}}
|
||||
{{htmlClass "ai-artifact-expanded"}}
|
||||
@@ -128,7 +118,6 @@ export default class AiArtifactComponent extends Component {
|
||||
src={{this.artifactUrl}}
|
||||
width="100%"
|
||||
frameborder="0"
|
||||
{{didInsert this.setDataAttributes}}
|
||||
></iframe>
|
||||
{{/if}}
|
||||
{{#unless this.requireClickToRun}}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,6 @@ import { composeAiBotMessage } from "../lib/ai-bot-helper";
|
||||
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
|
||||
|
||||
export default class AiBotHeaderIcon extends Component {
|
||||
@service appEvents;
|
||||
@service composer;
|
||||
@service currentUser;
|
||||
@service navigationMenu;
|
||||
@@ -52,7 +51,6 @@ export default class AiBotHeaderIcon extends Component {
|
||||
}
|
||||
|
||||
if (this.siteSettings.ai_bot_enable_dedicated_ux) {
|
||||
this.appEvents.trigger("discourse-ai:bot-header-icon-clicked");
|
||||
return this.router.transitionTo("discourse-ai-bot-conversations");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import DButton from "discourse/components/d-button";
|
||||
import { AI_CONVERSATIONS_PANEL } from "../services/ai-conversations-sidebar-manager";
|
||||
|
||||
export default class AiBotSidebarNewConversation extends Component {
|
||||
@service appEvents;
|
||||
@service router;
|
||||
@service sidebarState;
|
||||
|
||||
@@ -15,8 +14,6 @@ export default class AiBotSidebarNewConversation extends Component {
|
||||
|
||||
@action
|
||||
routeTo() {
|
||||
this.appEvents.trigger("discourse-ai:new-conversation-btn-clicked");
|
||||
|
||||
if (this.router.currentRouteName !== "discourse-ai-bot-conversations") {
|
||||
this.router.transitionTo("/discourse-ai/ai-bot/conversations");
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export default class AiPersonaCollapsableExample extends Component {
|
||||
(if (eq pairIdx 0) "user" "model")
|
||||
)
|
||||
}}
|
||||
@validation="required|length:1,5000"
|
||||
@validation="required|length:1,100"
|
||||
@disabled={{@system}}
|
||||
as |field|
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user