truthound-dashboard 1.0.1__py3-none-any.whl → 1.1.0__py3-none-any.whl
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.
- truthound_dashboard/__init__.py +8 -1
- truthound_dashboard/cli.py +397 -0
- truthound_dashboard/translate/__init__.py +61 -0
- truthound_dashboard/translate/config_updater.py +327 -0
- truthound_dashboard/translate/exceptions.py +98 -0
- truthound_dashboard/translate/providers/__init__.py +49 -0
- truthound_dashboard/translate/providers/anthropic.py +135 -0
- truthound_dashboard/translate/providers/base.py +225 -0
- truthound_dashboard/translate/providers/mistral.py +138 -0
- truthound_dashboard/translate/providers/ollama.py +226 -0
- truthound_dashboard/translate/providers/openai.py +187 -0
- truthound_dashboard/translate/providers/registry.py +217 -0
- truthound_dashboard/translate/translator.py +443 -0
- {truthound_dashboard-1.0.1.dist-info → truthound_dashboard-1.1.0.dist-info}/METADATA +103 -4
- {truthound_dashboard-1.0.1.dist-info → truthound_dashboard-1.1.0.dist-info}/RECORD +18 -7
- {truthound_dashboard-1.0.1.dist-info → truthound_dashboard-1.1.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.0.1.dist-info → truthound_dashboard-1.1.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.0.1.dist-info → truthound_dashboard-1.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Intlayer configuration updater.
|
|
2
|
+
|
|
3
|
+
This module handles updating intlayer.config.ts and related frontend
|
|
4
|
+
configuration files when new languages are added.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from truthound_dashboard.translate.exceptions import ConfigUpdateError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class LocaleMapping:
|
|
18
|
+
"""Mapping between language codes and Intlayer locale constants."""
|
|
19
|
+
|
|
20
|
+
code: str # ISO 639-1 code (e.g., 'ja', 'zh')
|
|
21
|
+
intlayer_const: str # Intlayer constant (e.g., 'JAPANESE', 'CHINESE')
|
|
22
|
+
name: str # English name
|
|
23
|
+
native_name: str # Native name
|
|
24
|
+
flag: str # Flag emoji
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_code(cls, code: str) -> "LocaleMapping":
|
|
28
|
+
"""Create a LocaleMapping from a language code.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
code: ISO 639-1 language code
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
LocaleMapping instance
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If language code is not supported
|
|
38
|
+
"""
|
|
39
|
+
code = code.lower()
|
|
40
|
+
if code not in LOCALE_MAPPINGS:
|
|
41
|
+
supported = ", ".join(sorted(LOCALE_MAPPINGS.keys()))
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Unsupported language code: '{code}'. "
|
|
44
|
+
f"Supported codes: {supported}"
|
|
45
|
+
)
|
|
46
|
+
return LOCALE_MAPPINGS[code]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Mapping of ISO 639-1 codes to Intlayer locale info
|
|
50
|
+
LOCALE_MAPPINGS: dict[str, LocaleMapping] = {
|
|
51
|
+
"en": LocaleMapping("en", "ENGLISH", "English", "English", "🇺🇸"),
|
|
52
|
+
"ko": LocaleMapping("ko", "KOREAN", "Korean", "한국어", "🇰🇷"),
|
|
53
|
+
"ja": LocaleMapping("ja", "JAPANESE", "Japanese", "日本語", "🇯🇵"),
|
|
54
|
+
"zh": LocaleMapping("zh", "CHINESE", "Chinese", "中文", "🇨🇳"),
|
|
55
|
+
"de": LocaleMapping("de", "GERMAN", "German", "Deutsch", "🇩🇪"),
|
|
56
|
+
"fr": LocaleMapping("fr", "FRENCH", "French", "Français", "🇫🇷"),
|
|
57
|
+
"es": LocaleMapping("es", "SPANISH", "Spanish", "Español", "🇪🇸"),
|
|
58
|
+
"pt": LocaleMapping("pt", "PORTUGUESE", "Portuguese", "Português", "🇵🇹"),
|
|
59
|
+
"it": LocaleMapping("it", "ITALIAN", "Italian", "Italiano", "🇮🇹"),
|
|
60
|
+
"ru": LocaleMapping("ru", "RUSSIAN", "Russian", "Русский", "🇷🇺"),
|
|
61
|
+
"ar": LocaleMapping("ar", "ARABIC", "Arabic", "العربية", "🇸🇦"),
|
|
62
|
+
"hi": LocaleMapping("hi", "HINDI", "Hindi", "हिन्दी", "🇮🇳"),
|
|
63
|
+
"th": LocaleMapping("th", "THAI", "Thai", "ไทย", "🇹🇭"),
|
|
64
|
+
"vi": LocaleMapping("vi", "VIETNAMESE", "Vietnamese", "Tiếng Việt", "🇻🇳"),
|
|
65
|
+
"nl": LocaleMapping("nl", "DUTCH", "Dutch", "Nederlands", "🇳🇱"),
|
|
66
|
+
"pl": LocaleMapping("pl", "POLISH", "Polish", "Polski", "🇵🇱"),
|
|
67
|
+
"tr": LocaleMapping("tr", "TURKISH", "Turkish", "Türkçe", "🇹🇷"),
|
|
68
|
+
"sv": LocaleMapping("sv", "SWEDISH", "Swedish", "Svenska", "🇸🇪"),
|
|
69
|
+
"da": LocaleMapping("da", "DANISH", "Danish", "Dansk", "🇩🇰"),
|
|
70
|
+
"no": LocaleMapping("no", "NORWEGIAN", "Norwegian", "Norsk", "🇳🇴"),
|
|
71
|
+
"fi": LocaleMapping("fi", "FINNISH", "Finnish", "Suomi", "🇫🇮"),
|
|
72
|
+
"cs": LocaleMapping("cs", "CZECH", "Czech", "Čeština", "🇨🇿"),
|
|
73
|
+
"hu": LocaleMapping("hu", "HUNGARIAN", "Hungarian", "Magyar", "🇭🇺"),
|
|
74
|
+
"el": LocaleMapping("el", "GREEK", "Greek", "Ελληνικά", "🇬🇷"),
|
|
75
|
+
"he": LocaleMapping("he", "HEBREW", "Hebrew", "עברית", "🇮🇱"),
|
|
76
|
+
"id": LocaleMapping("id", "INDONESIAN", "Indonesian", "Bahasa Indonesia", "🇮🇩"),
|
|
77
|
+
"ms": LocaleMapping("ms", "MALAY", "Malay", "Bahasa Melayu", "🇲🇾"),
|
|
78
|
+
"uk": LocaleMapping("uk", "UKRAINIAN", "Ukrainian", "Українська", "🇺🇦"),
|
|
79
|
+
"ro": LocaleMapping("ro", "ROMANIAN", "Romanian", "Română", "🇷🇴"),
|
|
80
|
+
"bg": LocaleMapping("bg", "BULGARIAN", "Bulgarian", "Български", "🇧🇬"),
|
|
81
|
+
"hr": LocaleMapping("hr", "CROATIAN", "Croatian", "Hrvatski", "🇭🇷"),
|
|
82
|
+
"sk": LocaleMapping("sk", "SLOVAK", "Slovak", "Slovenčina", "🇸🇰"),
|
|
83
|
+
"sl": LocaleMapping("sl", "SLOVENIAN", "Slovenian", "Slovenščina", "🇸🇮"),
|
|
84
|
+
"et": LocaleMapping("et", "ESTONIAN", "Estonian", "Eesti", "🇪🇪"),
|
|
85
|
+
"lv": LocaleMapping("lv", "LATVIAN", "Latvian", "Latviešu", "🇱🇻"),
|
|
86
|
+
"lt": LocaleMapping("lt", "LITHUANIAN", "Lithuanian", "Lietuvių", "🇱🇹"),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class IntlayerConfigUpdater:
|
|
91
|
+
"""Updates Intlayer configuration files with new languages.
|
|
92
|
+
|
|
93
|
+
This class handles:
|
|
94
|
+
1. intlayer.config.ts - Add new locales to the locales array
|
|
95
|
+
2. providers/intlayer/config.ts - Add locale info for UI
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
updater = IntlayerConfigUpdater(frontend_dir)
|
|
99
|
+
updater.add_languages(["ja", "zh", "de"])
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, frontend_dir: Path | str) -> None:
|
|
103
|
+
"""Initialize the config updater.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
frontend_dir: Path to the frontend directory
|
|
107
|
+
"""
|
|
108
|
+
self.frontend_dir = Path(frontend_dir)
|
|
109
|
+
self.intlayer_config = self.frontend_dir / "intlayer.config.ts"
|
|
110
|
+
self.provider_config = (
|
|
111
|
+
self.frontend_dir / "src" / "providers" / "intlayer" / "config.ts"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def get_current_locales(self) -> list[str]:
|
|
115
|
+
"""Get currently configured locales from intlayer.config.ts.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
List of language codes currently configured
|
|
119
|
+
"""
|
|
120
|
+
if not self.intlayer_config.exists():
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
content = self.intlayer_config.read_text()
|
|
124
|
+
|
|
125
|
+
# Parse locales array: [Locales.ENGLISH, Locales.KOREAN]
|
|
126
|
+
pattern = r"locales:\s*\[([\s\S]*?)\]"
|
|
127
|
+
match = re.search(pattern, content)
|
|
128
|
+
if not match:
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
locales_str = match.group(1)
|
|
132
|
+
locales = []
|
|
133
|
+
|
|
134
|
+
for const_name in re.findall(r"Locales\.(\w+)", locales_str):
|
|
135
|
+
for code, mapping in LOCALE_MAPPINGS.items():
|
|
136
|
+
if mapping.intlayer_const == const_name:
|
|
137
|
+
locales.append(code)
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
return locales
|
|
141
|
+
|
|
142
|
+
def add_languages(self, language_codes: list[str]) -> list[str]:
|
|
143
|
+
"""Add new languages to the configuration.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
language_codes: List of ISO 639-1 language codes to add
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
List of actually added language codes (excluding already existing)
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ConfigUpdateError: If configuration update fails
|
|
153
|
+
"""
|
|
154
|
+
# Validate all language codes first
|
|
155
|
+
mappings = []
|
|
156
|
+
for code in language_codes:
|
|
157
|
+
try:
|
|
158
|
+
mappings.append(LocaleMapping.from_code(code))
|
|
159
|
+
except ValueError as e:
|
|
160
|
+
raise ConfigUpdateError(str(self.intlayer_config), str(e)) from e
|
|
161
|
+
|
|
162
|
+
# Get current locales
|
|
163
|
+
current_locales = self.get_current_locales()
|
|
164
|
+
new_codes = [m.code for m in mappings if m.code not in current_locales]
|
|
165
|
+
|
|
166
|
+
if not new_codes:
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
# Update intlayer.config.ts
|
|
170
|
+
self._update_intlayer_config(mappings)
|
|
171
|
+
|
|
172
|
+
# Update provider config.ts
|
|
173
|
+
self._update_provider_config(mappings)
|
|
174
|
+
|
|
175
|
+
return new_codes
|
|
176
|
+
|
|
177
|
+
def _update_intlayer_config(self, mappings: list[LocaleMapping]) -> None:
|
|
178
|
+
"""Update intlayer.config.ts with new locales.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
mappings: List of LocaleMapping objects to add
|
|
182
|
+
"""
|
|
183
|
+
if not self.intlayer_config.exists():
|
|
184
|
+
raise ConfigUpdateError(
|
|
185
|
+
str(self.intlayer_config),
|
|
186
|
+
"File does not exist",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
content = self.intlayer_config.read_text()
|
|
190
|
+
current_locales = self.get_current_locales()
|
|
191
|
+
|
|
192
|
+
# Build new locales array
|
|
193
|
+
all_locales = current_locales.copy()
|
|
194
|
+
for mapping in mappings:
|
|
195
|
+
if mapping.code not in all_locales:
|
|
196
|
+
all_locales.append(mapping.code)
|
|
197
|
+
|
|
198
|
+
# Generate locales string
|
|
199
|
+
locales_entries = []
|
|
200
|
+
for code in all_locales:
|
|
201
|
+
mapping = LOCALE_MAPPINGS[code]
|
|
202
|
+
locales_entries.append(f"Locales.{mapping.intlayer_const}")
|
|
203
|
+
|
|
204
|
+
new_locales_str = ", ".join(locales_entries)
|
|
205
|
+
|
|
206
|
+
# Replace locales array
|
|
207
|
+
pattern = r"(locales:\s*\[)[\s\S]*?(\])"
|
|
208
|
+
replacement = f"\\g<1>{new_locales_str}\\g<2>"
|
|
209
|
+
new_content = re.sub(pattern, replacement, content)
|
|
210
|
+
|
|
211
|
+
self.intlayer_config.write_text(new_content)
|
|
212
|
+
|
|
213
|
+
def _update_provider_config(self, mappings: list[LocaleMapping]) -> None:
|
|
214
|
+
"""Update providers/intlayer/config.ts with new locale info.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
mappings: List of LocaleMapping objects to add
|
|
218
|
+
"""
|
|
219
|
+
if not self.provider_config.exists():
|
|
220
|
+
raise ConfigUpdateError(
|
|
221
|
+
str(self.provider_config),
|
|
222
|
+
"File does not exist",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
content = self.provider_config.read_text()
|
|
226
|
+
current_locales = self.get_current_locales()
|
|
227
|
+
|
|
228
|
+
# Update SUPPORTED_LOCALES array
|
|
229
|
+
all_locales = current_locales.copy()
|
|
230
|
+
for mapping in mappings:
|
|
231
|
+
if mapping.code not in all_locales:
|
|
232
|
+
all_locales.append(mapping.code)
|
|
233
|
+
|
|
234
|
+
# Generate SUPPORTED_LOCALES entries
|
|
235
|
+
locales_entries = []
|
|
236
|
+
for code in all_locales:
|
|
237
|
+
mapping = LOCALE_MAPPINGS[code]
|
|
238
|
+
locales_entries.append(f"Locales.{mapping.intlayer_const}")
|
|
239
|
+
|
|
240
|
+
new_supported_str = ", ".join(locales_entries)
|
|
241
|
+
|
|
242
|
+
# Replace SUPPORTED_LOCALES
|
|
243
|
+
pattern = r"(export const SUPPORTED_LOCALES = \[)[\s\S]*?(\] as const)"
|
|
244
|
+
replacement = f"\\g<1>{new_supported_str}\\g<2>"
|
|
245
|
+
new_content = re.sub(pattern, replacement, content)
|
|
246
|
+
|
|
247
|
+
# Update LOCALE_INFO array
|
|
248
|
+
locale_info_entries = []
|
|
249
|
+
for code in all_locales:
|
|
250
|
+
mapping = LOCALE_MAPPINGS[code]
|
|
251
|
+
entry = (
|
|
252
|
+
f" {{\n"
|
|
253
|
+
f" code: Locales.{mapping.intlayer_const},\n"
|
|
254
|
+
f" name: '{mapping.name}',\n"
|
|
255
|
+
f" nativeName: '{mapping.native_name}',\n"
|
|
256
|
+
f" flag: '{mapping.flag}',\n"
|
|
257
|
+
f" }}"
|
|
258
|
+
)
|
|
259
|
+
locale_info_entries.append(entry)
|
|
260
|
+
|
|
261
|
+
new_locale_info_str = ",\n".join(locale_info_entries)
|
|
262
|
+
|
|
263
|
+
# Replace LOCALE_INFO array
|
|
264
|
+
pattern = r"(export const LOCALE_INFO: readonly LocaleInfo\[\] = \[)\n[\s\S]*?(\n\] as const)"
|
|
265
|
+
replacement = f"\\g<1>\n{new_locale_info_str},\\g<2>"
|
|
266
|
+
new_content = re.sub(pattern, replacement, new_content)
|
|
267
|
+
|
|
268
|
+
# Update getBrowserLocale function
|
|
269
|
+
self._update_browser_locale_function(new_content, all_locales)
|
|
270
|
+
|
|
271
|
+
self.provider_config.write_text(new_content)
|
|
272
|
+
|
|
273
|
+
def _update_browser_locale_function(
|
|
274
|
+
self,
|
|
275
|
+
content: str,
|
|
276
|
+
all_locales: list[str],
|
|
277
|
+
) -> str:
|
|
278
|
+
"""Update getBrowserLocale function with new language mappings.
|
|
279
|
+
|
|
280
|
+
This is a simplified approach - for complex cases, consider
|
|
281
|
+
using a proper AST parser.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
content: Current file content
|
|
285
|
+
all_locales: List of all locale codes
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Updated content
|
|
289
|
+
"""
|
|
290
|
+
# For now, we keep the basic structure and just ensure
|
|
291
|
+
# the function exists. Full implementation would require
|
|
292
|
+
# AST manipulation for clean results.
|
|
293
|
+
return content
|
|
294
|
+
|
|
295
|
+
def validate_config(self) -> bool:
|
|
296
|
+
"""Validate that configuration files exist and are valid.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
True if all configurations are valid
|
|
300
|
+
"""
|
|
301
|
+
if not self.intlayer_config.exists():
|
|
302
|
+
return False
|
|
303
|
+
if not self.provider_config.exists():
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
self.get_current_locales()
|
|
308
|
+
return True
|
|
309
|
+
except Exception:
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def get_supported_languages() -> list[dict[str, str]]:
|
|
314
|
+
"""Get list of all supported languages for translation.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
List of dicts with language info
|
|
318
|
+
"""
|
|
319
|
+
return [
|
|
320
|
+
{
|
|
321
|
+
"code": mapping.code,
|
|
322
|
+
"name": mapping.name,
|
|
323
|
+
"native_name": mapping.native_name,
|
|
324
|
+
"flag": mapping.flag,
|
|
325
|
+
}
|
|
326
|
+
for mapping in LOCALE_MAPPINGS.values()
|
|
327
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Custom exceptions for the translation module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TranslationError(Exception):
|
|
7
|
+
"""Base exception for translation-related errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProviderNotFoundError(TranslationError):
|
|
13
|
+
"""Raised when a requested AI provider is not found or not supported."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, provider_name: str, available: list[str] | None = None) -> None:
|
|
16
|
+
self.provider_name = provider_name
|
|
17
|
+
self.available = available or []
|
|
18
|
+
message = f"Provider '{provider_name}' not found."
|
|
19
|
+
if self.available:
|
|
20
|
+
message += f" Available providers: {', '.join(self.available)}"
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class APIKeyNotFoundError(TranslationError):
|
|
25
|
+
"""Raised when required API key is not set in environment."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, provider_name: str, env_var: str) -> None:
|
|
28
|
+
self.provider_name = provider_name
|
|
29
|
+
self.env_var = env_var
|
|
30
|
+
message = (
|
|
31
|
+
f"API key not found for provider '{provider_name}'. "
|
|
32
|
+
f"Please set the {env_var} environment variable."
|
|
33
|
+
)
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TranslationAPIError(TranslationError):
|
|
38
|
+
"""Raised when AI provider API call fails."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
provider_name: str,
|
|
43
|
+
message: str,
|
|
44
|
+
status_code: int | None = None,
|
|
45
|
+
response_body: str | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
self.provider_name = provider_name
|
|
48
|
+
self.status_code = status_code
|
|
49
|
+
self.response_body = response_body
|
|
50
|
+
full_message = f"API error from '{provider_name}': {message}"
|
|
51
|
+
if status_code:
|
|
52
|
+
full_message += f" (status: {status_code})"
|
|
53
|
+
super().__init__(full_message)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ContentParseError(TranslationError):
|
|
57
|
+
"""Raised when content file parsing fails."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, file_path: str, reason: str) -> None:
|
|
60
|
+
self.file_path = file_path
|
|
61
|
+
self.reason = reason
|
|
62
|
+
message = f"Failed to parse content file '{file_path}': {reason}"
|
|
63
|
+
super().__init__(message)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ConfigUpdateError(TranslationError):
|
|
67
|
+
"""Raised when intlayer config update fails."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, config_path: str, reason: str) -> None:
|
|
70
|
+
self.config_path = config_path
|
|
71
|
+
self.reason = reason
|
|
72
|
+
message = f"Failed to update config '{config_path}': {reason}"
|
|
73
|
+
super().__init__(message)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class NodejsNotFoundError(TranslationError):
|
|
77
|
+
"""Raised when Node.js is not installed or version is too old."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, required_version: str = "18.0.0") -> None:
|
|
80
|
+
self.required_version = required_version
|
|
81
|
+
message = (
|
|
82
|
+
f"Node.js {required_version}+ is required for translation. "
|
|
83
|
+
"Please install from https://nodejs.org/"
|
|
84
|
+
)
|
|
85
|
+
super().__init__(message)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class OllamaNotRunningError(TranslationError):
|
|
89
|
+
"""Raised when Ollama is not running for local LLM translation."""
|
|
90
|
+
|
|
91
|
+
def __init__(self) -> None:
|
|
92
|
+
message = (
|
|
93
|
+
"Ollama is not running. Please start Ollama first:\n"
|
|
94
|
+
" 1. Install: https://ollama.ai/download\n"
|
|
95
|
+
" 2. Start: ollama serve\n"
|
|
96
|
+
" 3. Pull a model: ollama pull llama2"
|
|
97
|
+
)
|
|
98
|
+
super().__init__(message)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""AI provider abstraction layer for translation.
|
|
2
|
+
|
|
3
|
+
This module provides a unified interface for multiple AI providers,
|
|
4
|
+
enabling seamless switching between different translation backends.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from truthound_dashboard.translate.providers import get_provider, detect_provider
|
|
8
|
+
|
|
9
|
+
# Explicit provider selection
|
|
10
|
+
provider = get_provider("openai")
|
|
11
|
+
|
|
12
|
+
# Auto-detection based on environment
|
|
13
|
+
provider = detect_provider()
|
|
14
|
+
|
|
15
|
+
# Translate text
|
|
16
|
+
result = await provider.translate(
|
|
17
|
+
text="Hello, world!",
|
|
18
|
+
source_lang="en",
|
|
19
|
+
target_lang="ja",
|
|
20
|
+
)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from truthound_dashboard.translate.providers.base import (
|
|
24
|
+
AIProvider,
|
|
25
|
+
ProviderConfig,
|
|
26
|
+
TranslationRequest,
|
|
27
|
+
TranslationResponse,
|
|
28
|
+
)
|
|
29
|
+
from truthound_dashboard.translate.providers.registry import (
|
|
30
|
+
ProviderRegistry,
|
|
31
|
+
get_provider,
|
|
32
|
+
detect_provider,
|
|
33
|
+
list_available_providers,
|
|
34
|
+
register_provider,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Base classes
|
|
39
|
+
"AIProvider",
|
|
40
|
+
"ProviderConfig",
|
|
41
|
+
"TranslationRequest",
|
|
42
|
+
"TranslationResponse",
|
|
43
|
+
# Registry functions
|
|
44
|
+
"ProviderRegistry",
|
|
45
|
+
"get_provider",
|
|
46
|
+
"detect_provider",
|
|
47
|
+
"list_available_providers",
|
|
48
|
+
"register_provider",
|
|
49
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Anthropic provider for translation.
|
|
2
|
+
|
|
3
|
+
This module implements the Anthropic translation provider using
|
|
4
|
+
Claude models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from truthound_dashboard.translate.exceptions import TranslationAPIError
|
|
12
|
+
from truthound_dashboard.translate.providers.base import (
|
|
13
|
+
AIProvider,
|
|
14
|
+
ProviderConfig,
|
|
15
|
+
TranslationRequest,
|
|
16
|
+
TranslationResponse,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AnthropicProvider(AIProvider):
|
|
21
|
+
"""Anthropic translation provider.
|
|
22
|
+
|
|
23
|
+
Uses Anthropic's Claude models for translation.
|
|
24
|
+
|
|
25
|
+
Environment:
|
|
26
|
+
ANTHROPIC_API_KEY: API key for authentication
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
provider = AnthropicProvider()
|
|
30
|
+
response = await provider.translate(
|
|
31
|
+
TranslationRequest(text="Hello", source_lang="en", target_lang="ja")
|
|
32
|
+
)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
name = "anthropic"
|
|
36
|
+
display_name = "Anthropic"
|
|
37
|
+
env_var = "ANTHROPIC_API_KEY"
|
|
38
|
+
default_model = "claude-sonnet-4-20250514"
|
|
39
|
+
supported_models = [
|
|
40
|
+
"claude-sonnet-4-20250514",
|
|
41
|
+
"claude-opus-4-20250514",
|
|
42
|
+
"claude-3-5-sonnet-20241022",
|
|
43
|
+
"claude-3-5-haiku-20241022",
|
|
44
|
+
"claude-3-opus-20240229",
|
|
45
|
+
"claude-3-sonnet-20240229",
|
|
46
|
+
"claude-3-haiku-20240307",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
DEFAULT_BASE_URL = "https://api.anthropic.com"
|
|
50
|
+
API_VERSION = "2023-06-01"
|
|
51
|
+
|
|
52
|
+
def __init__(self, config: ProviderConfig | None = None) -> None:
|
|
53
|
+
super().__init__(config)
|
|
54
|
+
self.base_url = self.config.base_url or self.DEFAULT_BASE_URL
|
|
55
|
+
|
|
56
|
+
async def translate(self, request: TranslationRequest) -> TranslationResponse:
|
|
57
|
+
"""Translate text using Anthropic API.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
request: Translation request
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Translation response with translated text
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
TranslationAPIError: If API call fails
|
|
67
|
+
"""
|
|
68
|
+
prompt = self.get_translation_prompt(request)
|
|
69
|
+
|
|
70
|
+
async with httpx.AsyncClient(timeout=self.config.timeout) as client:
|
|
71
|
+
try:
|
|
72
|
+
response = await client.post(
|
|
73
|
+
f"{self.base_url}/v1/messages",
|
|
74
|
+
headers={
|
|
75
|
+
"x-api-key": self.config.api_key,
|
|
76
|
+
"anthropic-version": self.API_VERSION,
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
json={
|
|
80
|
+
"model": self.model,
|
|
81
|
+
"max_tokens": 4096,
|
|
82
|
+
"system": (
|
|
83
|
+
"You are a professional translator. "
|
|
84
|
+
"Translate the given text accurately and naturally. "
|
|
85
|
+
"Only output the translated text, nothing else."
|
|
86
|
+
),
|
|
87
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if response.status_code != 200:
|
|
92
|
+
raise TranslationAPIError(
|
|
93
|
+
provider_name=self.name,
|
|
94
|
+
message=f"API request failed: {response.text}",
|
|
95
|
+
status_code=response.status_code,
|
|
96
|
+
response_body=response.text,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
data = response.json()
|
|
100
|
+
translated_text = data["content"][0]["text"].strip()
|
|
101
|
+
|
|
102
|
+
usage = None
|
|
103
|
+
if "usage" in data:
|
|
104
|
+
usage = {
|
|
105
|
+
"input_tokens": data["usage"].get("input_tokens", 0),
|
|
106
|
+
"output_tokens": data["usage"].get("output_tokens", 0),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return TranslationResponse(
|
|
110
|
+
translated_text=translated_text,
|
|
111
|
+
source_lang=request.source_lang,
|
|
112
|
+
target_lang=request.target_lang,
|
|
113
|
+
model=self.model,
|
|
114
|
+
provider=self.name,
|
|
115
|
+
usage=usage,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
except httpx.TimeoutException as e:
|
|
119
|
+
raise TranslationAPIError(
|
|
120
|
+
provider_name=self.name,
|
|
121
|
+
message=f"Request timed out after {self.config.timeout}s",
|
|
122
|
+
) from e
|
|
123
|
+
except httpx.RequestError as e:
|
|
124
|
+
raise TranslationAPIError(
|
|
125
|
+
provider_name=self.name,
|
|
126
|
+
message=f"Request failed: {e}",
|
|
127
|
+
) from e
|
|
128
|
+
|
|
129
|
+
async def is_available(self) -> bool:
|
|
130
|
+
"""Check if Anthropic API is available.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True if API key is set
|
|
134
|
+
"""
|
|
135
|
+
return bool(self.config.api_key)
|