iatoolkit 0.11.0__py3-none-any.whl → 0.66.2__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.
Files changed (106) hide show
  1. iatoolkit/base_company.py +11 -3
  2. iatoolkit/common/routes.py +92 -52
  3. iatoolkit/common/session_manager.py +0 -1
  4. iatoolkit/common/util.py +17 -27
  5. iatoolkit/iatoolkit.py +91 -47
  6. iatoolkit/infra/llm_client.py +7 -8
  7. iatoolkit/infra/openai_adapter.py +1 -1
  8. iatoolkit/infra/redis_session_manager.py +48 -2
  9. iatoolkit/locales/en.yaml +144 -0
  10. iatoolkit/locales/es.yaml +140 -0
  11. iatoolkit/repositories/database_manager.py +17 -2
  12. iatoolkit/repositories/models.py +31 -4
  13. iatoolkit/repositories/profile_repo.py +7 -2
  14. iatoolkit/services/auth_service.py +193 -0
  15. iatoolkit/services/branding_service.py +59 -18
  16. iatoolkit/services/dispatcher_service.py +10 -40
  17. iatoolkit/services/excel_service.py +15 -15
  18. iatoolkit/services/help_content_service.py +30 -0
  19. iatoolkit/services/history_service.py +2 -11
  20. iatoolkit/services/i18n_service.py +104 -0
  21. iatoolkit/services/jwt_service.py +15 -24
  22. iatoolkit/services/language_service.py +77 -0
  23. iatoolkit/services/onboarding_service.py +43 -0
  24. iatoolkit/services/profile_service.py +148 -75
  25. iatoolkit/services/query_service.py +124 -81
  26. iatoolkit/services/tasks_service.py +1 -1
  27. iatoolkit/services/user_feedback_service.py +68 -32
  28. iatoolkit/services/user_session_context_service.py +112 -54
  29. iatoolkit/static/images/fernando.jpeg +0 -0
  30. iatoolkit/static/js/chat_feedback_button.js +80 -0
  31. iatoolkit/static/js/chat_help_content.js +124 -0
  32. iatoolkit/static/js/chat_history_button.js +112 -0
  33. iatoolkit/static/js/chat_logout_button.js +36 -0
  34. iatoolkit/static/js/chat_main.js +148 -220
  35. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  36. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  37. iatoolkit/static/js/chat_reload_button.js +35 -0
  38. iatoolkit/static/styles/chat_iatoolkit.css +367 -199
  39. iatoolkit/static/styles/chat_modal.css +98 -76
  40. iatoolkit/static/styles/chat_public.css +107 -0
  41. iatoolkit/static/styles/landing_page.css +182 -0
  42. iatoolkit/static/styles/onboarding.css +169 -0
  43. iatoolkit/system_prompts/query_main.prompt +3 -12
  44. iatoolkit/templates/_company_header.html +20 -0
  45. iatoolkit/templates/_login_widget.html +42 -0
  46. iatoolkit/templates/base.html +40 -20
  47. iatoolkit/templates/change_password.html +57 -36
  48. iatoolkit/templates/chat.html +169 -83
  49. iatoolkit/templates/chat_modals.html +134 -68
  50. iatoolkit/templates/error.html +44 -8
  51. iatoolkit/templates/forgot_password.html +40 -23
  52. iatoolkit/templates/index.html +145 -0
  53. iatoolkit/templates/login_simulation.html +34 -0
  54. iatoolkit/templates/onboarding_shell.html +104 -0
  55. iatoolkit/templates/signup.html +63 -65
  56. iatoolkit/views/base_login_view.py +92 -0
  57. iatoolkit/views/change_password_view.py +56 -30
  58. iatoolkit/views/external_login_view.py +61 -28
  59. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
  60. iatoolkit/views/forgot_password_view.py +27 -19
  61. iatoolkit/views/help_content_api_view.py +54 -0
  62. iatoolkit/views/history_api_view.py +56 -0
  63. iatoolkit/views/home_view.py +50 -23
  64. iatoolkit/views/index_view.py +14 -0
  65. iatoolkit/views/init_context_api_view.py +73 -0
  66. iatoolkit/views/llmquery_api_view.py +57 -0
  67. iatoolkit/views/login_simulation_view.py +81 -0
  68. iatoolkit/views/login_view.py +130 -37
  69. iatoolkit/views/logout_api_view.py +49 -0
  70. iatoolkit/views/profile_api_view.py +46 -0
  71. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +10 -10
  72. iatoolkit/views/signup_view.py +42 -35
  73. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  74. iatoolkit/views/tasks_review_api_view.py +55 -0
  75. iatoolkit/views/user_feedback_api_view.py +60 -0
  76. iatoolkit/views/verify_user_view.py +35 -28
  77. {iatoolkit-0.11.0.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
  78. iatoolkit-0.66.2.dist-info/RECORD +119 -0
  79. iatoolkit/common/auth.py +0 -200
  80. iatoolkit/static/images/arrow_up.png +0 -0
  81. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  82. iatoolkit/static/images/logo_clinica.png +0 -0
  83. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  84. iatoolkit/static/images/logo_maxxa.png +0 -0
  85. iatoolkit/static/images/logo_notaria.png +0 -0
  86. iatoolkit/static/images/logo_tarjeta.png +0 -0
  87. iatoolkit/static/images/logo_umayor.png +0 -0
  88. iatoolkit/static/images/upload.png +0 -0
  89. iatoolkit/static/js/chat_feedback.js +0 -115
  90. iatoolkit/static/js/chat_history.js +0 -117
  91. iatoolkit/static/styles/chat_info.css +0 -53
  92. iatoolkit/templates/header.html +0 -31
  93. iatoolkit/templates/home.html +0 -199
  94. iatoolkit/templates/login.html +0 -43
  95. iatoolkit/templates/test.html +0 -9
  96. iatoolkit/views/chat_token_request_view.py +0 -98
  97. iatoolkit/views/chat_view.py +0 -58
  98. iatoolkit/views/download_file_view.py +0 -58
  99. iatoolkit/views/external_chat_login_view.py +0 -95
  100. iatoolkit/views/history_view.py +0 -57
  101. iatoolkit/views/llmquery_view.py +0 -65
  102. iatoolkit/views/tasks_review_view.py +0 -83
  103. iatoolkit/views/user_feedback_view.py +0 -74
  104. iatoolkit-0.11.0.dist-info/RECORD +0 -110
  105. {iatoolkit-0.11.0.dist-info → iatoolkit-0.66.2.dist-info}/WHEEL +0 -0
  106. {iatoolkit-0.11.0.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -0
@@ -5,91 +5,163 @@
5
5
 
6
6
  from injector import inject
7
7
  from iatoolkit.repositories.profile_repo import ProfileRepo
8
+ from iatoolkit.services.i18n_service import I18nService
8
9
  from iatoolkit.repositories.models import User, Company, ApiKey
9
10
  from flask_bcrypt import check_password_hash
10
11
  from iatoolkit.common.session_manager import SessionManager
12
+ from iatoolkit.services.user_session_context_service import UserSessionContextService
11
13
  from flask_bcrypt import Bcrypt
12
14
  from iatoolkit.infra.mail_app import MailApp
13
15
  import random
14
- import logging
15
16
  import re
16
17
  import secrets
17
18
  import string
18
- from datetime import datetime, timezone
19
- from iatoolkit.services.user_session_context_service import UserSessionContextService
20
- from iatoolkit.services.query_service import QueryService
19
+ import logging
20
+ from iatoolkit.services.dispatcher_service import Dispatcher
21
21
 
22
22
 
23
23
  class ProfileService:
24
24
  @inject
25
25
  def __init__(self,
26
+ i18n_service: I18nService,
26
27
  profile_repo: ProfileRepo,
27
28
  session_context_service: UserSessionContextService,
28
- query_service: QueryService,
29
+ dispatcher: Dispatcher,
29
30
  mail_app: MailApp):
31
+ self.i18n_service = i18n_service
30
32
  self.profile_repo = profile_repo
33
+ self.dispatcher = dispatcher
31
34
  self.session_context = session_context_service
32
- self.query_service = query_service
33
35
  self.mail_app = mail_app
34
36
  self.bcrypt = Bcrypt()
35
37
 
36
38
 
37
39
  def login(self, company_short_name: str, email: str, password: str) -> dict:
38
40
  try:
39
- # check if exits
41
+ # check if user exists
40
42
  user = self.profile_repo.get_user_by_email(email)
41
43
  if not user:
42
- return {"error": "Usuario no encontrado"}
44
+ return {'success': False, 'message': self.i18n_service.t('errors.auth.user_not_found')}
43
45
 
44
46
  # check the encrypted password
45
47
  if not check_password_hash(user.password, password):
46
- return {"error": "Contraseña inválida"}
48
+ return {'success': False, 'message': self.i18n_service.t('errors.auth.invalid_password')}
47
49
 
48
- company = self.get_company_by_short_name(company_short_name)
50
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
49
51
  if not company:
50
- return {"error": "Empresa no encontrada"}
52
+ return {'success': False, "message": "Empresa no encontrada"}
51
53
 
52
- # check that user belongs to company
54
+ # check that user belongs to company
53
55
  if company not in user.companies:
54
- return {"error": "Usuario no esta autorizado para esta empresa"}
56
+ return {'success': False, "message": "Usuario no esta autorizado para esta empresa"}
55
57
 
56
58
  if not user.verified:
57
- return {"error": "Tu cuenta no ha sido verificada. Por favor, revisa tu correo."}
59
+ return {'success': False,
60
+ "message": "Tu cuenta no ha sido verificada. Por favor, revisa tu correo."}
61
+
62
+ # 1. Build the local user profile dictionary here.
63
+ # the user_profile variables are used on the LLM templates also (see in query_main.prompt)
64
+ user_identifier = user.email # no longer de ID
65
+ user_profile = {
66
+ "user_email": user.email,
67
+ "user_fullname": f'{user.first_name} {user.last_name}',
68
+ "user_is_local": True,
69
+ "extras": {}
70
+ }
71
+
72
+ # 2. create user_profile in context
73
+ self.save_user_profile(company, user_identifier, user_profile)
74
+
75
+ # 3. create the web session
76
+ self.set_session_for_user(company.short_name, user_identifier)
77
+ return {'success': True, "user_identifier": user_identifier, "message": "Login exitoso"}
78
+ except Exception as e:
79
+ logging.error(f"Error in login: {e}")
80
+ return {'success': False, "message": str(e)}
58
81
 
59
- # clear history save user data into session manager
60
- self.set_user_session(user=user, company=company)
82
+ def create_external_user_profile_context(self, company: Company, user_identifier: str):
83
+ """
84
+ Public method for views to create a user profile context for an external user.
85
+ """
86
+ # 1. Fetch the external user profile via Dispatcher.
87
+ external_user_profile = self.dispatcher.get_user_info(
88
+ company_name=company.short_name,
89
+ user_identifier=user_identifier
90
+ )
91
+
92
+ # 2. Call the session creation helper with external_user_id as user_identifier
93
+ self.save_user_profile(
94
+ company=company,
95
+ user_identifier=user_identifier,
96
+ user_profile=external_user_profile)
97
+
98
+ def save_user_profile(self, company: Company, user_identifier: str, user_profile: dict):
99
+ """
100
+ Private helper: Takes a pre-built profile, saves it to Redis, and sets the Flask cookie.
101
+ """
102
+ user_profile['company_short_name'] = company.short_name
103
+ user_profile['user_identifier'] = user_identifier
104
+ user_profile['id'] = user_identifier
105
+ user_profile['company_id'] = company.id
106
+ user_profile['company'] = company.name
61
107
 
62
- # initialize company context
63
- self.query_service.llm_init_context(
64
- company_short_name=company_short_name,
65
- local_user_id=user.id
66
- )
108
+ # save user_profile in Redis session
109
+ self.session_context.save_profile_data(company.short_name, user_identifier, user_profile)
67
110
 
68
- return {"message": "Login exitoso"}
69
- except Exception as e:
70
- logging.exception(f"login error: {str(e)}")
71
- return {"error": str(e)}
72
-
73
- def set_user_session(self, user: User, company: Company):
74
- SessionManager.set('user_id', user.id)
75
- SessionManager.set('company_id', company.id)
76
- SessionManager.set('company_short_name', company.short_name)
77
-
78
- # save user data into session manager
79
- user_data = {
80
- "id": user.id,
81
- "email": user.email,
82
- "user_fullname": f'{user.first_name} {user.last_name}',
83
- "company_id": company.id,
84
- "company": company.name,
85
- "company_short_name": company.short_name,
86
- "user_is_local": True, # origin of data
87
- "extras": {} # company specific data
111
+ def set_session_for_user(self, company_short_name: str, user_identifier:str ):
112
+ # save a min Flask session cookie for this user
113
+ SessionManager.set('company_short_name', company_short_name)
114
+ SessionManager.set('user_identifier', user_identifier)
115
+
116
+ def get_current_session_info(self) -> dict:
117
+ """
118
+ Gets the current web user's profile from the unified session.
119
+ This is the standard way to access user data for web requests.
120
+ """
121
+ # 1. Get identifiers from the simple Flask session cookie.
122
+ user_identifier = SessionManager.get('user_identifier')
123
+ company_short_name = SessionManager.get('company_short_name')
124
+
125
+ if not user_identifier or not company_short_name:
126
+ # No authenticated web user.
127
+ return {}
128
+
129
+ # 2. Use the identifiers to fetch the full, authoritative profile from Redis.
130
+ profile = self.session_context.get_profile_data(company_short_name, user_identifier)
131
+
132
+ return {
133
+ "user_identifier": user_identifier,
134
+ "company_short_name": company_short_name,
135
+ "profile": profile
88
136
  }
89
- SessionManager.set('user', user_data)
90
137
 
91
- # save time session was activated (in timestamp format)
92
- SessionManager.set('last_activity', datetime.now(timezone.utc).timestamp())
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')}
155
+
156
+
157
+ def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
158
+ """
159
+ Fetches a user profile directly by their identifier, bypassing the Flask session.
160
+ This is ideal for API-side checks.
161
+ """
162
+ if not company_short_name or not user_identifier:
163
+ return {}
164
+ return self.session_context.get_profile_data(company_short_name, user_identifier)
93
165
 
94
166
 
95
167
  def signup(self,
@@ -103,9 +175,10 @@ class ProfileService:
103
175
  try:
104
176
 
105
177
  # get company info
106
- company = self.get_company_by_short_name(company_short_name)
178
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
107
179
  if not company:
108
- 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)}
109
182
 
110
183
  # normalize format's
111
184
  email = email.lower()
@@ -115,24 +188,25 @@ class ProfileService:
115
188
  if existing_user:
116
189
  # validate password
117
190
  if not self.bcrypt.check_password_hash(existing_user.password, password):
118
- 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)}
119
192
 
