iatoolkit 0.3.9__py3-none-any.whl → 0.107.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 (150) hide show
  1. iatoolkit/__init__.py +27 -35
  2. iatoolkit/base_company.py +3 -35
  3. iatoolkit/cli_commands.py +18 -47
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +48 -0
  6. iatoolkit/common/interfaces/__init__.py +0 -0
  7. iatoolkit/common/interfaces/asset_storage.py +34 -0
  8. iatoolkit/common/interfaces/database_provider.py +39 -0
  9. iatoolkit/common/model_registry.py +159 -0
  10. iatoolkit/common/routes.py +138 -0
  11. iatoolkit/common/session_manager.py +26 -0
  12. iatoolkit/common/util.py +353 -0
  13. iatoolkit/company_registry.py +66 -29
  14. iatoolkit/core.py +514 -0
  15. iatoolkit/infra/__init__.py +5 -0
  16. iatoolkit/infra/brevo_mail_app.py +123 -0
  17. iatoolkit/infra/call_service.py +140 -0
  18. iatoolkit/infra/connectors/__init__.py +5 -0
  19. iatoolkit/infra/connectors/file_connector.py +17 -0
  20. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  21. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  22. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  23. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  24. iatoolkit/infra/connectors/s3_connector.py +33 -0
  25. iatoolkit/infra/google_chat_app.py +57 -0
  26. iatoolkit/infra/llm_providers/__init__.py +0 -0
  27. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  28. iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
  29. iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
  30. iatoolkit/infra/llm_proxy.py +268 -0
  31. iatoolkit/infra/llm_response.py +45 -0
  32. iatoolkit/infra/redis_session_manager.py +122 -0
  33. iatoolkit/locales/en.yaml +222 -0
  34. iatoolkit/locales/es.yaml +225 -0
  35. iatoolkit/repositories/__init__.py +5 -0
  36. iatoolkit/repositories/database_manager.py +187 -0
  37. iatoolkit/repositories/document_repo.py +33 -0
  38. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  39. iatoolkit/repositories/llm_query_repo.py +105 -0
  40. iatoolkit/repositories/models.py +279 -0
  41. iatoolkit/repositories/profile_repo.py +171 -0
  42. iatoolkit/repositories/vs_repo.py +150 -0
  43. iatoolkit/services/__init__.py +5 -0
  44. iatoolkit/services/auth_service.py +193 -0
  45. {services → iatoolkit/services}/benchmark_service.py +7 -7
  46. iatoolkit/services/branding_service.py +153 -0
  47. iatoolkit/services/company_context_service.py +214 -0
  48. iatoolkit/services/configuration_service.py +375 -0
  49. iatoolkit/services/dispatcher_service.py +134 -0
  50. {services → iatoolkit/services}/document_service.py +20 -8
  51. iatoolkit/services/embedding_service.py +148 -0
  52. iatoolkit/services/excel_service.py +156 -0
  53. {services → iatoolkit/services}/file_processor_service.py +36 -21
  54. iatoolkit/services/history_manager_service.py +208 -0
  55. iatoolkit/services/i18n_service.py +104 -0
  56. iatoolkit/services/jwt_service.py +80 -0
  57. iatoolkit/services/language_service.py +89 -0
  58. iatoolkit/services/license_service.py +82 -0
  59. iatoolkit/services/llm_client_service.py +438 -0
  60. iatoolkit/services/load_documents_service.py +174 -0
  61. iatoolkit/services/mail_service.py +213 -0
  62. {services → iatoolkit/services}/profile_service.py +200 -101
  63. iatoolkit/services/prompt_service.py +303 -0
  64. iatoolkit/services/query_service.py +467 -0
  65. iatoolkit/services/search_service.py +55 -0
  66. iatoolkit/services/sql_service.py +169 -0
  67. iatoolkit/services/tool_service.py +246 -0
  68. iatoolkit/services/user_feedback_service.py +117 -0
  69. iatoolkit/services/user_session_context_service.py +213 -0
  70. iatoolkit/static/images/fernando.jpeg +0 -0
  71. iatoolkit/static/images/iatoolkit_core.png +0 -0
  72. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  73. iatoolkit/static/js/chat_feedback_button.js +80 -0
  74. iatoolkit/static/js/chat_filepond.js +85 -0
  75. iatoolkit/static/js/chat_help_content.js +124 -0
  76. iatoolkit/static/js/chat_history_button.js +110 -0
  77. iatoolkit/static/js/chat_logout_button.js +36 -0
  78. iatoolkit/static/js/chat_main.js +401 -0
  79. iatoolkit/static/js/chat_model_selector.js +227 -0
  80. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  81. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  82. iatoolkit/static/js/chat_reload_button.js +38 -0
  83. iatoolkit/static/styles/chat_iatoolkit.css +559 -0
  84. iatoolkit/static/styles/chat_modal.css +133 -0
  85. iatoolkit/static/styles/chat_public.css +135 -0
  86. iatoolkit/static/styles/documents.css +598 -0
  87. iatoolkit/static/styles/landing_page.css +398 -0
  88. iatoolkit/static/styles/llm_output.css +148 -0
  89. iatoolkit/static/styles/onboarding.css +176 -0
  90. iatoolkit/system_prompts/__init__.py +0 -0
  91. iatoolkit/system_prompts/query_main.prompt +30 -23
  92. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  93. iatoolkit/templates/_company_header.html +45 -0
  94. iatoolkit/templates/_login_widget.html +42 -0
  95. iatoolkit/templates/base.html +78 -0
  96. iatoolkit/templates/change_password.html +66 -0
  97. iatoolkit/templates/chat.html +337 -0
  98. iatoolkit/templates/chat_modals.html +185 -0
  99. iatoolkit/templates/error.html +51 -0
  100. iatoolkit/templates/forgot_password.html +51 -0
  101. iatoolkit/templates/onboarding_shell.html +106 -0
  102. iatoolkit/templates/signup.html +79 -0
  103. iatoolkit/views/__init__.py +5 -0
  104. iatoolkit/views/base_login_view.py +96 -0
  105. iatoolkit/views/change_password_view.py +116 -0
  106. iatoolkit/views/chat_view.py +76 -0
  107. iatoolkit/views/embedding_api_view.py +65 -0
  108. iatoolkit/views/forgot_password_view.py +75 -0
  109. iatoolkit/views/help_content_api_view.py +54 -0
  110. iatoolkit/views/history_api_view.py +56 -0
  111. iatoolkit/views/home_view.py +63 -0
  112. iatoolkit/views/init_context_api_view.py +74 -0
  113. iatoolkit/views/llmquery_api_view.py +59 -0
  114. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  115. iatoolkit/views/load_document_api_view.py +65 -0
  116. iatoolkit/views/login_view.py +170 -0
  117. iatoolkit/views/logout_api_view.py +57 -0
  118. iatoolkit/views/profile_api_view.py +46 -0
  119. iatoolkit/views/prompt_api_view.py +37 -0
  120. iatoolkit/views/root_redirect_view.py +22 -0
  121. iatoolkit/views/signup_view.py +100 -0
  122. iatoolkit/views/static_page_view.py +27 -0
  123. iatoolkit/views/user_feedback_api_view.py +60 -0
  124. iatoolkit/views/users_api_view.py +33 -0
  125. iatoolkit/views/verify_user_view.py +60 -0
  126. iatoolkit-0.107.4.dist-info/METADATA +268 -0
  127. iatoolkit-0.107.4.dist-info/RECORD +132 -0
  128. iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
  129. iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  130. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
  131. iatoolkit/iatoolkit.py +0 -413
  132. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  133. iatoolkit-0.3.9.dist-info/METADATA +0 -252
  134. iatoolkit-0.3.9.dist-info/RECORD +0 -32
  135. services/__init__.py +0 -5
  136. services/api_service.py +0 -75
  137. services/dispatcher_service.py +0 -351
  138. services/excel_service.py +0 -98
  139. services/history_service.py +0 -45
  140. services/jwt_service.py +0 -91
  141. services/load_documents_service.py +0 -212
  142. services/mail_service.py +0 -62
  143. services/prompt_manager_service.py +0 -172
  144. services/query_service.py +0 -334
  145. services/search_service.py +0 -32
  146. services/sql_service.py +0 -42
  147. services/tasks_service.py +0 -188
  148. services/user_feedback_service.py +0 -67
  149. services/user_session_context_service.py +0 -85
  150. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,246 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from injector import inject
