iatoolkit 0.3.9__py3-none-any.whl → 0.107.4__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 +27 -35
- iatoolkit/base_company.py +3 -35
- iatoolkit/cli_commands.py +18 -47
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +48 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +39 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +138 -0
- iatoolkit/common/session_manager.py +26 -0
- iatoolkit/common/util.py +353 -0
- iatoolkit/company_registry.py +66 -29
- iatoolkit/core.py +514 -0
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/brevo_mail_app.py +123 -0
- iatoolkit/infra/call_service.py +140 -0
- iatoolkit/infra/connectors/__init__.py +5 -0
- iatoolkit/infra/connectors/file_connector.py +17 -0
- iatoolkit/infra/connectors/file_connector_factory.py +57 -0
- iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
- iatoolkit/infra/connectors/google_drive_connector.py +68 -0
- iatoolkit/infra/connectors/local_file_connector.py +46 -0
- iatoolkit/infra/connectors/s3_connector.py +33 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
- iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
- iatoolkit/infra/llm_proxy.py +268 -0
- iatoolkit/infra/llm_response.py +45 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +222 -0
- iatoolkit/locales/es.yaml +225 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +187 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +105 -0
- iatoolkit/repositories/models.py +279 -0
- iatoolkit/repositories/profile_repo.py +171 -0
- iatoolkit/repositories/vs_repo.py +150 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +7 -7
- iatoolkit/services/branding_service.py +153 -0
- iatoolkit/services/company_context_service.py +214 -0
- iatoolkit/services/configuration_service.py +375 -0
- iatoolkit/services/dispatcher_service.py +134 -0
- {services → iatoolkit/services}/document_service.py +20 -8
- iatoolkit/services/embedding_service.py +148 -0
- iatoolkit/services/excel_service.py +156 -0
- {services → iatoolkit/services}/file_processor_service.py +36 -21
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +80 -0
- iatoolkit/services/language_service.py +89 -0
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/services/llm_client_service.py +438 -0
- iatoolkit/services/load_documents_service.py +174 -0
- iatoolkit/services/mail_service.py +213 -0
- {services → iatoolkit/services}/profile_service.py +200 -101
- iatoolkit/services/prompt_service.py +303 -0
- iatoolkit/services/query_service.py +467 -0
- iatoolkit/services/search_service.py +55 -0
- iatoolkit/services/sql_service.py +169 -0
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +117 -0
- iatoolkit/services/user_session_context_service.py +213 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_filepond.js +85 -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 +401 -0
- iatoolkit/static/js/chat_model_selector.js +227 -0
- 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 +38 -0
- iatoolkit/static/styles/chat_iatoolkit.css +559 -0
- iatoolkit/static/styles/chat_modal.css +133 -0
- iatoolkit/static/styles/chat_public.css +135 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +398 -0
- iatoolkit/static/styles/llm_output.css +148 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +30 -23
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +45 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +78 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +337 -0
- iatoolkit/templates/chat_modals.html +185 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +51 -0
- iatoolkit/templates/onboarding_shell.html +106 -0
- iatoolkit/templates/signup.html +79 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +96 -0
- iatoolkit/views/change_password_view.py +116 -0
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +75 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +63 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +59 -0
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/load_document_api_view.py +65 -0
- iatoolkit/views/login_view.py +170 -0
- iatoolkit/views/logout_api_view.py +57 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +100 -0
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +60 -0
- iatoolkit-0.107.4.dist-info/METADATA +268 -0
- iatoolkit-0.107.4.dist-info/RECORD +132 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
- iatoolkit/iatoolkit.py +0 -413
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.3.9.dist-info/METADATA +0 -252
- iatoolkit-0.3.9.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/dispatcher_service.py +0 -351
- services/excel_service.py +0 -98
- services/history_service.py +0 -45
- services/jwt_service.py +0 -91
- services/load_documents_service.py +0 -212
- services/mail_service.py +0 -62
- services/prompt_manager_service.py +0 -172
- services/query_service.py +0 -334
- services/search_service.py +0 -32
- services/sql_service.py +0 -42
- services/tasks_service.py +0 -188
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/WHEEL +0 -0
services/query_service.py
DELETED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Producto: IAToolkit
|
|
3
|
-
# Todos los derechos reservados.
|
|
4
|
-
# En trámite de registro en el Registro de Propiedad Intelectual de Chile.
|
|
5
|
-
|
|
6
|
-
from infra.llm_client import llmClient
|
|
7
|
-
from repositories.document_repo import DocumentRepo
|
|
8
|
-
from repositories.profile_repo import ProfileRepo
|
|
9
|
-
from services.document_service import DocumentService
|
|
10
|
-
from repositories.llm_query_repo import LLMQueryRepo
|
|
11
|
-
from repositories.models import Task
|
|
12
|
-
from services.dispatcher_service import Dispatcher
|
|
13
|
-
from services.prompt_manager_service import PromptService
|
|
14
|
-
from services.user_session_context_service import UserSessionContextService
|
|
15
|
-
from common.util import Utility
|
|
16
|
-
from common.exceptions import IAToolkitException
|
|
17
|
-
from injector import inject
|
|
18
|
-
import base64
|
|
19
|
-
import logging
|
|
20
|
-
from typing import Optional, TYPE_CHECKING
|
|
21
|
-
import json
|
|
22
|
-
import time
|
|
23
|
-
import os
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
GEMINI_MAX_TOKENS_CONTEXT_HISTORY = 200000
|
|
27
|
-
|
|
28
|
-
class QueryService:
|
|
29
|
-
@inject
|
|
30
|
-
def __init__(self,
|
|
31
|
-
llm_client: llmClient,
|
|
32
|
-
document_service: DocumentService,
|
|
33
|
-
document_repo: DocumentRepo,
|
|
34
|
-
llmquery_repo: LLMQueryRepo,
|
|
35
|
-
profile_repo: ProfileRepo,
|
|
36
|
-
prompt_service: PromptService,
|
|
37
|
-
util: Utility,
|
|
38
|
-
dispatcher: Dispatcher,
|
|
39
|
-
session_context: UserSessionContextService
|
|
40
|
-
):
|
|
41
|
-
self.document_service = document_service
|
|
42
|
-
self.document_repo = document_repo
|
|
43
|
-
self.llmquery_repo = llmquery_repo
|
|
44
|
-
self.profile_repo = profile_repo
|
|
45
|
-
self.prompt_service = prompt_service
|
|
46
|
-
self.util = util
|
|
47
|
-
self.dispatcher = dispatcher
|
|
48
|
-
self.session_context = session_context
|
|
49
|
-
self.llm_client = llm_client
|
|
50
|
-
|
|
51
|
-
# Obtener el modelo de las variables de entorno
|
|
52
|
-
self.model = os.getenv("LLM_MODEL", "")
|
|
53
|
-
if not self.model:
|
|
54
|
-
raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
|
|
55
|
-
"La variable de entorno 'LLM_MODEL' no está configurada.")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def llm_init_context(self,
|
|
59
|
-
company_short_name: str,
|
|
60
|
-
external_user_id: str = None,
|
|
61
|
-
local_user_id: int = 0,
|
|
62
|
-
model: str = ''):
|
|
63
|
-
start_time = time.time()
|
|
64
|
-
if not model:
|
|
65
|
-
model = self.model
|
|
66
|
-
|
|
67
|
-
# Validate the user and company
|
|
68
|
-
user_identifier, is_local_user = self.util.resolve_user_identifier(external_user_id, local_user_id)
|
|
69
|
-
if not user_identifier:
|
|
70
|
-
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_USER,
|
|
71
|
-
"No se pudo resolver el identificador del usuario")
|
|
72
|
-
|
|
73
|
-
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
74
|
-
if not company:
|
|
75
|
-
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
|
|
76
|
-
f"Empresa no encontrada: {company_short_name}")
|
|
77
|
-
|
|
78
|
-
logging.info(f"Inicializando contexto para {company_short_name}/{user_identifier} con modelo {model} ...")
|
|
79
|
-
try:
|
|
80
|
-
# 1. clean any previous context for company/user
|
|
81
|
-
self.session_context.clear_all_context(
|
|
82
|
-
company_short_name=company_short_name,
|
|
83
|
-
user_identifier=user_identifier
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
# 2. get dictionary with user information from company DB
|
|
87
|
-
# user roles are read at this point from company db
|
|
88
|
-
user_profile = self.dispatcher.get_user_info(
|
|
89
|
-
company_name=company_short_name,
|
|
90
|
-
user_identifier=user_identifier,
|
|
91
|
-
is_local_user=is_local_user
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
# add the user logged in to the user_info dictionary
|
|
95
|
-
user_profile['user_id'] = user_identifier
|
|
96
|
-
|
|
97
|
-
# save the user information in the session context
|
|
98
|
-
# it's needed for the jinja predefined prompts (filtering)
|
|
99
|
-
self.session_context.save_user_session_data(company_short_name, user_identifier, user_profile)
|
|
100
|
-
|
|
101
|
-
# 3. render the iatoolkit main system prompt with the company/user information
|
|
102
|
-
system_prompt_template = self.prompt_service.get_system_prompt()
|
|
103
|
-
rendered_system_prompt = self.util.render_prompt_from_string(
|
|
104
|
-
template_string=system_prompt_template,
|
|
105
|
-
question=None,
|
|
106
|
-
client_data=user_profile,
|
|
107
|
-
company=company,
|
|
108
|
-
service_list=self.dispatcher.get_company_services(company)
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
# 4. add more company context: schemas, database models, .md files
|
|
112
|
-
company_specific_context = self.dispatcher.get_company_context(company_name=company_short_name)
|
|
113
|
-
|
|
114
|
-
# 5. merge contexts
|
|
115
|
-
final_system_context = f"{company_specific_context}\n{rendered_system_prompt}"
|
|
116
|
-
|
|
117
|
-
if self.util.is_gemini_model(model):
|
|
118
|
-
# save the initial context as `context_history` (list of messages)
|
|
119
|
-
context_history = [{"role": "user", "content": final_system_context}]
|
|
120
|
-
self.session_context.save_context_history(company_short_name, user_identifier, context_history)
|
|
121
|
-
logging.info(f"Contexto inicial para Gemini guardado en sesión")
|
|
122
|
-
return "gemini-context-initialized"
|
|
123
|
-
|
|
124
|
-
elif self.util.is_openai_model(model):
|
|
125
|
-
|
|
126
|
-
# 6. set the company/user context as the initial context for the LLM
|
|
127
|
-
response_id = self.llm_client.set_company_context(
|
|
128
|
-
company=company,
|
|
129
|
-
company_base_context=final_system_context,
|
|
130
|
-
model=model
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# 7. save response_id in the session context
|
|
134
|
-
self.session_context.save_last_response_id(company_short_name, user_identifier, response_id)
|
|
135
|
-
|
|
136
|
-
logging.info(f"Contexto inicial de company '{company_short_name}/{user_identifier}' ha sido establecido en {int(time.time() - start_time)} seg.")
|
|
137
|
-
return response_id
|
|
138
|
-
|
|
139
|
-
except Exception as e:
|
|
140
|
-
logging.exception(f"Error al inicializar el contexto del LLM para {company_short_name}: {e}")
|
|
141
|
-
raise e
|
|
142
|
-
|
|
143
|
-
def llm_query(self,
|
|
144
|
-
company_short_name: str,
|
|
145
|
-
external_user_id: Optional[str] = None,
|
|
146
|
-
local_user_id: int = 0,
|
|
147
|
-
task: Optional[Task] = None,
|
|
148
|
-
prompt_name: str = None,
|
|
149
|
-
question: str = '',
|
|
150
|
-
client_data: dict = {},
|
|
151
|
-
files: list = []) -> dict:
|
|
152
|
-
try:
|
|
153
|
-
user_identifier, is_local_user = self.util.resolve_user_identifier(external_user_id, local_user_id)
|
|
154
|
-
if not user_identifier:
|
|
155
|
-
return {"error": True,
|
|
156
|
-
"error_message": "No se pudo identificar al usuario"}
|
|
157
|
-
|
|
158
|
-
company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
|
|
159
|
-
if not company:
|
|
160
|
-
return {"error": True,
|
|
161
|
-
"error_message": f'No existe Company ID: {company_short_name}'}
|
|
162
|
-
|
|
163
|
-
if not prompt_name and not question:
|
|
164
|
-
return {"error": True,
|
|
165
|
-
"error_message": f'Hola, cual es tu pregunta?'}
|
|
166
|
-
|
|
167
|
-
# get the previous response_id and context history
|
|
168
|
-
previous_response_id = None
|
|
169
|
-
context_history = self.session_context.get_context_history(company.short_name, user_identifier) or []
|
|
170
|
-
|
|
171
|
-
if self.util.is_openai_model(self.model):
|
|
172
|
-
# get user context
|
|
173
|
-
previous_response_id = self.session_context.get_last_response_id(company.short_name, user_identifier)
|
|
174
|
-
if not previous_response_id:
|
|
175
|
-
# try to initialize the company/user context
|
|
176
|
-
previous_response_id = self.llm_init_context(company.short_name, external_user_id, local_user_id)
|
|
177
|
-
if not previous_response_id:
|
|
178
|
-
return {'error': True,
|
|
179
|
-
"error_message": f"FATAL: No se encontró 'previous_response_id' para '{company.short_name}/{user_identifier}'. La conversación no puede continuar."
|
|
180
|
-
}
|
|
181
|
-
elif self.util.is_gemini_model(self.model):
|
|
182
|
-
# check the length of the context_history and remove old messages
|
|
183
|
-
self._trim_context_history(context_history)
|
|
184
|
-
|
|
185
|
-
# get the user data from the session context
|
|
186
|
-
user_info_from_session = self.session_context.get_user_session_data(company.short_name, user_identifier)
|
|
187
|
-
|
|
188
|
-
# Combinar datos: los datos de la tarea/request tienen prioridad sobre los de la sesión
|
|
189
|
-
final_client_data = (user_info_from_session or {}).copy()
|
|
190
|
-
final_client_data.update(client_data)
|
|
191
|
-
final_client_data['user_id'] = user_identifier
|
|
192
|
-
|
|
193
|
-
# Load attached files into the context
|
|
194
|
-
files_context = self.load_files_for_context(files)
|
|
195
|
-
|
|
196
|
-
# Initialize prompt_content. It will be an empty string for direct questions.
|
|
197
|
-
main_prompt = ""
|
|
198
|
-
if prompt_name:
|
|
199
|
-
# For task-based queries, wrap data into a JSON string and get the specific prompt template
|
|
200
|
-
question_dict = {'prompt': prompt_name, 'data': final_client_data }
|
|
201
|
-
question = json.dumps(question_dict)
|
|
202
|
-
prompt_content = self.prompt_service.get_prompt_content(company, prompt_name)
|
|
203
|
-
|
|
204
|
-
# Render the main user prompt using the appropriate template (or an empty one)
|
|
205
|
-
main_prompt = self.util.render_prompt_from_string(
|
|
206
|
-
template_string=prompt_content,
|
|
207
|
-
question=question,
|
|
208
|
-
client_data=final_client_data,
|
|
209
|
-
external_user_id=external_user_id,
|
|
210
|
-
company=company,
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
# client profile
|
|
214
|
-
client_profile = ''
|
|
215
|
-
if final_client_data.get('client_identity'):
|
|
216
|
-
client_profile = f"cliente sobre el cual se esta consultando se identifica como ´client_identity´ y tiene el valor: {final_client_data.get('client_identity')}"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# This is the final user-facing prompt for this specific turn
|
|
220
|
-
user_turn_prompt = f"{main_prompt}\n{client_profile}\n{files_context}"
|
|
221
|
-
if not prompt_name:
|
|
222
|
-
user_turn_prompt += f"\n### La pregunta que debes responder es: {question}"
|
|
223
|
-
else:
|
|
224
|
-
user_turn_prompt += f'\n### Contexto Adicional: El usuario ha aportado este contexto puede ayudar: {question}'
|
|
225
|
-
|
|
226
|
-
# add to the history context
|
|
227
|
-
if self.util.is_gemini_model(self.model):
|
|
228
|
-
context_history.append({"role": "user", "content": user_turn_prompt})
|
|
229
|
-
|
|
230
|
-
# service list for the function calls
|
|
231
|
-
tools = self.dispatcher.get_company_services(company)
|
|
232
|
-
|
|
233
|
-
# openai structured output instructions
|
|
234
|
-
output_schema = {}
|
|
235
|
-
|
|
236
|
-
# Now send the instructions to the llm
|
|
237
|
-
response = self.llm_client.invoke(
|
|
238
|
-
company=company,
|
|
239
|
-
user_identifier=user_identifier,
|
|
240
|
-
previous_response_id=previous_response_id,
|
|
241
|
-
context_history=context_history if self.util.is_gemini_model(self.model) else None,
|
|
242
|
-
question=question,
|
|
243
|
-
context=user_turn_prompt,
|
|
244
|
-
tools=tools,
|
|
245
|
-
text=output_schema
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
if not response.get('valid_response'):
|
|
249
|
-
response['error'] = True
|
|
250
|
-
|
|
251
|
-
# save last_response_id for the history chain
|
|
252
|
-
if "response_id" in response:
|
|
253
|
-
self.session_context.save_last_response_id(company.short_name, user_identifier, response["response_id"])
|
|
254
|
-
if self.util.is_gemini_model(self.model):
|
|
255
|
-
self.session_context.save_context_history(company.short_name, user_identifier, context_history)
|
|
256
|
-
|
|
257
|
-
return response
|
|
258
|
-
except Exception as e:
|
|
259
|
-
logging.exception(e)
|
|
260
|
-
return {'error': True, "error_message": f"{str(e)}"}
|
|
261
|
-
|
|
262
|
-
def load_files_for_context(self, files: list) -> str:
|
|
263
|
-
"""
|
|
264
|
-
Processes a list of attached files, decodes their content,
|
|
265
|
-
and formats them into a string context for the LLM.
|
|
266
|
-
"""
|
|
267
|
-
if not files:
|
|
268
|
-
return ''
|
|
269
|
-
|
|
270
|
-
context = f"""
|
|
271
|
-
A continuación encontraras una lista de documentos adjuntos
|
|
272
|
-
enviados por el usuario que hace la pregunta,
|
|
273
|
-
en total son: {len(files)} documentos adjuntos
|
|
274
|
-
"""
|
|
275
|
-
for document in files:
|
|
276
|
-
# Support both 'file_id' and 'filename' for robustness
|
|
277
|
-
filename = document.get('file_id') or document.get('filename')
|
|
278
|
-
if not filename:
|
|
279
|
-
context += "\n<error>Documento adjunto sin nombre ignorado.</error>\n"
|
|
280
|
-
continue
|
|
281
|
-
|
|
282
|
-
# Support both 'base64' and 'content' for robustness
|
|
283
|
-
base64_content = document.get('base64') or document.get('content')
|
|
284
|
-
|
|
285
|
-
if not base64_content:
|
|
286
|
-
# Handles the case where a file is referenced but no content is provided
|
|
287
|
-
context += f"\n<error>El archivo '{filename}' no fue encontrado y no pudo ser cargado.</error>\n"
|
|
288
|
-
continue
|
|
289
|
-
|
|
290
|
-
try:
|
|
291
|
-
# Ensure content is bytes before decoding
|
|
292
|
-
if isinstance(base64_content, str):
|
|
293
|
-
base64_content = base64_content.encode('utf-8')
|
|
294
|
-
|
|
295
|
-
file_content = base64.b64decode(base64_content)
|
|
296
|
-
document_text = self.document_service.file_to_txt(filename, file_content)
|
|
297
|
-
context += f"\n<document name='{filename}'>\n{document_text}\n</document>\n"
|
|
298
|
-
except Exception as e:
|
|
299
|
-
# Catches errors from b64decode or file_to_txt
|
|
300
|
-
logging.error(f"Failed to process file {filename}: {e}")
|
|
301
|
-
context += f"\n<error>Error al procesar el archivo {filename}: {str(e)}</error>\n"
|
|
302
|
-
continue
|
|
303
|
-
|
|
304
|
-
return context
|
|
305
|
-
|
|
306
|
-
def _trim_context_history(self, context_history: list):
|
|
307
|
-
"""
|
|
308
|
-
Verifica el tamaño del historial de contexto y elimina los mensajes más antiguos
|
|
309
|
-
si supera un umbral, conservando siempre el mensaje del sistema (índice 0).
|
|
310
|
-
"""
|
|
311
|
-
if not context_history or len(context_history) <= 1:
|
|
312
|
-
return # nothing to remember
|
|
313
|
-
|
|
314
|
-
# calculate total tokens
|
|
315
|
-
try:
|
|
316
|
-
total_tokens = sum(self.llm_client.count_tokens(json.dumps(message)) for message in context_history)
|
|
317
|
-
except Exception as e:
|
|
318
|
-
logging.error(f"Error al calcular tokens del historial: {e}. No se pudo recortar el contexto.")
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
# Si se excede el límite, eliminar mensajes antiguos (empezando por el segundo)
|
|
322
|
-
while total_tokens > GEMINI_MAX_TOKENS_CONTEXT_HISTORY and len(context_history) > 1:
|
|
323
|
-
try:
|
|
324
|
-
# Eliminar el mensaje más antiguo después del prompt del sistema
|
|
325
|
-
removed_message = context_history.pop(1)
|
|
326
|
-
removed_tokens = self.llm_client.count_tokens(json.dumps(removed_message))
|
|
327
|
-
total_tokens -= removed_tokens
|
|
328
|
-
logging.warning(
|
|
329
|
-
f"Historial de contexto ({total_tokens + removed_tokens} tokens) excedía el límite de {GEMINI_MAX_TOKENS_CONTEXT_HISTORY}. "
|
|
330
|
-
f"Nuevo total: {total_tokens} tokens."
|
|
331
|
-
)
|
|
332
|
-
except IndexError:
|
|
333
|
-
# Se produce si solo queda el mensaje del sistema, el bucle debería detenerse.
|
|
334
|
-
break
|
services/search_service.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Producto: IAToolkit
|
|
3
|
-
# Todos los derechos reservados.
|
|
4
|
-
# En trámite de registro en el Registro de Propiedad Intelectual de Chile.
|
|
5
|
-
|
|
6
|
-
from repositories.vs_repo import VSRepo
|
|
7
|
-
from repositories.document_repo import DocumentRepo
|
|
8
|
-
from injector import inject
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class SearchService:
|
|
12
|
-
@inject
|
|
13
|
-
def __init__(self,
|
|
14
|
-
doc_repo: DocumentRepo,
|
|
15
|
-
vs_repo: VSRepo):
|
|
16
|
-
super().__init__()
|
|
17
|
-
self.vs_repo = vs_repo
|
|
18
|
-
self.doc_repo = doc_repo
|
|
19
|
-
|
|
20
|
-
def search(self, company_id: int, query: str, metadata_filter: dict = None) -> str:
|
|
21
|
-
document_list = self.vs_repo.query(company_id=company_id,
|
|
22
|
-
query_text=query,
|
|
23
|
-
metadata_filter=metadata_filter)
|
|
24
|
-
|
|
25
|
-
search_context = ''
|
|
26
|
-
for doc in document_list:
|
|
27
|
-
search_context += f'documento "{doc.filename}"'
|
|
28
|
-
if doc.meta and 'document_type' in doc.meta:
|
|
29
|
-
search_context += f' tipo: {doc.meta.get('document_type', '')}'
|
|
30
|
-
search_context += f': {doc.content}\n'
|
|
31
|
-
|
|
32
|
-
return search_context
|
services/sql_service.py
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Producto: IAToolkit
|
|
3
|
-
# Todos los derechos reservados.
|
|
4
|
-
# En trámite de registro en el Registro de Propiedad Intelectual de Chile.
|
|
5
|
-
|
|
6
|
-
from repositories.database_manager import DatabaseManager
|
|
7
|
-
from common.util import Utility
|
|
8
|
-
from sqlalchemy import text
|
|
9
|
-
from injector import inject
|
|
10
|
-
import json
|
|
11
|
-
from common.exceptions import IAToolkitException
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class SqlService:
|
|
15
|
-
@inject
|
|
16
|
-
def __init__(self,util: Utility):
|
|
17
|
-
self.util = util
|
|
18
|
-
|
|
19
|
-
def exec_sql(self, db_manager: DatabaseManager, sql_statement: str) -> str:
|
|
20
|
-
try:
|
|
21
|
-
# here the SQL is executed
|
|
22
|
-
result = db_manager.get_session().execute(text(sql_statement))
|
|
23
|
-
|
|
24
|
-
# get the column names
|
|
25
|
-
cols = result.keys()
|
|
26
|
-
|
|
27
|
-
# convert rows to dict
|
|
28
|
-
rows_context = [dict(zip(cols, row)) for row in result.fetchall()]
|
|
29
|
-
|
|
30
|
-
# Serialize to JSON with type convertion
|
|
31
|
-
sql_result_json = json.dumps(rows_context, default=self.util.serialize)
|
|
32
|
-
|
|
33
|
-
return sql_result_json
|
|
34
|
-
except Exception as e:
|
|
35
|
-
db_manager.get_session().rollback()
|
|
36
|
-
|
|
37
|
-
error_message = str(e)
|
|
38
|
-
if 'timed out' in str(e):
|
|
39
|
-
error_message = 'Intentalo de nuevo, se agoto el tiempo de espera'
|
|
40
|
-
|
|
41
|
-
raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
|
|
42
|
-
error_message) from e
|
services/tasks_service.py
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Producto: IAToolkit
|
|
3
|
-
# Todos los derechos reservados.
|
|
4
|
-
# En trámite de registro en el Registro de Propiedad Intelectual de Chile.
|
|
5
|
-
|
|
6
|
-
from injector import inject
|
|
7
|
-
from repositories.models import Task, TaskStatus
|
|
8
|
-
from services.query_service import QueryService
|
|
9
|
-
from repositories.tasks_repo import TaskRepo
|
|
10
|
-
from repositories.profile_repo import ProfileRepo
|
|
11
|
-
from infra.call_service import CallServiceClient
|
|
12
|
-
from common.exceptions import IAToolkitException
|
|
13
|
-
from datetime import datetime
|
|
14
|
-
from werkzeug.utils import secure_filename
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class TaskService:
|
|
18
|
-
@inject
|
|
19
|
-
def __init__(self,
|
|
20
|
-
task_repo: TaskRepo,
|
|
21
|
-
query_service: QueryService,
|
|
22
|
-
profile_repo: ProfileRepo,
|
|
23
|
-
call_service: CallServiceClient):
|
|
24
|
-
self.task_repo = task_repo
|
|
25
|
-
self.query_service = query_service
|
|
26
|
-
self.profile_repo = profile_repo
|
|
27
|
-
self.call_service = call_service
|
|
28
|
-
|
|
29
|
-
def create_task(self,
|
|
30
|
-
company_short_name: str,
|
|
31
|
-
task_type_name: str,
|
|
32
|
-
client_data: dict,
|
|
33
|
-
company_task_id: int= 0,
|
|
34
|
-
execute_at: datetime = None,
|
|
35
|
-
files: list = []
|
|
36
|
-
) -> Task:
|
|
37
|
-
|
|
38
|
-
# validate company
|
|
39
|
-
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
40
|
-
if not company:
|
|
41
|
-
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
|
|
42
|
-
f'No existe la empresa: {company_short_name}')
|
|
43
|
-
|
|
44
|
-
# validate task_type
|
|
45
|
-
task_type = self.task_repo.get_task_type(task_type_name)
|
|
46
|
-
if not task_type:
|
|
47
|
-
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
|
|
48
|
-
f'No existe el task_type: {task_type_name}')
|
|
49
|
-
|
|
50
|
-
# process the task files
|
|
51
|
-
task_files = self.get_task_files(files)
|
|
52
|
-
|
|
53
|
-
# create Task object
|
|
54
|
-
new_task = Task(
|
|
55
|
-
company_id=company.id,
|
|
56
|
-
task_type_id=task_type.id,
|
|
57
|
-
company_task_id=company_task_id,
|
|
58
|
-
client_data=client_data,
|
|
59
|
-
execute_at=execute_at,
|
|
60
|
-
files=task_files
|
|
61
|
-
)
|
|
62
|
-
new_task = self.task_repo.create_task(new_task)
|
|
63
|
-
if execute_at and execute_at > datetime.now():
|
|
64
|
-
self.execute_task(new_task)
|
|
65
|
-
|
|
66
|
-
return new_task
|
|
67
|
-
|
|
68
|
-
def review_task(self, task_id: int, review_user: str, approved: bool, comment: str):
|
|
69
|
-
# get the task
|
|
70
|
-
task = self.task_repo.get_task_by_id(task_id)
|
|
71
|
-
if not task:
|
|
72
|
-
raise IAToolkitException(IAToolkitException.ErrorType.TASK_NOT_FOUND,
|
|
73
|
-
f'No existe la tarea: {task_id}')
|
|
74
|
-
|
|
75
|
-
if task.status != TaskStatus.ejecutado:
|
|
76
|
-
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_STATE,
|
|
77
|
-
f'La tarea debe estar en estado ejecutada: {task_id}')
|
|
78
|
-
|
|
79
|
-
# update the task
|
|
80
|
-
task.approved = approved
|
|
81
|
-
task.status = TaskStatus.aprobada if approved else TaskStatus.rechazada
|
|
82
|
-
task.review_user = review_user
|
|
83
|
-
task.comment = comment
|
|
84
|
-
task.review_date = datetime.now()
|
|
85
|
-
self.task_repo.update_task(task)
|
|
86
|
-
return task
|
|
87
|
-
|
|
88
|
-
def execute_task(self, task: Task):
|
|
89
|
-
# in this case do nothing
|
|
90
|
-
if (task.status != TaskStatus.pendiente or
|
|
91
|
-
(task.execute_at and task.execute_at > datetime.now())):
|
|
92
|
-
return task
|
|
93
|
-
|
|
94
|
-
# get the Task template prompt
|
|
95
|
-
if not task.task_type.prompt_template:
|
|
96
|
-
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
|
|
97
|
-
f'No existe el prompt_template para el task_type: {task.task_type.name}')
|
|
98
|
-
|
|
99
|
-
template_dir = f'companies/{task.company.short_name}/prompts'
|
|
100
|
-
|
|
101
|
-
# call the IA
|
|
102
|
-
response = self.query_service.llm_query(
|
|
103
|
-
task=task,
|
|
104
|
-
local_user_id=0,
|
|
105
|
-
company_short_name=task.company.short_name,
|
|
106
|
-
prompt_name=task.task_type.name,
|
|
107
|
-
client_data=task.client_data,
|
|
108
|
-
files=task.files
|
|
109
|
-
)
|
|
110
|
-
if 'error' in response:
|
|
111
|
-
raise IAToolkitException(IAToolkitException.ErrorType.LLM_ERROR,
|
|
112
|
-
response.get('error'))
|
|
113
|
-
|
|
114
|
-
# update the Task with the response from llm_query
|
|
115
|
-
task.llm_query_id = response.get('query_id', 0)
|
|
116
|
-
|
|
117
|
-
# update task status
|
|
118
|
-
if not response.get('valid_response'):
|
|
119
|
-
task.status = TaskStatus.fallida
|
|
120
|
-
else:
|
|
121
|
-
task.status = TaskStatus.ejecutado
|
|
122
|
-
self.task_repo.update_task(task)
|
|
123
|
-
|
|
124
|
-
# call the callback url
|
|
125
|
-
if task.callback_url:
|
|
126
|
-
self.notify_callback(task, response)
|
|
127
|
-
|
|
128
|
-
return task
|
|
129
|
-
|
|
130
|
-
def notify_callback(self, task: Task, response: dict):
|
|
131
|
-
response_data = {
|
|
132
|
-
'task_id': task.id,
|
|
133
|
-
'company_task_id': task.company_task_id,
|
|
134
|
-
'status': task.status.name,
|
|
135
|
-
'answer': response.get('answer', ''),
|
|
136
|
-
'additional_data': response.get('additional_data', {}),
|
|
137
|
-
'client_data': task.client_data,
|
|
138
|
-
}
|
|
139
|
-
try:
|
|
140
|
-
response, status_code = self.call_service.post(task.callback_url, response_data)
|
|
141
|
-
except Exception as e:
|
|
142
|
-
raise IAToolkitException(
|
|
143
|
-
IAToolkitException.ErrorType.REQUEST_ERROR,
|
|
144
|
-
f"Error al notificar callback {task.callback_url}: {str(e)}"
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
def get_task_files(self, uploaded_files):
|
|
148
|
-
files_info = []
|
|
149
|
-
|
|
150
|
-
for file in uploaded_files:
|
|
151
|
-
filename = secure_filename(file.filename)
|
|
152
|
-
|
|
153
|
-
try:
|
|
154
|
-
# the file is already in base64
|
|
155
|
-
file_content = file.read().decode('utf-8')
|
|
156
|
-
except Exception as e:
|
|
157
|
-
raise IAToolkitException(
|
|
158
|
-
IAToolkitException.ErrorType.FILE_IO_ERROR,
|
|
159
|
-
f"Error al extraer el contenido del archivo {filename}: {str(e)}"
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
files_info.append({
|
|
163
|
-
'filename': filename,
|
|
164
|
-
'content': file_content, # file in base64
|
|
165
|
-
'type': file.content_type
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
return files_info
|
|
169
|
-
|
|
170
|
-
def trigger_pending_tasks(self, company_short_name: str):
|
|
171
|
-
n_tasks = 0
|
|
172
|
-
try:
|
|
173
|
-
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
174
|
-
pending_tasks = self.task_repo.get_pending_tasks(company.id)
|
|
175
|
-
for task in pending_tasks:
|
|
176
|
-
self.execute_task(task)
|
|
177
|
-
n_tasks += 1
|
|
178
|
-
except Exception as e:
|
|
179
|
-
raise IAToolkitException(
|
|
180
|
-
IAToolkitException.ErrorType.TASK_EXECUTION_ERROR,
|
|
181
|
-
f"Error ejecutando tareas pendientes: {str(e)}"
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
return {'message': f'{n_tasks} tareas ejecutadas.'}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Producto: IAToolkit
|
|
3
|
-
# Todos los derechos reservados.
|
|
4
|
-
# En trámite de registro en el Registro de Propiedad Intelectual de Chile.
|
|
5
|
-
|
|
6
|
-
from repositories.models import UserFeedback
|
|
7
|
-
from injector import inject
|
|
8
|
-
from repositories.profile_repo import ProfileRepo
|
|
9
|
-
from infra.google_chat_app import GoogleChatApp
|
|
10
|
-
import logging
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class UserFeedbackService:
|
|
14
|
-
@inject
|
|
15
|
-
def __init__(self, profile_repo: ProfileRepo, google_chat_app: GoogleChatApp):
|
|
16
|
-
self.profile_repo = profile_repo
|
|
17
|
-
self.google_chat_app = google_chat_app
|
|
18
|
-
|
|
19
|
-
def new_feedback(self,
|
|
20
|
-
company_short_name: str,
|
|
21
|
-
message: str,
|
|
22
|
-
external_user_id: str = None,
|
|
23
|
-
local_user_id: int = 0,
|
|
24
|
-
space: str = None,
|
|
25
|
-
type: str = None,
|
|
26
|
-
rating: int = None) -> dict:
|
|
27
|
-
try:
|
|
28
|
-
# validate company
|
|
29
|
-
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
30
|
-
if not company:
|
|
31
|
-
return {'error': f'No existe la empresa: {company_short_name}'}
|
|
32
|
-
|
|
33
|
-
# send notification to Google Chat
|
|
34
|
-
chat_message = f"*Nuevo feedback de {company_short_name}*:\n*Usuario:* {external_user_id or local_user_id}\n*Mensaje:* {message}\n*Calificación:* {rating}"
|
|
35
|
-
|
|
36
|
-
# TO DO: get the space and type from the input data
|
|
37
|
-
chat_data = {
|
|
38
|
-
"type": type,
|
|
39
|
-
"space": {
|
|
40
|
-
"name": space
|
|
41
|
-
},
|
|
42
|
-
"message": {
|
|
43
|
-
"text": chat_message
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
chat_result = self.google_chat_app.send_message(message_data=chat_data)
|
|
48
|
-
|
|
49
|
-
if not chat_result.get('success'):
|
|
50
|
-
logging.warning(f"Error al enviar notificación a Google Chat: {chat_result.get('message')}")
|
|
51
|
-
|
|
52
|
-
# create the UserFeedback object
|
|
53
|
-
new_feedback = UserFeedback(
|
|
54
|
-
company_id=company.id,
|
|
55
|
-
message=message,
|
|
56
|
-
local_user_id=local_user_id,
|
|
57
|
-
external_user_id=external_user_id,
|
|
58
|
-
rating=rating
|
|
59
|
-
)
|
|
60
|
-
new_feedback = self.profile_repo.save_feedback(new_feedback)
|
|
61
|
-
if not new_feedback:
|
|
62
|
-
return {'error': 'No se pudo guardar el feedback'}
|
|
63
|
-
|
|
64
|
-
return {'message': 'Feedback guardado correctamente'}
|
|
65
|
-
|
|
66
|
-
except Exception as e:
|
|
67
|
-
return {'error': str(e)}
|