iatoolkit 0.59.1__py3-none-any.whl → 0.67.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.

Potentially problematic release.


This version of iatoolkit might be problematic. Click here for more details.

Files changed (93) hide show
  1. iatoolkit/__init__.py +2 -0
  2. iatoolkit/base_company.py +2 -19
  3. iatoolkit/common/routes.py +28 -25
  4. iatoolkit/common/session_manager.py +2 -0
  5. iatoolkit/common/util.py +17 -6
  6. iatoolkit/company_registry.py +1 -2
  7. iatoolkit/iatoolkit.py +54 -20
  8. iatoolkit/locales/en.yaml +167 -0
  9. iatoolkit/locales/es.yaml +163 -0
  10. iatoolkit/repositories/database_manager.py +3 -3
  11. iatoolkit/repositories/document_repo.py +1 -1
  12. iatoolkit/repositories/models.py +3 -4
  13. iatoolkit/repositories/profile_repo.py +0 -4
  14. iatoolkit/services/auth_service.py +44 -32
  15. iatoolkit/services/branding_service.py +35 -27
  16. iatoolkit/services/configuration_service.py +140 -0
  17. iatoolkit/services/dispatcher_service.py +20 -18
  18. iatoolkit/services/document_service.py +5 -2
  19. iatoolkit/services/excel_service.py +15 -11
  20. iatoolkit/services/file_processor_service.py +4 -12
  21. iatoolkit/services/history_service.py +8 -7
  22. iatoolkit/services/i18n_service.py +104 -0
  23. iatoolkit/services/jwt_service.py +7 -9
  24. iatoolkit/services/language_service.py +79 -0
  25. iatoolkit/services/load_documents_service.py +4 -4
  26. iatoolkit/services/mail_service.py +9 -4
  27. iatoolkit/services/onboarding_service.py +10 -4
  28. iatoolkit/services/profile_service.py +59 -38
  29. iatoolkit/services/prompt_manager_service.py +20 -16
  30. iatoolkit/services/query_service.py +15 -14
  31. iatoolkit/services/sql_service.py +6 -2
  32. iatoolkit/services/user_feedback_service.py +70 -29
  33. iatoolkit/static/js/chat_feedback_button.js +80 -0
  34. iatoolkit/static/js/chat_help_content.js +124 -0
  35. iatoolkit/static/js/chat_history_button.js +110 -0
  36. iatoolkit/static/js/chat_logout_button.js +36 -0
  37. iatoolkit/static/js/chat_main.js +32 -184
  38. iatoolkit/static/js/{chat_onboarding.js → chat_onboarding_button.js} +0 -1
  39. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  40. iatoolkit/static/js/chat_reload_button.js +35 -0
  41. iatoolkit/static/styles/chat_iatoolkit.css +251 -205
  42. iatoolkit/static/styles/chat_modal.css +63 -95
  43. iatoolkit/static/styles/chat_public.css +107 -0
  44. iatoolkit/static/styles/landing_page.css +121 -167
  45. iatoolkit/templates/_company_header.html +20 -0
  46. iatoolkit/templates/_login_widget.html +10 -10
  47. iatoolkit/templates/base.html +36 -19
  48. iatoolkit/templates/change_password.html +24 -22
  49. iatoolkit/templates/chat.html +121 -93
  50. iatoolkit/templates/chat_modals.html +113 -74
  51. iatoolkit/templates/error.html +44 -8
  52. iatoolkit/templates/forgot_password.html +17 -15
  53. iatoolkit/templates/index.html +66 -81
  54. iatoolkit/templates/login_simulation.html +16 -5
  55. iatoolkit/templates/onboarding_shell.html +1 -2
  56. iatoolkit/templates/signup.html +22 -20
  57. iatoolkit/views/base_login_view.py +12 -1
  58. iatoolkit/views/change_password_view.py +50 -33
  59. iatoolkit/views/external_login_view.py +5 -11
  60. iatoolkit/views/file_store_api_view.py +7 -9
  61. iatoolkit/views/forgot_password_view.py +21 -19
  62. iatoolkit/views/help_content_api_view.py +54 -0
  63. iatoolkit/views/history_api_view.py +16 -12
  64. iatoolkit/views/home_view.py +61 -0
  65. iatoolkit/views/index_view.py +5 -34
  66. iatoolkit/views/init_context_api_view.py +16 -13
  67. iatoolkit/views/llmquery_api_view.py +38 -28
  68. iatoolkit/views/login_simulation_view.py +14 -2
  69. iatoolkit/views/login_view.py +48 -33
  70. iatoolkit/views/logout_api_view.py +49 -0
  71. iatoolkit/views/profile_api_view.py +46 -0
  72. iatoolkit/views/prompt_api_view.py +8 -8
  73. iatoolkit/views/signup_view.py +27 -25
  74. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  75. iatoolkit/views/tasks_review_api_view.py +55 -0
  76. iatoolkit/views/user_feedback_api_view.py +21 -32
  77. iatoolkit/views/verify_user_view.py +33 -26
  78. {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/METADATA +40 -22
  79. iatoolkit-0.67.0.dist-info/RECORD +120 -0
  80. iatoolkit-0.67.0.dist-info/licenses/LICENSE +21 -0
  81. iatoolkit/static/js/chat_context_reload.js +0 -54
  82. iatoolkit/static/js/chat_feedback.js +0 -115
  83. iatoolkit/static/js/chat_history.js +0 -127
  84. iatoolkit/static/styles/chat_info.css +0 -53
  85. iatoolkit/templates/_branding_styles.html +0 -53
  86. iatoolkit/templates/_navbar.html +0 -9
  87. iatoolkit/templates/header.html +0 -31
  88. iatoolkit/templates/test.html +0 -9
  89. iatoolkit/views/chat_token_request_view.py +0 -98
  90. iatoolkit/views/tasks_review_view.py +0 -83
  91. iatoolkit-0.59.1.dist-info/RECORD +0 -111
  92. {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/WHEEL +0 -0
  93. {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,7 @@
5
5
 
6
6
  from injector import inject
7
7
  from iatoolkit.repositories.profile_repo import ProfileRepo
8
- from iatoolkit.services.dispatcher_service import Dispatcher
8
+ from iatoolkit.services.i18n_service import I18nService
9
9
  from iatoolkit.repositories.models import User, Company, ApiKey
10
10
  from flask_bcrypt import check_password_hash
11
11
  from iatoolkit.common.session_manager import SessionManager
@@ -16,16 +16,19 @@ import random
16
16
  import re
17
17
  import secrets
18
18
  import string
19
+ import logging
19
20
  from iatoolkit.services.dispatcher_service import Dispatcher
20
21
 
21
22
 
22
23
  class ProfileService:
23
24
  @inject
24
25
  def __init__(self,
26
+ i18n_service: I18nService,
25
27
  profile_repo: ProfileRepo,
26
28
  session_context_service: UserSessionContextService,
27
29
  dispatcher: Dispatcher,
28
30
  mail_app: MailApp):
31
+ self.i18n_service = i18n_service
29
32
  self.profile_repo = profile_repo
30
33
  self.dispatcher = dispatcher
31
34
  self.session_context = session_context_service
@@ -38,23 +41,23 @@ class ProfileService:
38
41
  # check if user exists
39
42
  user = self.profile_repo.get_user_by_email(email)
40
43
  if not user:
41
- return {'success': False, "message": "Usuario no encontrado"}
44
+ return {'success': False, 'message': self.i18n_service.t('errors.auth.user_not_found')}
42
45
 
43
46
  # check the encrypted password
44
47
  if not check_password_hash(user.password, password):
45
- return {'success': False, "message": "Contraseña inválida"}
48
+ return {'success': False, 'message': self.i18n_service.t('errors.auth.invalid_password')}
46
49
 
47
50
  company = self.profile_repo.get_company_by_short_name(company_short_name)
48
51
  if not company:
49
- return {'success': False, "message": "Empresa no encontrada"}
52
+ return {'success': False, "message": "missing company"}
50
53
 
51
54
  # check that user belongs to company
52
55
  if company not in user.companies:
53
- return {'success': False, "message": "Usuario no esta autorizado para esta empresa"}
56
+ return {'success': False, "message": self.i18n_service.t('errors.services.user_not_authorized')}
54
57
 
55
58
  if not user.verified:
56
59
  return {'success': False,
57
- "message": "Tu cuenta no ha sido verificada. Por favor, revisa tu correo."}
60
+ "message": self.i18n_service.t('errors.services.account_not_verified')}
58
61
 
59
62
  # 1. Build the local user profile dictionary here.
60
63
  # the user_profile variables are used on the LLM templates also (see in query_main.prompt)
@@ -71,8 +74,9 @@ class ProfileService:
71
74
 
72
75
  # 3. create the web session
73
76
  self.set_session_for_user(company.short_name, user_identifier)
74
- return {'success': True, "user_identifier": user_identifier, "message": "Login exitoso"}
77
+ return {'success': True, "user_identifier": user_identifier, "message": "Login ok"}
75
78
  except Exception as e:
79
+ logging.error(f"Error in login: {e}")
76
80
  return {'success': False, "message": str(e)}
77
81
 
78
82
  def create_external_user_profile_context(self, company: Company, user_identifier: str):
@@ -131,6 +135,25 @@ class ProfileService:
131
135
  "profile": profile
132
136
  }
133
137
 
138
+ def update_user_language(self, user_identifier: str, new_lang: str) -> dict:
139
+ """
140
+ Business logic to update a user's preferred language.
141
+ It validates the language and then calls the generic update method.
142
+ """
143
+ # 1. Validate that the language is supported by checking the loaded translations.
144
+ if new_lang not in self.i18n_service.translations:
145
+ return {'success': False, 'error_message': self.i18n_service.t('errors.general.unsupported_language')}
146
+
147
+ try:
148
+ # 2. Call the generic update_user method, passing the specific field to update.
149
+ self.update_user(user_identifier, preferred_language=new_lang)
150
+ return {'success': True, 'message': 'Language updated successfully.'}
151
+ except Exception as e:
152
+ # Log the error and return a generic failure message.
153
+ logging.error(f"Failed to update language for {user_identifier}: {e}")
154
+ return {'success': False, 'error_message': self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
155
+
156
+
134
157
  def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
135
158
  """
136
159
  Fetches a user profile directly by their identifier, bypassing the Flask session.
@@ -154,7 +177,8 @@ class ProfileService:
154
177
  # get company info
155
178
  company = self.profile_repo.get_company_by_short_name(company_short_name)
156
179
  if not company:
157
- return {"error": f"la empresa {company_short_name} no existe"}
180
+ return {
181
+ "error": self.i18n_service.t('errors.signup.company_not_found', company_name=company_short_name)}
158
182
 
159
183
  # normalize format's
160
184
  email = email.lower()
@@ -164,24 +188,25 @@ class ProfileService:
164
188
  if existing_user:
165
189
  # validate password
166
190
  if not self.bcrypt.check_password_hash(existing_user.password, password):
167
- return {"error": "La contraseña es incorrecta. No se puede agregar a la nueva empresa."}
191
+ return {"error": self.i18n_service.t('errors.signup.incorrect_password_for_existing_user', email=email)}
168
192
 
169
193
  # check if register
170
194
  if company in existing_user.companies:
171
- return {"error": "Usuario ya registrado en esta empresa"}
195
+ return {"error": self.i18n_service.t('errors.signup.user_already_registered', email=email)}
172
196
  else:
173
197
  # add new company to existing user
174
198
  existing_user.companies.append(company)
175
199
  self.profile_repo.save_user(existing_user)
176
- return {"message": "Usuario asociado a nueva empresa"}
200
+ return {"message": self.i18n_service.t('flash_messages.user_associated_success')}
177
201
 
178
202
  # add the new user
179
203
  if password != confirm_password:
180
- return {"error": "Las contraseñas no coinciden. Por favor, inténtalo de nuevo."}
204
+ return {"error": self.i18n_service.t('errors.signup.password_mismatch')}
181
205
 
182
206
  is_valid, message = self.validate_password(password)
183
207
  if not is_valid:
184
- return {"error": message}
208
+ # Translate the key returned by validate_password
209
+ return {"error": self.i18n_service.t(message)}
185
210
 
186
211
  # encrypt the password
187
212
  hashed_password = self.bcrypt.generate_password_hash(password).decode('utf-8')
@@ -203,9 +228,9 @@ class ProfileService:
203
228
  # send email with verification
204
229
  self.send_verification_email(new_user, company_short_name)
205
230
 
206
- return {"message": "Registro exitoso. Por favor, revisa tu correo para verificar tu cuenta."}
231
+ return {"message": self.i18n_service.t('flash_messages.signup_success')}
207
232
  except Exception as e:
208
- return {"error": str(e)}
233
+ return {"error": self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
209
234
 
210
235
  def update_user(self, email: str, **kwargs) -> User:
211
236
  return self.profile_repo.update_user(email, **kwargs)
@@ -215,14 +240,14 @@ class ProfileService:
215
240
  # check if user exist
216
241
  user = self.profile_repo.get_user_by_email(email)
217
242
  if not user:
218
- return {"error": "El usuario no existe."}
243
+ return {"error": self.i18n_service.t('errors.verification.user_not_found')}
219
244
 
220
245
  # activate the user account
221
246
  self.profile_repo.verify_user(email)
222
- return {"message": "Tu cuenta ha sido verificada exitosamente. Ahora puedes iniciar sesión."}
247
+ return {"message": self.i18n_service.t('flash_messages.account_verified_success')}
223
248
 
224
249
  except Exception as e:
225
- return {"error": str(e)}
250
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
226
251
 
227
252
  def change_password(self,
228
253
  email: str,
@@ -231,28 +256,28 @@ class ProfileService:
231
256
  confirm_password: str):
232
257
  try:
233
258
  if new_password != confirm_password:
234
- return {"error": "Las contraseñas no coinciden. Por favor, inténtalo nuevamente."}
259
+ return {"error": self.i18n_service.t('errors.change_password.password_mismatch')}
235
260
 
236
261
  # check the temporary code
237
262
  user = self.profile_repo.get_user_by_email(email)
238
263
  if not user or user.temp_code != temp_code:
239
- return {"error": "El código temporal no es válido. Por favor, verifica o solicita uno nuevo."}
264
+ return {"error": self.i18n_service.t('errors.change_password.invalid_temp_code')}
240
265
 
241
266
  # encrypt and save the password, make the temporary code invalid
242
267
  hashed_password = self.bcrypt.generate_password_hash(new_password).decode('utf-8')
243
268
  self.profile_repo.update_password(email, hashed_password)
244
269
  self.profile_repo.reset_temp_code(email)
245
270
 
246
- return {"message": "La clave se cambio correctamente"}
271
+ return {"message": self.i18n_service.t('flash_messages.password_changed_success')}
247
272
  except Exception as e:
248
- return {"error": str(e)}
273
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
249
274
 
250
275
  def forgot_password(self, email: str, reset_url: str):
251
276
  try:
252
277
  # Verificar si el usuario existe
253
278
  user = self.profile_repo.get_user_by_email(email)
254
279
  if not user:
255
- return {"error": "El usuario no existe."}
280
+ return {"error": self.i18n_service.t('errors.forgot_password.user_not_registered', email=email)}
256
281
 
257
282
  # Gen a temporary code and store in the repositories
258
283
  temp_code = ''.join(random.choices(string.ascii_letters + string.digits, k=6)).upper()
@@ -261,35 +286,31 @@ class ProfileService:
261
286
  # send email to the user
262
287
  self.send_forgot_password_email(user, reset_url)
263
288
 
264
- return {"message": "se envio mail para cambio de clave"}
289
+ return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
265
290
  except Exception as e:
266
- return {"error": str(e)}
291
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
267
292
 
268
293
  def validate_password(self, password):
269
294
  """
270
- Valida que una contraseña cumpla con los siguientes requisitos:
271
- - Al menos 8 caracteres de longitud
272
- - Contiene al menos una letra mayúscula
273
- - Contiene al menos una letra minúscula
274
- - Contiene al menos un número
275
- - Contiene al menos un carácter especial
295
+ Validates that a password meets all requirements.
296
+ Returns (True, "...") on success, or (False, "translation.key") on failure.
276
297
  """
277
298
  if len(password) < 8:
278
- return False, "La contraseña debe tener al menos 8 caracteres."
299
+ return False, "errors.validation.password_too_short"
279
300
 
280
301
  if not any(char.isupper() for char in password):
281
- return False, "La contraseña debe tener al menos una letra mayúscula."
302
+ return False, "errors.validation.password_no_uppercase"
282
303
 
283
304
  if not any(char.islower() for char in password):
284
- return False, "La contraseña debe tener al menos una letra minúscula."
305
+ return False, "errors.validation.password_no_lowercase"
285
306
 
286
307
  if not any(char.isdigit() for char in password):
287
- return False, "La contraseña debe tener al menos un número."
308
+ return False, "errors.validation.password_no_digit"
288
309
 
289
310
  if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
290
- return False, "La contraseña debe tener al menos un carácter especial."
311
+ return False, "errors.validation.password_no_special_char"
291
312
 
292
- return True, "La contraseña es válida."
313
+ return True, "Password is valid."
293
314
 
294
315
  def get_companies(self):
295
316
  return self.profile_repo.get_companies()
@@ -303,7 +324,7 @@ class ProfileService:
303
324
  def new_api_key(self, company_short_name: str):
304
325
  company = self.get_company_by_short_name(company_short_name)
305
326
  if not company:
306
- return {"error": f"la empresa {company_short_name} no existe"}
327
+ return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
307
328
 
308
329
  length = 40 # lenght of the api key
309
330
  alphabet = string.ascii_letters + string.digits
@@ -5,21 +5,25 @@
5
5
 
6
6
  from injector import inject
7
7
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
-
9
- import logging
8
+ from iatoolkit.services.i18n_service import I18nService
10
9
  from iatoolkit.repositories.profile_repo import ProfileRepo
11
10
  from collections import defaultdict
12
11
  from iatoolkit.repositories.models import Prompt, PromptCategory, Company
13
12
  import os
14
13
  from iatoolkit.common.exceptions import IAToolkitException
15
14
  import importlib.resources
15
+ import logging
16
16
 
17
17
 
18
18
  class PromptService:
19
19
  @inject
20
- def __init__(self, llm_query_repo: LLMQueryRepo, profile_repo: ProfileRepo):
20
+ def __init__(self,
21
+ llm_query_repo: LLMQueryRepo,
22
+ profile_repo: ProfileRepo,
23
+ i18n_service: I18nService):
21
24
  self.llm_query_repo = llm_query_repo
22
25
  self.profile_repo = profile_repo
26
+ self.i18n_service = i18n_service
23
27
 
24
28
  def create_prompt(self,
25
29
  prompt_name: str,
@@ -36,20 +40,20 @@ class PromptService:
36
40
  if is_system_prompt:
37
41
  if not importlib.resources.files('iatoolkit.system_prompts').joinpath(prompt_filename).is_file():
38
42
  raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
39
- f'No existe el archivo de prompt de sistemas: {prompt_filename}')
43
+ f'missing system prompt file: {prompt_filename}')
40
44
  else:
41
45
  template_dir = f'companies/{company.short_name}/prompts'
42
46
 
43
47
  relative_prompt_path = os.path.join(template_dir, prompt_filename)
44
48
  if not os.path.exists(relative_prompt_path):
45
49
  raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
46
- f'No existe el archivo de prompt: {relative_prompt_path}')
50
+ f'missing prompt file: {relative_prompt_path}')
47
51
 
48
52
  if custom_fields:
49
53
  for f in custom_fields:
50
54
  if ('data_key' not in f) or ('label' not in f):
51
55
  raise IAToolkitException(IAToolkitException.ErrorType.INVALID_PARAMETER,
52
- f'El campo custom_fields debe contener los campos: data_key y label')
56
+ f'The field "custom_fields" must contain the following keys: data_key y label')
53
57
 
54
58
  # add default value for data_type
55
59
  if 'type' not in f:
@@ -82,20 +86,20 @@ class PromptService:
82
86
  user_prompt = self.llm_query_repo.get_prompt_by_name(company, prompt_name)
83
87
  if not user_prompt:
84
88
  raise IAToolkitException(IAToolkitException.ErrorType.DOCUMENT_NOT_FOUND,
85
- f"No se encontró el prompt '{prompt_name}' para la empresa '{company.short_name}'")
89
+ f"prompt not found '{prompt_name}' for company '{company.short_name}'")
86
90
 
87
91
  prompt_file = f'companies/{company.short_name}/prompts/{user_prompt.filename}'
88
92
  absolute_filepath = os.path.join(execution_dir, prompt_file)
89
93
  if not os.path.exists(absolute_filepath):
90
94
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
91
- f"El archivo para el prompt '{prompt_name}' no existe: {absolute_filepath}")
95
+ f"prompt file '{prompt_name}' does not exist: {absolute_filepath}")
92
96
 
93
97
  try:
94
98
  with open(absolute_filepath, 'r', encoding='utf-8') as f:
95
99
  user_prompt_content = f.read()
96
100
  except Exception as e:
97
101
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
98
- f"Error leyendo el archivo de prompt '{prompt_name}' en {absolute_filepath}: {e}")
102
+ f"error while reading prompt: '{prompt_name}' in this pathname {absolute_filepath}: {e}")
99
103
 
100
104
  return user_prompt_content
101
105
 
@@ -105,9 +109,9 @@ class PromptService:
105
109
  raise
106
110
  except Exception as e:
107
111
  logging.exception(
108
- f"Error al obtener el contenido del prompt para la empresa '{company.short_name}' y prompt '{prompt_name}': {e}")
112
+ f"error loading prompt '{prompt_name}' content for '{company.short_name}': {e}")
109
113
  raise IAToolkitException(IAToolkitException.ErrorType.PROMPT_ERROR,
110
- f'Error al obtener el contenido del prompt "{prompt_name}" para la empresa {company.short_name}: {str(e)}')
114
+ f'error loading prompt "{prompt_name}" content for company {company.short_name}: {str(e)}')
111
115
 
112
116
  def get_system_prompt(self):
113
117
  try:
@@ -121,10 +125,10 @@ class PromptService:
121
125
  content = importlib.resources.read_text('iatoolkit.system_prompts', prompt.filename)
122
126
  system_prompt_content.append(content)
123
127
  except FileNotFoundError:
124
- logging.warning(f"El archivo para el prompt de sistema no existe en el paquete: {prompt.filename}")
128
+ logging.warning(f"Prompt file does not exist in the package: {prompt.filename}")
125
129
  except Exception as e:
126
130
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
127
- f"Error leyendo el archivo de prompt del sistema '{prompt.filename}': {e}")
131
+ f"error reading system prompt '{prompt.filename}': {e}")
128
132
 
129
133
  # join the system prompts into a single string
130
134
  return "\n".join(system_prompt_content)
@@ -135,14 +139,14 @@ class PromptService:
135
139
  logging.exception(
136
140
  f"Error al obtener el contenido del prompt de sistema: {e}")
137
141
  raise IAToolkitException(IAToolkitException.ErrorType.PROMPT_ERROR,
138
- f'Error al obtener el contenido de los prompts de sistema": {str(e)}')
142
+ f'error reading the system prompts": {str(e)}')
139
143
 
140
144
  def get_user_prompts(self, company_short_name: str) -> dict:
141
145
  try:
142
146
  # validate company
143
147
  company = self.profile_repo.get_company_by_short_name(company_short_name)
144
148
  if not company:
145
- return {'error': f'No existe la empresa: {company_short_name}'}
149
+ return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
146
150
 
147
151
  # get all the prompts
148
152
  all_prompts = self.llm_query_repo.get_prompts(company)
@@ -183,6 +187,6 @@ class PromptService:
183
187
  return {'message': categorized_prompts}
184
188
 
185
189
  except Exception as e:
186
- logging.error(f"Error en get_prompts: {e}")
190
+ logging.error(f"error in get_prompts: {e}")
187
191
  return {'error': str(e)}
188
192
 
@@ -8,6 +8,7 @@ from iatoolkit.services.profile_service import ProfileService
8
8
  from iatoolkit.repositories.document_repo import DocumentRepo
9
9
  from iatoolkit.repositories.profile_repo import ProfileRepo
10
10
  from iatoolkit.services.document_service import DocumentService
11
+ from iatoolkit.services.i18n_service import I18nService
11
12
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
12
13
  from iatoolkit.repositories.models import Task
13
14
  from iatoolkit.services.dispatcher_service import Dispatcher
@@ -37,6 +38,7 @@ class QueryService:
37
38
  llmquery_repo: LLMQueryRepo,
38
39
  profile_repo: ProfileRepo,
39
40
  prompt_service: PromptService,
41
+ i18n_service: I18nService,
40
42
  util: Utility,
41
43
  dispatcher: Dispatcher,
42
44
  session_context: UserSessionContextService
@@ -47,6 +49,7 @@ class QueryService:
47
49
  self.llmquery_repo = llmquery_repo
48
50
  self.profile_repo = profile_repo
49
51
  self.prompt_service = prompt_service
52
+ self.i18n_service = i18n_service
50
53
  self.util = util
51
54
  self.dispatcher = dispatcher
52
55
  self.session_context = session_context
@@ -56,7 +59,7 @@ class QueryService:
56
59
  self.model = os.getenv("LLM_MODEL", "")
57
60
  if not self.model:
58
61
  raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
59
- "La variable de entorno 'LLM_MODEL' no está configurada.")
62
+ "missing ENV variable 'LLM_MODEL' configuration.")
60
63
 
61
64
  def _build_context_and_profile(self, company_short_name: str, user_identifier: str) -> tuple:
62
65
  # this method read the user/company context from the database and renders the system prompt
@@ -127,7 +130,7 @@ class QueryService:
127
130
  lock_key = f"lock:context:{company_short_name}/{user_identifier}"
128
131
  if not self.session_context.acquire_lock(lock_key, expire_seconds=60):
129
132
  logging.warning(
130
- f"Intento de reconstruir contexto para {user_identifier} mientras ya estaba en progreso. Se omite.")
133
+ f"try to rebuild context for user {user_identifier} while is still in process, ignored.")
131
134
  return
132
135
 
133
136
  try:
@@ -138,11 +141,9 @@ class QueryService:
138
141
  prepared_context, version_to_save = self.session_context.get_and_clear_prepared_context(company_short_name,
139
142
  user_identifier)
140
143
  if not prepared_context:
141
- logging.info(
142
- f"No se requiere reconstrucción de contexto para {company_short_name}/{user_identifier}. Finalización rápida.")
143
144
  return
144
145
 
145
- logging.info(f"Enviando contexto al LLM para {company_short_name}/{user_identifier}...")
146
+ logging.info(f"sending context to LLM for: {company_short_name}/{user_identifier}...")
146
147
 
147
148
  # Limpiar solo el historial de chat y el ID de respuesta anterior
148
149
  self.session_context.clear_llm_history(company_short_name, user_identifier)
@@ -161,9 +162,9 @@ class QueryService:
161
162
  self.session_context.save_context_version(company_short_name, user_identifier, version_to_save)
162
163
 
163
164
  logging.info(
164
- f"Contexto de {company_short_name}/{user_identifier} establecido en {int(time.time() - start_time)} seg.")
165
+ f"Context for: {company_short_name}/{user_identifier} settled in {int(time.time() - start_time)} sec.")
165
166
  except Exception as e:
166
- logging.exception(f"Error en finalize_context_rebuild para {company_short_name}: {e}")
167
+ logging.exception(f"Error in finalize_context_rebuild for {company_short_name}: {e}")
167
168
  raise e
168
169
  finally:
169
170
  # --- Liberar el Bloqueo ---
@@ -181,11 +182,11 @@ class QueryService:
181
182
  company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
182
183
  if not company:
183
184
  return {"error": True,
184
- "error_message": f'No existe Company ID: {company_short_name}'}
185
+ "error_message": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
185
186
 
186
187
  if not prompt_name and not question:
187
188
  return {"error": True,
188
- "error_message": f'Hola, cual es tu pregunta?'}
189
+ "error_message": self.i18n_service.t('services.start_query')}
189
190
 
190
191
  # get the previous response_id and context history
191
192
  previous_response_id = None
@@ -196,7 +197,7 @@ class QueryService:
196
197
  previous_response_id = self.session_context.get_last_response_id(company.short_name, user_identifier)
197
198
  if not previous_response_id:
198
199
  return {'error': True,
199
- "error_message": f"No se encontró 'previous_response_id' para '{company.short_name}/{user_identifier}'. Reinicia el contexto para continuar."
200
+ "error_message": self.i18n_service.t('errors.services.missing_response_id', company_short_name=company.short_name, user_identifier=user_identifier)
200
201
  }
201
202
  elif self.util.is_gemini_model(self.model):
202
203
  # check the length of the context_history and remove old messages
@@ -294,7 +295,7 @@ class QueryService:
294
295
  return len(history) >= 1
295
296
  return False
296
297
  except Exception as e:
297
- logging.warning(f"Error verificando caché de contexto: {e}")
298
+ logging.warning(f"error verifying context cache: {e}")
298
299
  return False
299
300
 
300
301
  def load_files_for_context(self, files: list) -> str:
@@ -353,7 +354,7 @@ class QueryService:
353
354
  try:
354
355
  total_tokens = sum(self.llm_client.count_tokens(json.dumps(message)) for message in context_history)
355
356
  except Exception as e:
356
- logging.error(f"Error al calcular tokens del historial: {e}. No se pudo recortar el contexto.")
357
+ logging.error(f"error counting tokens for history: {e}.")
357
358
  return
358
359
 
359
360
  # Si se excede el límite, eliminar mensajes antiguos (empezando por el segundo)
@@ -364,8 +365,8 @@ class QueryService:
364
365
  removed_tokens = self.llm_client.count_tokens(json.dumps(removed_message))
365
366
  total_tokens -= removed_tokens
366
367
  logging.warning(
367
- f"Historial de contexto ({total_tokens + removed_tokens} tokens) excedía el límite de {GEMINI_MAX_TOKENS_CONTEXT_HISTORY}. "
368
- f"Nuevo total: {total_tokens} tokens."
368
+ f"history tokens ({total_tokens + removed_tokens} tokens) exceed the limit of: {GEMINI_MAX_TOKENS_CONTEXT_HISTORY}. "
369
+ f"new context: {total_tokens} tokens."
369
370
  )
370
371
  except IndexError:
371
372
  # Se produce si solo queda el mensaje del sistema, el bucle debería detenerse.
@@ -6,6 +6,7 @@
6
6
  from iatoolkit.repositories.database_manager import DatabaseManager
7
7
 
8
8
  from iatoolkit.common.util import Utility
9
+ from iatoolkit.services.i18n_service import I18nService
9
10
  from sqlalchemy import text
10
11
  from injector import inject
11
12
  import json
@@ -14,8 +15,11 @@ from iatoolkit.common.exceptions import IAToolkitException
14
15
 
15
16
  class SqlService:
16
17
  @inject
17
- def __init__(self,util: Utility):
18
+ def __init__(self,
19
+ util: Utility,
20
+ i18n_service: I18nService):
18
21
  self.util = util
22
+ self.i18n_service = i18n_service
19
23
 
20
24
  def exec_sql(self, db_manager: DatabaseManager, sql_statement: str) -> str:
21
25
  """
@@ -54,7 +58,7 @@ class SqlService:
54
58
 
55
59
  error_message = str(e)
56
60
  if 'timed out' in str(e):
57
- error_message = 'Intentalo de nuevo, se agoto el tiempo de espera'
61
+ error_message = self.i18n_service.t('errors.timeout')
58
62
 
59
63
  raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
60
64
  error_message) from e