iatoolkit 0.63.1__py3-none-any.whl → 0.67.0__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 +2 -0
- iatoolkit/base_company.py +1 -20
- iatoolkit/common/routes.py +11 -2
- iatoolkit/common/session_manager.py +2 -0
- iatoolkit/common/util.py +17 -0
- iatoolkit/company_registry.py +1 -2
- iatoolkit/iatoolkit.py +41 -5
- iatoolkit/locales/en.yaml +167 -0
- iatoolkit/locales/es.yaml +163 -0
- iatoolkit/repositories/database_manager.py +3 -3
- iatoolkit/repositories/document_repo.py +1 -1
- iatoolkit/repositories/models.py +2 -3
- iatoolkit/repositories/profile_repo.py +0 -4
- iatoolkit/services/auth_service.py +14 -9
- iatoolkit/services/branding_service.py +32 -22
- iatoolkit/services/configuration_service.py +140 -0
- iatoolkit/services/dispatcher_service.py +20 -18
- iatoolkit/services/document_service.py +5 -2
- iatoolkit/services/excel_service.py +15 -11
- iatoolkit/services/file_processor_service.py +4 -12
- iatoolkit/services/history_service.py +8 -7
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +7 -9
- iatoolkit/services/language_service.py +79 -0
- iatoolkit/services/load_documents_service.py +4 -4
- iatoolkit/services/mail_service.py +9 -4
- iatoolkit/services/onboarding_service.py +10 -4
- iatoolkit/services/profile_service.py +58 -38
- iatoolkit/services/prompt_manager_service.py +20 -16
- iatoolkit/services/query_service.py +15 -14
- iatoolkit/services/sql_service.py +6 -2
- iatoolkit/services/user_feedback_service.py +16 -14
- iatoolkit/static/js/chat_feedback_button.js +57 -87
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +48 -65
- iatoolkit/static/js/chat_main.js +27 -24
- iatoolkit/static/js/chat_reload_button.js +28 -45
- iatoolkit/static/styles/chat_iatoolkit.css +223 -315
- iatoolkit/static/styles/chat_modal.css +63 -97
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +0 -1
- iatoolkit/templates/_company_header.html +6 -2
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +34 -19
- iatoolkit/templates/change_password.html +22 -20
- iatoolkit/templates/chat.html +58 -27
- iatoolkit/templates/chat_modals.html +113 -74
- iatoolkit/templates/error.html +12 -13
- iatoolkit/templates/forgot_password.html +11 -7
- iatoolkit/templates/index.html +8 -3
- iatoolkit/templates/login_simulation.html +16 -5
- iatoolkit/templates/onboarding_shell.html +0 -1
- iatoolkit/templates/signup.html +14 -14
- iatoolkit/views/base_login_view.py +12 -1
- iatoolkit/views/change_password_view.py +49 -33
- iatoolkit/views/forgot_password_view.py +20 -19
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +13 -9
- iatoolkit/views/home_view.py +30 -38
- iatoolkit/views/init_context_api_view.py +16 -11
- iatoolkit/views/llmquery_api_view.py +38 -26
- iatoolkit/views/login_simulation_view.py +14 -2
- iatoolkit/views/login_view.py +47 -35
- iatoolkit/views/logout_api_view.py +26 -22
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +6 -6
- iatoolkit/views/signup_view.py +26 -24
- iatoolkit/views/user_feedback_api_view.py +19 -18
- iatoolkit/views/verify_user_view.py +30 -29
- {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/METADATA +40 -22
- iatoolkit-0.67.0.dist-info/RECORD +120 -0
- iatoolkit-0.67.0.dist-info/licenses/LICENSE +21 -0
- iatoolkit/static/styles/chat_info.css +0 -53
- iatoolkit/templates/header.html +0 -31
- iatoolkit/templates/test.html +0 -9
- iatoolkit-0.63.1.dist-info/RECORD +0 -112
- {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/WHEEL +0 -0
- {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/top_level.txt +0 -0
|
@@ -5,21 +5,25 @@
|
|
|
5
5
|
|
|
6
6
|
from injector import inject
|
|
7
7
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
8
|
-
|
|
9
|
-
import logging
|
|
8
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
10
9
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
11
10
|
from collections import defaultdict
|
|
12
11
|
from iatoolkit.repositories.models import Prompt, PromptCategory, Company
|
|
13
12
|
import os
|
|
14
13
|
from iatoolkit.common.exceptions import IAToolkitException
|
|
15
14
|
import importlib.resources
|
|
15
|
+
import logging
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class PromptService:
|
|
19
19
|
@inject
|
|
20
|
-
def __init__(self,
|
|
20
|
+
def __init__(self,
|
|
21
|
+
llm_query_repo: LLMQueryRepo,
|
|
22
|
+
profile_repo: ProfileRepo,
|
|
23
|
+
i18n_service: I18nService):
|
|
21
24
|
self.llm_query_repo = llm_query_repo
|
|
22
25
|
self.profile_repo = profile_repo
|
|
26
|
+
self.i18n_service = i18n_service
|
|
23
27
|
|
|
24
28
|
def create_prompt(self,
|
|
25
29
|
prompt_name: str,
|
|
@@ -36,20 +40,20 @@ class PromptService:
|
|
|
36
40
|
if is_system_prompt:
|
|
37
41
|
if not importlib.resources.files('iatoolkit.system_prompts').joinpath(prompt_filename).is_file():
|
|
38
42
|
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
|
|
39
|
-
f'
|
|
43
|
+
f'missing system prompt file: {prompt_filename}')
|
|
40
44
|
else:
|
|
41
45
|
template_dir = f'companies/{company.short_name}/prompts'
|
|
42
46
|
|
|
43
47
|
relative_prompt_path = os.path.join(template_dir, prompt_filename)
|
|
44
48
|
if not os.path.exists(relative_prompt_path):
|
|
45
49
|
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
|
|
46
|
-
f'
|
|
50
|
+
f'missing prompt file: {relative_prompt_path}')
|
|
47
51
|
|
|
48
52
|
if custom_fields:
|
|
49
53
|
for f in custom_fields:
|
|
50
54
|
if ('data_key' not in f) or ('label' not in f):
|
|
51
55
|
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_PARAMETER,
|
|
52
|
-
f'
|
|
56
|
+
f'The field "custom_fields" must contain the following keys: data_key y label')
|
|
53
57
|
|
|
54
58
|
# add default value for data_type
|
|
55
59
|
if 'type' not in f:
|
|
@@ -82,20 +86,20 @@ class PromptService:
|
|
|
82
86
|
user_prompt = self.llm_query_repo.get_prompt_by_name(company, prompt_name)
|
|
83
87
|
if not user_prompt:
|
|
84
88
|
raise IAToolkitException(IAToolkitException.ErrorType.DOCUMENT_NOT_FOUND,
|
|
85
|
-
f"
|
|
89
|
+
f"prompt not found '{prompt_name}' for company '{company.short_name}'")
|
|
86
90
|
|
|
87
91
|
prompt_file = f'companies/{company.short_name}/prompts/{user_prompt.filename}'
|
|
88
92
|
absolute_filepath = os.path.join(execution_dir, prompt_file)
|
|
89
93
|
if not os.path.exists(absolute_filepath):
|
|
90
94
|
raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
|
|
91
|
-
f"
|
|
95
|
+
f"prompt file '{prompt_name}' does not exist: {absolute_filepath}")
|
|
92
96
|
|
|
93
97
|
try:
|
|
94
98
|
with open(absolute_filepath, 'r', encoding='utf-8') as f:
|
|
95
99
|
user_prompt_content = f.read()
|
|
96
100
|
except Exception as e:
|
|
97
101
|
raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
|
|
98
|
-
f"
|
|
102
|
+
f"error while reading prompt: '{prompt_name}' in this pathname {absolute_filepath}: {e}")
|
|
99
103
|
|
|
100
104
|
return user_prompt_content
|
|
101
105
|
|
|
@@ -105,9 +109,9 @@ class PromptService:
|
|
|
105
109
|
raise
|
|
106
110
|
except Exception as e:
|
|
107
111
|
logging.exception(
|
|
108
|
-
f"
|
|
112
|
+
f"error loading prompt '{prompt_name}' content for '{company.short_name}': {e}")
|
|
109
113
|
raise IAToolkitException(IAToolkitException.ErrorType.PROMPT_ERROR,
|
|
110
|
-
f'
|
|
114
|
+
f'error loading prompt "{prompt_name}" content for company {company.short_name}: {str(e)}')
|
|
111
115
|
|
|
112
116
|
def get_system_prompt(self):
|
|
113
117
|
try:
|
|
@@ -121,10 +125,10 @@ class PromptService:
|
|
|
121
125
|
content = importlib.resources.read_text('iatoolkit.system_prompts', prompt.filename)
|
|
122
126
|
system_prompt_content.append(content)
|
|
123
127
|
except FileNotFoundError:
|
|
124
|
-
logging.warning(f"
|
|
128
|
+
logging.warning(f"Prompt file does not exist in the package: {prompt.filename}")
|
|
125
129
|
except Exception as e:
|
|
126
130
|
raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
|
|
127
|
-
f"
|
|
131
|
+
f"error reading system prompt '{prompt.filename}': {e}")
|
|
128
132
|
|
|
129
133
|
# join the system prompts into a single string
|
|
130
134
|
return "\n".join(system_prompt_content)
|
|
@@ -135,14 +139,14 @@ class PromptService:
|
|
|
135
139
|
logging.exception(
|
|
136
140
|
f"Error al obtener el contenido del prompt de sistema: {e}")
|
|
137
141
|
raise IAToolkitException(IAToolkitException.ErrorType.PROMPT_ERROR,
|
|
138
|
-
f'
|
|
142
|
+
f'error reading the system prompts": {str(e)}')
|
|
139
143
|
|
|
140
144
|
def get_user_prompts(self, company_short_name: str) -> dict:
|
|
141
145
|
try:
|
|
142
146
|
# validate company
|
|
143
147
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
144
148
|
if not company:
|
|
145
|
-
return {
|
|
149
|
+
return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
|
|
146
150
|
|
|
147
151
|
# get all the prompts
|
|
148
152
|
all_prompts = self.llm_query_repo.get_prompts(company)
|
|
@@ -183,6 +187,6 @@ class PromptService:
|
|
|
183
187
|
return {'message': categorized_prompts}
|
|
184
188
|
|
|
185
189
|
except Exception as e:
|
|
186
|
-
logging.error(f"
|
|
190
|
+
logging.error(f"error in get_prompts: {e}")
|
|
187
191
|
return {'error': str(e)}
|
|
188
192
|
|
|
@@ -8,6 +8,7 @@ from iatoolkit.services.profile_service import ProfileService
|
|
|
8
8
|
from iatoolkit.repositories.document_repo import DocumentRepo
|
|
9
9
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
10
10
|
from iatoolkit.services.document_service import DocumentService
|
|
11
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
11
12
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
12
13
|
from iatoolkit.repositories.models import Task
|
|
13
14
|
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
@@ -37,6 +38,7 @@ class QueryService:
|
|
|
37
38
|
llmquery_repo: LLMQueryRepo,
|
|
38
39
|
profile_repo: ProfileRepo,
|
|
39
40
|
prompt_service: PromptService,
|
|
41
|
+
i18n_service: I18nService,
|
|
40
42
|
util: Utility,
|
|
41
43
|
dispatcher: Dispatcher,
|
|
42
44
|
session_context: UserSessionContextService
|
|
@@ -47,6 +49,7 @@ class QueryService:
|
|
|
47
49
|
self.llmquery_repo = llmquery_repo
|
|
48
50
|
self.profile_repo = profile_repo
|
|
49
51
|
self.prompt_service = prompt_service
|
|
52
|
+
self.i18n_service = i18n_service
|
|
50
53
|
self.util = util
|
|
51
54
|
self.dispatcher = dispatcher
|
|
52
55
|
self.session_context = session_context
|
|
@@ -56,7 +59,7 @@ class QueryService:
|
|
|
56
59
|
self.model = os.getenv("LLM_MODEL", "")
|
|
57
60
|
if not self.model:
|
|
58
61
|
raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
|
|
59
|
-
"
|
|
62
|
+
"missing ENV variable 'LLM_MODEL' configuration.")
|
|
60
63
|
|
|
61
64
|
def _build_context_and_profile(self, company_short_name: str, user_identifier: str) -> tuple:
|
|
62
65
|
# this method read the user/company context from the database and renders the system prompt
|
|
@@ -127,7 +130,7 @@ class QueryService:
|
|
|
127
130
|
lock_key = f"lock:context:{company_short_name}/{user_identifier}"
|
|
128
131
|
if not self.session_context.acquire_lock(lock_key, expire_seconds=60):
|
|
129
132
|
logging.warning(
|
|
130
|
-
f"
|
|
133
|
+
f"try to rebuild context for user {user_identifier} while is still in process, ignored.")
|
|
131
134
|
return
|
|
132
135
|
|
|
133
136
|
try:
|
|
@@ -138,11 +141,9 @@ class QueryService:
|
|
|
138
141
|
prepared_context, version_to_save = self.session_context.get_and_clear_prepared_context(company_short_name,
|
|
139
142
|
user_identifier)
|
|
140
143
|
if not prepared_context:
|
|
141
|
-
logging.info(
|
|
142
|
-
f"No se requiere reconstrucción de contexto para {company_short_name}/{user_identifier}. Finalización rápida.")
|
|
143
144
|
return
|
|
144
145
|
|
|
145
|
-
logging.info(f"
|
|
146
|
+
logging.info(f"sending context to LLM for: {company_short_name}/{user_identifier}...")
|
|
146
147
|
|
|
147
148
|
# Limpiar solo el historial de chat y el ID de respuesta anterior
|
|
148
149
|
self.session_context.clear_llm_history(company_short_name, user_identifier)
|
|
@@ -161,9 +162,9 @@ class QueryService:
|
|
|
161
162
|
self.session_context.save_context_version(company_short_name, user_identifier, version_to_save)
|
|
162
163
|
|
|
163
164
|
logging.info(
|
|
164
|
-
f"
|
|
165
|
+
f"Context for: {company_short_name}/{user_identifier} settled in {int(time.time() - start_time)} sec.")
|
|
165
166
|
except Exception as e:
|
|
166
|
-
logging.exception(f"Error
|
|
167
|
+
logging.exception(f"Error in finalize_context_rebuild for {company_short_name}: {e}")
|
|
167
168
|
raise e
|
|
168
169
|
finally:
|
|
169
170
|
# --- Liberar el Bloqueo ---
|
|
@@ -181,11 +182,11 @@ class QueryService:
|
|
|
181
182
|
company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
|
|
182
183
|
if not company:
|
|
183
184
|
return {"error": True,
|
|
184
|
-
"error_message":
|
|
185
|
+
"error_message": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
|
|
185
186
|
|
|
186
187
|
if not prompt_name and not question:
|
|
187
188
|
return {"error": True,
|
|
188
|
-
"error_message":
|
|
189
|
+
"error_message": self.i18n_service.t('services.start_query')}
|
|
189
190
|
|
|
190
191
|
# get the previous response_id and context history
|
|
191
192
|
previous_response_id = None
|
|
@@ -196,7 +197,7 @@ class QueryService:
|
|
|
196
197
|
previous_response_id = self.session_context.get_last_response_id(company.short_name, user_identifier)
|
|
197
198
|
if not previous_response_id:
|
|
198
199
|
return {'error': True,
|
|
199
|
-
"error_message":
|
|
200
|
+
"error_message": self.i18n_service.t('errors.services.missing_response_id', company_short_name=company.short_name, user_identifier=user_identifier)
|
|
200
201
|
}
|
|
201
202
|
elif self.util.is_gemini_model(self.model):
|
|
202
203
|
# check the length of the context_history and remove old messages
|
|
@@ -294,7 +295,7 @@ class QueryService:
|
|
|
294
295
|
return len(history) >= 1
|
|
295
296
|
return False
|
|
296
297
|
except Exception as e:
|
|
297
|
-
logging.warning(f"
|
|
298
|
+
logging.warning(f"error verifying context cache: {e}")
|
|
298
299
|
return False
|
|
299
300
|
|
|
300
301
|
def load_files_for_context(self, files: list) -> str:
|
|
@@ -353,7 +354,7 @@ class QueryService:
|
|
|
353
354
|
try:
|
|
354
355
|
total_tokens = sum(self.llm_client.count_tokens(json.dumps(message)) for message in context_history)
|
|
355
356
|
except Exception as e:
|
|
356
|
-
logging.error(f"
|
|
357
|
+
logging.error(f"error counting tokens for history: {e}.")
|
|
357
358
|
return
|
|
358
359
|
|
|
359
360
|
# Si se excede el límite, eliminar mensajes antiguos (empezando por el segundo)
|
|
@@ -364,8 +365,8 @@ class QueryService:
|
|
|
364
365
|
removed_tokens = self.llm_client.count_tokens(json.dumps(removed_message))
|
|
365
366
|
total_tokens -= removed_tokens
|
|
366
367
|
logging.warning(
|
|
367
|
-
f"
|
|
368
|
-
f"
|
|
368
|
+
f"history tokens ({total_tokens + removed_tokens} tokens) exceed the limit of: {GEMINI_MAX_TOKENS_CONTEXT_HISTORY}. "
|
|
369
|
+
f"new context: {total_tokens} tokens."
|
|
369
370
|
)
|
|
370
371
|
except IndexError:
|
|
371
372
|
# Se produce si solo queda el mensaje del sistema, el bucle debería detenerse.
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
7
7
|
|
|
8
8
|
from iatoolkit.common.util import Utility
|
|
9
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
9
10
|
from sqlalchemy import text
|
|
10
11
|
from injector import inject
|
|
11
12
|
import json
|
|
@@ -14,8 +15,11 @@ from iatoolkit.common.exceptions import IAToolkitException
|
|
|
14
15
|
|
|
15
16
|
class SqlService:
|
|
16
17
|
@inject
|
|
17
|
-
def __init__(self,
|
|
18
|
+
def __init__(self,
|
|
19
|
+
util: Utility,
|
|
20
|
+
i18n_service: I18nService):
|
|
18
21
|
self.util = util
|
|
22
|
+
self.i18n_service = i18n_service
|
|
19
23
|
|
|
20
24
|
def exec_sql(self, db_manager: DatabaseManager, sql_statement: str) -> str:
|
|
21
25
|
"""
|
|
@@ -54,7 +58,7 @@ class SqlService:
|
|
|
54
58
|
|
|
55
59
|
error_message = str(e)
|
|
56
60
|
if 'timed out' in str(e):
|
|
57
|
-
error_message = '
|
|
61
|
+
error_message = self.i18n_service.t('errors.timeout')
|
|
58
62
|
|
|
59
63
|
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
60
64
|
error_message) from e
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
from iatoolkit.repositories.models import UserFeedback, Company
|
|
7
7
|
from injector import inject
|
|
8
8
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
9
10
|
from iatoolkit.infra.google_chat_app import GoogleChatApp
|
|
10
|
-
from iatoolkit.infra.mail_app import MailApp
|
|
11
|
+
from iatoolkit.infra.mail_app import MailApp
|
|
11
12
|
import logging
|
|
12
13
|
|
|
13
14
|
|
|
@@ -15,9 +16,11 @@ class UserFeedbackService:
|
|
|
15
16
|
@inject
|
|
16
17
|
def __init__(self,
|
|
17
18
|
profile_repo: ProfileRepo,
|
|
19
|
+
i18n_service: I18nService,
|
|
18
20
|
google_chat_app: GoogleChatApp,
|
|
19
21
|
mail_app: MailApp):
|
|
20
22
|
self.profile_repo = profile_repo
|
|
23
|
+
self.i18n_service = i18n_service
|
|
21
24
|
self.google_chat_app = google_chat_app
|
|
22
25
|
self.mail_app = mail_app
|
|
23
26
|
|
|
@@ -31,9 +34,9 @@ class UserFeedbackService:
|
|
|
31
34
|
}
|
|
32
35
|
chat_result = self.google_chat_app.send_message(message_data=chat_data)
|
|
33
36
|
if not chat_result.get('success'):
|
|
34
|
-
logging.warning(f"
|
|
37
|
+
logging.warning(f"error sending notification to Google Chat: {chat_result.get('message')}")
|
|
35
38
|
except Exception as e:
|
|
36
|
-
logging.exception(f"
|
|
39
|
+
logging.exception(f"error sending notification to Google Chat: {e}")
|
|
37
40
|
|
|
38
41
|
def _send_email_notification(self, destination_email: str, company_name: str, message_text: str):
|
|
39
42
|
"""Envía una notificación de feedback por correo electrónico."""
|
|
@@ -43,20 +46,20 @@ class UserFeedbackService:
|
|
|
43
46
|
html_body = message_text.replace('\n', '<br>')
|
|
44
47
|
self.mail_app.send_email(to=destination_email, subject=subject, body=html_body)
|
|
45
48
|
except Exception as e:
|
|
46
|
-
logging.exception(f"
|
|
49
|
+
logging.exception(f"error sending email de feedback: {e}")
|
|
47
50
|
|
|
48
51
|
def _handle_notification(self, company: Company, message_text: str):
|
|
49
52
|
"""Lee la configuración de la empresa y envía la notificación al canal correspondiente."""
|
|
50
53
|
feedback_params = company.parameters.get('user_feedback')
|
|
51
54
|
if not isinstance(feedback_params, dict):
|
|
52
|
-
logging.warning(f"
|
|
55
|
+
logging.warning(f"missing 'user_feedback' configuration for company: {company.short_name}.")
|
|
53
56
|
return
|
|
54
57
|
|
|
55
58
|
# get channel and destination
|
|
56
59
|
channel = feedback_params.get('channel')
|
|
57
60
|
destination = feedback_params.get('destination')
|
|
58
61
|
if not channel or not destination:
|
|
59
|
-
logging.warning(f"
|
|
62
|
+
logging.warning(f"invalid 'user_feedback' configuration for: {company.short_name}. Faltan 'channel' o 'destination'.")
|
|
60
63
|
return
|
|
61
64
|
|
|
62
65
|
if channel == 'google_chat':
|
|
@@ -64,7 +67,7 @@ class UserFeedbackService:
|
|
|
64
67
|
elif channel == 'email':
|
|
65
68
|
self._send_email_notification(destination_email=destination, company_name=company.short_name, message_text=message_text)
|
|
66
69
|
else:
|
|
67
|
-
logging.warning(f"
|
|
70
|
+
logging.warning(f"unknown feedback channel: '{channel}' for company {company.short_name}.")
|
|
68
71
|
|
|
69
72
|
def new_feedback(self,
|
|
70
73
|
company_short_name: str,
|
|
@@ -72,19 +75,18 @@ class UserFeedbackService:
|
|
|
72
75
|
user_identifier: str,
|
|
73
76
|
rating: int = None) -> dict:
|
|
74
77
|
try:
|
|
75
|
-
# 1. Validar empresa
|
|
76
78
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
77
79
|
if not company:
|
|
78
|
-
return {'error':
|
|
80
|
+
return {'error': self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
|
|
79
81
|
|
|
80
|
-
# 2.
|
|
82
|
+
# 2. send notification using company configuration
|
|
81
83
|
notification_text = (f"*Nuevo feedback de {company_short_name}*:\n"
|
|
82
84
|
f"*Usuario:* {user_identifier}\n"
|
|
83
85
|
f"*Mensaje:* {message}\n"
|
|
84
86
|
f"*Calificación:* {rating if rating is not None else 'N/A'}")
|
|
85
87
|
self._handle_notification(company, notification_text)
|
|
86
88
|
|
|
87
|
-
# 3.
|
|
89
|
+
# 3. always save the feedback in the database
|
|
88
90
|
new_feedback_obj = UserFeedback(
|
|
89
91
|
company_id=company.id,
|
|
90
92
|
message=message,
|
|
@@ -93,10 +95,10 @@ class UserFeedbackService:
|
|
|
93
95
|
)
|
|
94
96
|
saved_feedback = self.profile_repo.save_feedback(new_feedback_obj)
|
|
95
97
|
if not saved_feedback:
|
|
96
|
-
logging.error(f"
|
|
97
|
-
return {'error': '
|
|
98
|
+
logging.error(f"can't save feedback for user {user_identifier}/{company_short_name}")
|
|
99
|
+
return {'error': 'can not save the feedback'}
|
|
98
100
|
|
|
99
|
-
return {'message': 'Feedback guardado correctamente'}
|
|
101
|
+
return {'success': True, 'message': 'Feedback guardado correctamente'}
|
|
100
102
|
|
|
101
103
|
except Exception as e:
|
|
102
104
|
logging.exception(f"Error crítico en el servicio de feedback: {e}")
|
|
@@ -1,110 +1,80 @@
|
|
|
1
1
|
$(document).ready(function () {
|
|
2
|
+
const feedbackModal = $('#feedbackModal');
|
|
3
|
+
$('#submit-feedback').on('click', function () {
|
|
4
|
+
sendFeedback(this);
|
|
5
|
+
});
|
|
2
6
|
|
|
3
7
|
// Evento para enviar el feedback
|
|
4
|
-
|
|
8
|
+
async function sendFeedback(submitButton) {
|
|
9
|
+
toastr.options = {"positionClass": "toast-bottom-right", "preventDuplicates": true};
|
|
5
10
|
const feedbackText = $('#feedback-text').val().trim();
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
// --- LÓGICA DE COMPATIBILIDAD BS3 / BS5 ---
|
|
9
|
-
// Detecta si Bootstrap 5 está presente.
|
|
10
|
-
const isBootstrap5 = (typeof bootstrap !== 'undefined');
|
|
11
|
-
|
|
12
|
-
// Define el HTML del botón de cierre según la versión.
|
|
13
|
-
const closeButtonHtml = isBootstrap5 ?
|
|
14
|
-
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' : // Versión BS5
|
|
15
|
-
'<button type="button" class="close" data-dismiss="alert"><span>×</span></button>'; // Versión BS3/BS4
|
|
16
|
-
// --- FIN DE LA LÓGICA DE COMPATIBILIDAD ---
|
|
11
|
+
const activeStars = $('.star.active').length;
|
|
17
12
|
|
|
18
13
|
if (!feedbackText) {
|
|
19
|
-
|
|
20
|
-
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
|
21
|
-
<strong>¡Atención!</strong> Por favor, escribe tu comentario antes de enviar.
|
|
22
|
-
${closeButtonHtml}
|
|
23
|
-
</div>`;
|
|
24
|
-
$('.modal-body .alert').remove();
|
|
25
|
-
$('.modal-body').prepend(alertHtml);
|
|
14
|
+
toastr.error(t_js('feedback_comment_error'));
|
|
26
15
|
return;
|
|
27
16
|
}
|
|
28
17
|
|
|
29
|
-
const activeStars = $('.star.active').length;
|
|
30
18
|
if (activeStars === 0) {
|
|
31
|
-
|
|
32
|
-
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
|
33
|
-
<strong>¡Atención!</strong> Por favor, califica al asistente con las estrellas.
|
|
34
|
-
${closeButtonHtml}
|
|
35
|
-
</div>`;
|
|
36
|
-
$('.modal-body .alert').remove();
|
|
37
|
-
$('.modal-body').prepend(alertHtml);
|
|
19
|
+
toastr.error(t_js('feedback_rating_error'));
|
|
38
20
|
return;
|
|
39
21
|
}
|
|
40
22
|
|
|
41
|
-
submitButton.
|
|
42
|
-
submitButton.html('<i class="bi bi-send me-1 icon-spaced"></i>Enviando...');
|
|
23
|
+
submitButton.disabled = true;
|
|
43
24
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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'));
|
|
48
35
|
else
|
|
49
|
-
toastr.error('
|
|
50
|
-
});
|
|
36
|
+
toastr.error(t_js('feedback_sent_error'));
|
|
51
37
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
$('#submit-feedback').html('<i class="bi bi-send me-1 icon-spaced"></i>Enviar');
|
|
56
|
-
$('.star').removeClass('active hover-active'); // Resetea estrellas
|
|
57
|
-
$('#feedback-text').val(''); // Limpia texto
|
|
58
|
-
$('.modal-body .alert').remove(); // Quita alertas previas
|
|
59
|
-
$('#feedbackModal').modal('show');
|
|
60
|
-
});
|
|
38
|
+
submitButton.disabled = false;
|
|
39
|
+
feedbackModal.modal('hide');
|
|
40
|
+
}
|
|
61
41
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
42
|
+
// Evento para abrir el modal de feedback
|
|
43
|
+
$('#send-feedback-button').on('click', function () {
|
|
44
|
+
$('#submit-feedback').prop('disabled', false);
|
|
45
|
+
$('.star').removeClass('active hover-active'); // Resetea estrellas
|
|
46
|
+
$('#feedback-text').val('');
|
|
47
|
+
feedbackModal.modal('show');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Evento que se dispara DESPUÉS de que el modal se ha ocultado
|
|
51
|
+
$('#feedbackModal').on('hidden.bs.modal', function () {
|
|
52
|
+
$('#feedback-text').val('');
|
|
53
|
+
$('.star').removeClass('active');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Function for the star rating system
|
|
57
|
+
window.gfg = function (rating) {
|
|
58
|
+
$('.star').removeClass('active');
|
|
59
|
+
$('.star').each(function (index) {
|
|
60
|
+
if (index < rating) {
|
|
61
|
+
$(this).addClass('active');
|
|
62
|
+
}
|
|
67
63
|
});
|
|
64
|
+
};
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
$(
|
|
72
|
-
$('.star').
|
|
73
|
-
|
|
74
|
-
|
|
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');
|
|
75
73
|
}
|
|
76
74
|
});
|
|
77
|
-
}
|
|
75
|
+
},
|
|
76
|
+
function () {
|
|
77
|
+
$('.star').removeClass('hover-active');
|
|
78
|
+
});
|
|
78
79
|
|
|
79
|
-
$('.star').hover(
|
|
80
|
-
function() {
|
|
81
|
-
const rating = $(this).data('rating');
|
|
82
|
-
$('.star').removeClass('hover-active');
|
|
83
|
-
$('.star').each(function(index) {
|
|
84
|
-
if ($(this).data('rating') <= rating) {
|
|
85
|
-
$(this).addClass('hover-active');
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
},
|
|
89
|
-
function() {
|
|
90
|
-
$('.star').removeClass('hover-active');
|
|
91
|
-
}
|
|
92
|
-
);
|
|
93
80
|
});
|
|
94
|
-
|
|
95
|
-
const sendFeedback = async function(message) {
|
|
96
|
-
const activeStars = $('.star.active').length;
|
|
97
|
-
const data = {
|
|
98
|
-
"user_identifier": window.user_identifier,
|
|
99
|
-
"message": message,
|
|
100
|
-
"rating": activeStars,
|
|
101
|
-
};
|
|
102
|
-
try {
|
|
103
|
-
// Asumiendo que callLLMAPI está definido globalmente en otro archivo (ej. chat_main.js)
|
|
104
|
-
const responseData = await callToolkit('/api/feedback', data, "POST");
|
|
105
|
-
return responseData;
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.error("Error al enviar feedback:", error);
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|