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
@@ -0,0 +1,348 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ import logging
7
+ from typing import List
8
+ from iatoolkit.common.exceptions import IAToolkitException
9
+ from injector import inject
10
+ import os
11
+ from jinja2 import Environment, FileSystemLoader
12
+ from datetime import datetime, date
13
+ from decimal import Decimal
14
+ import yaml
15
+ from cryptography.fernet import Fernet
16
+ import base64
17
+
18
+
19
+ class Utility:
20
+ @inject
21
+ def __init__(self):
22
+ self.encryption_key = os.getenv('FERNET_KEY')
23
+
24
+ def render_prompt_from_template(self,
25
+ template_pathname: str,
26
+ client_data: dict = {},
27
+ **kwargs) -> str:
28
+
29
+ try:
30
+ # Normalizar la ruta para que funcione en cualquier SO
31
+ template_pathname = os.path.abspath(template_pathname)
32
+ template_dir = os.path.dirname(template_pathname)
33
+ template_file = os.path.basename(template_pathname)
34
+
35
+ env = Environment(loader=FileSystemLoader(template_dir))
36
+ template = env.get_template(template_file)
37
+
38
+ # add all the keys in client_data to kwargs
39
+ kwargs.update(client_data)
40
+
41
+ # render my dynamic prompt
42
+ prompt = template.render(**kwargs)
43
+ return prompt
44
+ except Exception as e:
45
+ logging.exception(e)
46
+ raise IAToolkitException(IAToolkitException.ErrorType.TEMPLATE_ERROR,
47
+ f'No se pudo renderizar el template: {template_pathname}, error: {str(e)}') from e
48
+
49
+ def render_prompt_from_string(self,
50
+ template_string: str,
51
+ searchpath: str | list[str] = None,
52
+ client_data: dict = {},
53
+ **kwargs) -> str:
54
+ """
55
+ Renderiza un prompt a partir de un string de plantilla Jinja2.
56
+
57
+ :param template_string: El string que contiene la plantilla Jinja2.
58
+ :param searchpath: Una ruta o lista de rutas a directorios para buscar plantillas incluidas (con {% include %}).
59
+ :param query: El query principal a pasar a la plantilla.
60
+ :param client_data: Un diccionario con datos adicionales para la plantilla.
61
+ :param kwargs: Argumentos adicionales para la plantilla.
62
+ :return: El prompt renderizado como un string.
63
+ """
64
+ try:
65
+ # Si se proporciona un searchpath, se usa un FileSystemLoader para permitir includes.
66
+ if searchpath:
67
+ loader = FileSystemLoader(searchpath)
68
+ else:
69
+ loader = None # Sin loader, no se pueden incluir plantillas desde archivos.
70
+
71
+ env = Environment(loader=loader)
72
+ template = env.from_string(template_string)
73
+
74
+ kwargs.update(client_data)
75
+
76
+ prompt = template.render(**kwargs)
77
+ return prompt
78
+ except Exception as e:
79
+ logging.exception(e)
80
+ raise IAToolkitException(IAToolkitException.ErrorType.TEMPLATE_ERROR,
81
+ f'No se pudo renderizar el template desde el string, error: {str(e)}') from e
82
+
83
+
84
+ def get_company_template(self, company_short_name: str, template_name: str) -> str:
85
+ # 1. get the path to the company specific template
86
+ template_path = os.path.join(os.getcwd(), f'companies/{company_short_name}/templates/{template_name}')
87
+ if not os.path.exists(template_path):
88
+ return None
89
+
90
+ # 2. read the file
91
+ try:
92
+ with open(template_path, 'r') as f:
93
+ template_string = f.read()
94
+
95
+ return template_string
96
+ except Exception as e:
97
+ logging.exception(e)
98
+ return None
99
+
100
+
101
+ def serialize(self, obj):
102
+ if isinstance(obj, datetime) or isinstance(obj, date):
103
+ return obj.isoformat()
104
+ elif isinstance(obj, Decimal):
105
+ return float(obj)
106
+ elif isinstance(obj, bytes):
107
+ return obj.decode('utf-8')
108
+ else:
109
+ raise TypeError(f"Type {type(obj)} not serializable")
110
+
111
+ def encrypt_key(self, key: str) -> str:
112
+ if not self.encryption_key:
113
+ raise IAToolkitException(IAToolkitException.ErrorType.CRYPT_ERROR,
114
+ 'No se pudo obtener variable de ambiente para encriptar')
115
+
116
+ if not key:
117
+ raise IAToolkitException(IAToolkitException.ErrorType.CRYPT_ERROR,
118
+ 'falta la clave a encriptar')
119
+ try:
120
+ cipher_suite = Fernet(self.encryption_key.encode('utf-8'))
121
+
122
+ encrypted_key = cipher_suite.encrypt(key.encode('utf-8'))
123
+ encrypted_key_str = base64.urlsafe_b64encode(encrypted_key).decode('utf-8')
124
+
125
+ return encrypted_key_str
126
+ except Exception as e:
127
+ raise IAToolkitException(IAToolkitException.ErrorType.CRYPT_ERROR,
128
+ f'No se pudo encriptar la clave: {str(e)}') from e
129
+
130
+ def decrypt_key(self, encrypted_key: str) -> str:
131
+ if not self.encryption_key:
132
+ raise IAToolkitException(IAToolkitException.ErrorType.CRYPT_ERROR,
133
+ 'No se pudo obtener variable de ambiente para desencriptar')
134
+ if not encrypted_key:
135
+ raise IAToolkitException(IAToolkitException.ErrorType.CRYPT_ERROR,
136
+ 'falta la clave a encriptar')
137
+
138
+ try:
139
+ # transform to bytes first
140
+ encrypted_data_from_storage_bytes = base64.urlsafe_b64decode(encrypted_key.encode('utf-8'))
141
+
142
+ cipher_suite = Fernet(self.encryption_key.encode('utf-8'))
143
+ decrypted_key_bytes = cipher_suite.decrypt(encrypted_data_from_storage_bytes)
144
+ return decrypted_key_bytes.decode('utf-8')
145
+ except Exception as e:
146
+ raise IAToolkitException(IAToolkitException.ErrorType.CRYPT_ERROR,
147
+ f'No se pudo desencriptar la clave: {str(e)}') from e
148
+
149
+ def load_schema_from_yaml(self, file_path):
150
+ with open(file_path, 'r', encoding='utf-8') as f:
151
+ schema = yaml.safe_load(f)
152
+ return schema
153
+
154
+ def generate_context_for_schema(self, entity_name: str, schema_file: str = None, schema: dict = {}) -> str:
155
+ if not schema_file and not schema:
156
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
157
+ f'No se pudo obtener schema de la entidad: {entity_name}')
158
+
159
+ try:
160
+ if schema_file:
161
+ schema = self.load_schema_from_yaml(schema_file)
162
+ table_schema = self.generate_schema_table(schema)
163
+ return table_schema
164
+ except Exception as e:
165
+ logging.exception(e)
166
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
167
+ f'No se pudo leer el schema de la entidad: {entity_name}') from e
168
+
169
+ def generate_schema_table(self, schema: dict) -> str:
170
+ """
171
+ Genera una descripción detallada y formateada en Markdown de un esquema.
172
+ Esta función está diseñada para manejar el formato específico de nuestros
173
+ archivos YAML, donde el esquema se define bajo una única clave raíz.
174
+ """
175
+ if not schema or not isinstance(schema, dict):
176
+ return ""
177
+
178
+ # Asumimos que el YAML tiene una única clave raíz que nombra a la entidad.
179
+ if len(schema) == 1:
180
+ root_name = list(schema.keys())[0]
181
+ root_details = schema[root_name]
182
+
183
+ if isinstance(root_details, dict):
184
+ # Las claves de metadatos describen el objeto en sí, no sus propiedades hijas.
185
+ METADATA_KEYS = ['description', 'type', 'format', 'items', 'properties']
186
+
187
+ # Las propiedades son las claves restantes en el diccionario.
188
+ properties = {
189
+ k: v for k, v in root_details.items() if k not in METADATA_KEYS
190
+ }
191
+
192
+ # La descripción del objeto raíz.
193
+ root_description = root_details.get('description', '')
194
+
195
+ # Formatea las propiedades extraídas usando la función auxiliar recursiva.
196
+ formatted_properties = self._format_json_schema(properties, 0)
197
+
198
+ # Construcción del resultado final, incluyendo el nombre del objeto raíz.
199
+ output_parts = [f"\n\n### Objeto: `{root_name}`"]
200
+ if root_description:
201
+ # Limpia la descripción para que se muestre bien
202
+ cleaned_description = '\n'.join(line.strip() for line in root_description.strip().split('\n'))
203
+ output_parts.append(f"{cleaned_description}")
204
+
205
+ if formatted_properties:
206
+ output_parts.append(f"**Campos del objeto `{root_name}`:**\n{formatted_properties}")
207
+
208
+ return "\n".join(output_parts)
209
+
210
+ # Si el esquema (como tender_schema.yaml) no tiene un objeto raíz,
211
+ # se formatea directamente como una lista de propiedades.
212
+ return self._format_json_schema(schema, 0)
213
+
214
+ def _format_json_schema(self, properties: dict, indent_level: int) -> str:
215
+ """
216
+ Formatea de manera recursiva las propiedades de un esquema JSON/YAML.
217
+ """
218
+ output = []
219
+ indent_str = ' ' * indent_level
220
+
221
+ for name, details in properties.items():
222
+ if not isinstance(details, dict):
223
+ continue
224
+
225
+ description = details.get('description', '')
226
+ data_type = details.get('type', 'any')
227
+ output.append(f"{indent_str}- **`{name.lower()}`** ({data_type}): {description}")
228
+
229
+ child_indent_str = ' ' * (indent_level + 1)
230
+
231
+ # Manejo de 'oneOf' para mostrar valores constantes
232
+ if 'oneOf' in details:
233
+ for item in details['oneOf']:
234
+ if 'const' in item:
235
+ const_desc = item.get('description', '')
236
+ output.append(f"{child_indent_str}- `{item['const']}`: {const_desc}")
237
+
238
+ # Manejo de 'items' para arrays
239
+ if 'items' in details:
240
+ items_details = details.get('items', {})
241
+ if isinstance(items_details, dict):
242
+ item_description = items_details.get('description')
243
+ if item_description:
244
+ # Limpiamos y añadimos la descripción del item
245
+ cleaned_description = '\n'.join(
246
+ f"{line.strip()}" for line in item_description.strip().split('\n')
247
+ )
248
+ output.append(
249
+ f"{child_indent_str}*Descripción de los elementos del array:*\n{child_indent_str}{cleaned_description}")
250
+
251
+ if 'properties' in items_details:
252
+ nested_properties = self._format_json_schema(items_details['properties'], indent_level + 1)
253
+ output.append(nested_properties)
254
+
255
+ # Manejo de 'properties' para objetos anidados estándar
256
+ if 'properties' in details:
257
+ nested_properties = self._format_json_schema(details['properties'], indent_level + 1)
258
+ output.append(nested_properties)
259
+
260
+ elif 'additionalProperties' in details and 'properties' in details.get('additionalProperties', {}):
261
+ # Imprime un marcador de posición para la clave dinámica.
262
+ output.append(
263
+ f"{child_indent_str}- **[*]** (object): Las claves de este objeto son dinámicas (ej. un ID).")
264
+ # Procesa las propiedades del objeto anidado.
265
+ nested_properties = self._format_json_schema(details['additionalProperties']['properties'],
266
+ indent_level + 2)
267
+ output.append(nested_properties)
268
+
269
+ return '\n'.join(output)
270
+
271
+ def load_markdown_context(self, filepath: str) -> str:
272
+ with open(filepath, 'r', encoding='utf-8') as f:
273
+ return f.read()
274
+
275
+ @classmethod
276
+ def _get_verifier(self, rut: int):
277
+ value = 11 - sum([int(a) * int(b) for a, b in zip(str(rut).zfill(8), '32765432')]) % 11
278
+ return {10: 'K', 11: '0'}.get(value, str(value))
279
+
280
+ def validate_rut(self, rut_str):
281
+ if not rut_str or not isinstance(rut_str, str):
282
+ return False
283
+
284
+ rut_str = rut_str.strip().replace('.', '').upper()
285
+ parts = rut_str.split('-')
286
+ if not len(parts) == 2:
287
+ return False
288
+
289
+ try:
290
+ rut = int(parts[0])
291
+ except ValueError:
292
+ return False
293
+
294
+ if rut < 1000000:
295
+ return False
296
+
297
+ if not len(parts[1]) == 1:
298
+ return False
299
+
300
+ digit = parts[1].upper()
301
+ return digit == self._get_verifier(rut)
302
+
303
+ def get_files_by_extension(self, directory: str, extension: str, return_extension: bool = False) -> List[str]:
304
+ try:
305
+ # Normalizar la extensión (agregar punto si no lo tiene)
306
+ if not extension.startswith('.'):
307
+ extension = '.' + extension
308
+
309
+ # Verificar que el directorio existe
310
+ if not os.path.exists(directory):
311
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
312
+ f'El directorio no existe: {directory}')
313
+
314
+ if not os.path.isdir(directory):
315
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
316
+ f'La ruta no es un directorio: {directory}')
317
+
318
+ # Buscar archivos con la extensión especificada
319
+ files = []
320
+ for filename in os.listdir(directory):
321
+ file_path = os.path.join(directory, filename)
322
+ if os.path.isfile(file_path) and filename.endswith(extension):
323
+ if return_extension:
324
+ files.append(filename)
325
+ else:
326
+ name_without_extension = os.path.splitext(filename)[0]
327
+ files.append(name_without_extension)
328
+
329
+ return sorted(files) # Retornar lista ordenada alfabéticamente
330
+
331
+ except IAToolkitException:
332
+ raise
333
+ except Exception as e:
334
+ logging.exception(e)
335
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
336
+ f'Error al buscar archivos en el directorio {directory}: {str(e)}') from e
337
+
338
+ def is_openai_model(self, model: str) -> bool:
339
+ openai_models = [
340
+ 'gpt-5', 'gpt'
341
+ ]
342
+ return any(openai_model in model.lower() for openai_model in openai_models)
343
+
344
+ def is_gemini_model(self, model: str) -> bool:
345
+ gemini_models = [
346
+ 'gemini', 'gemini-2.5-pro'
347
+ ]
348
+ return any(gemini_model in model.lower() for gemini_model in gemini_models)
@@ -1,5 +1,7 @@
1
1
  # Copyright (c) 2024 Fernando Libedinsky
