0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-07 13:19:19 +08:00
discourse/plugins/discourse-ai/lib/agents/post_raw_translator.rb
Rafael dos Santos Silva 491e246166
FIX: Steer translation agents away from ASCII quotes in structured output (#42131)
## Problem

A topic title translation was saved truncated: `Permanently Delete does
not show (even after 5 minutes)` → `"Endgültig löschen`.

The raw completion showed the model emitted `{"output": "\"Endgültig
löschen" }` with `finish_reason: stop` at 12 tokens — complete, valid
JSON that our parsing pipeline reproduced faithfully. The truncation
happened at generation time: the model opened a quoted UI label with an
escaped ASCII quote (`\"`), then sampled a bare `"` where it meant `\"`.
Under strict `json_schema` guided decoding that bare quote terminates
the JSON string value, and with `additionalProperties: false` the
grammar then only allows `}`, forcing the generation to end
mid-sentence.

Native typographic quotation marks (`„…“`, `«…»`, `「…」`) are ordinary
characters inside a JSON string — no escaping, no ambiguity — so they
can't trigger this failure mode. A parallel successful run that happened
to use `„…“` completed fine.

## Fix

`PostRawTranslator` already has a rule instructing native quotation
marks. This adds the same rule to `TopicTitleTranslator` and
`ShortTextTranslator`:

- **TopicTitleTranslator**: new guideline 6, plus a few-shot example
using the exact title from the incident, showing German `„…“` around the
UI label.
- **ShortTextTranslator**: new guideline 5, plus a `Click "Reply"` →
`Cliquez sur « Répondre »` example.
- **PostRawTranslator**: the existing rule's German example closed with
an ASCII `"` instead of `“` — fixed.

Seeded system agents re-sync `system_prompt` and `examples` from the
classes on migration, so sites pick this up automatically.
2026-07-29 18:36:34 -03:00

97 lines
5.6 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseAi
module Agents
class PostRawTranslator < Agent
def self.default_enabled
false
end
def system_prompt
<<~PROMPT.strip
You are a friendly and very skilled human linguist and translator. Your goal is to produce translations that read naturally to native speakers, as if originally written in the target language indistinguishable from content written by a human. Follow these instructions strictly:
1. Preserve Markdown elements, HTML elements, BBCode tags and their attributes, or newlines. Text must be translated without altering the original formatting.
2. Maintain the original document structure including headings, lists, tables, code blocks, etc.
3. Preserve all links, images, and other media references without translation.
4. For technical and brand terminology:
- Provide the accepted target language term if it exists.
- If no equivalent exists, transliterate the term and include the original term in parentheses.
5. For ambiguous terms or phrases, do not translate word-for-word in isolation. Derive the intended meaning from the full context of the document before choosing a translation.
6. Ensure the translation only contains the original language and the target language.
7. Match the tone and register of the source text. If the source is informal and conversational, use informal address forms and a casual tone in the target language. Do not default to formal address (e.g. German Sie, French vous) unless the source text is itself formal.
8. For any quoted text, use the target language's native quotation marks — for example German „…“, French «…», Japanese 「…」 — rather than ASCII quotes.
Follow these instructions on what NOT to do:
9. Do not translate code snippets or programming language names, but ensure that any comments within the code are translated. Code can be represented in ``` or in single ` backticks or in <code> HTML tags.
10. Do not add any content besides the translation.
11. Do not add unnecessary newlines.
The text to translate will be provided in JSON format with the following structure:
{"content": "Text to translate", "target_locale": "Target language code"}
Format your response as a JSON object with a single key named "output", which has the translation as the value.
Your output should be in the following format:
{"output": "xx"}
Where "xx" is replaced by the translation.
reply with valid JSON only
PROMPT
end
def response_format
[{ "key" => "output", "type" => "string" }]
end
def examples
[
[
{
content:
"**Heathrow fechado**: Suspensão de voos deve continuar nos próximos dias, afirma gerente do aeroporto de Londres\n\n[details=Do site da BBC]\n\nA British Airways estimou que 85% de seus voos planejados seriam realizados no sábado, mas com atrasos em todos os voos. Às 7h GMT, a maioria das partidas havia ocorrido conforme o esperado, mas, das chegadas, nove dos primeiros 20 voos programados para aterrissar foram cancelados.\n\n[/details]",
target_locale: "en",
}.to_json,
{
output:
"**Heathrow Closed**: Flight Suspension Expected to Continue for the Coming Days, Says London Airport Manager\n\n[details=From the BBC website]\n\nBritish Airways estimated that 85% of its scheduled flights would operate on Saturday, but all flights were delayed. By 7:00 a.m. GMT, most departures had proceeded as expected, but of the arrivals, nine of the first 20 flights scheduled to land were canceled.\n\n[/details]",
}.to_json,
],
[
{
content:
"[quote=\"alice, post:3, topic:42\"]\nHow do I get started?\n[/quote]\n\nWe're so glad you're here! Want to run your own Minecraft server? Just click the Get Started button below — it's easy.",
target_locale: "de",
}.to_json,
{
output:
"[quote=\"alice, post:3, topic:42\"]\nWie fange ich an?\n[/quote]\n\nWir freuen uns sehr, dass du hier bist! Möchtest du deinen eigenen Minecraft-Server betreiben? Klick einfach unten auf den „Loslegen“-Button — es ist ganz einfach.",
}.to_json,
],
[
{
content:
"There has been an error in my update\n\n```ruby\napi_key = \"a quick brown fox\"\nfetch(\"https://api.example.com/data\", headers: { 'Authorization' => api_key })\n```\n\nPlease help me fix it.",
target_locale: "ja",
}.to_json,
{
output:
"アップデートでエラーが発生しました\n\n```ruby\napi_key = \"a quick brown fox\"\nfetch(\"https://api.example.com/data\", headers: { 'Authorization' => api_key })\n```\n\n修正にご協力ください。",
}.to_json,
],
[
{
content:
"He proposed a so-called clean architecture for the new service. But clean doesn't always mean simple.",
target_locale: "de",
}.to_json,
{
output:
"Er schlug eine sogenannte „saubere“ Architektur für den neuen Dienst vor. Doch „sauber“ bedeutet nicht immer einfach.",
}.to_json,
],
]
end
end
end
end