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.

Files changed (159) hide show
  1. iatoolkit/__init__.py +8 -34
  2. iatoolkit/base_company.py +14 -3
  3. iatoolkit/common/routes.py +83 -52
  4. iatoolkit/common/session_manager.py +0 -1
  5. iatoolkit/common/util.py +0 -27
  6. iatoolkit/iatoolkit.py +61 -46
  7. iatoolkit/infra/llm_client.py +7 -8
  8. iatoolkit/infra/openai_adapter.py +1 -1
  9. iatoolkit/infra/redis_session_manager.py +48 -2
  10. iatoolkit/repositories/database_manager.py +17 -2
  11. iatoolkit/repositories/models.py +31 -6
  12. iatoolkit/repositories/profile_repo.py +7 -2
  13. iatoolkit/services/auth_service.py +188 -0
  14. iatoolkit/services/branding_service.py +147 -0
  15. iatoolkit/services/dispatcher_service.py +10 -40
  16. iatoolkit/services/excel_service.py +15 -15
  17. iatoolkit/services/history_service.py +3 -12
  18. iatoolkit/services/jwt_service.py +15 -24
  19. iatoolkit/services/onboarding_service.py +43 -0
  20. iatoolkit/services/profile_service.py +97 -44
  21. iatoolkit/services/query_service.py +124 -81
  22. iatoolkit/services/tasks_service.py +1 -1
  23. iatoolkit/services/user_feedback_service.py +67 -31
  24. iatoolkit/services/user_session_context_service.py +112 -54
  25. iatoolkit/static/images/fernando.jpeg +0 -0
  26. iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +6 -11
  27. iatoolkit/static/js/chat_history_button.js +126 -0
  28. iatoolkit/static/js/chat_logout_button.js +36 -0
  29. iatoolkit/static/js/chat_main.js +130 -220
  30. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  31. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  32. iatoolkit/static/js/chat_reload_button.js +52 -0
  33. iatoolkit/static/styles/chat_iatoolkit.css +329 -507
  34. iatoolkit/static/styles/chat_modal.css +95 -56
  35. iatoolkit/static/styles/landing_page.css +182 -0
  36. iatoolkit/static/styles/onboarding.css +169 -0
  37. iatoolkit/system_prompts/query_main.prompt +3 -12
  38. iatoolkit/templates/_company_header.html +20 -0
  39. iatoolkit/templates/_login_widget.html +40 -0
  40. iatoolkit/templates/base.html +8 -3
  41. iatoolkit/templates/change_password.html +54 -37
  42. iatoolkit/templates/chat.html +149 -66
  43. iatoolkit/templates/chat_modals.html +47 -18
  44. iatoolkit/templates/error.html +41 -8
  45. iatoolkit/templates/forgot_password.html +37 -24
  46. iatoolkit/templates/index.html +140 -0
  47. iatoolkit/templates/login_simulation.html +34 -0
  48. iatoolkit/templates/onboarding_shell.html +105 -0
  49. iatoolkit/templates/signup.html +64 -66
  50. iatoolkit/views/base_login_view.py +81 -0
  51. iatoolkit/views/change_password_view.py +23 -12
  52. iatoolkit/views/external_login_view.py +61 -28
  53. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +9 -2
  54. iatoolkit/views/forgot_password_view.py +23 -13
  55. iatoolkit/views/history_api_view.py +52 -0
  56. iatoolkit/views/home_view.py +58 -25
  57. iatoolkit/views/index_view.py +14 -0
  58. iatoolkit/views/init_context_api_view.py +68 -0
  59. iatoolkit/views/llmquery_api_view.py +45 -0
  60. iatoolkit/views/login_simulation_view.py +81 -0
  61. iatoolkit/views/login_view.py +118 -34
  62. iatoolkit/views/logout_api_view.py +45 -0
  63. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +7 -7
  64. iatoolkit/views/signup_view.py +38 -29
  65. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  66. iatoolkit/views/tasks_review_api_view.py +55 -0
  67. iatoolkit/views/{user_feedback_view.py → user_feedback_api_view.py} +16 -31
  68. iatoolkit/views/verify_user_view.py +13 -8
  69. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/METADATA +2 -2
  70. iatoolkit-0.63.4.dist-info/RECORD +113 -0
  71. {iatoolkit-0.8.1.dist-info → iatoolkit-0.63.4.dist-info}/top_level.txt +0 -1
  72. iatoolkit/common/auth.py +0 -200
  73. iatoolkit/static/images/arrow_up.png +0 -0
  74. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  75. iatoolkit/static/images/logo_clinica.png +0 -0
  76. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  77. iatoolkit/static/images/logo_maxxa.png +0 -0
  78. iatoolkit/static/images/logo_notaria.png +0 -0
  79. iatoolkit/static/images/logo_tarjeta.png +0 -0
  80. iatoolkit/static/images/logo_umayor.png +0 -0
  81. iatoolkit/static/images/upload.png +0 -0
  82. iatoolkit/static/js/chat_history.js +0 -117
  83. iatoolkit/templates/home.html +0 -201
  84. iatoolkit/templates/login.html +0 -43
  85. iatoolkit/views/chat_token_request_view.py +0 -98
  86. iatoolkit/views/chat_view.py +0 -51
  87. iatoolkit/views/download_file_view.py +0 -58
  88. iatoolkit/views/external_chat_login_view.py +0 -88
  89. iatoolkit/views/history_view.py +0 -57
  90. iatoolkit/views/llmquery_view.py +0 -65
  91. iatoolkit/views/tasks_review_view.py +0 -83
  92. iatoolkit-0.8.1.dist-info/RECORD +0 -175
  93. tests/__init__.py +0 -5
  94. tests/common/__init__.py +0 -0
  95. tests/common/test_auth.py +0 -279
  96. tests/common/test_routes.py +0 -42
  97. tests/common/test_session_manager.py +0 -59
  98. tests/common/test_util.py +0 -444
  99. tests/companies/__init__.py +0 -5
  100. tests/conftest.py +0 -36
  101. tests/infra/__init__.py +0 -5
  102. tests/infra/connectors/__init__.py +0 -5
  103. tests/infra/connectors/test_google_drive_connector.py +0 -107
  104. tests/infra/connectors/test_local_file_connector.py +0 -85
  105. tests/infra/connectors/test_s3_connector.py +0 -95
  106. tests/infra/test_call_service.py +0 -92
  107. tests/infra/test_database_manager.py +0 -59
  108. tests/infra/test_gemini_adapter.py +0 -137
  109. tests/infra/test_google_chat_app.py +0 -68
  110. tests/infra/test_llm_client.py +0 -165
  111. tests/infra/test_llm_proxy.py +0 -122
  112. tests/infra/test_mail_app.py +0 -94
  113. tests/infra/test_openai_adapter.py +0 -105
  114. tests/infra/test_redis_session_manager_service.py +0 -117
  115. tests/repositories/__init__.py +0 -5
  116. tests/repositories/test_database_manager.py +0 -87
  117. tests/repositories/test_document_repo.py +0 -76
  118. tests/repositories/test_llm_query_repo.py +0 -340
  119. tests/repositories/test_models.py +0 -38
  120. tests/repositories/test_profile_repo.py +0 -142
  121. tests/repositories/test_tasks_repo.py +0 -76
  122. tests/repositories/test_vs_repo.py +0 -107
  123. tests/services/__init__.py +0 -5
  124. tests/services/test_dispatcher_service.py +0 -274
  125. tests/services/test_document_service.py +0 -181
  126. tests/services/test_excel_service.py +0 -208
  127. tests/services/test_file_processor_service.py +0 -121
  128. tests/services/test_history_service.py +0 -164
  129. tests/services/test_jwt_service.py +0 -255
  130. tests/services/test_load_documents_service.py +0 -112
  131. tests/services/test_mail_service.py +0 -70
  132. tests/services/test_profile_service.py +0 -379
  133. tests/services/test_prompt_manager_service.py +0 -190
  134. tests/services/test_query_service.py +0 -243
  135. tests/services/test_search_service.py +0 -39
  136. tests/services/test_sql_service.py +0 -160
  137. tests/services/test_tasks_service.py +0 -252
  138. tests/services/test_user_feedback_service.py +0 -389
  139. tests/services/test_user_session_context_service.py +0 -132
  140. tests/views/__init__.py +0 -5
  141. tests/views/test_change_password_view.py +0 -191
  142. tests/views/test_chat_token_request_view.py +0 -188
  143. tests/views/test_chat_view.py +0 -98
  144. tests/views/test_download_file_view.py +0 -149
  145. tests/views/test_external_chat_login_view.py +0 -120
  146. tests/views/test_external_login_view.py +0 -102
  147. tests/views/test_file_store_view.py +0 -128
  148. tests/views/test_forgot_password_view.py +0 -142
  149. tests/views/test_history_view.py +0 -336
  150. tests/views/test_home_view.py +0 -61
  151. tests/views/test_llm_query_view.py +0 -154
  152. tests/views/test_login_view.py +0 -114
  153. tests/views/test_prompt_view.py +0 -111
  154. tests/views/test_signup_view.py +0 -140
  155. tests/views/test_tasks_review_view.py +0 -104
  156. tests/views/test_tasks_view.py +0 -130
  157. tests/views/test_user_feedback_view.py +0 -214
  158. tests/views/test_verify_user_view.py +0 -110
  159. {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, profile_repo: ProfileRepo, google_chat_app: GoogleChatApp):
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
- external_user_id: str = None,
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
- # validate company
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
- # 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
- }
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
- # create the UserFeedback object
53
- new_feedback = UserFeedback(
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
- local_user_id=local_user_id,
57
- external_user_id=external_user_id,
91
+ user_identifier=user_identifier,
58
92
  rating=rating
59
93
  )
