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