iatoolkit 0.59.1__py3-none-any.whl → 0.67.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of iatoolkit might be problematic. Click here for more details.
- iatoolkit/__init__.py +2 -0
- iatoolkit/base_company.py +2 -19
- iatoolkit/common/routes.py +28 -25
- iatoolkit/common/session_manager.py +2 -0
- iatoolkit/common/util.py +17 -6
- iatoolkit/company_registry.py +1 -2
- iatoolkit/iatoolkit.py +54 -20
- iatoolkit/locales/en.yaml +167 -0
- iatoolkit/locales/es.yaml +163 -0
- iatoolkit/repositories/database_manager.py +3 -3
- iatoolkit/repositories/document_repo.py +1 -1
- iatoolkit/repositories/models.py +3 -4
- iatoolkit/repositories/profile_repo.py +0 -4
- iatoolkit/services/auth_service.py +44 -32
- iatoolkit/services/branding_service.py +35 -27
- iatoolkit/services/configuration_service.py +140 -0
- iatoolkit/services/dispatcher_service.py +20 -18
- iatoolkit/services/document_service.py +5 -2
- iatoolkit/services/excel_service.py +15 -11
- iatoolkit/services/file_processor_service.py +4 -12
- iatoolkit/services/history_service.py +8 -7
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +7 -9
- iatoolkit/services/language_service.py +79 -0
- iatoolkit/services/load_documents_service.py +4 -4
- iatoolkit/services/mail_service.py +9 -4
- iatoolkit/services/onboarding_service.py +10 -4
- iatoolkit/services/profile_service.py +59 -38
- iatoolkit/services/prompt_manager_service.py +20 -16
- iatoolkit/services/query_service.py +15 -14
- iatoolkit/services/sql_service.py +6 -2
- iatoolkit/services/user_feedback_service.py +70 -29
- 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 +110 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +32 -184
- iatoolkit/static/js/{chat_onboarding.js → chat_onboarding_button.js} +0 -1
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +35 -0
- iatoolkit/static/styles/chat_iatoolkit.css +251 -205
- iatoolkit/static/styles/chat_modal.css +63 -95
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +121 -167
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +10 -10
- iatoolkit/templates/base.html +36 -19
- iatoolkit/templates/change_password.html +24 -22
- iatoolkit/templates/chat.html +121 -93
- iatoolkit/templates/chat_modals.html +113 -74
- iatoolkit/templates/error.html +44 -8
- iatoolkit/templates/forgot_password.html +17 -15
- iatoolkit/templates/index.html +66 -81
- iatoolkit/templates/login_simulation.html +16 -5
- iatoolkit/templates/onboarding_shell.html +1 -2
- iatoolkit/templates/signup.html +22 -20
- iatoolkit/views/base_login_view.py +12 -1
- iatoolkit/views/change_password_view.py +50 -33
- iatoolkit/views/external_login_view.py +5 -11
- iatoolkit/views/file_store_api_view.py +7 -9
- iatoolkit/views/forgot_password_view.py +21 -19
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +16 -12
- iatoolkit/views/home_view.py +61 -0
- iatoolkit/views/index_view.py +5 -34
- iatoolkit/views/init_context_api_view.py +16 -13
- iatoolkit/views/llmquery_api_view.py +38 -28
- iatoolkit/views/login_simulation_view.py +14 -2
- iatoolkit/views/login_view.py +48 -33
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +8 -8
- iatoolkit/views/signup_view.py +27 -25
- 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 +21 -32
- iatoolkit/views/verify_user_view.py +33 -26
- {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/METADATA +40 -22
- iatoolkit-0.67.0.dist-info/RECORD +120 -0
- iatoolkit-0.67.0.dist-info/licenses/LICENSE +21 -0
- iatoolkit/static/js/chat_context_reload.js +0 -54
- iatoolkit/static/js/chat_feedback.js +0 -115
- iatoolkit/static/js/chat_history.js +0 -127
- iatoolkit/static/styles/chat_info.css +0 -53
- iatoolkit/templates/_branding_styles.html +0 -53
- iatoolkit/templates/_navbar.html +0 -9
- iatoolkit/templates/header.html +0 -31
- iatoolkit/templates/test.html +0 -9
- iatoolkit/views/chat_token_request_view.py +0 -98
- iatoolkit/views/tasks_review_view.py +0 -83
- iatoolkit-0.59.1.dist-info/RECORD +0 -111
- {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/WHEEL +0 -0
- {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# locales/es.yaml
|
|
2
|
+
ui:
|
|
3
|
+
login_widget:
|
|
4
|
+
title: "Iniciar Sesión"
|
|
5
|
+
welcome_message: "Ingresa tus credenciales o registrate para acceder a la plataforma IA de Sample Company."
|
|
6
|
+
email_placeholder: "Correo electrónico"
|
|
7
|
+
login_button: "Acceder"
|
|
8
|
+
forgot_password_link: "¿Olvidaste tu contraseña?"
|
|
9
|
+
no_account_prompt: "¿No tienes una cuenta?"
|
|
10
|
+
signup_link: "Regístrate"
|
|
11
|
+
|
|
12
|
+
signup:
|
|
13
|
+
title: "Crear una Cuenta"
|
|
14
|
+
subtitle: "Comienza tu viaje con nosotros hoy."
|
|
15
|
+
first_name_label: "Nombre"
|
|
16
|
+
last_name_label: "Apellido"
|
|
17
|
+
email_label: "Correo Electrónico"
|
|
18
|
+
password_label: "Contraseña"
|
|
19
|
+
confirm_password_label: "Confirmar Contraseña"
|
|
20
|
+
signup_button: "Crear Cuenta"
|
|
21
|
+
already_have_account: "¿Ya tienes una cuenta?"
|
|
22
|
+
login_link: "Inicia Sesión"
|
|
23
|
+
disclaimer: "🔒 Valoramos tu privacidad. Tus datos se usarán exclusivamente para el funcionamiento de la plataforma."
|
|
24
|
+
|
|
25
|
+
forgot_password:
|
|
26
|
+
title: "Recuperar Contraseña"
|
|
27
|
+
subtitle: "Ingresa tu correo electrónico y te enviaremos un enlace para restablecer tu contraseña."
|
|
28
|
+
submit_button: "Enviar Enlace de Recuperación"
|
|
29
|
+
back_to_login: "Volver a Iniciar Sesión"
|
|
30
|
+
|
|
31
|
+
change_password:
|
|
32
|
+
title: "Crear Nueva Contraseña"
|
|
33
|
+
subtitle: "Estás cambiando la contraseña para <strong>{email}</strong>."
|
|
34
|
+
temp_code_label: "Código Temporal"
|
|
35
|
+
temp_code_placeholder: "Revisa tu correo electrónico"
|
|
36
|
+
new_password_label: "Nueva Contraseña"
|
|
37
|
+
password_instructions: "Debe contener al menos 8 caracteres, mayúscula, minúscula, número y un carácter especial."
|
|
38
|
+
confirm_password_label: "Confirmar Nueva Contraseña"
|
|
39
|
+
save_button: "Guardar Contraseña"
|
|
40
|
+
back_to_home: "Volver al inicio"
|
|
41
|
+
|
|
42
|
+
chat:
|
|
43
|
+
welcome_message: "¡Hola! ¿En qué te puedo ayudar hoy?"
|
|
44
|
+
input_placeholder: "Escribe tu consulta aquí..."
|
|
45
|
+
prompts_available: "Prompts disponibles"
|
|
46
|
+
|
|
47
|
+
tooltips:
|
|
48
|
+
history: "Historial con mis consultas"
|
|
49
|
+
reload_context: "Forzar Recarga de Contexto"
|
|
50
|
+
feedback: "Tu feedback es muy importante"
|
|
51
|
+
usage_guide: "Guía de Uso"
|
|
52
|
+
onboarding: "Cómo preguntar mejor"
|
|
53
|
+
logout: "Cerrar sesión"
|
|
54
|
+
use_prompt_assistant: "Usar Asistente de Prompts"
|
|
55
|
+
attach_files: "Adjuntar archivos"
|
|
56
|
+
view_attached_files: "Ver archivos adjuntos"
|
|
57
|
+
modals:
|
|
58
|
+
files_title: "Archivos Cargados"
|
|
59
|
+
feedback_title: "Tu Opinión es Importante"
|
|
60
|
+
feedback_prompt: "¿Qué tan útil fue la respuesta del asistente?"
|
|
61
|
+
feedback_comment_label: "Tu comentario nos ayuda a mejorar:"
|
|
62
|
+
feedback_comment_placeholder: "Escribe aquí tu opinión, sugerencias o comentarios..."
|
|
63
|
+
history_title: "Historial de Consultas"
|
|
64
|
+
history_table_date: "Fecha"
|
|
65
|
+
history_table_query: "Consulta"
|
|
66
|
+
loading_history: "Cargando historial..."
|
|
67
|
+
no_history_found: "No se encontró historial de consultas."
|
|
68
|
+
help_title: "Guía de uso del Asistente IA"
|
|
69
|
+
|
|
70
|
+
buttons:
|
|
71
|
+
cancel: "Cerrar"
|
|
72
|
+
send: "Enviar"
|
|
73
|
+
stop: "Detener"
|
|
74
|
+
|
|
75
|
+
errors:
|
|
76
|
+
company_not_found: "La empresa {company_short_name} no existe."
|
|
77
|
+
timeout: "El tiempo de espera ha expirado."
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
auth:
|
|
81
|
+
invalid_password: "La contraseña proporcionada es incorrecta."
|
|
82
|
+
user_not_found: "No se encontró un usuario con ese correo."
|
|
83
|
+
invalid_or_expired_token: "Token inválido o expirado."
|
|
84
|
+
session_creation_failed: "No se pudo crear la sesión del usuario."
|
|
85
|
+
authentication_required: "Autenticación requerida. No se proporcionó cookie de sesión o clave de API."
|
|
86
|
+
invalid_api_key: "Clave de API inválida o inactiva."
|
|
87
|
+
no_user_identifier_api: "No se proporcionó user_identifier para la llamada a la API."
|
|
88
|
+
templates:
|
|
89
|
+
company_not_found: "Empresa no encontrada."
|
|
90
|
+
home_template_not_found: "La plantilla de la página de inicio para la empresa '{company_name}' no está configurada."
|
|
91
|
+
processing_error: "Error al procesar el template: {error}"
|
|
92
|
+
template_not_found: "No se encontro el template: '{template_name}'."
|
|
93
|
+
|
|
94
|
+
general:
|
|
95
|
+
unexpected_error: "Ha ocurrido un error inesperado: {error}."
|
|
96
|
+
unsupported_language: "El idioma seleccionado no es válido."
|
|
97
|
+
signup:
|
|
98
|
+
company_not_found: "La empresa {company_name} no existe."
|
|
99
|
+
incorrect_password_for_existing_user: "La contraseña para el usuario {email} es incorrecta."
|
|
100
|
+
user_already_registered: "El usuario con email '{email}' ya existe en esta empresa."
|
|
101
|
+
password_mismatch: "Las contraseñas no coinciden. Por favor, inténtalo de nuevo."
|
|
102
|
+
change_password:
|
|
103
|
+
token_expired: "El enlace de cambio de contraseña ha expirado. Por favor, solicita uno nuevo."
|
|
104
|
+
password_mismatch: "Las contraseñas no coinciden. Por favor, inténtalo nuevamente."
|
|
105
|
+
invalid_temp_code: "El código temporal no es válido. Por favor, verifica o solicita uno nuevo."
|
|
106
|
+
forgot_password:
|
|
107
|
+
user_not_registered: "El usuario con correo {email} no está registrado."
|
|
108
|
+
verification:
|
|
109
|
+
token_expired: "El enlace de verificación ha expirado. Por favor, contacta a soporte si necesitas uno nuevo."
|
|
110
|
+
user_not_found: "El usuario que intentas verificar no existe."
|
|
111
|
+
validation:
|
|
112
|
+
password_too_short: "La contraseña debe tener al menos 8 caracteres."
|
|
113
|
+
password_no_uppercase: "La contraseña debe tener al menos una letra mayúscula."
|
|
114
|
+
password_no_lowercase: "La contraseña debe tener al menos una letra minúscula."
|
|
115
|
+
password_no_digit: "La contraseña debe tener al menos un número."
|
|
116
|
+
password_no_special_char: "La contraseña debe tener al menos un carácter especial."
|
|
117
|
+
|
|
118
|
+
services:
|
|
119
|
+
no_text_file: "El archivo no es texto o la codificación no es UTF-8"
|
|
120
|
+
no_output_file: "falta el nombre del archivo de salida"
|
|
121
|
+
no_data_for_excel: "faltan los datos o no es una lista de diccionarios"
|
|
122
|
+
no_download_directory: "no esta configurado el directorio temporal para guardar excels"
|
|
123
|
+
cannot_create_excel: "no se pudo crear el archivo excel"
|
|
124
|
+
invalid_filename: "Nombre de archivo inválido"
|
|
125
|
+
file_not_exist : "Archivo no encontrado"
|
|
126
|
+
path_is_not_a_file : "La ruta no corresponde a un archivo"
|
|
127
|
+
file_validation_error : "Error validando archivo"
|
|
128
|
+
user_not_authorized: "Usuario no esta autorizado para esta empresa"
|
|
129
|
+
account_not_verified: "Tu cuenta no ha sido verificada. Por favor, revisa tu correo."
|
|
130
|
+
missing_response_id: "No se encontró 'previous_response_id' para '{company_short_name}/{user_identifier}'. Reinicia el contexto."
|
|
131
|
+
|
|
132
|
+
api_responses:
|
|
133
|
+
context_reloaded_success: "El contexto se ha recargado con éxito."
|
|
134
|
+
|
|
135
|
+
services:
|
|
136
|
+
mail_sent: "mail enviado exitosamente."
|
|
137
|
+
start_query: "Hola, cual es tu pregunta?"
|
|
138
|
+
|
|
139
|
+
flash_messages:
|
|
140
|
+
password_changed_success: "Tu contraseña ha sido restablecida exitosamente. Ahora puedes iniciar sesión."
|
|
141
|
+
signup_success: "Registro exitoso. Por favor, revisa tu correo para verificar tu cuenta."
|
|
142
|
+
user_associated_success: "Usuario existente asociado exitosamente a la nueva empresa."
|
|
143
|
+
account_verified_success: "Tu cuenta ha sido verificada exitosamente. ¡Bienvenido!"
|
|
144
|
+
forgot_password_success: "Si tu correo está registrado, recibirás un enlace para restablecer tu contraseña."
|
|
145
|
+
|
|
146
|
+
# Claves específicas para JavaScript
|
|
147
|
+
js_messages:
|
|
148
|
+
feedback_sent_success_title: "Feedback Enviado"
|
|
149
|
+
feedback_sent_success_body: "¡Gracias por tu comentario!"
|
|
150
|
+
feedback_sent_error: "No se pudo enviar el feedback, por favor intente nuevamente."
|
|
151
|
+
feedback_rating_error: "Por favor, califica al asistente con las estrellas."
|
|
152
|
+
feedback_comment_error: "Por favor, escribe tu comentario antes de enviar."
|
|
153
|
+
context_reloaded: "El contexto ha sido recargado."
|
|
154
|
+
error_loading_history: "Ocurrió un error al cargar el historial."
|
|
155
|
+
request_aborted: "La generación de la respuesta ha sido detenida."
|
|
156
|
+
processing_error: "Ocurrió un error al procesar la solicitud."
|
|
157
|
+
server_comm_error: "Error de comunicación con el servidor ({status}). Por favor, intente de nuevo más tarde."
|
|
158
|
+
network_error: "Ocurrió un error de red. Por favor, inténtalo de nuevo en unos momentos."
|
|
159
|
+
unknown_server_error: "Error desconocido del servidor."
|
|
160
|
+
loading: "Cargando..."
|
|
161
|
+
reload_init: "Iniciando recarga de contexto en segundo plano..."
|
|
162
|
+
no_history_found: "No existe historial de consultas."
|
|
163
|
+
|
|
@@ -27,8 +27,8 @@ class DatabaseManager:
|
|
|
27
27
|
self._engine = create_engine(
|
|
28
28
|
database_url,
|
|
29
29
|
echo=False,
|
|
30
|
-
pool_size=
|
|
31
|
-
max_overflow=
|
|
30
|
+
pool_size=10, # per worker
|
|
31
|
+
max_overflow=20,
|
|
32
32
|
pool_timeout=30,
|
|
33
33
|
pool_recycle=1800,
|
|
34
34
|
pool_pre_ping=True,
|
|
@@ -77,7 +77,7 @@ class DatabaseManager:
|
|
|
77
77
|
inspector = inspect(self._engine)
|
|
78
78
|
|
|
79
79
|
if table_name not in inspector.get_table_names():
|
|
80
|
-
raise RuntimeError(f"
|
|
80
|
+
raise RuntimeError(f"Table '{table_name}' does not exist.")
|
|
81
81
|
|
|
82
82
|
if exclude_columns is None:
|
|
83
83
|
exclude_columns = []
|
|
@@ -22,7 +22,7 @@ class DocumentRepo:
|
|
|
22
22
|
def get(self, company_id, filename: str ) -> Document:
|
|
23
23
|
if not company_id or not filename:
|
|
24
24
|
raise IAToolkitException(IAToolkitException.ErrorType.PARAM_NOT_FILLED,
|
|
25
|
-
'
|
|
25
|
+
'missing company_id or filename')
|
|
26
26
|
|
|
27
27
|
return self.session.query(Document).filter_by(company_id=company_id, filename=filename).first()
|
|
28
28
|
|
iatoolkit/repositories/models.py
CHANGED
|
@@ -53,14 +53,12 @@ class Company(Base):
|
|
|
53
53
|
id = Column(Integer, primary_key=True)
|
|
54
54
|
short_name = Column(String(20), nullable=False, unique=True, index=True)
|
|
55
55
|
name = Column(String(256), nullable=False)
|
|
56
|
+
default_language = Column(String(5), nullable=False, default='es')
|
|
56
57
|
|
|
57
58
|
# encrypted api-key
|
|
58
59
|
openai_api_key = Column(String, nullable=True)
|
|
59
60
|
gemini_api_key = Column(String, nullable=True)
|
|
60
|
-
|
|
61
|
-
branding = Column(JSON, nullable=True)
|
|
62
|
-
onboarding_cards = Column(JSON, nullable=True)
|
|
63
|
-
parameters = Column(JSON, nullable=True, default={})
|
|
61
|
+
parameters = Column(JSON, nullable=True)
|
|
64
62
|
created_at = Column(DateTime, default=datetime.now)
|
|
65
63
|
allow_jwt = Column(Boolean, default=True, nullable=True)
|
|
66
64
|
|
|
@@ -107,6 +105,7 @@ class User(Base):
|
|
|
107
105
|
created_at = Column(DateTime, default=datetime.now)
|
|
108
106
|
password = Column(String, nullable=False)
|
|
109
107
|
verified = Column(Boolean, nullable=False, default=False)
|
|
108
|
+
preferred_language = Column(String(5), nullable=True)
|
|
110
109
|
verification_url = Column(String, nullable=True)
|
|
111
110
|
temp_code = Column(String, nullable=True)
|
|
112
111
|
|
|
@@ -74,10 +74,6 @@ class ProfileRepo:
|
|
|
74
74
|
if company:
|
|
75
75
|
if company.parameters != new_company.parameters:
|
|
76
76
|
company.parameters = new_company.parameters
|
|
77
|
-
if company.branding != new_company.branding:
|
|
78
|
-
company.branding = new_company.branding
|
|
79
|
-
if company.onboarding_cards != new_company.onboarding_cards:
|
|
80
|
-
company.onboarding_cards = new_company.onboarding_cards
|
|
81
77
|
else:
|
|
82
78
|
# Si la compañía no existe, la añade a la sesión.
|
|
83
79
|
self.session.add(new_company)
|
|
@@ -7,6 +7,7 @@ from flask import request
|
|
|
7
7
|
from injector import inject
|
|
8
8
|
from iatoolkit.services.profile_service import ProfileService
|
|
9
9
|
from iatoolkit.services.jwt_service import JWTService
|
|
10
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
10
11
|
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
11
12
|
from iatoolkit.repositories.models import AccessLog
|
|
12
13
|
from flask import request
|
|
@@ -23,18 +24,20 @@ class AuthService:
|
|
|
23
24
|
@inject
|
|
24
25
|
def __init__(self, profile_service: ProfileService,
|
|
25
26
|
jwt_service: JWTService,
|
|
26
|
-
db_manager: DatabaseManager
|
|
27
|
+
db_manager: DatabaseManager,
|
|
28
|
+
i18n_service: I18nService
|
|
27
29
|
):
|
|
28
30
|
self.profile_service = profile_service
|
|
29
31
|
self.jwt_service = jwt_service
|
|
30
32
|
self.db_manager = db_manager
|
|
33
|
+
self.i18n_service = i18n_service
|
|
31
34
|
|
|
32
35
|
def login_local_user(self, company_short_name: str, email: str, password: str) -> dict:
|
|
33
36
|
# try to autenticate a local user, register the event and return the result
|
|
34
37
|
auth_response = self.profile_service.login(
|
|
35
38
|
company_short_name=company_short_name,
|
|
36
39
|
email=email,
|
|
37
|
-
password=password
|
|
40
|
+
password=password,
|
|
38
41
|
)
|
|
39
42
|
|
|
40
43
|
if not auth_response.get('success'):
|
|
@@ -66,7 +69,7 @@ class AuthService:
|
|
|
66
69
|
outcome='failure',
|
|
67
70
|
reason_code='JWT_INVALID'
|
|
68
71
|
)
|
|
69
|
-
return {'success': False, 'error': '
|
|
72
|
+
return {'success': False, 'error': self.i18n_service.t('errors.auth.invalid_or_expired_token')}
|
|
70
73
|
|
|
71
74
|
# 2. if token is valid, extract the user_identifier
|
|
72
75
|
user_identifier = payload.get('user_identifier')
|
|
@@ -81,7 +84,7 @@ class AuthService:
|
|
|
81
84
|
)
|
|
82
85
|
return {'success': True, 'user_identifier': user_identifier}
|
|
83
86
|
except Exception as e:
|
|
84
|
-
logging.error(f"
|
|
87
|
+
logging.error(f"error creeating session for Token of {user_identifier}: {e}")
|
|
85
88
|
self.log_access(
|
|
86
89
|
company_short_name=company_short_name,
|
|
87
90
|
auth_type='redeem_token',
|
|
@@ -89,11 +92,12 @@ class AuthService:
|
|
|
89
92
|
reason_code='SESSION_CREATION_FAILED',
|
|
90
93
|
user_identifier=user_identifier
|
|
91
94
|
)
|
|
92
|
-
return {'success': False, 'error': '
|
|
95
|
+
return {'success': False, 'error': self.i18n_service.t('errors.auth.session_creation_failed')}
|
|
93
96
|
|
|
94
|
-
def verify(self) -> dict:
|
|
97
|
+
def verify(self, anonymous: bool = False) -> dict:
|
|
95
98
|
"""
|
|
96
99
|
Verifies the current request and identifies the user.
|
|
100
|
+
If anonymous is True the non-presence of use_identifier is ignored
|
|
97
101
|
|
|
98
102
|
Returns a dictionary with:
|
|
99
103
|
- success: bool
|
|
@@ -109,7 +113,7 @@ class AuthService:
|
|
|
109
113
|
return {
|
|
110
114
|
"success": True,
|
|
111
115
|
"company_short_name": session_info['company_short_name'],
|
|
112
|
-
"user_identifier": session_info['user_identifier']
|
|
116
|
+
"user_identifier": session_info['user_identifier'],
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
# --- Priority 2: Check for a valid API Key in headers ---
|
|
@@ -118,31 +122,39 @@ class AuthService:
|
|
|
118
122
|
if isinstance(auth, str) and auth.lower().startswith('bearer '):
|
|
119
123
|
api_key = auth.split(' ', 1)[1].strip()
|
|
120
124
|
|
|
121
|
-
if api_key:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
if not api_key:
|
|
126
|
+
# --- Failure: No valid credentials found ---
|
|
127
|
+
logging.info(f"Authentication required. No session cookie or API Key provided.")
|
|
128
|
+
return {"success": False,
|
|
129
|
+
"error_message": self.i18n_service.t('errors.auth.authentication_required'),
|
|
130
|
+
"status_code": 401}
|
|
131
|
+
|
|
132
|
+
# check if the api-key is valid and active
|
|
133
|
+
api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
|
|
134
|
+
if not api_key_entry:
|
|
135
|
+
logging.info(f"Invalid or inactive API Key {api_key}")
|
|
136
|
+
return {"success": False,
|
|
137
|
+
"error_message": self.i18n_service.t('errors.auth.invalid_api_key'),
|
|
138
|
+
"status_code": 402}
|
|
139
|
+
|
|
140
|
+
# get the company from the api_key_entry
|
|
141
|
+
company = api_key_entry.company
|
|
142
|
+
|
|
143
|
+
# For API calls, the external_user_id must be provided in the request.
|
|
144
|
+
data = request.get_json(silent=True) or {}
|
|
145
|
+
user_identifier = data.get('user_identifier', '')
|
|
146
|
+
if not anonymous and not user_identifier:
|
|
147
|
+
logging.info(f"No user_identifier provided for API call.")
|
|
148
|
+
return {"success": False,
|
|
149
|
+
"error_message": self.i18n_service.t('errors.auth.no_user_identifier_api'),
|
|
150
|
+
"status_code": 403}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"success": True,
|
|
154
|
+
"company_short_name": company.short_name,
|
|
155
|
+
"user_identifier": user_identifier
|
|
156
|
+
}
|
|
126
157
|
|
|
127
|
-
# obtain the company from the api_key_entry
|
|
128
|
-
company = api_key_entry.company
|
|
129
|
-
|
|
130
|
-
# For API calls, the external_user_id must be provided in the request.
|
|
131
|
-
user_identifier = ''
|
|
132
|
-
if request.is_json:
|
|
133
|
-
data = request.get_json() or {}
|
|
134
|
-
user_identifier = data.get('user_identifier', '')
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
"success": True,
|
|
138
|
-
"company_short_name": company.short_name,
|
|
139
|
-
"user_identifier": user_identifier
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
# --- Failure: No valid credentials found ---
|
|
143
|
-
logging.info(f"Authentication required. No session cookie or API Key provided. session: {str(session_info)}")
|
|
144
|
-
return {"success": False, "error_message": "Authentication required. No session cookie or API Key provided.",
|
|
145
|
-
"status_code": 402}
|
|
146
158
|
|
|
147
159
|
def log_access(self,
|
|
148
160
|
company_short_name: str,
|
|
@@ -177,5 +189,5 @@ class AuthService:
|
|
|
177
189
|
session.commit()
|
|
178
190
|
|
|
179
191
|
except Exception as e:
|
|
180
|
-
logging.error(f"
|
|
192
|
+
logging.error(f"error writting to AccessLog: {e}", exc_info=False)
|
|
181
193
|
session.rollback()
|
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
from iatoolkit.repositories.models import Company
|
|
7
|
+
from iatoolkit.services.configuration_service import ConfigurationService
|
|
8
|
+
from injector import inject
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class BrandingService:
|
|
10
12
|
"""
|
|
11
|
-
|
|
13
|
+
Branding configuration for IAToolkit
|
|
12
14
|
"""
|
|
13
|
-
|
|
14
|
-
def __init__(self):
|
|
15
|
+
@inject
|
|
16
|
+
def __init__(self, config_service: ConfigurationService):
|
|
17
|
+
self.config_service = config_service
|
|
15
18
|
"""
|
|
16
19
|
Define los estilos de branding por defecto para la aplicación.
|
|
17
20
|
"""
|
|
@@ -19,13 +22,16 @@ class BrandingService:
|
|
|
19
22
|
# --- Estilos del Encabezado Principal ---
|
|
20
23
|
"header_background_color": "#FFFFFF",
|
|
21
24
|
"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.
|
|
25
|
+
"primary_font_weight": "600",
|
|
26
|
+
"primary_font_size": "1.2rem",
|
|
27
|
+
"secondary_font_weight": "400",
|
|
28
|
+
"secondary_font_size": "0.9rem",
|
|
29
|
+
"tertiary_font_weight": "300",
|
|
30
|
+
"tertiary_font_size": "0.8rem",
|
|
31
|
+
"tertiary_opacity": "0.7",
|
|
32
|
+
|
|
33
|
+
# headings
|
|
34
|
+
"brand_text_heading_color": "#334155", # Gris pizarra por defecto
|
|
29
35
|
|
|
30
36
|
# Estilos Globales de la Marca ---
|
|
31
37
|
"brand_primary_color": "#0d6efd", # Azul de Bootstrap por defecto
|
|
@@ -40,25 +46,27 @@ class BrandingService:
|
|
|
40
46
|
"brand_danger_border": "#f5c2c7", # Borde rojo intermedio
|
|
41
47
|
|
|
42
48
|
# Estilos para Alertas Informativas ---
|
|
43
|
-
"brand_info_bg": "#
|
|
44
|
-
"brand_info_text": "#
|
|
45
|
-
"brand_info_border": "#
|
|
49
|
+
"brand_info_bg": "#F0F4F8", # Un fondo de gris azulado muy pálido
|
|
50
|
+
"brand_info_text": "#0d6efd", # Texto en el color primario
|
|
51
|
+
"brand_info_border": "#D9E2EC", # Borde de gris azulado pálido
|
|
46
52
|
|
|
47
53
|
# Estilos para el Asistente de Prompts ---
|
|
48
54
|
"prompt_assistant_bg": "#f8f9fa",
|
|
49
55
|
"prompt_assistant_border": "#dee2e6",
|
|
50
|
-
"prompt_assistant_icon_color": "#6c757d",
|
|
51
56
|
"prompt_assistant_button_bg": "#FFFFFF",
|
|
52
57
|
"prompt_assistant_button_text": "#495057",
|
|
53
58
|
"prompt_assistant_button_border": "#ced4da",
|
|
54
59
|
"prompt_assistant_dropdown_bg": "#f8f9fa",
|
|
55
60
|
"prompt_assistant_header_bg": "#e9ecef",
|
|
56
61
|
"prompt_assistant_header_text": "#495057",
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
|
|
63
|
+
# this use the primary by default
|
|
64
|
+
"prompt_assistant_icon_color": None,
|
|
65
|
+
"prompt_assistant_item_hover_bg": None,
|
|
66
|
+
"prompt_assistant_item_hover_text": None,
|
|
59
67
|
|
|
60
68
|
# Color para el botón de Enviar ---
|
|
61
|
-
"send_button_color": "#212529"
|
|
69
|
+
"send_button_color": "#212529" # Gris oscuro/casi negro por defecto
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
def get_company_branding(self, company: Company | None) -> dict:
|
|
@@ -68,8 +76,9 @@ class BrandingService:
|
|
|
68
76
|
"""
|
|
69
77
|
final_branding_values = self._default_branding.copy()
|
|
70
78
|
|
|
71
|
-
if company
|
|
72
|
-
|
|
79
|
+
if company:
|
|
80
|
+
branding_data = self.config_service.get_company_content(company.short_name, 'branding')
|
|
81
|
+
final_branding_values.update(branding_data)
|
|
73
82
|
|
|
74
83
|
# Función para convertir HEX a RGB
|
|
75
84
|
def hex_to_rgb(hex_color):
|
|
@@ -80,10 +89,6 @@ class BrandingService:
|
|
|
80
89
|
secondary_rgb = hex_to_rgb(final_branding_values['brand_secondary_color'])
|
|
81
90
|
|
|
82
91
|
# --- CONSTRUCCIÓN DE ESTILOS Y VARIABLES CSS ---
|
|
83
|
-
header_style = (
|
|
84
|
-
f"background-color: {final_branding_values['header_background_color']}; "
|
|
85
|
-
f"color: {final_branding_values['header_text_color']};"
|
|
86
|
-
)
|
|
87
92
|
primary_text_style = (
|
|
88
93
|
f"font-weight: {final_branding_values['primary_font_weight']}; "
|
|
89
94
|
f"font-size: {final_branding_values['primary_font_size']};"
|
|
@@ -103,6 +108,10 @@ class BrandingService:
|
|
|
103
108
|
:root {{
|
|
104
109
|
--brand-primary-color: {final_branding_values['brand_primary_color']};
|
|
105
110
|
--brand-secondary-color: {final_branding_values['brand_secondary_color']};
|
|
111
|
+
--brand-header-bg: {final_branding_values['header_background_color']};
|
|
112
|
+
--brand-header-text: {final_branding_values['header_text_color']};
|
|
113
|
+
--brand-text-heading-color: {final_branding_values['brand_text_heading_color']};
|
|
114
|
+
|
|
106
115
|
--brand-primary-color-rgb: {', '.join(map(str, primary_rgb))};
|
|
107
116
|
--brand-secondary-color-rgb: {', '.join(map(str, secondary_rgb))};
|
|
108
117
|
--brand-text-on-primary: {final_branding_values['brand_text_on_primary']};
|
|
@@ -114,11 +123,11 @@ class BrandingService:
|
|
|
114
123
|
--brand-danger-text: {final_branding_values['brand_danger_text']};
|
|
115
124
|
--brand-danger-border: {final_branding_values['brand_danger_border']};
|
|
116
125
|
--brand-info-bg: {final_branding_values['brand_info_bg']};
|
|
117
|
-
--brand-info-text: {final_branding_values['brand_info_text']};
|
|
126
|
+
--brand-info-text: {final_branding_values['brand_info_text'] or final_branding_values['brand_primary_color']};
|
|
118
127
|
--brand-info-border: {final_branding_values['brand_info_border']};
|
|
119
128
|
--brand-prompt-assistant-bg: {final_branding_values['prompt_assistant_bg']};
|
|
120
129
|
--brand-prompt-assistant-border: {final_branding_values['prompt_assistant_border']};
|
|
121
|
-
--brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color']};
|
|
130
|
+
--brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color'] or final_branding_values['brand_primary_color']};
|
|
122
131
|
--brand-prompt-assistant-button-bg: {final_branding_values['prompt_assistant_button_bg']};
|
|
123
132
|
--brand-prompt-assistant-button-text: {final_branding_values['prompt_assistant_button_text']};
|
|
124
133
|
--brand-prompt-assistant-button-border: {final_branding_values['prompt_assistant_button_border']};
|
|
@@ -133,11 +142,10 @@ class BrandingService:
|
|
|
133
142
|
|
|
134
143
|
return {
|
|
135
144
|
"name": company.name if company else "IAToolkit",
|
|
136
|
-
"header_style": header_style,
|
|
137
145
|
"primary_text_style": primary_text_style,
|
|
138
146
|
"secondary_text_style": secondary_text_style,
|
|
139
147
|
"tertiary_text_style": tertiary_text_style,
|
|
140
148
|
"header_text_color": final_branding_values['header_text_color'],
|
|
141
149
|
"css_variables": css_variables,
|
|
142
|
-
"send_button_color": final_branding_values['
|
|
150
|
+
"send_button_color": final_branding_values['brand_primary_color']
|
|
143
151
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# iatoolkit/services/configuration_service.py
|
|
2
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
3
|
+
# Product: IAToolkit
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from iatoolkit import BaseCompany
|
|
7
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
8
|
+
from iatoolkit.repositories.models import Company
|
|
9
|
+
from iatoolkit.common.util import Utility
|
|
10
|
+
from injector import inject
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
class ConfigurationService:
|
|
14
|
+
"""
|
|
15
|
+
Orchestrates the configuration of a Company by reading its YAML files
|
|
16
|
+
and using the BaseCompany's protected methods to register settings.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@inject
|
|
20
|
+
def __init__(self,
|
|
21
|
+
profile_repo: ProfileRepo,
|
|
22
|
+
utility: Utility):
|
|
23
|
+
self.profile_repo = profile_repo
|
|
24
|
+
self.utility = utility
|
|
25
|
+
self._loaded_configs = {} # cache for store loaded configurations
|
|
26
|
+
|
|
27
|
+
def get_company_content(self, company_short_name: str, content_key: str) -> dict | list | None:
|
|
28
|
+
"""
|
|
29
|
+
Public method to provide a specific section of a company's configuration.
|
|
30
|
+
It uses a cache to avoid reading files from disk on every call.
|
|
31
|
+
"""
|
|
32
|
+
self._ensure_config_loaded(company_short_name)
|
|
33
|
+
return self._loaded_configs[company_short_name].get(content_key)
|
|
34
|
+
|
|
35
|
+
def register_company(self, company_short_name: str, company_instance: BaseCompany):
|
|
36
|
+
"""
|
|
37
|
+
Main entry point for configuring a company instance.
|
|
38
|
+
This method is invoked by the dispatcher for each registered company.
|
|
39
|
+
"""
|
|
40
|
+
logging.info(f"⚙️ Starting configuration for company '{company_short_name}'...")
|
|
41
|
+
|
|
42
|
+
# 1. identify the instance with his name and load info from database
|
|
43
|
+
company_instance.id = company_short_name
|
|
44
|
+
company_instance.company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
45
|
+
|
|
46
|
+
# 2. Load the main configuration file and supplementary content files
|
|
47
|
+
config = self._load_and_merge_configs(company_short_name)
|
|
48
|
+
|
|
49
|
+
# 3. Register core company details and get the database object
|
|
50
|
+
company_db_object = self._register_core_details(company_instance, config)
|
|
51
|
+
|
|
52
|
+
# 4. Register tools (functions)
|
|
53
|
+
self._register_tools(company_instance, config.get('tools', []))
|
|
54
|
+
|
|
55
|
+
# 5. Register prompt categories and prompts
|
|
56
|
+
self._register_prompts(company_instance, config)
|
|
57
|
+
|
|
58
|
+
# 6. Link the persisted Company object back to the running instance
|
|
59
|
+
company_instance.company = company_db_object
|
|
60
|
+
|
|
61
|
+
logging.info(f"✅ Company '{company_short_name}' configured successfully.")
|
|
62
|
+
|
|
63
|
+
def _ensure_config_loaded(self, company_short_name: str):
|
|
64
|
+
"""
|
|
65
|
+
Checks if the configuration for a company is in the cache.
|
|
66
|
+
If not, it loads it from files and stores it.
|
|
67
|
+
"""
|
|
68
|
+
if company_short_name not in self._loaded_configs:
|
|
69
|
+
self._loaded_configs[company_short_name] = self._load_and_merge_configs(company_short_name)
|
|
70
|
+
|
|
71
|
+
def _load_and_merge_configs(self, company_short_name: str) -> dict:
|
|
72
|
+
"""
|
|
73
|
+
Loads the main company.yaml and merges data from supplementary files
|
|
74
|
+
specified in the 'content_files' section.
|
|
75
|
+
"""
|
|
76
|
+
config_dir = Path("companies") / company_short_name / "config"
|
|
77
|
+
main_config_path = config_dir / "company.yaml"
|
|
78
|
+
|
|
79
|
+
if not main_config_path.exists():
|
|
80
|
+
raise FileNotFoundError(f"Main configuration file not found: {main_config_path}")
|
|
81
|
+
|
|
82
|
+
config = self.utility.load_schema_from_yaml(main_config_path)
|
|
83
|
+
|
|
84
|
+
# Load and merge supplementary content files (e.g., onboarding_cards)
|
|
85
|
+
for key, file_path in config.get('help_files', {}).items():
|
|
86
|
+
supplementary_path = config_dir / file_path
|
|
87
|
+
if supplementary_path.exists():
|
|
88
|
+
config[key] = self.utility.load_schema_from_yaml(supplementary_path)
|
|
89
|
+
else:
|
|
90
|
+
logging.warning(f"⚠️ Warning: Content file not found: {supplementary_path}")
|
|
91
|
+
config[key] = None # Ensure the key exists but is empty
|
|
92
|
+
|
|
93
|
+
return config
|
|
94
|
+
|
|
95
|
+
def _register_core_details(self, company_instance: BaseCompany, config: dict) -> Company:
|
|
96
|
+
"""Calls _create_company with data from the merged YAML config."""
|
|
97
|
+
return company_instance._create_company(
|
|
98
|
+
name=config['name'],
|
|
99
|
+
short_name=config['id'],
|
|
100
|
+
parameters=config.get('parameters', {})
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _register_tools(self, company_instance: BaseCompany, tools_config: list):
|
|
104
|
+
"""Calls _create_function for each tool defined in the YAML."""
|
|
105
|
+
for tool in tools_config:
|
|
106
|
+
company_instance._create_function(
|
|
107
|
+
function_name=tool['function_name'],
|
|
108
|
+
description=tool['description'],
|
|
109
|
+
params=tool['params']
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _register_prompts(self, company_instance: BaseCompany, config: dict):
|
|
113
|
+
"""
|
|
114
|
+
Creates prompt categories first, then creates each prompt and assigns
|
|
115
|
+
it to its respective category.
|
|
116
|
+
"""
|
|
117
|
+
prompts_config = config.get('prompts', [])
|
|
118
|
+
categories_config = config.get('prompt_categories', [])
|
|
119
|
+
|
|
120
|
+
created_categories = {}
|
|
121
|
+
for i, category_name in enumerate(categories_config):
|
|
122
|
+
category_obj = company_instance._create_prompt_category(name=category_name, order=i + 1)
|
|
123
|
+
created_categories[category_name] = category_obj
|
|
124
|
+
|
|
125
|
+
for prompt_data in prompts_config:
|
|
126
|
+
category_name = prompt_data.get('category')
|
|
127
|
+
if not category_name or category_name not in created_categories:
|
|
128
|
+
logging.info(f"⚠️ Warning: Prompt '{prompt_data['name']}' has an invalid or missing category. Skipping.")
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
category_obj = created_categories[category_name]
|
|
132
|
+
|
|
133
|
+
company_instance._create_prompt(
|
|
134
|
+
prompt_name=prompt_data['name'],
|
|
135
|
+
description=prompt_data['description'],
|
|
136
|
+
order=prompt_data['order'],
|
|
137
|
+
category=category_obj,
|
|
138
|
+
active=prompt_data.get('active', True),
|
|
139
|
+
custom_fields=prompt_data.get('custom_fields', [])
|
|
140
|
+
)
|