120
193
  # check if register
121
194
  if company in existing_user.companies:
122
- return {"error": "Usuario ya registrado en esta empresa"}
195
+ return {"error": self.i18n_service.t('errors.signup.user_already_registered', email=email)}
123
196
  else:
124
197
  # add new company to existing user
125
198
  existing_user.companies.append(company)
126
199
  self.profile_repo.save_user(existing_user)
127
- return {"message": "Usuario asociado a nueva empresa"}
200
+ return {"message": self.i18n_service.t('flash_messages.user_associated_success')}
128
201
 
129
202
  # add the new user
130
203
  if password != confirm_password:
131
- return {"error": "Las contraseñas no coinciden. Por favor, inténtalo de nuevo."}
204
+ return {"error": self.i18n_service.t('errors.signup.password_mismatch')}
132
205
 
133
206
  is_valid, message = self.validate_password(password)
134
207
  if not is_valid:
135
- return {"error": message}
208
+ # Translate the key returned by validate_password
209
+ return {"error": self.i18n_service.t(message)}
136
210
 
137
211
  # encrypt the password
138
212
  hashed_password = self.bcrypt.generate_password_hash(password).decode('utf-8')
@@ -154,9 +228,9 @@ class ProfileService:
154
228
  # send email with verification
155
229
  self.send_verification_email(new_user, company_short_name)
