iatoolkit 0.59.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 +2 -19
- iatoolkit/common/routes.py +28 -25
- iatoolkit/common/session_manager.py +2 -0
- iatoolkit/common/util.py +17 -6
- iatoolkit/company_registry.py +1 -2
- iatoolkit/iatoolkit.py +54 -20
- 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 +3 -4
- iatoolkit/repositories/profile_repo.py +0 -4
- iatoolkit/services/auth_service.py +44 -32
- iatoolkit/services/branding_service.py +35 -27
- 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 +59 -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 +70 -29
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +110 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +32 -184
- iatoolkit/static/js/{chat_onboarding.js → chat_onboarding_button.js} +0 -1
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +35 -0
- iatoolkit/static/styles/chat_iatoolkit.css +251 -205
- iatoolkit/static/styles/chat_modal.css +63 -95
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +121 -167
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +10 -10
- iatoolkit/templates/base.html +36 -19
- iatoolkit/templates/change_password.html +24 -22
- iatoolkit/templates/chat.html +121 -93
- iatoolkit/templates/chat_modals.html +113 -74
- iatoolkit/templates/error.html +44 -8
- iatoolkit/templates/forgot_password.html +17 -15
- iatoolkit/templates/index.html +66 -81
- iatoolkit/templates/login_simulation.html +16 -5
- iatoolkit/templates/onboarding_shell.html +1 -2
- iatoolkit/templates/signup.html +22 -20
- iatoolkit/views/base_login_view.py +12 -1
- iatoolkit/views/change_password_view.py +50 -33
- iatoolkit/views/external_login_view.py +5 -11
- iatoolkit/views/file_store_api_view.py +7 -9
- iatoolkit/views/forgot_password_view.py +21 -19
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +16 -12
- iatoolkit/views/home_view.py +61 -0
- iatoolkit/views/index_view.py +5 -34
- iatoolkit/views/init_context_api_view.py +16 -13
- iatoolkit/views/llmquery_api_view.py +38 -28
- iatoolkit/views/login_simulation_view.py +14 -2
- iatoolkit/views/login_view.py +48 -33
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +8 -8
- iatoolkit/views/signup_view.py +27 -25
- iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/user_feedback_api_view.py +21 -32
- iatoolkit/views/verify_user_view.py +33 -26
- {iatoolkit-0.59.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/js/chat_context_reload.js +0 -54
- iatoolkit/static/js/chat_feedback.js +0 -115
- iatoolkit/static/js/chat_history.js +0 -127
- iatoolkit/static/styles/chat_info.css +0 -53
- iatoolkit/templates/_branding_styles.html +0 -53
- iatoolkit/templates/_navbar.html +0 -9
- iatoolkit/templates/header.html +0 -31
- iatoolkit/templates/test.html +0 -9
- iatoolkit/views/chat_token_request_view.py +0 -98
- iatoolkit/views/tasks_review_view.py +0 -83
- iatoolkit-0.59.1.dist-info/RECORD +0 -111
- {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/WHEEL +0 -0
- {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/top_level.txt +0 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
from injector import inject
|
|
7
7
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
8
|
-
from iatoolkit.services.
|
|
8
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
9
9
|
from iatoolkit.repositories.models import User, Company, ApiKey
|
|
10
10
|
from flask_bcrypt import check_password_hash
|
|
11
11
|
from iatoolkit.common.session_manager import SessionManager
|
|
@@ -16,16 +16,19 @@ import random
|
|
|
16
16
|
import re
|
|
17
17
|
import secrets
|
|
18
18
|
import string
|
|
19
|
+
import logging
|
|
19
20
|
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class ProfileService:
|
|
23
24
|
@inject
|
|
24
25
|
def __init__(self,
|
|
26
|
+
i18n_service: I18nService,
|
|
25
27
|
profile_repo: ProfileRepo,
|
|
26
28
|
session_context_service: UserSessionContextService,
|
|
27
29
|
dispatcher: Dispatcher,
|
|
28
30
|
mail_app: MailApp):
|
|
31
|
+
self.i18n_service = i18n_service
|
|
29
32
|
self.profile_repo = profile_repo
|
|
30
33
|
self.dispatcher = dispatcher
|
|
31
34
|
self.session_context = session_context_service
|
|
@@ -38,23 +41,23 @@ class ProfileService:
|
|
|
38
41
|
# check if user exists
|
|
39
42
|
user = self.profile_repo.get_user_by_email(email)
|
|
40
43
|
if not user:
|
|
41
|
-
return {'success': False,
|
|
44
|
+
return {'success': False, 'message': self.i18n_service.t('errors.auth.user_not_found')}
|
|
42
45
|
|
|
43
46
|
# check the encrypted password
|
|
44
47
|
if not check_password_hash(user.password, password):
|
|
45
|
-
return {'success': False,
|
|
48
|
+
return {'success': False, 'message': self.i18n_service.t('errors.auth.invalid_password')}
|
|
46
49
|
|
|
47
50
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
48
51
|
if not company:
|
|
49
|
-
return {'success': False, "message": "
|
|
52
|
+
return {'success': False, "message": "missing company"}
|
|
50
53
|
|
|
51
54
|
# check that user belongs to company
|
|
52
55
|
if company not in user.companies:
|
|
53
|
-
return {'success': False, "message":
|
|
56
|
+
return {'success': False, "message": self.i18n_service.t('errors.services.user_not_authorized')}
|
|
54
57
|
|
|
55
58
|
if not user.verified:
|
|
56
59
|
return {'success': False,
|
|
57
|
-
"message":
|
|
60
|
+
"message": self.i18n_service.t('errors.services.account_not_verified')}
|
|
58
61
|
|
|
59
62
|
# 1. Build the local user profile dictionary here.
|
|
60
63
|
# the user_profile variables are used on the LLM templates also (see in query_main.prompt)
|
|
@@ -71,8 +74,9 @@ class ProfileService:
|
|
|
71
74
|
|
|
72
75
|
# 3. create the web session
|
|
73
76
|
self.set_session_for_user(company.short_name, user_identifier)
|
|
74
|
-
return {'success': True, "user_identifier": user_identifier, "message": "Login
|
|
77
|
+
return {'success': True, "user_identifier": user_identifier, "message": "Login ok"}
|
|
75
78
|
except Exception as e:
|
|
79
|
+
logging.error(f"Error in login: {e}")
|
|
76
80
|
return {'success': False, "message": str(e)}
|
|
77
81
|
|
|
78
82
|
def create_external_user_profile_context(self, company: Company, user_identifier: str):
|
|
@@ -131,6 +135,25 @@ class ProfileService:
|
|
|
131
135
|
"profile": profile
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
def update_user_language(self, user_identifier: str, new_lang: str) -> dict:
|
|
139
|
+
"""
|
|
140
|
+
Business logic to update a user's preferred language.
|
|
141
|
+
It validates the language and then calls the generic update method.
|
|
142
|
+
"""
|
|
143
|
+
# 1. Validate that the language is supported by checking the loaded translations.
|
|
144
|
+
if new_lang not in self.i18n_service.translations:
|
|
145
|
+
return {'success': False, 'error_message': self.i18n_service.t('errors.general.unsupported_language')}
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
# 2. Call the generic update_user method, passing the specific field to update.
|
|
149
|
+
self.update_user(user_identifier, preferred_language=new_lang)
|
|
150
|
+
return {'success': True, 'message': 'Language updated successfully.'}
|
|
151
|
+
except Exception as e:
|
|
152
|
+
# Log the error and return a generic failure message.
|
|
153
|
+
logging.error(f"Failed to update language for {user_identifier}: {e}")
|
|
154
|
+
return {'success': False, 'error_message': self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
|
|
155
|
+
|
|
156
|
+
|
|
134
157
|
def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
|
|
135
158
|
"""
|
|
136
159
|
Fetches a user profile directly by their identifier, bypassing the Flask session.
|
|
@@ -154,7 +177,8 @@ class ProfileService:
|
|
|
154
177
|
# get company info
|
|
155
178
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
156
179
|
if not company:
|
|
157
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
"error": self.i18n_service.t('errors.signup.company_not_found', company_name=company_short_name)}
|
|
158
182
|
|
|
159
183
|
# normalize format's
|
|
160
184
|
email = email.lower()
|
|
@@ -164,24 +188,25 @@ class ProfileService:
|
|
|
164
188
|
if existing_user:
|
|
165
189
|
# validate password
|
|
166
190
|
if not self.bcrypt.check_password_hash(existing_user.password, password):
|
|
167
|
-
return {"error":
|
|
191
|
+
return {"error": self.i18n_service.t('errors.signup.incorrect_password_for_existing_user', email=email)}
|
|
168
192
|
|
|
169
193
|
# check if register
|
|
170
194
|
if company in existing_user.companies:
|
|
171
|
-
return {"error":
|
|
195
|
+
return {"error": self.i18n_service.t('errors.signup.user_already_registered', email=email)}
|
|
172
196
|
else:
|
|
173
197
|
# add new company to existing user
|
|
174
198
|
existing_user.companies.append(company)
|
|
175
199
|
self.profile_repo.save_user(existing_user)
|
|
176
|
-
return {"message":
|
|
200
|
+
return {"message": self.i18n_service.t('flash_messages.user_associated_success')}
|
|
177
201
|
|
|
178
202
|
# add the new user
|
|
179
203
|
if password != confirm_password:
|
|
180
|
-
return {"error":
|
|
204
|
+
return {"error": self.i18n_service.t('errors.signup.password_mismatch')}
|
|
181
205
|
|
|
182
206
|
is_valid, message = self.validate_password(password)
|
|
183
207
|
if not is_valid:
|
|
184
|
-
|
|
208
|
+
# Translate the key returned by validate_password
|
|
209
|
+
return {"error": self.i18n_service.t(message)}
|
|
185
210
|
|
|
186
211
|
# encrypt the password
|
|
187
212
|
hashed_password = self.bcrypt.generate_password_hash(password).decode('utf-8')
|
|
@@ -203,9 +228,9 @@ class ProfileService:
|
|
|
203
228
|
# send email with verification
|
|
204
229
|
self.send_verification_email(new_user, company_short_name)
|
|
205
230
|
|
|
206
|
-
return {"message":
|
|
231
|
+
return {"message": self.i18n_service.t('flash_messages.signup_success')}
|
|
207
232
|
except Exception as e:
|
|
208
|
-
return {"error": str(e)}
|
|
233
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
|
|
209
234
|
|
|
210
235
|
def update_user(self, email: str, **kwargs) -> User:
|
|
211
236
|
return self.profile_repo.update_user(email, **kwargs)
|
|
@@ -215,14 +240,14 @@ class ProfileService:
|
|
|
215
240
|
# check if user exist
|
|
216
241
|
user = self.profile_repo.get_user_by_email(email)
|
|
217
242
|
if not user:
|
|
218
|
-
return {"error":
|
|
243
|
+
return {"error": self.i18n_service.t('errors.verification.user_not_found')}
|
|
219
244
|
|
|
220
245
|
# activate the user account
|
|
221
246
|
self.profile_repo.verify_user(email)
|
|
222
|
-
return {"message":
|
|
247
|
+
return {"message": self.i18n_service.t('flash_messages.account_verified_success')}
|
|
223
248
|
|
|
224
249
|
except Exception as e:
|
|
225
|
-
return {"error":
|
|
250
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
226
251
|
|
|
227
252
|
def change_password(self,
|
|
228
253
|
email: str,
|
|
@@ -231,28 +256,28 @@ class ProfileService:
|
|
|
231
256
|
confirm_password: str):
|
|
232
257
|
try:
|
|
233
258
|
if new_password != confirm_password:
|
|
234
|
-
return {"error":
|
|
259
|
+
return {"error": self.i18n_service.t('errors.change_password.password_mismatch')}
|
|
235
260
|
|
|
236
261
|
# check the temporary code
|
|
237
262
|
user = self.profile_repo.get_user_by_email(email)
|
|
238
263
|
if not user or user.temp_code != temp_code:
|
|
239
|
-
return {"error":
|
|
264
|
+
return {"error": self.i18n_service.t('errors.change_password.invalid_temp_code')}
|
|
240
265
|
|
|
241
266
|
# encrypt and save the password, make the temporary code invalid
|
|
242
267
|
hashed_password = self.bcrypt.generate_password_hash(new_password).decode('utf-8')
|
|
243
268
|
self.profile_repo.update_password(email, hashed_password)
|
|
244
269
|
self.profile_repo.reset_temp_code(email)
|
|
245
270
|
|
|
246
|
-
return {"message":
|
|
271
|
+
return {"message": self.i18n_service.t('flash_messages.password_changed_success')}
|
|
247
272
|
except Exception as e:
|
|
248
|
-
return {"error":
|
|
273
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
249
274
|
|
|
250
275
|
def forgot_password(self, email: str, reset_url: str):
|
|
251
276
|
try:
|
|
252
277
|
# Verificar si el usuario existe
|
|
253
278
|
user = self.profile_repo.get_user_by_email(email)
|
|
254
279
|
if not user:
|
|
255
|
-
return {"error":
|
|
280
|
+
return {"error": self.i18n_service.t('errors.forgot_password.user_not_registered', email=email)}
|
|
256
281
|
|
|
257
282
|
# Gen a temporary code and store in the repositories
|
|
258
283
|
temp_code = ''.join(random.choices(string.ascii_letters + string.digits, k=6)).upper()
|
|
@@ -261,35 +286,31 @@ class ProfileService:
|
|
|
261
286
|
# send email to the user
|
|
262
287
|
self.send_forgot_password_email(user, reset_url)
|
|
263
288
|
|
|
264
|
-
return {"message":
|
|
289
|
+
return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
|
|
265
290
|
except Exception as e:
|
|
266
|
-
return {"error":
|
|
291
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
267
292
|
|
|
268
293
|
def validate_password(self, password):
|
|
269
294
|
"""
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
- Contiene al menos una letra mayúscula
|
|
273
|
-
- Contiene al menos una letra minúscula
|
|
274
|
-
- Contiene al menos un número
|
|
275
|
-
- Contiene al menos un carácter especial
|
|
295
|
+
Validates that a password meets all requirements.
|
|
296
|
+
Returns (True, "...") on success, or (False, "translation.key") on failure.
|
|
276
297
|
"""
|
|
277
298
|
if len(password) < 8:
|
|
278
|
-
return False, "
|
|
299
|
+
return False, "errors.validation.password_too_short"
|
|
279
300
|
|
|
280
301
|
if not any(char.isupper() for char in password):
|
|
281
|
-
return False, "
|
|
302
|
+
return False, "errors.validation.password_no_uppercase"
|
|
282
303
|
|
|
283
304
|
if not any(char.islower() for char in password):
|
|
284
|
-
return False, "
|
|
305
|
+
return False, "errors.validation.password_no_lowercase"
|
|
285
306
|
|
|
286
307
|
if not any(char.isdigit() for char in password):
|
|
287
|
-
return False, "
|
|
308
|
+
return False, "errors.validation.password_no_digit"
|
|
288
309
|
|
|
289
310
|
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
|
290
|
-
return False, "
|
|
311
|
+
return False, "errors.validation.password_no_special_char"
|
|
291
312
|
|
|
292
|
-
return True, "
|
|
313
|
+
return True, "Password is valid."
|
|
293
314
|
|
|
294
315
|
def get_companies(self):
|
|
295
316
|
return self.profile_repo.get_companies()
|
|
@@ -303,7 +324,7 @@ class ProfileService:
|
|
|
303
324
|
def new_api_key(self, company_short_name: str):
|
|
304
325
|
company = self.get_company_by_short_name(company_short_name)
|
|
305
326
|
if not company:
|
|
306
|
-
return {"error":
|
|
327
|
+
return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
|
|
307
328
|
|
|
308
329
|
length = 40 # lenght of the api key
|
|
309
330
|
alphabet = string.ascii_letters + string.digits
|
|
@@ -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
|