7
+ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
+ from iatoolkit.repositories.profile_repo import ProfileRepo
9
+ from iatoolkit.repositories.models import Company, Tool
10
+ from iatoolkit.common.exceptions import IAToolkitException
11
+ from iatoolkit.services.sql_service import SqlService
12
+ from iatoolkit.services.excel_service import ExcelService
13
+ from iatoolkit.services.mail_service import MailService
14
+
15
+
16
+ _SYSTEM_TOOLS = [
17
+ {
18
+ "function_name": "iat_generate_excel",
19
+ "description": "Generador de Excel."
20
+ "Genera un archivo Excel (.xlsx) a partir de una lista de diccionarios. "
21
+ "Cada diccionario representa una fila del archivo. "
22
+ "el archivo se guarda en directorio de descargas."
23
+ "retorna diccionario con filename, attachment_token (para enviar archivo por mail)"
24
+ "content_type y download_link",
25
+ "parameters": {
26
+ "type": "object",
27
+ "properties": {
28
+ "filename": {
29
+ "type": "string",
30
+ "description": "Nombre del archivo de salida (ejemplo: 'reporte.xlsx')",
31
+ "pattern": "^.+\\.xlsx?$"
32
+ },
33
+ "sheet_name": {
34
+ "type": "string",
35
+ "description": "Nombre de la hoja dentro del Excel",
36
+ "minLength": 1
37
+ },
38
+ "data": {
39
+ "type": "array",
40
+ "description": "Lista de diccionarios. Cada diccionario representa una fila.",
41
+ "minItems": 1,
42
+ "items": {
43
+ "type": "object",
44
+ "properties": {},
45
+ "additionalProperties": {
46
+ "anyOf": [
47
+ {"type": "string"},
48
+ {"type": "number"},
49
+ {"type": "boolean"},
50
+ {"type": "null"},
51
+ {
52
+ "type": "string",
53
+ "format": "date"
54
+ }
55
+ ]
56
+ }
57
+ }
58
+ }
59
+ },
60
+ "required": ["filename", "sheet_name", "data"]
61
+ }
62
+ },
63
+ {
64
+ 'function_name': "iat_send_email",
65
+ 'description': "iatoolkit mail system. "
66
+ "envia mails cuando un usuario lo solicita.",
67
+ 'parameters': {
68
+ "type": "object",
69
+ "properties": {
70
+ "recipient": {"type": "string", "description": "email del destinatario"},
71
+ "subject": {"type": "string", "description": "asunto del email"},
72
+ "body": {"type": "string", "description": "HTML del email"},
73
+ "attachments": {
74
+ "type": "array",
75
+ "description": "Lista de archivos adjuntos codificados en base64",
76
+ "items": {
77
+ "type": "object",
78
+ "properties": {
79
+ "filename": {
80
+ "type": "string",
81
+ "description": "Nombre del archivo con su extensión (ej. informe.pdf)"
82
+ },
83
+ "content": {
84
+ "type": "string",
85
+ "description": "Contenido del archivo en b64."
86
+ },
87
+ "attachment_token": {
88
+ "type": "string",
89
+ "description": "token para descargar el archivo."
90
+ }
91
+ },
92
+ "required": ["filename", "content", "attachment_token"],
93
+ "additionalProperties": False
94
+ }
95
+ }
96
+ },
97
+ "required": ["recipient", "subject", "body", "attachments"]
98
+ }
99
+ },
100
+ {
101
+ "function_name": "iat_sql_query",
102
+ "description": "Servicio SQL de IAToolkit: debes utilizar este servicio para todas las consultas SQL a bases de datos.",
103
+ "parameters": {
104
+ "type": "object",
105
+ "properties": {
106
+ "database_key": {
107
+ "type": "string",
108
+ "description": "IMPORTANT: nombre de la base de datos a consultar."
109
+ },
110
+ "query": {
111
+ "type": "string",
112
+ "description": "string con la consulta en sql"
113
+ },
114
+ },
115
+ "required": ["database_key", "query"]
116
+ }
117
+ }
118
+ ]
119
+
120
+
121
+ class ToolService:
122
+ @inject
123
+ def __init__(self,
124
+ llm_query_repo: LLMQueryRepo,
125
+ profile_repo: ProfileRepo,
126
+ sql_service: SqlService,
127
+ excel_service: ExcelService,
128
+ mail_service: MailService):
129
+ self.llm_query_repo = llm_query_repo
130
+ self.profile_repo = profile_repo
131
+ self.sql_service = sql_service
132
+ self.excel_service = excel_service
133
+ self.mail_service = mail_service
134
+
135
+ # execution mapper for system tools
136
+ self.system_handlers = {
137
+ "iat_generate_excel": self.excel_service.excel_generator,
138
+ "iat_send_email": self.mail_service.send_mail,
139
+ "iat_sql_query": self.sql_service.exec_sql
140
+ }
141
+
142
+ def register_system_tools(self):
143
+ """Creates or updates system functions in the database."""
144
+ try:
145
+ # delete all system tools
146
+ self.llm_query_repo.delete_system_tools()
147
+
148
+ # create new system tools
149
+ for function in _SYSTEM_TOOLS:
150
+ new_tool = Tool(
151
+ company_id=None,
152
+ system_function=True,
153
+ name=function['function_name'],
154
+ description=function['description'],
155
+ parameters=function['parameters']
156
+ )
157
+ self.llm_query_repo.create_or_update_tool(new_tool)
158
+
159
+ self.llm_query_repo.commit()
160
+ except Exception as e:
161
+ self.llm_query_repo.rollback()
162
+ raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR, str(e))
163
+
164
+ def sync_company_tools(self, company_short_name: str, tools_config: list):
165
+ """
166
+ Synchronizes tools from YAML config to Database (Create/Update/Delete strategy).
167
+ """
168
+ if not tools_config:
169
+ return
170
+
171
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
172
+ if not company:
173
+ raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
174
+ f'Company {company_short_name} not found')
175
+
176
+ try:
177
+ # 1. Get existing tools map for later cleanup
178
+ existing_tools = {
179
+ f.name: f for f in self.llm_query_repo.get_company_tools(company)
180
+ }
181
+ defined_tool_names = set()
182
+
183
+ # 2. Sync (Create or Update) from Config
184
+ for tool_data in tools_config:
185
+ name = tool_data['function_name']
186
+ defined_tool_names.add(name)
187
+
188
+ # Construct the tool object with current config values
189
+ # We create a new transient object and let the repo merge it
190
+ tool_obj = Tool(
191
+ company_id=company.id,
192
+ name=name,
193
+ description=tool_data['description'],
194
+ parameters=tool_data['params'],
195
+ system_function=False
196
+ )
197
+
198
+ # Always call create_or_update. The repo handles checking for existence by name.
199
+ self.llm_query_repo.create_or_update_tool(tool_obj)
200
+
201
+ # 3. Cleanup: Delete tools present in DB but not in Config
202
+ for name, tool in existing_tools.items():
203
+ # Ensure we don't delete system functions or active tools accidentally if logic changes,
204
+ # though get_company_tools filters by company_id so system functions shouldn't be here usually.
205
+ if not tool.system_function and (name not in defined_tool_names):
206
+ self.llm_query_repo.delete_tool(tool)
207
+
208
+ self.llm_query_repo.commit()
209
+
210
+ except Exception as e:
211
+ self.llm_query_repo.rollback()
212
+ raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR, str(e))
213
+
214
+
215
+ def get_tools_for_llm(self, company: Company) -> list[dict]:
216
+ """
217
+ Returns the list of tools (System + Company) formatted for the LLM (OpenAI Schema).
218
+ """
219
+ tools = []
220
+
221
+ # get all the tools for the company and system
222
+ company_tools = self.llm_query_repo.get_company_tools(company)
223
+
224
+ for function in company_tools:
225
+ # clone for no modify the SQLAlchemy session object
226
+ params = function.parameters.copy() if function.parameters else {}
227
+ params["additionalProperties"] = False
228
+
229
+ ai_tool = {
230
+ "type": "function",
231
+ "name": function.name,
232
+ "description": function.description,
233
+ "parameters": params,
234
+ "strict": True
235
+ }
236
+ if function.name == 'iat_sql_query':
237
+ params['properties']['database_key']['enum'] = self.sql_service.get_db_names(company.short_name)
238
+
239
+ tools.append(ai_tool)
240
+ return tools
241
+
242
+ def get_system_handler(self, function_name: str):
243
+ return self.system_handlers.get(function_name)
244
+
245
+ def is_system_tool(self, function_name: str) -> bool:
246
+ return function_name in self.system_handlers
@@ -0,0 +1,117 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.repositories.models import UserFeedback, Company
7
+ from injector import inject
8
+ from iatoolkit.repositories.profile_repo import ProfileRepo
9
+ from iatoolkit.services.i18n_service import I18nService
10
+ from iatoolkit.infra.google_chat_app import GoogleChatApp
11
+ from iatoolkit.services.mail_service import MailService
12
+ import logging
13
+
14
+
15
+ class UserFeedbackService:
16
+ @inject
17
+ def __init__(self,
18
+ profile_repo: ProfileRepo,
19
+ i18n_service: I18nService,
20
+ google_chat_app: GoogleChatApp,
21
+ mail_service: MailService):
22
+ self.profile_repo = profile_repo
23
+ self.i18n_service = i18n_service
24
+ self.google_chat_app = google_chat_app
25
+ self.mail_service = mail_service
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,
42
+ company_short_name: str,
43
+ destination_email: str,
44
+ company_name: str,
45
+ message_text: str):
46
+ """Envía una notificación de feedback por correo electrónico."""
47
+ try:
48
+ subject = f"Nuevo Feedback de {company_name}"
49
+ # Convertir el texto plano a un HTML simple para mantener los saltos de línea
50
+ html_body = message_text.replace('\n', '<br>')
51
+ self.mail_service.send_mail(
52
+ company_short_name=company_short_name,
53
+ to=destination_email,
54
+ subject=subject,
55
+ body=html_body)
56
+ except Exception as e:
57
+ logging.exception(f"error sending email de feedback: {e}")
58
+
59
+ def _handle_notification(self, company: Company, message_text: str):
60
+ """Lee la configuración de la empresa y envía la notificación al canal correspondiente."""
61
+ feedback_params = company.parameters.get('user_feedback')
62
+ if not isinstance(feedback_params, dict):
63
+ logging.warning(f"missing 'user_feedback' configuration for company: {company.short_name}.")
64
+ return
65
+
66
+ # get channel and destination
67
+ channel = feedback_params.get('channel')
68
+ destination = feedback_params.get('destination')
69
+ if not channel or not destination:
70
+ logging.warning(f"invalid 'user_feedback' configuration for: {company.short_name}. Faltan 'channel' o 'destination'.")
71
+ return
72
+
73
+ if channel == 'google_chat':
74
+ self._send_google_chat_notification(space_name=destination, message_text=message_text)
75
+ elif channel == 'email':
76
+ self._send_email_notification(
77
+ company_short_name=company.short_name,
78
+ destination_email=destination,
79
+ company_name=company.short_name,
80
+ message_text=message_text)
81
+ else:
82
+ logging.warning(f"unknown feedback channel: '{channel}' for company {company.short_name}.")
83
+
84
+ def new_feedback(self,
85
+ company_short_name: str,
86
+ message: str,
87
+ user_identifier: str,
88
+ rating: int = None) -> dict:
89
+ try:
90
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
91
+ if not company:
92
+ return {'error': self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
93
+
94
+ # 2. send notification using company configuration
95
+ notification_text = (f"*Nuevo feedback de {company_short_name}*:\n"
96
+ f"*Usuario:* {user_identifier}\n"
97
+ f"*Mensaje:* {message}\n"
98
+ f"*Calificación:* {rating if rating is not None else 'N/A'}")
99
+ self._handle_notification(company, notification_text)
100
+
101
+ # 3. always save the feedback in the database
102
+ new_feedback_obj = UserFeedback(
103
+ company_id=company.id,
104
+ message=message,
105
+ user_identifier=user_identifier,
106
+ rating=rating
107
+ )
108
+ saved_feedback = self.profile_repo.save_feedback(new_feedback_obj)
109
+ if not saved_feedback:
110
+ logging.error(f"can't save feedback for user {user_identifier}/{company_short_name}")
111
+ return {'error': 'can not save the feedback'}
112
+
113
+ return {'success': True, 'message': 'Feedback guardado correctamente'}
114
+
115
+ except Exception as e:
116
+ logging.exception(f"Error crítico en el servicio de feedback: {e}")
117
+ return {'error': str(e)}
@@ -0,0 +1,213 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from iatoolkit.infra.redis_session_manager import RedisSessionManager
7
+ from typing import List, Dict, Optional
8
+ import json
9
+ import logging
10
+
11
+
12
+ class UserSessionContextService:
13
+ """
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.
16
+ """
17
+
18
+ def _get_session_key(self, company_short_name: str, user_identifier: str, model: str = None) -> Optional[str]:
19
+ """Devuelve la clave única de Redis para el Hash de sesión del usuario."""
20
+ user_identifier = (user_identifier or "").strip()
21
+ if not company_short_name or not user_identifier:
22
+ return None
23
+
24
+ model_key = "" if not model else f"-{model}"
25
+ return f"session:{company_short_name}/{user_identifier}{model_key}"
26
+
27
+ def clear_all_context(self, company_short_name: str, user_identifier: str, model: str = None):
28
+ """Clears LLM-related context for a user (history and response IDs), preserving profile_data."""
29
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
30
+ if session_key:
31
+ # 'profile_data' should not be deleted
32
+ RedisSessionManager.hdel(session_key, "context_version")
33
+ RedisSessionManager.hdel(session_key, "context_history")
34
+ RedisSessionManager.hdel(session_key, "last_response_id")
35
+
36
+ def clear_llm_history(self, company_short_name: str, user_identifier: str, model: str = None):
37
+ """Clears only LLM history fields (last_response_id and context_history)."""
38
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
39
+ if session_key:
40
+ RedisSessionManager.hdel(session_key, "last_response_id", "context_history")
41
+
42
+ def get_last_response_id(self, company_short_name: str, user_identifier: str, model: str = None) -> Optional[str]:
43
+ """Returns the last LLM response ID for this user/model combination."""
44
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
45
+ if not session_key:
46
+ return None
47
+ return RedisSessionManager.hget(session_key, "last_response_id")
48
+
49
+ def save_last_response_id(self,
50
+ company_short_name: str,
51
+ user_identifier: str,
52
+ response_id: str,
53
+ model: str = None,
54
+ ):
55
+ """Persists the last LLM response ID for this user/model combination."""
56
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
57
+ if session_key:
58
+ RedisSessionManager.hset(session_key, "last_response_id", response_id)
59
+
60
+ def get_initial_response_id(self,
61
+ company_short_name: str,
62
+ user_identifier: str,
63
+ model: str = None,
64
+ ) -> Optional[str]:
65
+ """
66
+ Returns the initial LLM response ID for this user/model combination.
67
+ This ID represents the state right after the context was set on the LLM.
68
+ """
69
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
70
+ if not session_key:
71
+ return None
72
+ return RedisSessionManager.hget(session_key, "initial_response_id")
73
+
74
+ def save_initial_response_id(self,
75
+ company_short_name: str,
76
+ user_identifier: str,
77
+ response_id: str,
78
+ model: str = None,
79
+ ):
80
+ """Persists the initial LLM response ID for this user/model combination."""
81
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
82
+ if session_key:
83
+ RedisSessionManager.hset(session_key, "initial_response_id", response_id)
84
+
85
+ def save_context_history(
86
+ self,
87
+ company_short_name: str,
88
+ user_identifier: str,
89
+ context_history: List[Dict],
90
+ model: str = None,
91
+ ):
92
+ """Serializes and stores the context history for this user/model combination."""
93
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
94
+ if session_key:
95
+ try:
96
+ history_json = json.dumps(context_history)
97
+ RedisSessionManager.hset(session_key, "context_history", history_json)
98
+ except (TypeError, ValueError) as e:
99
+ logging.error(f"Error serializing context_history for {session_key}: {e}")
100
+
101
+ def get_context_history(
102
+ self,
103
+ company_short_name: str,
104
+ user_identifier: str,
105
+ model: str = None,
106
+ ) -> Optional[List[Dict]]:
107
+ """Reads and deserializes the context history for this user/model combination."""
108
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
109
+ if not session_key:
110
+ return []
111
+
112
+ history_json = RedisSessionManager.hget(session_key, "context_history")
113
+ if not history_json:
114
+ return []
115
+
116
+ try:
117
+ return json.loads(history_json)
118
+ except json.JSONDecodeError:
119
+ return []
120
+
121
+ def save_profile_data(self, company_short_name: str, user_identifier: str, data: dict):
122
+ session_key = self._get_session_key(company_short_name, user_identifier)
123
+ if session_key:
124
+ try:
125
+ data_json = json.dumps(data)
126
+ RedisSessionManager.hset(session_key, 'profile_data', data_json)
127
+ except (TypeError, ValueError) as e:
128
+ logging.error(f"Error al serializar profile_data para {session_key}: {e}")
129
+
130
+ def get_profile_data(self, company_short_name: str, user_identifier: str) -> dict:
131
+ session_key = self._get_session_key(company_short_name, user_identifier)
132
+ if not session_key:
133
+ return {}
134
+
135
+ data_json = RedisSessionManager.hget(session_key, 'profile_data')
136
+ if not data_json:
137
+ return {}
138
+
139
+ try:
140
+ return json.loads(data_json)
141
+ except json.JSONDecodeError:
142
+ return {}
143
+
144
+ def save_context_version(self,
145
+ company_short_name: str,
146
+ user_identifier: str,
147
+ version: str,
148
+ model: str = None,
149
+ ):
150
+ """Saves the context version for this user/model combination."""
151
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
152
+ if session_key:
153
+ RedisSessionManager.hset(session_key, "context_version", version)
154
+
155
+ def get_context_version(self,
156
+ company_short_name: str,
157
+ user_identifier: str,
158
+ model: str = None,
159
+ ) -> Optional[str]:
160
+ """Returns the context version for this user/model combination."""
161
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
162
+ if not session_key:
163
+ return None
164
+ return RedisSessionManager.hget(session_key, "context_version")
165
+
166
+ def save_prepared_context(self,
167
+ company_short_name: str,
168
+ user_identifier: str,
169
+ context: str,
170
+ version: str,
171
+ model: str = None,
172
+ ):
173
+ """Stores a pre-rendered system context and its version, ready to be sent to the LLM."""
174
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
175
+ if session_key:
176
+ RedisSessionManager.hset(session_key, "prepared_context", context)
177
+ RedisSessionManager.hset(session_key, "prepared_context_version", version)
178
+
179
+ def get_and_clear_prepared_context(self,
180
+ company_short_name: str,
181
+ user_identifier: str,
182
+ model: str = None,
183
+ ) -> tuple:
184
+ """
185
+ Atomically retrieves the prepared context and its version and then deletes them
186
+ to guarantee they are consumed only once.
187
+ """
188
+ session_key = self._get_session_key(company_short_name, user_identifier, model=model)
189
+ if not session_key:
190
+ return None, None
191
+
192
+ pipe = RedisSessionManager.pipeline()
193
+ pipe.hget(session_key, "prepared_context")
194
+ pipe.hget(session_key, "prepared_context_version")
195
+ pipe.hdel(session_key, "prepared_context", "prepared_context_version")
196
+ results = pipe.execute()
197
+
198
+ # results[0] is the context, results[1] is the version
199
+ return (results[0], results[1]) if results else (None, None)
200
+
201
+ # --- Métodos de Bloqueo ---
202
+ def acquire_lock(self, lock_key: str, expire_seconds: int) -> bool:
203
+ """Intenta adquirir un lock. Devuelve True si se adquiere, False si no."""
204
+ # SET con NX (solo si no existe) y EX (expiración) es una operación atómica.
205
+ return RedisSessionManager.set(lock_key, "1", ex=expire_seconds, nx=True)
206
+
207
+ def release_lock(self, lock_key: str):
208
+ """Libera un lock."""
209
+ RedisSessionManager.remove(lock_key)
210
+
211
+ def is_locked(self, lock_key: str) -> bool:
212
+ """Verifica si un lock existe."""
213
+ 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
+ // Tool 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
+ });