mistral_translator 0.1.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.
data/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # MistralTranslator
2
+
3
+ Une gem Ruby pour traduire et résumer du texte en utilisant l'API Mistral AI.
4
+
5
+ ## Installation
6
+
7
+ Ajoutez cette ligne à votre Gemfile :
8
+
9
+ ```ruby
10
+ gem 'mistral_translator'
11
+ ```
12
+
13
+ Puis exécutez :
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ Ou installez directement :
20
+
21
+ ```bash
22
+ gem install mistral_translator
23
+ ```
24
+
25
+ ## Configuration
26
+
27
+ ```ruby
28
+ require 'mistral_translator'
29
+
30
+ MistralTranslator.configure do |config|
31
+ config.api_key = 'votre_clé_api_mistral'
32
+ config.api_url = 'https://api.mistral.ai' # optionnel
33
+ config.model = 'mistral-small' # optionnel
34
+ end
35
+ ```
36
+
37
+ ### Variables d'environnement
38
+
39
+ Vous pouvez aussi utiliser des variables d'environnement :
40
+
41
+ ```bash
42
+ export MISTRAL_API_KEY=votre_clé_api
43
+ export MISTRAL_API_URL=https://api.mistral.ai
44
+ ```
45
+
46
+ ## Utilisation
47
+
48
+ ### Traduction simple
49
+
50
+ ```ruby
51
+ # Traduction de base
52
+ result = MistralTranslator.translate("Bonjour le monde", from: 'fr', to: 'en')
53
+ # => "Hello world"
54
+
55
+ # Vers plusieurs langues
56
+ results = MistralTranslator.translate_to_multiple(
57
+ "Bonjour le monde",
58
+ from: 'fr',
59
+ to: ['en', 'es', 'de']
60
+ )
61
+ # => { 'en' => "Hello world", 'es' => "Hola mundo", 'de' => "Hallo Welt" }
62
+ ```
63
+
64
+ ### Traduction en lot
65
+
66
+ ```ruby
67
+ texts = ["Bonjour", "Au revoir", "Merci"]
68
+ results = MistralTranslator.translate_batch(texts, from: 'fr', to: 'en')
69
+ # => { 0 => "Hello", 1 => "Goodbye", 2 => "Thank you" }
70
+ ```
71
+
72
+ ### Auto-détection de langue
73
+
74
+ ```ruby
75
+ result = MistralTranslator.translate_auto("Hello world", to: 'fr')
76
+ # => "Bonjour le monde"
77
+ ```
78
+
79
+ ### Résumés
80
+
81
+ ```ruby
82
+ long_text = "Un très long texte à résumer..."
83
+
84
+ # Résumé simple
85
+ summary = MistralTranslator.summarize(long_text, language: 'fr', max_words: 100)
86
+
87
+ # Résumé avec traduction
88
+ summary = MistralTranslator.summarize_and_translate(
89
+ long_text,
90
+ from: 'fr',
91
+ to: 'en',
92
+ max_words: 150
93
+ )
94
+
95
+ # Résumés par niveaux
96
+ summaries = MistralTranslator.summarize_tiered(
97
+ long_text,
98
+ language: 'fr',
99
+ short: 50,
100
+ medium: 150,
101
+ long: 300
102
+ )
103
+ # => { short: "...", medium: "...", long: "..." }
104
+ ```
105
+
106
+ ### Extensions String (optionnel)
107
+
108
+ ```ruby
109
+ # Activer les extensions String
110
+ ENV['MISTRAL_TRANSLATOR_EXTEND_STRING'] = 'true'
111
+ require 'mistral_translator'
112
+
113
+ "Bonjour".mistral_translate(from: 'fr', to: 'en')
114
+ # => "Hello"
115
+
116
+ "Long texte...".mistral_summarize(language: 'fr', max_words: 50)
117
+ # => "Résumé..."
118
+ ```
119
+
120
+ ## Langues supportées
121
+
122
+ La gem supporte les langues suivantes :
123
+ - Français (fr)
124
+ - Anglais (en)
125
+ - Espagnol (es)
126
+ - Portugais (pt)
127
+ - Allemand (de)
128
+ - Italien (it)
129
+ - Néerlandais (nl)
130
+ - Russe (ru)
131
+ - Japonais (ja)
132
+ - Coréen (ko)
133
+ - Chinois (zh)
134
+ - Arabe (ar)
135
+
136
+ ```ruby
137
+ # Vérifier les langues supportées
138
+ MistralTranslator.supported_languages
139
+ MistralTranslator.locale_supported?('fr') # => true
140
+ ```
141
+
142
+ ## Gestion d'erreurs
143
+
144
+ ```ruby
145
+ begin
146
+ result = MistralTranslator.translate("Hello", from: 'en', to: 'fr')
147
+ rescue MistralTranslator::RateLimitError
148
+ puts "Rate limit dépassé, réessayez plus tard"
149
+ rescue MistralTranslator::AuthenticationError
150
+ puts "Clé API invalide"
151
+ rescue MistralTranslator::UnsupportedLanguageError => e
152
+ puts "Langue non supportée: #{e.language}"
153
+ end
154
+ ```
155
+
156
+ ## Utilisation avancée
157
+
158
+ ### Client personnalisé
159
+
160
+ ```ruby
161
+ client = MistralTranslator::Client.new(api_key: 'autre_clé')
162
+ translator = MistralTranslator::Translator.new(client: client)
163
+ result = translator.translate("Hello", from: 'en', to: 'fr')
164
+ ```
165
+
166
+ ### Health check
167
+
168
+ ```ruby
169
+ status = MistralTranslator.health_check
170
+ # => { status: :ok, message: "API connection successful" }
171
+ ```
172
+
173
+ ## Développement
174
+
175
+ Après avoir cloné le repo :
176
+
177
+ ```bash
178
+ bundle install
179
+ bin/setup
180
+ ```
181
+
182
+ Pour lancer les tests :
183
+
184
+ ```bash
185
+ bundle exec rspec
186
+ ```
187
+
188
+ Pour lancer RuboCop :
189
+
190
+ ```bash
191
+ bundle exec rubocop
192
+ ```
193
+
194
+ ## Contribution
195
+
196
+ Les contributions sont les bienvenues ! Merci de :
197
+ 1. Forker le projet
198
+ 2. Créer une branche pour votre feature
199
+ 3. Commiter vos changements
200
+ 4. Pousser vers la branche
201
+ 5. Ouvrir une Pull Request
202
+
203
+ ## Licence
204
+
205
+ Cette gem est disponible sous la licence MIT.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Exemple d'utilisation de base de la gem MistralTranslator
5
+ require "mistral_translator"
6
+
7
+ # Configuration
8
+ MistralTranslator.configure do |config|
9
+ config.api_key = ENV["MISTRAL_API_KEY"] || "your_api_key_here"
10
+ # config.api_url = 'https://api.mistral.ai' # optionnel
11
+ # config.model = 'mistral-small' # optionnel
12
+ end
13
+
14
+ puts "=== MistralTranslator Examples ==="
15
+ puts "Version: #{MistralTranslator.version}"
16
+ puts
17
+
18
+ # Vérification de la santé de l'API
19
+ puts "🔍 Health Check:"
20
+ health = MistralTranslator.health_check
21
+ puts "Status: #{health[:status]} - #{health[:message]}"
22
+ puts
23
+
24
+ # Exemple 1: Traduction simple
25
+ puts "📝 Traduction simple:"
26
+ begin
27
+ result = MistralTranslator.translate("Bonjour le monde", from: "fr", to: "en")
28
+ puts "FR → EN: 'Bonjour le monde' → '#{result}'"
29
+ rescue MistralTranslator::Error => e
30
+ puts "Erreur: #{e.message}"
31
+ end
32
+ puts
33
+
34
+ # Exemple 2: Traduction vers plusieurs langues
35
+ puts "🌍 Traduction vers plusieurs langues:"
36
+ begin
37
+ results = MistralTranslator.translate_to_multiple(
38
+ "Hello world",
39
+ from: "en",
40
+ to: %w[fr es de]
41
+ )
42
+
43
+ results.each do |locale, translation|
44
+ puts "EN → #{locale.upcase}: '#{translation}'"
45
+ end
46
+ rescue MistralTranslator::Error => e
47
+ puts "Erreur: #{e.message}"
48
+ end
49
+ puts
50
+
51
+ # Exemple 3: Traduction en lot
52
+ puts "📦 Traduction en lot:"
53
+ begin
54
+ texts = ["Good morning", "Good afternoon", "Good evening"]
55
+ results = MistralTranslator.translate_batch(texts, from: "en", to: "fr")
56
+
57
+ results.each do |index, translation|
58
+ puts "#{index}: '#{texts[index]}' → '#{translation}'"
59
+ end
60
+ rescue MistralTranslator::Error => e
61
+ puts "Erreur: #{e.message}"
62
+ end
63
+ puts
64
+
65
+ # Exemple 4: Auto-détection de langue
66
+ puts "🔍 Auto-détection de langue:"
67
+ begin
68
+ result = MistralTranslator.translate_auto("¡Hola mundo!", to: "en")
69
+ puts "Auto-détection → EN: '#{result}'"
70
+ rescue MistralTranslator::Error => e
71
+ puts "Erreur: #{e.message}"
72
+ end
73
+ puts
74
+
75
+ # Exemple 5: Résumé de texte
76
+ puts "📄 Résumé de texte:"
77
+ long_text = <<~TEXT
78
+ Ruby on Rails est un framework de développement web écrit en Ruby qui suit le paradigme#{" "}
79
+ Modèle-Vue-Contrôleur (MVC). Il privilégie la convention plutôt que la configuration,#{" "}
80
+ ce qui permet aux développeurs de créer rapidement des applications web robustes.#{" "}
81
+ Rails comprend de nombreux outils intégrés comme Active Record pour l'ORM,#{" "}
82
+ Action View pour les templates, Action Controller pour la logique métier,#{" "}
83
+ et bien d'autres composants. Le framework a été créé par David Heinemeier Hansson#{" "}
84
+ en 2004 et continue d'évoluer avec une communauté active de développeurs.
85
+ TEXT
86
+
87
+ begin
88
+ summary = MistralTranslator.summarize(long_text, language: "fr", max_words: 50)
89
+ puts "Texte original: #{long_text.length} caractères"
90
+ puts "Résumé (50 mots max): #{summary}"
91
+ rescue MistralTranslator::Error => e
92
+ puts "Erreur: #{e.message}"
93
+ end
94
+ puts
95
+
96
+ # Exemple 6: Résumé avec traduction
97
+ puts "🔄 Résumé + Traduction:"
98
+ begin
99
+ result = MistralTranslator.summarize_and_translate(
100
+ long_text,
101
+ from: "fr",
102
+ to: "en",
103
+ max_words: 75
104
+ )
105
+ puts "Résumé traduit EN (75 mots max): #{result}"
106
+ rescue MistralTranslator::Error => e
107
+ puts "Erreur: #{e.message}"
108
+ end
109
+ puts
110
+
111
+ # Exemple 7: Résumés par niveaux
112
+ puts "📊 Résumés par niveaux:"
113
+ begin
114
+ tiered = MistralTranslator.summarize_tiered(
115
+ long_text,
116
+ language: "fr",
117
+ short: 25,
118
+ medium: 75,
119
+ long: 150
120
+ )
121
+
122
+ puts "Court (25 mots): #{tiered[:short]}"
123
+ puts "Moyen (75 mots): #{tiered[:medium]}"
124
+ puts "Long (150 mots): #{tiered[:long]}"
125
+ rescue MistralTranslator::Error => e
126
+ puts "Erreur: #{e.message}"
127
+ end
128
+ puts
129
+
130
+ # Informations sur les langues supportées
131
+ puts "🌐 Langues supportées:"
132
+ puts MistralTranslator.supported_languages
133
+ puts
134
+
135
+ puts "✅ Exemples terminés!"
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+ require_relative "logger"
7
+
8
+ module MistralTranslator
9
+ class Client
10
+ def initialize(api_key: nil)
11
+ @api_key = api_key || MistralTranslator.configuration.api_key!
12
+ @base_uri = MistralTranslator.configuration.api_url
13
+ @model = MistralTranslator.configuration.model
14
+ @retry_delays = MistralTranslator.configuration.retry_delays
15
+ end
16
+
17
+ def complete(prompt, max_tokens: nil, temperature: nil)
18
+ response = make_request_with_retry(prompt, max_tokens, temperature)
19
+ parsed_response = JSON.parse(response.body)
20
+
21
+ content = parsed_response.dig("choices", 0, "message", "content")
22
+ raise InvalidResponseError, "No content in API response" if content.nil? || content.empty?
23
+
24
+ content
25
+ rescue JSON::ParserError => e
26
+ raise InvalidResponseError, "Invalid JSON in API response: #{e.message}"
27
+ rescue NoMethodError => e
28
+ raise InvalidResponseError, "Invalid response structure: #{e.message}"
29
+ end
30
+
31
+ def chat(prompt, max_tokens: nil, temperature: nil)
32
+ response = make_request_with_retry(prompt, max_tokens, temperature)
33
+ parsed_response = JSON.parse(response.body)
34
+
35
+ parsed_response.dig("choices", 0, "message", "content")
36
+ rescue JSON::ParserError => e
37
+ raise InvalidResponseError, "JSON parse error: #{e.message}"
38
+ rescue NoMethodError => e
39
+ raise InvalidResponseError, "Invalid response structure: #{e.message}"
40
+ end
41
+
42
+ private
43
+
44
+ def make_request_with_retry(prompt, max_tokens, temperature, attempt = 0)
45
+ response = make_request(prompt, max_tokens, temperature)
46
+
47
+ # Vérifier les erreurs dans la réponse
48
+ check_response_for_errors(response)
49
+
50
+ if rate_limit_exceeded?(response)
51
+ handle_rate_limit(prompt, max_tokens, temperature, attempt)
52
+ else
53
+ response
54
+ end
55
+ end
56
+
57
+ def make_request(prompt, max_tokens, temperature)
58
+ uri = URI("#{@base_uri}/v1/chat/completions")
59
+
60
+ request_body = build_request_body(prompt, max_tokens, temperature)
61
+
62
+ http = Net::HTTP.new(uri.host, uri.port)
63
+ http.use_ssl = (uri.scheme == "https")
64
+ http.read_timeout = 60 # 60 secondes de timeout
65
+
66
+ request = Net::HTTP::Post.new(uri.path, headers)
67
+ request.body = request_body.to_json
68
+
69
+ response = http.request(request)
70
+ log_request_response(request_body, response)
71
+
72
+ response
73
+ rescue Net::ReadTimeout, Timeout::Error => e
74
+ raise ApiError, "Request timeout: #{e.message}"
75
+ rescue Net::HTTPError => e
76
+ raise ApiError, "HTTP error: #{e.message}"
77
+ end
78
+
79
+ def build_request_body(prompt, max_tokens, temperature)
80
+ body = {
81
+ model: @model,
82
+ messages: [{ role: "user", content: prompt }]
83
+ }
84
+
85
+ body[:max_tokens] = max_tokens if max_tokens
86
+ body[:temperature] = temperature if temperature
87
+
88
+ body
89
+ end
90
+
91
+ def headers
92
+ {
93
+ "Authorization" => "Bearer #{@api_key}",
94
+ "Content-Type" => "application/json",
95
+ "User-Agent" => "mistral-translator-gem/#{MistralTranslator::VERSION}"
96
+ }
97
+ end
98
+
99
+ def check_response_for_errors(response)
100
+ case response.code.to_i
101
+ when 401
102
+ raise AuthenticationError, "Invalid API key"
103
+ when 429
104
+ # Rate limit sera géré séparément
105
+ nil
106
+ when 400..499
107
+ raise ApiError, "Client error (#{response.code})}"
108
+ when 500..599
109
+ raise ApiError, "Server error (#{response.code})}"
110
+ end
111
+ end
112
+
113
+ def rate_limit_exceeded?(response)
114
+ return true if response.code.to_i == 429
115
+
116
+ return false unless response.code.to_i == 200
117
+
118
+ body_content = response.body.to_s
119
+ return false if body_content.length > 1000
120
+
121
+ body_content.match?(/rate.?limit|quota.?exceeded/i)
122
+ end
123
+
124
+ def handle_rate_limit(prompt, max_tokens, temperature, attempt)
125
+ unless attempt < @retry_delays.length
126
+ raise RateLimitError, "API rate limit exceeded after #{@retry_delays.length} retries"
127
+ end
128
+
129
+ wait_time = @retry_delays[attempt]
130
+ log_rate_limit_retry(wait_time, attempt)
131
+ sleep(wait_time)
132
+ make_request_with_retry(prompt, max_tokens, temperature, attempt + 1)
133
+ end
134
+
135
+ def log_request_response(_request_body, response)
136
+ # Log seulement si mode verbose activé
137
+ Logger.debug_if_verbose("Request sent to API", sensitive: true)
138
+ Logger.debug_if_verbose("Response received: #{response.code}", sensitive: false)
139
+ end
140
+
141
+ def log_rate_limit_retry(wait_time, attempt)
142
+ message = "Rate limit exceeded, retrying in #{wait_time} seconds (attempt #{attempt + 1})"
143
+ # Log une seule fois par session pour éviter le spam
144
+ Logger.warn_once(message, key: "rate_limit_retry", sensitive: false, ttl: 60)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MistralTranslator
4
+ class Configuration
5
+ attr_accessor :api_key, :api_url, :model, :default_max_tokens, :default_temperature, :retry_delays
6
+
7
+ def initialize
8
+ @api_key = nil
9
+ @api_url = "https://api.mistral.ai"
10
+ @model = "mistral-small"
11
+ @default_max_tokens = nil
12
+ @default_temperature = nil
13
+ @retry_delays = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
14
+ end
15
+
16
+ def api_key!
17
+ if @api_key.nil?
18
+ raise ConfigurationError,
19
+ "API key is required. Set it with MistralTranslator.configure { |c| c.api_key = 'your_key' }"
20
+ end
21
+
22
+ @api_key
23
+ end
24
+ end
25
+
26
+ class << self
27
+ def configuration
28
+ @configuration ||= Configuration.new
29
+ end
30
+
31
+ def configure
32
+ yield(configuration)
33
+ end
34
+
35
+ def reset_configuration!
36
+ @configuration = Configuration.new
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MistralTranslator
4
+ class Error < StandardError; end
5
+
6
+ class ConfigurationError < Error; end
7
+
8
+ class ApiError < Error
9
+ attr_reader :response, :status_code
10
+
11
+ def initialize(message, response = nil, status_code = nil)
12
+ super(message)
13
+ @response = response
14
+ @status_code = status_code
15
+ end
16
+ end
17
+
18
+ class RateLimitError < ApiError
19
+ def initialize(message = "API rate limit exceeded", response = nil, status_code = 429)
20
+ super
21
+ end
22
+ end
23
+
24
+ class AuthenticationError < ApiError
25
+ def initialize(message = "Invalid API key", response = nil, status_code = 401)
26
+ super
27
+ end
28
+ end
29
+
30
+ class InvalidResponseError < Error
31
+ attr_reader :raw_response
32
+
33
+ def initialize(message, raw_response = nil)
34
+ super(message)
35
+ @raw_response = raw_response
36
+ end
37
+ end
38
+
39
+ class EmptyTranslationError < Error
40
+ def initialize(message = "Empty translation received from API")
41
+ super
42
+ end
43
+ end
44
+
45
+ class UnsupportedLanguageError < Error
46
+ attr_reader :language
47
+
48
+ def initialize(language)
49
+ @language = language
50
+ super("Unsupported language: #{language}")
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MistralTranslator
4
+ module LocaleHelper
5
+ SUPPORTED_LANGUAGES = {
6
+ "fr" => "français",
7
+ "en" => "english",
8
+ "es" => "español",
9
+ "pt" => "português",
10
+ "de" => "deutsch",
11
+ "it" => "italiano",
12
+ "nl" => "nederlands",
13
+ "ru" => "русский",
14
+ "mg" => "malagasy",
15
+ "ja" => "日本語",
16
+ "ko" => "한국어",
17
+ "zh" => "中文",
18
+ "ar" => "العربية"
19
+ }.freeze
20
+
21
+ # Mapping des noms alternatifs vers les codes de langue
22
+ LANGUAGE_ALIASES = {
23
+ "french" => "fr",
24
+ "german" => "de",
25
+ "spanish" => "es",
26
+ "portuguese" => "pt",
27
+ "italian" => "it",
28
+ "dutch" => "nl",
29
+ "russian" => "ru",
30
+ "malagasy" => "mg",
31
+ "japanese" => "ja",
32
+ "korean" => "ko",
33
+ "chinese" => "zh",
34
+ "arabic" => "ar"
35
+ }.freeze
36
+
37
+ class << self
38
+ def locale_to_language(locale)
39
+ locale_code = normalize_locale(locale)
40
+ SUPPORTED_LANGUAGES[locale_code] || locale_code.to_s
41
+ end
42
+
43
+ def language_to_locale(language)
44
+ normalized_language = language.to_s.downcase.strip
45
+
46
+ # Vérifier d'abord les alias
47
+ return LANGUAGE_ALIASES[normalized_language] if LANGUAGE_ALIASES.key?(normalized_language)
48
+
49
+ # Recherche directe par valeur
50
+ found_locale = SUPPORTED_LANGUAGES.find { |_, lang| lang.downcase == normalized_language }
51
+ return found_locale[0] if found_locale
52
+
53
+ # Retourne la langue telle quelle si pas trouvée
54
+ normalized_language
55
+ end
56
+
57
+ def supported_locales
58
+ SUPPORTED_LANGUAGES.keys
59
+ end
60
+
61
+ def supported_languages
62
+ SUPPORTED_LANGUAGES.values
63
+ end
64
+
65
+ def locale_supported?(locale)
66
+ SUPPORTED_LANGUAGES.key?(normalize_locale(locale))
67
+ end
68
+
69
+ def validate_locale!(locale)
70
+ normalized = normalize_locale(locale)
71
+ raise UnsupportedLanguageError, normalized unless locale_supported?(normalized)
72
+
73
+ normalized
74
+ end
75
+
76
+ def normalize_locale(locale)
77
+ return "" if locale.nil? || locale.to_s.empty?
78
+
79
+ normalized = locale.to_s.downcase
80
+ return "" if normalized.empty?
81
+
82
+ # Diviser par "-" et prendre la première partie
83
+ first_part = normalized.split("-").first
84
+ return "" if first_part.nil? || first_part.empty?
85
+
86
+ # Diviser par "_" et prendre la première partie
87
+ result = first_part.split("_").first
88
+ return "" if result.nil? || result.empty?
89
+
90
+ result
91
+ end
92
+
93
+ # Méthode pour obtenir une liste formatée des langues supportées
94
+ def supported_languages_list
95
+ SUPPORTED_LANGUAGES.map { |code, name| "#{code} (#{name})" }.join(", ")
96
+ end
97
+ end
98
+ end
99
+ end