mistral_translator 0.1.0 → 0.2.1
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 +21 -0
- data/README.md +189 -121
- data/README_TESTING.md +33 -0
- data/SECURITY.md +157 -0
- data/docs/.nojekyll +2 -0
- data/docs/404.html +30 -0
- data/docs/README.md +153 -0
- data/docs/advanced-usage/batch-processing.md +158 -0
- data/docs/advanced-usage/error-handling.md +106 -0
- data/docs/advanced-usage/monitoring.md +133 -0
- data/docs/advanced-usage/summarization.md +86 -0
- data/docs/advanced-usage/translations.md +141 -0
- data/docs/api-reference/callbacks.md +231 -0
- data/docs/api-reference/configuration.md +74 -0
- data/docs/api-reference/errors.md +673 -0
- data/docs/api-reference/methods.md +539 -0
- data/docs/getting-started.md +179 -0
- data/docs/index.html +27 -0
- data/docs/installation.md +142 -0
- data/docs/migration-0.1.0-to-0.2.0.md +61 -0
- data/docs/rails-integration/adapters.md +84 -0
- data/docs/rails-integration/controllers.md +107 -0
- data/docs/rails-integration/jobs.md +97 -0
- data/docs/rails-integration/setup.md +339 -0
- data/examples/basic_usage.rb +129 -102
- data/examples/batch-job.rb +511 -0
- data/examples/monitoring-setup.rb +499 -0
- data/examples/rails-model.rb +399 -0
- data/lib/mistral_translator/adapters.rb +261 -0
- data/lib/mistral_translator/client.rb +103 -100
- data/lib/mistral_translator/client_helpers.rb +161 -0
- data/lib/mistral_translator/configuration.rb +171 -1
- data/lib/mistral_translator/errors.rb +16 -0
- data/lib/mistral_translator/helpers.rb +292 -0
- data/lib/mistral_translator/helpers_extensions.rb +150 -0
- data/lib/mistral_translator/levenshtein_helpers.rb +40 -0
- data/lib/mistral_translator/logger.rb +28 -4
- data/lib/mistral_translator/prompt_builder.rb +93 -41
- data/lib/mistral_translator/prompt_helpers.rb +83 -0
- data/lib/mistral_translator/prompt_metadata_helpers.rb +42 -0
- data/lib/mistral_translator/response_parser.rb +194 -23
- data/lib/mistral_translator/security.rb +72 -0
- data/lib/mistral_translator/summarizer.rb +41 -2
- data/lib/mistral_translator/translator.rb +174 -98
- data/lib/mistral_translator/translator_helpers.rb +268 -0
- data/lib/mistral_translator/version.rb +1 -1
- data/lib/mistral_translator.rb +51 -25
- metadata +39 -3
@@ -0,0 +1,511 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Exemple de traitement par batch avec MistralTranslator
|
5
|
+
# Usage: ruby examples/batch-job.rb
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
8
|
+
require "mistral_translator"
|
9
|
+
|
10
|
+
# Configuration
|
11
|
+
MistralTranslator.configure do |config|
|
12
|
+
config.api_key = ENV["MISTRAL_API_KEY"] || "your_api_key_here"
|
13
|
+
config.enable_metrics = true
|
14
|
+
config.retry_delays = [2, 4, 8, 16]
|
15
|
+
end
|
16
|
+
|
17
|
+
# === EXEMPLE 1: Traduction de fichiers CSV ===
|
18
|
+
|
19
|
+
require "csv"
|
20
|
+
require "fileutils"
|
21
|
+
|
22
|
+
class CSVTranslationBatch
|
23
|
+
def initialize(input_file, output_file, from:, to:, text_columns: [])
|
24
|
+
@input_file = input_file
|
25
|
+
@output_file = output_file
|
26
|
+
@from = from
|
27
|
+
@to = to
|
28
|
+
@text_columns = text_columns
|
29
|
+
@batch_size = 10
|
30
|
+
end
|
31
|
+
|
32
|
+
def process!
|
33
|
+
rows = CSV.read(@input_file, headers: true)
|
34
|
+
puts "📄 Processing #{rows.size} rows from #{@input_file}"
|
35
|
+
|
36
|
+
translated_rows = []
|
37
|
+
|
38
|
+
total_batches = (rows.size.to_f / @batch_size).ceil
|
39
|
+
rows.each_slice(@batch_size).with_index do |batch, index|
|
40
|
+
puts "🔄 Processing batch of #{batch.size} rows..."
|
41
|
+
translated_rows.concat(process_batch(batch))
|
42
|
+
|
43
|
+
# Rate limiting uniquement entre les batches
|
44
|
+
sleep(2) if index < total_batches - 1
|
45
|
+
end
|
46
|
+
|
47
|
+
# Écriture du fichier de sortie
|
48
|
+
CSV.open(@output_file, "w", headers: true) do |csv|
|
49
|
+
# Headers
|
50
|
+
csv << translated_rows.first.headers if translated_rows.any?
|
51
|
+
|
52
|
+
# Data
|
53
|
+
translated_rows.each { |row| csv << row }
|
54
|
+
end
|
55
|
+
|
56
|
+
puts "✅ Traduction terminée: #{@output_file}"
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def process_batch(batch)
|
62
|
+
batch.map do |row|
|
63
|
+
translated_row = row.dup
|
64
|
+
|
65
|
+
@text_columns.each do |column|
|
66
|
+
text = row[column]
|
67
|
+
next if text.nil? || text.empty?
|
68
|
+
|
69
|
+
begin
|
70
|
+
translated = MistralTranslator.translate(text, from: @from, to: @to)
|
71
|
+
translated_row["#{column}_#{@to}"] = translated
|
72
|
+
rescue MistralTranslator::Error => e
|
73
|
+
puts "❌ Error translating row #{row.to_h}: #{e.message}"
|
74
|
+
translated_row["#{column}_#{@to}"] = "[TRANSLATION_ERROR]"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
translated_row
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# === EXEMPLE 2: Job de traduction avec queue ===
|
84
|
+
|
85
|
+
class TranslationQueue
|
86
|
+
def initialize
|
87
|
+
@queue = []
|
88
|
+
@results = {}
|
89
|
+
@workers = 3
|
90
|
+
@batch_size = 5
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_job(id, text, from:, to:, context: nil)
|
94
|
+
@queue << {
|
95
|
+
id: id,
|
96
|
+
text: text,
|
97
|
+
from: from,
|
98
|
+
to: to,
|
99
|
+
context: context,
|
100
|
+
status: :pending
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def process_all!
|
105
|
+
puts "🚀 Starting translation queue with #{@workers} workers"
|
106
|
+
puts "📋 #{@queue.size} jobs to process"
|
107
|
+
|
108
|
+
threads = []
|
109
|
+
job_chunks = @queue.each_slice(@batch_size).to_a
|
110
|
+
|
111
|
+
@workers.times do |worker_id|
|
112
|
+
threads << Thread.new do
|
113
|
+
process_worker_jobs(worker_id, job_chunks)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
threads.each(&:join)
|
118
|
+
|
119
|
+
# Résultats
|
120
|
+
successful = @results.values.count { |r| r[:status] == :success }
|
121
|
+
failed = @results.values.count { |r| r[:status] == :error }
|
122
|
+
|
123
|
+
puts "\n📊 Results:"
|
124
|
+
puts "✅ Successful: #{successful}"
|
125
|
+
puts "❌ Failed: #{failed}"
|
126
|
+
puts "📈 Success rate: #{(successful.to_f / @queue.size * 100).round(1)}%"
|
127
|
+
|
128
|
+
@results
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def process_worker_jobs(worker_id, job_chunks)
|
134
|
+
my_chunks = job_chunks.select.with_index { |_, i| i % @workers == worker_id }
|
135
|
+
|
136
|
+
my_chunks.each do |chunk|
|
137
|
+
puts "🔧 Worker #{worker_id} processing #{chunk.size} jobs"
|
138
|
+
|
139
|
+
chunk.each do |job|
|
140
|
+
process_single_job(worker_id, job)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Rate limiting entre les chunks
|
144
|
+
sleep(1)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def process_single_job(worker_id, job)
|
149
|
+
translated = MistralTranslator.translate(
|
150
|
+
job[:text],
|
151
|
+
from: job[:from],
|
152
|
+
to: job[:to],
|
153
|
+
context: job[:context]
|
154
|
+
)
|
155
|
+
|
156
|
+
@results[job[:id]] = {
|
157
|
+
status: :success,
|
158
|
+
original: job[:text],
|
159
|
+
translated: translated,
|
160
|
+
worker: worker_id
|
161
|
+
}
|
162
|
+
|
163
|
+
puts "✅ Worker #{worker_id}: Job #{job[:id]} completed"
|
164
|
+
rescue MistralTranslator::Error => e
|
165
|
+
@results[job[:id]] = {
|
166
|
+
status: :error,
|
167
|
+
original: job[:text],
|
168
|
+
error: e.message,
|
169
|
+
worker: worker_id
|
170
|
+
}
|
171
|
+
|
172
|
+
puts "❌ Worker #{worker_id}: Job #{job[:id]} failed - #{e.message}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# === EXEMPLE 3: Traitement de fichiers JSON ===
|
177
|
+
|
178
|
+
require "json"
|
179
|
+
|
180
|
+
class JSONBatchProcessor
|
181
|
+
def initialize(input_file, output_file)
|
182
|
+
@input_file = input_file
|
183
|
+
@output_file = output_file
|
184
|
+
end
|
185
|
+
|
186
|
+
def translate_nested_json(from:, to:)
|
187
|
+
data = JSON.parse(File.read(@input_file))
|
188
|
+
puts "📄 Processing JSON file: #{@input_file}"
|
189
|
+
|
190
|
+
translated_data = translate_recursive(data, from, to)
|
191
|
+
|
192
|
+
File.write(@output_file, JSON.pretty_generate(translated_data))
|
193
|
+
puts "✅ Translated JSON saved: #{@output_file}"
|
194
|
+
|
195
|
+
translated_data
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def translate_recursive(obj, from, to, path = [])
|
201
|
+
case obj
|
202
|
+
when Hash
|
203
|
+
result = {}
|
204
|
+
obj.each do |key, value|
|
205
|
+
current_path = path + [key]
|
206
|
+
|
207
|
+
if translatable_key?(key) && value.is_a?(String)
|
208
|
+
puts "🔄 Translating #{current_path.join(".")}: #{value[0..50]}..."
|
209
|
+
|
210
|
+
begin
|
211
|
+
result[key] = MistralTranslator.translate(value, from: from, to: to)
|
212
|
+
result["#{key}_original"] = value # Garder l'original
|
213
|
+
rescue MistralTranslator::Error => e
|
214
|
+
puts "❌ Translation failed for #{current_path.join(".")}: #{e.message}"
|
215
|
+
result[key] = value # Garder l'original en cas d'erreur
|
216
|
+
end
|
217
|
+
else
|
218
|
+
result[key] = translate_recursive(value, from, to, current_path)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
result
|
222
|
+
|
223
|
+
when Array
|
224
|
+
obj.map.with_index do |item, index|
|
225
|
+
translate_recursive(item, from, to, path + [index])
|
226
|
+
end
|
227
|
+
|
228
|
+
else
|
229
|
+
obj # Primitive values
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def translatable_key?(key)
|
234
|
+
# Keys qui contiennent du texte à traduire
|
235
|
+
%w[title description content text message label name summary].include?(key.to_s.downcase)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# === EXEMPLE 4: Batch avec retry et monitoring ===
|
240
|
+
|
241
|
+
class RobustBatchProcessor
|
242
|
+
def initialize(items, from:, to:)
|
243
|
+
@items = items
|
244
|
+
@from = from
|
245
|
+
@to = to
|
246
|
+
@results = {}
|
247
|
+
@errors = {}
|
248
|
+
@retry_queue = []
|
249
|
+
end
|
250
|
+
|
251
|
+
def process_with_monitoring!
|
252
|
+
puts "🚀 Starting robust batch processing"
|
253
|
+
puts "📊 Items: #{@items.size}"
|
254
|
+
|
255
|
+
# Première passe
|
256
|
+
process_initial_batch
|
257
|
+
|
258
|
+
# Retry des éléments échoués
|
259
|
+
retry_failed_items if @retry_queue.any?
|
260
|
+
|
261
|
+
# Rapport final
|
262
|
+
generate_report
|
263
|
+
|
264
|
+
@results
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
def process_initial_batch
|
270
|
+
@items.each_with_index do |item, index|
|
271
|
+
print_progress(index + 1, @items.size)
|
272
|
+
|
273
|
+
begin
|
274
|
+
result = MistralTranslator.translate(item, from: @from, to: @to)
|
275
|
+
@results[index] = result
|
276
|
+
rescue MistralTranslator::RateLimitError
|
277
|
+
puts "\n⏳ Rate limit hit, waiting..."
|
278
|
+
sleep(30)
|
279
|
+
@retry_queue << { index: index, item: item, attempts: 1 }
|
280
|
+
rescue MistralTranslator::Error => e
|
281
|
+
@errors[index] = e.message
|
282
|
+
@retry_queue << { index: index, item: item, attempts: 1 }
|
283
|
+
end
|
284
|
+
|
285
|
+
# Rate limiting
|
286
|
+
sleep(0.5)
|
287
|
+
end
|
288
|
+
|
289
|
+
puts "\n✅ Initial batch completed"
|
290
|
+
end
|
291
|
+
|
292
|
+
def retry_failed_items
|
293
|
+
puts "🔄 Retrying #{@retry_queue.size} failed items..."
|
294
|
+
|
295
|
+
@retry_queue.each do |retry_item|
|
296
|
+
next if retry_item[:attempts] >= 3
|
297
|
+
|
298
|
+
begin
|
299
|
+
result = MistralTranslator.translate(retry_item[:item], from: @from, to: @to)
|
300
|
+
@results[retry_item[:index]] = result
|
301
|
+
puts "✅ Retry successful for item #{retry_item[:index]}"
|
302
|
+
rescue MistralTranslator::Error => e
|
303
|
+
retry_item[:attempts] += 1
|
304
|
+
@errors[retry_item[:index]] = "#{e.message} (#{retry_item[:attempts]} attempts)"
|
305
|
+
puts "❌ Retry failed for item #{retry_item[:index]}: #{e.message}"
|
306
|
+
end
|
307
|
+
|
308
|
+
sleep(2) # Plus de délai pour les retries
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def print_progress(current, total)
|
313
|
+
percentage = (current.to_f / total * 100).round(1)
|
314
|
+
print "\r🔄 Progress: #{current}/#{total} (#{percentage}%)"
|
315
|
+
end
|
316
|
+
|
317
|
+
def generate_report
|
318
|
+
totals = compute_totals
|
319
|
+
print_header_and_totals(totals)
|
320
|
+
print_translator_metrics_if_enabled
|
321
|
+
print_top_errors
|
322
|
+
end
|
323
|
+
|
324
|
+
def compute_totals
|
325
|
+
successful = @results.size
|
326
|
+
failed = @errors.size
|
327
|
+
total = @items.size
|
328
|
+
{
|
329
|
+
successful: successful,
|
330
|
+
failed: failed,
|
331
|
+
total: total,
|
332
|
+
success_rate: percentage(successful, total),
|
333
|
+
fail_rate: percentage(failed, total)
|
334
|
+
}
|
335
|
+
end
|
336
|
+
|
337
|
+
def percentage(count, total)
|
338
|
+
return 0.0 if total.zero?
|
339
|
+
|
340
|
+
(count.to_f / total * 100).round(1)
|
341
|
+
end
|
342
|
+
|
343
|
+
def print_header_and_totals(totals)
|
344
|
+
puts "\n\n📊 BATCH PROCESSING REPORT"
|
345
|
+
puts "=" * 40
|
346
|
+
puts "Total items: #{totals[:total]}"
|
347
|
+
puts "Successful: #{totals[:successful]} (#{totals[:success_rate]}%)"
|
348
|
+
puts "Failed: #{totals[:failed]} (#{totals[:fail_rate]}%)"
|
349
|
+
end
|
350
|
+
|
351
|
+
def print_translator_metrics_if_enabled
|
352
|
+
return unless MistralTranslator.configuration.enable_metrics
|
353
|
+
|
354
|
+
metrics = MistralTranslator.metrics
|
355
|
+
puts "\n📈 Translation Metrics:"
|
356
|
+
puts "Total translations: #{metrics[:total_translations]}"
|
357
|
+
puts "Average time: #{metrics[:average_translation_time]}s"
|
358
|
+
puts "Error rate: #{metrics[:error_rate]}%"
|
359
|
+
end
|
360
|
+
|
361
|
+
def print_top_errors
|
362
|
+
return unless @errors.any?
|
363
|
+
|
364
|
+
puts "\n❌ Top Errors:"
|
365
|
+
error_groups = @errors.values.group_by(&:itself)
|
366
|
+
error_groups.sort_by { |_, v| -v.size }.first(3).each do |error, occurrences|
|
367
|
+
puts " #{error}: #{occurrences.size} times"
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# === UTILISATION ===
|
373
|
+
|
374
|
+
puts "=== MistralTranslator Batch Processing Examples ==="
|
375
|
+
|
376
|
+
# Test de base avec vérification de l'API
|
377
|
+
begin
|
378
|
+
test_result = MistralTranslator.translate("Hello", from: "en", to: "fr")
|
379
|
+
puts "✅ API connection OK: #{test_result}"
|
380
|
+
rescue MistralTranslator::Error => e
|
381
|
+
puts "❌ API Error: #{e.message}"
|
382
|
+
puts "Continuing with examples that don't require API..."
|
383
|
+
end
|
384
|
+
|
385
|
+
# EXEMPLE 1: CSV
|
386
|
+
puts "\n1. CSV Translation Example"
|
387
|
+
puts "-" * 30
|
388
|
+
|
389
|
+
# Créer un fichier CSV d'exemple
|
390
|
+
sample_csv = "tmp_products.csv"
|
391
|
+
CSV.open(sample_csv, "w", headers: true) do |csv|
|
392
|
+
csv << %w[id name description]
|
393
|
+
csv << ["1", "Laptop Premium", "Ordinateur portable haute performance"]
|
394
|
+
csv << ["2", "Souris Gaming", "Souris optique pour gaming"]
|
395
|
+
csv << ["3", "Clavier Mécanique", "Clavier mécanique rétroéclairé"]
|
396
|
+
end
|
397
|
+
|
398
|
+
if File.exist?(sample_csv)
|
399
|
+
processor = CSVTranslationBatch.new(
|
400
|
+
sample_csv,
|
401
|
+
"tmp_products_en.csv",
|
402
|
+
from: "fr",
|
403
|
+
to: "en",
|
404
|
+
text_columns: %w[name description]
|
405
|
+
)
|
406
|
+
|
407
|
+
begin
|
408
|
+
processor.process!
|
409
|
+
rescue StandardError => e
|
410
|
+
puts "CSV processing error: #{e.message}"
|
411
|
+
ensure
|
412
|
+
FileUtils.rm_f(sample_csv)
|
413
|
+
FileUtils.rm_f("tmp_products_en.csv")
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# EXEMPLE 2: Queue
|
418
|
+
puts "\n2. Translation Queue Example"
|
419
|
+
puts "-" * 30
|
420
|
+
|
421
|
+
queue = TranslationQueue.new
|
422
|
+
|
423
|
+
# Ajouter des jobs
|
424
|
+
sample_texts = [
|
425
|
+
"Bonjour le monde",
|
426
|
+
"Comment allez-vous ?",
|
427
|
+
"Ruby on Rails est génial",
|
428
|
+
"J'aime la programmation",
|
429
|
+
"Les tests sont importants"
|
430
|
+
]
|
431
|
+
|
432
|
+
sample_texts.each_with_index do |text, i|
|
433
|
+
queue.add_job(i, text, from: "fr", to: "en", context: "casual conversation")
|
434
|
+
end
|
435
|
+
|
436
|
+
begin
|
437
|
+
results = queue.process_all!
|
438
|
+
|
439
|
+
puts "\nSample results:"
|
440
|
+
results.values.first(2).each do |result|
|
441
|
+
if result[:status] == :success
|
442
|
+
puts " #{result[:original]} → #{result[:translated]}"
|
443
|
+
else
|
444
|
+
puts " ERROR: #{result[:error]}"
|
445
|
+
end
|
446
|
+
end
|
447
|
+
rescue StandardError => e
|
448
|
+
puts "Queue processing error: #{e.message}"
|
449
|
+
end
|
450
|
+
|
451
|
+
# EXEMPLE 3: JSON
|
452
|
+
puts "\n3. JSON Translation Example"
|
453
|
+
puts "-" * 30
|
454
|
+
|
455
|
+
sample_json = {
|
456
|
+
"app" => {
|
457
|
+
"title" => "Mon Application",
|
458
|
+
"description" => "Une application formidable pour tous",
|
459
|
+
"version" => "1.0.0",
|
460
|
+
"features" => [
|
461
|
+
{
|
462
|
+
"name" => "Traduction Automatique",
|
463
|
+
"description" => "Traduit votre contenu instantanément"
|
464
|
+
},
|
465
|
+
{
|
466
|
+
"name" => "Interface Intuitive",
|
467
|
+
"description" => "Design simple et élégant"
|
468
|
+
}
|
469
|
+
]
|
470
|
+
}
|
471
|
+
}
|
472
|
+
|
473
|
+
json_file = "tmp_app.json"
|
474
|
+
File.write(json_file, JSON.generate(sample_json))
|
475
|
+
|
476
|
+
processor = JSONBatchProcessor.new(json_file, "tmp_app_en.json")
|
477
|
+
|
478
|
+
begin
|
479
|
+
processor.translate_nested_json(from: "fr", to: "en")
|
480
|
+
rescue StandardError => e
|
481
|
+
puts "JSON processing error: #{e.message}"
|
482
|
+
ensure
|
483
|
+
FileUtils.rm_f(json_file)
|
484
|
+
FileUtils.rm_f("tmp_app_en.json")
|
485
|
+
end
|
486
|
+
|
487
|
+
# EXEMPLE 4: Robust batch
|
488
|
+
puts "\n4. Robust Batch Processing Example"
|
489
|
+
puts "-" * 30
|
490
|
+
|
491
|
+
texts_to_translate = [
|
492
|
+
"Bienvenue dans notre application",
|
493
|
+
"Votre compte a été créé avec succès",
|
494
|
+
"Merci pour votre commande",
|
495
|
+
"Erreur de connexion au serveur",
|
496
|
+
"Votre mot de passe a été mis à jour"
|
497
|
+
]
|
498
|
+
|
499
|
+
robust_processor = RobustBatchProcessor.new(
|
500
|
+
texts_to_translate,
|
501
|
+
from: "fr",
|
502
|
+
to: "en"
|
503
|
+
)
|
504
|
+
|
505
|
+
begin
|
506
|
+
robust_processor.process_with_monitoring!
|
507
|
+
rescue StandardError => e
|
508
|
+
puts "Robust batch error: #{e.message}"
|
509
|
+
end
|
510
|
+
|
511
|
+
puts "\n🎉 All batch examples completed!"
|