60
- new_feedback = self.profile_repo.save_feedback(new_feedback)
61
- if not new_feedback:
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, incluyendo el historial
14
- de conversación con el LLM y datos de la sesión del usuario.
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 _get_llm_history_key(self, company_short_name: str, user_identifier: str) -> str:
20
- user_identifier = (user_identifier or "").strip()
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"user_data:{company_short_name}/{user_identifier}"
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 todo el contexto de sesión para un usuario."""
33
- self.clear_llm_history(company_short_name, user_identifier)
34
- self.clear_user_session_data(company_short_name, user_identifier)
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
- history_key = self._get_llm_history_key(company_short_name, user_identifier)
38
- if history_key:
39
- RedisSessionManager.remove(history_key)
40
-
41
- def get_last_response_id(self, company_short_name: str, user_identifier: str) -> str:
42
- history_key = self._get_llm_history_key(company_short_name, user_identifier)
43
- if not history_key:
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
- user_identifier = (user_identifier or "").strip()
50
- history_key = self._get_llm_history_key(company_short_name, user_identifier)
51
- if not history_key or not user_identifier:
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
- history_key = f"chat_history:{company_short_name}/{user_identifier}"
58
- if not history_key:
59
- return
60
- RedisSessionManager.set(history_key, json.dumps(context_history))
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
- history_key = f"chat_history:{company_short_name}/{user_identifier}"
64
- return RedisSessionManager.get_json(history_key, {})
62
+ session_key = self._get_session_key(company_short_name, user_identifier)
63
+ if not session_key:
64
+ return None
65
65
 
66
- def save_user_session_data(self, company_short_name: str, user_identifier: str, data: dict):
67
- """Guarda un diccionario de datos en la sesión del usuario."""
68
- user_identifier = (user_identifier or "").strip()
69
- data_key = self._get_user_data_key(company_short_name, user_identifier)
70
- if data_key:
71
- RedisSessionManager.set_json(data_key, data)
72
-
73
- def get_user_session_data(self, company_short_name: str, user_identifier: str) -> dict:
74
- """Recupera el diccionario de datos de la sesión del usuario."""
75
- data_key = self._get_user_data_key(company_short_name, user_identifier)
76
- if not data_key:
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
- return RedisSessionManager.get_json(data_key, {})
89
+ data_json = RedisSessionManager.hget(session_key, 'profile_data')
90
+ if not data_json:
91
+ return {}
80
92
 
81
- def clear_user_session_data(self, company_short_name: str, user_identifier: str):
82
- """Limpia los datos de la sesión del usuario."""
83
- data_key = self._get_user_data_key(company_short_name, user_identifier)
84
- if data_key:
85
- RedisSessionManager.remove(data_key)
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
- if (response) {
49
- Swal.fire({ icon: 'success', title: 'Feedback enviado', text: 'Gracias por tu comentario.' });
50
- } else {
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
- "external_user_id": window.externalUserId,
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 callLLMAPI('/feedback', data, "POST");
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
+ });