iatoolkit 0.63.1__py3-none-any.whl → 0.69.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 (83) hide show
  1. iatoolkit/__init__.py +0 -2
  2. iatoolkit/base_company.py +1 -26
  3. iatoolkit/common/routes.py +11 -2
  4. iatoolkit/common/session_manager.py +2 -0
  5. iatoolkit/common/util.py +17 -0
  6. iatoolkit/company_registry.py +1 -2
  7. iatoolkit/iatoolkit.py +39 -6
  8. iatoolkit/locales/en.yaml +167 -0
  9. iatoolkit/locales/es.yaml +163 -0
  10. iatoolkit/repositories/database_manager.py +8 -3
  11. iatoolkit/repositories/document_repo.py +1 -1
  12. iatoolkit/repositories/models.py +1 -4
  13. iatoolkit/repositories/profile_repo.py +0 -4
  14. iatoolkit/services/auth_service.py +14 -9
  15. iatoolkit/services/branding_service.py +36 -24
  16. iatoolkit/services/company_context_service.py +145 -0
  17. iatoolkit/services/configuration_service.py +133 -0
  18. iatoolkit/services/dispatcher_service.py +51 -48
  19. iatoolkit/services/document_service.py +5 -2
  20. iatoolkit/services/excel_service.py +15 -11
  21. iatoolkit/services/file_processor_service.py +4 -12
  22. iatoolkit/services/history_service.py +8 -7
  23. iatoolkit/services/i18n_service.py +104 -0
  24. iatoolkit/services/jwt_service.py +7 -9
  25. iatoolkit/services/language_service.py +83 -0
  26. iatoolkit/services/load_documents_service.py +4 -4
  27. iatoolkit/services/mail_service.py +9 -4
  28. iatoolkit/services/profile_service.py +61 -38
  29. iatoolkit/services/prompt_manager_service.py +20 -16
  30. iatoolkit/services/query_service.py +19 -15
  31. iatoolkit/services/search_service.py +11 -4
  32. iatoolkit/services/sql_service.py +55 -25
  33. iatoolkit/services/user_feedback_service.py +16 -14
  34. iatoolkit/static/js/chat_feedback_button.js +57 -87
  35. iatoolkit/static/js/chat_help_content.js +124 -0
  36. iatoolkit/static/js/chat_history_button.js +48 -65
  37. iatoolkit/static/js/chat_main.js +27 -24
  38. iatoolkit/static/js/chat_onboarding_button.js +6 -0
  39. iatoolkit/static/js/chat_reload_button.js +28 -45
  40. iatoolkit/static/styles/chat_iatoolkit.css +223 -315
  41. iatoolkit/static/styles/chat_modal.css +63 -97
  42. iatoolkit/static/styles/chat_public.css +107 -0
  43. iatoolkit/static/styles/landing_page.css +0 -1
  44. iatoolkit/static/styles/onboarding.css +7 -0
  45. iatoolkit/templates/_company_header.html +6 -2
  46. iatoolkit/templates/_login_widget.html +42 -0
  47. iatoolkit/templates/base.html +34 -19
  48. iatoolkit/templates/change_password.html +22 -20
  49. iatoolkit/templates/chat.html +59 -27
  50. iatoolkit/templates/chat_modals.html +114 -74
  51. iatoolkit/templates/error.html +12 -13
  52. iatoolkit/templates/forgot_password.html +11 -7
  53. iatoolkit/templates/index.html +8 -3
  54. iatoolkit/templates/login_simulation.html +17 -6
  55. iatoolkit/templates/onboarding_shell.html +4 -2
  56. iatoolkit/templates/signup.html +14 -14
  57. iatoolkit/views/base_login_view.py +19 -9
  58. iatoolkit/views/change_password_view.py +50 -35
  59. iatoolkit/views/external_login_view.py +1 -1
  60. iatoolkit/views/forgot_password_view.py +21 -22
  61. iatoolkit/views/help_content_api_view.py +54 -0
  62. iatoolkit/views/history_api_view.py +13 -9
  63. iatoolkit/views/home_view.py +30 -39
  64. iatoolkit/views/init_context_api_view.py +16 -11
  65. iatoolkit/views/llmquery_api_view.py +38 -26
  66. iatoolkit/views/login_simulation_view.py +14 -2
  67. iatoolkit/views/login_view.py +52 -40
  68. iatoolkit/views/logout_api_view.py +26 -22
  69. iatoolkit/views/profile_api_view.py +46 -0
  70. iatoolkit/views/prompt_api_view.py +6 -6
  71. iatoolkit/views/signup_view.py +27 -27
  72. iatoolkit/views/user_feedback_api_view.py +19 -18
  73. iatoolkit/views/verify_user_view.py +29 -30
  74. {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/METADATA +40 -22
  75. iatoolkit-0.69.0.dist-info/RECORD +120 -0
  76. iatoolkit-0.69.0.dist-info/licenses/LICENSE +21 -0
  77. iatoolkit/services/onboarding_service.py +0 -43
  78. iatoolkit/static/styles/chat_info.css +0 -53
  79. iatoolkit/templates/header.html +0 -31
  80. iatoolkit/templates/test.html +0 -9
  81. iatoolkit-0.63.1.dist-info/RECORD +0 -112
  82. {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/WHEEL +0 -0
  83. {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.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):
@@ -91,6 +95,9 @@ class ProfileService:
91
95
  user_identifier=user_identifier,
92
96
  user_profile=external_user_profile)
93
97
 
98
+ # 3. make sure the flask session is clean
99
+ SessionManager.clear()
100
+
94
101
  def save_user_profile(self, company: Company, user_identifier: str, user_profile: dict):
95
102
  """
96
103
  Private helper: Takes a pre-built profile, saves it to Redis, and sets the Flask cookie.
@@ -131,6 +138,24 @@ class ProfileService:
131
138
  "profile": profile
132
139
  }
133
140
 
141
+ def update_user_language(self, user_identifier: str, new_lang: str) -> dict:
142
+ """
143
+ Business logic to update a user's preferred language.
144
+ It validates the language and then calls the generic update method.
145
+ """
146
+ # 1. Validate that the language is supported by checking the loaded translations.
147
+ if new_lang not in self.i18n_service.translations:
148
+ return {'success': False, 'error_message': self.i18n_service.t('errors.general.unsupported_language')}
149
+
150
+ try:
151
+ # 2. Call the generic update_user method, passing the specific field to update.
152
+ self.update_user(user_identifier, preferred_language=new_lang)
153
+ return {'success': True, 'message': 'Language updated successfully.'}
154
+ except Exception as e:
155
+ # Log the error and return a generic failure message.
156
+ logging.error(f"Failed to update language for {user_identifier}: {e}")
157
+ return {'success': False, 'error_message': self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
158
+
134
159
 
135
160
  def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
136
161
  """
@@ -155,7 +180,8 @@ class ProfileService:
155
180
  # get company info
156
181
  company = self.profile_repo.get_company_by_short_name(company_short_name)
157
182
  if not company:
158
- return {"error": f"la empresa {company_short_name} no existe"}
183
+ return {
184
+ "error": self.i18n_service.t('errors.signup.company_not_found', company_name=company_short_name)}
159
185
 
160
186
  # normalize format's
161
187
  email = email.lower()
@@ -165,24 +191,25 @@ class ProfileService:
165
191
  if existing_user:
166
192
  # validate password
167
193
  if not self.bcrypt.check_password_hash(existing_user.password, password):
168
- return {"error": "La contraseña es incorrecta. No se puede agregar a la nueva empresa."}
194
+ return {"error": self.i18n_service.t('errors.signup.incorrect_password_for_existing_user', email=email)}
169
195
 
170
196
  # check if register
171
197
  if company in existing_user.companies:
172
- return {"error": "Usuario ya registrado en esta empresa"}
198
+ return {"error": self.i18n_service.t('errors.signup.user_already_registered', email=email)}
173
199
  else:
174
200
  # add new company to existing user
175
201
  existing_user.companies.append(company)
176
202
  self.profile_repo.save_user(existing_user)
177
- return {"message": "Usuario asociado a nueva empresa"}
203
+ return {"message": self.i18n_service.t('flash_messages.user_associated_success')}
178
204
 
179
205
  # add the new user
180
206
  if password != confirm_password:
181
- return {"error": "Las contraseñas no coinciden. Por favor, inténtalo de nuevo."}
207
+ return {"error": self.i18n_service.t('errors.signup.password_mismatch')}
182
208
 
183
209
  is_valid, message = self.validate_password(password)
184
210
  if not is_valid:
185
- return {"error": message}
211
+ # Translate the key returned by validate_password
212
+ return {"error": self.i18n_service.t(message)}
186
213
 
187
214
  # encrypt the password
188
215
  hashed_password = self.bcrypt.generate_password_hash(password).decode('utf-8')
@@ -204,9 +231,9 @@ class ProfileService:
204
231
  # send email with verification
205
232
  self.send_verification_email(new_user, company_short_name)
206
233
 
207
- return {"message": "Registro exitoso. Por favor, revisa tu correo para verificar tu cuenta."}
234
+ return {"message": self.i18n_service.t('flash_messages.signup_success')}
208
235
  except Exception as e:
209
- return {"error": str(e)}
236
+ return {"error": self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
210
237
 
211
238
  def update_user(self, email: str, **kwargs) -> User:
212
239
  return self.profile_repo.update_user(email, **kwargs)
@@ -216,14 +243,14 @@ class ProfileService:
216
243
  # check if user exist
217
244
  user = self.profile_repo.get_user_by_email(email)
218
245
  if not user:
219
- return {"error": "El usuario no existe."}
246
+ return {"error": self.i18n_service.t('errors.verification.user_not_found')}
220
247
 
221
248
  # activate the user account
222
249
  self.profile_repo.verify_user(email)
223
- return {"message": "Tu cuenta ha sido verificada exitosamente. Ahora puedes iniciar sesión."}
250
+ return {"message": self.i18n_service.t('flash_messages.account_verified_success')}
224
251
 
225
252
  except Exception as e:
226
- return {"error": str(e)}
253
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
227
254
 
228
255
  def change_password(self,
229
256
  email: str,
@@ -232,28 +259,28 @@ class ProfileService:
232
259
  confirm_password: str):
233
260
  try:
234
261
  if new_password != confirm_password:
235
- return {"error": "Las contraseñas no coinciden. Por favor, inténtalo nuevamente."}
262
+ return {"error": self.i18n_service.t('errors.change_password.password_mismatch')}
236
263
 
237
264
  # check the temporary code
238
265
  user = self.profile_repo.get_user_by_email(email)
239
266
  if not user or user.temp_code != temp_code:
240
- return {"error": "El código temporal no es válido. Por favor, verifica o solicita uno nuevo."}
267
+ return {"error": self.i18n_service.t('errors.change_password.invalid_temp_code')}
241
268
 
242
269
  # encrypt and save the password, make the temporary code invalid
243
270
  hashed_password = self.bcrypt.generate_password_hash(new_password).decode('utf-8')
244
271
  self.profile_repo.update_password(email, hashed_password)
245
272
  self.profile_repo.reset_temp_code(email)
246
273
 
247
- return {"message": "La clave se cambio correctamente"}
274
+ return {"message": self.i18n_service.t('flash_messages.password_changed_success')}
248
275
  except Exception as e:
249
- return {"error": str(e)}
276
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
250
277
 
251
278
  def forgot_password(self, email: str, reset_url: str):
252
279
  try:
253
280
  # Verificar si el usuario existe
254
281
  user = self.profile_repo.get_user_by_email(email)
255
282
  if not user:
256
- return {"error": "El usuario no existe."}
283
+ return {"error": self.i18n_service.t('errors.forgot_password.user_not_registered', email=email)}
257
284
 
258
285
  # Gen a temporary code and store in the repositories
259
286
  temp_code = ''.join(random.choices(string.ascii_letters + string.digits, k=6)).upper()
@@ -262,35 +289,31 @@ class ProfileService:
262
289
  # send email to the user
263
290
  self.send_forgot_password_email(user, reset_url)
264
291
 
265
- return {"message": "se envio mail para cambio de clave"}
292
+ return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
266
293
  except Exception as e:
267
- return {"error": str(e)}
294
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
268
295
 
269
296
  def validate_password(self, password):
270
297
  """
271
- Valida que una contraseña cumpla con los siguientes requisitos:
272
- - Al menos 8 caracteres de longitud
273
- - Contiene al menos una letra mayúscula
274
- - Contiene al menos una letra minúscula
275
- - Contiene al menos un número
276
- - Contiene al menos un carácter especial
298
+ Validates that a password meets all requirements.
299
+ Returns (True, "...") on success, or (False, "translation.key") on failure.
277
300
  """
278
301
  if len(password) < 8:
279
- return False, "La contraseña debe tener al menos 8 caracteres."
302
+ return False, "errors.validation.password_too_short"
280
303
 
281
304
  if not any(char.isupper() for char in password):
282
- return False, "La contraseña debe tener al menos una letra mayúscula."
305
+ return False, "errors.validation.password_no_uppercase"
283
306
 
284
307
  if not any(char.islower() for char in password):
285
- return False, "La contraseña debe tener al menos una letra minúscula."
308
+ return False, "errors.validation.password_no_lowercase"
286
309
 
287
310
  if not any(char.isdigit() for char in password):
288
- return False, "La contraseña debe tener al menos un número."
311
+ return False, "errors.validation.password_no_digit"
289
312
 
290
313
  if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
291
- return False, "La contraseña debe tener al menos un carácter especial."
314
+ return False, "errors.validation.password_no_special_char"
292
315
 
293
- return True, "La contraseña es válida."
316
+ return True, "Password is valid."
294
317
 
295
318
  def get_companies(self):
296
319
  return self.profile_repo.get_companies()
@@ -304,7 +327,7 @@ class ProfileService:
304
327
  def new_api_key(self, company_short_name: str):
305
328
  company = self.get_company_by_short_name(company_short_name)
306
329
  if not company:
307
- return {"error": f"la empresa {company_short_name} no existe"}
330
+ return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
308
331
 
309
332
  length = 40 # lenght of the api key
310
333
  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,8 @@ 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.company_context_service import CompanyContextService
12
+ from iatoolkit.services.i18n_service import I18nService
11
13
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
12
14
  from iatoolkit.repositories.models import Task
13
15
  from iatoolkit.services.dispatcher_service import Dispatcher
@@ -32,21 +34,25 @@ class QueryService:
32
34
  def __init__(self,
33
35
  llm_client: llmClient,
34
36
  profile_service: ProfileService,
37
+ company_context_service: CompanyContextService,
35
38
  document_service: DocumentService,
36
39
  document_repo: DocumentRepo,
37
40
  llmquery_repo: LLMQueryRepo,
38
41
  profile_repo: ProfileRepo,
39
42
  prompt_service: PromptService,
43
+ i18n_service: I18nService,
40
44
  util: Utility,
41
45
  dispatcher: Dispatcher,
42
46
  session_context: UserSessionContextService
43
47
  ):
44
48
  self.profile_service = profile_service
49
+ self.company_context_service = company_context_service
45
50
  self.document_service = document_service
46
51
  self.document_repo = document_repo
47
52
  self.llmquery_repo = llmquery_repo
48
53
  self.profile_repo = profile_repo
49
54
  self.prompt_service = prompt_service
55
+ self.i18n_service = i18n_service
50
56
  self.util = util
51
57
  self.dispatcher = dispatcher
52
58
  self.session_context = session_context
@@ -56,7 +62,7 @@ class QueryService:
56
62
  self.model = os.getenv("LLM_MODEL", "")
57
63
  if not self.model:
58
64
  raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
59
- "La variable de entorno 'LLM_MODEL' no está configurada.")
65
+ "missing ENV variable 'LLM_MODEL' configuration.")
60
66
 
