iatoolkit 0.11.0__py3-none-any.whl → 0.71.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/__init__.py +2 -6
- iatoolkit/base_company.py +9 -29
- iatoolkit/cli_commands.py +1 -1
- iatoolkit/common/routes.py +96 -52
- iatoolkit/common/session_manager.py +2 -1
- iatoolkit/common/util.py +17 -27
- iatoolkit/company_registry.py +1 -2
- iatoolkit/iatoolkit.py +97 -53
- iatoolkit/infra/llm_client.py +15 -20
- iatoolkit/infra/llm_proxy.py +38 -10
- iatoolkit/infra/openai_adapter.py +1 -1
- iatoolkit/infra/redis_session_manager.py +48 -2
- iatoolkit/locales/en.yaml +167 -0
- iatoolkit/locales/es.yaml +163 -0
- iatoolkit/repositories/database_manager.py +23 -3
- iatoolkit/repositories/document_repo.py +1 -1
- iatoolkit/repositories/models.py +35 -10
- iatoolkit/repositories/profile_repo.py +3 -2
- iatoolkit/repositories/vs_repo.py +26 -20
- iatoolkit/services/auth_service.py +193 -0
- iatoolkit/services/branding_service.py +70 -25
- iatoolkit/services/company_context_service.py +155 -0
- iatoolkit/services/configuration_service.py +133 -0
- iatoolkit/services/dispatcher_service.py +80 -105
- iatoolkit/services/document_service.py +5 -2
- iatoolkit/services/embedding_service.py +146 -0
- iatoolkit/services/excel_service.py +30 -26
- iatoolkit/services/file_processor_service.py +4 -12
- iatoolkit/services/history_service.py +7 -16
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +18 -29
- iatoolkit/services/language_service.py +83 -0
- iatoolkit/services/load_documents_service.py +100 -113
- iatoolkit/services/mail_service.py +9 -4
- iatoolkit/services/profile_service.py +152 -76
- iatoolkit/services/prompt_manager_service.py +20 -16
- iatoolkit/services/query_service.py +208 -96
- iatoolkit/services/search_service.py +11 -4
- iatoolkit/services/sql_service.py +57 -25
- iatoolkit/services/tasks_service.py +1 -1
- iatoolkit/services/user_feedback_service.py +72 -34
- 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 +110 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +135 -222
- iatoolkit/static/js/chat_onboarding_button.js +103 -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 +289 -210
- iatoolkit/static/styles/chat_modal.css +63 -77
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/query_main.prompt +5 -22
- 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 +180 -86
- iatoolkit/templates/chat_modals.html +138 -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 +45 -0
- iatoolkit/templates/onboarding_shell.html +107 -0
- iatoolkit/templates/signup.html +63 -65
- iatoolkit/views/base_login_view.py +91 -0
- iatoolkit/views/change_password_view.py +56 -31
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/external_login_view.py +61 -28
- iatoolkit/views/{file_store_view.py → file_store_api_view.py} +10 -3
- iatoolkit/views/forgot_password_view.py +27 -21
- 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 +74 -0
- iatoolkit/views/llmquery_api_view.py +58 -0
- iatoolkit/views/login_simulation_view.py +93 -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 +41 -36
- 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 +34 -29
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/METADATA +41 -23
- iatoolkit-0.71.2.dist-info/RECORD +122 -0
- iatoolkit-0.71.2.dist-info/licenses/LICENSE +21 -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.71.2.dist-info}/WHEEL +0 -0
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/top_level.txt +0 -0
|
@@ -3,65 +3,103 @@
|
|
|
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
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
9
10
|
from iatoolkit.infra.google_chat_app import GoogleChatApp
|
|
11
|
+
from iatoolkit.infra.mail_app import MailApp
|
|
10
12
|
import logging
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class UserFeedbackService:
|
|
14
16
|
@inject
|
|
15
|
-
def __init__(self,
|
|
17
|
+
def __init__(self,
|
|
18
|
+
profile_repo: ProfileRepo,
|
|
19
|
+
i18n_service: I18nService,
|
|
20
|
+
google_chat_app: GoogleChatApp,
|
|
21
|
+
mail_app: MailApp):
|
|
16
22
|
self.profile_repo = profile_repo
|
|
23
|
+
self.i18n_service = i18n_service
|
|
17
24
|
self.google_chat_app = google_chat_app
|
|
25
|
+
self.mail_app = mail_app
|
|
26
|
+
|
|
27
|
+
def _send_google_chat_notification(self, space_name: str, message_text: str):
|
|
28
|
+
"""Envía una notificación de feedback a un espacio de Google Chat."""
|
|
29
|
+
try:
|
|
30
|
+
chat_data = {
|
|
31
|
+
"type": "MESSAGE_TRIGGER",
|
|
32
|
+
"space": {"name": space_name},
|
|
33
|
+
"message": {"text": message_text}
|
|
34
|
+
}
|
|
35
|
+
chat_result = self.google_chat_app.send_message(message_data=chat_data)
|
|
36
|
+
if not chat_result.get('success'):
|
|
37
|
+
logging.warning(f"error sending notification to Google Chat: {chat_result.get('message')}")
|
|
38
|
+
except Exception as e:
|
|
39
|
+
logging.exception(f"error sending notification to Google Chat: {e}")
|
|
40
|
+
|
|
41
|
+
def _send_email_notification(self, destination_email: str, company_name: str, message_text: str):
|
|
42
|
+
"""Envía una notificación de feedback por correo electrónico."""
|
|
43
|
+
try:
|
|
44
|
+
subject = f"Nuevo Feedback de {company_name}"
|
|
45
|
+
# Convertir el texto plano a un HTML simple para mantener los saltos de línea
|
|
46
|
+
html_body = message_text.replace('\n', '<br>')
|
|
47
|
+
self.mail_app.send_email(to=destination_email, subject=subject, body=html_body)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logging.exception(f"error sending email de feedback: {e}")
|
|
50
|
+
|
|
51
|
+
def _handle_notification(self, company: Company, message_text: str):
|
|
52
|
+
"""Lee la configuración de la empresa y envía la notificación al canal correspondiente."""
|
|
53
|
+
feedback_params = company.parameters.get('user_feedback')
|
|
54
|
+
if not isinstance(feedback_params, dict):
|
|
55
|
+
logging.warning(f"missing 'user_feedback' configuration for company: {company.short_name}.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# get channel and destination
|
|
59
|
+
channel = feedback_params.get('channel')
|
|
60
|
+
destination = feedback_params.get('destination')
|
|
61
|
+
if not channel or not destination:
|
|
62
|
+
logging.warning(f"invalid 'user_feedback' configuration for: {company.short_name}. Faltan 'channel' o 'destination'.")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
if channel == 'google_chat':
|
|
66
|
+
self._send_google_chat_notification(space_name=destination, message_text=message_text)
|
|
67
|
+
elif channel == 'email':
|
|
68
|
+
self._send_email_notification(destination_email=destination, company_name=company.short_name, message_text=message_text)
|
|
69
|
+
else:
|
|
70
|
+
logging.warning(f"unknown feedback channel: '{channel}' for company {company.short_name}.")
|
|
18
71
|
|
|
19
72
|
def new_feedback(self,
|
|
20
73
|
company_short_name: str,
|
|
21
74
|
message: str,
|
|
22
|
-
|
|
23
|
-
local_user_id: int = 0,
|
|
24
|
-
space: str = None,
|
|
25
|
-
type: str = None,
|
|
75
|
+
user_identifier: str,
|
|
26
76
|
rating: int = None) -> dict:
|
|
27
77
|
try:
|
|
28
|
-
# validate company
|
|
29
78
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
30
79
|
if not company:
|
|
31
|
-
return {'error':
|
|
32
|
-
|
|
33
|
-
# send notification to Google Chat
|
|
34
|
-
chat_message = f"*Nuevo feedback de {company_short_name}*:\n*Usuario:* {external_user_id or local_user_id}\n*Mensaje:* {message}\n*Calificación:* {rating}"
|
|
35
|
-
|
|
36
|
-
# TO DO: get the space and type from the input data
|
|
37
|
-
chat_data = {
|
|
38
|
-
"type": type,
|
|
39
|
-
"space": {
|
|
40
|
-
"name": space
|
|
41
|
-
},
|
|
42
|
-
"message": {
|
|
43
|
-
"text": chat_message
|
|
44
|
-
}
|
|
45
|
-
}
|
|
80
|
+
return {'error': self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
|
|
46
81
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
# 2. send notification using company configuration
|
|
83
|
+
notification_text = (f"*Nuevo feedback de {company_short_name}*:\n"
|
|
84
|
+
f"*Usuario:* {user_identifier}\n"
|
|
85
|
+
f"*Mensaje:* {message}\n"
|
|
86
|
+
f"*Calificación:* {rating if rating is not None else 'N/A'}")
|
|
87
|
+
self._handle_notification(company, notification_text)
|
|
51
88
|
|
|
52
|
-
#
|
|
53
|
-
|
|
89
|
+
# 3. always save the feedback in the database
|
|
90
|
+
new_feedback_obj = UserFeedback(
|
|
54
91
|
company_id=company.id,
|
|
55
92
|
message=message,
|
|
56
|
-
|
|
57
|
-
external_user_id=external_user_id,
|
|
93
|
+
user_identifier=user_identifier,
|
|
58
94
|
rating=rating
|
|
59
95
|
)
|
|
60
|
-
|
|
61
|
-
if not
|
|
62
|
-
|
|
96
|
+
saved_feedback = self.profile_repo.save_feedback(new_feedback_obj)
|
|
97
|
+
if not saved_feedback:
|
|
98
|
+
logging.error(f"can't save feedback for user {user_identifier}/{company_short_name}")
|
|
99
|
+
return {'error': 'can not save the feedback'}
|
|
63
100
|
|
|
64
|
-
return {'message': 'Feedback guardado correctamente'}
|
|
101
|
+
return {'success': True, 'message': 'Feedback guardado correctamente'}
|
|
65
102
|
|
|
66
103
|
except Exception as e:
|
|
104
|
+
logging.exception(f"Error crítico en el servicio de feedback: {e}")
|
|
67
105
|
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
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
$(document).ready(function () {
|
|
2
|
+
const feedbackModal = $('#feedbackModal');
|
|
3
|
+
$('#submit-feedback').on('click', function () {
|
|
4
|
+
sendFeedback(this);
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
// Evento para enviar el feedback
|
|
8
|
+
async function sendFeedback(submitButton) {
|
|
9
|
+
toastr.options = {"positionClass": "toast-bottom-right", "preventDuplicates": true};
|
|
10
|
+
const feedbackText = $('#feedback-text').val().trim();
|
|
11
|
+
const activeStars = $('.star.active').length;
|
|
12
|
+
|
|
13
|
+
if (!feedbackText) {
|
|
14
|
+
toastr.error(t_js('feedback_comment_error'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (activeStars === 0) {
|
|
19
|
+
toastr.error(t_js('feedback_rating_error'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
submitButton.disabled = true;
|
|
24
|
+
|
|
25
|
+
// call the IAToolkit API to send feedback
|
|
26
|
+
const data = {
|
|
27
|
+
"user_identifier": window.user_identifier,
|
|
28
|
+
"message": feedbackText,
|
|
29
|
+
"rating": activeStars,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const responseData = await callToolkit('/api/feedback', data, "POST");
|
|
33
|
+
if (responseData)
|
|
34
|
+
toastr.success(t_js('feedback_sent_success_body'), t_js('feedback_sent_success_title'));
|
|
35
|
+
else
|
|
36
|
+
toastr.error(t_js('feedback_sent_error'));
|
|
37
|
+
|
|
38
|
+
submitButton.disabled = false;
|
|
39
|
+
feedbackModal.modal('hide');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Evento para abrir el modal de feedback
|
|
43
|
+
$('#send-feedback-button').on('click', function () {
|
|
44
|
+
$('#submit-feedback').prop('disabled', false);
|
|
45
|
+
$('.star').removeClass('active hover-active'); // Resetea estrellas
|
|
46
|
+
$('#feedback-text').val('');
|
|
47
|
+
feedbackModal.modal('show');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Evento que se dispara DESPUÉS de que el modal se ha ocultado
|
|
51
|
+
$('#feedbackModal').on('hidden.bs.modal', function () {
|
|
52
|
+
$('#feedback-text').val('');
|
|
53
|
+
$('.star').removeClass('active');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Function for the star rating system
|
|
57
|
+
window.gfg = function (rating) {
|
|
58
|
+
$('.star').removeClass('active');
|
|
59
|
+
$('.star').each(function (index) {
|
|
60
|
+
if (index < rating) {
|
|
61
|
+
$(this).addClass('active');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
$('.star').hover(
|
|
67
|
+
function () {
|
|
68
|
+
const rating = $(this).data('rating');
|
|
69
|
+
$('.star').removeClass('hover-active');
|
|
70
|
+
$('.star').each(function (index) {
|
|
71
|
+
if ($(this).data('rating') <= rating) {
|
|
72
|
+
$(this).addClass('hover-active');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
function () {
|
|
77
|
+
$('.star').removeClass('hover-active');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
$(document).ready(function () {
|
|
2
|
+
|
|
3
|
+
let helpContent = null; // Variable para cachear el contenido de ayuda
|
|
4
|
+
|
|
5
|
+
// Evento de clic en el botón de ayuda
|
|
6
|
+
$('#open-help-button').on('click', async function () {
|
|
7
|
+
const helpModal = new bootstrap.Modal(document.getElementById('helpModal'));
|
|
8
|
+
const accordionContainer = $('#help-accordion-container');
|
|
9
|
+
const spinner = $('#help-spinner');
|
|
10
|
+
|
|
11
|
+
// Si el contenido no se ha cargado, hacer la llamada a la API
|
|
12
|
+
if (helpContent) {
|
|
13
|
+
// Si el contenido ya está cacheado, solo muestra el modal
|
|
14
|
+
helpModal.show();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
spinner.show();
|
|
19
|
+
accordionContainer.hide();
|
|
20
|
+
helpModal.show();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const helpContent = await callToolkit('/api/help-content', {}, "POST");
|
|
24
|
+
|
|
25
|
+
if (!helpContent) {
|
|
26
|
+
toastr.error('No se pudo cargar la guía de uso. Por favor, intente más tarde.');
|
|
27
|
+
spinner.hide();
|
|
28
|
+
helpModal.hide();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Construir el HTML del acordeón y mostrarlo
|
|
33
|
+
buildHelpAccordion(helpContent);
|
|
34
|
+
spinner.hide();
|
|
35
|
+
accordionContainer.show();
|
|
36
|
+
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("Error al cargar el contenido de ayuda:", error);
|
|
39
|
+
toastr.error('Ocurrió un error de red al cargar la guía.');
|
|
40
|
+
spinner.hide();
|
|
41
|
+
helpModal.hide();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Construye dinámicamente el HTML para el acordeón de ayuda a partir de los datos.
|
|
47
|
+
* @param {object} data El objeto JSON con el contenido de ayuda.
|
|
48
|
+
*/
|
|
49
|
+
function buildHelpAccordion(data) {
|
|
50
|
+
const container = $('#help-accordion-container');
|
|
51
|
+
container.empty(); // Limpiar cualquier contenido previo
|
|
52
|
+
|
|
53
|
+
let accordionHtml = '';
|
|
54
|
+
|
|
55
|
+
if (data.example_questions) {
|
|
56
|
+
let contentHtml = '';
|
|
57
|
+
data.example_questions.forEach(cat => {
|
|
58
|
+
contentHtml += `<h6 class="fw-bold">${cat.category}</h6><ul>`;
|
|
59
|
+
cat.questions.forEach(q => contentHtml += `<li>${q}</li>`);
|
|
60
|
+
contentHtml += `</ul>`;
|
|
61
|
+
});
|
|
62
|
+
accordionHtml += createAccordionItem('examples', 'Preguntas de Ejemplo', contentHtml, true);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (data.data_sources) {
|
|
66
|
+
let contentHtml = '<dl>';
|
|
67
|
+
data.data_sources.forEach(p => {
|
|
68
|
+
contentHtml += `<dt>${p.source}</dt><dd>${p.description}</dd>`;
|
|
69
|
+
});
|
|
70
|
+
contentHtml += `</dl>`;
|
|
71
|
+
accordionHtml += createAccordionItem('sources', 'Datos disponibles', contentHtml );
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (data.best_practices) {
|
|
75
|
+
let contentHtml = '<dl>';
|
|
76
|
+
data.best_practices.forEach(p => {
|
|
77
|
+
contentHtml += `<dt>${p.title}</dt><dd>${p.description}`;
|
|
78
|
+
if (p.example) {
|
|
79
|
+
contentHtml += `<br><small class="text-muted"><em>Ej: "${p.example}"</em></small>`;
|
|
80
|
+
}
|
|
81
|
+
contentHtml += `</dd>`;
|
|
82
|
+
});
|
|
83
|
+
contentHtml += `</dl>`;
|
|
84
|
+
accordionHtml += createAccordionItem('practices', 'Mejores Prácticas', contentHtml);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (data.capabilities) {
|
|
88
|
+
let contentHtml = `<div class="row">`;
|
|
89
|
+
contentHtml += `<div class="col-md-6"><h6 class="fw-bold">Puede hacer:</h6><ul>${data.capabilities.can_do.map(item => `<li>${item}</li>`).join('')}</ul></div>`;
|
|
90
|
+
contentHtml += `<div class="col-md-6"><h6 class="fw-bold">No puede hacer:</h6><ul>${data.capabilities.cannot_do.map(item => `<li>${item}</li>`).join('')}</ul></div>`;
|
|
91
|
+
contentHtml += `</div>`;
|
|
92
|
+
accordionHtml += createAccordionItem('capabilities', 'Capacidades y Límites', contentHtml);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
container.html(accordionHtml);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Helper para crear un item del acordeón de Bootstrap.
|
|
100
|
+
* @param {string} id El ID base para los elementos.
|
|
101
|
+
* @param {string} title El título que se muestra en el botón del acordeón.
|
|
102
|
+
* @param {string} contentHtml El HTML que va dentro del cuerpo colapsable.
|
|
103
|
+
* @param {boolean} isOpen Si el item debe estar abierto por defecto.
|
|
104
|
+
* @returns {string} El string HTML del item del acordeón.
|
|
105
|
+
*/
|
|
106
|
+
function createAccordionItem(id, title, contentHtml, isOpen = false) {
|
|
107
|
+
const showClass = isOpen ? 'show' : '';
|
|
108
|
+
const collapsedClass = isOpen ? '' : 'collapsed';
|
|
109
|
+
|
|
110
|
+
return `
|
|
111
|
+
<div class="accordion-item">
|
|
112
|
+
<h2 class="accordion-header" id="heading-${id}">
|
|
113
|
+
<button class="accordion-button ${collapsedClass}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-${id}" aria-expanded="${isOpen}" aria-controls="collapse-${id}">
|
|
114
|
+
${title}
|
|
115
|
+
</button>
|
|
116
|
+
</h2>
|
|
117
|
+
<div id="collapse-${id}" class="accordion-collapse collapse ${showClass}" aria-labelledby="heading-${id}" data-bs-parent="#help-accordion-container">
|
|
118
|
+
<div class="accordion-body">
|
|
119
|
+
${contentHtml}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>`;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
$(document).ready(function () {
|
|
2
|
+
// Evento para abrir el modal de historial
|
|
3
|
+
const historyModal = $('#historyModal');
|
|
4
|
+
|
|
5
|
+
$('#history-button').on('click', function() {
|
|
6
|
+
historyModal.modal('show');
|
|
7
|
+
loadHistory();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
// Función para cargar el historial
|
|
12
|
+
async function loadHistory() {
|
|
13
|
+
const historyLoading = $('#history-loading');
|
|
14
|
+
const historyContent = $('#history-content');
|
|
15
|
+
const historyTable = historyContent.find('table');
|
|
16
|
+
const noHistoryMessage = $('#no-history-message');
|
|
17
|
+
|
|
18
|
+
// prepare UI for loading
|
|
19
|
+
historyLoading.show();
|
|
20
|
+
historyContent.hide();
|
|
21
|
+
historyTable.show();
|
|
22
|
+
noHistoryMessage.hide();
|
|
23
|
+
|
|
24
|
+
// cal the toolkit, handle the response and errors
|
|
25
|
+
const data = await callToolkit("/api/history", {}, "POST");
|
|
26
|
+
|
|
27
|
+
if (!data || !data.history) {
|
|
28
|
+
historyLoading.hide();
|
|
29
|
+
toastr.error(t_js('error_loading_history'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (data.history.length === 0) {
|
|
34
|
+
historyTable.hide();
|
|
35
|
+
noHistoryMessage.show();
|
|
36
|
+
} else
|
|
37
|
+
displayAllHistory(data.history);
|
|
38
|
+
|
|
39
|
+
historyLoading.hide();
|
|
40
|
+
historyContent.show();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Función para mostrar todo el historial
|
|
44
|
+
function displayAllHistory(historyData) {
|
|
45
|
+
const historyTableBody = $('#history-table-body');
|
|
46
|
+
|
|
47
|
+
historyTableBody.empty();
|
|
48
|
+
|
|
49
|
+
// Filtrar solo consultas que son strings simples
|
|
50
|
+
const filteredHistory = historyData.filter(item => {
|
|
51
|
+
try {
|
|
52
|
+
JSON.parse(item.query);
|
|
53
|
+
return false;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Poblar la tabla
|
|
60
|
+
filteredHistory.forEach((item, index) => {
|
|
61
|
+
const icon = $('<i>').addClass('bi bi-pencil-fill');
|
|
62
|
+
|
|
63
|
+
const edit_link = $('<a>')
|
|
64
|
+
.attr('href', 'javascript:void(0);')
|
|
65
|
+
.addClass('edit-pencil')
|
|
66
|
+
.attr('title', t_js('edit_query'))
|
|
67
|
+
.data('query', item.query)
|
|
68
|
+
.append(icon);
|
|
69
|
+
|
|
70
|
+
const row = $('<tr>').append(
|
|
71
|
+
$('<td>').addClass('text-nowrap').text(formatDate(item.created_at)),
|
|
72
|
+
$('<td>').text(item.query),
|
|
73
|
+
$('<td>').append(edit_link),
|
|
74
|
+
);
|
|
75
|
+
historyTableBody.append(row);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatDate(dateString) {
|
|
80
|
+
const date = new Date(dateString);
|
|
81
|
+
const padTo2Digits = (num) => num.toString().padStart(2, '0');
|
|
82
|
+
|
|
83
|
+
const day = padTo2Digits(date.getDate());
|
|
84
|
+
const month = padTo2Digits(date.getMonth() + 1);
|
|
85
|
+
const year = date.getFullYear();
|
|
86
|
+
const hours = padTo2Digits(date.getHours());
|
|
87
|
+
const minutes = padTo2Digits(date.getMinutes());
|
|
88
|
+
|
|
89
|
+
return `${day}-${month} ${hours}:${minutes}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// event handler for the edit pencil icon
|
|
93
|
+
$('#history-table-body').on('click', '.edit-pencil', function() {
|
|
94
|
+
const queryText = $(this).data('query');
|
|
95
|
+
|
|
96
|
+
// copy the text to the chat input box
|
|
97
|
+
if (queryText) {
|
|
98
|
+
$('#question').val(queryText);
|
|
99
|
+
autoResizeTextarea($('#question')[0]);
|
|
100
|
+
$('#send-button').removeClass('disabled');
|
|
101
|
+
|
|
102
|
+
// Cerrar el modal
|
|
103
|
+
$('#historyModal').modal('hide');
|
|
104
|
+
|
|
105
|
+
// Hacer focus en el textarea
|
|
106
|
+
if (window.innerWidth > 768)
|
|
107
|
+
$('#question').focus();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|