2
- # Producto: IAToolkit
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
3
5
 
4
6
  from typing import Dict, Type, Any
5
7
  from .base_company import BaseCompany
@@ -19,19 +21,17 @@ class CompanyRegistry:
19
21
 
20
22
  def instantiate_companies(self, injector) -> Dict[str, BaseCompany]:
21
23
  """
22
- Instancia todas las empresas registradas con inyección de dependencias.
23
-
24
- Returns:
25
- Dict con instancias de empresas {name: instance}
24
+ intantiate all registered companies using the toolkit injector
26
25
  """
27
-
28
26
  for company_key, company_class in self._company_classes.items():
29
27
  if company_key not in self._company_instances:
30
28
  try:
31
29
  # use de injector to create the instance
32
30
  company_instance = injector.get(company_class)
31
+
32
+ # save the created instance in the registry
33
33
  self._company_instances[company_key] = company_instance
34
- logging.info(f"company '{company_key}' created in dispatcher")
34
+ logging.info(f"company '{company_key}' instantiated")
35
35
 
36
36
  except Exception as e:
37
37
  logging.error(f"Error instanciando empresa {company_key}: {e}")
@@ -48,7 +48,6 @@ class CompanyRegistry:
48
48
  return self._company_classes.copy()
49
49
 
50
50
  def clear(self) -> None:
51
- """Limpia el registro (útil para tests)"""
52
51
  self._company_classes.clear()
53
52
  self._company_instances.clear()
54
53