61
67
  def _build_context_and_profile(self, company_short_name: str, user_identifier: str) -> tuple:
62
68
  # this method read the user/company context from the database and renders the system prompt
@@ -78,7 +84,7 @@ class QueryService:
78
84
  )
79
85
 
80
86
  # get the company context: schemas, database models, .md files
81
- company_specific_context = self.dispatcher.get_company_context(company_name=company_short_name)
87
+ company_specific_context = self.company_context_service.get_company_context(company_short_name)
82
88
 
83
89
  # merge context: company + user
84
90
  final_system_context = f"{company_specific_context}\n{rendered_system_prompt}"
@@ -127,7 +133,7 @@ class QueryService:
127
133
  lock_key = f"lock:context:{company_short_name}/{user_identifier}"
128
134
  if not self.session_context.acquire_lock(lock_key, expire_seconds=60):
129
135
  logging.warning(
130
- f"Intento de reconstruir contexto para {user_identifier} mientras ya estaba en progreso. Se omite.")
136
+ f"try to rebuild context for user {user_identifier} while is still in process, ignored.")
131
137
  return
132
138
 
133
139
  try:
@@ -138,11 +144,9 @@ class QueryService:
138
144
  prepared_context, version_to_save = self.session_context.get_and_clear_prepared_context(company_short_name,
139
145
  user_identifier)
