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.
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "logger"
4
+
5
+ module MistralTranslator
6
+ class Translator
7
+ DEFAULT_RETRY_COUNT = 3
8
+ DEFAULT_RETRY_DELAY = 2
9
+
10
+ def initialize(client: nil)
11
+ @client = client || Client.new
12
+ end
13
+
14
+ # Traduction simple d'un texte vers une langue
15
+ def translate(text, from:, to:)
16
+ validate_inputs!(text, from, to)
17
+
18
+ source_locale = LocaleHelper.validate_locale!(from)
19
+ target_locale = LocaleHelper.validate_locale!(to)
20
+
21
+ translate_with_retry(text, source_locale, target_locale)
22
+ end
23
+
24
+ # Traduction vers plusieurs langues
25
+ def translate_to_multiple(text, from:, to:)
26
+ validate_translation_inputs!(text, from, to)
27
+
28
+ source_locale = LocaleHelper.validate_locale!(from)
29
+ target_locales = Array(to).map { |locale| LocaleHelper.validate_locale!(locale) }
30
+
31
+ results = {}
32
+
33
+ target_locales.each_with_index do |target_locale, index|
34
+ # Délai entre les requêtes, mais pas avant la première
35
+ sleep(DEFAULT_RETRY_DELAY) if index.positive?
36
+ results[target_locale] = translate_with_retry(text, source_locale, target_locale)
37
+ end
38
+
39
+ results
40
+ end
41
+
42
+ # Traduction en lot (plusieurs textes vers une langue)
43
+ def translate_batch(texts, from:, to:)
44
+ validate_batch_inputs!(texts, from, to)
45
+
46
+ source_locale = LocaleHelper.validate_locale!(from)
47
+ target_locale = LocaleHelper.validate_locale!(to)
48
+
49
+ # Pour des lots importants, on peut les découper
50
+ if texts.length > 10
51
+ translate_large_batch(texts, source_locale, target_locale)
52
+ else
53
+ translate_small_batch(texts, source_locale, target_locale)
54
+ end
55
+ end
56
+
57
+ # Auto-détection de la langue source (utilise l'API pour détecter)
58
+ def translate_auto(text, to:)
59
+ target_locale = LocaleHelper.validate_locale!(to)
60
+
61
+ # Premier appel pour détecter la langue
62
+ detection_prompt = build_language_detection_prompt(text)
63
+ detection_response = @client.complete(detection_prompt)
64
+ detected_language = parse_language_detection(detection_response)
65
+
66
+ # Puis traduction normale
67
+ translate(text, from: detected_language, to: target_locale)
68
+ end
69
+
70
+ private
71
+
72
+ def translate_with_retry(text, source_locale, target_locale, attempt = 0)
73
+ prompt = PromptBuilder.translation_prompt(text, source_locale, target_locale)
74
+ raw_response = @client.complete(prompt)
75
+
76
+ result = ResponseParser.parse_translation_response(raw_response)
77
+ raise EmptyTranslationError if result.nil? || result[:translated].nil?
78
+
79
+ result[:translated]
80
+ rescue EmptyTranslationError, InvalidResponseError => e
81
+ raise e unless attempt < DEFAULT_RETRY_COUNT
82
+
83
+ wait_time = DEFAULT_RETRY_DELAY * (2**attempt) # Backoff exponentiel
84
+ log_retry(e, attempt + 1, wait_time)
85
+ sleep(wait_time)
86
+ translate_with_retry(text, source_locale, target_locale, attempt + 1)
87
+ rescue RateLimitError => e
88
+ log_rate_limit_hit(source_locale, target_locale)
89
+ sleep(DEFAULT_RETRY_DELAY)
90
+ retry
91
+ end
92
+
93
+ def translate_small_batch(texts, source_locale, target_locale)
94
+ prompt = PromptBuilder.bulk_translation_prompt(texts, source_locale, target_locale)
95
+ raw_response = @client.complete(prompt)
96
+
97
+ results = ResponseParser.parse_bulk_translation_response(raw_response)
98
+
99
+ # Retourner un hash indexé par l'ordre original
100
+ results.each_with_object({}) do |result, hash|
101
+ original_index = result[:index] - 1 # L'API retourne 1-indexed
102
+ hash[original_index] = result[:translated]
103
+ end
104
+ end
105
+
106
+ def translate_large_batch(texts, source_locale, target_locale)
107
+ results = {}
108
+
109
+ texts.each_slice(10).with_index do |batch, batch_index|
110
+ sleep(DEFAULT_RETRY_DELAY) if batch_index.positive? # Délai entre les batches
111
+
112
+ batch_results = translate_small_batch(batch, source_locale, target_locale)
113
+
114
+ # Ajuster les index pour le batch
115
+ batch_results.each do |local_index, translation|
116
+ global_index = (batch_index * 10) + local_index
117
+ results[global_index] = translation
118
+ end
119
+ end
120
+
121
+ results
122
+ end
123
+
124
+ def build_language_detection_prompt(text)
125
+ PromptBuilder.language_detection_prompt(text)
126
+ end
127
+
128
+ def parse_language_detection(response)
129
+ json_content = response.match(/\{.*\}/m)&.[](0)
130
+ return "en" unless json_content # Défaut en anglais si détection échoue
131
+
132
+ data = JSON.parse(json_content)
133
+ detected = data["detected_language"]
134
+
135
+ LocaleHelper.locale_supported?(detected) ? detected : "en"
136
+ rescue JSON::ParserError
137
+ "en" # Défaut en anglais si parsing échoue
138
+ end
139
+
140
+ def validate_inputs!(text, from, to)
141
+ raise ArgumentError, "Text cannot be nil or empty" if text.nil? || text.empty?
142
+ raise ArgumentError, "Source language cannot be nil" if from.nil?
143
+ raise ArgumentError, "Target language cannot be nil" if to.nil?
144
+ raise ArgumentError, "Source and target languages cannot be the same" if from == to
145
+ end
146
+
147
+ def validate_translation_inputs!(text, from, to)
148
+ # Convertir to en array pour la validation
149
+ target_languages = Array(to)
150
+ raise ArgumentError, "Target languages cannot be empty" if target_languages.empty?
151
+
152
+ validate_inputs!(text, from, target_languages.first)
153
+ end
154
+
155
+ def validate_batch_inputs!(texts, from, to)
156
+ raise ArgumentError, "Texts array cannot be nil or empty" if texts.nil? || texts.empty?
157
+ raise ArgumentError, "Source language cannot be nil" if from.nil?
158
+ raise ArgumentError, "Target language cannot be nil" if to.nil?
159
+
160
+ texts.each_with_index do |text, index|
161
+ raise ArgumentError, "Text at index #{index} cannot be nil or empty" if text.nil? || text.empty?
162
+ end
163
+ end
164
+
165
+ def log_retry(error, attempt, wait_time)
166
+ message = "#{error.class.name}: #{error.message}. Retry #{attempt}/#{DEFAULT_RETRY_COUNT} in #{wait_time}s"
167
+ # Log une seule fois par type d'erreur pour éviter le spam
168
+ Logger.warn_once(message, key: "retry_#{error.class.name}", sensitive: false, ttl: 120)
169
+ end
170
+
171
+ def log_rate_limit_hit(source, target)
172
+ message = "Rate limit hit for translation #{source} -> #{target}, retrying..."
173
+ # Log une seule fois par paire de langues
174
+ Logger.warn_once(message, key: "rate_limit_#{source}_#{target}", sensitive: false, ttl: 300)
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MistralTranslator
4
+ VERSION = "0.1.0"
5
+
6
+ # Informations additionnelles sur la gem
7
+ API_VERSION = "v1"
8
+ SUPPORTED_MODEL = "mistral-small"
9
+
10
+ # Méthode pour obtenir des informations complètes sur la version
11
+ def self.version_info
12
+ {
13
+ gem_version: VERSION,
14
+ api_version: API_VERSION,
15
+ supported_model: SUPPORTED_MODEL,
16
+ ruby_version: RUBY_VERSION,
17
+ platform: RUBY_PLATFORM
18
+ }
19
+ end
20
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mistral_translator/version"
4
+ require_relative "mistral_translator/errors"
5
+ require_relative "mistral_translator/configuration"
6
+ require_relative "mistral_translator/locale_helper"
7
+ require_relative "mistral_translator/prompt_builder"
8
+ require_relative "mistral_translator/response_parser"
9
+ require_relative "mistral_translator/client"
10
+ require_relative "mistral_translator/translator"
11
+ require_relative "mistral_translator/summarizer"
12
+
13
+ module MistralTranslator
14
+ class << self
15
+ # Méthodes de convenance pour accès direct
16
+ def translate(text, from:, to:)
17
+ translator.translate(text, from: from, to: to)
18
+ end
19
+
20
+ def translate_to_multiple(text, from:, to:)
21
+ translator.translate_to_multiple(text, from: from, to: to)
22
+ end
23
+
24
+ def translate_batch(texts, from:, to:)
25
+ translator.translate_batch(texts, from: from, to: to)
26
+ end
27
+
28
+ def translate_auto(text, to:)
29
+ translator.translate_auto(text, to: to)
30
+ end
31
+
32
+ def summarize(text, language: "fr", max_words: 250)
33
+ summarizer.summarize(text, language: language, max_words: max_words)
34
+ end
35
+
36
+ def summarize_and_translate(text, from:, to:, max_words: 250)
37
+ summarizer.summarize_and_translate(text, from: from, to: to, max_words: max_words)
38
+ end
39
+
40
+ def summarize_to_multiple(text, languages:, max_words: 250)
41
+ summarizer.summarize_to_multiple(text, languages: languages, max_words: max_words)
42
+ end
43
+
44
+ def summarize_tiered(text, language: "fr", short: 50, medium: 150, long: 300)
45
+ summarizer.summarize_tiered(text, language: language, short: short, medium: medium, long: long)
46
+ end
47
+
48
+ # Méthodes utilitaires
49
+ def supported_languages
50
+ LocaleHelper.supported_languages_list
51
+ end
52
+
53
+ def supported_locales
54
+ LocaleHelper.supported_locales
55
+ end
56
+
57
+ def locale_supported?(locale)
58
+ LocaleHelper.locale_supported?(locale)
59
+ end
60
+
61
+ # Configuration
62
+ def configure
63
+ yield(configuration) if block_given?
64
+ end
65
+
66
+ def configuration
67
+ @configuration ||= Configuration.new
68
+ end
69
+
70
+ def reset_configuration!
71
+ @configuration = Configuration.new
72
+ @translator = nil
73
+ @summarizer = nil
74
+ @client = nil
75
+ end
76
+
77
+ # Version info
78
+ def version
79
+ VERSION
80
+ end
81
+
82
+ # Health check
83
+ def health_check
84
+ client.complete("Hello", max_tokens: 10)
85
+ { status: :ok, message: "API connection successful" }
86
+ rescue AuthenticationError
87
+ { status: :error, message: "Authentication failed - check your API key" }
88
+ rescue ApiError => e
89
+ { status: :error, message: "API error: #{e.message}" }
90
+ rescue StandardError => e
91
+ { status: :error, message: "Unexpected error: #{e.message}" }
92
+ end
93
+
94
+ private
95
+
96
+ def translator
97
+ @translator ||= Translator.new(client: client)
98
+ end
99
+
100
+ def summarizer
101
+ @summarizer ||= Summarizer.new(client: client)
102
+ end
103
+
104
+ def client
105
+ @client ||= Client.new
106
+ end
107
+ end
108
+
109
+ # Alias pour compatibilité
110
+ module Convenience
111
+ def self.included(base)
112
+ base.extend(ClassMethods)
113
+ end
114
+
115
+ module ClassMethods
116
+ def mistral_translate(text, from:, to:)
117
+ MistralTranslator.translate(text, from: from, to: to)
118
+ end
119
+
120
+ def mistral_summarize(text, language: "fr", max_words: 250)
121
+ MistralTranslator.summarize(text, language: language, max_words: max_words)
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ # Extensions optionnelles pour String
128
+ if ENV["MISTRAL_TRANSLATOR_EXTEND_STRING"]
129
+ class String
130
+ def mistral_translate(from:, to:)
131
+ MistralTranslator.translate(self, from: from, to: to)
132
+ end
133
+
134
+ def mistral_summarize(language: "fr", max_words: 250)
135
+ MistralTranslator.summarize(self, language: language, max_words: max_words)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,4 @@
1
+ module MistralTranslator
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mistral_translator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Peyochanchan
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: net-http
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.4'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.21'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.21'
68
+ - !ruby/object:Gem::Dependency
69
+ name: vcr
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '6.2'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '6.2'
82
+ - !ruby/object:Gem::Dependency
83
+ name: webmock
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.18'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.18'
96
+ description: Allows translating text into different languages and generating summaries
97
+ using the MistralAI API
98
+ email:
99
+ - cameleon24@outlook.fr
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".rspec"
105
+ - ".rubocop.yml"
106
+ - ".ruby-version"
107
+ - CHANGELOG.md
108
+ - CODE_OF_CONDUCT.md
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - examples/basic_usage.rb
113
+ - lib/mistral_translator.rb
114
+ - lib/mistral_translator/client.rb
115
+ - lib/mistral_translator/configuration.rb
116
+ - lib/mistral_translator/errors.rb
117
+ - lib/mistral_translator/locale_helper.rb
118
+ - lib/mistral_translator/logger.rb
119
+ - lib/mistral_translator/prompt_builder.rb
120
+ - lib/mistral_translator/response_parser.rb
121
+ - lib/mistral_translator/summarizer.rb
122
+ - lib/mistral_translator/translator.rb
123
+ - lib/mistral_translator/version.rb
124
+ - sig/mistral_translator.rbs
125
+ homepage: https://github.com/Peyochanchan/mistral_translator
126
+ licenses:
127
+ - MIT
128
+ metadata:
129
+ allowed_push_host: https://rubygems.org
130
+ homepage_uri: https://github.com/Peyochanchan/mistral_translator
131
+ changelog_uri: https://github.com/Peyochanchan/mistral_translator/blob/main/CHANGELOG.md
132
+ rubygems_mfa_required: 'true'
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: 3.2.0
141
+ required_rubygems_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ requirements: []
147
+ rubygems_version: 3.7.1
148
+ specification_version: 4
149
+ summary: Gem to translate and summarize text with Mistral API
150
+ test_files: []