iatoolkit 0.4.2__py3-none-any.whl → 0.66.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 +13 -35
- iatoolkit/base_company.py +74 -8
- iatoolkit/cli_commands.py +15 -23
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +46 -0
- iatoolkit/common/routes.py +141 -0
- iatoolkit/common/session_manager.py +24 -0
- iatoolkit/common/util.py +348 -0
- iatoolkit/company_registry.py +7 -8
- iatoolkit/iatoolkit.py +169 -96
- iatoolkit/infra/__init__.py +5 -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/gemini_adapter.py +356 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_client.py +429 -0
- iatoolkit/infra/llm_proxy.py +139 -0
- iatoolkit/infra/llm_response.py +40 -0
- iatoolkit/infra/mail_app.py +145 -0
- iatoolkit/infra/openai_adapter.py +90 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +144 -0
- iatoolkit/locales/es.yaml +140 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +110 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/llm_query_repo.py +91 -0
- iatoolkit/repositories/models.py +336 -0
- iatoolkit/repositories/profile_repo.py +123 -0
- iatoolkit/repositories/tasks_repo.py +52 -0
- iatoolkit/repositories/vs_repo.py +139 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +6 -6
- iatoolkit/services/branding_service.py +149 -0
- {services → iatoolkit/services}/dispatcher_service.py +39 -99
- {services → iatoolkit/services}/document_service.py +5 -5
- {services → iatoolkit/services}/excel_service.py +27 -21
- {services → iatoolkit/services}/file_processor_service.py +5 -5
- iatoolkit/services/help_content_service.py +30 -0
- {services → iatoolkit/services}/history_service.py +8 -16
- iatoolkit/services/i18n_service.py +104 -0
- {services → iatoolkit/services}/jwt_service.py +18 -27
- iatoolkit/services/language_service.py +77 -0
- {services → iatoolkit/services}/load_documents_service.py +19 -14
- {services → iatoolkit/services}/mail_service.py +5 -5
- iatoolkit/services/onboarding_service.py +43 -0
- {services → iatoolkit/services}/profile_service.py +155 -89
- {services → iatoolkit/services}/prompt_manager_service.py +26 -11
- {services → iatoolkit/services}/query_service.py +142 -104
- {services → iatoolkit/services}/search_service.py +21 -5
- {services → iatoolkit/services}/sql_service.py +24 -6
- {services → iatoolkit/services}/tasks_service.py +10 -10
- iatoolkit/services/user_feedback_service.py +103 -0
- iatoolkit/services/user_session_context_service.py +143 -0
- iatoolkit/static/images/fernando.jpeg +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 +112 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +364 -0
- iatoolkit/static/js/chat_onboarding_button.js +97 -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 +592 -0
- iatoolkit/static/styles/chat_modal.css +169 -0
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/llm_output.css +115 -0
- iatoolkit/static/styles/onboarding.css +169 -0
- iatoolkit/system_prompts/query_main.prompt +5 -15
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/about.html +13 -0
- iatoolkit/templates/base.html +65 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +287 -0
- iatoolkit/templates/chat_modals.html +181 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +50 -0
- iatoolkit/templates/index.html +145 -0
- iatoolkit/templates/login_simulation.html +34 -0
- iatoolkit/templates/onboarding_shell.html +104 -0
- iatoolkit/templates/signup.html +76 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +92 -0
- iatoolkit/views/change_password_view.py +117 -0
- iatoolkit/views/external_login_view.py +73 -0
- iatoolkit/views/file_store_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +72 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +61 -0
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +73 -0
- iatoolkit/views/llmquery_api_view.py +57 -0
- iatoolkit/views/login_simulation_view.py +81 -0
- iatoolkit/views/login_view.py +153 -0
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/signup_view.py +94 -0
- iatoolkit/views/tasks_api_view.py +72 -0
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/verify_user_view.py +62 -0
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
- iatoolkit-0.66.2.dist-info/RECORD +119 -0
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -1
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.4.2.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/WHEEL +0 -0
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
from infra.llm_client import llmClient
|
|
7
|
-
from
|
|
8
|
-
from repositories.
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from repositories.
|
|
12
|
-
from
|
|
13
|
-
from services.
|
|
14
|
-
from services.
|
|
15
|
-
from
|
|
16
|
-
from common.
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.infra.llm_client import llmClient
|
|
7
|
+
from iatoolkit.services.profile_service import ProfileService
|
|
8
|
+
from iatoolkit.repositories.document_repo import DocumentRepo
|
|
9
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
10
|
+
from iatoolkit.services.document_service import DocumentService
|
|
11
|
+
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
12
|
+
from iatoolkit.repositories.models import Task
|
|
13
|
+
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
14
|
+
from iatoolkit.services.prompt_manager_service import PromptService
|
|
15
|
+
from iatoolkit.services.user_session_context_service import UserSessionContextService
|
|
16
|
+
from iatoolkit.common.util import Utility
|
|
17
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
17
18
|
from injector import inject
|
|
18
19
|
import base64
|
|
19
20
|
import logging
|
|
20
|
-
from typing import Optional
|
|
21
|
+
from typing import Optional
|
|
21
22
|
import json
|
|
22
23
|
import time
|
|
24
|
+
import hashlib
|
|
23
25
|
import os
|
|
24
26
|
|
|
25
27
|
|
|
@@ -29,6 +31,7 @@ class QueryService:
|
|
|
29
31
|
@inject
|
|
30
32
|
def __init__(self,
|
|
31
33
|
llm_client: llmClient,
|
|
34
|
+
profile_service: ProfileService,
|
|
32
35
|
document_service: DocumentService,
|
|
33
36
|
document_repo: DocumentRepo,
|
|
34
37
|
llmquery_repo: LLMQueryRepo,
|
|
@@ -38,6 +41,7 @@ class QueryService:
|
|
|
38
41
|
dispatcher: Dispatcher,
|
|
39
42
|
session_context: UserSessionContextService
|
|
40
43
|
):
|
|
44
|
+
self.profile_service = profile_service
|
|
41
45
|
self.document_service = document_service
|
|
42
46
|
self.document_repo = document_repo
|
|
43
47
|
self.llmquery_repo = llmquery_repo
|
|
@@ -48,113 +52,132 @@ class QueryService:
|
|
|
48
52
|
self.session_context = session_context
|
|
49
53
|
self.llm_client = llm_client
|
|
50
54
|
|
|
51
|
-
#
|
|
55
|
+
# get the model from the environment variable
|
|
52
56
|
self.model = os.getenv("LLM_MODEL", "")
|
|
53
57
|
if not self.model:
|
|
54
58
|
raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
|
|
55
59
|
"La variable de entorno 'LLM_MODEL' no está configurada.")
|
|
56
60
|
|
|
61
|
+
def _build_context_and_profile(self, company_short_name: str, user_identifier: str) -> tuple:
|
|
62
|
+
# this method read the user/company context from the database and renders the system prompt
|
|
63
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
64
|
+
if not company:
|
|
65
|
+
return None, None
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
# Get the user profile from the single source of truth.
|
|
68
|
+
user_profile = self.profile_service.get_profile_by_identifier(company_short_name, user_identifier)
|
|
69
|
+
|
|
70
|
+
# render the iatoolkit main system prompt with the company/user information
|
|
71
|
+
system_prompt_template = self.prompt_service.get_system_prompt()
|
|
72
|
+
rendered_system_prompt = self.util.render_prompt_from_string(
|
|
73
|
+
template_string=system_prompt_template,
|
|
74
|
+
question=None,
|
|
75
|
+
client_data=user_profile,
|
|
76
|
+
company=company,
|
|
77
|
+
service_list=self.dispatcher.get_company_services(company)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# get the company context: schemas, database models, .md files
|
|
81
|
+
company_specific_context = self.dispatcher.get_company_context(company_name=company_short_name)
|
|
66
82
|
|
|
67
|
-
#
|
|
68
|
-
|
|
83
|
+
# merge context: company + user
|
|
84
|
+
final_system_context = f"{company_specific_context}\n{rendered_system_prompt}"
|
|
85
|
+
|
|
86
|
+
return final_system_context, user_profile
|
|
87
|
+
|
|
88
|
+
def prepare_context(self, company_short_name: str, user_identifier: str) -> dict:
|
|
89
|
+
# prepare the context and decide if it needs to be rebuilt
|
|
90
|
+
# save the generated context in the session context for later use
|
|
69
91
|
if not user_identifier:
|
|
70
|
-
|
|
71
|
-
"No se pudo resolver el identificador del usuario")
|
|
92
|
+
return {'rebuild_needed': True, 'error': 'Invalid user identifier'}
|
|
72
93
|
|
|
73
|
-
company
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
# create the company/user context and compute its version
|
|
95
|
+
final_system_context, user_profile = self._build_context_and_profile(
|
|
96
|
+
company_short_name, user_identifier)
|
|
97
|
+
|
|
98
|
+
# save the user information in the session context
|
|
99
|
+
# it's needed for the jinja predefined prompts (filtering)
|
|
100
|
+
self.session_context.save_profile_data(company_short_name, user_identifier, user_profile)
|
|
101
|
+
|
|
102
|
+
# calculate the context version
|
|
103
|
+
current_version = self._compute_context_version_from_string(final_system_context)
|
|
77
104
|
|
|
78
|
-
logging.info(f"Inicializando contexto para {company_short_name}/{user_identifier} con modelo {model} ...")
|
|
79
105
|
try:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
user_identifier=user_identifier
|
|
84
|
-
)
|
|
106
|
+
prev_version = self.session_context.get_context_version(company_short_name, user_identifier)
|
|
107
|
+
except Exception:
|
|
108
|
+
prev_version = None
|
|
85
109
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
)
|
|
110
|
+
rebuild_is_needed = not (prev_version and prev_version == current_version and
|
|
111
|
+
self._has_valid_cached_context(company_short_name, user_identifier))
|
|
93
112
|
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
if rebuild_is_needed:
|
|
114
|
+
# Guardar el contexto preparado y su versión para que `finalize_context_rebuild` los use.
|
|
115
|
+
self.session_context.save_prepared_context(company_short_name, user_identifier, final_system_context,
|
|
116
|
+
current_version)
|
|
96
117
|
|
|
97
|
-
|
|
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)
|
|
118
|
+
return {'rebuild_needed': rebuild_is_needed}
|
|
100
119
|
|
|
101
|
-
|
|
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
|
-
)
|
|
120
|
+
def finalize_context_rebuild(self, company_short_name: str, user_identifier: str, model: str = ''):
|
|
110
121
|
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
# end the initilization, if there is a prepare context send it to llm
|
|
123
|
+
if not model:
|
|
124
|
+
model = self.model
|
|
113
125
|
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
# --- Lógica de Bloqueo ---
|
|
127
|
+
lock_key = f"lock:context:{company_short_name}/{user_identifier}"
|
|
128
|
+
if not self.session_context.acquire_lock(lock_key, expire_seconds=60):
|
|
129
|
+
logging.warning(
|
|
130
|
+
f"Intento de reconstruir contexto para {user_identifier} mientras ya estaba en progreso. Se omite.")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
start_time = time.time()
|
|
135
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
136
|
+
|
|
137
|
+
# get the prepared context and version from the session cache
|
|
138
|
+
prepared_context, version_to_save = self.session_context.get_and_clear_prepared_context(company_short_name,
|
|
139
|
+
user_identifier)
|
|
140
|
+
if not prepared_context:
|
|
141
|
+
logging.info(
|
|
142
|
+
f"No se requiere reconstrucción de contexto para {company_short_name}/{user_identifier}. Finalización rápida.")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
logging.info(f"Enviando contexto al LLM para {company_short_name}/{user_identifier}...")
|
|
146
|
+
|
|
147
|
+
# Limpiar solo el historial de chat y el ID de respuesta anterior
|
|
148
|
+
self.session_context.clear_llm_history(company_short_name, user_identifier)
|
|
116
149
|
|
|
117
150
|
if self.util.is_gemini_model(model):
|
|
118
|
-
|
|
119
|
-
context_history = [{"role": "user", "content": final_system_context}]
|
|
151
|
+
context_history = [{"role": "user", "content": prepared_context}]
|
|
120
152
|
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
153
|
|
|
124
154
|
elif self.util.is_openai_model(model):
|
|
125
|
-
|
|
126
|
-
# 6. set the company/user context as the initial context for the LLM
|
|
127
155
|
response_id = self.llm_client.set_company_context(
|
|
128
|
-
company=company,
|
|
129
|
-
company_base_context=final_system_context,
|
|
130
|
-
model=model
|
|
156
|
+
company=company, company_base_context=prepared_context, model=model
|
|
131
157
|
)
|
|
132
|
-
|
|
133
|
-
# 7. save response_id in the session context
|
|
134
158
|
self.session_context.save_last_response_id(company_short_name, user_identifier, response_id)
|
|
135
159
|
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
if version_to_save:
|
|
161
|
+
self.session_context.save_context_version(company_short_name, user_identifier, version_to_save)
|
|
138
162
|
|
|
163
|
+
logging.info(
|
|
164
|
+
f"Contexto de {company_short_name}/{user_identifier} establecido en {int(time.time() - start_time)} seg.")
|
|
139
165
|
except Exception as e:
|
|
140
|
-
logging.exception(f"Error
|
|
166
|
+
logging.exception(f"Error en finalize_context_rebuild para {company_short_name}: {e}")
|
|
141
167
|
raise e
|
|
168
|
+
finally:
|
|
169
|
+
# --- Liberar el Bloqueo ---
|
|
170
|
+
self.session_context.release_lock(lock_key)
|
|
142
171
|
|
|
143
172
|
def llm_query(self,
|
|
144
173
|
company_short_name: str,
|
|
145
|
-
|
|
146
|
-
local_user_id: int = 0,
|
|
174
|
+
user_identifier: str,
|
|
147
175
|
task: Optional[Task] = None,
|
|
148
176
|
prompt_name: str = None,
|
|
149
177
|
question: str = '',
|
|
150
178
|
client_data: dict = {},
|
|
151
179
|
files: list = []) -> dict:
|
|
152
180
|
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
181
|
company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
|
|
159
182
|
if not company:
|
|
160
183
|
return {"error": True,
|
|
@@ -172,23 +195,19 @@ class QueryService:
|
|
|
172
195
|
# get user context
|
|
173
196
|
previous_response_id = self.session_context.get_last_response_id(company.short_name, user_identifier)
|
|
174
197
|
if not previous_response_id:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
}
|
|
198
|
+
return {'error': True,
|
|
199
|
+
"error_message": f"No se encontró 'previous_response_id' para '{company.short_name}/{user_identifier}'. Reinicia el contexto para continuar."
|
|
200
|
+
}
|
|
181
201
|
elif self.util.is_gemini_model(self.model):
|
|
182
202
|
# check the length of the context_history and remove old messages
|
|
183
203
|
self._trim_context_history(context_history)
|
|
184
204
|
|
|
185
|
-
# get the user data from the session context
|
|
186
|
-
|
|
205
|
+
# get the user profile data from the session context
|
|
206
|
+
user_profile = self.profile_service.get_profile_by_identifier(company.short_name, user_identifier)
|
|
187
207
|
|
|
188
|
-
#
|
|
189
|
-
final_client_data = (
|
|
208
|
+
# combine client_data with user_profile
|
|
209
|
+
final_client_data = (user_profile or {}).copy()
|
|
190
210
|
final_client_data.update(client_data)
|
|
191
|
-
final_client_data['user_id'] = user_identifier
|
|
192
211
|
|
|
193
212
|
# Load attached files into the context
|
|
194
213
|
files_context = self.load_files_for_context(files)
|
|
@@ -206,18 +225,12 @@ class QueryService:
|
|
|
206
225
|
template_string=prompt_content,
|
|
207
226
|
question=question,
|
|
208
227
|
client_data=final_client_data,
|
|
209
|
-
|
|
228
|
+
user_identifier=user_identifier,
|
|
210
229
|
company=company,
|
|
211
230
|
)
|
|
212
231
|
|
|
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
232
|
# This is the final user-facing prompt for this specific turn
|
|
220
|
-
user_turn_prompt = f"{main_prompt}\n{
|
|
233
|
+
user_turn_prompt = f"{main_prompt}\n{files_context}"
|
|
221
234
|
if not prompt_name:
|
|
222
235
|
user_turn_prompt += f"\n### La pregunta que debes responder es: {question}"
|
|
223
236
|
else:
|
|
@@ -259,6 +272,31 @@ class QueryService:
|
|
|
259
272
|
logging.exception(e)
|
|
260
273
|
return {'error': True, "error_message": f"{str(e)}"}
|
|
261
274
|
|
|
275
|
+
def _compute_context_version_from_string(self, final_system_context: str) -> str:
|
|
276
|
+
# returns a hash of the context string
|
|
277
|
+
try:
|
|
278
|
+
return hashlib.sha256(final_system_context.encode("utf-8")).hexdigest()
|
|
279
|
+
except Exception:
|
|
280
|
+
return "unknown"
|
|
281
|
+
|
|
282
|
+
def _has_valid_cached_context(self, company_short_name: str, user_identifier: str) -> bool:
|
|
283
|
+
"""
|
|
284
|
+
Verifica si existe un estado de contexto reutilizable en sesión.
|
|
285
|
+
- OpenAI: last_response_id presente.
|
|
286
|
+
- Gemini: context_history con al menos 1 mensaje.
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
if self.util.is_openai_model(self.model):
|
|
290
|
+
prev_id = self.session_context.get_last_response_id(company_short_name, user_identifier)
|
|
291
|
+
return bool(prev_id)
|
|
292
|
+
if self.util.is_gemini_model(self.model):
|
|
293
|
+
history = self.session_context.get_context_history(company_short_name, user_identifier) or []
|
|
294
|
+
return len(history) >= 1
|
|
295
|
+
return False
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logging.warning(f"Error verificando caché de contexto: {e}")
|
|
298
|
+
return False
|
|
299
|
+
|
|
262
300
|
def load_files_for_context(self, files: list) -> str:
|
|
263
301
|
"""
|
|
264
302
|
Processes a list of attached files, decodes their content,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from repositories.vs_repo import VSRepo
|
|
7
|
-
from repositories.document_repo import DocumentRepo
|
|
6
|
+
from iatoolkit.repositories.vs_repo import VSRepo
|
|
7
|
+
from iatoolkit.repositories.document_repo import DocumentRepo
|
|
8
8
|
from injector import inject
|
|
9
9
|
|
|
10
10
|
|
|
@@ -18,6 +18,22 @@ class SearchService:
|
|
|
18
18
|
self.doc_repo = doc_repo
|
|
19
19
|
|
|
20
20
|
def search(self, company_id: int, query: str, metadata_filter: dict = None) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Performs a semantic search for a given query within a company's documents.
|
|
23
|
+
|
|
24
|
+
This method queries the vector store for relevant documents based on the
|
|
25
|
+
provided query text. It then constructs a formatted string containing the
|
|
26
|
+
content of the retrieved documents, which can be used as context for an LLM.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
company_id: The ID of the company to search within.
|
|
30
|
+
query: The text query to search for.
|
|
31
|
+
metadata_filter: An optional dictionary to filter documents by their metadata.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A string containing the concatenated content of the found documents,
|
|
35
|
+
formatted to be used as a context.
|
|
36
|
+
"""
|
|
21
37
|
document_list = self.vs_repo.query(company_id=company_id,
|
|
22
38
|
query_text=query,
|
|
23
39
|
metadata_filter=metadata_filter)
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from repositories.database_manager import DatabaseManager
|
|
7
|
-
|
|
6
|
+
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
7
|
+
|
|
8
|
+
from iatoolkit.common.util import Utility
|
|
8
9
|
from sqlalchemy import text
|
|
9
10
|
from injector import inject
|
|
10
11
|
import json
|
|
11
|
-
from common.exceptions import IAToolkitException
|
|
12
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class SqlService:
|
|
@@ -17,6 +18,23 @@ class SqlService:
|
|
|
17
18
|
self.util = util
|
|
18
19
|
|
|
19
20
|
def exec_sql(self, db_manager: DatabaseManager, sql_statement: str) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Executes a raw SQL statement and returns the result as a JSON string.
|
|
23
|
+
|
|
24
|
+
This method takes a DatabaseManager instance and a SQL query, executes it
|
|
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.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
db_manager: The DatabaseManager instance to get the database session from.
|
|
33
|
+
sql_statement: The raw SQL statement to be executed.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A JSON string representing the list of rows returned by the query.
|
|
37
|
+
"""
|
|
20
38
|
try:
|
|
21
39
|
# here the SQL is executed
|
|
22
40
|
result = db_manager.get_session().execute(text(sql_statement))
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
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
|
|
7
|
+
from iatoolkit.repositories.models import Task, TaskStatus
|
|
8
|
+
from iatoolkit.services.query_service import QueryService
|
|
9
|
+
from iatoolkit.repositories.tasks_repo import TaskRepo
|
|
10
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
11
|
+
from iatoolkit.infra.call_service import CallServiceClient
|
|
12
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
13
13
|
from datetime import datetime
|
|
14
14
|
from werkzeug.utils import secure_filename
|
|
15
15
|
|
|
@@ -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,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.repositories.models import UserFeedback, Company
|
|
7
|
+
from injector import inject
|
|
8
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
|
+
from iatoolkit.infra.google_chat_app import GoogleChatApp
|
|
10
|
+
from iatoolkit.infra.mail_app import MailApp # <-- 1. Importar MailApp
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UserFeedbackService:
|
|
15
|
+
@inject
|
|
16
|
+
def __init__(self,
|
|
17
|
+
profile_repo: ProfileRepo,
|
|
18
|
+
google_chat_app: GoogleChatApp,
|
|
19
|
+
mail_app: MailApp):
|
|
20
|
+
self.profile_repo = profile_repo
|
|
21
|
+
self.google_chat_app = google_chat_app
|
|
22
|
+
self.mail_app = mail_app
|
|
23
|
+
|
|
24
|
+
def _send_google_chat_notification(self, space_name: str, message_text: str):
|
|
25
|
+
"""Envía una notificación de feedback a un espacio de Google Chat."""
|
|
26
|
+
try:
|
|
27
|
+
chat_data = {
|
|
28
|
+
"type": "MESSAGE_TRIGGER",
|
|
29
|
+
"space": {"name": space_name},
|
|
30
|
+
"message": {"text": message_text}
|
|
31
|
+
}
|
|
32
|
+
chat_result = self.google_chat_app.send_message(message_data=chat_data)
|
|
33
|
+
if not chat_result.get('success'):
|
|
34
|
+
logging.warning(f"Error al enviar notificación a Google Chat: {chat_result.get('message')}")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
logging.exception(f"Fallo inesperado al enviar notificación a Google Chat: {e}")
|
|
37
|
+
|
|
38
|
+
def _send_email_notification(self, destination_email: str, company_name: str, message_text: str):
|
|
39
|
+
"""Envía una notificación de feedback por correo electrónico."""
|
|
40
|
+
try:
|
|
41
|
+
subject = f"Nuevo Feedback de {company_name}"
|
|
42
|
+
# Convertir el texto plano a un HTML simple para mantener los saltos de línea
|
|
43
|
+
html_body = message_text.replace('\n', '<br>')
|
|
44
|
+
self.mail_app.send_email(to=destination_email, subject=subject, body=html_body)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logging.exception(f"Fallo inesperado al enviar email de feedback: {e}")
|
|
47
|
+
|
|
48
|
+
def _handle_notification(self, company: Company, message_text: str):
|
|
49
|
+
"""Lee la configuración de la empresa y envía la notificación al canal correspondiente."""
|
|
50
|
+
feedback_params = company.parameters.get('user_feedback')
|
|
51
|
+
if not isinstance(feedback_params, dict):
|
|
52
|
+
logging.warning(f"No se encontró configuración de 'user_feedback' para la empresa {company.short_name}.")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# get channel and destination
|
|
56
|
+
channel = feedback_params.get('channel')
|
|
57
|
+
destination = feedback_params.get('destination')
|
|
58
|
+
if not channel or not destination:
|
|
59
|
+
logging.warning(f"Configuración 'user_feedback' incompleta para {company.short_name}. Faltan 'channel' o 'destination'.")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if channel == 'google_chat':
|
|
63
|
+
self._send_google_chat_notification(space_name=destination, message_text=message_text)
|
|
64
|
+
elif channel == 'email':
|
|
65
|
+
self._send_email_notification(destination_email=destination, company_name=company.short_name, message_text=message_text)
|
|
66
|
+
else:
|
|
67
|
+
logging.warning(f"Canal de feedback '{channel}' no reconocido para la empresa {company.short_name}.")
|
|
68
|
+
|
|
69
|
+
def new_feedback(self,
|
|
70
|
+
company_short_name: str,
|
|
71
|
+
message: str,
|
|
72
|
+
user_identifier: str,
|
|
73
|
+
rating: int = None) -> dict:
|
|
74
|
+
try:
|
|
75
|
+
# 1. Validar empresa
|
|
76
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
77
|
+
if not company:
|
|
78
|
+
return {'error': f'No existe la empresa: {company_short_name}'}
|
|
79
|
+
|
|
80
|
+
# 2. Enviar notificación según la configuración de la empresa
|
|
81
|
+
notification_text = (f"*Nuevo feedback de {company_short_name}*:\n"
|
|
82
|
+
f"*Usuario:* {user_identifier}\n"
|
|
83
|
+
f"*Mensaje:* {message}\n"
|
|
84
|
+
f"*Calificación:* {rating if rating is not None else 'N/A'}")
|
|
85
|
+
self._handle_notification(company, notification_text)
|
|
86
|
+
|
|
87
|
+
# 3. Guardar el feedback en la base de datos (independientemente del éxito de la notificación)
|
|
88
|
+
new_feedback_obj = UserFeedback(
|
|
89
|
+
company_id=company.id,
|
|
90
|
+
message=message,
|
|
91
|
+
user_identifier=user_identifier,
|
|
92
|
+
rating=rating
|
|
93
|
+
)
|
|
94
|
+
saved_feedback = self.profile_repo.save_feedback(new_feedback_obj)
|
|
95
|
+
if not saved_feedback:
|
|
96
|
+
logging.error(f"No se pudo guardar el feedback para el usuario {user_identifier} en la empresa {company_short_name}")
|
|
97
|
+
return {'error': 'No se pudo guardar el feedback'}
|
|
98
|
+
|
|
99
|
+
return {'success': True, 'message': 'Feedback guardado correctamente'}
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logging.exception(f"Error crítico en el servicio de feedback: {e}")
|
|
103
|
+
return {'error': str(e)}
|