iatoolkit 0.63.1__py3-none-any.whl → 0.69.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 +0 -2
- iatoolkit/base_company.py +1 -26
- 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 +39 -6
- iatoolkit/locales/en.yaml +167 -0
- iatoolkit/locales/es.yaml +163 -0
- iatoolkit/repositories/database_manager.py +8 -3
- iatoolkit/repositories/document_repo.py +1 -1
- iatoolkit/repositories/models.py +1 -4
- iatoolkit/repositories/profile_repo.py +0 -4
- iatoolkit/services/auth_service.py +14 -9
- iatoolkit/services/branding_service.py +36 -24
- iatoolkit/services/company_context_service.py +145 -0
- iatoolkit/services/configuration_service.py +133 -0
- iatoolkit/services/dispatcher_service.py +51 -48
- 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 +83 -0
- iatoolkit/services/load_documents_service.py +4 -4
- iatoolkit/services/mail_service.py +9 -4
- iatoolkit/services/profile_service.py +61 -38
- iatoolkit/services/prompt_manager_service.py +20 -16
- iatoolkit/services/query_service.py +19 -15
- iatoolkit/services/search_service.py +11 -4
- iatoolkit/services/sql_service.py +55 -25
- 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_onboarding_button.js +6 -0
- 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/static/styles/onboarding.css +7 -0
- 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 +59 -27
- iatoolkit/templates/chat_modals.html +114 -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 +17 -6
- iatoolkit/templates/onboarding_shell.html +4 -2
- iatoolkit/templates/signup.html +14 -14
- iatoolkit/views/base_login_view.py +19 -9
- iatoolkit/views/change_password_view.py +50 -35
- iatoolkit/views/external_login_view.py +1 -1
- iatoolkit/views/forgot_password_view.py +21 -22
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +13 -9
- iatoolkit/views/home_view.py +30 -39
- 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 +52 -40
- 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 +27 -27
- iatoolkit/views/user_feedback_api_view.py +19 -18
- iatoolkit/views/verify_user_view.py +29 -30
- {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/METADATA +40 -22
- iatoolkit-0.69.0.dist-info/RECORD +120 -0
- iatoolkit-0.69.0.dist-info/licenses/LICENSE +21 -0
- iatoolkit/services/onboarding_service.py +0 -43
- 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.69.0.dist-info}/WHEEL +0 -0
- {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.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):
|
|
@@ -91,6 +95,9 @@ class ProfileService:
|
|
|
91
95
|
user_identifier=user_identifier,
|
|
92
96
|
user_profile=external_user_profile)
|
|
93
97
|
|
|
98
|
+
# 3. make sure the flask session is clean
|
|
99
|
+
SessionManager.clear()
|
|
100
|
+
|
|
94
101
|
def save_user_profile(self, company: Company, user_identifier: str, user_profile: dict):
|
|
95
102
|
"""
|
|
96
103
|
Private helper: Takes a pre-built profile, saves it to Redis, and sets the Flask cookie.
|
|
@@ -131,6 +138,24 @@ class ProfileService:
|
|
|
131
138
|
"profile": profile
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
def update_user_language(self, user_identifier: str, new_lang: str) -> dict:
|
|
142
|
+
"""
|
|
143
|
+
Business logic to update a user's preferred language.
|
|
144
|
+
It validates the language and then calls the generic update method.
|
|
145
|
+
"""
|
|
146
|
+
# 1. Validate that the language is supported by checking the loaded translations.
|
|
147
|
+
if new_lang not in self.i18n_service.translations:
|
|
148
|
+
return {'success': False, 'error_message': self.i18n_service.t('errors.general.unsupported_language')}
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# 2. Call the generic update_user method, passing the specific field to update.
|
|
152
|
+
self.update_user(user_identifier, preferred_language=new_lang)
|
|
153
|
+
return {'success': True, 'message': 'Language updated successfully.'}
|
|
154
|
+
except Exception as e:
|
|
155
|
+
# Log the error and return a generic failure message.
|
|
156
|
+
logging.error(f"Failed to update language for {user_identifier}: {e}")
|
|
157
|
+
return {'success': False, 'error_message': self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
|
|
158
|
+
|
|
134
159
|
|
|
135
160
|
def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
|
|
136
161
|
"""
|
|
@@ -155,7 +180,8 @@ class ProfileService:
|
|
|
155
180
|
# get company info
|
|
156
181
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
157
182
|
if not company:
|
|
158
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
"error": self.i18n_service.t('errors.signup.company_not_found', company_name=company_short_name)}
|
|
159
185
|
|
|
160
186
|
# normalize format's
|
|
161
187
|
email = email.lower()
|
|
@@ -165,24 +191,25 @@ class ProfileService:
|
|
|
165
191
|
if existing_user:
|
|
166
192
|
# validate password
|
|
167
193
|
if not self.bcrypt.check_password_hash(existing_user.password, password):
|
|
168
|
-
return {"error":
|
|
194
|
+
return {"error": self.i18n_service.t('errors.signup.incorrect_password_for_existing_user', email=email)}
|
|
169
195
|
|
|
170
196
|
# check if register
|
|
171
197
|
if company in existing_user.companies:
|
|
172
|
-
return {"error":
|
|
198
|
+
return {"error": self.i18n_service.t('errors.signup.user_already_registered', email=email)}
|
|
173
199
|
else:
|
|
174
200
|
# add new company to existing user
|
|
175
201
|
existing_user.companies.append(company)
|
|
176
202
|
self.profile_repo.save_user(existing_user)
|
|
177
|
-
return {"message":
|
|
203
|
+
return {"message": self.i18n_service.t('flash_messages.user_associated_success')}
|
|
178
204
|
|
|
179
205
|
# add the new user
|
|
180
206
|
if password != confirm_password:
|
|
181
|
-
return {"error":
|
|
207
|
+
return {"error": self.i18n_service.t('errors.signup.password_mismatch')}
|
|
182
208
|
|
|
183
209
|
is_valid, message = self.validate_password(password)
|
|
184
210
|
if not is_valid:
|
|
185
|
-
|
|
211
|
+
# Translate the key returned by validate_password
|
|
212
|
+
return {"error": self.i18n_service.t(message)}
|
|
186
213
|
|
|
187
214
|
# encrypt the password
|
|
188
215
|
hashed_password = self.bcrypt.generate_password_hash(password).decode('utf-8')
|
|
@@ -204,9 +231,9 @@ class ProfileService:
|
|
|
204
231
|
# send email with verification
|
|
205
232
|
self.send_verification_email(new_user, company_short_name)
|
|
206
233
|
|
|
207
|
-
return {"message":
|
|
234
|
+
return {"message": self.i18n_service.t('flash_messages.signup_success')}
|
|
208
235
|
except Exception as e:
|
|
209
|
-
return {"error": str(e)}
|
|
236
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
|
|
210
237
|
|
|
211
238
|
def update_user(self, email: str, **kwargs) -> User:
|
|
212
239
|
return self.profile_repo.update_user(email, **kwargs)
|
|
@@ -216,14 +243,14 @@ class ProfileService:
|
|
|
216
243
|
# check if user exist
|
|
217
244
|
user = self.profile_repo.get_user_by_email(email)
|
|
218
245
|
if not user:
|
|
219
|
-
return {"error":
|
|
246
|
+
return {"error": self.i18n_service.t('errors.verification.user_not_found')}
|
|
220
247
|
|
|
221
248
|
# activate the user account
|
|
222
249
|
self.profile_repo.verify_user(email)
|
|
223
|
-
return {"message":
|
|
250
|
+
return {"message": self.i18n_service.t('flash_messages.account_verified_success')}
|
|
224
251
|
|
|
225
252
|
except Exception as e:
|
|
226
|
-
return {"error":
|
|
253
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
227
254
|
|
|
228
255
|
def change_password(self,
|
|
229
256
|
email: str,
|
|
@@ -232,28 +259,28 @@ class ProfileService:
|
|
|
232
259
|
confirm_password: str):
|
|
233
260
|
try:
|
|
234
261
|
if new_password != confirm_password:
|
|
235
|
-
return {"error":
|
|
262
|
+
return {"error": self.i18n_service.t('errors.change_password.password_mismatch')}
|
|
236
263
|
|
|
237
264
|
# check the temporary code
|
|
238
265
|
user = self.profile_repo.get_user_by_email(email)
|
|
239
266
|
if not user or user.temp_code != temp_code:
|
|
240
|
-
return {"error":
|
|
267
|
+
return {"error": self.i18n_service.t('errors.change_password.invalid_temp_code')}
|
|
241
268
|
|
|
242
269
|
# encrypt and save the password, make the temporary code invalid
|
|
243
270
|
hashed_password = self.bcrypt.generate_password_hash(new_password).decode('utf-8')
|
|
244
271
|
self.profile_repo.update_password(email, hashed_password)
|
|
245
272
|
self.profile_repo.reset_temp_code(email)
|
|
246
273
|
|
|
247
|
-
return {"message":
|
|
274
|
+
return {"message": self.i18n_service.t('flash_messages.password_changed_success')}
|
|
248
275
|
except Exception as e:
|
|
249
|
-
return {"error":
|
|
276
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
250
277
|
|
|
251
278
|
def forgot_password(self, email: str, reset_url: str):
|
|
252
279
|
try:
|
|
253
280
|
# Verificar si el usuario existe
|
|
254
281
|
user = self.profile_repo.get_user_by_email(email)
|
|
255
282
|
if not user:
|
|
256
|
-
return {"error":
|
|
283
|
+
return {"error": self.i18n_service.t('errors.forgot_password.user_not_registered', email=email)}
|
|
257
284
|
|
|
258
285
|
# Gen a temporary code and store in the repositories
|
|
259
286
|
temp_code = ''.join(random.choices(string.ascii_letters + string.digits, k=6)).upper()
|
|
@@ -262,35 +289,31 @@ class ProfileService:
|
|
|
262
289
|
# send email to the user
|
|
263
290
|
self.send_forgot_password_email(user, reset_url)
|
|
264
291
|
|
|
265
|
-
return {"message":
|
|
292
|
+
return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
|
|
266
293
|
except Exception as e:
|
|
267
|
-
return {"error":
|
|
294
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
268
295
|
|
|
269
296
|
def validate_password(self, password):
|
|
270
297
|
"""
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
- Contiene al menos una letra mayúscula
|
|
274
|
-
- Contiene al menos una letra minúscula
|
|
275
|
-
- Contiene al menos un número
|
|
276
|
-
- Contiene al menos un carácter especial
|
|
298
|
+
Validates that a password meets all requirements.
|
|
299
|
+
Returns (True, "...") on success, or (False, "translation.key") on failure.
|
|
277
300
|
"""
|
|
278
301
|
if len(password) < 8:
|
|
279
|
-
return False, "
|
|
302
|
+
return False, "errors.validation.password_too_short"
|
|
280
303
|
|
|
281
304
|
if not any(char.isupper() for char in password):
|
|
282
|
-
return False, "
|
|
305
|
+
return False, "errors.validation.password_no_uppercase"
|
|
283
306
|
|
|
284
307
|
if not any(char.islower() for char in password):
|
|
285
|
-
return False, "
|
|
308
|
+
return False, "errors.validation.password_no_lowercase"
|
|
286
309
|
|
|
287
310
|
if not any(char.isdigit() for char in password):
|
|
288
|
-
return False, "
|
|
311
|
+
return False, "errors.validation.password_no_digit"
|
|
289
312
|
|
|
290
313
|
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
|
291
|
-
return False, "
|
|
314
|
+
return False, "errors.validation.password_no_special_char"
|
|
292
315
|
|
|
293
|
-
return True, "
|
|
316
|
+
return True, "Password is valid."
|
|
294
317
|
|
|
295
318
|
def get_companies(self):
|
|
296
319
|
return self.profile_repo.get_companies()
|
|
@@ -304,7 +327,7 @@ class ProfileService:
|
|
|
304
327
|
def new_api_key(self, company_short_name: str):
|
|
305
328
|
company = self.get_company_by_short_name(company_short_name)
|
|
306
329
|
if not company:
|
|
307
|
-
return {"error":
|
|
330
|
+
return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
|
|
308
331
|
|
|
309
332
|
length = 40 # lenght of the api key
|
|
310
333
|
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,8 @@ 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.company_context_service import CompanyContextService
|
|
12
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
11
13
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
12
14
|
from iatoolkit.repositories.models import Task
|
|
13
15
|
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
@@ -32,21 +34,25 @@ class QueryService:
|
|
|
32
34
|
def __init__(self,
|
|
33
35
|
llm_client: llmClient,
|
|
34
36
|
profile_service: ProfileService,
|
|
37
|
+
company_context_service: CompanyContextService,
|
|
35
38
|
document_service: DocumentService,
|
|
36
39
|
document_repo: DocumentRepo,
|
|
37
40
|
llmquery_repo: LLMQueryRepo,
|
|
38
41
|
profile_repo: ProfileRepo,
|
|
39
42
|
prompt_service: PromptService,
|
|
43
|
+
i18n_service: I18nService,
|
|
40
44
|
util: Utility,
|
|
41
45
|
dispatcher: Dispatcher,
|
|
42
46
|
session_context: UserSessionContextService
|
|
43
47
|
):
|
|
44
48
|
self.profile_service = profile_service
|
|
49
|
+
self.company_context_service = company_context_service
|
|
45
50
|
self.document_service = document_service
|
|
46
51
|
self.document_repo = document_repo
|
|
47
52
|
self.llmquery_repo = llmquery_repo
|
|
48
53
|
self.profile_repo = profile_repo
|
|
49
54
|
self.prompt_service = prompt_service
|
|
55
|
+
self.i18n_service = i18n_service
|
|
50
56
|
self.util = util
|
|
51
57
|
self.dispatcher = dispatcher
|
|
52
58
|
self.session_context = session_context
|
|
@@ -56,7 +62,7 @@ class QueryService:
|
|
|
56
62
|
self.model = os.getenv("LLM_MODEL", "")
|
|
57
63
|
if not self.model:
|
|
58
64
|
raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
|
|
59
|
-
"
|
|
65
|
+
"missing ENV variable 'LLM_MODEL' configuration.")
|
|
60
66
|
|
|
61
67
|
def _build_context_and_profile(self, company_short_name: str, user_identifier: str) -> tuple:
|
|
62
68
|
# this method read the user/company context from the database and renders the system prompt
|
|
@@ -78,7 +84,7 @@ class QueryService:
|
|
|
78
84
|
)
|
|
79
85
|
|
|
80
86
|
# get the company context: schemas, database models, .md files
|
|
81
|
-
company_specific_context = self.
|
|
87
|
+
company_specific_context = self.company_context_service.get_company_context(company_short_name)
|
|
82
88
|
|
|
83
89
|
# merge context: company + user
|
|
84
90
|
final_system_context = f"{company_specific_context}\n{rendered_system_prompt}"
|
|
@@ -127,7 +133,7 @@ class QueryService:
|
|
|
127
133
|
lock_key = f"lock:context:{company_short_name}/{user_identifier}"
|
|
128
134
|
if not self.session_context.acquire_lock(lock_key, expire_seconds=60):
|
|
129
135
|
logging.warning(
|
|
130
|
-
f"
|
|
136
|
+
f"try to rebuild context for user {user_identifier} while is still in process, ignored.")
|
|
131
137
|
return
|
|
132
138
|
|
|
133
139
|
try:
|
|
@@ -138,11 +144,9 @@ class QueryService:
|
|
|
138
144
|
prepared_context, version_to_save = self.session_context.get_and_clear_prepared_context(company_short_name,
|
|
139
145
|
user_identifier)
|
|
140
146
|
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
147
|
return
|
|
144
148
|
|
|
145
|
-
logging.info(f"
|
|
149
|
+
logging.info(f"sending context to LLM for: {company_short_name}/{user_identifier}...")
|
|
146
150
|
|
|
147
151
|
# Limpiar solo el historial de chat y el ID de respuesta anterior
|
|
148
152
|
self.session_context.clear_llm_history(company_short_name, user_identifier)
|
|
@@ -161,9 +165,9 @@ class QueryService:
|
|
|
161
165
|
self.session_context.save_context_version(company_short_name, user_identifier, version_to_save)
|
|
162
166
|
|
|
163
167
|
logging.info(
|
|
164
|
-
f"
|
|
168
|
+
f"Context for: {company_short_name}/{user_identifier} settled in {int(time.time() - start_time)} sec.")
|
|
165
169
|
except Exception as e:
|
|
166
|
-
logging.exception(f"Error
|
|
170
|
+
logging.exception(f"Error in finalize_context_rebuild for {company_short_name}: {e}")
|
|
167
171
|
raise e
|
|
168
172
|
finally:
|
|
169
173
|
# --- Liberar el Bloqueo ---
|
|
@@ -181,11 +185,11 @@ class QueryService:
|
|
|
181
185
|
company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
|
|
182
186
|
if not company:
|
|
183
187
|
return {"error": True,
|
|
184
|
-
"error_message":
|
|
188
|
+
"error_message": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
|
|
185
189
|
|
|
186
190
|
if not prompt_name and not question:
|
|
187
191
|
return {"error": True,
|
|
188
|
-
"error_message":
|
|
192
|
+
"error_message": self.i18n_service.t('services.start_query')}
|
|
189
193
|
|
|
190
194
|
# get the previous response_id and context history
|
|
191
195
|
previous_response_id = None
|
|
@@ -196,7 +200,7 @@ class QueryService:
|
|
|
196
200
|
previous_response_id = self.session_context.get_last_response_id(company.short_name, user_identifier)
|
|
197
201
|
if not previous_response_id:
|
|
198
202
|
return {'error': True,
|
|
199
|
-
"error_message":
|
|
203
|
+
"error_message": self.i18n_service.t('errors.services.missing_response_id', company_short_name=company.short_name, user_identifier=user_identifier)
|
|
200
204
|
}
|
|
201
205
|
elif self.util.is_gemini_model(self.model):
|
|
202
206
|
# check the length of the context_history and remove old messages
|
|
@@ -294,7 +298,7 @@ class QueryService:
|
|
|
294
298
|
return len(history) >= 1
|
|
295
299
|
return False
|
|
296
300
|
except Exception as e:
|
|
297
|
-
logging.warning(f"
|
|
301
|
+
logging.warning(f"error verifying context cache: {e}")
|
|
298
302
|
return False
|
|
299
303
|
|
|
300
304
|
def load_files_for_context(self, files: list) -> str:
|
|
@@ -353,7 +357,7 @@ class QueryService:
|
|
|
353
357
|
try:
|
|
354
358
|
total_tokens = sum(self.llm_client.count_tokens(json.dumps(message)) for message in context_history)
|
|
355
359
|
except Exception as e:
|
|
356
|
-
logging.error(f"
|
|
360
|
+
logging.error(f"error counting tokens for history: {e}.")
|
|
357
361
|
return
|
|
358
362
|
|
|
359
363
|
# Si se excede el límite, eliminar mensajes antiguos (empezando por el segundo)
|
|
@@ -364,8 +368,8 @@ class QueryService:
|
|
|
364
368
|
removed_tokens = self.llm_client.count_tokens(json.dumps(removed_message))
|
|
365
369
|
total_tokens -= removed_tokens
|
|
366
370
|
logging.warning(
|
|
367
|
-
f"
|
|
368
|
-
f"
|
|
371
|
+
f"history tokens ({total_tokens + removed_tokens} tokens) exceed the limit of: {GEMINI_MAX_TOKENS_CONTEXT_HISTORY}. "
|
|
372
|
+
f"new context: {total_tokens} tokens."
|
|
369
373
|
)
|
|
370
374
|
except IndexError:
|
|
371
375
|
# Se produce si solo queda el mensaje del sistema, el bucle debería detenerse.
|
|
@@ -5,19 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
from iatoolkit.repositories.vs_repo import VSRepo
|
|
7
7
|
from iatoolkit.repositories.document_repo import DocumentRepo
|
|
8
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
|
+
from iatoolkit.repositories.models import Company
|
|
8
10
|
from injector import inject
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class SearchService:
|
|
12
14
|
@inject
|
|
13
15
|
def __init__(self,
|
|
16
|
+
profile_repo: ProfileRepo,
|
|
14
17
|
doc_repo: DocumentRepo,
|
|
15
18
|
vs_repo: VSRepo):
|
|
16
|
-
|
|
19
|
+
self.profile_repo = profile_repo
|
|
17
20
|
self.vs_repo = vs_repo
|
|
18
21
|
self.doc_repo = doc_repo
|
|
19
22
|
|
|
20
|
-
def search(self,
|
|
23
|
+
def search(self, company_short_name: str, query: str, metadata_filter: dict = None) -> str:
|
|
21
24
|
"""
|
|
22
25
|
Performs a semantic search for a given query within a company's documents.
|
|
23
26
|
|
|
@@ -26,7 +29,7 @@ class SearchService:
|
|
|
26
29
|
content of the retrieved documents, which can be used as context for an LLM.
|
|
27
30
|
|
|
28
31
|
Args:
|
|
29
|
-
|
|
32
|
+
company_short_name: The company to search within.
|
|
30
33
|
query: The text query to search for.
|
|
31
34
|
metadata_filter: An optional dictionary to filter documents by their metadata.
|
|
32
35
|
|
|
@@ -34,7 +37,11 @@ class SearchService:
|
|
|
34
37
|
A string containing the concatenated content of the found documents,
|
|
35
38
|
formatted to be used as a context.
|
|
36
39
|
"""
|
|
37
|
-
|
|
40
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
41
|
+
if not company:
|
|
42
|
+
return f"error: company {company_short_name} not found"
|
|
43
|
+
|
|
44
|
+
document_list = self.vs_repo.query(company_id=company.id,
|
|
38
45
|
query_text=query,
|
|
39
46
|
metadata_filter=metadata_filter)
|
|
40
47
|
|