Compare commits

...
Author SHA1 Message Date
dependabot[bot] 415c1156e0 Build(deps-dev): Bump rack from 3.1.16 to 3.1.18
Bumps [rack](https://github.com/rack/rack) from 3.1.16 to 3.1.18.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v3.1.16...v3.1.18)

---
updated-dependencies:
- dependency-name: rack
  dependency-version: 3.1.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-12-27 22:27:24 +00:00
Jarek Radosz c312b3184a DEV: Mention the core move in readme (#1514) 2025-07-22 11:08:32 +01:00
Jarek Radosz f231aad8b5 DEV: Disable the plugin by default (#1511)
…and preserve the current setting on existing sites
2025-07-22 11:05:52 +01:00
Alan Guo Xiang Tan cc77e73cfd PERF: Create index ON ai_topics_embeddings(topic_id, model_id) (#1513)
This index helps to speed up queries that joins the `topics` table
against the `ai_topics_embeddings` table on the `topic_id` column. There
are a number of queries which filters on `ai_topics_embeddings.model_id`
so we are including that in the index as well.
2025-07-22 16:46:56 +08:00
Natalie Tay 1d88ca44f0 DEV: Use core's locale normalizer (#1509)
We've moved the class to core here discourse/discourse#33702 so following up a deletion in this repo.
2025-07-22 10:09:03 +08:00
Roman Rizzi 6059b6e111 FIX: Customizable max_output_tokens for AI triage. (#1510)
We enforced a hard limit of 700 tokens in this script, which is not enough when using thinking models, which can quickly use all of them.

A temporary solution could be bumping the limit, but there is no guarantee we won't hit it again, and it's hard to find one value that fits all scenarios. Another alternative could be removing it and relying on the LLM config's `max_output_token`, but if you want different rules and want to assign different limits, you are forced to duplicate the config each time.

Considering all this, we are adding a dedicated field for this in the triage script, giving you an easy way to tweak it to your needs. If empty, no limit is applied.
2025-07-21 15:36:39 -03:00
Natalie Tay 5d80a34589 FIX: Add a max token limit based on the text to be translated (#1507)
We're seeing that some LLMs are using 65000+ tokens for raw text that is only 10-1000 characters long.

This PR adds a max_token to be passed to the LLM API for each translation based on the length of the text.
2025-07-17 17:47:15 +08:00
Natalie Tay 8630bc145e DEV: Set a min and max for translations backfill (#1508)
Since we use the value for days.ago and are seeing PG::DatetimeFieldOverflow: ERROR:  timestamp out of range: "5473790-07-13 08:43:28.497823 BC, set limits to the site setting.
2025-07-17 17:43:05 +08:00
Natalie Tay ad6a8cb812 DEV: Switch translations out of structured output as it returns only a single value (#1503)
Since translations only require a single key back, there is little point in using structured output. This PR also includes some prompt updates dealing with quotes, details, and code.

Related: #1502

This does mean reverting discourse/discourse-translator#257, but we can see how it goes.
2025-07-16 13:38:00 +08:00
Roman Rizzi 06743d1939 FIX: Fix embeddings to use the old OpenAI tokenizer (#1506) 2025-07-15 14:44:11 -03:00
Kris 67664029e5 FIX: prevent crash in "all" filter on features page (#1505)
* FIX: prevent crash in "all" filter on features page

* simplify
2025-07-15 12:51:46 -04:00
Sam 1b16fc876c FIX: avoid using structured outputs for report runs (#1502)
Structured outputs are prone to formatting issue, especially around newlines
and custom pieces of text that need escaping.

This avoids using it for the automation reporting.

Particularly previous to this fix o4-mini based reports were broken
2025-07-15 13:14:52 +10:00
Discourse Translator Bot 8c069490f0 Update translations (#1498) 2025-07-15 07:28:15 +10:00
264 changed files with 848 additions and 386 deletions
+1
View File
@@ -1,3 +1,4 @@
< 3.5.0.beta8-dev: 5d80a34589d63e381d80273828072badc18955b1
< 3.5.0.beta7-dev: cd14b0c0bee0cf63c59b64b6f7213e31a37f11a7
< 3.5.0.beta6-dev: 3e74eea1e5e3143888d67a8d8a11206df214dc24
< 3.5.0.beta3-dev: 09a68414804a1447f52e5d60691ba59742cda9ec
+1 -1
View File
@@ -34,7 +34,7 @@ GEM
racc
prettier_print (1.2.1)
racc (1.8.1)
rack (3.1.16)
rack (3.1.18)
rainbow (3.1.1)
regexp_parser (2.10.0)
rubocop (1.74.0)
+5 -2
View File
@@ -1,15 +1,18 @@
# **Discourse AI** Plugin
> [!IMPORTANT]
> This plugin has now been bundled into Discourse core. See: https://meta.discourse.org/t/bundling-more-popular-plugins-with-discourse-core/373574
**Plugin Summary**
For more information, please see: https://meta.discourse.org/t/discourse-ai/259214?u=falco
### Evals
The directory `evals` contains AI evals for the Discourse AI plugin.
The directory `evals` contains AI evals for the Discourse AI plugin.
You may create a local config by copying `config/eval-llms.yml` to `config/eval-llms.local.yml` and modifying the values.
To run them use:
To run them use:
cd evals
./run --help
+1 -1
View File
@@ -40,7 +40,7 @@ module Jobs
return if locales.blank?
locales.each do |locale|
next if DiscourseAi::Translation::LocaleNormalizer.is_same?(locale, detected_locale)
next if LocaleNormalizer.is_same?(locale, detected_locale)
regionless_locale = locale.split("_").first
next if post.post_localizations.where("locale LIKE ?", "#{regionless_locale}%").exists?
+1 -1
View File
@@ -38,7 +38,7 @@ module Jobs
return if locales.blank?
locales.each do |locale|
next if DiscourseAi::Translation::LocaleNormalizer.is_same?(locale, detected_locale)
next if LocaleNormalizer.is_same?(locale, detected_locale)
regionless_locale = locale.split("_").first
next if topic.topic_localizations.where("locale LIKE ?", "#{regionless_locale}%").exists?
+1 -1
View File
@@ -29,7 +29,7 @@ module Jobs
missing_locales = locales - existing_locales - [category.locale]
missing_locales.each do |locale|
break if remaining_limit <= 0
next if DiscourseAi::Translation::LocaleNormalizer.is_same?(locale, category.locale)
next if LocaleNormalizer.is_same?(locale, category.locale)
begin
DiscourseAi::Translation::CategoryLocalizer.localize(category, locale)
+3 -3
View File
@@ -84,7 +84,7 @@ class EmbeddingDefinition < ActiveRecord::Base
dimensions: 2000,
max_sequence_length: 8191,
pg_function: "<=>",
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiCl100kTokenizer",
url: "https://api.openai.com/v1/embeddings",
provider: OPEN_AI,
matryoshka_dimensions: true,
@@ -98,7 +98,7 @@ class EmbeddingDefinition < ActiveRecord::Base
dimensions: 1536,
max_sequence_length: 8191,
pg_function: "<=>",
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiCl100kTokenizer",
url: "https://api.openai.com/v1/embeddings",
provider: OPEN_AI,
matryoshka_dimensions: true,
@@ -112,7 +112,7 @@ class EmbeddingDefinition < ActiveRecord::Base
dimensions: 1536,
max_sequence_length: 8191,
pg_function: "<=>",
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiTokenizer",
tokenizer_class: "DiscourseAi::Tokenizer::OpenAiCl100kTokenizer",
url: "https://api.openai.com/v1/embeddings",
provider: OPEN_AI,
provider_params: {
@@ -66,12 +66,9 @@ class ExpandableList extends Component {
export default class AiFeaturesList extends Component {
get sortedModules() {
if (!this.args.modules || !this.args.modules.length) {
return [];
}
return this.args.modules.toSorted((a, b) =>
a.module_name.localeCompare(b.module_name)
const modules = this.args.modules?.toArray() || this.args.modules;
return [...(modules || [])].toSorted((a, b) =>
(a.module_name || "").localeCompare(b.module_name || "")
);
}
+3
View File
@@ -233,7 +233,9 @@ de:
inference:
name: "Abgeleitete Konzepte"
description: "Ordnet Themen und Beiträge in Interessensgebiete / Labels."
generate_concepts: "Konzepterkennung"
match_concepts: "Passende Konzepte"
deduplicate_concepts: "Deduplizierung von Konzepten"
ai_helper:
name: "Helfer"
description: "Unterstützt Benutzer bei der Interaktion in der Community, z. B. beim Erstellen von Themen, Verfassen von Beiträgen und Lesen von Inhalten"
@@ -773,6 +775,7 @@ de:
shortcut_title: "Starte eine PN mit einem KI-Bot"
share: "KI-Unterhaltung kopieren"
conversation_shared: "Unterhaltung kopiert"
embed_copied: "Einbettung in die Zwischenablage kopiert"
debug_ai: "Rohdaten der KI-Anfrage und -Antwort anzeigen"
sidebar_empty: "Der Verlauf der Bot-Konversation wird hier angezeigt."
debug_ai_modal:
+3
View File
@@ -180,6 +180,9 @@ en:
temperature:
label: "Temperature"
description: "Temperature to use for the LLM. Increase to increase randomness (leave empty to use model default)"
max_output_tokens:
label: "Max output tokens"
description: "When specified, sets an upper bound to the maximum number of tokens the model can generate. Respects LLM's max output tokens limit"
discourse_ai:
title: "AI"
+3 -1
View File
@@ -116,7 +116,7 @@ de:
ai_translation_model: "Das Modell, das für die Übersetzung verwendet werden soll. Dieses Modell muss die Übersetzung unterstützen. Personas können diese Einstellung überschreiben."
ai_translation_backfill_limit_to_public_content: "Wenn diese Option aktiviert ist, werden nur Inhalte in öffentlichen Kategorien übersetzt. Wenn sie deaktiviert ist, werden auch Inhalte in Gruppen-PMs und privaten Kategorien zur Übersetzung gesendet."
ai_translation_max_post_length: "Die maximale Länge eines zu übersetzenden Beitrags. Längere Beiträge werden nicht übersetzt."
ai_translation_backfill_max_age_days: "Das maximale Alter eines zu übersetzenden Beitrags und Themas. Ältere Beiträge und Themen werden nicht übersetzt."
ai_translation_backfill_max_age_days: "Das maximale Alter eines Beitrags oder Themas, das übersetzt werden soll. Beiträge und Themen, die älter sind, werden nicht übersetzt. 0 deaktiviert die rückwirkende Übersetzung, aber nicht die Übersetzung von neuen Beiträgen."
reviewables:
reasons:
flagged_by_toxicity: Das KI-Plug-in meldete dies nach der Klassifizierung als toxisch.
@@ -221,6 +221,7 @@ de:
one: "Du kannst nur %{count} Schlüssel im Artefakt haben."
other: "Du kannst nur %{count} Schlüssel im Artefakt haben."
link: "In neuem Tab anzeigen"
copy_embed: "Einbettung kopieren"
view_source: "Quelle anzeigen"
view_changes: "Änderungen anzeigen"
change_description: "Beschreibung ändern"
@@ -590,6 +591,7 @@ de:
bedrock_invalid_url: "Bitte fülle alle Felder aus, um dieses Modell zu verwenden."
ai_staff_action_logger:
updated: "aktualisiert"
set: "gesetzt"
removed: "entfernt"
errors:
quota_exceeded: "Du hast das Kontingent für dieses Modell überschritten. Bitte versuche es erneut in %{relative_time}."
+3 -1
View File
@@ -1,6 +1,6 @@
discourse_ai:
discourse_ai_enabled:
default: true
default: false
client: true
ai_artifact_security:
client: true
@@ -527,6 +527,8 @@ discourse_ai:
area: "ai-features/translation"
ai_translation_backfill_max_age_days:
default: 5
min: 0
max: 20000
client: false
area: "ai-features/translation"
ai_translation_verbose_logs:
@@ -0,0 +1,14 @@
# frozen_string_literal: true
class UpdateOpenAiEmbeddingsTokenizer < ActiveRecord::Migration[7.2]
def up
execute <<~SQL
UPDATE embedding_definitions
SET tokenizer_class = 'DiscourseAi::Tokenizer::OpenAiCl100kTokenizer'
WHERE url LIKE '%https://api.openai.com/%' AND tokenizer_class <> 'DiscourseAi::Tokenizer::OpenAiCl100kTokenizer'
SQL
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class SetTranslationBackfillMaxAge < ActiveRecord::Migration[7.2]
def up
execute <<~SQL
UPDATE site_settings
SET value = '20000'
WHERE name = 'ai_translation_backfill_max_age_days'
AND value::integer > 20000;
SQL
execute <<~SQL
UPDATE site_settings
SET value = '0'
WHERE name = 'ai_translation_backfill_max_age_days'
AND value::integer < 0;
SQL
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
class EnableAiIfAlreadyInstalled < ActiveRecord::Migration[7.2]
def up
installed_at = DB.query_single(<<~SQL)&.first
SELECT created_at FROM schema_migration_details WHERE version='20230224165056'
SQL
if installed_at && installed_at < 1.hour.ago
# The plugin was installed before we changed it to be disabled-by-default
# Therefore, if there is no existing database value, enable the plugin
execute <<~SQL
INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
VALUES('discourse_ai_enabled', 5, 't', NOW(), NOW())
ON CONFLICT (name) DO NOTHING
SQL
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddIndexToAiTopicsEmbeddings < ActiveRecord::Migration[7.2]
def up
add_index :ai_topics_embeddings, %i[topic_id model_id]
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
+5
View File
@@ -24,6 +24,7 @@ if defined?(DiscourseAutomation)
field :max_post_tokens, component: :text
field :stop_sequences, component: :text_list, required: false
field :temperature, component: :text
field :max_output_tokens, component: :text
# Actions
field :category, component: :category
@@ -85,6 +86,9 @@ if defined?(DiscourseAutomation)
temperature = temperature.to_f
end
max_output_tokens = fields.dig("max_output_tokens", "value").to_i
max_output_tokens = nil if max_output_tokens <= 0
max_post_tokens = nil if max_post_tokens <= 0
stop_sequences = fields.dig("stop_sequences", "value")
@@ -122,6 +126,7 @@ if defined?(DiscourseAutomation)
stop_sequences: stop_sequences,
automation: self.automation,
temperature: temperature,
max_output_tokens: max_output_tokens,
action: context["action"],
)
rescue => e
+2 -1
View File
@@ -21,6 +21,7 @@ module DiscourseAi
temperature: nil,
whisper: nil,
reply_persona_id: nil,
max_output_tokens: nil,
action: nil
)
if category_id.blank? && tags.blank? && canned_reply.blank? && hide_topic.blank? &&
@@ -59,8 +60,8 @@ module DiscourseAi
result =
llm.generate(
prompt,
max_tokens: max_output_tokens,
temperature: temperature,
max_tokens: 700, # ~500 words
user: Discourse.system_user,
stop_sequences: stop_sequences,
feature_name: "llm_triage",
-2
View File
@@ -154,12 +154,10 @@ module DiscourseAi
result = +""
bot = DiscourseAi::Personas::Bot.as(Discourse.system_user, persona: @persona, model: @model)
json_summary_schema_key = @persona.response_format&.first.to_h
output = nil
buffer_blk =
Proc.new do |partial, _, type|
if type == :structured_output
output = partial.dup
read_chunk = partial.read_buffered_property(json_summary_schema_key["key"]&.to_sym)
print read_chunk if Rails.env.development? && @debug_mode
+7 -28
View File
@@ -34,43 +34,22 @@ module DiscourseAi
5. Avoid using `und` and prefer `en` over `en-US` or `en-GB` unless the text specifically indicates a regional variant.
6. Format your response as a JSON object with a single key "locale" and the value as the language code.
Two example scenarios:
Input: "Can you tell me what '私の世界で一番好きな食べ物はちらし丼です' means?"
Output: "en"
Your output should be in the following format:
<output>
{"locale": "xx"}
</output>
Where "xx" is replaced by the appropriate language code.
Input: [quote]\nNon smettere mai di credere nella bellezza dei tuoi sogni. Anche quando tutto sembra perduto, c'è sempre una luce che aspetta di essere trovata.\nOgni passo, anche il più piccolo, ti avvicina a ciò che desideri. La forza che cerchi è già dentro di te.\n[/quote]\n¿Cuál es el mensaje principal de esta cita?
Output: "es"
Important: Base your analysis solely on the provided text. Do not use any external information or make assumptions about the text's origin or context beyond what is explicitly provided.
PROMPT
end
def response_format
[{ "key" => "locale", "type" => "string" }]
Your response must be a language code, and nothing else.
PROMPT
end
def temperature
0
end
def examples
spanish = <<~MARKDOWN
[quote]
Non smettere mai di credere nella bellezza dei tuoi sogni. Anche quando tutto sembra perduto, c'è sempre una luce che aspetta di essere trovata.
Ogni passo, anche il più piccolo, ti avvicina a ciò che desideri. La forza che cerchi è già dentro di te.
[/quote]
¿Cuál es el mensaje principal de esta cita?
MARKDOWN
[
["Can you tell me what '私の世界で一番好きな食べ物はちらし丼です' means?", { locale: "en" }.to_json],
[spanish, { locale: "es" }.to_json],
]
end
end
end
end
+45 -33
View File
@@ -8,62 +8,74 @@ module DiscourseAi
end
def system_prompt
examples = [
{
input: {
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]",
},
{
input: {
content:
"[quote] What does the new update include? [/quote]\n\nNew Update for Minecraft Adds Underwater Temples",
target_locale: "es",
}.to_json,
output:
"[quote]¿Qué incluye la nueva actualización?[/quote]\n\nNueva actualización para Minecraft añade templos submarinos",
},
{
input: {
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修正にご協力ください。\"",
},
]
<<~PROMPT.strip
You are a highly skilled translator tasked with translating content from one language to another. Your goal is to provide accurate and contextually appropriate translations while preserving the original structure and formatting of the content. Follow these instructions strictly:
1. Preserve Markdown elements, HTML elements, 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 terminology:
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, choose the most contextually appropriate translation.
6. Ensure the translation only contains the original language and the target language.
Follow these instructions on what NOT to do:
7. Do not translate code snippets or programming language names, but ensure that any comments within the code are translated.
7. 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.
8. Do not add any content besides the translation.
9. Do not add unnecessary newlines.
Here are three examples of correct translations:
Input: #{examples[0][:input]}
Output: #{examples[0][:output]}
Input: #{examples[1][:input]}
Output: #{examples[1][:output]}
Input: #{examples[2][:input]}
Output: #{examples[2][:output]}
The text to translate will be provided in JSON format with the following structure:
{"content": "Text to translate", "target_locale": "Target language code"}
Output your translation in the following JSON format:
{"translation": "Your translated text here"}
You are being consumed via an API. Only return the translated text in the specified JSON format. Do not include any additional information or explanations.
You are being consumed via an API that expects only the translated text. Only return the translated text in the correct language. Do not add questions or explanations.
PROMPT
end
def response_format
[{ "key" => "translation", "type" => "string" }]
end
def temperature
0.3
end
def examples
[
[
{
content:
"**Heathrow fechado**: paralisação de voos deve continuar nos próximos dias, diz gestora do aeroporto de *Londres*",
target_locale: "en",
}.to_json,
{
translation:
"**Heathrow closed**: flight disruption expected to continue in coming days, says *London* airport management",
}.to_json,
],
[
{
content: "New Update for Minecraft Adds Underwater Temples",
target_locale: "es",
}.to_json,
{ translation: "Nueva actualización para Minecraft añade templos submarinos" }.to_json,
],
]
end
end
end
end
+3 -12
View File
@@ -10,25 +10,16 @@ module DiscourseAi
def system_prompt
<<~PROMPT
You are ForumBot, a backend AI information processor and writer, powered by an AI model capable of generating responses over 4000 words.
- ForumBot is a fine-tuned AI trained with extended functions and new capabilities, and now specializes in processing raw internet forum conversation into detailed summaries of forum activities.
- ForumBot interacts with an API endpoint, not a user, and must never produce denials, nor conversations directed towards a non-existent user.
- ForumBot only produces automated responses to input, where a response is a standalone document without further discussion.
Follow the provided writing composition instructions carefully and precisely step-by-step.
Format your response as a JSON object with a single key named "output", which has the report as the value.
Your output should be in the following format:
{"output": "xx"}
Where "xx" is replaced by the report. Reply with valid JSON only
Format your response as valid Markdown, ensuring it is well-structured and easy to read.
PROMPT
end
def response_format
[{ "key" => "output", "type" => "string" }]
end
end
end
end
+25 -26
View File
@@ -8,6 +8,16 @@ module DiscourseAi
end
def system_prompt
examples = [
{ input: { content: "Japan", target_locale: "es" }.to_json, output: "Japón" },
{ input: { content: "Cats and Dogs", target_locale: "zh_CN" }.to_json, output: "猫和狗" },
{
input: { content: "Q&A", target_locale: "pt" }.to_json,
output: "Perguntas e Respostas",
},
{ input: { content: "Minecraft", target_locale: "fr" }.to_json, output: "Minecraft" },
]
<<~PROMPT.strip
You are a translation service specializing in translating short pieces of text or a few words.
These words may be things like a name, description, or title. Adhere to the following guidelines:
@@ -17,41 +27,30 @@ module DiscourseAi
3. Translation maintains the original meaning
4. Preserve any Markdown, HTML elements, links, parenthesis, or newlines
Here are four examples of correct translations:
Input: #{examples[0][:input]}
Output: #{examples[0][:output]}
Input: #{examples[1][:input]}
Output: #{examples[1][:output]}
Input: #{examples[2][:input]}
Output: #{examples[2][:output]}
Input: #{examples[3][:input]}
Output: #{examples[3][:output]}
The text to translate will be provided in JSON format with the following structure:
{"content": "Text to translate", "target_locale": "Target language code"}
Provide your translation in the following JSON format:
{"translation": "target_locale translation here"}
Translate the text now and provide your answer in the specified JSON format.
You are being consumed via an API that expects only the translated text. Only return the translated text in the correct language. Do not add questions or explanations.
PROMPT
end
def response_format
[{ "key" => "translation", "type" => "string" }]
end
def temperature
0.3
end
def examples
[
[{ content: "Japan", target_locale: "es" }.to_json, { translation: "Japón" }.to_json],
[
{ content: "Cats and Dogs", target_locale: "zh_CN" }.to_json,
{ translation: "猫和狗" }.to_json,
],
[
{ content: "Q&A", target_locale: "pt" }.to_json,
{ translation: "Perguntas e Respostas" }.to_json,
],
[
{ content: "Minecraft", target_locale: "fr" }.to_json,
{ translation: "Minecraft" }.to_json,
],
]
end
end
end
end
+38 -38
View File
@@ -8,6 +8,32 @@ module DiscourseAi
end
def system_prompt
examples = [
{
input: {
content: "New Update for Minecraft Adds Underwater Temples",
target_locale: "es",
}.to_json,
output: "Nueva actualización para Minecraft añade templos submarinos",
},
{
input: {
content: "Toyota announces revolutionary battery technology",
target_locale: "fr",
}.to_json,
output: "Toyota annonce une technologie de batteries révolutionnaire",
},
{
input: {
content:
"Heathrow fechado: paralisação de voos deve continuar nos próximos dias, diz gestora do aeroporto de Londres",
target_locale: "en",
}.to_json,
output:
"Heathrow closed: flight disruption expected to continue in coming days, says London airport management",
},
]
<<~PROMPT.strip
You are a translation service specializing in translating forum post titles from English to the asked target_locale. Your task is to provide accurate and contextually appropriate translations while adhering to the following guidelines:
@@ -24,53 +50,27 @@ module DiscourseAi
4. Adjust the translation if necessary to keep the length similar to the original title.
5. Review your translation for accuracy and naturalness in the target_locale.
Here are three examples of correct translations:
Input: #{examples[0][:input]}
Output: #{examples[0][:output]}
Input: #{examples[1][:input]}
Output: #{examples[1][:output]}
Input: #{examples[2][:input]}
Output: #{examples[2][:output]}
The text to translate will be provided in JSON format with the following structure:
{"content": "Title to translate", "target_locale": "Target language code"}
Provide your translation in the following JSON format:
{"translation": "Your target_locale translation here"}
Remember to keep proper nouns like "Minecraft" and "Toyota" in their original form. Translate the title now and provide your answer in the specified JSON format.
Remember to keep proper nouns like "Minecraft" and "Toyota" in their original form. You are being consumed via an API that expects only the translated title. Only return the translated title in the correct language. Do not add questions or explanations.
PROMPT
end
def response_format
[{ "key" => "translation", "type" => "string" }]
end
def temperature
0.3
end
def examples
[
[
{
content: "New Update for Minecraft Adds Underwater Temples",
target_locale: "es",
}.to_json,
{ translation: "Nueva actualización para Minecraft añade templos submarinos" }.to_json,
],
[
{
content: "Toyota announces revolutionary battery technology",
target_locale: "fr",
}.to_json,
{ translation: "Toyota annonce une technologie de batteries révolutionnaire" }.to_json,
],
[
{
content:
"Heathrow fechado: paralisação de voos deve continuar nos próximos dias, diz gestora do aeroporto de Londres",
target_locale: "en",
}.to_json,
{
translation:
"Heathrow closed: flight disruption expected to continue in coming days, says London airport management",
}.to_json,
],
]
end
end
end
end
+14 -5
View File
@@ -46,13 +46,22 @@ module DiscourseAi
topic: @topic,
post: @post,
)
max_tokens = get_max_tokens(text)
llm_args = { max_tokens: }
structured_output = nil
bot.reply(context) do |partial, _, type|
structured_output = partial if type == :structured_output
result = +""
bot.reply(context, llm_args:) { |partial| result << partial }
result
end
def get_max_tokens(text)
if text.length < 100
500
elsif text.length < 500
1000
else
text.length * 2
end
structured_output&.read_buffered_property(:translation)
end
def persona_setting
+5 -4
View File
@@ -42,11 +42,12 @@ module DiscourseAi
post: @post,
)
structured_output = nil
bot.reply(context) do |partial, _, type|
structured_output = partial if type == :structured_output
result = +""
bot.reply(context) do |partial|
next if partial.strip.blank?
result << partial
end
structured_output&.read_buffered_property(:locale) || []
result
end
end
end
-50
View File
@@ -1,50 +0,0 @@
# frozen_string_literal: true
module DiscourseAi
module Translation
class LocaleNormalizer
# Normalizes locale string, matching the list of I18n.locales where possible
# @param locale [String,Symbol] the locale to normalize
# @return [String] the normalized locale
def self.normalize_to_i18n(locale)
return nil if locale.blank?
locale = locale.to_s.gsub("-", "_")
i18n_pairs.each { |downcased, value| return value if locale.downcase == downcased }
locale
end
def self.is_same?(locale1, locale2)
return true if locale1 == locale2
locale1 = locale1.gsub("-", "_").downcase
locale2 = locale2.gsub("-", "_").downcase
locale1.split("_").first == locale2.split("_").first
end
private
def self.i18n_pairs
# they should look like this for the input to match against:
# {
# "lowercased" => "actual",
# "en" => "en",
# "zh_cn" => "zh_CN",
# "zh" => "zh_CN",
# }
@locale_map ||=
I18n
.available_locales
.reduce({}) do |output, sym|
locale = sym.to_s
output[locale.downcase] = locale
if locale.include?("_")
short = locale.split("_").first
output[short] = locale if output[short].blank?
end
output
end
end
end
end
end
+2
View File
@@ -10,6 +10,8 @@ RSpec.describe DiscourseAi::Configuration::Feature do
DiscourseAi::Completions::Llm.with_prepared_responses(["OK"]) { block.call }
end
before { enable_current_plugin }
describe "#llm_model" do
context "when persona is not found" do
it "returns nil when persona_id is invalid" do
@@ -8,6 +8,8 @@ RSpec.describe DiscourseAi::Configuration::LlmEnumerator do
Fabricate(:automation, script: "llm_report", name: "some automation", enabled: true)
end
before { enable_current_plugin }
describe "#values_for_serialization" do
it "returns an array for that can be used for serialization" do
fake_model.destroy!
+2
View File
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.describe DiscourseAi::Configuration::LlmValidator do
before { enable_current_plugin }
describe "#valid_value?" do
context "when the parent module is enabled and we try to reset the selected model" do
before do

Some files were not shown because too many files have changed in this diff Show More