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
@@ -4,6 +4,7 @@
4
4
  # IAToolkit is open source software.
5
5
 
6
6
  from iatoolkit.repositories.models import Company
7
+ from injector import inject
7
8
 
8
9
 
9
10
  class BrandingService:
@@ -11,6 +12,7 @@ class BrandingService:
11
12
  Servicio centralizado que gestiona la configuración de branding.
12
13
  """
13
14
 
15
+ @inject
14
16
  def __init__(self):
15
17
  """
16
18
  Define los estilos de branding por defecto para la aplicación.
@@ -19,13 +21,16 @@ class BrandingService:
19
21
  # --- Estilos del Encabezado Principal ---
20
22
  "header_background_color": "#FFFFFF",
21
23
  "header_text_color": "#6C757D",
22
- "primary_font_weight": "bold",
23
- "primary_font_size": "1rem",
24
- "secondary_font_weight": "600",
25
- "secondary_font_size": "0.875rem",
26
- "tertiary_font_weight": "normal",
27
- "tertiary_font_size": "0.75rem",
28
- "tertiary_opacity": "0.8",
24
+ "primary_font_weight": "600",
25
+ "primary_font_size": "1.2rem",
26
+ "secondary_font_weight": "400",
27
+ "secondary_font_size": "0.9rem",
28
+ "tertiary_font_weight": "300",
29
+ "tertiary_font_size": "0.8rem",
30
+ "tertiary_opacity": "0.7",
31
+
32
+ # headings
33
+ "brand_text_heading_color": "#334155", # Gris pizarra por defecto
29
34
 
30
35
  # Estilos Globales de la Marca ---
31
36
  "brand_primary_color": "#0d6efd", # Azul de Bootstrap por defecto
@@ -40,12 +45,27 @@ class BrandingService:
40
45
  "brand_danger_border": "#f5c2c7", # Borde rojo intermedio
41
46
 
42
47
  # Estilos para Alertas Informativas ---
43
- "brand_info_bg": "#cff4fc", # Fondo celeste pálido
44
- "brand_info_text": "#055160", # Texto azul oscuro
45
- "brand_info_border": "#b6effb",
48
+ "brand_info_bg": "#F0F4F8", # Un fondo de gris azulado muy pálido
49
+ "brand_info_text": "#0d6efd", # Texto en el color primario
50
+ "brand_info_border": "#D9E2EC", # Borde de gris azulado pálido
51
+
52
+ # Estilos para el Asistente de Prompts ---
53
+ "prompt_assistant_bg": "#f8f9fa",
54
+ "prompt_assistant_border": "#dee2e6",
55
+ "prompt_assistant_button_bg": "#FFFFFF",
56
+ "prompt_assistant_button_text": "#495057",
57
+ "prompt_assistant_button_border": "#ced4da",
58
+ "prompt_assistant_dropdown_bg": "#f8f9fa",
59
+ "prompt_assistant_header_bg": "#e9ecef",
60
+ "prompt_assistant_header_text": "#495057",
61
+
62
+ # this use the primary by default
63
+ "prompt_assistant_icon_color": None,
64
+ "prompt_assistant_item_hover_bg": None,
65
+ "prompt_assistant_item_hover_text": None,
46
66
 
47
67
  # Color para el botón de Enviar ---
48
- "send_button_color": "#212529" # Gris oscuro/casi negro por defecto
68
+ "send_button_color": "#212529" # Gris oscuro/casi negro por defecto
49
69
  }
50
70
 
51
71
  def get_company_branding(self, company: Company | None) -> dict:
@@ -58,11 +78,15 @@ class BrandingService:
58
78
  if company and company.branding:
59
79
  final_branding_values.update(company.branding)
60
80
 
81
+ # Función para convertir HEX a RGB
82
+ def hex_to_rgb(hex_color):
83
+ hex_color = hex_color.lstrip('#')
84
+ return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4))
85
+
86
+ primary_rgb = hex_to_rgb(final_branding_values['brand_primary_color'])
87
+ secondary_rgb = hex_to_rgb(final_branding_values['brand_secondary_color'])
88
+
61
89
  # --- CONSTRUCCIÓN DE ESTILOS Y VARIABLES CSS ---
62
- header_style = (
63
- f"background-color: {final_branding_values['header_background_color']}; "
64
- f"color: {final_branding_values['header_text_color']};"
65
- )
66
90
  primary_text_style = (
67
91
  f"font-weight: {final_branding_values['primary_font_weight']}; "
68
92
  f"font-size: {final_branding_values['primary_font_size']};"
@@ -82,6 +106,12 @@ class BrandingService:
82
106
  :root {{
83
107
  --brand-primary-color: {final_branding_values['brand_primary_color']};
84
108
  --brand-secondary-color: {final_branding_values['brand_secondary_color']};
109
+ --brand-header-bg: {final_branding_values['header_background_color']};
110
+ --brand-header-text: {final_branding_values['header_text_color']};
111
+ --brand-text-heading-color: {final_branding_values['brand_text_heading_color']};
112
+
113
+ --brand-primary-color-rgb: {', '.join(map(str, primary_rgb))};
114
+ --brand-secondary-color-rgb: {', '.join(map(str, secondary_rgb))};
85
115
  --brand-text-on-primary: {final_branding_values['brand_text_on_primary']};
86
116
  --brand-text-on-secondary: {final_branding_values['brand_text_on_secondary']};
87
117
  --brand-modal-header-bg: {final_branding_values['header_background_color']};
@@ -91,18 +121,29 @@ class BrandingService:
91
121
  --brand-danger-text: {final_branding_values['brand_danger_text']};
92
122
  --brand-danger-border: {final_branding_values['brand_danger_border']};
93
123
  --brand-info-bg: {final_branding_values['brand_info_bg']};
94
- --brand-info-text: {final_branding_values['brand_info_text']};
124
+ --brand-info-text: {final_branding_values['brand_info_text'] or final_branding_values['brand_primary_color']};
95
125
  --brand-info-border: {final_branding_values['brand_info_border']};
126
+ --brand-prompt-assistant-bg: {final_branding_values['prompt_assistant_bg']};
127
+ --brand-prompt-assistant-border: {final_branding_values['prompt_assistant_border']};
128
+ --brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color'] or final_branding_values['brand_primary_color']};
129
+ --brand-prompt-assistant-button-bg: {final_branding_values['prompt_assistant_button_bg']};
130
+ --brand-prompt-assistant-button-text: {final_branding_values['prompt_assistant_button_text']};
131
+ --brand-prompt-assistant-button-border: {final_branding_values['prompt_assistant_button_border']};
132
+ --brand-prompt-assistant-dropdown-bg: {final_branding_values['prompt_assistant_dropdown_bg']};
133
+ --brand-prompt-assistant-header-bg: {final_branding_values['prompt_assistant_header_bg']};
134
+ --brand-prompt-assistant-header-text: {final_branding_values['prompt_assistant_header_text']};
135
+ --brand-prompt-assistant-item-hover-bg: {final_branding_values['prompt_assistant_item_hover_bg'] or final_branding_values['brand_primary_color']};
136
+ --brand-prompt-assistant-item-hover-text: {final_branding_values['prompt_assistant_item_hover_text'] or final_branding_values['brand_text_on_primary']};
96
137
 
97
138
  }}
98
139
  """
99
140
 
100
141
  return {
101
142
  "name": company.name if company else "IAToolkit",
102
- "header_style": header_style,
103
143
  "primary_text_style": primary_text_style,
104
144
  "secondary_text_style": secondary_text_style,
105
145
  "tertiary_text_style": tertiary_text_style,
106
146
  "header_text_color": final_branding_values['header_text_color'],
107
- "css_variables": css_variables
147
+ "css_variables": css_variables,
148
+ "send_button_color": final_branding_values['brand_primary_color']
108
149
  }
@@ -10,7 +10,6 @@ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
10
10
  from iatoolkit.repositories.models import Company, Function
11
11
  from iatoolkit.services.excel_service import ExcelService
12
12
  from iatoolkit.services.mail_service import MailService
13
- from iatoolkit.common.session_manager import SessionManager
14
13
  from iatoolkit.common.util import Utility
15
14
  from injector import inject
16
15
  import logging
@@ -171,50 +170,21 @@ class Dispatcher:
171
170
  tools.append(ai_tool)
172
171
  return tools
173
172
 
174
- def get_user_info(self, company_name: str, user_identifier: str, is_local_user: bool) -> dict:
173
+ def get_user_info(self, company_name: str, user_identifier: str) -> dict:
175
174
  if company_name not in self.company_instances:
176
175
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
177
176
  f"Empresa no configurada: {company_name}")
178
177
 
179
- raw_user_data = {}
180
- if is_local_user:
181
- # source 1: local user login into IAToolkit
182
- raw_user_data = SessionManager.get('user', {})
183
- else:
184
- # source 2: external company user
185
- company_instance = self.company_instances[company_name]
186
- try:
187
- raw_user_data = company_instance.get_user_info(user_identifier)
188
- except Exception as e:
189
- logging.exception(e)
190
- raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
191
- f"Error en get_user_info de {company_name}: {str(e)}") from e
192
-
193
- # always normalize the data for consistent structure
194
- return self._normalize_user_data(raw_user_data, is_local_user)
195
-
196
- def _normalize_user_data(self, raw_data: dict, is_local: bool) -> dict:
197
- """
198
- Asegura que los datos del usuario siempre tengan una estructura consistente.
199
- """
200
- # default values
201
- normalized_user = {
202
- "id": raw_data.get("id", 0),
203
- "user_email": raw_data.get("email", ""),
204
- "user_fullname": raw_data.get("user_fullname", ""),
205
- "company_id": raw_data.get("company_id", 0),
206
- "company_name": raw_data.get("company", ""),
207
- "company_short_name": raw_data.get("company_short_name", ""),
208
- "is_local": is_local,
209
- "extras": raw_data.get("extras", {})
210
- }
211
-
212
- # get the extras from the raw data, if any
213
- extras = raw_data.get("extras", {})
214
- if isinstance(extras, dict):
215
- normalized_user.update(extras)
178
+ # source 2: external company user
179
+ company_instance = self.company_instances[company_name]
180
+ try:
181
+ external_user_profile = company_instance.get_user_info(user_identifier)
182
+ except Exception as e:
183
+ logging.exception(e)
184
+ raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
185
+ f"Error en get_user_info de {company_name}: {str(e)}") from e
216
186
 
217
- return normalized_user
187
+ return external_user_profile
218
188
 
219
189
  def get_metadata_from_filename(self, company_name: str, filename: str) -> dict:
220
190
  if company_name not in self.company_instances:
@@ -23,21 +23,21 @@ class ExcelService:
23
23
 
24
24
  def excel_generator(self, **kwargs) -> str:
25
25
  """
26
- Genera un Excel a partir de una lista de diccionarios.
27
-
28
- Parámetros esperados en kwargs:
29
- - filename: str (nombre lógico a mostrar, ej. "reporte_clientes.xlsx") [obligatorio]
30
- - data: list[dict] (filas del excel) [obligatorio]
31
- - sheet_name: str = "hoja 1"
32
-
33
- Retorna:
34
- {
35
- "filename": "reporte.xlsx",
36
- "attachment_token": "8b7f8a66-...-c1c3.xlsx",
37
- "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
38
- "download_link": "/download/8b7f8a66-...-c1c3.xlsx"
39
- }
40
- """
26
+ Genera un Excel a partir de una lista de diccionarios.
27
+
28
+ Parámetros esperados en kwargs:
29
+ - filename: str (nombre lógico a mostrar, ej. "reporte_clientes.xlsx") [obligatorio]
30
+ - data: list[dict] (filas del excel) [obligatorio]
31
+ - sheet_name: str = "hoja 1"
32
+
33
+ Retorna:
34
+ {
35
+ "filename": "reporte.xlsx",
36
+ "attachment_token": "8b7f8a66-...-c1c3.xlsx",
37
+ "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
38
+ "download_link": "/download/8b7f8a66-...-c1c3.xlsx"
39
+ }
40
+ """
41
41
  try:
42
42
  # get the parameters
43
43
  fname = kwargs.get('filename')
@@ -0,0 +1,30 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.common.util import Utility
7
+ from iatoolkit.common.exceptions import IAToolkitException
8
+ import os
9
+ from injector import inject
10
+ import logging
11
+
12
+
13
+ class HelpContentService:
14
+ @inject
15
+ def __init__(self, util: Utility):
16
+ self.util = util
17
+
18
+ def get_content(self, company_short_name: str | None) -> dict:
19
+ filepath = f'companies/{company_short_name}/help_content.yaml'
20
+ if not os.path.exists(filepath):
21
+ return {}
22
+
23
+ # read the file
24
+ try:
25
+ help_content = self.util.load_schema_from_yaml(filepath)
26
+ return help_content
27
+ except Exception as e:
28
+ logging.exception(e)
29
+ raise IAToolkitException(IAToolkitException.ErrorType.CONFIG_ERROR,
30
+ f"Error obteniendo help de {company_short_name}: {str(e)}") from e
@@ -5,29 +5,20 @@
5
5
 
6
6
  from injector import inject
7
7
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
-
9
8
  from iatoolkit.repositories.profile_repo import ProfileRepo
10
- from iatoolkit.common.util import Utility
11
9
 
12
10
 
13
11
  class HistoryService:
14
12
  @inject
15
13
  def __init__(self, llm_query_repo: LLMQueryRepo,
16
- profile_repo: ProfileRepo,
17
- util: Utility):
14
+ profile_repo: ProfileRepo):
18
15
  self.llm_query_repo = llm_query_repo
19
16
  self.profile_repo = profile_repo
20
- self.util = util
21
17
 
22
18
  def get_history(self,
23
19
  company_short_name: str,
24
- external_user_id: str = None,
25
- local_user_id: int = 0) -> dict:
20
+ user_identifier: str) -> dict:
26
21
  try:
27
- user_identifier, _ = self.util.resolve_user_identifier(external_user_id, local_user_id)
28
- if not user_identifier:
29
- return {'error': "No se pudo resolver el identificador del usuario"}
30
-
31
22
  # validate company
32
23
  company = self.profile_repo.get_company_by_short_name(company_short_name)
33
24
  if not company:
@@ -0,0 +1,104 @@
1
+ # iatoolkit/services/i18n_service.py
2
+ import os
3
+ import logging
4
+ from injector import inject
5
+ from iatoolkit.common.util import Utility
6
+ from iatoolkit.services.language_service import LanguageService
7
+
8
+
9
+ class I18nService:
10
+ """
11
+ Servicio centralizado para manejar la internacionalización (i18n).
12
+ Carga todas las traducciones desde archivos YAML en memoria al iniciar.
13
+ """
14
+ FALLBACK_LANGUAGE = 'es'
15
+
16
+ @inject
17
+ def __init__(self, util: Utility, language_service: LanguageService):
18
+ self.util = util
19
+ self.language_service = language_service
20
+
21
+ self.translations = {}
22
+ self._load_translations()
23
+
24
+ def _load_translations(self):
25
+ """
26
+ Carga todos los archivos .yaml del directorio 'locales' en memoria.
27
+ """
28
+ locales_dir = os.path.join(os.path.dirname(__file__), '..', 'locales')
29
+ if not os.path.exists(locales_dir):
30
+ logging.error("El directorio 'locales' no fue encontrado.")
31
+ return
32
+
33
+ for filename in os.listdir(locales_dir):
34
+ if filename.endswith('.yaml'):
35
+ lang_code = filename.split('.')[0]
36
+ filepath = os.path.join(locales_dir, filename)
37
+ try:
38
+ self.translations[lang_code] = self.util.load_schema_from_yaml(filepath)
39
+ except Exception as e:
40
+ logging.error(f"Fallo al cargar el archivo de traducción {filepath}: {e}")
41
+
42
+ def _get_nested_key(self, lang: str, key: str):
43
+ """
44
+ Obtiene un valor de un diccionario anidado usando una clave con puntos.
45
+ """
46
+ data = self.translations.get(lang, {})
47
+ keys = key.split('.')
48
+ for k in keys:
49
+ if isinstance(data, dict) and k in data:
50
+ data = data[k]
51
+ else:
52
+ return None
53
+ return data
54
+
55
+ def get_translation_block(self, key: str, lang: str = None) -> dict:
56
+ """
57
+ Gets a whole dictionary block from the translations.
58
+ Useful for passing a set of translations to JavaScript.
59
+ """
60
+ if lang is None:
61
+ lang = self.language_service.get_current_language()
62
+
63
+ # 1. Try to get the block in the requested language
64
+ block = self._get_nested_key(lang, key)
65
+
66
+ # 2. If not found, try the fallback language
67
+ if not isinstance(block, dict):
68
+ block = self._get_nested_key(self.FALLBACK_LANGUAGE, key)
69
+
70
+ return block if isinstance(block, dict) else {}
71
+
72
+ def t(self, key: str, lang: str = None, **kwargs) -> str:
73
+ """
74
+ Gets the translation for a given key.
75
+ If 'lang' is provided, it's used. Otherwise, it's determined automatically.
76
+ """
77
+ # If no specific language is requested, determine it from the current context.
78
+ if lang is None:
79
+ lang = self.language_service.get_current_language()
80
+
81
+ # 1. Attempt to get the translation in the requested language
82
+ message = self._get_nested_key(lang, key)
83
+
84
+ # 2. If not found, try the fallback language
85
+ if message is None and lang != self.FALLBACK_LANGUAGE:
86
+ logging.warning(
87
+ f"Translation key '{key}' not found for language '{lang}'. Attempting fallback to '{self.FALLBACK_LANGUAGE}'.")
88
+ message = self._get_nested_key(self.FALLBACK_LANGUAGE, key)
89
+
90
+ # 3. If still not found, return the key itself as a last resort
91
+ if message is None:
92
+ logging.error(
93
+ f"Translation key '{key}' not found, even in fallback '{self.FALLBACK_LANGUAGE}'.")
94
+ return key
95
+
96
+ # 4. If variables are provided, format the message
97
+ if kwargs:
98
+ try:
99
+ return message.format(**kwargs)
100
+ except KeyError as e:
101
+ logging.error(f"Error formatting key '{key}': missing variable {e} in arguments.")
102
+ return message
103
+
104
+ return message
@@ -24,16 +24,18 @@ class JWTService:
24
24
  raise RuntimeError(f"Configuración JWT esencial faltante: {e}")
25
25
 
26
26
  def generate_chat_jwt(self,
27
- company_id: int,
28
27
  company_short_name: str,
29
- external_user_id: str,
28
+ user_identifier: str,
30
29
  expires_delta_seconds: int) -> Optional[str]:
31
30
  # generate a JWT for a chat session
32
31
  try:
32
+ if not company_short_name or not user_identifier:
33
+ logging.error(f"Missing token ID: {company_short_name}/{user_identifier}")
34
+ return None
35
+
33
36
  payload = {
34
- 'company_id': company_id,
35
37
  'company_short_name': company_short_name,
36
- 'external_user_id': external_user_id,
38
+ 'user_identifier': user_identifier,
37
39
  'exp': time.time() + expires_delta_seconds,
38
40
  'iat': time.time(),
39
41
  'type': 'chat_session' # Identificador del tipo de token
@@ -41,10 +43,10 @@ class JWTService:
41
43
  token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
42
44
  return token
43
45
  except Exception as e:
44
- logging.error(f"Error al generar JWT para company {company_id}, user {external_user_id}: {e}")
46
+ logging.error(f"Error al generar JWT para {company_short_name}/{user_identifier}: {e}")
45
47
  return None
46
48
 
47
- def validate_chat_jwt(self, token: str, expected_company_short_name: str) -> Optional[Dict[str, Any]]:
49
+ def validate_chat_jwt(self, token: str) -> Optional[Dict[str, Any]]:
48
50
  """
49
51
  Valida un JWT de sesión de chat.
50
52
  Retorna el payload decodificado si es válido y coincide con la empresa, o None.
@@ -59,33 +61,22 @@ class JWTService:
59
61
  logging.warning(f"Validación JWT fallida: tipo incorrecto '{payload.get('type')}'")
60
62
  return None
61
63
 
62
- if payload.get('company_short_name') != expected_company_short_name:
63
- logging.warning(
64
- f"Validación JWT fallida: company_short_name no coincide. "
65
- f"Esperado: {expected_company_short_name}, Obtenido: {payload.get('company_short_name')}"
66
- )
64
+ # user_identifier debe estar presente
65
+ if not payload.get('user_identifier'):
66
+ logging.warning(f"Validación JWT fallida: user_identifier ausente o vacío.")
67
67
  return None
68
68
 
69
- # external_user_id debe estar presente
70
- if 'external_user_id' not in payload or not payload['external_user_id']:
71
- logging.warning(f"Validación JWT fallida: external_user_id ausente o vacío.")
72
- return None
73
-
74
- # company_id debe estar presente
75
- if 'company_id' not in payload or not isinstance(payload['company_id'], int):
76
- logging.warning(f"Validación JWT fallida: company_id ausente o tipo incorrecto.")
69
+ if not payload.get('company_short_name'):
70
+ logging.warning(f"Validación JWT fallida: company_short_name ausente.")
77
71
  return None
78
72
 
79
73
  logging.debug(
80
74
  f"JWT validado exitosamente para company: {payload.get('company_short_name')}, user: {payload.get('external_user_id')}")
81
75
  return payload
82
76
 
83
- except jwt.ExpiredSignatureError:
84
- logging.info(f"Validación JWT fallida: token expirado para {expected_company_short_name}")
85
- return None
86
77
  except jwt.InvalidTokenError as e:
87
- logging.warning(f"Validación JWT fallida: token inválido para {expected_company_short_name}. Error: {e}")
78
+ logging.warning(f"Validación JWT fallida: token inválido . Error: {e}")
88
79
  return None
89
80
  except Exception as e:
90
- logging.error(f"Error inesperado durante validación de JWT para {expected_company_short_name}: {e}")
81
+ logging.error(f"Error inesperado durante validación de JWT : {e}")
91
82
  return None
@@ -0,0 +1,77 @@
1
+ # iatoolkit/services/language_service.py
2
+
3
+ import logging
4
+ from injector import inject
5
+ from flask import g, request
6
+ from iatoolkit.repositories.profile_repo import ProfileRepo
7
+ from iatoolkit.common.session_manager import SessionManager
8
+
9
+
10
+ class LanguageService:
11
+ """
12
+ Determines the correct language for the current request
13
+ based on a defined priority order (session, URL, etc.)
14
+ and caches it in the Flask 'g' object for the request's lifecycle.
15
+ """
16
+
17
+ @inject
18
+ def __init__(self, profile_repo: ProfileRepo):
19
+ self.profile_repo = profile_repo
20
+
21
+ def _get_company_short_name(self) -> str | None:
22
+ """
23
+ Gets the company_short_name from the current request context.
24
+ This handles different scenarios like web sessions, public URLs, and API calls.
25
+
26
+ Priority Order:
27
+ 1. Flask Session (for logged-in web users).
28
+ 2. URL rule variable (for public pages and API endpoints).
29
+ """
30
+ # 1. Check session for logged-in users
31
+ company_short_name = SessionManager.get('company_short_name')
32
+ if company_short_name:
33
+ return company_short_name
34
+
35
+ # 2. Check URL arguments (e.g., /<company_short_name>/login)
36
+ # This covers public pages and most API calls.
37
+ if request.view_args and 'company_short_name' in request.view_args:
38
+ return request.view_args['company_short_name']
39
+
40
+ return None
41
+
42
+ def get_current_language(self) -> str:
43
+ """
44
+ Determines and caches the language for the current request using a priority order:
45
+ 1. User's preference (from their profile).
46
+ 2. Company's default language.
47
+ 3. System-wide fallback language ('es').
48
+ """
49
+ if 'lang' in g:
50
+ return g.lang
51
+
52
+ from iatoolkit.services.i18n_service import I18nService
53
+ lang = I18nService.FALLBACK_LANGUAGE
54
+
55
+ try:
56
+ company_short_name = self._get_company_short_name()
57
+ if company_short_name:
58
+ # Prioridad 1: Preferencia del Usuario
59
+ user_identifier = SessionManager.get('user_identifier')
60
+ if user_identifier:
61
+ # Usamos el repositorio para obtener el objeto User
62
+ user = self.profile_repo.get_user_by_email(
63
+ user_identifier) # Asumiendo que el email es el identificador
64
+ if user and user.preferred_language:
65
+ g.lang = user.preferred_language
66
+ return g.lang
67
+
68
+ # Prioridad 2: Idioma por defecto de la Compañía (si no se encontró preferencia de usuario)
69
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
70
+ if company and company.default_language:
71
+ lang = company.default_language
72
+ except Exception as e:
73
+ logging.debug(f"Could not determine language, falling back to default. Reason: {e}")
74
+ pass
75
+
76
+ g.lang = lang
77
+ return lang
@@ -0,0 +1,43 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.repositories.models import Company
7
+ from typing import List, Dict, Any
8
+
9
+
10
+ class OnboardingService:
11
+ """
12
+ Servicio para gestionar las tarjetas de contenido que se muestran
13
+ durante la pantalla de carga (onboarding).
14
+ """
15
+
16
+ def __init__(self):
17
+ """
18
+ Define el conjunto de tarjetas de onboarding por defecto.
19
+ """
20
+ self._default_cards = [
21
+ {'icon': 'fas fa-users', 'title': 'Clientes',
22
+ 'text': 'Conozco en detalle a nuestros clientes: antigüedad, contactos, historial de operaciones.<br><br><strong>Ejemplo:</strong> ¿cuántos clientes nuevos se incorporaron a mi cartera este año?'},
23
+ {'icon': 'fas fa-cubes', 'title': 'Productos',
24
+ 'text': 'Productos: características, condiciones, historial.'},
25
+
26
+ {'icon': 'fas fa-cogs', 'title': 'Personaliza tus Prompts',
27
+ 'text': 'Utiliza la varita mágica y podrás explorar los prompts predefinidos que he preparado para ti.'},
28
+ {'icon': 'fas fa-table', 'title': 'Tablas y Excel',
29
+ 'text': 'Puedes pedirme la respuesta en formato de tablas o excel.<br><br><strong>Ejemplo:</strong> dame una tabla con los 10 certificados más grandes este año.'},
30
+ {'icon': 'fas fa-shield-alt', 'title': 'Seguridad y Confidencialidad',
31
+ 'text': 'Toda tu información es procesada de forma segura y confidencial dentro de nuestro entorno protegido.'}
32
+ ]
33
+
34
+ def get_onboarding_cards(self, company: Company | None) -> List[Dict[str, Any]]:
35
+ """
36
+ Retorna la lista de tarjetas de onboarding para una compañía.
37
+ Si la compañía tiene tarjetas personalizadas, las devuelve.
38
+ De lo contrario, devuelve las tarjetas por defecto.
39
+ """
40
+ if company and company.onboarding_cards:
41
+ return company.onboarding_cards
42
+
43
+ return self._default_cards