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.

Files changed (150) hide show
  1. iatoolkit/__init__.py +27 -35
  2. iatoolkit/base_company.py +3 -35
  3. iatoolkit/cli_commands.py +18 -47
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +48 -0
  6. iatoolkit/common/interfaces/__init__.py +0 -0
  7. iatoolkit/common/interfaces/asset_storage.py +34 -0
  8. iatoolkit/common/interfaces/database_provider.py +39 -0
  9. iatoolkit/common/model_registry.py +159 -0
  10. iatoolkit/common/routes.py +138 -0
  11. iatoolkit/common/session_manager.py +26 -0
  12. iatoolkit/common/util.py +353 -0
  13. iatoolkit/company_registry.py +66 -29
  14. iatoolkit/core.py +514 -0
  15. iatoolkit/infra/__init__.py +5 -0
  16. iatoolkit/infra/brevo_mail_app.py +123 -0
  17. iatoolkit/infra/call_service.py +140 -0
  18. iatoolkit/infra/connectors/__init__.py +5 -0
  19. iatoolkit/infra/connectors/file_connector.py +17 -0
  20. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  21. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  22. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  23. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  24. iatoolkit/infra/connectors/s3_connector.py +33 -0
  25. iatoolkit/infra/google_chat_app.py +57 -0
  26. iatoolkit/infra/llm_providers/__init__.py +0 -0
  27. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  28. iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
  29. iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
  30. iatoolkit/infra/llm_proxy.py +268 -0
  31. iatoolkit/infra/llm_response.py +45 -0
  32. iatoolkit/infra/redis_session_manager.py +122 -0
  33. iatoolkit/locales/en.yaml +222 -0
  34. iatoolkit/locales/es.yaml +225 -0
  35. iatoolkit/repositories/__init__.py +5 -0
  36. iatoolkit/repositories/database_manager.py +187 -0
  37. iatoolkit/repositories/document_repo.py +33 -0
  38. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  39. iatoolkit/repositories/llm_query_repo.py +105 -0
  40. iatoolkit/repositories/models.py +279 -0
  41. iatoolkit/repositories/profile_repo.py +171 -0
  42. iatoolkit/repositories/vs_repo.py +150 -0
  43. iatoolkit/services/__init__.py +5 -0
  44. iatoolkit/services/auth_service.py +193 -0
  45. {services → iatoolkit/services}/benchmark_service.py +7 -7
  46. iatoolkit/services/branding_service.py +153 -0
  47. iatoolkit/services/company_context_service.py +214 -0
  48. iatoolkit/services/configuration_service.py +375 -0
  49. iatoolkit/services/dispatcher_service.py +134 -0
  50. {services → iatoolkit/services}/document_service.py +20 -8
  51. iatoolkit/services/embedding_service.py +148 -0
  52. iatoolkit/services/excel_service.py +156 -0
  53. {services → iatoolkit/services}/file_processor_service.py +36 -21
  54. iatoolkit/services/history_manager_service.py +208 -0
  55. iatoolkit/services/i18n_service.py +104 -0
  56. iatoolkit/services/jwt_service.py +80 -0
  57. iatoolkit/services/language_service.py +89 -0
  58. iatoolkit/services/license_service.py +82 -0
  59. iatoolkit/services/llm_client_service.py +438 -0
  60. iatoolkit/services/load_documents_service.py +174 -0
  61. iatoolkit/services/mail_service.py +213 -0
  62. {services → iatoolkit/services}/profile_service.py +200 -101
  63. iatoolkit/services/prompt_service.py +303 -0
  64. iatoolkit/services/query_service.py +467 -0
  65. iatoolkit/services/search_service.py +55 -0
  66. iatoolkit/services/sql_service.py +169 -0
  67. iatoolkit/services/tool_service.py +246 -0
  68. iatoolkit/services/user_feedback_service.py +117 -0
  69. iatoolkit/services/user_session_context_service.py +213 -0
  70. iatoolkit/static/images/fernando.jpeg +0 -0
  71. iatoolkit/static/images/iatoolkit_core.png +0 -0
  72. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  73. iatoolkit/static/js/chat_feedback_button.js +80 -0
  74. iatoolkit/static/js/chat_filepond.js +85 -0
  75. iatoolkit/static/js/chat_help_content.js +124 -0
  76. iatoolkit/static/js/chat_history_button.js +110 -0
  77. iatoolkit/static/js/chat_logout_button.js +36 -0
  78. iatoolkit/static/js/chat_main.js +401 -0
  79. iatoolkit/static/js/chat_model_selector.js +227 -0
  80. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  81. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  82. iatoolkit/static/js/chat_reload_button.js +38 -0
  83. iatoolkit/static/styles/chat_iatoolkit.css +559 -0
  84. iatoolkit/static/styles/chat_modal.css +133 -0
  85. iatoolkit/static/styles/chat_public.css +135 -0
  86. iatoolkit/static/styles/documents.css +598 -0
  87. iatoolkit/static/styles/landing_page.css +398 -0
  88. iatoolkit/static/styles/llm_output.css +148 -0
  89. iatoolkit/static/styles/onboarding.css +176 -0
  90. iatoolkit/system_prompts/__init__.py +0 -0
  91. iatoolkit/system_prompts/query_main.prompt +30 -23
  92. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  93. iatoolkit/templates/_company_header.html +45 -0
  94. iatoolkit/templates/_login_widget.html +42 -0
  95. iatoolkit/templates/base.html +78 -0
  96. iatoolkit/templates/change_password.html +66 -0
  97. iatoolkit/templates/chat.html +337 -0
  98. iatoolkit/templates/chat_modals.html +185 -0
  99. iatoolkit/templates/error.html +51 -0
  100. iatoolkit/templates/forgot_password.html +51 -0
  101. iatoolkit/templates/onboarding_shell.html +106 -0
  102. iatoolkit/templates/signup.html +79 -0
  103. iatoolkit/views/__init__.py +5 -0
  104. iatoolkit/views/base_login_view.py +96 -0
  105. iatoolkit/views/change_password_view.py +116 -0
  106. iatoolkit/views/chat_view.py +76 -0
  107. iatoolkit/views/embedding_api_view.py +65 -0
  108. iatoolkit/views/forgot_password_view.py +75 -0
  109. iatoolkit/views/help_content_api_view.py +54 -0
  110. iatoolkit/views/history_api_view.py +56 -0
  111. iatoolkit/views/home_view.py +63 -0
  112. iatoolkit/views/init_context_api_view.py +74 -0
  113. iatoolkit/views/llmquery_api_view.py +59 -0
  114. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  115. iatoolkit/views/load_document_api_view.py +65 -0
  116. iatoolkit/views/login_view.py +170 -0
  117. iatoolkit/views/logout_api_view.py +57 -0
  118. iatoolkit/views/profile_api_view.py +46 -0
  119. iatoolkit/views/prompt_api_view.py +37 -0
  120. iatoolkit/views/root_redirect_view.py +22 -0
  121. iatoolkit/views/signup_view.py +100 -0
  122. iatoolkit/views/static_page_view.py +27 -0
  123. iatoolkit/views/user_feedback_api_view.py +60 -0
  124. iatoolkit/views/users_api_view.py +33 -0
  125. iatoolkit/views/verify_user_view.py +60 -0
  126. iatoolkit-0.107.4.dist-info/METADATA +268 -0
  127. iatoolkit-0.107.4.dist-info/RECORD +132 -0
  128. iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
  129. iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  130. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
  131. iatoolkit/iatoolkit.py +0 -413
  132. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  133. iatoolkit-0.3.9.dist-info/METADATA +0 -252
  134. iatoolkit-0.3.9.dist-info/RECORD +0 -32
  135. services/__init__.py +0 -5
  136. services/api_service.py +0 -75
  137. services/dispatcher_service.py +0 -351
  138. services/excel_service.py +0 -98
  139. services/history_service.py +0 -45
  140. services/jwt_service.py +0 -91
  141. services/load_documents_service.py +0 -212
  142. services/mail_service.py +0 -62
  143. services/prompt_manager_service.py +0 -172
  144. services/query_service.py +0 -334
  145. services/search_service.py +0 -32
  146. services/sql_service.py +0 -42
  147. services/tasks_service.py +0 -188
  148. services/user_feedback_service.py +0 -67
  149. services/user_session_context_service.py +0 -85
  150. {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
@@ -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)}