140
146
  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
147
  return
144
148
 
145
- logging.info(f"Enviando contexto al LLM para {company_short_name}/{user_identifier}...")
149
+ logging.info(f"sending context to LLM for: {company_short_name}/{user_identifier}...")
146
150
 
147
151
  # Limpiar solo el historial de chat y el ID de respuesta anterior
148
152
  self.session_context.clear_llm_history(company_short_name, user_identifier)
@@ -161,9 +165,9 @@ class QueryService:
161
165
  self.session_context.save_context_version(company_short_name, user_identifier, version_to_save)
162
166
 
163
167
  logging.info(
164
- f"Contexto de {company_short_name}/{user_identifier} establecido en {int(time.time() - start_time)} seg.")
168
+ f"Context for: {company_short_name}/{user_identifier} settled in {int(time.time() - start_time)} sec.")
165
169
  except Exception as e:
166
- logging.exception(f"Error en finalize_context_rebuild para {company_short_name}: {e}")
170
+ logging.exception(f"Error in finalize_context_rebuild for {company_short_name}: {e}")
167
171
  raise e
168
172
  finally:
169
173
  # --- Liberar el Bloqueo ---
@@ -181,11 +185,11 @@ class QueryService:
181
185
  company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
182
186
  if not company:
183
187
  return {"error": True,
184
- "error_message": f'No existe Company ID: {company_short_name}'}
188
+ "error_message": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
185
189
 
