iatoolkit 0.8.1__py3-none-any.whl → 0.63.4__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 +8 -34
- iatoolkit/base_company.py +14 -3
- iatoolkit/common/routes.py +83 -52
- iatoolkit/common/session_manager.py +0 -1
- iatoolkit/common/util.py +0 -27
- iatoolkit/iatoolkit.py +61 -46
- iatoolkit/infra/llm_client.py +7 -8
- iatoolkit/infra/openai_adapter.py +1 -1
- iatoolkit/infra/redis_session_manager.py +48 -2
- iatoolkit/repositories/database_manager.py +17 -2
- iatoolkit/repositories/models.py +31 -6
- iatoolkit/repositories/profile_repo.py +7 -2
- iatoolkit/services/auth_service.py +188 -0
- iatoolkit/services/branding_service.py +147 -0
- iatoolkit/services/dispatcher_service.py +10 -40
- iatoolkit/services/excel_service.py +15 -15
- iatoolkit/services/history_service.py +3 -12
- iatoolkit/services/jwt_service.py +15 -24
- iatoolkit/services/onboarding_service.py +43 -0
- iatoolkit/services/profile_service.py +97 -44
- iatoolkit/services/query_service.py +124 -81
- iatoolkit/services/tasks_service.py +1 -1
- iatoolkit/services/user_feedback_service.py +67 -31
- iatoolkit/services/user_session_context_service.py +112 -54
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +6 -11
- iatoolkit/static/js/chat_history_button.js +126 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +130 -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 +52 -0
- iatoolkit/static/styles/chat_iatoolkit.css +329 -507
- iatoolkit/static/styles/chat_modal.css +95 -56
- 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 +40 -0
- iatoolkit/templates/base.html +8 -3
- iatoolkit/templates/change_password.html +54 -37
- iatoolkit/templates/chat.html +149 -66
- iatoolkit/templates/chat_modals.html +47 -18
- iatoolkit/templates/error.html +41 -8
- iatoolkit/templates/forgot_password.html +37 -24
- iatoolkit/templates/index.html +140 -0
- iatoolkit/templates/login_simulation.html +34 -0
- iatoolkit/templates/onboarding_shell.html +105 -0
- iatoolkit/templates/signup.html +64 -66
- iatoolkit/views/base_login_view.py +81 -0
- iatoolkit/views/change_password_view.py +23 -12
- 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 +23 -13
- iatoolkit/views/history_api_view.py +52 -0
- iatoolkit/views/home_view.py +58 -25
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +68 -0
- iatoolkit/views/llmquery_api_view.py +45 -0
- iatoolkit/views/login_simulation_view.py +81 -0
- iatoolkit/views/login_view.py +118 -34
- iatoolkit/views/logout_api_view.py +45 -0
- iatoolkit/views/{prompt_view.py → prompt_api_view.py} +7 -7
- iatoolkit/views/signup_view.py +38 -29
- iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -31
- iatoolkit/views/verify_user_view.py +13 -8
- {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/METADATA +2 -2
- iatoolkit-0.63.4.dist-info/RECORD +113 -0
- {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/top_level.txt +0 -1
- 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_history.js +0 -117
- iatoolkit/templates/home.html +0 -201
- iatoolkit/templates/login.html +0 -43
- iatoolkit/views/chat_token_request_view.py +0 -98
- iatoolkit/views/chat_view.py +0 -51
- iatoolkit/views/download_file_view.py +0 -58
- iatoolkit/views/external_chat_login_view.py +0 -88
- iatoolkit/views/history_view.py +0 -57
- iatoolkit/views/llmquery_view.py +0 -65
- iatoolkit/views/tasks_review_view.py +0 -83
- iatoolkit-0.8.1.dist-info/RECORD +0 -175
- tests/__init__.py +0 -5
- tests/common/__init__.py +0 -0
- tests/common/test_auth.py +0 -279
- tests/common/test_routes.py +0 -42
- tests/common/test_session_manager.py +0 -59
- tests/common/test_util.py +0 -444
- tests/companies/__init__.py +0 -5
- tests/conftest.py +0 -36
- tests/infra/__init__.py +0 -5
- tests/infra/connectors/__init__.py +0 -5
- tests/infra/connectors/test_google_drive_connector.py +0 -107
- tests/infra/connectors/test_local_file_connector.py +0 -85
- tests/infra/connectors/test_s3_connector.py +0 -95
- tests/infra/test_call_service.py +0 -92
- tests/infra/test_database_manager.py +0 -59
- tests/infra/test_gemini_adapter.py +0 -137
- tests/infra/test_google_chat_app.py +0 -68
- tests/infra/test_llm_client.py +0 -165
- tests/infra/test_llm_proxy.py +0 -122
- tests/infra/test_mail_app.py +0 -94
- tests/infra/test_openai_adapter.py +0 -105
- tests/infra/test_redis_session_manager_service.py +0 -117
- tests/repositories/__init__.py +0 -5
- tests/repositories/test_database_manager.py +0 -87
- tests/repositories/test_document_repo.py +0 -76
- tests/repositories/test_llm_query_repo.py +0 -340
- tests/repositories/test_models.py +0 -38
- tests/repositories/test_profile_repo.py +0 -142
- tests/repositories/test_tasks_repo.py +0 -76
- tests/repositories/test_vs_repo.py +0 -107
- tests/services/__init__.py +0 -5
- tests/services/test_dispatcher_service.py +0 -274
- tests/services/test_document_service.py +0 -181
- tests/services/test_excel_service.py +0 -208
- tests/services/test_file_processor_service.py +0 -121
- tests/services/test_history_service.py +0 -164
- tests/services/test_jwt_service.py +0 -255
- tests/services/test_load_documents_service.py +0 -112
- tests/services/test_mail_service.py +0 -70
- tests/services/test_profile_service.py +0 -379
- tests/services/test_prompt_manager_service.py +0 -190
- tests/services/test_query_service.py +0 -243
- tests/services/test_search_service.py +0 -39
- tests/services/test_sql_service.py +0 -160
- tests/services/test_tasks_service.py +0 -252
- tests/services/test_user_feedback_service.py +0 -389
- tests/services/test_user_session_context_service.py +0 -132
- tests/views/__init__.py +0 -5
- tests/views/test_change_password_view.py +0 -191
- tests/views/test_chat_token_request_view.py +0 -188
- tests/views/test_chat_view.py +0 -98
- tests/views/test_download_file_view.py +0 -149
- tests/views/test_external_chat_login_view.py +0 -120
- tests/views/test_external_login_view.py +0 -102
- tests/views/test_file_store_view.py +0 -128
- tests/views/test_forgot_password_view.py +0 -142
- tests/views/test_history_view.py +0 -336
- tests/views/test_home_view.py +0 -61
- tests/views/test_llm_query_view.py +0 -154
- tests/views/test_login_view.py +0 -114
- tests/views/test_prompt_view.py +0 -111
- tests/views/test_signup_view.py +0 -140
- tests/views/test_tasks_review_view.py +0 -104
- tests/views/test_tasks_view.py +0 -130
- tests/views/test_user_feedback_view.py +0 -214
- tests/views/test_verify_user_view.py +0 -110
- {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/WHEEL +0 -0
|
@@ -3,65 +3,101 @@
|
|
|
3
3
|
#
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from iatoolkit.repositories.models import UserFeedback
|
|
6
|
+
from iatoolkit.repositories.models import UserFeedback, Company
|
|
7
7
|
from injector import inject
|
|
8
8
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
9
|
from iatoolkit.infra.google_chat_app import GoogleChatApp
|
|
10
|
+
from iatoolkit.infra.mail_app import MailApp # <-- 1. Importar MailApp
|
|
10
11
|
import logging
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class UserFeedbackService:
|
|
14
15
|
@inject
|
|
15
|
-
def __init__(self,
|
|
16
|
+
def __init__(self,
|
|
17
|
+
profile_repo: ProfileRepo,
|
|
18
|
+
google_chat_app: GoogleChatApp,
|
|
19
|
+
mail_app: MailApp):
|
|
16
20
|
self.profile_repo = profile_repo
|
|
17
21
|
self.google_chat_app = google_chat_app
|
|
22
|
+
self.mail_app = mail_app
|
|
23
|
+
|
|
24
|
+
def _send_google_chat_notification(self, space_name: str, message_text: str):
|
|
25
|
+
"""Envía una notificación de feedback a un espacio de Google Chat."""
|
|
26
|
+
try:
|
|
27
|
+
chat_data = {
|
|
28
|
+
"type": "MESSAGE_TRIGGER",
|
|
29
|
+
"space": {"name": space_name},
|
|
30
|
+
"message": {"text": message_text}
|
|
31
|
+
}
|
|
32
|
+
chat_result = self.google_chat_app.send_message(message_data=chat_data)
|
|
33
|
+
if not chat_result.get('success'):
|
|
34
|
+
logging.warning(f"Error al enviar notificación a Google Chat: {chat_result.get('message')}")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
logging.exception(f"Fallo inesperado al enviar notificación a Google Chat: {e}")
|
|
37
|
+
|
|
38
|
+
def _send_email_notification(self, destination_email: str, company_name: str, message_text: str):
|
|
39
|
+
"""Envía una notificación de feedback por correo electrónico."""
|
|
40
|
+
try:
|
|
41
|
+
subject = f"Nuevo Feedback de {company_name}"
|
|
42
|
+
# Convertir el texto plano a un HTML simple para mantener los saltos de línea
|
|
43
|
+
html_body = message_text.replace('\n', '<br>')
|
|
44
|
+
self.mail_app.send_email(to=destination_email, subject=subject, body=html_body)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logging.exception(f"Fallo inesperado al enviar email de feedback: {e}")
|
|
47
|
+
|
|
48
|
+
def _handle_notification(self, company: Company, message_text: str):
|
|
49
|
+
"""Lee la configuración de la empresa y envía la notificación al canal correspondiente."""
|
|
50
|
+
feedback_params = company.parameters.get('user_feedback')
|
|
51
|
+
if not isinstance(feedback_params, dict):
|
|
52
|
+
logging.warning(f"No se encontró configuración de 'user_feedback' para la empresa {company.short_name}.")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# get channel and destination
|
|
56
|
+
channel = feedback_params.get('channel')
|
|
57
|
+
destination = feedback_params.get('destination')
|
|
58
|
+
if not channel or not destination:
|
|
59
|
+
logging.warning(f"Configuración 'user_feedback' incompleta para {company.short_name}. Faltan 'channel' o 'destination'.")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if channel == 'google_chat':
|
|
63
|
+
self._send_google_chat_notification(space_name=destination, message_text=message_text)
|
|
64
|
+
elif channel == 'email':
|
|
65
|
+
self._send_email_notification(destination_email=destination, company_name=company.short_name, message_text=message_text)
|
|
66
|
+
else:
|
|
67
|
+
logging.warning(f"Canal de feedback '{channel}' no reconocido para la empresa {company.short_name}.")
|
|
18
68
|
|
|
19
69
|
def new_feedback(self,
|
|
20
70
|
company_short_name: str,
|
|
21
71
|
message: str,
|
|
22
|
-
|
|
23
|
-
local_user_id: int = 0,
|
|
24
|
-
space: str = None,
|
|
25
|
-
type: str = None,
|
|
72
|
+
user_identifier: str,
|
|
26
73
|
rating: int = None) -> dict:
|
|
27
74
|
try:
|
|
28
|
-
#
|
|
75
|
+
# 1. Validar empresa
|
|
29
76
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
30
77
|
if not company:
|
|
31
78
|
return {'error': f'No existe la empresa: {company_short_name}'}
|
|
32
79
|
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"space": {
|
|
40
|
-
"name": space
|
|
41
|
-
},
|
|
42
|
-
"message": {
|
|
43
|
-
"text": chat_message
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
chat_result = self.google_chat_app.send_message(message_data=chat_data)
|
|
48
|
-
|
|
49
|
-
if not chat_result.get('success'):
|
|
50
|
-
logging.warning(f"Error al enviar notificación a Google Chat: {chat_result.get('message')}")
|
|
80
|
+
# 2. Enviar notificación según la configuración de la empresa
|
|
81
|
+
notification_text = (f"*Nuevo feedback de {company_short_name}*:\n"
|
|
82
|
+
f"*Usuario:* {user_identifier}\n"
|
|
83
|
+
f"*Mensaje:* {message}\n"
|
|
84
|
+
f"*Calificación:* {rating if rating is not None else 'N/A'}")
|
|
85
|
+
self._handle_notification(company, notification_text)
|
|
51
86
|
|
|
52
|
-
#
|
|
53
|
-
|
|
87
|
+
# 3. Guardar el feedback en la base de datos (independientemente del éxito de la notificación)
|
|
88
|
+
new_feedback_obj = UserFeedback(
|
|
54
89
|
company_id=company.id,
|
|
55
90
|
message=message,
|
|
56
|
-
|
|
57
|
-
external_user_id=external_user_id,
|
|
91
|
+
user_identifier=user_identifier,
|
|
58
92
|
rating=rating
|
|
59
93
|
)
|
|
60
|
-
|
|
61
|
-
if not
|
|
94
|
+
saved_feedback = self.profile_repo.save_feedback(new_feedback_obj)
|
|
95
|
+
if not saved_feedback:
|
|
96
|
+
logging.error(f"No se pudo guardar el feedback para el usuario {user_identifier} en la empresa {company_short_name}")
|
|
62
97
|
return {'error': 'No se pudo guardar el feedback'}
|
|
63
98
|
|
|
64
99
|
return {'message': 'Feedback guardado correctamente'}
|
|
65
100
|
|
|
66
101
|
except Exception as e:
|
|
102
|
+
logging.exception(f"Error crítico en el servicio de feedback: {e}")
|
|
67
103
|
return {'error': str(e)}
|
|
@@ -6,80 +6,138 @@
|
|
|
6
6
|
from iatoolkit.infra.redis_session_manager import RedisSessionManager
|
|
7
7
|
from typing import List, Dict, Optional
|
|
8
8
|
import json
|
|
9
|
+
import logging
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class UserSessionContextService:
|
|
12
13
|
"""
|
|
13
|
-
Gestiona el contexto de la sesión del usuario
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Usa RedisSessionManager para persistencia directa en Redis.
|
|
14
|
+
Gestiona el contexto de la sesión del usuario usando un único Hash de Redis por sesión.
|
|
15
|
+
Esto mejora la atomicidad y la eficiencia.
|
|
17
16
|
"""
|
|
18
17
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
if not user_identifier:
|
|
22
|
-
return None
|
|
23
|
-
return f"llm_history:{company_short_name}/{user_identifier}"
|
|
24
|
-
|
|
25
|
-
def _get_user_data_key(self, company_short_name: str, user_identifier: str) -> str:
|
|
18
|
+
def _get_session_key(self, company_short_name: str, user_identifier: str) -> Optional[str]:
|
|
19
|
+
"""Devuelve la clave única de Redis para el Hash de sesión del usuario."""
|
|
26
20
|
user_identifier = (user_identifier or "").strip()
|
|
27
|
-
if not user_identifier:
|
|
21
|
+
if not company_short_name or not user_identifier:
|
|
28
22
|
return None
|
|
29
|
-
return f"
|
|
23
|
+
return f"session:{company_short_name}/{user_identifier}"
|
|
30
24
|
|
|
31
25
|
def clear_all_context(self, company_short_name: str, user_identifier: str):
|
|
32
|
-
"""Limpia
|
|
33
|
-
self.
|
|
34
|
-
|
|
26
|
+
"""Limpia el contexto del LLM en la sesión para un usuario de forma atómica."""
|
|
27
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
28
|
+
if session_key:
|
|
29
|
+
# RedisSessionManager.remove(session_key)
|
|
30
|
+
# 'profile_data' should not be deleted
|
|
31
|
+
RedisSessionManager.hdel(session_key, 'context_version')
|
|
32
|
+
RedisSessionManager.hdel(session_key, 'context_history')
|
|
33
|
+
RedisSessionManager.hdel(session_key, 'last_response_id')
|
|
35
34
|
|
|
36
35
|
def clear_llm_history(self, company_short_name: str, user_identifier: str):
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
"""Limpia solo los campos relacionados con el historial del LLM (ID y chat)."""
|
|
37
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
38
|
+
if session_key:
|
|
39
|
+
RedisSessionManager.hdel(session_key, 'last_response_id', 'context_history')
|
|
40
|
+
|
|
41
|
+
def get_last_response_id(self, company_short_name: str, user_identifier: str) -> Optional[str]:
|
|
42
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
43
|
+
if not session_key:
|
|
44
44
|
return None
|
|
45
|
-
|
|
46
|
-
return RedisSessionManager.get(history_key, '')
|
|
45
|
+
return RedisSessionManager.hget(session_key, 'last_response_id')
|
|
47
46
|
|
|
48
47
|
def save_last_response_id(self, company_short_name: str, user_identifier: str, response_id: str):
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
RedisSessionManager.set(history_key, response_id)
|
|
48
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
49
|
+
if session_key:
|
|
50
|
+
RedisSessionManager.hset(session_key, 'last_response_id', response_id)
|
|
55
51
|
|
|
56
52
|
def save_context_history(self, company_short_name: str, user_identifier: str, context_history: List[Dict]):
|
|
57
|
-
|
|
58
|
-
if
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
54
|
+
if session_key:
|
|
55
|
+
try:
|
|
56
|
+
history_json = json.dumps(context_history)
|
|
57
|
+
RedisSessionManager.hset(session_key, 'context_history', history_json)
|
|
58
|
+
except (TypeError, ValueError) as e:
|
|
59
|
+
logging.error(f"Error al serializar context_history para {session_key}: {e}")
|
|
61
60
|
|
|
62
61
|
def get_context_history(self, company_short_name: str, user_identifier: str) -> Optional[List[Dict]]:
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
63
|
+
if not session_key:
|
|
64
|
+
return None
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
history_json = RedisSessionManager.hget(session_key, 'context_history')
|
|
67
|
+
if not history_json:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
return json.loads(history_json)
|
|
72
|
+
except json.JSONDecodeError:
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
def save_profile_data(self, company_short_name: str, user_identifier: str, data: dict):
|
|
76
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
77
|
+
if session_key:
|
|
78
|
+
try:
|
|
79
|
+
data_json = json.dumps(data)
|
|
80
|
+
RedisSessionManager.hset(session_key, 'profile_data', data_json)
|
|
81
|
+
except (TypeError, ValueError) as e:
|
|
82
|
+
logging.error(f"Error al serializar profile_data para {session_key}: {e}")
|
|
83
|
+
|
|
84
|
+
def get_profile_data(self, company_short_name: str, user_identifier: str) -> dict:
|
|
85
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
86
|
+
if not session_key:
|
|
77
87
|
return {}
|
|
78
88
|
|
|
79
|
-
|
|
89
|
+
data_json = RedisSessionManager.hget(session_key, 'profile_data')
|
|
90
|
+
if not data_json:
|
|
91
|
+
return {}
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
try:
|
|
94
|
+
return json.loads(data_json)
|
|
95
|
+
except json.JSONDecodeError:
|
|
96
|
+
return {}
|
|
97
|
+
|
|
98
|
+
def save_context_version(self, company_short_name: str, user_identifier: str, version: str):
|
|
99
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
100
|
+
if session_key:
|
|
101
|
+
RedisSessionManager.hset(session_key, 'context_version', version)
|
|
102
|
+
|
|
103
|
+
def get_context_version(self, company_short_name: str, user_identifier: str) -> Optional[str]:
|
|
104
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
105
|
+
if not session_key:
|
|
106
|
+
return None
|
|
107
|
+
return RedisSessionManager.hget(session_key, 'context_version')
|
|
108
|
+
|
|
109
|
+
def save_prepared_context(self, company_short_name: str, user_identifier: str, context: str, version: str):
|
|
110
|
+
"""Guarda un contexto de sistema pre-renderizado y su versión, listos para ser enviados al LLM."""
|
|
111
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
112
|
+
if session_key:
|
|
113
|
+
RedisSessionManager.hset(session_key, 'prepared_context', context)
|
|
114
|
+
RedisSessionManager.hset(session_key, 'prepared_context_version', version)
|
|
115
|
+
|
|
116
|
+
def get_and_clear_prepared_context(self, company_short_name: str, user_identifier: str) -> tuple:
|
|
117
|
+
"""Obtiene el contexto preparado y su versión, y los elimina para asegurar que se usan una sola vez."""
|
|
118
|
+
session_key = self._get_session_key(company_short_name, user_identifier)
|
|
119
|
+
if not session_key:
|
|
120
|
+
return None, None
|
|
121
|
+
|
|
122
|
+
pipe = RedisSessionManager.pipeline()
|
|
123
|
+
pipe.hget(session_key, 'prepared_context')
|
|
124
|
+
pipe.hget(session_key, 'prepared_context_version')
|
|
125
|
+
pipe.hdel(session_key, 'prepared_context', 'prepared_context_version')
|
|
126
|
+
results = pipe.execute()
|
|
127
|
+
|
|
128
|
+
# results[0] es el contexto, results[1] es la versión
|
|
129
|
+
return (results[0], results[1]) if results else (None, None)
|
|
130
|
+
|
|
131
|
+
# --- Métodos de Bloqueo ---
|
|
132
|
+
def acquire_lock(self, lock_key: str, expire_seconds: int) -> bool:
|
|
133
|
+
"""Intenta adquirir un lock. Devuelve True si se adquiere, False si no."""
|
|
134
|
+
# SET con NX (solo si no existe) y EX (expiración) es una operación atómica.
|
|
135
|
+
return RedisSessionManager.set(lock_key, "1", ex=expire_seconds, nx=True)
|
|
136
|
+
|
|
137
|
+
def release_lock(self, lock_key: str):
|
|
138
|
+
"""Libera un lock."""
|
|
139
|
+
RedisSessionManager.remove(lock_key)
|
|
140
|
+
|
|
141
|
+
def is_locked(self, lock_key: str) -> bool:
|
|
142
|
+
"""Verifica si un lock existe."""
|
|
143
|
+
return RedisSessionManager.exists(lock_key)
|
|
Binary file
|
|
@@ -42,14 +42,11 @@ $(document).ready(function () {
|
|
|
42
42
|
submitButton.html('<i class="bi bi-send me-1 icon-spaced"></i>Enviando...');
|
|
43
43
|
|
|
44
44
|
const response = await sendFeedback(feedbackText);
|
|
45
|
-
|
|
46
45
|
$('#feedbackModal').modal('hide');
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Swal.fire({ icon: 'error', title: 'Error', text: 'No se pudo enviar el feedback, intente nuevamente.' });
|
|
52
|
-
}
|
|
46
|
+
if (response)
|
|
47
|
+
toastr.success('¡Gracias por tu comentario!', 'Feedback Enviado');
|
|
48
|
+
else
|
|
49
|
+
toastr.error('No se pudo enviar el feedback, por favor intente nuevamente.', 'Error');
|
|
53
50
|
});
|
|
54
51
|
|
|
55
52
|
// Evento para abrir el modal de feedback
|
|
@@ -98,15 +95,13 @@ $(document).ready(function () {
|
|
|
98
95
|
const sendFeedback = async function(message) {
|
|
99
96
|
const activeStars = $('.star.active').length;
|
|
100
97
|
const data = {
|
|
101
|
-
"
|
|
98
|
+
"user_identifier": window.user_identifier,
|
|
102
99
|
"message": message,
|
|
103
100
|
"rating": activeStars,
|
|
104
|
-
"space": "spaces/AAQAupQldd4", // Este valor podría necesitar ser dinámico
|
|
105
|
-
"type": "MESSAGE_TRIGGER"
|
|
106
101
|
};
|
|
107
102
|
try {
|
|
108
103
|
// Asumiendo que callLLMAPI está definido globalmente en otro archivo (ej. chat_main.js)
|
|
109
|
-
const responseData = await
|
|
104
|
+
const responseData = await callToolkit('/api/feedback', data, "POST");
|
|
110
105
|
return responseData;
|
|
111
106
|
} catch (error) {
|
|
112
107
|
console.error("Error al enviar feedback:", error);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
$(document).ready(function () {
|
|
2
|
+
// Evento para abrir el modal de historial
|
|
3
|
+
$('#history-button').on('click', function() {
|
|
4
|
+
loadHistory();
|
|
5
|
+
$('#historyModal').modal('show');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// Evento delegado para el icono de copiar.
|
|
9
|
+
// Se adjunta UNA SOLA VEZ al cuerpo de la tabla y funciona para todas las filas
|
|
10
|
+
// que se añadan dinámicamente.
|
|
11
|
+
$('#history-table-body').on('click', '.copy-query-icon', function() {
|
|
12
|
+
const queryText = $(this).data('query');
|
|
13
|
+
|
|
14
|
+
// Copiar el texto al textarea del chat
|
|
15
|
+
if (queryText) {
|
|
16
|
+
$('#question').val(queryText);
|
|
17
|
+
autoResizeTextarea($('#question')[0]);
|
|
18
|
+
$('#send-button').removeClass('disabled');
|
|
19
|
+
|
|
20
|
+
// Cerrar el modal
|
|
21
|
+
$('#historyModal').modal('hide');
|
|
22
|
+
|
|
23
|
+
// Hacer focus en el textarea
|
|
24
|
+
$('#question').focus();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Variables globales para el historial
|
|
29
|
+
let historyData = [];
|
|
30
|
+
|
|
31
|
+
// Función para cargar el historial
|
|
32
|
+
async function loadHistory() {
|
|
33
|
+
const historyLoading = $('#history-loading');
|
|
34
|
+
const historyError = $('#history-error');
|
|
35
|
+
const historyContent = $('#history-content');
|
|
36
|
+
|
|
37
|
+
// Mostrar loading
|
|
38
|
+
historyLoading.show();
|
|
39
|
+
historyError.hide();
|
|
40
|
+
historyContent.hide();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const responseData = await callToolkit("/api/history", {}, "POST");
|
|
44
|
+
|
|
45
|
+
if (responseData && responseData.history) {
|
|
46
|
+
// Guardar datos globalmente
|
|
47
|
+
historyData = responseData.history;
|
|
48
|
+
|
|
49
|
+
// Mostrar todos los datos
|
|
50
|
+
displayAllHistory();
|
|
51
|
+
|
|
52
|
+
// Mostrar contenido
|
|
53
|
+
historyContent.show();
|
|
54
|
+
} else {
|
|
55
|
+
throw new Error('La respuesta del servidor no contenía el formato esperado.');
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("Error al cargar historial:", error);
|
|
59
|
+
|
|
60
|
+
const friendlyErrorMessage = "No hemos podido cargar tu historial en este momento. Por favor, cierra esta ventana y vuelve a intentarlo en unos instantes.";
|
|
61
|
+
const errorHtml = `
|
|
62
|
+
<div class="text-center p-4">
|
|
63
|
+
<i class="bi bi-exclamation-triangle text-danger" style="font-size: 2.5rem; opacity: 0.8;"></i>
|
|
64
|
+
<h5 class="mt-3 mb-2">Ocurrió un Problema</h5>
|
|
65
|
+
<p class="text-muted">${friendlyErrorMessage}</p>
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
historyError.html(errorHtml).show();
|
|
69
|
+
} finally {
|
|
70
|
+
historyLoading.hide();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Función para mostrar todo el historial
|
|
75
|
+
function displayAllHistory() {
|
|
76
|
+
const historyTableBody = $('#history-table-body');
|
|
77
|
+
|
|
78
|
+
// Limpiar tabla
|
|
79
|
+
historyTableBody.empty();
|
|
80
|
+
|
|
81
|
+
// Filtrar solo consultas que son strings simples
|
|
82
|
+
const filteredHistory = historyData.filter(item => {
|
|
83
|
+
try {
|
|
84
|
+
JSON.parse(item.query);
|
|
85
|
+
return false;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Poblar la tabla
|
|
92
|
+
filteredHistory.forEach((item, index) => {
|
|
93
|
+
const icon = $('<i>').addClass('bi bi-pencil-fill');
|
|
94
|
+
|
|
95
|
+
const link = $('<a>')
|
|
96
|
+
.attr('href', 'javascript:void(0);')
|
|
97
|
+
.addClass('copy-query-icon')
|
|
98
|
+
.attr('title', 'Copiar consulta al chat')
|
|
99
|
+
.data('query', item.query)
|
|
100
|
+
.append(icon);
|
|
101
|
+
|
|
102
|
+
const row = $('<tr>').append(
|
|
103
|
+
$('<td>').addClass('date-cell').text(formatDate(item.created_at)),
|
|
104
|
+
$('<td>').text(item.query),
|
|
105
|
+
$('<td>').addClass('text-center').append(link),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
historyTableBody.append(row);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Función para formatear fecha
|
|
113
|
+
function formatDate(dateString) {
|
|
114
|
+
const date = new Date(dateString);
|
|
115
|
+
|
|
116
|
+
const padTo2Digits = (num) => num.toString().padStart(2, '0');
|
|
117
|
+
|
|
118
|
+
const day = padTo2Digits(date.getDate());
|
|
119
|
+
const month = padTo2Digits(date.getMonth() + 1);
|
|
120
|
+
const year = date.getFullYear();
|
|
121
|
+
const hours = padTo2Digits(date.getHours());
|
|
122
|
+
const minutes = padTo2Digits(date.getMinutes());
|
|
123
|
+
|
|
124
|
+
return `${day}-${month} ${hours}:${minutes}`;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
2
|
+
const logoutButton = document.getElementById('logout-button');
|
|
3
|
+
if (!logoutButton) {
|
|
4
|
+
console.warn('El botón de logout con id "logout-button" no fue encontrado.');
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (window.toastr) {
|
|
9
|
+
toastr.options = { "positionClass": "toast-bottom-right", "preventDuplicates": true };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
logoutButton.addEventListener('click', async function(event) {
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const apiPath = '/api/logout';
|
|
17
|
+
const data = await callToolkit(apiPath, null, 'GET');
|
|
18
|
+
|
|
19
|
+
// Procesar la respuesta
|
|
20
|
+
if (data && data.status === 'success' && data.url) {
|
|
21
|
+
window.top.location.href = data.url;
|
|
22
|
+
} else {
|
|
23
|
+
// Si algo falla, callToolkit usualmente muestra un error.
|
|
24
|
+
// Mostramos un toast como fallback.
|
|
25
|
+
if (window.toastr) {
|
|
26
|
+
toastr.error('No se pudo procesar el cierre de sesión. Por favor, intente de nuevo.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Error durante el logout:', error);
|
|
31
|
+
if (window.toastr) {
|
|
32
|
+
toastr.error('Ocurrió un error de red al intentar cerrar sesión.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|