iatoolkit 0.11.0__py3-none-any.whl → 0.71.2__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.
- iatoolkit/__init__.py +2 -6
- iatoolkit/base_company.py +9 -29
- iatoolkit/cli_commands.py +1 -1
- iatoolkit/common/routes.py +96 -52
- iatoolkit/common/session_manager.py +2 -1
- iatoolkit/common/util.py +17 -27
- iatoolkit/company_registry.py +1 -2
- iatoolkit/iatoolkit.py +97 -53
- iatoolkit/infra/llm_client.py +15 -20
- iatoolkit/infra/llm_proxy.py +38 -10
- iatoolkit/infra/openai_adapter.py +1 -1
- iatoolkit/infra/redis_session_manager.py +48 -2
- iatoolkit/locales/en.yaml +167 -0
- iatoolkit/locales/es.yaml +163 -0
- iatoolkit/repositories/database_manager.py +23 -3
- iatoolkit/repositories/document_repo.py +1 -1
- iatoolkit/repositories/models.py +35 -10
- iatoolkit/repositories/profile_repo.py +3 -2
- iatoolkit/repositories/vs_repo.py +26 -20
- iatoolkit/services/auth_service.py +193 -0
- iatoolkit/services/branding_service.py +70 -25
- iatoolkit/services/company_context_service.py +155 -0
- iatoolkit/services/configuration_service.py +133 -0
- iatoolkit/services/dispatcher_service.py +80 -105
- iatoolkit/services/document_service.py +5 -2
- iatoolkit/services/embedding_service.py +146 -0
- iatoolkit/services/excel_service.py +30 -26
- iatoolkit/services/file_processor_service.py +4 -12
- iatoolkit/services/history_service.py +7 -16
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +18 -29
- iatoolkit/services/language_service.py +83 -0
- iatoolkit/services/load_documents_service.py +100 -113
- iatoolkit/services/mail_service.py +9 -4
- iatoolkit/services/profile_service.py +152 -76
- iatoolkit/services/prompt_manager_service.py +20 -16
- iatoolkit/services/query_service.py +208 -96
- iatoolkit/services/search_service.py +11 -4
- iatoolkit/services/sql_service.py +57 -25
- iatoolkit/services/tasks_service.py +1 -1
- iatoolkit/services/user_feedback_service.py +72 -34
- iatoolkit/services/user_session_context_service.py +112 -54
- iatoolkit/static/images/fernando.jpeg +0 -0
- 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 +135 -222
- 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 +35 -0
- iatoolkit/static/styles/chat_iatoolkit.css +289 -210
- iatoolkit/static/styles/chat_modal.css +63 -77
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/query_main.prompt +5 -22
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +40 -20
- iatoolkit/templates/change_password.html +57 -36
- iatoolkit/templates/chat.html +180 -86
- iatoolkit/templates/chat_modals.html +138 -68
- iatoolkit/templates/error.html +44 -8
- iatoolkit/templates/forgot_password.html +40 -23
- iatoolkit/templates/index.html +145 -0
- iatoolkit/templates/login_simulation.html +45 -0
- iatoolkit/templates/onboarding_shell.html +107 -0
- iatoolkit/templates/signup.html +63 -65
- iatoolkit/views/base_login_view.py +91 -0
- iatoolkit/views/change_password_view.py +56 -31
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/external_login_view.py +61 -28
- iatoolkit/views/{file_store_view.py → file_store_api_view.py} +10 -3
- iatoolkit/views/forgot_password_view.py +27 -21
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +50 -23
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +58 -0
- iatoolkit/views/login_simulation_view.py +93 -0
- iatoolkit/views/login_view.py +130 -37
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/{prompt_view.py → prompt_api_view.py} +10 -10
- iatoolkit/views/signup_view.py +41 -36
- 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 +60 -0
- iatoolkit/views/verify_user_view.py +34 -29
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/METADATA +41 -23
- iatoolkit-0.71.2.dist-info/RECORD +122 -0
- iatoolkit-0.71.2.dist-info/licenses/LICENSE +21 -0
- iatoolkit/common/auth.py +0 -200
- iatoolkit/static/images/arrow_up.png +0 -0
- iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
- iatoolkit/static/images/logo_clinica.png +0 -0
- iatoolkit/static/images/logo_iatoolkit.png +0 -0
- iatoolkit/static/images/logo_maxxa.png +0 -0
- iatoolkit/static/images/logo_notaria.png +0 -0
- iatoolkit/static/images/logo_tarjeta.png +0 -0
- iatoolkit/static/images/logo_umayor.png +0 -0
- iatoolkit/static/images/upload.png +0 -0
- iatoolkit/static/js/chat_feedback.js +0 -115
- iatoolkit/static/js/chat_history.js +0 -117
- iatoolkit/static/styles/chat_info.css +0 -53
- iatoolkit/templates/header.html +0 -31
- iatoolkit/templates/home.html +0 -199
- iatoolkit/templates/login.html +0 -43
- iatoolkit/templates/test.html +0 -9
- iatoolkit/views/chat_token_request_view.py +0 -98
- iatoolkit/views/chat_view.py +0 -58
- iatoolkit/views/download_file_view.py +0 -58
- iatoolkit/views/external_chat_login_view.py +0 -95
- iatoolkit/views/history_view.py +0 -57
- iatoolkit/views/llmquery_view.py +0 -65
- iatoolkit/views/tasks_review_view.py +0 -83
- iatoolkit/views/user_feedback_view.py +0 -74
- iatoolkit-0.11.0.dist-info/RECORD +0 -110
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/WHEEL +0 -0
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/top_level.txt +0 -0
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
from iatoolkit.infra.llm_client import llmClient
|
|
7
|
+
from iatoolkit.services.profile_service import ProfileService
|
|
7
8
|
from iatoolkit.repositories.document_repo import DocumentRepo
|
|
8
9
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
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
|
|
13
|
+
from iatoolkit.services.configuration_service import ConfigurationService
|
|
10
14
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
11
|
-
|
|
12
15
|
from iatoolkit.repositories.models import Task
|
|
13
16
|
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
14
17
|
from iatoolkit.services.prompt_manager_service import PromptService
|
|
@@ -21,6 +24,7 @@ import logging
|
|
|
21
24
|
from typing import Optional
|
|
22
25
|
import json
|
|
23
26
|
import time
|
|
27
|
+
import hashlib
|
|
24
28
|
import os
|
|
25
29
|
|
|
26
30
|
|
|
@@ -30,166 +34,248 @@ class QueryService:
|
|
|
30
34
|
@inject
|
|
31
35
|
def __init__(self,
|
|
32
36
|
llm_client: llmClient,
|
|
37
|
+
profile_service: ProfileService,
|
|
38
|
+
company_context_service: CompanyContextService,
|
|
33
39
|
document_service: DocumentService,
|
|
34
40
|
document_repo: DocumentRepo,
|
|
35
41
|
llmquery_repo: LLMQueryRepo,
|
|
36
42
|
profile_repo: ProfileRepo,
|
|
37
43
|
prompt_service: PromptService,
|
|
44
|
+
i18n_service: I18nService,
|
|
38
45
|
util: Utility,
|
|
39
46
|
dispatcher: Dispatcher,
|
|
40
|
-
session_context: UserSessionContextService
|
|
47
|
+
session_context: UserSessionContextService,
|
|
48
|
+
configuration_service: ConfigurationService
|
|
41
49
|
):
|
|
50
|
+
self.profile_service = profile_service
|
|
51
|
+
self.company_context_service = company_context_service
|
|
42
52
|
self.document_service = document_service
|
|
43
53
|
self.document_repo = document_repo
|
|
44
54
|
self.llmquery_repo = llmquery_repo
|
|
45
55
|
self.profile_repo = profile_repo
|
|
46
56
|
self.prompt_service = prompt_service
|
|
57
|
+
self.i18n_service = i18n_service
|
|
47
58
|
self.util = util
|
|
48
59
|
self.dispatcher = dispatcher
|
|
49
60
|
self.session_context = session_context
|
|
61
|
+
self.configuration_service = configuration_service
|
|
50
62
|
self.llm_client = llm_client
|
|
51
63
|
|
|
52
64
|
# get the model from the environment variable
|
|
53
|
-
self.
|
|
54
|
-
if not self.
|
|
65
|
+
self.default_model = os.getenv("LLM_MODEL", "")
|
|
66
|
+
if not self.default_model:
|
|
55
67
|
raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
|
|
56
|
-
"
|
|
68
|
+
"missing ENV variable 'LLM_MODEL' configuration.")
|
|
57
69
|
|
|
70
|
+
def init_context(self, company_short_name: str,
|
|
71
|
+
user_identifier: str,
|
|
72
|
+
model: str = None) -> dict:
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
local_user_id: int = 0,
|
|
63
|
-
model: str = ''):
|
|
64
|
-
start_time = time.time()
|
|
65
|
-
if not model:
|
|
66
|
-
model = self.model
|
|
74
|
+
# 1. Execute the forced rebuild sequence using the unified identifier.
|
|
75
|
+
self.session_context.clear_all_context(company_short_name, user_identifier)
|
|
76
|
+
logging.info(f"Context for {company_short_name}/{user_identifier} has been cleared.")
|
|
67
77
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
# 2. LLM context is clean, now we can load it again
|
|
79
|
+
self.prepare_context(
|
|
80
|
+
company_short_name=company_short_name,
|
|
81
|
+
user_identifier=user_identifier
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# 3. communicate the new context to the LLM
|
|
85
|
+
response = self.set_context_for_llm(
|
|
86
|
+
company_short_name=company_short_name,
|
|
87
|
+
user_identifier=user_identifier,
|
|
88
|
+
model=model
|
|
89
|
+
)
|
|
73
90
|
|
|
91
|
+
return response
|
|
92
|
+
|
|
93
|
+
def _build_context_and_profile(self, company_short_name: str, user_identifier: str) -> tuple:
|
|
94
|
+
# this method read the user/company context from the database and renders the system prompt
|
|
74
95
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
75
96
|
if not company:
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
return None, None
|
|
98
|
+
|
|
99
|
+
# Get the user profile from the single source of truth.
|
|
100
|
+
user_profile = self.profile_service.get_profile_by_identifier(company_short_name, user_identifier)
|
|
101
|
+
|
|
102
|
+
# render the iatoolkit main system prompt with the company/user information
|
|
103
|
+
system_prompt_template = self.prompt_service.get_system_prompt()
|
|
104
|
+
rendered_system_prompt = self.util.render_prompt_from_string(
|
|
105
|
+
template_string=system_prompt_template,
|
|
106
|
+
question=None,
|
|
107
|
+
client_data=user_profile,
|
|
108
|
+
company=company,
|
|
109
|
+
service_list=self.dispatcher.get_company_services(company)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# get the company context: schemas, database models, .md files
|
|
113
|
+
company_specific_context = self.company_context_service.get_company_context(company_short_name)
|
|
114
|
+
|
|
115
|
+
# merge context: company + user
|
|
116
|
+
final_system_context = f"{company_specific_context}\n{rendered_system_prompt}"
|
|
117
|
+
|
|
118
|
+
return final_system_context, user_profile
|
|
119
|
+
|
|
120
|
+
def prepare_context(self, company_short_name: str, user_identifier: str) -> dict:
|
|
121
|
+
# prepare the context and decide if it needs to be rebuilt
|
|
122
|
+
# save the generated context in the session context for later use
|
|
123
|
+
if not user_identifier:
|
|
124
|
+
return {'rebuild_needed': True, 'error': 'Invalid user identifier'}
|
|
125
|
+
|
|
126
|
+
# create the company/user context and compute its version
|
|
127
|
+
final_system_context, user_profile = self._build_context_and_profile(
|
|
128
|
+
company_short_name, user_identifier)
|
|
129
|
+
|
|
130
|
+
# save the user information in the session context
|
|
131
|
+
# it's needed for the jinja predefined prompts (filtering)
|
|
132
|
+
self.session_context.save_profile_data(company_short_name, user_identifier, user_profile)
|
|
133
|
+
|
|
134
|
+
# calculate the context version
|
|
135
|
+
current_version = self._compute_context_version_from_string(final_system_context)
|
|
78
136
|
|
|
79
|
-
logging.info(f"Inicializando contexto para {company_short_name}/{user_identifier} con modelo {model} ...")
|
|
80
137
|
try:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
user_identifier=user_identifier
|
|
85
|
-
)
|
|
138
|
+
prev_version = self.session_context.get_context_version(company_short_name, user_identifier)
|
|
139
|
+
except Exception:
|
|
140
|
+
prev_version = None
|
|
86
141
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
user_profile = self.dispatcher.get_user_info(
|
|
90
|
-
company_name=company_short_name,
|
|
91
|
-
user_identifier=user_identifier,
|
|
92
|
-
is_local_user=is_local_user
|
|
93
|
-
)
|
|
142
|
+
rebuild_is_needed = not (prev_version and prev_version == current_version and
|
|
143
|
+
self._has_valid_cached_context(company_short_name, user_identifier))
|
|
94
144
|
|
|
95
|
-
|
|
96
|
-
|
|
145
|
+
if rebuild_is_needed:
|
|
146
|
+
# Guardar el contexto preparado y su versión para que `finalize_context_rebuild` los use.
|
|
147
|
+
self.session_context.save_prepared_context(company_short_name,
|
|
148
|
+
user_identifier,
|
|
149
|
+
final_system_context,
|
|
150
|
+
current_version)
|
|
97
151
|
|
|
98
|
-
|
|
99
|
-
# it's needed for the jinja predefined prompts (filtering)
|
|
100
|
-
self.session_context.save_user_session_data(company_short_name, user_identifier, user_profile)
|
|
152
|
+
return {'rebuild_needed': rebuild_is_needed}
|
|
101
153
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
question=None,
|
|
107
|
-
client_data=user_profile,
|
|
108
|
-
company=company,
|
|
109
|
-
service_list=self.dispatcher.get_company_services(company)
|
|
110
|
-
)
|
|
154
|
+
def set_context_for_llm(self,
|
|
155
|
+
company_short_name: str,
|
|
156
|
+
user_identifier: str,
|
|
157
|
+
model: str = ''):
|
|
111
158
|
|
|
112
|
-
|
|
113
|
-
|
|
159
|
+
# This service takes a pre-built context and send to the LLM
|
|
160
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
161
|
+
if not company:
|
|
162
|
+
logging.error(f"Company not found: {company_short_name} in set_context_for_llm")
|
|
163
|
+
return
|
|
114
164
|
|
|
115
|
-
|
|
116
|
-
|
|
165
|
+
# --- Model Resolution ---
|
|
166
|
+
# Priority: 1. Explicit model -> 2. Company config -> 3. Global default
|
|
167
|
+
effective_model = model
|
|
168
|
+
if not effective_model:
|
|
169
|
+
llm_config = self.configuration_service.get_configuration(company_short_name, 'llm')
|
|
170
|
+
if llm_config and llm_config.get('model'):
|
|
171
|
+
effective_model = llm_config['model']
|
|
172
|
+
|
|
173
|
+
effective_model = effective_model or self.default_model
|
|
174
|
+
|
|
175
|
+
# blocking logic to avoid multiple requests for the same user/company at the same time
|
|
176
|
+
lock_key = f"lock:context:{company_short_name}/{user_identifier}"
|
|
177
|
+
if not self.session_context.acquire_lock(lock_key, expire_seconds=60):
|
|
178
|
+
logging.warning(
|
|
179
|
+
f"try to rebuild context for user {user_identifier} while is still in process, ignored.")
|
|
180
|
+
return
|
|
117
181
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
182
|
+
try:
|
|
183
|
+
start_time = time.time()
|
|
184
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
185
|
+
|
|
186
|
+
# get the prepared context and version from the session cache
|
|
187
|
+
prepared_context, version_to_save = self.session_context.get_and_clear_prepared_context(company_short_name,
|
|
188
|
+
user_identifier)
|
|
189
|
+
if not prepared_context:
|
|
190
|
+
return
|
|
124
191
|
|
|
125
|
-
|
|
192
|
+
logging.info(f"sending context to LLM model {effective_model} for: {company_short_name}/{user_identifier}...")
|
|
126
193
|
|
|
127
|
-
|
|
194
|
+
# clean only the chat history and the last response ID for this user/company
|
|
195
|
+
self.session_context.clear_llm_history(company_short_name, user_identifier)
|
|
196
|
+
|
|
197
|
+
response_id = ''
|
|
198
|
+
if self.util.is_gemini_model(effective_model):
|
|
199
|
+
context_history = [{"role": "user", "content": prepared_context}]
|
|
200
|
+
self.session_context.save_context_history(company_short_name, user_identifier, context_history)
|
|
201
|
+
elif self.util.is_openai_model(effective_model):
|
|
202
|
+
# Here is the call to the LLM client for settling the company/user context
|
|
128
203
|
response_id = self.llm_client.set_company_context(
|
|
129
204
|
company=company,
|
|
130
|
-
company_base_context=
|
|
131
|
-
model=
|
|
205
|
+
company_base_context=prepared_context,
|
|
206
|
+
model=effective_model
|
|
132
207
|
)
|
|
133
|
-
|
|
134
|
-
# 7. save response_id in the session context
|
|
135
208
|
self.session_context.save_last_response_id(company_short_name, user_identifier, response_id)
|
|
136
209
|
|
|
137
|
-
|
|
138
|
-
|
|
210
|
+
if version_to_save:
|
|
211
|
+
self.session_context.save_context_version(company_short_name, user_identifier, version_to_save)
|
|
139
212
|
|
|
213
|
+
logging.info(
|
|
214
|
+
f"Context for: {company_short_name}/{user_identifier} settled in {int(time.time() - start_time)} sec.")
|
|
140
215
|
except Exception as e:
|
|
141
|
-
logging.exception(f"Error
|
|
216
|
+
logging.exception(f"Error in finalize_context_rebuild for {company_short_name}: {e}")
|
|
142
217
|
raise e
|
|
218
|
+
finally:
|
|
219
|
+
# release the lock
|
|
220
|
+
self.session_context.release_lock(lock_key)
|
|
221
|
+
|
|
222
|
+
return {'response_id': response_id }
|
|
143
223
|
|
|
144
224
|
def llm_query(self,
|
|
145
225
|
company_short_name: str,
|
|
146
|
-
|
|
147
|
-
local_user_id: int = 0,
|
|
226
|
+
user_identifier: str,
|
|
148
227
|
task: Optional[Task] = None,
|
|
149
228
|
prompt_name: str = None,
|
|
150
229
|
question: str = '',
|
|
151
230
|
client_data: dict = {},
|
|
152
|
-
|
|
231
|
+
response_id: str = '',
|
|
232
|
+
files: list = [],
|
|
233
|
+
model: Optional[str] = None) -> dict:
|
|
153
234
|
try:
|
|
154
|
-
user_identifier, is_local_user = self.util.resolve_user_identifier(external_user_id, local_user_id)
|
|
155
|
-
if not user_identifier:
|
|
156
|
-
return {"error": True,
|
|
157
|
-
"error_message": "No se pudo identificar al usuario"}
|
|
158
|
-
|
|
159
235
|
company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
|
|
160
236
|
if not company:
|
|
161
237
|
return {"error": True,
|
|
162
|
-
"error_message":
|
|
238
|
+
"error_message": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
|
|
163
239
|
|
|
164
240
|
if not prompt_name and not question:
|
|
165
241
|
return {"error": True,
|
|
166
|
-
"error_message":
|
|
242
|
+
"error_message": self.i18n_service.t('services.start_query')}
|
|
243
|
+
|
|
244
|
+
# --- Model Resolution ---
|
|
245
|
+
# Priority: 1. Explicit model -> 2. Company config -> 3. Global default
|
|
246
|
+
effective_model = model
|
|
247
|
+
if not effective_model:
|
|
248
|
+
llm_config = self.configuration_service.get_configuration(company_short_name, 'llm')
|
|
249
|
+
if llm_config and llm_config.get('model'):
|
|
250
|
+
effective_model = llm_config['model']
|
|
251
|
+
|
|
252
|
+
effective_model = effective_model or self.default_model
|
|
167
253
|
|
|
168
254
|
# get the previous response_id and context history
|
|
169
255
|
previous_response_id = None
|
|
170
256
|
context_history = self.session_context.get_context_history(company.short_name, user_identifier) or []
|
|
171
257
|
|
|
172
|
-
if self.util.is_openai_model(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
258
|
+
if self.util.is_openai_model(effective_model):
|
|
259
|
+
if response_id:
|
|
260
|
+
# context is getting from this response_id
|
|
261
|
+
previous_response_id = response_id
|
|
262
|
+
else:
|
|
263
|
+
# use the full user history context
|
|
264
|
+
previous_response_id = self.session_context.get_last_response_id(company.short_name, user_identifier)
|
|
178
265
|
if not previous_response_id:
|
|
179
266
|
return {'error': True,
|
|
180
|
-
"error_message":
|
|
267
|
+
"error_message": self.i18n_service.t('errors.services.missing_response_id', company_short_name=company.short_name, user_identifier=user_identifier)
|
|
181
268
|
}
|
|
182
|
-
elif self.util.is_gemini_model(
|
|
269
|
+
elif self.util.is_gemini_model(effective_model):
|
|
183
270
|
# check the length of the context_history and remove old messages
|
|
184
271
|
self._trim_context_history(context_history)
|
|
185
272
|
|
|
186
|
-
# get the user data from the session context
|
|
187
|
-
|
|
273
|
+
# get the user profile data from the session context
|
|
274
|
+
user_profile = self.profile_service.get_profile_by_identifier(company.short_name, user_identifier)
|
|
188
275
|
|
|
189
|
-
#
|
|
190
|
-
final_client_data = (
|
|
276
|
+
# combine client_data with user_profile
|
|
277
|
+
final_client_data = (user_profile or {}).copy()
|
|
191
278
|
final_client_data.update(client_data)
|
|
192
|
-
final_client_data['user_id'] = user_identifier
|
|
193
279
|
|
|
194
280
|
# Load attached files into the context
|
|
195
281
|
files_context = self.load_files_for_context(files)
|
|
@@ -207,7 +293,7 @@ class QueryService:
|
|
|
207
293
|
template_string=prompt_content,
|
|
208
294
|
question=question,
|
|
209
295
|
client_data=final_client_data,
|
|
210
|
-
|
|
296
|
+
user_identifier=user_identifier,
|
|
211
297
|
company=company,
|
|
212
298
|
)
|
|
213
299
|
|
|
@@ -219,7 +305,7 @@ class QueryService:
|
|
|
219
305
|
user_turn_prompt += f'\n### Contexto Adicional: El usuario ha aportado este contexto puede ayudar: {question}'
|
|
220
306
|
|
|
221
307
|
# add to the history context
|
|
222
|
-
if self.util.is_gemini_model(
|
|
308
|
+
if self.util.is_gemini_model(effective_model):
|
|
223
309
|
context_history.append({"role": "user", "content": user_turn_prompt})
|
|
224
310
|
|
|
225
311
|
# service list for the function calls
|
|
@@ -232,8 +318,9 @@ class QueryService:
|
|
|
232
318
|
response = self.llm_client.invoke(
|
|
233
319
|
company=company,
|
|
234
320
|
user_identifier=user_identifier,
|
|
321
|
+
model=effective_model,
|
|
235
322
|
previous_response_id=previous_response_id,
|
|
236
|
-
context_history=context_history if self.util.is_gemini_model(
|
|
323
|
+
context_history=context_history if self.util.is_gemini_model(effective_model) else None,
|
|
237
324
|
question=question,
|
|
238
325
|
context=user_turn_prompt,
|
|
239
326
|
tools=tools,
|
|
@@ -246,7 +333,7 @@ class QueryService:
|
|
|
246
333
|
# save last_response_id for the history chain
|
|
247
334
|
if "response_id" in response:
|
|
248
335
|
self.session_context.save_last_response_id(company.short_name, user_identifier, response["response_id"])
|
|
249
|
-
if self.util.is_gemini_model(
|
|
336
|
+
if self.util.is_gemini_model(effective_model):
|
|
250
337
|
self.session_context.save_context_history(company.short_name, user_identifier, context_history)
|
|
251
338
|
|
|
252
339
|
return response
|
|
@@ -254,6 +341,31 @@ class QueryService:
|
|
|
254
341
|
logging.exception(e)
|
|
255
342
|
return {'error': True, "error_message": f"{str(e)}"}
|
|
256
343
|
|
|
344
|
+
def _compute_context_version_from_string(self, final_system_context: str) -> str:
|
|
345
|
+
# returns a hash of the context string
|
|
346
|
+
try:
|
|
347
|
+
return hashlib.sha256(final_system_context.encode("utf-8")).hexdigest()
|
|
348
|
+
except Exception:
|
|
349
|
+
return "unknown"
|
|
350
|
+
|
|
351
|
+
def _has_valid_cached_context(self, company_short_name: str, user_identifier: str) -> bool:
|
|
352
|
+
"""
|
|
353
|
+
Verifica si existe un estado de contexto reutilizable en sesión.
|
|
354
|
+
- OpenAI: last_response_id presente.
|
|
355
|
+
- Gemini: context_history con al menos 1 mensaje.
|
|
356
|
+
"""
|
|
357
|
+
try:
|
|
358
|
+
if self.util.is_openai_model(self.default_model):
|
|
359
|
+
prev_id = self.session_context.get_last_response_id(company_short_name, user_identifier)
|
|
360
|
+
return bool(prev_id)
|
|
361
|
+
if self.util.is_gemini_model(self.default_model):
|
|
362
|
+
history = self.session_context.get_context_history(company_short_name, user_identifier) or []
|
|
363
|
+
return len(history) >= 1
|
|
364
|
+
return False
|
|
365
|
+
except Exception as e:
|
|
366
|
+
logging.warning(f"error verifying context cache: {e}")
|
|
367
|
+
return False
|
|
368
|
+
|
|
257
369
|
def load_files_for_context(self, files: list) -> str:
|
|
258
370
|
"""
|
|
259
371
|
Processes a list of attached files, decodes their content,
|
|
@@ -310,7 +422,7 @@ class QueryService:
|
|
|
310
422
|
try:
|
|
311
423
|
total_tokens = sum(self.llm_client.count_tokens(json.dumps(message)) for message in context_history)
|
|
312
424
|
except Exception as e:
|
|
313
|
-
logging.error(f"
|
|
425
|
+
logging.error(f"error counting tokens for history: {e}.")
|
|
314
426
|
return
|
|
315
427
|
|
|
316
428
|
# Si se excede el límite, eliminar mensajes antiguos (empezando por el segundo)
|
|
@@ -321,8 +433,8 @@ class QueryService:
|
|
|
321
433
|
removed_tokens = self.llm_client.count_tokens(json.dumps(removed_message))
|
|
322
434
|
total_tokens -= removed_tokens
|
|
323
435
|
logging.warning(
|
|
324
|
-
f"
|
|
325
|
-
f"
|
|
436
|
+
f"history tokens ({total_tokens + removed_tokens} tokens) exceed the limit of: {GEMINI_MAX_TOKENS_CONTEXT_HISTORY}. "
|
|
437
|
+
f"new context: {total_tokens} tokens."
|
|
326
438
|
)
|
|
327
439
|
except IndexError:
|
|
328
440
|
# 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_short_name=company_short_name,
|
|
38
45
|
query_text=query,
|
|
39
46
|
metadata_filter=metadata_filter)
|
|
40
47
|
|
|
@@ -4,57 +4,89 @@
|
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
7
|
-
|
|
8
7
|
from iatoolkit.common.util import Utility
|
|
8
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
9
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
9
10
|
from sqlalchemy import text
|
|
10
|
-
from injector import inject
|
|
11
|
+
from injector import inject, singleton
|
|
11
12
|
import json
|
|
12
|
-
|
|
13
|
+
import logging
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
@singleton
|
|
15
17
|
class SqlService:
|
|
18
|
+
"""
|
|
19
|
+
Manages database connections and executes SQL statements.
|
|
20
|
+
It maintains a cache of named DatabaseManager instances to avoid reconnecting.
|
|
21
|
+
"""
|
|
22
|
+
|
|
16
23
|
@inject
|
|
17
|
-
def __init__(self,
|
|
24
|
+
def __init__(self,
|
|
25
|
+
util: Utility,
|
|
26
|
+
i18n_service: I18nService):
|
|
18
27
|
self.util = util
|
|
28
|
+
self.i18n_service = i18n_service
|
|
29
|
+
|
|
30
|
+
# Cache for database connections
|
|
31
|
+
self._db_connections: dict[str, DatabaseManager] = {}
|
|
19
32
|
|
|
20
|
-
def
|
|
33
|
+
def register_database(self, db_name: str, db_uri: str):
|
|
34
|
+
"""
|
|
35
|
+
Creates and caches a DatabaseManager instance for a given database name and URI.
|
|
36
|
+
If a database with the same name is already registered, it does nothing.
|
|
21
37
|
"""
|
|
22
|
-
|
|
38
|
+
if db_name in self._db_connections:
|
|
39
|
+
return
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
against the database, and fetches all results. The results are converted
|
|
26
|
-
into a list of dictionaries, where each dictionary represents a row.
|
|
27
|
-
This list is then serialized to a JSON string.
|
|
28
|
-
If an exception occurs during execution, the transaction is rolled back,
|
|
29
|
-
and a custom IAToolkitException is raised.
|
|
41
|
+
logging.debug(f"Registering and creating connection for database: '{db_name}'")
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
# create the database connection and save it on the cache
|
|
44
|
+
db_manager = DatabaseManager(db_uri, register_pgvector=False)
|
|
45
|
+
self._db_connections[db_name] = db_manager
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
def get_database_manager(self, db_name: str) -> DatabaseManager:
|
|
48
|
+
"""
|
|
49
|
+
Retrieves a registered DatabaseManager instance from the cache.
|
|
37
50
|
"""
|
|
38
51
|
try:
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
return self._db_connections[db_name]
|
|
53
|
+
except KeyError:
|
|
54
|
+
logging.error(f"Attempted to access unregistered database: '{db_name}'")
|
|
55
|
+
raise IAToolkitException(
|
|
56
|
+
IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
57
|
+
f"Database '{db_name}' is not registered with the SqlService."
|
|
58
|
+
)
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
def exec_sql(self, database: str, query: str) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Executes a raw SQL statement against a registered database and returns the result as a JSON string.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
# 1. Get the database manager from the cache
|
|
66
|
+
db_manager = self.get_database_manager(database)
|
|
44
67
|
|
|
45
|
-
#
|
|
68
|
+
# 2. Execute the SQL statement
|
|
69
|
+
result = db_manager.get_session().execute(text(query))
|
|
70
|
+
cols = result.keys()
|
|
46
71
|
rows_context = [dict(zip(cols, row)) for row in result.fetchall()]
|
|
47
72
|
|
|
48
|
-
#
|
|
73
|
+
# seialize the result
|
|
49
74
|
sql_result_json = json.dumps(rows_context, default=self.util.serialize)
|
|
50
75
|
|
|
51
76
|
return sql_result_json
|
|
77
|
+
except IAToolkitException:
|
|
78
|
+
# Re-raise exceptions from get_database_manager to preserve the specific error
|
|
79
|
+
raise
|
|
52
80
|
except Exception as e:
|
|
53
|
-
|
|
81
|
+
# Attempt to rollback if a session was active
|
|
82
|
+
db_manager = self._db_connections.get(database)
|
|
83
|
+
if db_manager:
|
|
84
|
+
db_manager.get_session().rollback()
|
|
54
85
|
|
|
55
86
|
error_message = str(e)
|
|
56
87
|
if 'timed out' in str(e):
|
|
57
|
-
error_message = '
|
|
88
|
+
error_message = self.i18n_service.t('errors.timeout')
|
|
58
89
|
|
|
90
|
+
logging.error(f"Error executing SQL statement: {error_message}")
|
|
59
91
|
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
60
92
|
error_message) from e
|
|
@@ -101,7 +101,7 @@ class TaskService:
|
|
|
101
101
|
# call the IA
|
|
102
102
|
response = self.query_service.llm_query(
|
|
103
103
|
task=task,
|
|
104
|
-
|
|
104
|
+
user_identifier='task-monitor',
|
|
105
105
|
company_short_name=task.company.short_name,
|
|
106
106
|
prompt_name=task.task_type.name,
|
|
107
107
|
client_data=task.client_data,
|