186
190
  if not prompt_name and not question:
187
191
  return {"error": True,
188
- "error_message": f'Hola, cual es tu pregunta?'}
192
+ "error_message": self.i18n_service.t('services.start_query')}
189
193
 
190
194
  # get the previous response_id and context history
191
195
  previous_response_id = None
@@ -196,7 +200,7 @@ class QueryService:
196
200
  previous_response_id = self.session_context.get_last_response_id(company.short_name, user_identifier)
197
201
  if not previous_response_id:
198
202
  return {'error': True,
199
- "error_message": f"No se encontró 'previous_response_id' para '{company.short_name}/{user_identifier}'. Reinicia el contexto para continuar."
203
+ "error_message": self.i18n_service.t('errors.services.missing_response_id', company_short_name=company.short_name, user_identifier=user_identifier)
200
204
  }
201
205
  elif self.util.is_gemini_model(self.model):
202
206
  # check the length of the context_history and remove old messages
@@ -294,7 +298,7 @@ class QueryService:
294
298
  return len(history) >= 1
295
299
  return False
296
300
  except Exception as e:
297
- logging.warning(f"Error verificando caché de contexto: {e}")
301
+ logging.warning(f"error verifying context cache: {e}")
298
302
  return False
299
303
 
300
304
  def load_files_for_context(self, files: list) -> str:
