iatoolkit 0.71.4__py3-none-any.whl → 1.4.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 +19 -7
- iatoolkit/base_company.py +1 -71
- iatoolkit/cli_commands.py +9 -21
- iatoolkit/common/exceptions.py +2 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +38 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +53 -32
- iatoolkit/common/util.py +17 -12
- iatoolkit/company_registry.py +55 -14
- iatoolkit/{iatoolkit.py → core.py} +102 -72
- iatoolkit/infra/{mail_app.py → brevo_mail_app.py} +15 -37
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
- iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
- iatoolkit/infra/llm_proxy.py +235 -134
- iatoolkit/infra/llm_response.py +5 -0
- iatoolkit/locales/en.yaml +134 -4
- iatoolkit/locales/es.yaml +293 -162
- iatoolkit/repositories/database_manager.py +92 -22
- iatoolkit/repositories/document_repo.py +7 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +36 -22
- iatoolkit/repositories/models.py +86 -95
- iatoolkit/repositories/profile_repo.py +64 -13
- iatoolkit/repositories/vs_repo.py +31 -28
- iatoolkit/services/auth_service.py +1 -1
- iatoolkit/services/branding_service.py +1 -1
- iatoolkit/services/company_context_service.py +96 -39
- iatoolkit/services/configuration_service.py +329 -67
- iatoolkit/services/dispatcher_service.py +51 -227
- iatoolkit/services/document_service.py +10 -1
- iatoolkit/services/embedding_service.py +9 -6
- iatoolkit/services/excel_service.py +50 -2
- iatoolkit/services/file_processor_service.py +0 -5
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/jwt_service.py +1 -1
- iatoolkit/services/knowledge_base_service.py +412 -0
- iatoolkit/services/language_service.py +8 -2
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +42 -29
- iatoolkit/services/load_documents_service.py +18 -47
- iatoolkit/services/mail_service.py +171 -25
- iatoolkit/services/profile_service.py +69 -36
- iatoolkit/services/{prompt_manager_service.py → prompt_service.py} +136 -25
- iatoolkit/services/query_service.py +229 -203
- iatoolkit/services/sql_service.py +116 -34
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +18 -6
- iatoolkit/services/user_session_context_service.py +121 -51
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +1 -1
- iatoolkit/static/js/chat_help_content.js +4 -4
- iatoolkit/static/js/chat_main.js +61 -9
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +1 -1
- iatoolkit/static/js/chat_reload_button.js +4 -1
- iatoolkit/static/styles/chat_iatoolkit.css +59 -3
- iatoolkit/static/styles/chat_public.css +28 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +223 -7
- iatoolkit/static/styles/llm_output.css +34 -1
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +28 -3
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +30 -5
- iatoolkit/templates/_login_widget.html +3 -3
- iatoolkit/templates/base.html +13 -0
- iatoolkit/templates/chat.html +45 -3
- iatoolkit/templates/forgot_password.html +3 -2
- iatoolkit/templates/onboarding_shell.html +1 -2
- iatoolkit/templates/signup.html +3 -0
- iatoolkit/views/base_login_view.py +8 -3
- iatoolkit/views/change_password_view.py +1 -1
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/forgot_password_view.py +9 -4
- iatoolkit/views/history_api_view.py +3 -3
- iatoolkit/views/home_view.py +4 -2
- iatoolkit/views/init_context_api_view.py +1 -1
- iatoolkit/views/llmquery_api_view.py +4 -3
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/{file_store_api_view.py → load_document_api_view.py} +15 -11
- iatoolkit/views/login_view.py +25 -8
- iatoolkit/views/logout_api_view.py +10 -2
- iatoolkit/views/prompt_api_view.py +1 -1
- iatoolkit/views/rag_api_view.py +216 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +12 -4
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +1 -1
- iatoolkit-1.4.2.dist-info/METADATA +268 -0
- iatoolkit-1.4.2.dist-info/RECORD +133 -0
- iatoolkit-1.4.2.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- iatoolkit/repositories/tasks_repo.py +0 -52
- iatoolkit/services/history_service.py +0 -37
- iatoolkit/services/search_service.py +0 -55
- iatoolkit/services/tasks_service.py +0 -188
- iatoolkit/templates/about.html +0 -13
- iatoolkit/templates/index.html +0 -145
- iatoolkit/templates/login_simulation.html +0 -45
- iatoolkit/views/external_login_view.py +0 -73
- iatoolkit/views/index_view.py +0 -14
- iatoolkit/views/login_simulation_view.py +0 -93
- iatoolkit/views/tasks_api_view.py +0 -72
- iatoolkit/views/tasks_review_api_view.py +0 -55
- iatoolkit-0.71.4.dist-info/METADATA +0 -276
- iatoolkit-0.71.4.dist-info/RECORD +0 -122
- {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/WHEEL +0 -0
- {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/top_level.txt +0 -0
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
#
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from iatoolkit.
|
|
6
|
+
from iatoolkit.services.llm_client_service import llmClient
|
|
7
7
|
from iatoolkit.services.profile_service import ProfileService
|
|
8
|
-
from iatoolkit.repositories.document_repo import DocumentRepo
|
|
9
8
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
|
+
from iatoolkit.services.tool_service import ToolService
|
|
10
10
|
from iatoolkit.services.document_service import DocumentService
|
|
11
11
|
from iatoolkit.services.company_context_service import CompanyContextService
|
|
12
12
|
from iatoolkit.services.i18n_service import I18nService
|
|
13
13
|
from iatoolkit.services.configuration_service import ConfigurationService
|
|
14
|
-
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
15
|
-
from iatoolkit.repositories.models import Task
|
|
16
14
|
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
17
|
-
from iatoolkit.services.
|
|
15
|
+
from iatoolkit.services.prompt_service import PromptService
|
|
18
16
|
from iatoolkit.services.user_session_context_service import UserSessionContextService
|
|
17
|
+
from iatoolkit.services.history_manager_service import HistoryManagerService
|
|
18
|
+
from iatoolkit.common.model_registry import ModelRegistry
|
|
19
19
|
from iatoolkit.common.util import Utility
|
|
20
|
-
from iatoolkit.common.exceptions import IAToolkitException
|
|
21
20
|
from injector import inject
|
|
22
21
|
import base64
|
|
23
22
|
import logging
|
|
@@ -25,34 +24,42 @@ from typing import Optional
|
|
|
25
24
|
import json
|
|
26
25
|
import time
|
|
27
26
|
import hashlib
|
|
28
|
-
import
|
|
27
|
+
from dataclasses import dataclass
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
@dataclass
|
|
31
|
+
class HistoryHandle:
|
|
32
|
+
"""Encapsulates the state needed to manage history for a single turn."""
|
|
33
|
+
company_short_name: str
|
|
34
|
+
user_identifier: str
|
|
35
|
+
type: str
|
|
36
|
+
model: str | None = None
|
|
37
|
+
request_params: dict = None
|
|
38
|
+
|
|
32
39
|
|
|
33
40
|
class QueryService:
|
|
34
41
|
@inject
|
|
35
42
|
def __init__(self,
|
|
43
|
+
dispatcher: Dispatcher,
|
|
44
|
+
tool_service: ToolService,
|
|
36
45
|
llm_client: llmClient,
|
|
37
46
|
profile_service: ProfileService,
|
|
38
47
|
company_context_service: CompanyContextService,
|
|
39
48
|
document_service: DocumentService,
|
|
40
|
-
document_repo: DocumentRepo,
|
|
41
|
-
llmquery_repo: LLMQueryRepo,
|
|
42
49
|
profile_repo: ProfileRepo,
|
|
43
50
|
prompt_service: PromptService,
|
|
44
51
|
i18n_service: I18nService,
|
|
45
|
-
util: Utility,
|
|
46
|
-
dispatcher: Dispatcher,
|
|
47
52
|
session_context: UserSessionContextService,
|
|
48
|
-
configuration_service: ConfigurationService
|
|
53
|
+
configuration_service: ConfigurationService,
|
|
54
|
+
history_manager: HistoryManagerService,
|
|
55
|
+
util: Utility,
|
|
56
|
+
model_registry: ModelRegistry
|
|
49
57
|
):
|
|
50
58
|
self.profile_service = profile_service
|
|
51
59
|
self.company_context_service = company_context_service
|
|
52
60
|
self.document_service = document_service
|
|
53
|
-
self.document_repo = document_repo
|
|
54
|
-
self.llmquery_repo = llmquery_repo
|
|
55
61
|
self.profile_repo = profile_repo
|
|
62
|
+
self.tool_service = tool_service
|
|
56
63
|
self.prompt_service = prompt_service
|
|
57
64
|
self.i18n_service = i18n_service
|
|
58
65
|
self.util = util
|
|
@@ -60,35 +67,114 @@ class QueryService:
|
|
|
60
67
|
self.session_context = session_context
|
|
61
68
|
self.configuration_service = configuration_service
|
|
62
69
|
self.llm_client = llm_client
|
|
70
|
+
self.history_manager = history_manager
|
|
71
|
+
self.model_registry = model_registry
|
|
63
72
|
|
|
64
|
-
# get the model from the environment variable
|
|
65
|
-
self.default_model = os.getenv("LLM_MODEL", "")
|
|
66
|
-
if not self.default_model:
|
|
67
|
-
raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
|
|
68
|
-
"missing ENV variable 'LLM_MODEL' configuration.")
|
|
69
73
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
def _resolve_model(self, company_short_name: str, model: Optional[str]) -> str:
|
|
75
|
+
# Priority: 1. Explicit model -> 2. Company config
|
|
76
|
+
effective_model = model
|
|
77
|
+
if not effective_model:
|
|
78
|
+
llm_config = self.configuration_service.get_configuration(company_short_name, 'llm')
|
|
79
|
+
if llm_config and llm_config.get('model'):
|
|
80
|
+
effective_model = llm_config['model']
|
|
81
|
+
return effective_model
|
|
82
|
+
|
|
83
|
+
def _get_history_type(self, model: str) -> str:
|
|
84
|
+
history_type_str = self.model_registry.get_history_type(model)
|
|
85
|
+
if history_type_str == "server_side":
|
|
86
|
+
return HistoryManagerService.TYPE_SERVER_SIDE
|
|
87
|
+
else:
|
|
88
|
+
return HistoryManagerService.TYPE_CLIENT_SIDE
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _build_user_facing_prompt(self, company, user_identifier: str,
|
|
92
|
+
client_data: dict, files: list,
|
|
93
|
+
prompt_name: Optional[str], question: str):
|
|
94
|
+
# get the user profile data from the session context
|
|
95
|
+
user_profile = self.profile_service.get_profile_by_identifier(company.short_name, user_identifier)
|
|
96
|
+
|
|
97
|
+
# combine client_data with user_profile
|
|
98
|
+
final_client_data = (user_profile or {}).copy()
|
|
99
|
+
final_client_data.update(client_data)
|
|
100
|
+
|
|
101
|
+
# Load attached files into the context
|
|
102
|
+
files_context = self.load_files_for_context(files)
|
|
103
|
+
|
|
104
|
+
# Initialize prompt_content. It will be an empty string for direct questions.
|
|
105
|
+
main_prompt = ""
|
|
106
|
+
# We use a local variable for the question to avoid modifying the argument reference if it were mutable,
|
|
107
|
+
# although strings are immutable, this keeps the logic clean regarding what 'question' means in each context.
|
|
108
|
+
effective_question = question
|
|
109
|
+
|
|
110
|
+
if prompt_name:
|
|
111
|
+
question_dict = {'prompt': prompt_name, 'data': final_client_data}
|
|
112
|
+
effective_question = json.dumps(question_dict)
|
|
113
|
+
prompt_content = self.prompt_service.get_prompt_content(company, prompt_name)
|
|
114
|
+
|
|
115
|
+
# Render the user requested prompt
|
|
116
|
+
main_prompt = self.util.render_prompt_from_string(
|
|
117
|
+
template_string=prompt_content,
|
|
118
|
+
question=effective_question,
|
|
119
|
+
client_data=final_client_data,
|
|
120
|
+
user_identifier=user_identifier,
|
|
121
|
+
company=company,
|
|
122
|
+
)
|
|
73
123
|
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
124
|
+
# This is the final user-facing prompt for this specific turn
|
|
125
|
+
user_turn_prompt = f"{main_prompt}\n{files_context}"
|
|
126
|
+
if not prompt_name:
|
|
127
|
+
user_turn_prompt += f"\n### La pregunta que debes responder es: {effective_question}"
|
|
128
|
+
else:
|
|
129
|
+
user_turn_prompt += f'\n### Contexto Adicional: El usuario ha aportado este contexto puede ayudar: {effective_question}'
|
|
130
|
+
|
|
131
|
+
return user_turn_prompt, effective_question
|
|
132
|
+
|
|
133
|
+
def _ensure_valid_history(self, company,
|
|
134
|
+
user_identifier: str,
|
|
135
|
+
effective_model: str,
|
|
136
|
+
user_turn_prompt: str,
|
|
137
|
+
ignore_history: bool
|
|
138
|
+
) -> tuple[Optional[HistoryHandle], Optional[dict]]:
|
|
139
|
+
"""
|
|
140
|
+
Manages the history strategy and rebuilds context if necessary.
|
|
141
|
+
Returns: (HistoryHandle, error_response)
|
|
142
|
+
"""
|
|
143
|
+
history_type = self._get_history_type(effective_model)
|
|
77
144
|
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
company_short_name=
|
|
81
|
-
user_identifier=user_identifier
|
|
145
|
+
# Initialize the handle with base context info
|
|
146
|
+
handle = HistoryHandle(
|
|
147
|
+
company_short_name=company.short_name,
|
|
148
|
+
user_identifier=user_identifier,
|
|
149
|
+
type=history_type,
|
|
150
|
+
model=effective_model
|
|
82
151
|
)
|
|
83
152
|
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
user_identifier=user_identifier,
|
|
88
|
-
model=model
|
|
153
|
+
# pass the handle to populate request_params
|
|
154
|
+
needs_rebuild = self.history_manager.populate_request_params(
|
|
155
|
+
handle, user_turn_prompt, ignore_history
|
|
89
156
|
)
|
|
90
157
|
|
|
91
|
-
|
|
158
|
+
if needs_rebuild:
|
|
159
|
+
logging.warning(f"No valid history for {company.short_name}/{user_identifier}. Rebuilding context...")
|
|
160
|
+
|
|
161
|
+
# try to rebuild the context
|
|
162
|
+
self.prepare_context(company_short_name=company.short_name, user_identifier=user_identifier)
|
|
163
|
+
self.set_context_for_llm(company_short_name=company.short_name, user_identifier=user_identifier,
|
|
164
|
+
model=effective_model)
|
|
165
|
+
|
|
166
|
+
# Retry populating params with the same handle
|
|
167
|
+
needs_rebuild = self.history_manager.populate_request_params(
|
|
168
|
+
handle, user_turn_prompt, ignore_history
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if needs_rebuild:
|
|
172
|
+
error_key = 'errors.services.context_rebuild_failed'
|
|
173
|
+
error_message = self.i18n_service.t(error_key, company_short_name=company.short_name,
|
|
174
|
+
user_identifier=user_identifier)
|
|
175
|
+
return None, {'error': True, "error_message": error_message}
|
|
176
|
+
|
|
177
|
+
return handle, None
|
|
92
178
|
|
|
93
179
|
def _build_context_and_profile(self, company_short_name: str, user_identifier: str) -> tuple:
|
|
94
180
|
# this method read the user/company context from the database and renders the system prompt
|
|
@@ -106,7 +192,7 @@ class QueryService:
|
|
|
106
192
|
question=None,
|
|
107
193
|
client_data=user_profile,
|
|
108
194
|
company=company,
|
|
109
|
-
service_list=self.
|
|
195
|
+
service_list=self.tool_service.get_tools_for_llm(company)
|
|
110
196
|
)
|
|
111
197
|
|
|
112
198
|
# get the company context: schemas, database models, .md files
|
|
@@ -117,6 +203,44 @@ class QueryService:
|
|
|
117
203
|
|
|
118
204
|
return final_system_context, user_profile
|
|
119
205
|
|
|
206
|
+
|
|
207
|
+
def init_context(self, company_short_name: str,
|
|
208
|
+
user_identifier: str,
|
|
209
|
+
model: str = None) -> dict:
|
|
210
|
+
"""
|
|
211
|
+
Forces a context rebuild for a given user and (optionally) model.
|
|
212
|
+
|
|
213
|
+
- Clears LLM-related context for the resolved model.
|
|
214
|
+
- Regenerates the static company/user context.
|
|
215
|
+
- Sends the context to the LLM for that model.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
# 1. Resolve the effective model for this user/company
|
|
219
|
+
effective_model = self._resolve_model(company_short_name, model)
|
|
220
|
+
|
|
221
|
+
# 2. Clear only the LLM-related context for this model
|
|
222
|
+
self.session_context.clear_all_context(company_short_name, user_identifier,model=effective_model)
|
|
223
|
+
logging.info(
|
|
224
|
+
f"Context for {company_short_name}/{user_identifier} "
|
|
225
|
+
f"(model={effective_model}) has been cleared."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# 3. Static LLM context is now clean, we can prepare it again (model-agnostic)
|
|
229
|
+
self.prepare_context(
|
|
230
|
+
company_short_name=company_short_name,
|
|
231
|
+
user_identifier=user_identifier
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# 4. Communicate the new context to the specific LLM model
|
|
235
|
+
response = self.set_context_for_llm(
|
|
236
|
+
company_short_name=company_short_name,
|
|
237
|
+
user_identifier=user_identifier,
|
|
238
|
+
model=effective_model
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return response
|
|
242
|
+
|
|
243
|
+
|
|
120
244
|
def prepare_context(self, company_short_name: str, user_identifier: str) -> dict:
|
|
121
245
|
# prepare the context and decide if it needs to be rebuilt
|
|
122
246
|
# save the generated context in the session context for later use
|
|
@@ -134,46 +258,42 @@ class QueryService:
|
|
|
134
258
|
# calculate the context version
|
|
135
259
|
current_version = self._compute_context_version_from_string(final_system_context)
|
|
136
260
|
|
|
261
|
+
# get the current version from the session cache
|
|
137
262
|
try:
|
|
138
263
|
prev_version = self.session_context.get_context_version(company_short_name, user_identifier)
|
|
139
264
|
except Exception:
|
|
140
265
|
prev_version = None
|
|
141
266
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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)
|
|
267
|
+
# Determine if we need to persist the prepared context again.
|
|
268
|
+
# If versions match, we assume the artifact is likely safe, but forcing a save
|
|
269
|
+
# on version mismatch ensures data consistency.
|
|
270
|
+
rebuild_is_needed = (prev_version != current_version)
|
|
151
271
|
|
|
272
|
+
# Save the prepared context and its version for `set_context_for_llm` to use.
|
|
273
|
+
self.session_context.save_prepared_context(company_short_name,
|
|
274
|
+
user_identifier,
|
|
275
|
+
final_system_context,
|
|
276
|
+
current_version)
|
|
152
277
|
return {'rebuild_needed': rebuild_is_needed}
|
|
153
278
|
|
|
154
279
|
def set_context_for_llm(self,
|
|
155
280
|
company_short_name: str,
|
|
156
281
|
user_identifier: str,
|
|
157
282
|
model: str = ''):
|
|
158
|
-
|
|
159
|
-
|
|
283
|
+
"""
|
|
284
|
+
Takes a pre-built static context and sends it to the LLM for the given model.
|
|
285
|
+
Also initializes the model-specific history through HistoryManagerService.
|
|
286
|
+
"""
|
|
160
287
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
161
288
|
if not company:
|
|
162
289
|
logging.error(f"Company not found: {company_short_name} in set_context_for_llm")
|
|
163
290
|
return
|
|
164
291
|
|
|
165
292
|
# --- Model Resolution ---
|
|
166
|
-
|
|
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']
|
|
293
|
+
effective_model = self._resolve_model(company_short_name, model)
|
|
172
294
|
|
|
173
|
-
|
|
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}"
|
|
295
|
+
# Lock per (company, user, model) to avoid concurrent rebuilds for the same model
|
|
296
|
+
lock_key = f"lock:context:{company_short_name}/{user_identifier}/{effective_model}"
|
|
177
297
|
if not self.session_context.acquire_lock(lock_key, expire_seconds=60):
|
|
178
298
|
logging.warning(
|
|
179
299
|
f"try to rebuild context for user {user_identifier} while is still in process, ignored.")
|
|
@@ -181,37 +301,29 @@ class QueryService:
|
|
|
181
301
|
|
|
182
302
|
try:
|
|
183
303
|
start_time = time.time()
|
|
184
|
-
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
185
304
|
|
|
186
305
|
# 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)
|
|
306
|
+
prepared_context, version_to_save = self.session_context.get_and_clear_prepared_context(company_short_name, user_identifier)
|
|
189
307
|
if not prepared_context:
|
|
190
308
|
return
|
|
191
309
|
|
|
192
310
|
logging.info(f"sending context to LLM model {effective_model} for: {company_short_name}/{user_identifier}...")
|
|
193
311
|
|
|
194
|
-
#
|
|
195
|
-
self.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
203
|
-
response_id = self.llm_client.set_company_context(
|
|
204
|
-
company=company,
|
|
205
|
-
company_base_context=prepared_context,
|
|
206
|
-
model=effective_model
|
|
207
|
-
)
|
|
208
|
-
self.session_context.save_last_response_id(company_short_name, user_identifier, response_id)
|
|
312
|
+
# --- Use Strategy Pattern for History/Context Initialization ---
|
|
313
|
+
history_type = self._get_history_type(effective_model)
|
|
314
|
+
response_data = self.history_manager.initialize_context(
|
|
315
|
+
company_short_name, user_identifier, history_type, prepared_context, company, effective_model
|
|
316
|
+
)
|
|
209
317
|
|
|
210
318
|
if version_to_save:
|
|
211
319
|
self.session_context.save_context_version(company_short_name, user_identifier, version_to_save)
|
|
212
320
|
|
|
213
321
|
logging.info(
|
|
214
322
|
f"Context for: {company_short_name}/{user_identifier} settled in {int(time.time() - start_time)} sec.")
|
|
323
|
+
|
|
324
|
+
# Return data (e.g., response_id) if the manager generated any
|
|
325
|
+
return response_data
|
|
326
|
+
|
|
215
327
|
except Exception as e:
|
|
216
328
|
logging.exception(f"Error in finalize_context_rebuild for {company_short_name}: {e}")
|
|
217
329
|
raise e
|
|
@@ -219,18 +331,17 @@ class QueryService:
|
|
|
219
331
|
# release the lock
|
|
220
332
|
self.session_context.release_lock(lock_key)
|
|
221
333
|
|
|
222
|
-
return {'response_id': response_id }
|
|
223
334
|
|
|
224
335
|
def llm_query(self,
|
|
225
336
|
company_short_name: str,
|
|
226
337
|
user_identifier: str,
|
|
227
|
-
|
|
338
|
+
model: Optional[str] = None,
|
|
228
339
|
prompt_name: str = None,
|
|
229
340
|
question: str = '',
|
|
230
341
|
client_data: dict = {},
|
|
231
|
-
|
|
232
|
-
files: list = []
|
|
233
|
-
|
|
342
|
+
ignore_history: bool = False,
|
|
343
|
+
files: list = []
|
|
344
|
+
) -> dict:
|
|
234
345
|
try:
|
|
235
346
|
company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
|
|
236
347
|
if not company:
|
|
@@ -242,86 +353,48 @@ class QueryService:
|
|
|
242
353
|
"error_message": self.i18n_service.t('services.start_query')}
|
|
243
354
|
|
|
244
355
|
# --- Model Resolution ---
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
# check the length of the context_history and remove old messages
|
|
271
|
-
self._trim_context_history(context_history)
|
|
272
|
-
|
|
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)
|
|
275
|
-
|
|
276
|
-
# combine client_data with user_profile
|
|
277
|
-
final_client_data = (user_profile or {}).copy()
|
|
278
|
-
final_client_data.update(client_data)
|
|
279
|
-
|
|
280
|
-
# Load attached files into the context
|
|
281
|
-
files_context = self.load_files_for_context(files)
|
|
282
|
-
|
|
283
|
-
# Initialize prompt_content. It will be an empty string for direct questions.
|
|
284
|
-
main_prompt = ""
|
|
285
|
-
if prompt_name:
|
|
286
|
-
# For task-based queries, wrap data into a JSON string and get the specific prompt template
|
|
287
|
-
question_dict = {'prompt': prompt_name, 'data': final_client_data }
|
|
288
|
-
question = json.dumps(question_dict)
|
|
289
|
-
prompt_content = self.prompt_service.get_prompt_content(company, prompt_name)
|
|
290
|
-
|
|
291
|
-
# Render the main user prompt using the appropriate template (or an empty one)
|
|
292
|
-
main_prompt = self.util.render_prompt_from_string(
|
|
293
|
-
template_string=prompt_content,
|
|
294
|
-
question=question,
|
|
295
|
-
client_data=final_client_data,
|
|
296
|
-
user_identifier=user_identifier,
|
|
297
|
-
company=company,
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
# This is the final user-facing prompt for this specific turn
|
|
301
|
-
user_turn_prompt = f"{main_prompt}\n{files_context}"
|
|
302
|
-
if not prompt_name:
|
|
303
|
-
user_turn_prompt += f"\n### La pregunta que debes responder es: {question}"
|
|
304
|
-
else:
|
|
305
|
-
user_turn_prompt += f'\n### Contexto Adicional: El usuario ha aportado este contexto puede ayudar: {question}'
|
|
306
|
-
|
|
307
|
-
# add to the history context
|
|
308
|
-
if self.util.is_gemini_model(effective_model):
|
|
309
|
-
context_history.append({"role": "user", "content": user_turn_prompt})
|
|
310
|
-
|
|
311
|
-
# service list for the function calls
|
|
312
|
-
tools = self.dispatcher.get_company_services(company)
|
|
356
|
+
effective_model = self._resolve_model(company_short_name, model)
|
|
357
|
+
|
|
358
|
+
# --- Build User-Facing Prompt ---
|
|
359
|
+
user_turn_prompt, effective_question = self._build_user_facing_prompt(
|
|
360
|
+
company=company,
|
|
361
|
+
user_identifier=user_identifier,
|
|
362
|
+
client_data=client_data,
|
|
363
|
+
files=files,
|
|
364
|
+
prompt_name=prompt_name,
|
|
365
|
+
question=question
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# --- History Management (Strategy Pattern) ---
|
|
369
|
+
history_handle, error_response = self._ensure_valid_history(
|
|
370
|
+
company=company,
|
|
371
|
+
user_identifier=user_identifier,
|
|
372
|
+
effective_model=effective_model,
|
|
373
|
+
user_turn_prompt=user_turn_prompt,
|
|
374
|
+
ignore_history=ignore_history
|
|
375
|
+
)
|
|
376
|
+
if error_response:
|
|
377
|
+
return error_response
|
|
378
|
+
|
|
379
|
+
# get the tools availables for this company
|
|
380
|
+
tools = self.tool_service.get_tools_for_llm(company)
|
|
313
381
|
|
|
314
382
|
# openai structured output instructions
|
|
315
383
|
output_schema = {}
|
|
316
384
|
|
|
385
|
+
# Safely extract parameters for invoke using the handle
|
|
386
|
+
# The handle is guaranteed to have request_params populated if no error returned
|
|
387
|
+
previous_response_id = history_handle.request_params.get('previous_response_id')
|
|
388
|
+
context_history = history_handle.request_params.get('context_history')
|
|
389
|
+
|
|
317
390
|
# Now send the instructions to the llm
|
|
318
391
|
response = self.llm_client.invoke(
|
|
319
392
|
company=company,
|
|
320
393
|
user_identifier=user_identifier,
|
|
321
394
|
model=effective_model,
|
|
322
395
|
previous_response_id=previous_response_id,
|
|
323
|
-
context_history=context_history
|
|
324
|
-
question=
|
|
396
|
+
context_history=context_history,
|
|
397
|
+
question=effective_question,
|
|
325
398
|
context=user_turn_prompt,
|
|
326
399
|
tools=tools,
|
|
327
400
|
text=output_schema
|
|
@@ -330,11 +403,10 @@ class QueryService:
|
|
|
330
403
|
if not response.get('valid_response'):
|
|
331
404
|
response['error'] = True
|
|
332
405
|
|
|
333
|
-
# save
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
self.session_context.save_context_history(company.short_name, user_identifier, context_history)
|
|
406
|
+
# save history using the manager passing the handle
|
|
407
|
+
self.history_manager.update_history(
|
|
408
|
+
history_handle, user_turn_prompt, response
|
|
409
|
+
)
|
|
338
410
|
|
|
339
411
|
return response
|
|
340
412
|
except Exception as e:
|
|
@@ -348,23 +420,6 @@ class QueryService:
|
|
|
348
420
|
except Exception:
|
|
349
421
|
return "unknown"
|
|
350
422
|
|
|
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
423
|
|
|
369
424
|
def load_files_for_context(self, files: list) -> str:
|
|
370
425
|
"""
|
|
@@ -381,7 +436,7 @@ class QueryService:
|
|
|
381
436
|
"""
|
|
382
437
|
for document in files:
|
|
383
438
|
# Support both 'file_id' and 'filename' for robustness
|
|
384
|
-
filename = document.get('file_id') or document.get('filename')
|
|
439
|
+
filename = document.get('file_id') or document.get('filename') or document.get('name')
|
|
385
440
|
if not filename:
|
|
386
441
|
context += "\n<error>Documento adjunto sin nombre ignorado.</error>\n"
|
|
387
442
|
continue
|
|
@@ -410,32 +465,3 @@ class QueryService:
|
|
|
410
465
|
|
|
411
466
|
return context
|
|
412
467
|
|
|
413
|
-
def _trim_context_history(self, context_history: list):
|
|
414
|
-
"""
|
|
415
|
-
Verifica el tamaño del historial de contexto y elimina los mensajes más antiguos
|
|
416
|
-
si supera un umbral, conservando siempre el mensaje del sistema (índice 0).
|
|
417
|
-
"""
|
|
418
|
-
if not context_history or len(context_history) <= 1:
|
|
419
|
-
return # nothing to remember
|
|
420
|
-
|
|
421
|
-
# calculate total tokens
|
|
422
|
-
try:
|
|
423
|
-
total_tokens = sum(self.llm_client.count_tokens(json.dumps(message)) for message in context_history)
|
|
424
|
-
except Exception as e:
|
|
425
|
-
logging.error(f"error counting tokens for history: {e}.")
|
|
426
|
-
return
|
|
427
|
-
|
|
428
|
-
# Si se excede el límite, eliminar mensajes antiguos (empezando por el segundo)
|
|
429
|
-
while total_tokens > GEMINI_MAX_TOKENS_CONTEXT_HISTORY and len(context_history) > 1:
|
|
430
|
-
try:
|
|
431
|
-
# Eliminar el mensaje más antiguo después del prompt del sistema
|
|
432
|
-
removed_message = context_history.pop(1)
|
|
433
|
-
removed_tokens = self.llm_client.count_tokens(json.dumps(removed_message))
|
|
434
|
-
total_tokens -= removed_tokens
|
|
435
|
-
logging.warning(
|
|
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."
|
|
438
|
-
)
|
|
439
|
-
except IndexError:
|
|
440
|
-
# Se produce si solo queda el mensaje del sistema, el bucle debería detenerse.
|
|
441
|
-
break
|