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.
- iatoolkit/__init__.py +27 -35
- iatoolkit/base_company.py +3 -35
- iatoolkit/cli_commands.py +18 -47
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +48 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +39 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +138 -0
- iatoolkit/common/session_manager.py +26 -0
- iatoolkit/common/util.py +353 -0
- iatoolkit/company_registry.py +66 -29
- iatoolkit/core.py +514 -0
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/brevo_mail_app.py +123 -0
- iatoolkit/infra/call_service.py +140 -0
- iatoolkit/infra/connectors/__init__.py +5 -0
- iatoolkit/infra/connectors/file_connector.py +17 -0
- iatoolkit/infra/connectors/file_connector_factory.py +57 -0
- iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
- iatoolkit/infra/connectors/google_drive_connector.py +68 -0
- iatoolkit/infra/connectors/local_file_connector.py +46 -0
- iatoolkit/infra/connectors/s3_connector.py +33 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
- iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
- iatoolkit/infra/llm_proxy.py +268 -0
- iatoolkit/infra/llm_response.py +45 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +222 -0
- iatoolkit/locales/es.yaml +225 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +187 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +105 -0
- iatoolkit/repositories/models.py +279 -0
- iatoolkit/repositories/profile_repo.py +171 -0
- iatoolkit/repositories/vs_repo.py +150 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +7 -7
- iatoolkit/services/branding_service.py +153 -0
- iatoolkit/services/company_context_service.py +214 -0
- iatoolkit/services/configuration_service.py +375 -0
- iatoolkit/services/dispatcher_service.py +134 -0
- {services → iatoolkit/services}/document_service.py +20 -8
- iatoolkit/services/embedding_service.py +148 -0
- iatoolkit/services/excel_service.py +156 -0
- {services → iatoolkit/services}/file_processor_service.py +36 -21
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +80 -0
- iatoolkit/services/language_service.py +89 -0
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/services/llm_client_service.py +438 -0
- iatoolkit/services/load_documents_service.py +174 -0
- iatoolkit/services/mail_service.py +213 -0
- {services → iatoolkit/services}/profile_service.py +200 -101
- iatoolkit/services/prompt_service.py +303 -0
- iatoolkit/services/query_service.py +467 -0
- iatoolkit/services/search_service.py +55 -0
- iatoolkit/services/sql_service.py +169 -0
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +117 -0
- iatoolkit/services/user_session_context_service.py +213 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_filepond.js +85 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +110 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +401 -0
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +103 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +38 -0
- iatoolkit/static/styles/chat_iatoolkit.css +559 -0
- iatoolkit/static/styles/chat_modal.css +133 -0
- iatoolkit/static/styles/chat_public.css +135 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +398 -0
- iatoolkit/static/styles/llm_output.css +148 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +30 -23
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +45 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +78 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +337 -0
- iatoolkit/templates/chat_modals.html +185 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +51 -0
- iatoolkit/templates/onboarding_shell.html +106 -0
- iatoolkit/templates/signup.html +79 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +96 -0
- iatoolkit/views/change_password_view.py +116 -0
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +75 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +63 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +59 -0
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/load_document_api_view.py +65 -0
- iatoolkit/views/login_view.py +170 -0
- iatoolkit/views/logout_api_view.py +57 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +100 -0
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +60 -0
- iatoolkit-0.107.4.dist-info/METADATA +268 -0
- iatoolkit-0.107.4.dist-info/RECORD +132 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
- iatoolkit/iatoolkit.py +0 -413
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.3.9.dist-info/METADATA +0 -252
- iatoolkit-0.3.9.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/dispatcher_service.py +0 -351
- services/excel_service.py +0 -98
- services/history_service.py +0 -45
- services/jwt_service.py +0 -91
- services/load_documents_service.py +0 -212
- services/mail_service.py +0 -62
- services/prompt_manager_service.py +0 -172
- services/query_service.py +0 -334
- services/search_service.py +0 -32
- services/sql_service.py +0 -42
- services/tasks_service.py +0 -188
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {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,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
|
+
|