@@ -353,7 +357,7 @@ class QueryService:
353
357
  try:
354
358
  total_tokens = sum(self.llm_client.count_tokens(json.dumps(message)) for message in context_history)
355
359
  except Exception as e:
356
- logging.error(f"Error al calcular tokens del historial: {e}. No se pudo recortar el contexto.")
360
+ logging.error(f"error counting tokens for history: {e}.")
357
361
  return
358
362
 
359
363
  # Si se excede el límite, eliminar mensajes antiguos (empezando por el segundo)
@@ -364,8 +368,8 @@ class QueryService:
364
368
  removed_tokens = self.llm_client.count_tokens(json.dumps(removed_message))
365
369
  total_tokens -= removed_tokens
366
370
  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."
371
+ f"history tokens ({total_tokens + removed_tokens} tokens) exceed the limit of: {GEMINI_MAX_TOKENS_CONTEXT_HISTORY}. "
372
+ f"new context: {total_tokens} tokens."
369
373
  )
370
374
  except IndexError:
371
375
  # Se produce si solo queda el mensaje del sistema, el bucle debería detenerse.
@@ -5,19 +5,22 @@
5
5
 
6
6
  from iatoolkit.repositories.vs_repo import VSRepo
7
7
  from iatoolkit.repositories.document_repo import DocumentRepo
