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.
- iatoolkit/__init__.py +27 -35
- iatoolkit/base_company.py +3 -35
- iatoolkit/cli_commands.py +18 -47
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +48 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +39 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +138 -0
- iatoolkit/common/session_manager.py +26 -0
- iatoolkit/common/util.py +353 -0
- iatoolkit/company_registry.py +66 -29
- iatoolkit/core.py +514 -0
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/brevo_mail_app.py +123 -0
- iatoolkit/infra/call_service.py +140 -0
- iatoolkit/infra/connectors/__init__.py +5 -0
- iatoolkit/infra/connectors/file_connector.py +17 -0
- iatoolkit/infra/connectors/file_connector_factory.py +57 -0
- iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
- iatoolkit/infra/connectors/google_drive_connector.py +68 -0
- iatoolkit/infra/connectors/local_file_connector.py +46 -0
- iatoolkit/infra/connectors/s3_connector.py +33 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
- iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
- iatoolkit/infra/llm_proxy.py +268 -0
- iatoolkit/infra/llm_response.py +45 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +222 -0
- iatoolkit/locales/es.yaml +225 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +187 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +105 -0
- iatoolkit/repositories/models.py +279 -0
- iatoolkit/repositories/profile_repo.py +171 -0
- iatoolkit/repositories/vs_repo.py +150 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +7 -7
- iatoolkit/services/branding_service.py +153 -0
- iatoolkit/services/company_context_service.py +214 -0
- iatoolkit/services/configuration_service.py +375 -0
- iatoolkit/services/dispatcher_service.py +134 -0
- {services → iatoolkit/services}/document_service.py +20 -8
- iatoolkit/services/embedding_service.py +148 -0
- iatoolkit/services/excel_service.py +156 -0
- {services → iatoolkit/services}/file_processor_service.py +36 -21
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +80 -0
- iatoolkit/services/language_service.py +89 -0
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/services/llm_client_service.py +438 -0
- iatoolkit/services/load_documents_service.py +174 -0
- iatoolkit/services/mail_service.py +213 -0
- {services → iatoolkit/services}/profile_service.py +200 -101
- iatoolkit/services/prompt_service.py +303 -0
- iatoolkit/services/query_service.py +467 -0
- iatoolkit/services/search_service.py +55 -0
- iatoolkit/services/sql_service.py +169 -0
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +117 -0
- iatoolkit/services/user_session_context_service.py +213 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_filepond.js +85 -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 +401 -0
- iatoolkit/static/js/chat_model_selector.js +227 -0
- 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 +38 -0
- iatoolkit/static/styles/chat_iatoolkit.css +559 -0
- iatoolkit/static/styles/chat_modal.css +133 -0
- iatoolkit/static/styles/chat_public.css +135 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +398 -0
- iatoolkit/static/styles/llm_output.css +148 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +30 -23
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +45 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +78 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +337 -0
- iatoolkit/templates/chat_modals.html +185 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +51 -0
- iatoolkit/templates/onboarding_shell.html +106 -0
- iatoolkit/templates/signup.html +79 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +96 -0
- iatoolkit/views/change_password_view.py +116 -0
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +75 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +63 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +59 -0
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/load_document_api_view.py +65 -0
- iatoolkit/views/login_view.py +170 -0
- iatoolkit/views/logout_api_view.py +57 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +100 -0
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +60 -0
- iatoolkit-0.107.4.dist-info/METADATA +268 -0
- iatoolkit-0.107.4.dist-info/RECORD +132 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
- iatoolkit/iatoolkit.py +0 -413
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.3.9.dist-info/METADATA +0 -252
- iatoolkit-0.3.9.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/dispatcher_service.py +0 -351
- services/excel_service.py +0 -98
- services/history_service.py +0 -45
- services/jwt_service.py +0 -91
- services/load_documents_service.py +0 -212
- services/mail_service.py +0 -62
- services/prompt_manager_service.py +0 -172
- services/query_service.py +0 -334
- services/search_service.py +0 -32
- services/sql_service.py +0 -42
- services/tasks_service.py +0 -188
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {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
|
|
Binary file
|
|
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
|
+
});
|