156
230
 
157
- return {"message": "Registro exitoso. Por favor, revisa tu correo para verificar tu cuenta."}
231
+ return {"message": self.i18n_service.t('flash_messages.signup_success')}
158
232
  except Exception as e:
159
- return {"error": str(e)}
233
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
160
234
 
161
235
  def update_user(self, email: str, **kwargs) -> User:
162
236
  return self.profile_repo.update_user(email, **kwargs)
@@ -166,14 +240,14 @@ class ProfileService:
166
240
  # check if user exist
167
241
  user = self.profile_repo.get_user_by_email(email)
168
242
  if not user:
169
- return {"error": "El usuario no existe."}
243
+ return {"error": self.i18n_service.t('errors.verification.user_not_found')}
170
244
 
171
245
  # activate the user account
172
246
  self.profile_repo.verify_user(email)
173
- 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')}
174
248
 
175
249
  except Exception as e:
176
- return {"error": str(e)}
250
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
177
251
 
178
252
  def change_password(self,
179
253
  email: str,
@@ -182,28 +256,28 @@ class ProfileService:
182
256
  confirm_password: str):
183
257
  try:
184
258
  if new_password != confirm_password:
185
- return {"error": "Las contraseñas no coinciden. Por favor, inténtalo nuevamente."}
259
+ return {"error": self.i18n_service.t('errors.change_password.password_mismatch')}
186
260
 
