mistral_translator 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -0
- data/README.md +9 -2
- data/docs/api-reference/configuration.md +19 -2
- data/docs/api-reference/errors.md +15 -24
- data/docs/api-reference/methods.md +10 -4
- data/docs/getting-started.md +8 -2
- data/lib/mistral_translator/adapters.rb +73 -33
- data/lib/mistral_translator/client.rb +18 -19
- data/lib/mistral_translator/client_helpers.rb +41 -23
- data/lib/mistral_translator/configuration.rb +11 -1
- data/lib/mistral_translator/data/languages.json +912 -0
- data/lib/mistral_translator/helpers.rb +20 -16
- data/lib/mistral_translator/helpers_extensions.rb +1 -1
- data/lib/mistral_translator/locale_helper.rb +19 -31
- data/lib/mistral_translator/prompt.rb +48 -0
- data/lib/mistral_translator/prompt_builder.rb +40 -60
- data/lib/mistral_translator/prompt_helpers.rb +25 -12
- data/lib/mistral_translator/prompt_metadata_helpers.rb +16 -17
- data/lib/mistral_translator/proxy.rb +75 -0
- data/lib/mistral_translator/response_parser.rb +120 -140
- data/lib/mistral_translator/security.rb +16 -14
- data/lib/mistral_translator/summarizer.rb +37 -110
- data/lib/mistral_translator/translator.rb +33 -77
- data/lib/mistral_translator/translator_helpers.rb +30 -64
- data/lib/mistral_translator/version.rb +1 -1
- data/lib/mistral_translator.rb +1 -1
- metadata +4 -1
|
@@ -9,7 +9,6 @@ module MistralTranslator
|
|
|
9
9
|
include TranslatorHelpers::InputValidator
|
|
10
10
|
include TranslatorHelpers::RetryHandler
|
|
11
11
|
include TranslatorHelpers::LoggingHelper
|
|
12
|
-
include TranslatorHelpers::PromptHandler
|
|
13
12
|
include TranslatorHelpers::AnalysisHelper
|
|
14
13
|
include TranslatorHelpers::RequestHelper
|
|
15
14
|
include TranslatorHelpers::MultiTargetHelper
|
|
@@ -67,47 +66,49 @@ module MistralTranslator
|
|
|
67
66
|
# Traduction vers plusieurs langues avec support du batch
|
|
68
67
|
def translate_to_multiple(text, from:, to:, **options)
|
|
69
68
|
validate_translation_inputs!(text, from, to)
|
|
69
|
+
Security::BasicValidator.validate_text!(text)
|
|
70
70
|
|
|
71
71
|
source_locale = LocaleHelper.validate_locale!(from)
|
|
72
72
|
target_locales = Array(to).map { |locale| LocaleHelper.validate_locale!(locale) }
|
|
73
73
|
|
|
74
|
+
translation_options = { context: options[:context], glossary: options[:glossary] }
|
|
75
|
+
translation_options[:preserve_html] = true if options[:preserve_html]
|
|
76
|
+
|
|
74
77
|
if options[:use_batch] && target_locales.size > 3
|
|
75
|
-
translate_to_multiple_batch(text, source_locale, target_locales,
|
|
76
|
-
glossary: options[:glossary])
|
|
78
|
+
translate_to_multiple_batch(text, source_locale, target_locales, **translation_options)
|
|
77
79
|
else
|
|
78
|
-
translate_to_multiple_sequential(text, source_locale, target_locales,
|
|
79
|
-
glossary: options[:glossary])
|
|
80
|
+
translate_to_multiple_sequential(text, source_locale, target_locales, **translation_options)
|
|
80
81
|
end
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
# Traduction en lot (plusieurs textes vers une langue)
|
|
84
|
-
def translate_batch(texts, from:, to:, context: nil, glossary: nil)
|
|
85
|
+
def translate_batch(texts, from:, to:, context: nil, glossary: nil, preserve_html: false) # rubocop:disable Metrics/ParameterLists
|
|
85
86
|
validated_texts = Security::BasicValidator.validate_batch!(texts)
|
|
86
87
|
source_locale = LocaleHelper.validate_locale!(from)
|
|
87
88
|
target_locale = LocaleHelper.validate_locale!(to)
|
|
88
89
|
|
|
89
90
|
return handle_same_language_batch(validated_texts) if source_locale == target_locale
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
batch_config = { source_locale: source_locale, target_locale: target_locale, context: context,
|
|
93
|
+
glossary: glossary, preserve_html: preserve_html }
|
|
94
|
+
process_mixed_batch(validated_texts, batch_config)
|
|
92
95
|
end
|
|
93
96
|
|
|
94
97
|
# Auto-détection de la langue source avec support du contexte
|
|
95
|
-
def translate_auto(text, to:, context: nil, glossary: nil)
|
|
98
|
+
def translate_auto(text, to:, context: nil, glossary: nil, preserve_html: false)
|
|
99
|
+
validated_text = Security::BasicValidator.validate_text!(text)
|
|
100
|
+
return "" if validated_text.empty?
|
|
101
|
+
|
|
96
102
|
target_locale = LocaleHelper.validate_locale!(to)
|
|
97
103
|
|
|
98
|
-
# Premier appel pour détecter la langue
|
|
99
|
-
|
|
100
|
-
detection_response = @client.complete(detection_prompt)
|
|
104
|
+
# Premier appel pour détecter la langue ; un échec lève une erreur
|
|
105
|
+
detection_response = @client.complete(build_language_detection_prompt(validated_text), json: true)
|
|
101
106
|
detected_language = parse_language_detection(detection_response)
|
|
102
|
-
|
|
103
|
-
# Vérifier que la langue détectée est différente de la cible
|
|
104
|
-
if detected_language == target_locale
|
|
105
|
-
# Si même langue, retourner le texte original
|
|
106
|
-
return text
|
|
107
|
-
end
|
|
107
|
+
return validated_text if detected_language == target_locale
|
|
108
108
|
|
|
109
109
|
# Puis traduction normale avec contexte
|
|
110
|
-
translate(
|
|
110
|
+
translate(validated_text, from: detected_language, to: target_locale, context: context, glossary: glossary,
|
|
111
|
+
preserve_html: preserve_html)
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
private
|
|
@@ -118,10 +119,8 @@ module MistralTranslator
|
|
|
118
119
|
validated_texts.each_with_index.to_h { |text, index| [index, text] }
|
|
119
120
|
end
|
|
120
121
|
|
|
121
|
-
def process_mixed_batch(validated_texts,
|
|
122
|
+
def process_mixed_batch(validated_texts, batch_config)
|
|
122
123
|
results = {}
|
|
123
|
-
batch_config = { source_locale: source_locale, target_locale: target_locale, context: context,
|
|
124
|
-
glossary: glossary }
|
|
125
124
|
requests = build_batch_requests(validated_texts, batch_config, results)
|
|
126
125
|
|
|
127
126
|
process_batch_requests(requests, results) if requests.any?
|
|
@@ -146,7 +145,8 @@ module MistralTranslator
|
|
|
146
145
|
def create_batch_request(text, batch_config, index)
|
|
147
146
|
{
|
|
148
147
|
prompt: build_translation_prompt(text, batch_config[:source_locale], batch_config[:target_locale],
|
|
149
|
-
context: batch_config[:context], glossary: batch_config[:glossary]
|
|
148
|
+
context: batch_config[:context], glossary: batch_config[:glossary],
|
|
149
|
+
preserve_html: batch_config[:preserve_html]),
|
|
150
150
|
from: batch_config[:source_locale],
|
|
151
151
|
to: batch_config[:target_locale],
|
|
152
152
|
index: index,
|
|
@@ -171,6 +171,9 @@ module MistralTranslator
|
|
|
171
171
|
parsed_result[:translated] if parsed_result
|
|
172
172
|
rescue EmptyTranslationError
|
|
173
173
|
""
|
|
174
|
+
rescue InvalidResponseError
|
|
175
|
+
# Une réponse illisible fait échouer son élément, pas tout le lot
|
|
176
|
+
nil
|
|
174
177
|
end
|
|
175
178
|
|
|
176
179
|
def translate_with_retry(text, source_locale, target_locale, attempt = 0, **options)
|
|
@@ -184,9 +187,12 @@ module MistralTranslator
|
|
|
184
187
|
translate_with_retry(text, source_locale, target_locale, attempt + 1, **options)
|
|
185
188
|
end
|
|
186
189
|
rescue RateLimitError
|
|
190
|
+
# Le client a déjà relancé avec backoff : quelques tentatives de plus suffisent.
|
|
191
|
+
raise if attempt >= DEFAULT_RETRY_COUNT
|
|
192
|
+
|
|
187
193
|
log_rate_limit_hit(source_locale, target_locale)
|
|
188
194
|
sleep(DEFAULT_RETRY_DELAY)
|
|
189
|
-
|
|
195
|
+
translate_with_retry(text, source_locale, target_locale, attempt + 1, **options)
|
|
190
196
|
end
|
|
191
197
|
|
|
192
198
|
# Ces méthodes sont héritées via RequestHelper
|
|
@@ -194,60 +200,10 @@ module MistralTranslator
|
|
|
194
200
|
# Log via LoggingHelper
|
|
195
201
|
|
|
196
202
|
def build_translation_prompt(text, source_locale, target_locale, **options)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
context_present = options[:context] && !options[:context].to_s.strip.empty?
|
|
202
|
-
glossary_present =
|
|
203
|
-
(options[:glossary].is_a?(Hash) && options[:glossary].any?) ||
|
|
204
|
-
(options[:glossary].is_a?(String) && !options[:glossary].to_s.strip.empty?)
|
|
205
|
-
if context_present || glossary_present
|
|
206
|
-
enrich_prompt_with_context(base_prompt, options[:context], options[:glossary])
|
|
207
|
-
else
|
|
208
|
-
base_prompt
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def enrich_prompt_with_context(base_prompt, context, glossary)
|
|
213
|
-
enriched_parts = []
|
|
214
|
-
context_part = build_context_enrichment(context)
|
|
215
|
-
glossary_part = build_glossary_enrichment(glossary)
|
|
216
|
-
enriched_parts << context_part if context_part
|
|
217
|
-
enriched_parts << glossary_part if glossary_part
|
|
218
|
-
|
|
219
|
-
return base_prompt if enriched_parts.empty?
|
|
220
|
-
|
|
221
|
-
enriched_context = enriched_parts.join("\n")
|
|
222
|
-
base_prompt.sub("RÈGLES :", "#{enriched_context}\n\nRÈGLES :")
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def build_context_enrichment(context)
|
|
226
|
-
return nil if context.nil? || context.to_s.strip.empty?
|
|
227
|
-
|
|
228
|
-
"CONTEXTE : #{context}"
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def build_glossary_enrichment(glossary)
|
|
232
|
-
return nil if glossary.nil?
|
|
233
|
-
return build_glossary_hash(glossary) if glossary.is_a?(Hash)
|
|
234
|
-
return build_glossary_string(glossary) if glossary.is_a?(String)
|
|
235
|
-
|
|
236
|
-
nil
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def build_glossary_hash(glossary_hash)
|
|
240
|
-
return nil if glossary_hash.empty?
|
|
241
|
-
|
|
242
|
-
glossary_text = glossary_hash.map { |key, value| "#{key} → #{value}" }.join(", ")
|
|
243
|
-
"GLOSSAIRE (à respecter strictement) : #{glossary_text}"
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def build_glossary_string(glossary_string)
|
|
247
|
-
value = glossary_string.to_s.strip
|
|
248
|
-
return nil if value.empty?
|
|
249
|
-
|
|
250
|
-
"GLOSSAIRE : #{value}"
|
|
203
|
+
PromptBuilder.translation_prompt(text, source_locale, target_locale,
|
|
204
|
+
context: options[:context],
|
|
205
|
+
glossary: options[:glossary],
|
|
206
|
+
preserve_html: options.fetch(:preserve_html, false))
|
|
251
207
|
end
|
|
252
208
|
end
|
|
253
209
|
end
|
|
@@ -91,34 +91,6 @@ module MistralTranslator
|
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
# Helper pour la gestion des prompts
|
|
95
|
-
module PromptHandler
|
|
96
|
-
def enrich_prompt_with_context(base_prompt, context, glossary)
|
|
97
|
-
context_section = build_context_section(context, glossary)
|
|
98
|
-
base_prompt + context_section
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def build_context_section(context, glossary)
|
|
102
|
-
context_section = ""
|
|
103
|
-
context_section += add_context_text(context) if context && !context.to_s.strip.empty?
|
|
104
|
-
context_section += add_glossary_text(glossary) if glossary && !glossary.to_s.strip.empty?
|
|
105
|
-
context_section
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def add_context_text(context)
|
|
109
|
-
"\nCONTEXTE : #{context}\n"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def add_glossary_text(glossary)
|
|
113
|
-
if glossary.is_a?(Hash) && glossary.any?
|
|
114
|
-
glossary_text = glossary.map { |key, value| "#{key} → #{value}" }.join(", ")
|
|
115
|
-
"\nGLOSSAIRE (à respecter strictement) : #{glossary_text}\n"
|
|
116
|
-
else
|
|
117
|
-
"\nGLOSSAIRE : #{glossary}\n"
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
94
|
# Helper pour l'analyse et la détection de langue
|
|
123
95
|
module AnalysisHelper
|
|
124
96
|
def calculate_confidence_score(original, translated, from_locale, to_locale)
|
|
@@ -152,16 +124,17 @@ module MistralTranslator
|
|
|
152
124
|
PromptBuilder.language_detection_prompt(text)
|
|
153
125
|
end
|
|
154
126
|
|
|
127
|
+
# Fail-closed : une détection illisible ou une langue non supportée lève une erreur,
|
|
128
|
+
# au lieu de supposer une langue et de renvoyer le texte non traduit.
|
|
155
129
|
def parse_language_detection(response)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
detected = data.dig("metadata", "detected_language")
|
|
130
|
+
data = JSON.parse(response.to_s)
|
|
131
|
+
metadata = data["metadata"] if data.is_a?(Hash)
|
|
132
|
+
detected = LocaleHelper.normalize_locale(metadata["detected_language"]) if metadata.is_a?(Hash)
|
|
133
|
+
return detected if detected && LocaleHelper.locale_supported?(detected)
|
|
161
134
|
|
|
162
|
-
|
|
135
|
+
raise InvalidResponseError.new("Language detection failed", response)
|
|
163
136
|
rescue JSON::ParserError
|
|
164
|
-
"
|
|
137
|
+
raise InvalidResponseError.new("Language detection failed", response), cause: nil
|
|
165
138
|
end
|
|
166
139
|
end
|
|
167
140
|
|
|
@@ -187,31 +160,13 @@ module MistralTranslator
|
|
|
187
160
|
end
|
|
188
161
|
|
|
189
162
|
def perform_client_request(prompt, request_context)
|
|
190
|
-
|
|
191
|
-
begin
|
|
192
|
-
return @client.complete(prompt, context: request_context)
|
|
193
|
-
rescue StandardError => e
|
|
194
|
-
raise e unless e.class.name.include?("MockExpectationError") && @client.respond_to?(:make_request)
|
|
195
|
-
|
|
196
|
-
return @client.make_request(prompt, nil, nil)
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
return @client.make_request(prompt, nil, nil) if @client.respond_to?(:make_request)
|
|
201
|
-
|
|
202
|
-
@client.complete(prompt, context: request_context)
|
|
163
|
+
@client.complete(prompt, context: request_context, json: true)
|
|
203
164
|
end
|
|
204
165
|
|
|
166
|
+
# Une réponse inexploitable lève une erreur : jamais de réponse brute enregistrée comme traduction.
|
|
205
167
|
def extract_translated_text!(raw_response)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return result[:translated] if result && !result[:translated].to_s.empty?
|
|
209
|
-
rescue EmptyTranslationError, InvalidResponseError
|
|
210
|
-
# Fallback: accepter une réponse texte brute non vide
|
|
211
|
-
fallback_text = raw_response.to_s.strip
|
|
212
|
-
return fallback_text unless fallback_text.empty?
|
|
213
|
-
# sinon on relance l'erreur plus bas
|
|
214
|
-
end
|
|
168
|
+
result = ResponseParser.parse_translation_response(raw_response)
|
|
169
|
+
return result[:translated] if result && !result[:translated].to_s.empty?
|
|
215
170
|
|
|
216
171
|
raise EmptyTranslationError
|
|
217
172
|
end
|
|
@@ -228,10 +183,14 @@ module MistralTranslator
|
|
|
228
183
|
|
|
229
184
|
# Helper pour traductions multi-cibles
|
|
230
185
|
module MultiTargetHelper
|
|
231
|
-
|
|
186
|
+
DEFAULT_MULTI_TARGET_OPTIONS = { context: nil, glossary: nil }.freeze
|
|
187
|
+
|
|
188
|
+
# options : context, glossary, preserve_html
|
|
189
|
+
def translate_to_multiple_batch(text, source_locale, target_locales, **options)
|
|
190
|
+
options = DEFAULT_MULTI_TARGET_OPTIONS.merge(options)
|
|
232
191
|
requests = target_locales.map do |target_locale|
|
|
233
192
|
{
|
|
234
|
-
prompt: build_translation_prompt(text, source_locale, target_locale,
|
|
193
|
+
prompt: build_translation_prompt(text, source_locale, target_locale, **options),
|
|
235
194
|
from: source_locale,
|
|
236
195
|
to: target_locale,
|
|
237
196
|
original_text: text
|
|
@@ -244,21 +203,28 @@ module MistralTranslator
|
|
|
244
203
|
batch_results.each do |result|
|
|
245
204
|
next unless result[:success]
|
|
246
205
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
results[target_locale] = parsed_result[:translated] if parsed_result
|
|
206
|
+
translated = parse_batch_translation(result[:result])
|
|
207
|
+
results[result[:original_request][:to]] = translated if translated
|
|
250
208
|
end
|
|
251
209
|
|
|
252
210
|
results
|
|
253
211
|
end
|
|
254
212
|
|
|
255
|
-
|
|
213
|
+
# Une réponse illisible fait échouer sa langue, pas tout le lot
|
|
214
|
+
def parse_batch_translation(response)
|
|
215
|
+
ResponseParser.parse_translation_response(response)&.fetch(:translated)
|
|
216
|
+
rescue EmptyTranslationError, InvalidResponseError
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def translate_to_multiple_sequential(text, source_locale, target_locales, **options)
|
|
221
|
+
options = DEFAULT_MULTI_TARGET_OPTIONS.merge(options)
|
|
256
222
|
results = {}
|
|
257
223
|
|
|
258
224
|
target_locales.each_with_index do |target_locale, index|
|
|
259
225
|
sleep(MistralTranslator::Translator::DEFAULT_RETRY_DELAY) if index.positive?
|
|
260
226
|
results[target_locale] =
|
|
261
|
-
translate_with_retry(text, source_locale, target_locale,
|
|
227
|
+
translate_with_retry(text, source_locale, target_locale, **options)
|
|
262
228
|
end
|
|
263
229
|
|
|
264
230
|
results
|
data/lib/mistral_translator.rb
CHANGED
|
@@ -107,7 +107,7 @@ module MistralTranslator
|
|
|
107
107
|
|
|
108
108
|
# Health check
|
|
109
109
|
def health_check
|
|
110
|
-
client.
|
|
110
|
+
client.chat("Hello", max_tokens: 10)
|
|
111
111
|
{ status: :ok, message: "API connection successful" }
|
|
112
112
|
rescue AuthenticationError
|
|
113
113
|
{ status: :error, message: "Authentication failed - check your API key" }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mistral_translator
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pierre Cherbero
|
|
@@ -159,15 +159,18 @@ files:
|
|
|
159
159
|
- lib/mistral_translator/client.rb
|
|
160
160
|
- lib/mistral_translator/client_helpers.rb
|
|
161
161
|
- lib/mistral_translator/configuration.rb
|
|
162
|
+
- lib/mistral_translator/data/languages.json
|
|
162
163
|
- lib/mistral_translator/errors.rb
|
|
163
164
|
- lib/mistral_translator/helpers.rb
|
|
164
165
|
- lib/mistral_translator/helpers_extensions.rb
|
|
165
166
|
- lib/mistral_translator/levenshtein_helpers.rb
|
|
166
167
|
- lib/mistral_translator/locale_helper.rb
|
|
167
168
|
- lib/mistral_translator/logger.rb
|
|
169
|
+
- lib/mistral_translator/prompt.rb
|
|
168
170
|
- lib/mistral_translator/prompt_builder.rb
|
|
169
171
|
- lib/mistral_translator/prompt_helpers.rb
|
|
170
172
|
- lib/mistral_translator/prompt_metadata_helpers.rb
|
|
173
|
+
- lib/mistral_translator/proxy.rb
|
|
171
174
|
- lib/mistral_translator/response_parser.rb
|
|
172
175
|
- lib/mistral_translator/security.rb
|
|
173
176
|
- lib/mistral_translator/summarizer.rb
|