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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +189 -121
  4. data/README_TESTING.md +33 -0
  5. data/SECURITY.md +157 -0
  6. data/docs/.nojekyll +2 -0
  7. data/docs/404.html +30 -0
  8. data/docs/README.md +153 -0
  9. data/docs/advanced-usage/batch-processing.md +158 -0
  10. data/docs/advanced-usage/error-handling.md +106 -0
  11. data/docs/advanced-usage/monitoring.md +133 -0
  12. data/docs/advanced-usage/summarization.md +86 -0
  13. data/docs/advanced-usage/translations.md +141 -0
  14. data/docs/api-reference/callbacks.md +231 -0
  15. data/docs/api-reference/configuration.md +74 -0
  16. data/docs/api-reference/errors.md +673 -0
  17. data/docs/api-reference/methods.md +539 -0
  18. data/docs/getting-started.md +179 -0
  19. data/docs/index.html +27 -0
  20. data/docs/installation.md +142 -0
  21. data/docs/migration-0.1.0-to-0.2.0.md +61 -0
  22. data/docs/rails-integration/adapters.md +84 -0
  23. data/docs/rails-integration/controllers.md +107 -0
  24. data/docs/rails-integration/jobs.md +97 -0
  25. data/docs/rails-integration/setup.md +339 -0
  26. data/examples/basic_usage.rb +129 -102
  27. data/examples/batch-job.rb +511 -0
  28. data/examples/monitoring-setup.rb +499 -0
  29. data/examples/rails-model.rb +399 -0
  30. data/lib/mistral_translator/adapters.rb +261 -0
  31. data/lib/mistral_translator/client.rb +103 -100
  32. data/lib/mistral_translator/client_helpers.rb +161 -0
  33. data/lib/mistral_translator/configuration.rb +171 -1
  34. data/lib/mistral_translator/errors.rb +16 -0
  35. data/lib/mistral_translator/helpers.rb +292 -0
  36. data/lib/mistral_translator/helpers_extensions.rb +150 -0
  37. data/lib/mistral_translator/levenshtein_helpers.rb +40 -0
  38. data/lib/mistral_translator/logger.rb +28 -4
  39. data/lib/mistral_translator/prompt_builder.rb +93 -41
  40. data/lib/mistral_translator/prompt_helpers.rb +83 -0
  41. data/lib/mistral_translator/prompt_metadata_helpers.rb +42 -0
  42. data/lib/mistral_translator/response_parser.rb +194 -23
  43. data/lib/mistral_translator/security.rb +72 -0
  44. data/lib/mistral_translator/summarizer.rb +41 -2
  45. data/lib/mistral_translator/translator.rb +174 -98
  46. data/lib/mistral_translator/translator_helpers.rb +268 -0
  47. data/lib/mistral_translator/version.rb +1 -1
  48. data/lib/mistral_translator.rb +51 -25
  49. metadata +39 -3