187
261
  # check the temporary code
188
262
  user = self.profile_repo.get_user_by_email(email)
189
263
  if not user or user.temp_code != temp_code:
190
- 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')}
191
265
 
192
266
  # encrypt and save the password, make the temporary code invalid
193
267
  hashed_password = self.bcrypt.generate_password_hash(new_password).decode('utf-8')
194
268
  self.profile_repo.update_password(email, hashed_password)
195
269
  self.profile_repo.reset_temp_code(email)
196
270
 
197
- return {"message": "La clave se cambio correctamente"}
271
+ return {"message": self.i18n_service.t('flash_messages.password_changed_success')}
198
272
  except Exception as e:
199
- return {"error": str(e)}
273
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
200
274
 
201
275
  def forgot_password(self, email: str, reset_url: str):
202
276
  try:
203
277
  # Verificar si el usuario existe
204
278
  user = self.profile_repo.get_user_by_email(email)
205
279
  if not user:
206
- return {"error": "El usuario no existe."}
280
+ return {"error": self.i18n_service.t('errors.forgot_password.user_not_registered', email=email)}
207
281
 
208
282
  # Gen a temporary code and store in the repositories
209
283
  temp_code = ''.join(random.choices(string.ascii_letters + string.digits, k=6)).upper()
@@ -212,35 +286,31 @@ class ProfileService:
212
286
  # send email to the user
