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
iatoolkit/core.py ADDED
@@ -0,0 +1,514 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from flask import Flask, url_for, get_flashed_messages, request
7
+ from flask_session import Session
8
+ from flask_injector import FlaskInjector
9
+ from flask_bcrypt import Bcrypt
10
+ from flask_cors import CORS
11
+ from iatoolkit.common.exceptions import IAToolkitException
12
+ from iatoolkit.repositories.database_manager import DatabaseManager
13
+ from iatoolkit.common.interfaces.asset_storage import AssetRepository
14
+ from iatoolkit.company_registry import get_registered_companies
15
+ from werkzeug.middleware.proxy_fix import ProxyFix
16
+ from injector import Binder, Injector, singleton
17
+ from typing import Optional, Dict, Any
18
+ from urllib.parse import urlparse
19
+ import redis
20
+ import logging
21
+ import os
22
+
23
+ from iatoolkit import __version__ as IATOOLKIT_VERSION
24
+ from iatoolkit.services.configuration_service import ConfigurationService
25
+
26
+ # global variable for the unique instance of IAToolkit
27
+ _iatoolkit_instance: Optional['IAToolkit'] = None
28
+
29
+ def is_bound(injector: Injector, cls) -> bool:
30
+ return cls in injector.binder._bindings
31
+
32
+ class IAToolkit:
33
+ """
34
+ IAToolkit main class
35
+ """
36
+ def __new__(cls, config: Optional[Dict[str, Any]] = None):
37
+ """
38
+ Implementa el patrón Singleton
39
+ """
40
+ global _iatoolkit_instance
41
+ if _iatoolkit_instance is None:
42
+ _iatoolkit_instance = super().__new__(cls)
43
+ _iatoolkit_instance._initialized = False
44
+ return _iatoolkit_instance
45
+
46
+
47
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
48
+ """
49
+ Args:
50
+ config: Diccionario opcional de configuración que sobrescribe variables de entorno
51
+ """
52
+ if self._initialized:
53
+ return
54
+
55
+ self.config = config or {}
56
+ self.app = None
57
+ self.db_manager = None
58
+ self._injector = Injector() # init empty injector
59
+ self.version = IATOOLKIT_VERSION
60
+ self.license = "Community Edition"
61
+
62
+ @classmethod
63
+ def get_instance(cls) -> 'IAToolkit':
64
+ # get the global IAToolkit instance
65
+ global _iatoolkit_instance
66
+ if _iatoolkit_instance is None:
67
+ _iatoolkit_instance = cls()
68
+ return _iatoolkit_instance
69
+
70
+ def create_iatoolkit(self, start: bool = True):
71
+ """
72
+ Creates, configures, and returns the Flask application instance.
73
+ this is the main entry point for the application factory.
74
+ """
75
+ if self._initialized and self.app:
76
+ return self.app
77
+
78
+ self._setup_logging()
79
+
80
+ # Step 1: Create the Flask app instance
81
+ self._create_flask_instance()
82
+
83
+ # Step 2: Set up the core components that DI depends on
84
+ self._setup_database()
85
+
86
+ # Step 3: Configure dependencies using the existing injector
87
+ self._configure_core_dependencies(self._injector)
88
+
89
+ # Step 4: Register routes using the fully configured injector
90
+ self._register_routes()
91
+
92
+ # Step 5: Initialize FlaskInjector. This is now primarily for request-scoped injections
93
+ # and other integrations, as views are handled manually.
94
+ FlaskInjector(app=self.app, injector=self._injector)
95
+
96
+ # Step 6: initialize registered companies
97
+ self._instantiate_company_instances()
98
+
99
+ # Re-apply logging configuration in case it was modified by company-specific code
100
+ self._setup_logging()
101
+
102
+ # Step 7: load company configuration file
103
+ self._load_company_configuration()
104
+
105
+ # Step 8: Finalize setup within the application context
106
+ self._setup_redis_sessions()
107
+
108
+ self._setup_cors()
109
+ self._setup_additional_services()
110
+ self._setup_cli_commands()
111
+ self._setup_request_globals()
112
+ self._setup_context_processors()
113
+
114
+ # Step 9: define the download_dir
115
+ self._setup_download_dir()
116
+
117
+ # register data source
118
+ if start:
119
+ self.register_data_sources()
120
+
121
+ logging.info(f"🎉 IAToolkit {self.license} version {self.version} correctly initialized.")
122
+ self._initialized = True
123
+
124
+ return self.app
125
+
126
+ def register_data_sources(self):
127
+ # load the company configurations
128
+ configuration_service = self._injector.get(ConfigurationService)
129
+ for company in get_registered_companies():
130
+ configuration_service.register_data_sources(company)
131
+
132
+ def _get_config_value(self, key: str, default=None):
133
+ # get a value from the config dict or the environment variable
134
+ return self.config.get(key, os.getenv(key, default))
135
+
136
+ def _setup_request_globals(self):
137
+ """
138
+ Configures functions to run before each request to set up
139
+ request-global variables, such as language.
140
+ """
141
+ injector = self._injector
142
+
143
+ @self.app.before_request
144
+ def set_request_language():
145
+ """
146
+ Determines and caches the language for the current request in g.lang.
147
+ """
148
+ from iatoolkit.services.language_service import LanguageService
149
+ language_service = injector.get(LanguageService)
150
+ language_service.get_current_language()
151
+
152
+ def _setup_logging(self):
153
+ # Lee el nivel de log desde una variable de entorno, con 'INFO' como valor por defecto.
154
+ log_level_name = os.getenv('LOG_LEVEL', 'INFO').upper()
155
+ log_level = getattr(logging, log_level_name, logging.INFO)
156
+
157
+ logging.basicConfig(
158
+ level=log_level,
159
+ format="%(asctime)s - IATOOLKIT - %(name)s - %(levelname)s - %(message)s",
160
+ handlers=[logging.StreamHandler()],
161
+ force=True
162
+ )
163
+
164
+ logging.getLogger("httpx").setLevel(logging.WARNING)
165
+
166
+ def _register_routes(self):
167
+ """Registers routes by passing the configured injector."""
168
+ from iatoolkit.common.routes import register_views
169
+
170
+ # Pass the injector to the view registration function
171
+ register_views(self.app)
172
+ logging.info("✅ Community routes registered.")
173
+
174
+ def _create_flask_instance(self):
175
+ static_folder = self._get_config_value('STATIC_FOLDER') or self._get_default_static_folder()
176
+ template_folder = self._get_config_value('TEMPLATE_FOLDER') or self._get_default_template_folder()
177
+
178
+ self.app = Flask(__name__,
179
+ static_folder=static_folder,
180
+ template_folder=template_folder)
181
+
182
+ self.app.config.update({
183
+ 'VERSION': self.version,
184
+ 'SECRET_KEY': self._get_config_value('FLASK_SECRET_KEY', 'iatoolkit-default-secret'),
185
+ 'SESSION_COOKIE_SAMESITE': "None",
186
+ 'SESSION_COOKIE_SECURE': True,
187
+ 'SESSION_PERMANENT': False,
188
+ 'SESSION_USE_SIGNER': True,
189
+ 'IATOOLKIT_SECRET_KEY': self._get_config_value('IATOOLKIT_SECRET_KEY', 'iatoolkit-jwt-secret'),
190
+ 'JWT_ALGORITHM': 'HS256',
191
+ })
192
+
193
+ # 2. ProxyFix para no tener problemas con iframes y rutas
194
+ self.app.wsgi_app = ProxyFix(self.app.wsgi_app, x_proto=1)
195
+
196
+ # Configuración para tokenizers en desarrollo
197
+ if self._get_config_value('FLASK_ENV') == 'dev':
198
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
199
+
200
+ def _setup_database(self):
201
+ database_uri = self._get_config_value('DATABASE_URI') or self._get_config_value('DATABASE_URL')
202
+ if not database_uri:
203
+ raise IAToolkitException(
204
+ IAToolkitException.ErrorType.CONFIG_ERROR,
205
+ "DATABASE_URI is required (config dict or env. variable)"
206
+ )
207
+
208
+ self.db_manager = DatabaseManager(database_url=database_uri, schema='iatoolkit')
209
+ self.db_manager.create_all()
210
+ logging.info("✅ Database configured successfully")
211
+
212
+ @self.app.teardown_appcontext
213
+ def remove_session(exception=None):
214
+ """
215
+ Flask calls this after each request.
216
+ It ensures the SQLAlchemy session is properly closed
217
+ and the DB connection is returned to the pool.
218
+ """
219
+ self.db_manager.scoped_session.remove()
220
+
221
+ def _setup_redis_sessions(self):
222
+ redis_url = self._get_config_value('REDIS_URL')
223
+ if not redis_url:
224
+ logging.warning("⚠️ REDIS_URL not configured, will use memory sessions")
225
+ return
226
+
227
+ try:
228
+ url = urlparse(redis_url)
229
+ redis_instance = redis.Redis(
230
+ host=url.hostname,
231
+ port=url.port,
232
+ password=url.password,
233
+ ssl=(url.scheme == "rediss"),
234
+ ssl_cert_reqs=None
235
+ )
236
+
237
+ self.app.config.update({
238
+ 'SESSION_TYPE': 'redis',
239
+ 'SESSION_REDIS': redis_instance
240
+ })
241
+
242
+ Session(self.app)
243
+ logging.info("✅ Redis and sessions configured successfully")
244
+
245
+ except Exception as e:
246
+ logging.error(f"❌ Error configuring Redis: {e}")
247
+ raise e
248
+
249
+ def _setup_cors(self):
250
+ """🌐 Configura CORS"""
251
+ from iatoolkit.company_registry import get_company_registry
252
+
253
+ # default CORS origin
254
+ default_origins = []
255
+
256
+ # Iterate through the registered company names
257
+ extra_origins = []
258
+ all_company_instances = get_company_registry().get_all_company_instances()
259
+ for company_name, company_instance in all_company_instances.items():
260
+ if company_instance.company:
261
+ cors_origin = company_instance.company.parameters.get('cors_origin', [])
262
+ extra_origins += cors_origin
263
+
264
+ all_origins = default_origins + extra_origins
265
+
266
+ CORS(self.app,
267
+ supports_credentials=True,
268
+ origins=all_origins,
269
+ allow_headers=[
270
+ "Content-Type", "Authorization", "X-Requested-With",
271
+ "X-Chat-Token", "x-chat-token"
272
+ ],
273
+ methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
274
+
275
+ logging.info(f"✅ CORS configured for: {all_origins}")
276
+
277
+ def _configure_core_dependencies(self, injector: Injector):
278
+ """⚙️ Configures all system dependencies."""
279
+
280
+ # get the binder from injector
281
+ binder = injector.binder
282
+ try:
283
+ # Core dependencies
284
+ binder.bind(Flask, to=self.app)
285
+ binder.bind(DatabaseManager, to=self.db_manager, scope=singleton)
286
+
287
+ # Bind all application components by calling the specific methods
288
+ self._bind_repositories(binder)
289
+ self._bind_services(binder)
290
+ self._bind_infrastructure(binder)
291
+
292
+ logging.info("✅ Dependencies configured successfully")
293
+
294
+ except Exception as e:
295
+ logging.error(f"❌ Error configuring dependencies: {e}")
296
+ raise IAToolkitException(
297
+ IAToolkitException.ErrorType.CONFIG_ERROR,
298
+ f"❌ Error configuring dependencies: {e}"
299
+ )
300
+
301
+ def _bind_repositories(self, binder: Binder):
302
+ from iatoolkit.repositories.document_repo import DocumentRepo
303
+ from iatoolkit.repositories.profile_repo import ProfileRepo
304
+ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
305
+ from iatoolkit.repositories.vs_repo import VSRepo
306
+ from iatoolkit.repositories.filesystem_asset_repository import FileSystemAssetRepository
307
+
308
+ binder.bind(DocumentRepo, to=DocumentRepo)
309
+ binder.bind(ProfileRepo, to=ProfileRepo)
310
+ binder.bind(LLMQueryRepo, to=LLMQueryRepo)
311
+ binder.bind(VSRepo, to=VSRepo)
312
+
313
+ # this class can be setup befor by iatoolkit enterprise
314
+ if not is_bound(self._injector, AssetRepository):
315
+ binder.bind(AssetRepository, to=FileSystemAssetRepository)
316
+
317
+ def _bind_services(self, binder: Binder):
318
+ from iatoolkit.services.query_service import QueryService
319
+ from iatoolkit.services.benchmark_service import BenchmarkService
320
+ from iatoolkit.services.document_service import DocumentService
321
+ from iatoolkit.services.prompt_service import PromptService
322
+ from iatoolkit.services.excel_service import ExcelService
323
+ from iatoolkit.services.mail_service import MailService
324
+ from iatoolkit.services.load_documents_service import LoadDocumentsService
325
+ from iatoolkit.services.profile_service import ProfileService
326
+ from iatoolkit.services.jwt_service import JWTService
327
+ from iatoolkit.services.dispatcher_service import Dispatcher
328
+ from iatoolkit.services.branding_service import BrandingService
329
+ from iatoolkit.services.i18n_service import I18nService
330
+ from iatoolkit.services.language_service import LanguageService
331
+ from iatoolkit.services.configuration_service import ConfigurationService
332
+ from iatoolkit.services.embedding_service import EmbeddingService
333
+ from iatoolkit.services.history_manager_service import HistoryManagerService
334
+ from iatoolkit.services.tool_service import ToolService
335
+ from iatoolkit.services.llm_client_service import llmClient
336
+ from iatoolkit.services.auth_service import AuthService
337
+ from iatoolkit.services.sql_service import SqlService
338
+
339
+ binder.bind(QueryService, to=QueryService)
340
+ binder.bind(BenchmarkService, to=BenchmarkService)
341
+ binder.bind(DocumentService, to=DocumentService)
342
+ binder.bind(PromptService, to=PromptService)
343
+ binder.bind(ExcelService, to=ExcelService)
344
+ binder.bind(MailService, to=MailService)
345
+ binder.bind(LoadDocumentsService, to=LoadDocumentsService)
346
+ binder.bind(ProfileService, to=ProfileService)
347
+ binder.bind(JWTService, to=JWTService)
348
+ binder.bind(Dispatcher, to=Dispatcher)
349
+ binder.bind(BrandingService, to=BrandingService)
350
+ binder.bind(I18nService, to=I18nService)
351
+ binder.bind(LanguageService, to=LanguageService)
352
+ binder.bind(ConfigurationService, to=ConfigurationService)
353
+ binder.bind(EmbeddingService, to=EmbeddingService)
354
+ binder.bind(HistoryManagerService, to=HistoryManagerService)
355
+ binder.bind(ToolService, to=ToolService)
356
+ binder.bind(llmClient, to=llmClient)
357
+ binder.bind(AuthService, to=AuthService)
358
+ binder.bind(SqlService, to=SqlService)
359
+
360
+ def _bind_infrastructure(self, binder: Binder):
361
+ from iatoolkit.infra.llm_proxy import LLMProxy
362
+ from iatoolkit.infra.google_chat_app import GoogleChatApp
363
+ from iatoolkit.infra.brevo_mail_app import BrevoMailApp
364
+ from iatoolkit.common.util import Utility
365
+ from iatoolkit.common.model_registry import ModelRegistry
366
+
367
+ binder.bind(LLMProxy, to=LLMProxy)
368
+ binder.bind(GoogleChatApp, to=GoogleChatApp)
369
+ binder.bind(BrevoMailApp, to=BrevoMailApp)
370
+ binder.bind(Utility, to=Utility)
371
+ binder.bind(ModelRegistry, to=ModelRegistry)
372
+
373
+ def _setup_additional_services(self):
374
+ Bcrypt(self.app)
375
+
376
+ def _instantiate_company_instances(self):
377
+ from iatoolkit.company_registry import get_company_registry
378
+
379
+ # instantiate all the registered companies
380
+ get_company_registry().instantiate_companies(self._injector)
381
+
382
+ def _load_company_configuration(self):
383
+ from iatoolkit.services.dispatcher_service import Dispatcher
384
+
385
+ # use the dispatcher to load the company config.yaml file and prepare the execution
386
+ dispatcher = self._injector.get(Dispatcher)
387
+ dispatcher.load_company_configs()
388
+
389
+ def _setup_cli_commands(self):
390
+ from iatoolkit.cli_commands import register_core_commands
391
+ from iatoolkit.company_registry import get_company_registry
392
+
393
+ # 1. Register core commands
394
+ register_core_commands(self.app)
395
+ logging.info("✅ Core CLI commands registered.")
396
+
397
+ # 2. Register company-specific commands
398
+ try:
399
+ # Iterate through the registered company names
400
+ all_company_instances = get_company_registry().get_all_company_instances()
401
+ for company_name, company_instance in all_company_instances.items():
402
+ if hasattr(company_instance, "register_cli_commands"):
403
+ company_instance.register_cli_commands(self.app)
404
+
405
+ except Exception as e:
406
+ logging.error(f"❌ error while registering company commands: {e}")
407
+
408
+ def _setup_context_processors(self):
409
+ # Configura context processors para templates
410
+ @self.app.context_processor
411
+ def inject_globals():
412
+ from iatoolkit.common.session_manager import SessionManager
413
+ from iatoolkit.services.profile_service import ProfileService
414
+ from iatoolkit.services.i18n_service import I18nService
415
+
416
+ # Get services from the injector
417
+ profile_service = self._injector.get(ProfileService)
418
+ i18n_service = self._injector.get(I18nService)
419
+
420
+ # The 't' function wrapper no longer needs to determine the language itself.
421
+ # It will be automatically handled by the refactored I18nService.
422
+ def translate_for_template(key: str, **kwargs):
423
+ return i18n_service.t(key, **kwargs)
424
+
425
+ # Get user profile if a session exists
426
+ user_profile = profile_service.get_current_session_info().get('profile', {})
427
+
428
+ return {
429
+ 'url_for': url_for,
430
+ 'iatoolkit_version': f'{self.version}',
431
+ 'license': self.license,
432
+ 'app_name': 'IAToolkit',
433
+ 'user_identifier': SessionManager.get('user_identifier'),
434
+ 'company_short_name': SessionManager.get('company_short_name'),
435
+ 'user_role': user_profile.get('user_role'),
436
+ 'user_is_local': user_profile.get('user_is_local'),
437
+ 'user_email': user_profile.get('user_email'),
438
+ 'iatoolkit_base_url': request.url_root,
439
+ 'flashed_messages': get_flashed_messages(with_categories=True),
440
+ 't': translate_for_template,
441
+ 'google_analytics_id': self._get_config_value('GOOGLE_ANALYTICS_ID', ''),
442
+ }
443
+
444
+ def _get_default_static_folder(self) -> str:
445
+ try:
446
+ current_dir = os.path.dirname(os.path.abspath(__file__)) # .../src/iatoolkit
447
+ return os.path.join(current_dir, "static")
448
+ except:
449
+ return 'static'
450
+
451
+ def _get_default_template_folder(self) -> str:
452
+ try:
453
+ current_dir = os.path.dirname(os.path.abspath(__file__)) # .../src/iatoolkit
454
+ return os.path.join(current_dir, "templates")
455
+ except:
456
+ return 'templates'
457
+
458
+ def get_injector(self) -> Injector:
459
+ """Obtiene el injector actual"""
460
+ if not self._injector:
461
+ raise IAToolkitException(
462
+ IAToolkitException.ErrorType.CONFIG_ERROR,
463
+ f"❌ injector not initialized"
464
+ )
465
+ return self._injector
466
+
467
+ def get_dispatcher(self):
468
+ from iatoolkit.services.dispatcher_service import Dispatcher
469
+ if not self._injector:
470
+ raise IAToolkitException(
471
+ IAToolkitException.ErrorType.CONFIG_ERROR,
472
+ "App no initialized. Call create_app() first"
473
+ )
474
+ return self._injector.get(Dispatcher)
475
+
476
+ def get_database_manager(self) -> DatabaseManager:
477
+ if not self.db_manager:
478
+ raise IAToolkitException(
479
+ IAToolkitException.ErrorType.CONFIG_ERROR,
480
+ "Database manager not initialized."
481
+ )
482
+ return self.db_manager
483
+
484
+ def _setup_download_dir(self):
485
+ # 1. set the default download directory
486
+ default_download_dir = os.path.join(os.getcwd(), 'iatoolkit-downloads')
487
+
488
+ # 3. if user specified one, use it
489
+ download_dir = self._get_config_value('IATOOLKIT_DOWNLOAD_DIR', default_download_dir)
490
+
491
+ # 3. save it in the app config
492
+ self.app.config['IATOOLKIT_DOWNLOAD_DIR'] = download_dir
493
+
494
+ # 4. make sure the directory exists
495
+ try:
496
+ os.makedirs(download_dir, exist_ok=True)
497
+ except OSError as e:
498
+ raise IAToolkitException(
499
+ IAToolkitException.ErrorType.CONFIG_ERROR,
500
+ "No se pudo crear el directorio de descarga. Verifique que el directorio existe y tenga permisos de escritura."
501
+ )
502
+ logging.info(f"✅ download dir created in: {download_dir}")
503
+
504
+
505
+
506
+ def current_iatoolkit() -> IAToolkit:
507
+ return IAToolkit.get_instance()
508
+
509
+ # Función de conveniencia para inicialización rápida
510
+ def create_app(config: Optional[Dict[str, Any]] = None) -> Flask:
511
+ toolkit = IAToolkit(config)
512
+ toolkit.create_iatoolkit()
513
+
514
+ return toolkit.app
@@ -0,0 +1,5 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
@@ -0,0 +1,123 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ import sib_api_v3_sdk
7
+ from sib_api_v3_sdk.rest import ApiException
8
+ from iatoolkit.common.exceptions import IAToolkitException
9
+ import os
10
+ import base64
11
+ import logging
12
+
13
+ MAX_ATTACH_BYTES = int(os.getenv("BREVO_MAX_ATTACH_BYTES", str(5 * 1024 * 1024))) # 5MB seguro
14
+
15
+
16
+ class BrevoMailApp:
17
+ def _init_brevo(self, provider_config: dict, sender: dict = None):
18
+ # config and init the brevo client
19
+ self.configuration = sib_api_v3_sdk.Configuration()
20
+ self.configuration.api_key['api-key'] = provider_config.get("api_key")
21
+ self.mail_api = sib_api_v3_sdk.TransactionalEmailsApi(sib_api_v3_sdk.ApiClient(self.configuration))
22
+
23
+
24
+ @staticmethod
25
+ def _strip_data_url_prefix(b64: str) -> str:
26
+ if not isinstance(b64, str):
27
+ return b64
28
+ i = b64.find("base64,")
29
+ return b64[i + 7:] if i != -1 else b64
30
+
31
+ def _normalize_attachments(self, attachments: list[dict] | None):
32
+ if not attachments:
33
+ return None
34
+ sdk_attachments = []
35
+ for idx, a in enumerate(attachments, start=1):
36
+ # 1) claves obligatorias
37
+ if "filename" not in a:
38
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
39
+ f"Adjunto #{idx} inválido: falta 'filename'")
40
+ if "content" not in a:
41
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
42
+ f"Adjunto '{a.get('filename', '(sin nombre)')}' inválido: falta 'content'")
43
+
44
+ name = a["filename"]
45
+ content_b64 = a["content"]
46
+
47
+ # 2) quitar prefijo data URL si vino así
48
+ content_b64 = self._strip_data_url_prefix(content_b64)
49
+
50
+ # 3) validar base64 (y que no esté vacío)
51
+ try:
52
+ raw = base64.b64decode(content_b64, validate=True)
53
+ except Exception:
54
+ logging.error("Adjunto '%s' con base64 inválido (primeros 16 chars: %r)",
55
+ name, str(content_b64)[:16])
56
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
57
+ f"Adjunto '{name}' trae base64 inválido")
58
+
59
+ if not raw:
60
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
61
+ f"Adjunto '{name}' está vacío")
62
+
63
+ # 4) volver a base64 limpio (sin prefijos, sin espacios)
64
+ clean_b64 = base64.b64encode(raw).decode("utf-8")
65
+
66
+ # 5) construir objeto del SDK
67
+ sdk_attachments.append(
68
+ sib_api_v3_sdk.SendSmtpEmailAttachment(
69
+ name=name,
70
+ content=clean_b64
71
+ )
72
+ )
73
+ return sdk_attachments
74
+
75
+
76
+ def send_email(self,
77
+ provider_config: dict,
78
+ to: str,
79
+ subject: str,
80
+ body: str,
81
+ sender: dict,
82
+ attachments: list[dict] = None):
83
+
84
+ if not provider_config.get("api_key"):
85
+ logging.error(f'Try to send brevo_mail without api_key in provider_config')
86
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
87
+ f"Invalid mail configuration for Brevo: missing api-key")
88
+
89
+ # init the Brevo client
90
+ self._init_brevo(provider_config)
91
+
92
+ try:
93
+ sdk_attachments = self._normalize_attachments(attachments)
94
+ email = sib_api_v3_sdk.SendSmtpEmail(
95
+ to=[{"email": to}],
96
+ sender=sender,
97
+ subject=subject,
98
+ html_content=body,
99
+ attachment=sdk_attachments
100
+ )
101
+ api_response = self.mail_api.send_transac_email(email)
102
+
103
+ # Validación de respuesta
104
+ message_id = getattr(api_response, "message_id", None) or getattr(api_response, "messageId", None)
105
+ message_ids = getattr(api_response, "message_ids", None) or getattr(api_response, "messageIds", None)
106
+ if not ((isinstance(message_id, str) and message_id.strip()) or
107
+ (isinstance(message_ids, (list, tuple)) and len(message_ids) > 0)):
108
+ logging.error("MAIL ERROR: Respuesta sin message_id(s): %r", api_response)
109
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
110
+ "Brevo no retornó message_id; el envío podría haber fallado.")
111
+
112
+ return api_response
113
+
114
+ except ApiException as e:
115
+ logging.exception("MAIL ERROR (ApiException): status=%s reason=%s body=%s",
116
+ getattr(e, "status", None), getattr(e, "reason", None), getattr(e, "body", None))
117
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
118
+ f"Error Brevo (status={getattr(e, 'status', 'N/A')}): {getattr(e, 'reason', str(e))}") from e
119
+ except Exception as e:
120
+ logging.exception("MAIL ERROR: %s", str(e))
121
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
122
+ f"No se pudo enviar correo: {str(e)}") from e
123
+