@@ -0,0 +1,499 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Exemple de configuration complète de monitoring pour MistralTranslator
5
+ # Usage: ruby examples/monitoring-setup.rb
6
+
7
+ $stdout.sync = true
8
+ require "json"
9
+ require "fileutils"
10
+ require "time"
11
+
12
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
13
+ require "mistral_translator"
14
+
15
+ # === SETUP 1: Configuration de base avec métriques ===
16
+
17
+ puts "=== MistralTranslator Monitoring Setup ==="
18
+
19
+ # Configuration complète
20
+ MistralTranslator.configure do |config|
21
+ config.api_key = ENV["MISTRAL_API_KEY"] || "your_api_key_here"
22
+ config.enable_metrics = true
23
+ config.retry_delays = [1, 2, 4, 8, 16]
24
+
25
+ # Callbacks de monitoring
26
+ config.on_translation_start = lambda { |from, to, length, timestamp|
27
+ puts "🚀 [#{timestamp}] Translation #{from}→#{to} starting (#{length} chars)"
28
+ }
29
+
30
+ config.on_translation_complete = lambda { |from, to, orig_len, trans_len, duration|
31
+ puts "✅ Translation #{from}→#{to} completed in #{duration.round(3)}s"
32
+ puts " Efficiency: #{(trans_len.to_f / orig_len).round(2)} chars/char"
33
+ }
34
+
35
+ config.on_translation_error = lambda { |from, to, error, attempt, timestamp|
36
+ puts "❌ [#{timestamp}] Translation #{from}→#{to} failed (attempt #{attempt}): #{error.message}"
37
+ }
38
+
39
+ config.on_rate_limit = lambda { |from, to, wait_time, attempt, timestamp|
40
+ puts "⏳ [#{timestamp}] Rate limit #{from}→#{to}, waiting #{wait_time}s (attempt #{attempt})"
41
+ }
42
+
43
+ config.on_batch_complete = lambda { |size, duration, success, errors|
44
+ puts "📦 Batch completed: #{success}/#{size} success in #{duration.round(2)}s (#{errors} errors)"
45
+ }
46
+ end
47
+
48
+ # === CLASSE: Collecteur de métriques avancé ===
49
+
50
+ class TranslationMetricsCollector
51
+ CallbackSet = Struct.new(:start, :complete, :error, :rate_limit)
52
+ def initialize
53
+ @stats = {
54
+ translations: 0,
55
+ errors: 0,
56
+ rate_limits: 0,
57
+ total_duration: 0.0,
58
+ total_chars_input: 0,
59
+ total_chars_output: 0,
60
+ by_language: Hash.new(0),
61
+ error_types: Hash.new(0),
62
+ hourly_stats: Hash.new { |h, k| h[k] = { count: 0, errors: 0 } }
63
+ }
64
+ @start_time = Time.now
65
+ setup_callbacks!
66
+ end
67
+
68
+ def setup_callbacks!
69
+ originals = fetch_original_callbacks
70
+
71
+ MistralTranslator.configure do |config|
72
+ assign_wrapped_callbacks(config, originals)
73
+ end
74
+ end
75
+
76
+ def fetch_original_callbacks
77
+ config = MistralTranslator.configuration
78
+ CallbackSet.new(
79
+ config.on_translation_start,
80
+ config.on_translation_complete,
81
+ config.on_translation_error,
82
+ config.on_rate_limit
83
+ )
84
+ end
85
+
86
+ def assign_wrapped_callbacks(config, originals)
87
+ config.on_translation_start = lambda { |from, to, length, timestamp|
88
+ originals.start&.call(from, to, length, timestamp)
89
+ on_translation_start(from, to, length, timestamp)
90
+ }
91
+
92
+ config.on_translation_complete = lambda { |from, to, orig_len, trans_len, duration|
93
+ originals.complete&.call(from, to, orig_len, trans_len, duration)
94
+ on_translation_complete(from, to, orig_len, trans_len, duration)
95
+ }
96
+
97
+ config.on_translation_error = lambda { |from, to, error, attempt, timestamp|
98
+ originals.error&.call(from, to, error, attempt, timestamp)
99
+ on_translation_error(from, to, error, attempt, timestamp)
100
+ }
101
+
102
+ config.on_rate_limit = lambda { |from, to, wait_time, attempt, timestamp|
103
+ originals.rate_limit&.call(from, to, wait_time, attempt, timestamp)
104
+ on_rate_limit(from, to, wait_time, attempt, timestamp)
105
+ }
106
+ end
107
+
108
+ def on_translation_start(_from, _to, length, timestamp)
109
+ hour_key = timestamp.strftime("%Y-%m-%d %H:00")
110
+ @stats[:hourly_stats][hour_key][:count] += 1
111
+ @stats[:total_chars_input] += length
112
+ end
113
+
114
+ def on_translation_complete(from, to, _orig_len, trans_len, duration)
115
+ @stats[:translations] += 1
116
+ @stats[:total_duration] += duration
117
+ @stats[:total_chars_output] += trans_len
118
+ @stats[:by_language]["#{from}_to_#{to}"] += 1
119
+ end
120
+
121
+ def on_translation_error(_from, _to, error, _attempt, timestamp)
122
+ @stats[:errors] += 1
123
+ @stats[:error_types][error.class.name] += 1
124
+
125
+ hour_key = timestamp.strftime("%Y-%m-%d %H:00")
126
+ @stats[:hourly_stats][hour_key][:errors] += 1
127
+ end
128
+
129
+ def on_rate_limit(_from, _to, _wait_time, _attempt, _timestamp)
130
+ @stats[:rate_limits] += 1
131
+ end
132
+
133
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
134
+ def report
135
+ uptime = Time.now - @start_time
136
+
137
+ puts "\n#{"=" * 50}"
138
+ puts "📊 TRANSLATION METRICS REPORT"
139
+ puts "=" * 50
140
+
141
+ # Statistiques globales
142
+ puts "⏱️ Uptime: #{uptime.round(2)}s"
143
+ puts "📝 Translations: #{@stats[:translations]}"
144
+ puts "❌ Errors: #{@stats[:errors]} (#{error_rate}%)"
145
+ puts "⏳ Rate limits: #{@stats[:rate_limits]}"
146
+
147
+ # Performance
148
+ if @stats[:translations].positive?
149
+ avg_duration = @stats[:total_duration] / @stats[:translations]
150
+ puts "⚡ Avg duration: #{avg_duration.round(3)}s per translation"
151
+ puts "📊 Throughput: #{(@stats[:translations] / uptime).round(2)} translations/sec"
152
+ end
153
+
154
+ # Caractères
155
+ puts "📄 Input chars: #{@stats[:total_chars_input]}"
156
+ puts "📄 Output chars: #{@stats[:total_chars_output]}"
157
+ if @stats[:total_chars_input].positive?
158
+ expansion = @stats[:total_chars_output].to_f / @stats[:total_chars_input]
159
+ puts "📈 Text expansion: #{expansion.round(2)}x"
160
+ end
161
+
162
+ # Top langues
163
+ if @stats[:by_language].any?
164
+ puts "\n🌐 Top language pairs:"
165
+ @stats[:by_language].sort_by { |_, count| -count }.first(5).each do |pair, count|
166
+ percentage = (count.to_f / @stats[:translations] * 100).round(1)
167
+ puts " #{pair.gsub("_to_", " → ")}: #{count} (#{percentage}%)"
168
+ end
169
+ end
170
+
171
+ # Top erreurs
172
+ if @stats[:error_types].any?
173
+ puts "\n❌ Error breakdown:"
174
+ @stats[:error_types].each do |error_type, count|
175
+ percentage = (count.to_f / @stats[:errors] * 100).round(1)
176
+ puts " #{error_type}: #{count} (#{percentage}%)"
177
+ end
178
+ end
179
+
180
+ # Statistiques horaires
181
+ if @stats[:hourly_stats].any?
182
+ puts "\n🕐 Activity by hour (last 5):"
183
+ @stats[:hourly_stats].sort_by { |hour, _| hour }.last(5).each do |hour, stats|
184
+ error_rate = stats[:count].positive? ? (stats[:errors].to_f / stats[:count] * 100).round(1) : 0
185
+ puts " #{hour}: #{stats[:count]} translations, #{stats[:errors]} errors (#{error_rate}%)"
186
+ end
187
+ end
188
+
189
+ puts "=" * 50
190
+ end
191
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
192
+
193
+ def error_rate
194
+ return 0 if @stats[:translations].zero?
195
+
196
+ (@stats[:errors].to_f / (@stats[:translations] + @stats[:errors]) * 100).round(1)
197
+ end
198
+
199
+ def health_status
200
+ if error_rate > 20
201
+ { status: :critical, message: "High error rate: #{error_rate}%" }
202
+ elsif error_rate > 10
203
+ { status: :warning, message: "Elevated error rate: #{error_rate}%" }
204
+ elsif @stats[:rate_limits] > @stats[:translations] * 0.5
205
+ { status: :warning, message: "Frequent rate limiting" }
206
+ else
207
+ { status: :healthy, message: "All systems operational" }
208
+ end
209
+ end
210
+ end
211
+
212
+ # === CLASSE: Alerting System ===
213
+
214
+ class AlertingSystem
215
+ def initialize(metrics_collector)
216
+ @metrics = metrics_collector
217
+ @alerts_sent = Set.new
218
+ @alert_cooldown = 300 # 5 minutes
219
+ @last_alerts = {}
220
+ end
221
+
222
+ def check_and_alert!
223
+ health = @metrics.health_status
224
+
225
+ case health[:status]
226
+ when :critical
227
+ send_alert(:critical, health[:message])
228
+ when :warning
229
+ send_alert(:warning, health[:message])
230
+ end
231
+
232
+ # Vérifications spécifiques
233
+ check_rate_limiting!
234
+ check_error_spikes!
235
+ check_performance_degradation!
236
+ end
237
+
238
+ private
239
+
240
+ def check_rate_limiting!
241
+ return unless @metrics.instance_variable_get(:@stats)[:rate_limits] > 10
242
+
243
+ send_alert(:warning, "Excessive rate limiting detected")
244
+ end
245
+
246
+ def check_error_spikes!
247
+ return unless @metrics.error_rate > 15
248
+
249
+ send_alert(:critical, "Error rate spike: #{@metrics.error_rate}%")
250
+ end
251
+
252
+ def check_performance_degradation!
253
+ stats = @metrics.instance_variable_get(:@stats)
254
+ return unless stats[:translations].positive?
255
+
256
+ avg_duration = stats[:total_duration] / stats[:translations]
257
+ return unless avg_duration > 10.0 # Plus de 10 secondes en moyenne
258
+
259
+ send_alert(:warning, "Performance degradation: #{avg_duration.round(2)}s avg")
260
+ end
261
+
262
+ def send_alert(level, message)
263
+ alert_key = "#{level}_#{message}"
264
+
265
+ # Cooldown check
266
+ return if @last_alerts[alert_key] && Time.now - @last_alerts[alert_key] < @alert_cooldown
267
+
268
+ @last_alerts[alert_key] = Time.now
269
+
270
+ # Simuler l'envoi d'alerte
271
+ icon = level == :critical ? "🚨" : "⚠️"
272
+ puts "#{icon} ALERT [#{level.upcase}]: #{message}"
273
+
274
+ # Ici vous pourriez intégrer:
275
+ # - Slack notifications
276
+ # - Email alerts
277
+ # - PagerDuty
278
+ # - Sentry
279
+ # - Custom webhooks
280
+
281
+ simulate_external_alert(level, message)
282
+ end
283
+
284
+ def simulate_external_alert(_level, _message)
285
+ # Exemple d'intégration Slack
286
+ if ENV["SLACK_WEBHOOK_URL"]
287
+ # webhook_payload = {
288
+ # text: "MistralTranslator Alert",
289
+ # attachments: [{
290
+ # color: level == :critical ? "danger" : "warning",
291
+ # fields: [{
292
+ # title: level.to_s.capitalize,
293
+ # value: message,
294
+ # short: false
295
+ # }]
296
+ # }]
297
+ # }
298
+ puts " → Slack notification sent"
299
+ end
300
+
301
+ # Exemple d'intégration Sentry
302
+ return unless ENV["SENTRY_DSN"]
303
+
304
+ # Sentry.capture_message(message, level: level)
305
+ puts " → Sentry event created"
306
+ end
307
+ end
308
+
309
+ # === CLASSE: Dashboard Data Generator ===
310
+
311
+ class MonitoringDashboard
312
+ def initialize(metrics_collector)
313
+ @metrics = metrics_collector
314
+ end
315
+
316
+ def generate_dashboard_data
317
+ stats = @metrics.instance_variable_get(:@stats)
318
+
319
+ {
320
+ overview: {
321
+ translations_count: stats[:translations],
322
+ error_count: stats[:errors],
323
+ error_rate: @metrics.error_rate,
324
+ rate_limits: stats[:rate_limits],
325
+ health_status: @metrics.health_status
326
+ },
327
+
328
+ performance: {
329
+ avg_duration: stats[:translations].positive? ? (stats[:total_duration] / stats[:translations]).round(3) : 0,
330
+ total_duration: stats[:total_duration].round(3),
331
+ chars_per_second: calculate_chars_per_second(stats),
332
+ text_expansion_ratio: calculate_expansion_ratio(stats)
333
+ },
334
+
335
+ usage: {
336
+ language_pairs: stats[:by_language].sort_by { |_, count| -count }.first(10),
337
+ hourly_activity: stats[:hourly_stats].sort_by { |hour, _| hour }.last(24),
338
+ error_breakdown: stats[:error_types]
339
+ },
340
+
341
+ system: {
342
+ uptime: (Time.now - @metrics.instance_variable_get(:@start_time)).round(2),
343
+ gem_version: MistralTranslator::VERSION,
344
+ ruby_version: RUBY_VERSION,
345
+ timestamp: Time.now.iso8601
346
+ }
347
+ }
348
+ end
349
+
350
+ def export_to_json(filename = "translation_dashboard_#{Time.now.strftime("%Y%m%d_%H%M%S")}.json")
351
+ data = generate_dashboard_data
352
+ File.write(filename, JSON.pretty_generate(data))
353
+ puts "📄 Dashboard data exported to: #{filename}"
354
+ filename
355
+ end
356
+
357
+ private
358
+
359
+ def calculate_chars_per_second(stats)
360
+ uptime = Time.now - @metrics.instance_variable_get(:@start_time)
361
+ uptime.positive? ? (stats[:total_chars_input] / uptime).round(2) : 0
362
+ end
363
+
364
+ def calculate_expansion_ratio(stats)
365
+ return 1.0 if stats[:total_chars_input].zero?
366
+
367
+ (stats[:total_chars_output].to_f / stats[:total_chars_input]).round(3)
368
+ end
369
+ end
370
+
371
+ # === UTILISATION ET DÉMONSTRATION ===
372
+
373
+ puts "\n1. Setting up monitoring..."
374
+ metrics = TranslationMetricsCollector.new
375
+ alerting = AlertingSystem.new(metrics)
376
+ dashboard = MonitoringDashboard.new(metrics)
377
+
378
+ puts "\n2. Testing with sample translations..."
379
+
380
+ # Test basique pour vérifier la connexion
381
+ begin
382
+ test_result = MistralTranslator.translate("Hello monitoring", from: "en", to: "fr")
383
+ puts "API connection OK: #{test_result}"
384
+ rescue MistralTranslator::Error => e
385
+ puts "API Error: #{e.message}"
386
+ puts "Continuing with monitoring setup demo..."
387
+ end
388
+
389
+ # Simuler différents scénarios
390
+ sample_texts = [
391
+ "Bonjour le monde",
392
+ "Comment allez-vous ?",
393
+ "Ruby on Rails",
394
+ "Test de monitoring",
395
+ "Système de surveillance"
396
+ ]
397
+
398
+ puts "\n3. Running sample translations to generate metrics..."
399
+
400
+ sample_texts.each_with_index do |text, i|
401
+ MistralTranslator.translate(text, from: "fr", to: "en")
402
+ print "."
403
+
404
+ # Simuler quelques erreurs pour les métriques
405
+ if i == 2
406
+ begin
407
+ MistralTranslator.translate("", from: "invalid", to: "fr")
408
+ rescue MistralTranslator::Error
409
+ # Erreur attendue pour les métriques
410
+ end
411
+ end
412
+
413
+ sleep(0.2) # Rate limiting
414
+ rescue MistralTranslator::Error => e
415
+ print "x"
416
+ end
417
+
418
+ puts "\n\n4. Checking alerts..."
419
+ alerting.check_and_alert!
420
+
421
+ puts "\n5. Generating reports..."
422
+ metrics.report
423
+
424
+ puts "\n6. Health status check..."
425
+ health = metrics.health_status
426
+ puts "🏥 Health: #{health[:status]} - #{health[:message]}"
427
+
428
+ puts "\n7. Exporting dashboard data..."
429
+ dashboard_file = dashboard.export_to_json
430
+
431
+ puts "\n8. Built-in metrics from MistralTranslator:"
432
+ if MistralTranslator.configuration.enable_metrics
433
+ built_in_metrics = MistralTranslator.metrics
434
+ puts "📊 Built-in metrics:"
435
+ built_in_metrics.each do |key, value|
436
+ puts " #{key}: #{value}"
437
+ end
438
+ else
439
+ puts "⚠️ Built-in metrics not enabled"
440
+ end
441
+
442
+ # === CONFIGURATION POUR PRODUCTION ===
443
+
444
+ puts "\n#{"=" * 50}"
445
+ puts "📋 PRODUCTION MONITORING SETUP EXAMPLE"
446
+ puts "=" * 50
447
+
448
+ puts <<~SETUP
449
+ # config/initializers/mistral_translator.rb
450
+ MistralTranslator.configure do |config|
451
+ config.api_key = ENV['MISTRAL_API_KEY']
452
+ config.enable_metrics = true
453
+ #{" "}
454
+ # Callbacks pour intégration monitoring
455
+ config.on_translation_error = ->(from, to, error, attempt, timestamp) {
456
+ # Sentry/Honeybadger
457
+ if defined?(Sentry)
458
+ Sentry.capture_exception(error, extra: {
459
+ translation_direction: "\#{from} -> \#{to}",
460
+ attempt_number: attempt
461
+ })
462
+ end
463
+ #{" "}
464
+ # Logs structurés
465
+ Rails.logger.error({
466
+ event: 'translation_error',
467
+ from_language: from,
468
+ to_language: to,
469
+ error_class: error.class.name,
470
+ error_message: error.message,
471
+ attempt: attempt,
472
+ timestamp: timestamp
473
+ }.to_json)
474
+ }
475
+ #{" "}
476
+ config.on_rate_limit = ->(from, to, wait_time, attempt, timestamp) {
477
+ # Métriques custom (StatsD, DataDog, etc.)
478
+ if defined?(StatsD)
479
+ StatsD.increment('mistral_translator.rate_limit',
480
+ tags: ["from:\#{from}", "to:\#{to}"])
481
+ end
482
+ }
483
+ #{" "}
484
+ config.on_translation_complete = ->(from, to, orig, trans, duration) {
485
+ # Métriques de performance
486
+ if defined?(StatsD)
487
+ StatsD.timing('mistral_translator.duration', duration * 1000,
488
+ tags: ["from:\#{from}", "to:\#{to}"])
489
+ StatsD.histogram('mistral_translator.chars_ratio',#{" "}
490
+ trans.to_f / orig, tags: ["from:\#{from}", "to:\#{to}"])
491
+ end
492
+ }
493
+ end
494
+ SETUP
495
+
496
+ puts "\n🎉 Monitoring setup complete!"
497
+
498
+ # Nettoyage
499
+ FileUtils.rm_f(dashboard_file)