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
@@ -4,144 +4,147 @@ require "net/http"
|
|
4
4
|
require "json"
|
5
5
|
require "uri"
|
6
6
|
require_relative "logger"
|
7
|
+
require_relative "client_helpers"
|
8
|
+
require_relative "security"
|
7
9
|
|
8
10
|
module MistralTranslator
|
9
11
|
class Client
|
10
|
-
|
12
|
+
include ClientHelpers::RequestHandler
|
13
|
+
include ClientHelpers::ErrorHandler
|
14
|
+
include ClientHelpers::BatchHandler
|
15
|
+
include ClientHelpers::LoggingHelper
|
16
|
+
|
17
|
+
def initialize(api_key: nil, rate_limiter: nil)
|
11
18
|
@api_key = api_key || MistralTranslator.configuration.api_key!
|
12
19
|
@base_uri = MistralTranslator.configuration.api_url
|
13
20
|
@model = MistralTranslator.configuration.model
|
14
21
|
@retry_delays = MistralTranslator.configuration.retry_delays
|
22
|
+
@rate_limiter = rate_limiter || Security::BasicRateLimiter.new
|
15
23
|
end
|
16
24
|
|
17
|
-
def complete(prompt, max_tokens: nil, temperature: nil)
|
18
|
-
|
19
|
-
|
25
|
+
def complete(prompt, max_tokens: nil, temperature: nil, context: {})
|
26
|
+
# Vérifier le rate limit avant de faire la requête
|
27
|
+
@rate_limiter.wait_and_record!
|
20
28
|
|
21
|
-
|
22
|
-
|
29
|
+
ctx = (context || {}).merge(operation: :complete)
|
30
|
+
start_time = Time.now
|
31
|
+
trigger_translation_start_callback(ctx, prompt)
|
32
|
+
|
33
|
+
response = make_request_with_retry(prompt, max_tokens, temperature, ctx)
|
34
|
+
content = extract_content_from_response(response)
|
23
35
|
|
36
|
+
trigger_translation_complete_callback(ctx, prompt, content, start_time)
|
24
37
|
content
|
25
38
|
rescue JSON::ParserError => e
|
26
|
-
|
39
|
+
handle_json_parse_error(e, ctx)
|
27
40
|
rescue NoMethodError => e
|
28
|
-
|
41
|
+
handle_response_structure_error(e, ctx)
|
29
42
|
end
|
30
43
|
|
31
|
-
def chat(prompt, max_tokens: nil, temperature: nil)
|
32
|
-
|
33
|
-
|
44
|
+
def chat(prompt, max_tokens: nil, temperature: nil, context: {})
|
45
|
+
# Vérifier le rate limit avant de faire la requête
|
46
|
+
@rate_limiter.wait_and_record!
|
34
47
|
|
35
|
-
|
48
|
+
ctx = (context || {}).merge(operation: :chat)
|
49
|
+
start_time = Time.now
|
50
|
+
trigger_translation_start_callback(ctx, prompt)
|
51
|
+
|
52
|
+
response = make_request_with_retry(prompt, max_tokens, temperature, ctx)
|
53
|
+
content = extract_chat_content_from_response(response)
|
54
|
+
|
55
|
+
trigger_translation_complete_callback(ctx, prompt, content, start_time)
|
56
|
+
content
|
36
57
|
rescue JSON::ParserError => e
|
37
|
-
|
58
|
+
handle_json_parse_error(e, ctx)
|
38
59
|
rescue NoMethodError => e
|
39
|
-
|
60
|
+
handle_response_structure_error(e, ctx)
|
40
61
|
end
|
41
62
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
63
|
+
# Nouvelle méthode pour traduction par batch optimisée
|
64
|
+
def translate_batch(requests, batch_size: 5)
|
65
|
+
start_time = Time.now
|
66
|
+
results = []
|
67
|
+
success_count = 0
|
68
|
+
error_count = 0
|
69
|
+
|
70
|
+
requests.each_slice(batch_size) do |batch|
|
71
|
+
batch_results = process_batch_slice(batch)
|
72
|
+
results.concat(batch_results[:results])
|
73
|
+
success_count += batch_results[:success_count]
|
74
|
+
error_count += batch_results[:error_count]
|
75
|
+
|
76
|
+
# Délai entre les batches pour éviter les rate limits
|
77
|
+
sleep(2) unless batch == requests.last(batch_size)
|
54
78
|
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
79
|
|
62
|
-
|
63
|
-
|
64
|
-
|
80
|
+
total_duration = Time.now - start_time
|
81
|
+
MistralTranslator.configuration.trigger_batch_complete(
|
82
|
+
requests.size,
|
83
|
+
total_duration,
|
84
|
+
success_count,
|
85
|
+
error_count
|
86
|
+
)
|
65
87
|
|
66
|
-
|
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}"
|
88
|
+
results
|
77
89
|
end
|
78
90
|
|
79
|
-
|
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
|
91
|
+
private
|
87
92
|
|
88
|
-
|
93
|
+
def trigger_translation_start_callback(context, prompt)
|
94
|
+
MistralTranslator.configuration.trigger_translation_start(
|
95
|
+
context[:from_locale],
|
96
|
+
context[:to_locale],
|
97
|
+
prompt&.length || 0
|
98
|
+
)
|
89
99
|
end
|
90
100
|
|
91
|
-
def
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
"User-Agent" => "mistral-translator-gem/#{MistralTranslator::VERSION}"
|
96
|
-
}
|
97
|
-
end
|
101
|
+
def extract_content_from_response(response)
|
102
|
+
parsed_response = JSON.parse(response.body)
|
103
|
+
content = parsed_response.dig("choices", 0, "message", "content")
|
104
|
+
raise InvalidResponseError, "No content in API response" if content.nil? || content.empty?
|
98
105
|
|
99
|
-
|
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
|
106
|
+
content
|
111
107
|
end
|
112
108
|
|
113
|
-
def
|
114
|
-
|
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)
|
109
|
+
def extract_chat_content_from_response(response)
|
110
|
+
parsed_response = JSON.parse(response.body)
|
111
|
+
parsed_response.dig("choices", 0, "message", "content")
|
122
112
|
end
|
123
113
|
|
124
|
-
def
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
114
|
+
def trigger_translation_complete_callback(context, prompt, content, start_time)
|
115
|
+
duration = Time.now - start_time
|
116
|
+
MistralTranslator.configuration.trigger_translation_complete(
|
117
|
+
context[:from_locale],
|
118
|
+
context[:to_locale],
|
119
|
+
prompt&.length || 0,
|
120
|
+
content&.length || 0,
|
121
|
+
duration
|
122
|
+
)
|
133
123
|
end
|
134
124
|
|
135
|
-
def
|
136
|
-
|
137
|
-
|
138
|
-
|
125
|
+
def handle_json_parse_error(error, context)
|
126
|
+
MistralTranslator.configuration.trigger_translation_error(
|
127
|
+
context[:from_locale],
|
128
|
+
context[:to_locale],
|
129
|
+
error,
|
130
|
+
context[:attempt] || 0
|
131
|
+
)
|
132
|
+
message = if context[:operation] == :chat
|
133
|
+
"JSON parse error: #{error.message}"
|
134
|
+
else
|
135
|
+
"Invalid JSON in API response: #{error.message}"
|
136
|
+
end
|
137
|
+
raise InvalidResponseError, message
|
139
138
|
end
|
140
139
|
|
141
|
-
def
|
142
|
-
|
143
|
-
|
144
|
-
|
140
|
+
def handle_response_structure_error(error, context)
|
141
|
+
MistralTranslator.configuration.trigger_translation_error(
|
142
|
+
context[:from_locale],
|
143
|
+
context[:to_locale],
|
144
|
+
error,
|
145
|
+
context[:attempt] || 0
|
146
|
+
)
|
147
|
+
raise InvalidResponseError, "Invalid response structure: #{error.message}"
|
145
148
|
end
|
146
149
|
end
|
147
150
|
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MistralTranslator
|
4
|
+
module ClientHelpers
|
5
|
+
# Helper pour la gestion des requêtes HTTP
|
6
|
+
module RequestHandler
|
7
|
+
def make_request(prompt, max_tokens, temperature)
|
8
|
+
uri = URI("#{@base_uri}/v1/chat/completions")
|
9
|
+
|
10
|
+
request_body = build_request_body(prompt, max_tokens, temperature)
|
11
|
+
|
12
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
13
|
+
http.use_ssl = (uri.scheme == "https")
|
14
|
+
http.read_timeout = 60 # 60 secondes de timeout
|
15
|
+
|
16
|
+
request = Net::HTTP::Post.new(uri.path, headers)
|
17
|
+
request.body = request_body.to_json
|
18
|
+
|
19
|
+
response = http.request(request)
|
20
|
+
log_request_response(request_body, response)
|
21
|
+
|
22
|
+
response
|
23
|
+
rescue Net::ReadTimeout, Timeout::Error => e
|
24
|
+
raise ApiError, "Request timeout: #{e.message}"
|
25
|
+
rescue Net::HTTPError => e
|
26
|
+
raise ApiError, "HTTP error: #{e.message}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_request_body(prompt, max_tokens, temperature)
|
30
|
+
body = {
|
31
|
+
model: @model,
|
32
|
+
messages: [{ role: "user", content: prompt }]
|
33
|
+
}
|
34
|
+
|
35
|
+
body[:max_tokens] = max_tokens if max_tokens
|
36
|
+
body[:temperature] = temperature if temperature
|
37
|
+
|
38
|
+
body
|
39
|
+
end
|
40
|
+
|
41
|
+
def headers
|
42
|
+
{
|
43
|
+
"Authorization" => "Bearer #{@api_key}",
|
44
|
+
"Content-Type" => "application/json",
|
45
|
+
"User-Agent" => "mistral-translator-gem/#{MistralTranslator::VERSION}"
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Helper pour la gestion des erreurs et retry
|
51
|
+
module ErrorHandler
|
52
|
+
def make_request_with_retry(prompt, max_tokens, temperature, context, attempt = 0)
|
53
|
+
context[:attempt] = attempt
|
54
|
+
response = make_request(prompt, max_tokens, temperature)
|
55
|
+
|
56
|
+
# Vérifier les erreurs dans la réponse
|
57
|
+
check_response_for_errors(response)
|
58
|
+
|
59
|
+
if rate_limit_exceeded?(response)
|
60
|
+
handle_rate_limit(prompt, max_tokens, temperature, context, attempt)
|
61
|
+
else
|
62
|
+
response
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def check_response_for_errors(response)
|
67
|
+
case response.code.to_i
|
68
|
+
when 401
|
69
|
+
raise AuthenticationError, "Invalid API key"
|
70
|
+
when 429
|
71
|
+
# Rate limit sera géré séparément
|
72
|
+
nil
|
73
|
+
when 400..499
|
74
|
+
raise ApiError, "Client error (#{response.code})}"
|
75
|
+
when 500..599
|
76
|
+
raise ApiError, "Server error (#{response.code})}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def rate_limit_exceeded?(response)
|
81
|
+
return true if response.code.to_i == 429
|
82
|
+
|
83
|
+
return false unless response.code.to_i == 200
|
84
|
+
|
85
|
+
body_content = response.body.to_s
|
86
|
+
return false if body_content.length > 1000
|
87
|
+
|
88
|
+
body_content.match?(/rate.?limit|quota.?exceeded/i)
|
89
|
+
end
|
90
|
+
|
91
|
+
def handle_rate_limit(prompt, max_tokens, temperature, context, attempt)
|
92
|
+
unless attempt < @retry_delays.length
|
93
|
+
raise RateLimitError, "API rate limit exceeded after #{@retry_delays.length} retries"
|
94
|
+
end
|
95
|
+
|
96
|
+
wait_time = @retry_delays[attempt]
|
97
|
+
|
98
|
+
# Trigger callback pour rate limit
|
99
|
+
MistralTranslator.configuration.trigger_rate_limit(
|
100
|
+
context[:from_locale],
|
101
|
+
context[:to_locale],
|
102
|
+
wait_time,
|
103
|
+
attempt + 1
|
104
|
+
)
|
105
|
+
|
106
|
+
log_rate_limit_retry(wait_time, attempt)
|
107
|
+
sleep(wait_time)
|
108
|
+
make_request_with_retry(prompt, max_tokens, temperature, context, attempt + 1)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Helper pour la gestion des batches
|
113
|
+
module BatchHandler
|
114
|
+
def process_batch_slice(batch)
|
115
|
+
results = []
|
116
|
+
success_count = 0
|
117
|
+
error_count = 0
|
118
|
+
|
119
|
+
batch.each do |request|
|
120
|
+
context = {
|
121
|
+
from_locale: request[:from],
|
122
|
+
to_locale: request[:to],
|
123
|
+
attempt: 0
|
124
|
+
}
|
125
|
+
|
126
|
+
result = complete(request[:prompt], context: context)
|
127
|
+
results << {
|
128
|
+
success: true,
|
129
|
+
result: result,
|
130
|
+
original_request: request
|
131
|
+
}
|
132
|
+
success_count += 1
|
133
|
+
rescue StandardError => e
|
134
|
+
results << {
|
135
|
+
success: false,
|
136
|
+
error: e.message,
|
137
|
+
original_request: request
|
138
|
+
}
|
139
|
+
error_count += 1
|
140
|
+
end
|
141
|
+
|
142
|
+
{ results: results, success_count: success_count, error_count: error_count }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Helper pour le logging
|
147
|
+
module LoggingHelper
|
148
|
+
def log_request_response(request_body, response)
|
149
|
+
# Log seulement si mode verbose activé
|
150
|
+
Logger.debug_if_verbose("Request sent to API: #{request_body.to_json}", sensitive: true)
|
151
|
+
Logger.debug_if_verbose("Response received: #{response.code}", sensitive: false)
|
152
|
+
end
|
153
|
+
|
154
|
+
def log_rate_limit_retry(wait_time, attempt)
|
155
|
+
message = "Rate limit exceeded, retrying in #{wait_time} seconds (attempt #{attempt + 1})"
|
156
|
+
# Log une seule fois par session pour éviter le spam
|
157
|
+
Logger.warn_once(message, key: "rate_limit_retry", sensitive: false, ttl: 60)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -2,7 +2,10 @@
|
|
2
2
|
|
3
3
|
module MistralTranslator
|
4
4
|
class Configuration
|
5
|
-
attr_accessor :api_key, :
|
5
|
+
attr_accessor :api_key, :model, :default_max_tokens, :default_temperature, :retry_delays,
|
6
|
+
:on_translation_start, :on_translation_complete, :on_translation_error,
|
7
|
+
:on_rate_limit, :on_batch_complete, :enable_metrics
|
8
|
+
attr_reader :api_url
|
6
9
|
|
7
10
|
def initialize
|
8
11
|
@api_key = nil
|
@@ -11,6 +14,24 @@ module MistralTranslator
|
|
11
14
|
@default_max_tokens = nil
|
12
15
|
@default_temperature = nil
|
13
16
|
@retry_delays = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
|
17
|
+
|
18
|
+
# Callbacks pour le monitoring et la customisation
|
19
|
+
@on_translation_start = nil
|
20
|
+
@on_translation_complete = nil
|
21
|
+
@on_translation_error = nil
|
22
|
+
@on_rate_limit = nil
|
23
|
+
@on_batch_complete = nil
|
24
|
+
@enable_metrics = false
|
25
|
+
|
26
|
+
# Métriques intégrées
|
27
|
+
@metrics = {
|
28
|
+
total_translations: 0,
|
29
|
+
total_characters: 0,
|
30
|
+
total_duration: 0.0,
|
31
|
+
rate_limits_hit: 0,
|
32
|
+
errors_count: 0,
|
33
|
+
translations_by_language: Hash.new(0)
|
34
|
+
}
|
14
35
|
end
|
15
36
|
|
16
37
|
def api_key!
|
@@ -21,6 +42,146 @@ module MistralTranslator
|
|
21
42
|
|
22
43
|
@api_key
|
23
44
|
end
|
45
|
+
|
46
|
+
def api_url=(url)
|
47
|
+
validate_api_url(url)
|
48
|
+
@api_url = url
|
49
|
+
end
|
50
|
+
|
51
|
+
# Méthodes pour les callbacks
|
52
|
+
def trigger_translation_start(from_locale, to_locale, text_length)
|
53
|
+
@on_translation_start&.call(from_locale, to_locale, text_length, Time.now)
|
54
|
+
|
55
|
+
return unless @enable_metrics
|
56
|
+
|
57
|
+
@metrics[:total_translations] += 1
|
58
|
+
@metrics[:total_characters] += text_length
|
59
|
+
@metrics[:translations_by_language]["#{from_locale}->#{to_locale}"] += 1
|
60
|
+
end
|
61
|
+
|
62
|
+
def trigger_translation_complete(from_locale, to_locale, original_length, translated_length, duration)
|
63
|
+
@on_translation_complete&.call(from_locale, to_locale, original_length, translated_length, duration)
|
64
|
+
|
65
|
+
return unless @enable_metrics
|
66
|
+
|
67
|
+
@metrics[:total_duration] += duration
|
68
|
+
end
|
69
|
+
|
70
|
+
def trigger_translation_error(from_locale, to_locale, error, attempt)
|
71
|
+
@on_translation_error&.call(from_locale, to_locale, error, attempt, Time.now)
|
72
|
+
|
73
|
+
return unless @enable_metrics
|
74
|
+
|
75
|
+
@metrics[:errors_count] += 1
|
76
|
+
end
|
77
|
+
|
78
|
+
def trigger_rate_limit(from_locale, to_locale, wait_time, attempt)
|
79
|
+
@on_rate_limit&.call(from_locale, to_locale, wait_time, attempt, Time.now)
|
80
|
+
|
81
|
+
return unless @enable_metrics
|
82
|
+
|
83
|
+
@metrics[:rate_limits_hit] += 1
|
84
|
+
end
|
85
|
+
|
86
|
+
def trigger_batch_complete(batch_size, total_duration, success_count, error_count)
|
87
|
+
@on_batch_complete&.call(batch_size, total_duration, success_count, error_count)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Métriques
|
91
|
+
def metrics
|
92
|
+
return {} unless @enable_metrics
|
93
|
+
|
94
|
+
@metrics.merge({
|
95
|
+
average_translation_time: if @metrics[:total_translations].positive?
|
96
|
+
(@metrics[:total_duration] / @metrics[:total_translations]).round(3)
|
97
|
+
else
|
98
|
+
0
|
99
|
+
end,
|
100
|
+
average_characters_per_translation: if @metrics[:total_translations].positive?
|
101
|
+
(@metrics[:total_characters] /
|
102
|
+
@metrics[:total_translations]).round(0)
|
103
|
+
else
|
104
|
+
0
|
105
|
+
end,
|
106
|
+
error_rate: if @metrics[:total_translations].positive?
|
107
|
+
((@metrics[:errors_count].to_f / @metrics[:total_translations]) * 100).round(2)
|
108
|
+
else
|
109
|
+
0
|
110
|
+
end
|
111
|
+
})
|
112
|
+
end
|
113
|
+
|
114
|
+
def reset_metrics!
|
115
|
+
@metrics = {
|
116
|
+
total_translations: 0,
|
117
|
+
total_characters: 0,
|
118
|
+
total_duration: 0.0,
|
119
|
+
rate_limits_hit: 0,
|
120
|
+
errors_count: 0,
|
121
|
+
translations_by_language: Hash.new(0)
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
# Configuration helpers pour les callbacks les plus communs
|
126
|
+
def setup_rails_logging
|
127
|
+
return unless defined?(Rails)
|
128
|
+
|
129
|
+
@on_translation_start = lambda { |from, to, length, _timestamp|
|
130
|
+
Rails.logger.info "[MistralTranslator] Starting translation #{from}->#{to} (#{length} chars)"
|
131
|
+
}
|
132
|
+
|
133
|
+
@on_translation_complete = lambda { |from, to, _orig_len, _trans_len, duration|
|
134
|
+
Rails.logger.info "[MistralTranslator] Completed #{from}->#{to} in #{duration.round(2)}s"
|
135
|
+
}
|
136
|
+
|
137
|
+
@on_translation_error = lambda { |from, to, error, attempt, _timestamp|
|
138
|
+
Rails.logger.error "[MistralTranslator] Error #{from}->#{to} (attempt #{attempt}): #{error.message}"
|
139
|
+
}
|
140
|
+
|
141
|
+
@on_rate_limit = lambda { |from, to, wait_time, attempt, _timestamp|
|
142
|
+
Rails.logger.warn "[MistralTranslator] Rate limit #{from}->#{to}, waiting #{wait_time}s (attempt #{attempt})"
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def validate_api_url(url)
|
149
|
+
uri = parse_uri(url)
|
150
|
+
validate_uri_scheme(uri, url)
|
151
|
+
validate_uri_host(uri)
|
152
|
+
validate_uri_path(uri)
|
153
|
+
validate_uri_port(uri)
|
154
|
+
end
|
155
|
+
|
156
|
+
def parse_uri(url)
|
157
|
+
require "uri"
|
158
|
+
URI.parse(url)
|
159
|
+
rescue URI::InvalidURIError
|
160
|
+
raise ConfigurationError, "Invalid API URL format: #{url}"
|
161
|
+
end
|
162
|
+
|
163
|
+
def validate_uri_scheme(uri, url)
|
164
|
+
raise ConfigurationError, "Invalid API URL format: #{url}" if uri.scheme.nil?
|
165
|
+
raise ConfigurationError, "API URL must use HTTPS protocol. Got: #{uri.scheme}" unless uri.scheme == "https"
|
166
|
+
end
|
167
|
+
|
168
|
+
def validate_uri_host(uri)
|
169
|
+
return if uri.host == "api.mistral.ai"
|
170
|
+
|
171
|
+
raise ConfigurationError, "Invalid API host. Expected 'api.mistral.ai', got: #{uri.host}"
|
172
|
+
end
|
173
|
+
|
174
|
+
def validate_uri_path(uri)
|
175
|
+
return if uri.path.nil? || uri.path.empty? || uri.path == "/"
|
176
|
+
|
177
|
+
raise ConfigurationError, "Invalid API path. Expected root path, got: #{uri.path}"
|
178
|
+
end
|
179
|
+
|
180
|
+
def validate_uri_port(uri)
|
181
|
+
return if uri.port.nil? || uri.port == 443
|
182
|
+
|
183
|
+
raise ConfigurationError, "Invalid API port. Expected 443 or default, got: #{uri.port}"
|
184
|
+
end
|
24
185
|
end
|
25
186
|
|
26
187
|
class << self
|
@@ -35,5 +196,14 @@ module MistralTranslator
|
|
35
196
|
def reset_configuration!
|
36
197
|
@configuration = Configuration.new
|
37
198
|
end
|
199
|
+
|
200
|
+
# Méthodes utilitaires pour les métriques
|
201
|
+
def metrics
|
202
|
+
configuration.metrics
|
203
|
+
end
|
204
|
+
|
205
|
+
def reset_metrics!
|
206
|
+
configuration.reset_metrics!
|
207
|
+
end
|
38
208
|
end
|
39
209
|
end
|
@@ -50,4 +50,20 @@ module MistralTranslator
|
|
50
50
|
super("Unsupported language: #{language}")
|
51
51
|
end
|
52
52
|
end
|
53
|
+
|
54
|
+
class SecurityError < Error
|
55
|
+
def initialize(message = "Security violation detected")
|
56
|
+
super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class RateLimitExceededError < Error
|
61
|
+
attr_reader :wait_time, :retry_after
|
62
|
+
|
63
|
+
def initialize(message = "Rate limit exceeded", wait_time: nil, retry_after: nil)
|
64
|
+
super(message)
|
65
|
+
@wait_time = wait_time
|
66
|
+
@retry_after = retry_after
|
67
|
+
end
|
68
|
+
end
|
53
69
|
end
|