mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 04:03:45 +08:00
Two production bugs in the post raw translator, both surfacing on
German:
1. BBCode attribute substitution [quote="user, post:N, topic:M"] was
being rewritten as Beitrag:/Thema:, breaking the quote link
2. mid translation truncation due to `"` as qwen uses structured output
({"output": "..."}) and wrote a plain `"` for the closing quote in
`„flach"`, terminating the JSON output string.
https://github.com/discourse/discourse-ai-evals/pull/17
126 lines
4.1 KiB
Ruby
Vendored
126 lines
4.1 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
require_relative "base"
|
|
|
|
module DiscourseAi
|
|
module Evals
|
|
module Runners
|
|
class Translation < Base
|
|
OPERATIONS = {
|
|
"locale_detector" => DiscourseAi::Agents::LocaleDetector,
|
|
"post_raw_translator" => DiscourseAi::Agents::PostRawTranslator,
|
|
"topic_title_translator" => DiscourseAi::Agents::TopicTitleTranslator,
|
|
"short_text_translator" => DiscourseAi::Agents::ShortTextTranslator,
|
|
}.freeze
|
|
|
|
def self.can_handle?(feature_name)
|
|
feature_name&.start_with?("translation:")
|
|
end
|
|
|
|
def initialize(feature_name, agent_prompt_override = nil)
|
|
super
|
|
@operation = feature_name
|
|
if !OPERATIONS.key?(@operation)
|
|
raise ArgumentError, "Unsupported translation feature '#{feature_name}'"
|
|
end
|
|
end
|
|
|
|
def run(eval_case, llm, execution_context:)
|
|
raw_args = eval_case.args
|
|
if raw_args.present? && !raw_args.is_a?(Hash)
|
|
raise ArgumentError, "Translation evals expect args defined as a Hash"
|
|
end
|
|
|
|
args = (raw_args || {}).deep_symbolize_keys
|
|
case_defs = args.delete(:cases)
|
|
|
|
if case_defs.present?
|
|
case_defs.map do |case_args|
|
|
normalized_args = args.merge(case_args.symbolize_keys)
|
|
run_case(normalized_args, llm, execution_context:)
|
|
end
|
|
else
|
|
run_case(args, llm, execution_context:)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :operation
|
|
|
|
def run_case(case_args, llm, execution_context:)
|
|
content = extract_content(case_args)
|
|
raise ArgumentError, "Translation evals require :input or :conversation" if content.blank?
|
|
|
|
output =
|
|
if operation == "locale_detector"
|
|
detect_locale(content, llm, execution_context:)
|
|
else
|
|
target_locale =
|
|
case_args[:target_locale].presence ||
|
|
raise(ArgumentError, "Translation evals require :target_locale")
|
|
translate_content(content, target_locale, llm, execution_context:)
|
|
end
|
|
|
|
build_payload(case_args, content, output)
|
|
end
|
|
|
|
def detect_locale(content, llm, execution_context:)
|
|
agent = agent_for_operation
|
|
context =
|
|
DiscourseAi::Agents::BotContext.new(
|
|
user: system_user,
|
|
skip_show_thinking: true,
|
|
feature_name: "translation/#{operation}",
|
|
messages: [{ type: :user, content: content }],
|
|
)
|
|
|
|
bot = DiscourseAi::Agents::Bot.as(system_user, agent: agent, model: llm)
|
|
capture_plain_response(bot, context, execution_context:).strip
|
|
end
|
|
|
|
def translate_content(content, target_locale, llm, execution_context:)
|
|
agent = agent_for_operation
|
|
payload = { content:, target_locale: }.to_json
|
|
context =
|
|
DiscourseAi::Agents::BotContext.new(
|
|
user: system_user,
|
|
skip_show_thinking: true,
|
|
feature_name: "translation/#{operation}",
|
|
messages: [{ type: :user, content: payload }],
|
|
)
|
|
|
|
bot = DiscourseAi::Agents::Bot.as(system_user, agent: agent, model: llm)
|
|
capture_structured_response(bot, context, schema_key: :output, execution_context:).strip
|
|
end
|
|
|
|
def build_payload(case_args, content, output)
|
|
metadata = {
|
|
message: content,
|
|
target_locale: case_args[:target_locale],
|
|
expected_locale: case_args[:expected_locale],
|
|
}.compact
|
|
|
|
wrap_result(output, metadata)
|
|
end
|
|
|
|
def extract_content(case_args)
|
|
if case_args[:conversation].present?
|
|
Array(case_args[:conversation]).map(&:to_s).join("\n\n")
|
|
else
|
|
(case_args[:input] || case_args[:message]).to_s
|
|
end
|
|
end
|
|
|
|
def system_user
|
|
@user ||= Discourse.system_user
|
|
end
|
|
|
|
def agent_for_operation
|
|
agent_class = OPERATIONS.fetch(operation)
|
|
resolve_agent(agent_class: agent_class)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|