213
287
  self.send_forgot_password_email(user, reset_url)
214
288
 
215
- return {"message": "se envio mail para cambio de clave"}
289
+ return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
216
290
  except Exception as e:
217
- return {"error": str(e)}
291
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
218
292
 
219
293
  def validate_password(self, password):
220
294
  """
221
- Valida que una contraseña cumpla con los siguientes requisitos:
222
- - Al menos 8 caracteres de longitud
223
- - Contiene al menos una letra mayúscula
224
- - Contiene al menos una letra minúscula
225
- - Contiene al menos un número
226
- - 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.
227
297
  """
228
298
  if len(password) < 8:
229
- return False, "La contraseña debe tener al menos 8 caracteres."
299
+ return False, "errors.validation.password_too_short"
230
300
 
231
301
  if not any(char.isupper() for char in password):
232
- return False, "La contraseña debe tener al menos una letra mayúscula."
302
+ return False, "errors.validation.password_no_uppercase"
233
303
 
234
304
  if not any(char.islower() for char in password):
235
- return False, "La contraseña debe tener al menos una letra minúscula."
305
+ return False, "errors.validation.password_no_lowercase"
236
306
 
237
307
  if not any(char.isdigit() for char in password):
238
- return False, "La contraseña debe tener al menos un número."
308
+ return False, "errors.validation.password_no_digit"
239
309
 
240
310
  if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
241
- return False, "La contraseña debe tener al menos un carácter especial."
311
+ return False, "errors.validation.password_no_special_char"
242
312
 
243
- return True, "La contraseña es válida."
313
+ return True, "Password is valid."
244
314
 
245
315
  def get_companies(self):
246
316
  return self.profile_repo.get_companies()
@@ -248,6 +318,9 @@ class ProfileService:
248
318
  def get_company_by_short_name(self, short_name: str) -> Company:
249
319
  return self.profile_repo.get_company_by_short_name(short_name)
250
320
 
321
+ def get_active_api_key_entry(self, api_key_value: str) -> ApiKey | None:
322
+ return self.profile_repo.get_active_api_key_entry(api_key_value)
323
+
251
324
  def new_api_key(self, company_short_name: str):
252
325
  company = self.get_company_by_short_name(company_short_name)
253
326
  if not company: