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.
- iatoolkit/base_company.py +11 -3
- iatoolkit/common/routes.py +92 -52
- iatoolkit/common/session_manager.py +0 -1
- iatoolkit/common/util.py +17 -27
- iatoolkit/iatoolkit.py +91 -47
- iatoolkit/infra/llm_client.py +7 -8
- iatoolkit/infra/openai_adapter.py +1 -1
- iatoolkit/infra/redis_session_manager.py +48 -2
- iatoolkit/locales/en.yaml +144 -0
- iatoolkit/locales/es.yaml +140 -0
- iatoolkit/repositories/database_manager.py +17 -2
- iatoolkit/repositories/models.py +31 -4
- iatoolkit/repositories/profile_repo.py +7 -2
- iatoolkit/services/auth_service.py +193 -0
- iatoolkit/services/branding_service.py +59 -18
- iatoolkit/services/dispatcher_service.py +10 -40
- iatoolkit/services/excel_service.py +15 -15
- iatoolkit/services/help_content_service.py +30 -0
- iatoolkit/services/history_service.py +2 -11
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +15 -24
- iatoolkit/services/language_service.py +77 -0
- iatoolkit/services/onboarding_service.py +43 -0
- iatoolkit/services/profile_service.py +148 -75
- iatoolkit/services/query_service.py +124 -81
- iatoolkit/services/tasks_service.py +1 -1
- iatoolkit/services/user_feedback_service.py +68 -32
- iatoolkit/services/user_session_context_service.py +112 -54
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +112 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +148 -220
- iatoolkit/static/js/chat_onboarding_button.js +97 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +35 -0
- iatoolkit/static/styles/chat_iatoolkit.css +367 -199
- iatoolkit/static/styles/chat_modal.css +98 -76
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/onboarding.css +169 -0
- iatoolkit/system_prompts/query_main.prompt +3 -12
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +40 -20
- iatoolkit/templates/change_password.html +57 -36
- iatoolkit/templates/chat.html +169 -83
- iatoolkit/templates/chat_modals.html +134 -68
- iatoolkit/templates/error.html +44 -8
- iatoolkit/templates/forgot_password.html +40 -23
- iatoolkit/templates/index.html +145 -0
- iatoolkit/templates/login_simulation.html +34 -0
- iatoolkit/templates/onboarding_shell.html +104 -0
- iatoolkit/templates/signup.html +63 -65
- iatoolkit/views/base_login_view.py +92 -0
- iatoolkit/views/change_password_view.py +56 -30
- iatoolkit/views/external_login_view.py +61 -28
- iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
- iatoolkit/views/forgot_password_view.py +27 -19
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +50 -23
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +73 -0
- iatoolkit/views/llmquery_api_view.py +57 -0
- iatoolkit/views/login_simulation_view.py +81 -0
- iatoolkit/views/login_view.py +130 -37
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/{prompt_view.py → prompt_api_view.py} +10 -10
- iatoolkit/views/signup_view.py +42 -35
- iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/verify_user_view.py +35 -28
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
- iatoolkit-0.66.2.dist-info/RECORD +119 -0
- iatoolkit/common/auth.py +0 -200
- iatoolkit/static/images/arrow_up.png +0 -0
- iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
- iatoolkit/static/images/logo_clinica.png +0 -0
- iatoolkit/static/images/logo_iatoolkit.png +0 -0
- iatoolkit/static/images/logo_maxxa.png +0 -0
- iatoolkit/static/images/logo_notaria.png +0 -0
- iatoolkit/static/images/logo_tarjeta.png +0 -0
- iatoolkit/static/images/logo_umayor.png +0 -0
- iatoolkit/static/images/upload.png +0 -0
- iatoolkit/static/js/chat_feedback.js +0 -115
- iatoolkit/static/js/chat_history.js +0 -117
- iatoolkit/static/styles/chat_info.css +0 -53
- iatoolkit/templates/header.html +0 -31
- iatoolkit/templates/home.html +0 -199
- iatoolkit/templates/login.html +0 -43
- iatoolkit/templates/test.html +0 -9
- iatoolkit/views/chat_token_request_view.py +0 -98
- iatoolkit/views/chat_view.py +0 -58
- iatoolkit/views/download_file_view.py +0 -58
- iatoolkit/views/external_chat_login_view.py +0 -95
- iatoolkit/views/history_view.py +0 -57
- iatoolkit/views/llmquery_view.py +0 -65
- iatoolkit/views/tasks_review_view.py +0 -83
- iatoolkit/views/user_feedback_view.py +0 -74
- iatoolkit-0.11.0.dist-info/RECORD +0 -110
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.66.2.dist-info}/WHEEL +0 -0
- {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": "
|
|
23
|
-
"primary_font_size": "
|
|
24
|
-
"secondary_font_weight": "
|
|
25
|
-
"secondary_font_size": "0.
|
|
26
|
-
"tertiary_font_weight": "
|
|
27
|
-
"tertiary_font_size": "0.
|
|
28
|
-
"tertiary_opacity": "0.
|
|
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": "#
|
|
44
|
-
"brand_info_text": "#
|
|
45
|
-
"brand_info_border": "#
|
|
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"
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
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
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
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
|