Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30e258704e |
@@ -1,4 +1,3 @@
|
|||||||
< 3.5.0.beta7-dev: cd14b0c0bee0cf63c59b64b6f7213e31a37f11a7
|
|
||||||
< 3.5.0.beta6-dev: 3e74eea1e5e3143888d67a8d8a11206df214dc24
|
< 3.5.0.beta6-dev: 3e74eea1e5e3143888d67a8d8a11206df214dc24
|
||||||
< 3.5.0.beta3-dev: 09a68414804a1447f52e5d60691ba59742cda9ec
|
< 3.5.0.beta3-dev: 09a68414804a1447f52e5d60691ba59742cda9ec
|
||||||
< 3.5.0.beta2-dev: de8624416a15b3d8e7ad350b083cc1420451ccec
|
< 3.5.0.beta2-dev: de8624416a15b3d8e7ad350b083cc1420451ccec
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ evals/log
|
|||||||
evals/cases
|
evals/cases
|
||||||
config/eval-llms.local.yml
|
config/eval-llms.local.yml
|
||||||
# this gets rid of search results from ag, ripgrep, etc
|
# this gets rid of search results from ag, ripgrep, etc
|
||||||
|
tokenizers/
|
||||||
public/ai-share/highlight.min.js
|
public/ai-share/highlight.min.js
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ GEM
|
|||||||
racc
|
racc
|
||||||
prettier_print (1.2.1)
|
prettier_print (1.2.1)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.16)
|
rack (3.1.14)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
regexp_parser (2.10.0)
|
regexp_parser (2.10.0)
|
||||||
rubocop (1.74.0)
|
rubocop (1.74.0)
|
||||||
|
|||||||
-8
@@ -1,4 +1,3 @@
|
|||||||
import { action } from "@ember/object";
|
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import DiscourseRoute from "discourse/routes/discourse";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
import SiteSetting from "admin/models/site-setting";
|
import SiteSetting from "admin/models/site-setting";
|
||||||
@@ -25,11 +24,4 @@ export default class AdminPluginsShowDiscourseAiFeaturesEdit extends DiscourseRo
|
|||||||
|
|
||||||
return currentFeature;
|
return currentFeature;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
willTransition() {
|
|
||||||
// site settings may amend if a feature is enabled or disabled, so refresh the model
|
|
||||||
// even on back button
|
|
||||||
this.router.refresh("adminPlugins.show.discourse-ai-features");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,7 @@ module DiscourseAi
|
|||||||
def serialize_feature(feature)
|
def serialize_feature(feature)
|
||||||
{
|
{
|
||||||
name: feature.name,
|
name: feature.name,
|
||||||
personas: feature.persona_ids.map { |id| serialize_persona(persona_id_obj_hash[id]) },
|
persona: serialize_persona(persona_id_obj_hash[feature.persona_id]),
|
||||||
llm_models:
|
|
||||||
feature.llm_models.map do |llm_model|
|
|
||||||
{ id: llm_model.id, name: llm_model.display_name }
|
|
||||||
end,
|
|
||||||
enabled: feature.enabled?,
|
enabled: feature.enabled?,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -57,7 +53,9 @@ module DiscourseAi
|
|||||||
def persona_id_obj_hash
|
def persona_id_obj_hash
|
||||||
@persona_id_obj_hash ||=
|
@persona_id_obj_hash ||=
|
||||||
begin
|
begin
|
||||||
ids = DiscourseAi::Configuration::Feature.all.map(&:persona_ids).flatten.uniq
|
setting_names = DiscourseAi::Configuration::Feature.all_persona_setting_names
|
||||||
|
ids = setting_names.map { |sn| SiteSetting.public_send(sn) }
|
||||||
|
|
||||||
AiPersona.where(id: ids).index_by(&:id)
|
AiPersona.where(id: ids).index_by(&:id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module DiscourseAi
|
|||||||
class AiPersonasController < ::Admin::AdminController
|
class AiPersonasController < ::Admin::AdminController
|
||||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
before_action :find_ai_persona, only: %i[edit update destroy create_user export]
|
before_action :find_ai_persona, only: %i[edit update destroy create_user]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
ai_personas =
|
ai_personas =
|
||||||
@@ -105,43 +105,6 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def export
|
|
||||||
persona = AiPersona.find(params[:id])
|
|
||||||
exporter = DiscourseAi::PersonaExporter.new(persona: persona)
|
|
||||||
|
|
||||||
response.headers[
|
|
||||||
"Content-Disposition"
|
|
||||||
] = "attachment; filename=\"#{persona.name.parameterize}.json\""
|
|
||||||
|
|
||||||
render json: exporter.export
|
|
||||||
end
|
|
||||||
|
|
||||||
def import
|
|
||||||
name = params.dig(:persona, :name)
|
|
||||||
existing_persona = AiPersona.find_by(name: name)
|
|
||||||
force_update = params[:force].present? && params[:force].to_s.downcase == "true"
|
|
||||||
|
|
||||||
begin
|
|
||||||
importer = DiscourseAi::PersonaImporter.new(json: params.to_unsafe_h)
|
|
||||||
|
|
||||||
if existing_persona && force_update
|
|
||||||
initial_attributes = existing_persona.attributes.dup
|
|
||||||
persona = importer.import!(overwrite: true)
|
|
||||||
log_ai_persona_update(persona, initial_attributes)
|
|
||||||
render json: LocalizedAiPersonaSerializer.new(persona, root: false)
|
|
||||||
else
|
|
||||||
persona = importer.import!
|
|
||||||
log_ai_persona_creation(persona)
|
|
||||||
render json: LocalizedAiPersonaSerializer.new(persona, root: false), status: :created
|
|
||||||
end
|
|
||||||
rescue DiscourseAi::PersonaImporter::ImportError => e
|
|
||||||
render_json_error e.message, status: :unprocessable_entity
|
|
||||||
rescue StandardError => e
|
|
||||||
Rails.logger.error("AI Persona import failed: #{e.message}")
|
|
||||||
render_json_error "Import failed: #{e.message}", status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def stream_reply
|
def stream_reply
|
||||||
persona =
|
persona =
|
||||||
AiPersona.find_by(name: params[:persona_name]) ||
|
AiPersona.find_by(name: params[:persona_name]) ||
|
||||||
|
|||||||
@@ -11,13 +11,6 @@ module DiscourseAi
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
initial_settings = AiModerationSetting.spam
|
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_custom_instructions = initial_settings&.data&.dig("custom_instructions")
|
||||||
initial_llm_model_id = initial_settings&.llm_model_id
|
initial_llm_model_id = initial_settings&.llm_model_id
|
||||||
|
|
||||||
@@ -36,22 +29,6 @@ module DiscourseAi
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
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] = {
|
updated_params[:data] = {
|
||||||
custom_instructions: allowed_params[:custom_instructions],
|
custom_instructions: allowed_params[:custom_instructions],
|
||||||
} if allowed_params.key?(:custom_instructions)
|
} if allowed_params.key?(:custom_instructions)
|
||||||
@@ -64,7 +41,7 @@ module DiscourseAi
|
|||||||
AiModerationSetting.create!(updated_params.merge(setting_type: :spam))
|
AiModerationSetting.create!(updated_params.merge(setting_type: :spam))
|
||||||
end
|
end
|
||||||
|
|
||||||
log_ai_spam_update(initial_data, allowed_params)
|
log_ai_spam_update(initial_llm_model_id, initial_custom_instructions, allowed_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
is_enabled = ActiveModel::Type::Boolean.new.cast(allowed_params[:is_enabled])
|
is_enabled = ActiveModel::Type::Boolean.new.cast(allowed_params[:is_enabled])
|
||||||
@@ -142,10 +119,9 @@ module DiscourseAi
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def log_ai_spam_update(initial_data, params)
|
def log_ai_spam_update(initial_llm_model_id, initial_custom_instructions, params)
|
||||||
changes_to_log = {}
|
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
|
if params.key?(:llm_model_id) && initial_llm_model_id.to_s != params[:llm_model_id].to_s
|
||||||
old_model_name =
|
old_model_name =
|
||||||
LlmModel.find_by(id: initial_llm_model_id)&.display_name || initial_llm_model_id
|
LlmModel.find_by(id: initial_llm_model_id)&.display_name || initial_llm_model_id
|
||||||
@@ -155,22 +131,11 @@ module DiscourseAi
|
|||||||
changes_to_log[:llm_model_id] = "#{old_model_name} → #{new_model_name}"
|
changes_to_log[:llm_model_id] = "#{old_model_name} → #{new_model_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
initial_custom_instructions = initial_data[:custom_instructions]
|
|
||||||
if params.key?(:custom_instructions) &&
|
if params.key?(:custom_instructions) &&
|
||||||
initial_custom_instructions != params[:custom_instructions]
|
initial_custom_instructions != params[:custom_instructions]
|
||||||
changes_to_log[:custom_instructions] = params[:custom_instructions]
|
changes_to_log[:custom_instructions] = params[:custom_instructions]
|
||||||
end
|
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?
|
if changes_to_log.present?
|
||||||
changes_to_log[:subject] = I18n.t("discourse_ai.spam_detection.logging_subject")
|
changes_to_log[:subject] = I18n.t("discourse_ai.spam_detection.logging_subject")
|
||||||
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
logger = DiscourseAi::Utils::AiStaffActionLogger.new(current_user)
|
||||||
@@ -179,7 +144,7 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def allowed_params
|
def allowed_params
|
||||||
params.permit(:is_enabled, :llm_model_id, :custom_instructions, :ai_persona_id)
|
params.permit(:is_enabled, :llm_model_id, :custom_instructions)
|
||||||
end
|
end
|
||||||
|
|
||||||
def spam_config
|
def spam_config
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module DiscourseAi
|
|||||||
class AiToolsController < ::Admin::AdminController
|
class AiToolsController < ::Admin::AdminController
|
||||||
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
requires_plugin ::DiscourseAi::PLUGIN_NAME
|
||||||
|
|
||||||
before_action :find_ai_tool, only: %i[test edit update destroy export]
|
before_action :find_ai_tool, only: %i[test edit update destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
ai_tools = AiTool.all
|
ai_tools = AiTool.all
|
||||||
@@ -32,45 +32,6 @@ module DiscourseAi
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def export
|
|
||||||
response.headers[
|
|
||||||
"Content-Disposition"
|
|
||||||
] = "attachment; filename=\"#{@ai_tool.tool_name}.json\""
|
|
||||||
render_serialized(@ai_tool, AiCustomToolSerializer)
|
|
||||||
end
|
|
||||||
|
|
||||||
def import
|
|
||||||
existing_tool = AiTool.find_by(tool_name: ai_tool_params[:tool_name])
|
|
||||||
force_update = params[:force].present? && params[:force].to_s.downcase == "true"
|
|
||||||
|
|
||||||
if existing_tool && !force_update
|
|
||||||
return(
|
|
||||||
render_json_error "Tool with tool_name '#{ai_tool_params[:tool_name]}' already exists. Use force=true to overwrite.",
|
|
||||||
status: :conflict
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if existing_tool && force_update
|
|
||||||
initial_attributes = existing_tool.attributes.dup
|
|
||||||
if existing_tool.update(ai_tool_params)
|
|
||||||
log_ai_tool_update(existing_tool, initial_attributes)
|
|
||||||
render_serialized(existing_tool, AiCustomToolSerializer)
|
|
||||||
else
|
|
||||||
render_json_error existing_tool
|
|
||||||
end
|
|
||||||
else
|
|
||||||
ai_tool = AiTool.new(ai_tool_params)
|
|
||||||
ai_tool.created_by_id = current_user.id
|
|
||||||
|
|
||||||
if ai_tool.save
|
|
||||||
log_ai_tool_creation(ai_tool)
|
|
||||||
render_serialized(ai_tool, AiCustomToolSerializer, status: :created)
|
|
||||||
else
|
|
||||||
render_json_error ai_tool
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
initial_attributes = @ai_tool.attributes.dup
|
initial_attributes = @ai_tool.attributes.dup
|
||||||
|
|
||||||
|
|||||||
@@ -124,9 +124,6 @@ module DiscourseAi
|
|||||||
# otherwise we may end up streaming the data to the wrong client
|
# otherwise we may end up streaming the data to the wrong client
|
||||||
raise Discourse::InvalidParameters.new(:client_id) if params[:client_id].blank?
|
raise Discourse::InvalidParameters.new(:client_id) if params[:client_id].blank?
|
||||||
|
|
||||||
channel_id = next_channel_id
|
|
||||||
progress_channel = "discourse_ai_helper/stream_suggestions/#{channel_id}"
|
|
||||||
|
|
||||||
if location == "composer"
|
if location == "composer"
|
||||||
Jobs.enqueue(
|
Jobs.enqueue(
|
||||||
:stream_composer_helper,
|
:stream_composer_helper,
|
||||||
@@ -136,7 +133,6 @@ module DiscourseAi
|
|||||||
custom_prompt: params[:custom_prompt],
|
custom_prompt: params[:custom_prompt],
|
||||||
force_default_locale: params[:force_default_locale] || false,
|
force_default_locale: params[:force_default_locale] || false,
|
||||||
client_id: params[:client_id],
|
client_id: params[:client_id],
|
||||||
progress_channel:,
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
post_id = get_post_param!
|
post_id = get_post_param!
|
||||||
@@ -152,11 +148,10 @@ module DiscourseAi
|
|||||||
prompt: params[:mode],
|
prompt: params[:mode],
|
||||||
custom_prompt: params[:custom_prompt],
|
custom_prompt: params[:custom_prompt],
|
||||||
client_id: params[:client_id],
|
client_id: params[:client_id],
|
||||||
progress_channel:,
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: { success: true, progress_channel: }, status: 200
|
render json: { success: true }, status: 200
|
||||||
rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed
|
rescue DiscourseAi::Completions::Endpoints::Base::CompletionFailed
|
||||||
render_json_error I18n.t("discourse_ai.ai_helper.errors.completion_request_failed"),
|
render_json_error I18n.t("discourse_ai.ai_helper.errors.completion_request_failed"),
|
||||||
status: 502
|
status: 502
|
||||||
@@ -197,18 +192,6 @@ module DiscourseAi
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
CHANNEL_ID_KEY = "discourse_ai_helper_next_channel_id"
|
|
||||||
|
|
||||||
def next_channel_id
|
|
||||||
Discourse
|
|
||||||
.redis
|
|
||||||
.pipelined do |pipeline|
|
|
||||||
pipeline.incr(CHANNEL_ID_KEY)
|
|
||||||
pipeline.expire(CHANNEL_ID_KEY, 1.day)
|
|
||||||
end
|
|
||||||
.first
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_text_param!
|
def get_text_param!
|
||||||
params[:text].tap { |t| raise Discourse::InvalidParameters.new(:text) if t.blank? }
|
params[:text].tap { |t| raise Discourse::InvalidParameters.new(:text) if t.blank? }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
module Jobs
|
module Jobs
|
||||||
class DetectTranslatePost < ::Jobs::Base
|
class DetectTranslatePost < ::Jobs::Base
|
||||||
cluster_concurrency 1
|
|
||||||
sidekiq_options retry: false
|
sidekiq_options retry: false
|
||||||
|
|
||||||
def execute(args)
|
def execute(args)
|
||||||
@@ -36,7 +35,7 @@ module Jobs
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
locales = SiteSetting.content_localization_supported_locales.split("|")
|
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||||
return if locales.blank?
|
return if locales.blank?
|
||||||
|
|
||||||
locales.each do |locale|
|
locales.each do |locale|
|
||||||
@@ -49,7 +48,7 @@ module Jobs
|
|||||||
# do nothing, there are too many sporadic lookup failures
|
# do nothing, there are too many sporadic lookup failures
|
||||||
rescue => e
|
rescue => e
|
||||||
DiscourseAi::Translation::VerboseLogger.log(
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
"Failed to translate post #{post.id} to #{locale}: #{e.message}\n\n#{e.backtrace[0..3].join("\n")}",
|
"Failed to translate post #{post.id} to #{locale}: #{e.message}",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
module Jobs
|
module Jobs
|
||||||
class DetectTranslateTopic < ::Jobs::Base
|
class DetectTranslateTopic < ::Jobs::Base
|
||||||
cluster_concurrency 1
|
|
||||||
sidekiq_options retry: false
|
sidekiq_options retry: false
|
||||||
|
|
||||||
def execute(args)
|
def execute(args)
|
||||||
@@ -34,7 +33,7 @@ module Jobs
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
locales = SiteSetting.content_localization_supported_locales.split("|")
|
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||||
return if locales.blank?
|
return if locales.blank?
|
||||||
|
|
||||||
locales.each do |locale|
|
locales.each do |locale|
|
||||||
@@ -47,7 +46,7 @@ module Jobs
|
|||||||
# do nothing, there are too many sporadic lookup failures
|
# do nothing, there are too many sporadic lookup failures
|
||||||
rescue => e
|
rescue => e
|
||||||
DiscourseAi::Translation::VerboseLogger.log(
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
"Failed to translate topic #{topic.id} to #{locale}: #{e.message}\n\n#{e.backtrace[0..3].join("\n")}",
|
"Failed to translate topic #{topic.id} to #{locale}: #{e.message}",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,51 +5,51 @@ module Jobs
|
|||||||
cluster_concurrency 1
|
cluster_concurrency 1
|
||||||
sidekiq_options retry: false
|
sidekiq_options retry: false
|
||||||
|
|
||||||
def execute(args)
|
BATCH_SIZE = 50
|
||||||
limit = args[:limit]
|
|
||||||
raise Discourse::InvalidParameters.new(:limit) if limit.nil?
|
|
||||||
return if limit <= 0
|
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
return if !SiteSetting.ai_translation_enabled
|
return if !SiteSetting.ai_translation_enabled
|
||||||
locales = SiteSetting.content_localization_supported_locales.split("|")
|
|
||||||
|
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||||
return if locales.blank?
|
return if locales.blank?
|
||||||
|
|
||||||
categories = Category.where("locale IS NOT NULL")
|
cat_id = args[:from_category_id] || Category.order(:id).first&.id
|
||||||
|
last_id = nil
|
||||||
|
|
||||||
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
categories =
|
||||||
categories = categories.where(read_restricted: false)
|
Category.where("id >= ? AND locale IS NOT NULL", cat_id).order(:id).limit(BATCH_SIZE)
|
||||||
end
|
|
||||||
|
|
||||||
categories = categories.order(:id).limit(limit)
|
|
||||||
return if categories.empty?
|
return if categories.empty?
|
||||||
|
|
||||||
remaining_limit = limit
|
|
||||||
|
|
||||||
categories.each do |category|
|
categories.each do |category|
|
||||||
break if remaining_limit <= 0
|
if SiteSetting.ai_translation_backfill_limit_to_public_content && category.read_restricted?
|
||||||
|
last_id = category.id
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
existing_locales = CategoryLocalization.where(category_id: category.id).pluck(:locale)
|
locales.each do |locale|
|
||||||
missing_locales = locales - existing_locales - [category.locale]
|
localization = category.category_localizations.find_by(locale:)
|
||||||
missing_locales.each do |locale|
|
|
||||||
break if remaining_limit <= 0
|
|
||||||
|
|
||||||
begin
|
if locale == category.locale && localization
|
||||||
DiscourseAi::Translation::CategoryLocalizer.localize(category, locale)
|
localization.destroy
|
||||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
else
|
||||||
# do nothing, there are too many sporadic lookup failures
|
next if locale == category.locale
|
||||||
rescue => e
|
begin
|
||||||
DiscourseAi::Translation::VerboseLogger.log(
|
DiscourseAi::Translation::CategoryLocalizer.localize(category, locale)
|
||||||
"Failed to translate category #{category.id} to #{locale}: #{e.message}\n\n#{e.backtrace[0..3].join("\n")}",
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
)
|
# do nothing, there are too many sporadic lookup failures
|
||||||
ensure
|
rescue => e
|
||||||
remaining_limit -= 1
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
|
"Failed to translate category #{category.id} to #{locale}: #{e.message}",
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
last_id = category.id
|
||||||
|
end
|
||||||
|
|
||||||
if existing_locales.include?(category.locale)
|
if categories.size == BATCH_SIZE
|
||||||
CategoryLocalization.find_by(category_id: category.id, locale: category.locale).destroy
|
Jobs.enqueue_in(10.seconds, :localize_categories, from_category_id: last_id + 1)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ module Jobs
|
|||||||
cluster_concurrency 1
|
cluster_concurrency 1
|
||||||
sidekiq_options retry: false
|
sidekiq_options retry: false
|
||||||
|
|
||||||
def execute(args)
|
BATCH_SIZE = 50
|
||||||
limit = args[:limit]
|
|
||||||
raise Discourse::InvalidParameters.new(:limit) if limit.blank? || limit <= 0
|
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
return if !SiteSetting.ai_translation_enabled
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
|
||||||
locales = SiteSetting.content_localization_supported_locales.split("|")
|
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||||
return if locales.blank?
|
return if locales.blank?
|
||||||
|
|
||||||
|
limit = args[:limit] || BATCH_SIZE
|
||||||
|
|
||||||
locales.each do |locale|
|
locales.each do |locale|
|
||||||
posts =
|
posts =
|
||||||
Post
|
Post
|
||||||
@@ -65,7 +66,7 @@ module Jobs
|
|||||||
# do nothing, there are too many sporadic lookup failures
|
# do nothing, there are too many sporadic lookup failures
|
||||||
rescue => e
|
rescue => e
|
||||||
DiscourseAi::Translation::VerboseLogger.log(
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
"Failed to translate post #{post.id} to #{locale}: #{e.message}\n\n#{e.backtrace[0..3].join("\n")}",
|
"Failed to translate post #{post.id} to #{locale}: #{e.message}",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,16 +5,17 @@ module Jobs
|
|||||||
cluster_concurrency 1
|
cluster_concurrency 1
|
||||||
sidekiq_options retry: false
|
sidekiq_options retry: false
|
||||||
|
|
||||||
def execute(args)
|
BATCH_SIZE = 50
|
||||||
limit = args[:limit]
|
|
||||||
raise Discourse::InvalidParameters.new(:limit) if limit.blank? || limit <= 0
|
|
||||||
|
|
||||||
|
def execute(args)
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
return if !SiteSetting.ai_translation_enabled
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
|
||||||
locales = SiteSetting.content_localization_supported_locales.split("|")
|
locales = SiteSetting.experimental_content_localization_supported_locales.split("|")
|
||||||
return if locales.blank?
|
return if locales.blank?
|
||||||
|
|
||||||
|
limit = args[:limit] || BATCH_SIZE
|
||||||
|
|
||||||
locales.each do |locale|
|
locales.each do |locale|
|
||||||
topics =
|
topics =
|
||||||
Topic
|
Topic
|
||||||
@@ -62,7 +63,7 @@ module Jobs
|
|||||||
# do nothing, there are too many sporadic lookup failures
|
# do nothing, there are too many sporadic lookup failures
|
||||||
rescue => e
|
rescue => e
|
||||||
DiscourseAi::Translation::VerboseLogger.log(
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
"Failed to translate topic #{topic.id} to #{locale}: #{e.message}\n\n#{e.backtrace[0..3].join("\n")}",
|
"Failed to translate topic #{topic.id} to #{locale}: #{e.message}",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ module Jobs
|
|||||||
return unless user = User.find_by(id: args[:user_id])
|
return unless user = User.find_by(id: args[:user_id])
|
||||||
return unless args[:text]
|
return unless args[:text]
|
||||||
return unless args[:client_id]
|
return unless args[:client_id]
|
||||||
return unless args[:progress_channel]
|
|
||||||
|
|
||||||
helper_mode = args[:prompt]
|
helper_mode = args[:prompt]
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ module Jobs
|
|||||||
helper_mode,
|
helper_mode,
|
||||||
args[:text],
|
args[:text],
|
||||||
user,
|
user,
|
||||||
args[:progress_channel],
|
"/discourse-ai/ai-helper/stream_composer_suggestion",
|
||||||
force_default_locale: args[:force_default_locale],
|
force_default_locale: args[:force_default_locale],
|
||||||
client_id: args[:client_id],
|
client_id: args[:client_id],
|
||||||
custom_prompt: args[:custom_prompt],
|
custom_prompt: args[:custom_prompt],
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ module Jobs
|
|||||||
return unless post = Post.includes(:topic).find_by(id: args[:post_id])
|
return unless post = Post.includes(:topic).find_by(id: args[:post_id])
|
||||||
return unless user = User.find_by(id: args[:user_id])
|
return unless user = User.find_by(id: args[:user_id])
|
||||||
return unless args[:text]
|
return unless args[:text]
|
||||||
return unless args[:progress_channel]
|
|
||||||
return unless args[:client_id]
|
|
||||||
|
|
||||||
topic = post.topic
|
topic = post.topic
|
||||||
reply_to = post.reply_to_post
|
reply_to = post.reply_to_post
|
||||||
@@ -33,9 +31,8 @@ module Jobs
|
|||||||
helper_mode,
|
helper_mode,
|
||||||
input,
|
input,
|
||||||
user,
|
user,
|
||||||
args[:progress_channel],
|
"/discourse-ai/ai-helper/stream_suggestion/#{post.id}",
|
||||||
custom_prompt: args[:custom_prompt],
|
custom_prompt: args[:custom_prompt],
|
||||||
client_id: args[:client_id],
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ module Jobs
|
|||||||
def execute(args)
|
def execute(args)
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
return if !SiteSetting.ai_translation_enabled
|
return if !SiteSetting.ai_translation_enabled
|
||||||
limit = SiteSetting.ai_translation_backfill_hourly_rate
|
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||||
return if limit == 0
|
|
||||||
|
|
||||||
categories = Category.where(locale: nil)
|
categories = Category.where(locale: nil)
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ module Jobs
|
|||||||
categories = categories.where(read_restricted: false)
|
categories = categories.where(read_restricted: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
categories = categories.limit(limit)
|
categories = categories.limit(SiteSetting.ai_translation_backfill_rate)
|
||||||
return if categories.empty?
|
return if categories.empty?
|
||||||
|
|
||||||
categories.each do |category|
|
categories.each do |category|
|
||||||
@@ -27,7 +26,7 @@ module Jobs
|
|||||||
rescue FinalDestination::SSRFDetector::LookupFailedError
|
rescue FinalDestination::SSRFDetector::LookupFailedError
|
||||||
rescue => e
|
rescue => e
|
||||||
DiscourseAi::Translation::VerboseLogger.log(
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
"Failed to detect category #{category.id}'s locale: #{e.message}\n\n#{e.backtrace[0..3].join("\n")}",
|
"Failed to detect category #{category.id}'s locale: #{e.message}",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,17 +2,15 @@
|
|||||||
|
|
||||||
module Jobs
|
module Jobs
|
||||||
class CategoryLocalizationBackfill < ::Jobs::Scheduled
|
class CategoryLocalizationBackfill < ::Jobs::Scheduled
|
||||||
every 1.hour
|
every 12.hours
|
||||||
cluster_concurrency 1
|
cluster_concurrency 1
|
||||||
|
|
||||||
def execute(args)
|
def execute(args)
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
return if !SiteSetting.ai_translation_enabled
|
return if !SiteSetting.ai_translation_enabled
|
||||||
return if SiteSetting.content_localization_supported_locales.blank?
|
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||||
limit = SiteSetting.ai_translation_backfill_hourly_rate
|
|
||||||
return if limit == 0
|
|
||||||
|
|
||||||
Jobs.enqueue(:localize_categories, limit:)
|
Jobs.enqueue(:localize_categories)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,115 +18,105 @@ module Jobs
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
production_vector = DiscourseAi::Embeddings::Vector.instance
|
rebaked = 0
|
||||||
|
|
||||||
if SiteSetting.ai_embeddings_backfill_model.present? &&
|
vector = DiscourseAi::Embeddings::Vector.instance
|
||||||
SiteSetting.ai_embeddings_backfill_model != SiteSetting.ai_embeddings_selected_model
|
vector_def = vector.vdef
|
||||||
backfill_vector =
|
table_name = DiscourseAi::Embeddings::Schema::TOPICS_TABLE
|
||||||
DiscourseAi::Embeddings::Vector.new(
|
|
||||||
EmbeddingDefinition.find_by(id: SiteSetting.ai_embeddings_backfill_model),
|
topics =
|
||||||
|
Topic
|
||||||
|
.joins(
|
||||||
|
"LEFT JOIN #{table_name} ON #{table_name}.topic_id = topics.id AND #{table_name}.model_id = #{vector_def.id}",
|
||||||
)
|
)
|
||||||
end
|
.where(archetype: Archetype.default)
|
||||||
|
.where(deleted_at: nil)
|
||||||
|
.order("topics.bumped_at DESC")
|
||||||
|
|
||||||
topic_work_list = []
|
rebaked += populate_topic_embeddings(vector, topics.limit(limit - rebaked))
|
||||||
topic_work_list << production_vector
|
|
||||||
topic_work_list << backfill_vector if backfill_vector
|
|
||||||
|
|
||||||
topic_work_list.each do |vector|
|
return if rebaked >= limit
|
||||||
rebaked = 0
|
|
||||||
table_name = DiscourseAi::Embeddings::Schema::TOPICS_TABLE
|
|
||||||
vector_def = vector.vdef
|
|
||||||
|
|
||||||
topics =
|
# Then, we'll try to backfill embeddings for topics that have outdated
|
||||||
Topic
|
# embeddings, be it model or strategy version
|
||||||
.joins(
|
relation = topics.where(<<~SQL).limit(limit - rebaked)
|
||||||
"LEFT JOIN #{table_name} ON #{table_name}.topic_id = topics.id AND #{table_name}.model_id = #{vector_def.id}",
|
#{table_name}.model_version < #{vector_def.version}
|
||||||
)
|
OR
|
||||||
.where(archetype: Archetype.default)
|
#{table_name}.strategy_version < #{vector_def.strategy_version}
|
||||||
.where(deleted_at: nil)
|
SQL
|
||||||
.order("topics.bumped_at DESC")
|
|
||||||
|
|
||||||
rebaked += populate_topic_embeddings(vector, topics.limit(limit - rebaked))
|
rebaked += populate_topic_embeddings(vector, relation, force: true)
|
||||||
|
|
||||||
next if rebaked >= limit
|
return if rebaked >= limit
|
||||||
|
|
||||||
# Then, we'll try to backfill embeddings for topics that have outdated
|
# Finally, we'll try to backfill embeddings for topics that have outdated
|
||||||
# embeddings, be it model or strategy version
|
# embeddings due to edits or new replies. Here we only do 10% of the limit
|
||||||
relation = topics.where(<<~SQL).limit(limit - rebaked)
|
relation =
|
||||||
#{table_name}.model_version < #{vector_def.version}
|
topics
|
||||||
OR
|
.where("#{table_name}.updated_at < ?", 6.hours.ago)
|
||||||
#{table_name}.strategy_version < #{vector_def.strategy_version}
|
.where("#{table_name}.updated_at < topics.updated_at")
|
||||||
SQL
|
|
||||||
|
|
||||||
rebaked += populate_topic_embeddings(vector, relation, force: true)
|
|
||||||
|
|
||||||
next if rebaked >= limit
|
|
||||||
|
|
||||||
# Finally, we'll try to backfill embeddings for topics that have outdated
|
|
||||||
# embeddings due to edits or new replies. Here we only do 10% of the limit
|
|
||||||
relation =
|
|
||||||
topics
|
|
||||||
.where("#{table_name}.updated_at < ?", 6.hours.ago)
|
|
||||||
.where("#{table_name}.updated_at < topics.updated_at")
|
|
||||||
.limit((limit - rebaked) / 10)
|
|
||||||
|
|
||||||
populate_topic_embeddings(vector, relation, force: true)
|
|
||||||
|
|
||||||
next unless SiteSetting.ai_embeddings_per_post_enabled
|
|
||||||
|
|
||||||
# Now for posts
|
|
||||||
table_name = DiscourseAi::Embeddings::Schema::POSTS_TABLE
|
|
||||||
posts_batch_size = 1000
|
|
||||||
|
|
||||||
posts =
|
|
||||||
Post
|
|
||||||
.joins(
|
|
||||||
"LEFT JOIN #{table_name} ON #{table_name}.post_id = posts.id AND #{table_name}.model_id = #{vector_def.id}",
|
|
||||||
)
|
|
||||||
.where(deleted_at: nil)
|
|
||||||
.where(post_type: Post.types[:regular])
|
|
||||||
|
|
||||||
# First, we'll try to backfill embeddings for posts that have none
|
|
||||||
posts
|
|
||||||
.where("#{table_name}.post_id IS NULL")
|
|
||||||
.limit(limit - rebaked)
|
|
||||||
.pluck(:id)
|
|
||||||
.each_slice(posts_batch_size) do |batch|
|
|
||||||
vector.gen_bulk_reprensentations(Post.where(id: batch))
|
|
||||||
rebaked += batch.length
|
|
||||||
end
|
|
||||||
|
|
||||||
next if rebaked >= limit
|
|
||||||
|
|
||||||
# Then, we'll try to backfill embeddings for posts that have outdated
|
|
||||||
# embeddings, be it model or strategy version
|
|
||||||
posts
|
|
||||||
.where(<<~SQL)
|
|
||||||
#{table_name}.model_version < #{vector_def.version}
|
|
||||||
OR
|
|
||||||
#{table_name}.strategy_version < #{vector_def.strategy_version}
|
|
||||||
SQL
|
|
||||||
.limit(limit - rebaked)
|
|
||||||
.pluck(:id)
|
|
||||||
.each_slice(posts_batch_size) do |batch|
|
|
||||||
vector.gen_bulk_reprensentations(Post.where(id: batch))
|
|
||||||
rebaked += batch.length
|
|
||||||
end
|
|
||||||
|
|
||||||
next if rebaked >= limit
|
|
||||||
|
|
||||||
# Finally, we'll try to backfill embeddings for posts that have outdated
|
|
||||||
# embeddings due to edits. Here we only do 10% of the limit
|
|
||||||
posts
|
|
||||||
.where("#{table_name}.updated_at < ?", 7.days.ago)
|
|
||||||
.order("random()")
|
|
||||||
.limit((limit - rebaked) / 10)
|
.limit((limit - rebaked) / 10)
|
||||||
.pluck(:id)
|
|
||||||
.each_slice(posts_batch_size) do |batch|
|
populate_topic_embeddings(vector, relation, force: true)
|
||||||
vector.gen_bulk_reprensentations(Post.where(id: batch))
|
|
||||||
rebaked += batch.length
|
return if rebaked >= limit
|
||||||
end
|
|
||||||
end
|
return unless SiteSetting.ai_embeddings_per_post_enabled
|
||||||
|
|
||||||
|
# Now for posts
|
||||||
|
table_name = DiscourseAi::Embeddings::Schema::POSTS_TABLE
|
||||||
|
posts_batch_size = 1000
|
||||||
|
|
||||||
|
posts =
|
||||||
|
Post
|
||||||
|
.joins(
|
||||||
|
"LEFT JOIN #{table_name} ON #{table_name}.post_id = posts.id AND #{table_name}.model_id = #{vector_def.id}",
|
||||||
|
)
|
||||||
|
.where(deleted_at: nil)
|
||||||
|
.where(post_type: Post.types[:regular])
|
||||||
|
|
||||||
|
# First, we'll try to backfill embeddings for posts that have none
|
||||||
|
posts
|
||||||
|
.where("#{table_name}.post_id IS NULL")
|
||||||
|
.limit(limit - rebaked)
|
||||||
|
.pluck(:id)
|
||||||
|
.each_slice(posts_batch_size) do |batch|
|
||||||
|
vector.gen_bulk_reprensentations(Post.where(id: batch))
|
||||||
|
rebaked += batch.length
|
||||||
|
end
|
||||||
|
|
||||||
|
return if rebaked >= limit
|
||||||
|
|
||||||
|
# Then, we'll try to backfill embeddings for posts that have outdated
|
||||||
|
# embeddings, be it model or strategy version
|
||||||
|
posts
|
||||||
|
.where(<<~SQL)
|
||||||
|
#{table_name}.model_version < #{vector_def.version}
|
||||||
|
OR
|
||||||
|
#{table_name}.strategy_version < #{vector_def.strategy_version}
|
||||||
|
SQL
|
||||||
|
.limit(limit - rebaked)
|
||||||
|
.pluck(:id)
|
||||||
|
.each_slice(posts_batch_size) do |batch|
|
||||||
|
vector.gen_bulk_reprensentations(Post.where(id: batch))
|
||||||
|
rebaked += batch.length
|
||||||
|
end
|
||||||
|
|
||||||
|
return if rebaked >= limit
|
||||||
|
|
||||||
|
# Finally, we'll try to backfill embeddings for posts that have outdated
|
||||||
|
# embeddings due to edits. Here we only do 10% of the limit
|
||||||
|
posts
|
||||||
|
.where("#{table_name}.updated_at < ?", 7.days.ago)
|
||||||
|
.order("random()")
|
||||||
|
.limit((limit - rebaked) / 10)
|
||||||
|
.pluck(:id)
|
||||||
|
.each_slice(posts_batch_size) do |batch|
|
||||||
|
vector.gen_bulk_reprensentations(Post.where(id: batch))
|
||||||
|
rebaked += batch.length
|
||||||
|
end
|
||||||
|
|
||||||
|
rebaked
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ module Jobs
|
|||||||
return if !SiteSetting.discourse_ai_enabled
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
return if !SiteSetting.ai_translation_enabled
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
|
||||||
return if SiteSetting.content_localization_supported_locales.blank?
|
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||||
limit = SiteSetting.ai_translation_backfill_hourly_rate / (60 / 5) # this job runs in 5-minute intervals
|
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||||
return if limit == 0
|
|
||||||
|
|
||||||
Jobs.enqueue(:localize_posts, limit:)
|
Jobs.enqueue(:localize_posts, limit: SiteSetting.ai_translation_backfill_rate)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ module Jobs
|
|||||||
def execute(args)
|
def execute(args)
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
return if !SiteSetting.ai_translation_enabled
|
return if !SiteSetting.ai_translation_enabled
|
||||||
limit = SiteSetting.ai_translation_backfill_hourly_rate / (60 / 5) # this job runs in 5-minute intervals
|
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||||
return if limit == 0
|
|
||||||
|
|
||||||
posts =
|
posts =
|
||||||
Post
|
Post
|
||||||
@@ -20,17 +19,12 @@ module Jobs
|
|||||||
.where.not(raw: [nil, ""])
|
.where.not(raw: [nil, ""])
|
||||||
|
|
||||||
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||||
|
public_categories = Category.where(read_restricted: false).pluck(:id)
|
||||||
posts =
|
posts =
|
||||||
posts
|
posts
|
||||||
.joins(:topic)
|
.joins(:topic)
|
||||||
.where(topics: { category_id: Category.where(read_restricted: false).select(:id) })
|
.where(topics: { category_id: public_categories })
|
||||||
.where("archetype != ?", Archetype.private_message)
|
.where(topics: { archetype: "regular" })
|
||||||
else
|
|
||||||
posts =
|
|
||||||
posts.joins(:topic).where(
|
|
||||||
"topics.archetype != ? OR EXISTS (SELECT 1 FROM topic_allowed_groups WHERE topic_id = topics.id)",
|
|
||||||
Archetype.private_message,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||||
@@ -41,7 +35,7 @@ module Jobs
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
posts = posts.order(updated_at: :desc).limit(limit)
|
posts = posts.order(updated_at: :desc).limit(SiteSetting.ai_translation_backfill_rate)
|
||||||
return if posts.empty?
|
return if posts.empty?
|
||||||
|
|
||||||
posts.each do |post|
|
posts.each do |post|
|
||||||
@@ -51,7 +45,7 @@ module Jobs
|
|||||||
# do nothing, there are too many sporadic lookup failures
|
# do nothing, there are too many sporadic lookup failures
|
||||||
rescue => e
|
rescue => e
|
||||||
DiscourseAi::Translation::VerboseLogger.log(
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
"Failed to detect post #{post.id}'s locale: #{e.message}\n\n#{e.backtrace[0..3].join("\n")}",
|
"Failed to detect post #{post.id}'s locale: #{e.message}",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ module Jobs
|
|||||||
return if !SiteSetting.discourse_ai_enabled
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
return if !SiteSetting.ai_translation_enabled
|
return if !SiteSetting.ai_translation_enabled
|
||||||
|
|
||||||
return if SiteSetting.content_localization_supported_locales.blank?
|
return if SiteSetting.experimental_content_localization_supported_locales.blank?
|
||||||
limit = SiteSetting.ai_translation_backfill_hourly_rate / (60 / 5) # this job runs in 5-minute intervals
|
return if SiteSetting.ai_translation_backfill_rate == 0
|
||||||
return if limit == 0
|
|
||||||
|
|
||||||
Jobs.enqueue(:localize_topics, limit:)
|
Jobs.enqueue(:localize_topics, limit: SiteSetting.ai_translation_backfill_rate)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,23 +9,14 @@ module Jobs
|
|||||||
def execute(args)
|
def execute(args)
|
||||||
return if !SiteSetting.discourse_ai_enabled
|
return if !SiteSetting.discourse_ai_enabled
|
||||||
return if !SiteSetting.ai_translation_enabled
|
return if !SiteSetting.ai_translation_enabled
|
||||||
limit = SiteSetting.ai_translation_backfill_hourly_rate / (60 / 5) # this job runs in 5-minute intervals
|
limit = SiteSetting.ai_translation_backfill_rate
|
||||||
|
|
||||||
return if limit == 0
|
return if limit == 0
|
||||||
|
|
||||||
topics = Topic.where(locale: nil, deleted_at: nil).where("topics.user_id > 0")
|
topics = Topic.where(locale: nil, deleted_at: nil).where("topics.user_id > 0")
|
||||||
|
|
||||||
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
if SiteSetting.ai_translation_backfill_limit_to_public_content
|
||||||
topics =
|
topics = topics.where(category_id: Category.where(read_restricted: false).select(:id))
|
||||||
topics.where(category_id: Category.where(read_restricted: false).select(:id)).where(
|
|
||||||
"archetype != ?",
|
|
||||||
Archetype.private_message,
|
|
||||||
)
|
|
||||||
else
|
|
||||||
topics =
|
|
||||||
topics.where(
|
|
||||||
"archetype != ? OR EXISTS (SELECT 1 FROM topic_allowed_groups WHERE topic_id = topics.id)",
|
|
||||||
Archetype.private_message,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
if SiteSetting.ai_translation_backfill_max_age_days > 0
|
||||||
@@ -46,7 +37,7 @@ module Jobs
|
|||||||
# do nothing, there are too many sporadic lookup failures
|
# do nothing, there are too many sporadic lookup failures
|
||||||
rescue => e
|
rescue => e
|
||||||
DiscourseAi::Translation::VerboseLogger.log(
|
DiscourseAi::Translation::VerboseLogger.log(
|
||||||
"Failed to detect topic #{topic.id}'s locale: #{e.message}\n\n#{e.backtrace[0..3].join("\n")}",
|
"Failed to detect topic #{topic.id}'s locale: #{e.message}",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
class AiModerationSetting < ActiveRecord::Base
|
class AiModerationSetting < ActiveRecord::Base
|
||||||
belongs_to :llm_model
|
belongs_to :llm_model
|
||||||
belongs_to :ai_persona
|
|
||||||
|
|
||||||
validates :llm_model_id, presence: true
|
validates :llm_model_id, presence: true
|
||||||
validates :setting_type, presence: true
|
validates :setting_type, presence: true
|
||||||
@@ -20,13 +19,12 @@ end
|
|||||||
#
|
#
|
||||||
# Table name: ai_moderation_settings
|
# Table name: ai_moderation_settings
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# setting_type :enum not null
|
# setting_type :enum not null
|
||||||
# data :jsonb
|
# data :jsonb
|
||||||
# llm_model_id :bigint not null
|
# llm_model_id :bigint not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# ai_persona_id :bigint default(-31), not null
|
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ class EmbeddingDefinition < ActiveRecord::Base
|
|||||||
DiscourseAi::Tokenizer::GeminiTokenizer,
|
DiscourseAi::Tokenizer::GeminiTokenizer,
|
||||||
DiscourseAi::Tokenizer::MultilingualE5LargeTokenizer,
|
DiscourseAi::Tokenizer::MultilingualE5LargeTokenizer,
|
||||||
DiscourseAi::Tokenizer::OpenAiTokenizer,
|
DiscourseAi::Tokenizer::OpenAiTokenizer,
|
||||||
DiscourseAi::Tokenizer::OpenAiCl100kTokenizer,
|
DiscourseAi::Tokenizer::MixtralTokenizer,
|
||||||
DiscourseAi::Tokenizer::MistralTokenizer,
|
|
||||||
DiscourseAi::Tokenizer::QwenTokenizer,
|
|
||||||
].map(&:name)
|
].map(&:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -65,8 +65,6 @@ class LlmModel < ActiveRecord::Base
|
|||||||
google: {
|
google: {
|
||||||
disable_native_tools: :checkbox,
|
disable_native_tools: :checkbox,
|
||||||
enable_thinking: :checkbox,
|
enable_thinking: :checkbox,
|
||||||
disable_temperature: :checkbox,
|
|
||||||
disable_top_p: :checkbox,
|
|
||||||
thinking_tokens: :number,
|
thinking_tokens: :number,
|
||||||
},
|
},
|
||||||
azure: {
|
azure: {
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ class AiSpamSerializer < ApplicationSerializer
|
|||||||
:stats,
|
:stats,
|
||||||
:flagging_username,
|
:flagging_username,
|
||||||
:spam_score_type,
|
:spam_score_type,
|
||||||
:spam_scanning_user,
|
:spam_scanning_user
|
||||||
:ai_persona_id,
|
|
||||||
:available_personas
|
|
||||||
|
|
||||||
def is_enabled
|
def is_enabled
|
||||||
object[:enabled]
|
object[:enabled]
|
||||||
@@ -20,11 +18,6 @@ class AiSpamSerializer < ApplicationSerializer
|
|||||||
settings&.llm_model&.id
|
settings&.llm_model&.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def ai_persona_id
|
|
||||||
settings&.ai_persona&.id ||
|
|
||||||
DiscourseAi::Personas::Persona.system_personas[DiscourseAi::Personas::SpamDetector]
|
|
||||||
end
|
|
||||||
|
|
||||||
def custom_instructions
|
def custom_instructions
|
||||||
settings&.custom_instructions
|
settings&.custom_instructions
|
||||||
end
|
end
|
||||||
@@ -35,12 +28,6 @@ class AiSpamSerializer < ApplicationSerializer
|
|||||||
.map { |hash| { id: hash[:value], name: hash[:name] } }
|
.map { |hash| { id: hash[:value], name: hash[:name] } }
|
||||||
end
|
end
|
||||||
|
|
||||||
def available_personas
|
|
||||||
DiscourseAi::Configuration::PersonaEnumerator.values.map do |h|
|
|
||||||
{ id: h[:value], name: h[:name] }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def flagging_username
|
def flagging_username
|
||||||
object[:flagging_username]
|
object[:flagging_username]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DiscourseAi
|
|
||||||
class PersonaExporter
|
|
||||||
def initialize(persona:)
|
|
||||||
raise ArgumentError, "Invalid persona provided" if !persona.is_a?(AiPersona)
|
|
||||||
@persona = persona
|
|
||||||
end
|
|
||||||
|
|
||||||
def export
|
|
||||||
serialized_custom_tools = serialize_tools(@persona)
|
|
||||||
serialize_persona(@persona, serialized_custom_tools)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def serialize_tools(ai_persona)
|
|
||||||
custom_tool_ids =
|
|
||||||
(ai_persona.tools || []).filter_map do |tool_config|
|
|
||||||
# A tool config is an array like: ["custom-ID", {options}, force_flag]
|
|
||||||
if tool_config.is_a?(Array) && tool_config[0].to_s.start_with?("custom-")
|
|
||||||
tool_config[0].split("-", 2).last.to_i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return [] if custom_tool_ids.empty?
|
|
||||||
|
|
||||||
tools = AiTool.where(id: custom_tool_ids)
|
|
||||||
tools.map do |tool|
|
|
||||||
{
|
|
||||||
identifier: tool.tool_name, # Use tool_name for portability
|
|
||||||
name: tool.name,
|
|
||||||
description: tool.description,
|
|
||||||
tool_name: tool.tool_name,
|
|
||||||
parameters: tool.parameters,
|
|
||||||
summary: tool.summary,
|
|
||||||
script: tool.script,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def serialize_persona(ai_persona, serialized_custom_tools)
|
|
||||||
export_data = {
|
|
||||||
meta: {
|
|
||||||
version: "1.0",
|
|
||||||
exported_at: Time.zone.now.iso8601,
|
|
||||||
},
|
|
||||||
persona: {
|
|
||||||
name: ai_persona.name,
|
|
||||||
description: ai_persona.description,
|
|
||||||
system_prompt: ai_persona.system_prompt,
|
|
||||||
examples: ai_persona.examples,
|
|
||||||
temperature: ai_persona.temperature,
|
|
||||||
top_p: ai_persona.top_p,
|
|
||||||
response_format: ai_persona.response_format,
|
|
||||||
tools: transform_tools_for_export(ai_persona.tools, serialized_custom_tools),
|
|
||||||
},
|
|
||||||
custom_tools: serialized_custom_tools,
|
|
||||||
}
|
|
||||||
|
|
||||||
JSON.pretty_generate(export_data)
|
|
||||||
end
|
|
||||||
|
|
||||||
def transform_tools_for_export(tools_config, _serialized_custom_tools)
|
|
||||||
return [] if tools_config.blank?
|
|
||||||
|
|
||||||
tools_config.map do |tool_config|
|
|
||||||
unless tool_config.is_a?(Array) && tool_config[0].to_s.start_with?("custom-")
|
|
||||||
next tool_config
|
|
||||||
end
|
|
||||||
|
|
||||||
tool_id = tool_config[0].split("-", 2).last.to_i
|
|
||||||
tool = AiTool.find_by(id: tool_id)
|
|
||||||
next tool_config unless tool
|
|
||||||
|
|
||||||
["custom-#{tool.tool_name}", tool_config[1], tool_config[2]]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module DiscourseAi
|
|
||||||
class PersonaImporter
|
|
||||||
class ImportError < StandardError
|
|
||||||
attr_reader :conflicts
|
|
||||||
|
|
||||||
def initialize(message, conflicts = {})
|
|
||||||
super(message)
|
|
||||||
@conflicts = conflicts
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(json:)
|
|
||||||
@data =
|
|
||||||
case json
|
|
||||||
when String
|
|
||||||
JSON.parse(json)
|
|
||||||
when Hash
|
|
||||||
json
|
|
||||||
else
|
|
||||||
raise ArgumentError, "Invalid JSON payload"
|
|
||||||
end
|
|
||||||
|
|
||||||
validate_payload!
|
|
||||||
end
|
|
||||||
|
|
||||||
def import!(overwrite: false)
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
check_conflicts! unless overwrite
|
|
||||||
|
|
||||||
tool_name_to_id = import_custom_tools(@data["custom_tools"] || [], overwrite: overwrite)
|
|
||||||
persona_data = @data["persona"]
|
|
||||||
|
|
||||||
existing_persona = AiPersona.find_by(name: persona_data["name"])
|
|
||||||
|
|
||||||
attrs = {
|
|
||||||
description: persona_data["description"],
|
|
||||||
system_prompt: persona_data["system_prompt"],
|
|
||||||
examples: persona_data["examples"],
|
|
||||||
temperature: persona_data["temperature"],
|
|
||||||
top_p: persona_data["top_p"],
|
|
||||||
response_format: persona_data["response_format"],
|
|
||||||
tools: transform_tools_for_import(persona_data["tools"], tool_name_to_id),
|
|
||||||
}
|
|
||||||
|
|
||||||
if existing_persona && overwrite
|
|
||||||
existing_persona.update!(**attrs)
|
|
||||||
existing_persona
|
|
||||||
else
|
|
||||||
attrs[:name] = persona_data["name"]
|
|
||||||
AiPersona.create!(**attrs)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def validate_payload!
|
|
||||||
unless @data.is_a?(Hash) && @data["persona"].is_a?(Hash)
|
|
||||||
raise ArgumentError, "Invalid persona export data"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_conflicts!
|
|
||||||
conflicts = {}
|
|
||||||
|
|
||||||
persona_name = @data["persona"]["name"]
|
|
||||||
conflicts[:persona] = persona_name if AiPersona.exists?(name: persona_name)
|
|
||||||
|
|
||||||
if @data["custom_tools"].present?
|
|
||||||
existing_tools = []
|
|
||||||
@data["custom_tools"].each do |tool_data|
|
|
||||||
tool_name = tool_data["tool_name"] || tool_data["identifier"]
|
|
||||||
existing_tools << tool_name if AiTool.exists?(tool_name: tool_name)
|
|
||||||
end
|
|
||||||
conflicts[:custom_tools] = existing_tools if existing_tools.any?
|
|
||||||
end
|
|
||||||
|
|
||||||
if conflicts.any?
|
|
||||||
message = build_conflict_message(conflicts)
|
|
||||||
raise ImportError.new(message, conflicts)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_conflict_message(conflicts)
|
|
||||||
messages = []
|
|
||||||
|
|
||||||
if conflicts[:persona]
|
|
||||||
messages << I18n.t("discourse_ai.errors.persona_already_exists", name: conflicts[:persona])
|
|
||||||
end
|
|
||||||
|
|
||||||
if conflicts[:custom_tools] && conflicts[:custom_tools].any?
|
|
||||||
tools_list = conflicts[:custom_tools].join(", ")
|
|
||||||
error =
|
|
||||||
I18n.t(
|
|
||||||
"discourse_ai.errors.custom_tool_exists",
|
|
||||||
names: tools_list,
|
|
||||||
count: conflicts[:custom_tools].size,
|
|
||||||
)
|
|
||||||
messages << error
|
|
||||||
end
|
|
||||||
|
|
||||||
messages.join("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
def import_custom_tools(custom_tools, overwrite:)
|
|
||||||
return {} if custom_tools.blank?
|
|
||||||
|
|
||||||
custom_tools.each_with_object({}) do |tool_data, map|
|
|
||||||
tool_name = tool_data["tool_name"] || tool_data["identifier"]
|
|
||||||
|
|
||||||
if overwrite
|
|
||||||
tool = AiTool.find_or_initialize_by(tool_name: tool_name)
|
|
||||||
else
|
|
||||||
tool = AiTool.new(tool_name: tool_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
tool.tap do |t|
|
|
||||||
t.name = tool_data["name"]
|
|
||||||
t.description = tool_data["description"]
|
|
||||||
t.parameters = tool_data["parameters"]
|
|
||||||
t.script = tool_data["script"]
|
|
||||||
t.summary = tool_data["summary"]
|
|
||||||
t.created_by ||= Discourse.system_user
|
|
||||||
t.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
map[tool.tool_name] = tool.id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def transform_tools_for_import(tools_config, tool_name_to_id)
|
|
||||||
return [] if tools_config.blank?
|
|
||||||
|
|
||||||
tools_config.map do |tool_config|
|
|
||||||
if tool_config.is_a?(Array) && tool_config[0].to_s.start_with?("custom-")
|
|
||||||
tool_name = tool_config[0].split("-", 2).last
|
|
||||||
tool_id = tool_name_to_id[tool_name] || AiTool.find_by(tool_name: tool_name)&.id
|
|
||||||
raise ArgumentError, "Custom tool '#{tool_name}' not found" unless tool_id
|
|
||||||
|
|
||||||
["custom-#{tool_id}", tool_config[1], tool_config[2]]
|
|
||||||
else
|
|
||||||
tool_config
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -20,7 +20,7 @@ export default class AiBotHeaderIcon extends Component {
|
|||||||
|
|
||||||
get bots() {
|
get bots() {
|
||||||
const availableBots = this.currentUser.ai_enabled_chat_bots
|
const availableBots = this.currentUser.ai_enabled_chat_bots
|
||||||
.filter((bot) => !bot.is_persona || bot.has_default_llm)
|
.filter((bot) => !bot.is_persosna)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
return availableBots ? availableBots.map((bot) => bot.model_name) : [];
|
return availableBots ? availableBots.map((bot) => bot.model_name) : [];
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user