8
+ from iatoolkit.repositories.profile_repo import ProfileRepo
9
+ from iatoolkit.repositories.models import Company
8
10
  from injector import inject
9
11
 
10
12
 
11
13
  class SearchService:
12
14
  @inject
13
15
  def __init__(self,
16
+ profile_repo: ProfileRepo,
14
17
  doc_repo: DocumentRepo,
15
18
  vs_repo: VSRepo):
16
- super().__init__()
19
+ self.profile_repo = profile_repo
17
20
  self.vs_repo = vs_repo
18
21
  self.doc_repo = doc_repo
19
22
 
20
- def search(self, company_id: int, query: str, metadata_filter: dict = None) -> str:
23
+ def search(self, company_short_name: str, query: str, metadata_filter: dict = None) -> str:
21
24
  """
22
25
  Performs a semantic search for a given query within a company's documents.
23
26
 
@@ -26,7 +29,7 @@ class SearchService:
26
29
  content of the retrieved documents, which can be used as context for an LLM.
27
30
 
28
31
  Args:
29
- company_id: The ID of the company to search within.
32
+ company_short_name: The company to search within.
30
33
  query: The text query to search for.
31
34
  metadata_filter: An optional dictionary to filter documents by their metadata.
32
35
 
@@ -34,7 +37,11 @@ class SearchService:
34
37
  A string containing the concatenated content of the found documents,
35
38
  formatted to be used as a context.
36
39
  """
37
- document_list = self.vs_repo.query(company_id=company_id,
40
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
41
+ if not company:
42
+ return f"error: company {company_short_name} not found"
43
+
44
+ document_list = self.vs_repo.query(company_id=company.id,
38
45
  query_text=query,
39
46
  metadata_filter=metadata_filter)
40
47