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.
Files changed (123) hide show
  1. iatoolkit/__init__.py +13 -35
  2. iatoolkit/base_company.py +74 -8
  3. iatoolkit/cli_commands.py +15 -23
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +46 -0
  6. iatoolkit/common/routes.py +141 -0
  7. iatoolkit/common/session_manager.py +24 -0
  8. iatoolkit/common/util.py +348 -0
  9. iatoolkit/company_registry.py +7 -8
  10. iatoolkit/iatoolkit.py +169 -96
  11. iatoolkit/infra/__init__.py +5 -0
  12. iatoolkit/infra/call_service.py +140 -0
  13. iatoolkit/infra/connectors/__init__.py +5 -0
  14. iatoolkit/infra/connectors/file_connector.py +17 -0
  15. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  16. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  17. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  18. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  19. iatoolkit/infra/connectors/s3_connector.py +33 -0
  20. iatoolkit/infra/gemini_adapter.py +356 -0
  21. iatoolkit/infra/google_chat_app.py +57 -0
  22. iatoolkit/infra/llm_client.py +429 -0
  23. iatoolkit/infra/llm_proxy.py +139 -0
  24. iatoolkit/infra/llm_response.py +40 -0
  25. iatoolkit/infra/mail_app.py +145 -0
  26. iatoolkit/infra/openai_adapter.py +90 -0
  27. iatoolkit/infra/redis_session_manager.py +122 -0
  28. iatoolkit/locales/en.yaml +144 -0
  29. iatoolkit/locales/es.yaml +140 -0
  30. iatoolkit/repositories/__init__.py +5 -0
  31. iatoolkit/repositories/database_manager.py +110 -0
  32. iatoolkit/repositories/document_repo.py +33 -0
  33. iatoolkit/repositories/llm_query_repo.py +91 -0
  34. iatoolkit/repositories/models.py +336 -0
  35. iatoolkit/repositories/profile_repo.py +123 -0
  36. iatoolkit/repositories/tasks_repo.py +52 -0
  37. iatoolkit/repositories/vs_repo.py +139 -0
  38. iatoolkit/services/__init__.py +5 -0
  39. iatoolkit/services/auth_service.py +193 -0
  40. {services → iatoolkit/services}/benchmark_service.py +6 -6
  41. iatoolkit/services/branding_service.py +149 -0
  42. {services → iatoolkit/services}/dispatcher_service.py +39 -99
  43. {services → iatoolkit/services}/document_service.py +5 -5
  44. {services → iatoolkit/services}/excel_service.py +27 -21
  45. {services → iatoolkit/services}/file_processor_service.py +5 -5
  46. iatoolkit/services/help_content_service.py +30 -0
  47. {services → iatoolkit/services}/history_service.py +8 -16
  48. iatoolkit/services/i18n_service.py +104 -0
  49. {services → iatoolkit/services}/jwt_service.py +18 -27
  50. iatoolkit/services/language_service.py +77 -0
  51. {services → iatoolkit/services}/load_documents_service.py +19 -14
  52. {services → iatoolkit/services}/mail_service.py +5 -5
  53. iatoolkit/services/onboarding_service.py +43 -0
  54. {services → iatoolkit/services}/profile_service.py +155 -89
  55. {services → iatoolkit/services}/prompt_manager_service.py +26 -11
  56. {services → iatoolkit/services}/query_service.py +142 -104
  57. {services → iatoolkit/services}/search_service.py +21 -5
  58. {services → iatoolkit/services}/sql_service.py +24 -6
  59. {services → iatoolkit/services}/tasks_service.py +10 -10
  60. iatoolkit/services/user_feedback_service.py +103 -0
  61. iatoolkit/services/user_session_context_service.py +143 -0
  62. iatoolkit/static/images/fernando.jpeg +0 -0
  63. iatoolkit/static/js/chat_feedback_button.js +80 -0
  64. iatoolkit/static/js/chat_filepond.js +85 -0
  65. iatoolkit/static/js/chat_help_content.js +124 -0
  66. iatoolkit/static/js/chat_history_button.js +112 -0
  67. iatoolkit/static/js/chat_logout_button.js +36 -0
  68. iatoolkit/static/js/chat_main.js +364 -0
  69. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  70. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  71. iatoolkit/static/js/chat_reload_button.js +35 -0
  72. iatoolkit/static/styles/chat_iatoolkit.css +592 -0
  73. iatoolkit/static/styles/chat_modal.css +169 -0
  74. iatoolkit/static/styles/chat_public.css +107 -0
  75. iatoolkit/static/styles/landing_page.css +182 -0
  76. iatoolkit/static/styles/llm_output.css +115 -0
  77. iatoolkit/static/styles/onboarding.css +169 -0
  78. iatoolkit/system_prompts/query_main.prompt +5 -15
  79. iatoolkit/templates/_company_header.html +20 -0
  80. iatoolkit/templates/_login_widget.html +42 -0
  81. iatoolkit/templates/about.html +13 -0
  82. iatoolkit/templates/base.html +65 -0
  83. iatoolkit/templates/change_password.html +66 -0
  84. iatoolkit/templates/chat.html +287 -0
  85. iatoolkit/templates/chat_modals.html +181 -0
  86. iatoolkit/templates/error.html +51 -0
  87. iatoolkit/templates/forgot_password.html +50 -0
  88. iatoolkit/templates/index.html +145 -0
  89. iatoolkit/templates/login_simulation.html +34 -0
  90. iatoolkit/templates/onboarding_shell.html +104 -0
  91. iatoolkit/templates/signup.html +76 -0
  92. iatoolkit/views/__init__.py +5 -0
  93. iatoolkit/views/base_login_view.py +92 -0
  94. iatoolkit/views/change_password_view.py +117 -0
  95. iatoolkit/views/external_login_view.py +73 -0
  96. iatoolkit/views/file_store_api_view.py +65 -0
  97. iatoolkit/views/forgot_password_view.py +72 -0
  98. iatoolkit/views/help_content_api_view.py +54 -0
  99. iatoolkit/views/history_api_view.py +56 -0
  100. iatoolkit/views/home_view.py +61 -0
  101. iatoolkit/views/index_view.py +14 -0
  102. iatoolkit/views/init_context_api_view.py +73 -0
  103. iatoolkit/views/llmquery_api_view.py +57 -0
  104. iatoolkit/views/login_simulation_view.py +81 -0
  105. iatoolkit/views/login_view.py +153 -0
  106. iatoolkit/views/logout_api_view.py +49 -0
  107. iatoolkit/views/profile_api_view.py +46 -0
  108. iatoolkit/views/prompt_api_view.py +37 -0
  109. iatoolkit/views/signup_view.py +94 -0
  110. iatoolkit/views/tasks_api_view.py +72 -0
  111. iatoolkit/views/tasks_review_api_view.py +55 -0
  112. iatoolkit/views/user_feedback_api_view.py +60 -0
  113. iatoolkit/views/verify_user_view.py +62 -0
  114. {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
  115. iatoolkit-0.66.2.dist-info/RECORD +119 -0
  116. {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -1
  117. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  118. iatoolkit-0.4.2.dist-info/RECORD +0 -32
  119. services/__init__.py +0 -5
  120. services/api_service.py +0 -75
  121. services/user_feedback_service.py +0 -67
  122. services/user_session_context_service.py +0 -85
  123. {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
- # 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
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, TYPE_CHECKING
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
- # Obtener el modelo de las variables de entorno
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
- 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
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
- # Validate the user and company
68
- user_identifier, is_local_user = self.util.resolve_user_identifier(external_user_id, local_user_id)
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
- raise IAToolkitException(IAToolkitException.ErrorType.INVALID_USER,
71
- "No se pudo resolver el identificador del usuario")
92
+ return {'rebuild_needed': True, 'error': 'Invalid user identifier'}
72
93
 
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}")
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
- # 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
- )
106
+ prev_version = self.session_context.get_context_version(company_short_name, user_identifier)
107
+ except Exception:
108
+ prev_version = None
85
109
 
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
- )
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
- # add the user logged in to the user_info dictionary
95
- user_profile['user_id'] = user_identifier
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
- # 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)
118
+ return {'rebuild_needed': rebuild_is_needed}
100
119
 
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
- )
120
+ def finalize_context_rebuild(self, company_short_name: str, user_identifier: str, model: str = ''):
110
121
 
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)
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
- # 5. merge contexts
115
- final_system_context = f"{company_specific_context}\n{rendered_system_prompt}"
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
- # save the initial context as `context_history` (list of messages)
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
- 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
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 al inicializar el contexto del LLM para {company_short_name}: {e}")
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
- external_user_id: Optional[str] = None,
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
- # 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
- }
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
- user_info_from_session = self.session_context.get_user_session_data(company.short_name, user_identifier)
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
- # 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()
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
- external_user_id=external_user_id,
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{client_profile}\n{files_context}"
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
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
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
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
5
 
6
- from repositories.database_manager import DatabaseManager
7
- from common.util import Utility
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
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
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
- local_user_id=0,
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)}