iatoolkit 0.3.1__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 ADDED
@@ -0,0 +1,41 @@
1
+ """
2
+ IAToolkit Package
3
+ """
4
+
5
+ # Expose main classes and functions at the top level of the package
6
+
7
+ # Assuming 'toolkit.py' contains the IAToolkit class
8
+ from .iatoolkit import IAToolkit, create_app
9
+ from .iatoolkit import current_iatoolkit
10
+
11
+ # Assuming 'app_factory.py' contains create_app and register_company
12
+ from .company_registry import register_company
13
+
14
+ # Assuming 'base_company.py' contains BaseCompany
15
+ from .base_company import BaseCompany
16
+
17
+ # --- Services ---
18
+ # Assuming they are in a 'services' sub-package
19
+ from services.sql_service import SqlService
20
+ from services.excel_service import ExcelService
21
+ from services.dispatcher_service import Dispatcher
22
+ from services.document_service import DocumentService
23
+ from services.search_service import SearchService
24
+ from repositories.profile_repo import ProfileRepo
25
+ from repositories.database_manager import DatabaseManager
26
+
27
+
28
+ __all__ = [
29
+ 'IAToolkit',
30
+ 'create_app',
31
+ 'current_iatoolkit',
32
+ 'register_company',
33
+ 'BaseCompany',
34
+ 'SqlService',
35
+ 'ExcelService',
36
+ 'Dispatcher',
37
+ 'DocumentService',
38
+ 'SearchService',
39
+ 'ProfileRepo',
40
+ 'DatabaseManager',
41
+ ]
@@ -0,0 +1,42 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Producto: IAToolkit
3
+ # Todos los derechos reservados.
4
+ # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
5
+
6
+ # companies/base_company.py
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any
9
+
10
+
11
+ class BaseCompany(ABC):
12
+ def __init__(self, profile_repo: Any = None, llm_query_repo: Any = None):
13
+ self.profile_repo = profile_repo
14
+ self.llm_query_repo = llm_query_repo
15
+
16
+ @abstractmethod
17
+ # initialize all the database tables needed
18
+ def init_db(self):
19
+ raise NotImplementedError("La subclase debe implementar el método init_db()")
20
+
21
+ @abstractmethod
22
+ # get context specific for this company
23
+ def get_company_context(self, **kwargs) -> str:
24
+ raise NotImplementedError("La subclase debe implementar el método get_company_context()")
25
+
26
+ @abstractmethod
27
+ # execute the specific action configured in the intent table
28
+ def handle_request(self, tag: str, params: dict) -> dict:
29
+ raise NotImplementedError("La subclase debe implementar el método handle_request()")
30
+
31
+ @abstractmethod
32
+ # get context specific for the query
33
+ def start_execution(self):
34
+ raise NotImplementedError("La subclase debe implementar el método start_execution()")
35
+
36
+ @abstractmethod
37
+ # get context specific for the query
38
+ def get_metadata_from_filename(self, filename: str) -> dict:
39
+ raise NotImplementedError("La subclase debe implementar el método get_query_context()")
40
+
41
+ def unsupported_operation(self, tag):
42
+ raise NotImplementedError(f"La operación '{tag}' no está soportada por esta empresa.")
@@ -0,0 +1,98 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Producto: IAToolkit
3
+
4
+ from typing import Dict, Type, Any
5
+ from .base_company import BaseCompany
6
+ import logging
7
+
8
+
9
+ class CompanyRegistry:
10
+ """
11
+ Registro centralizado de empresas para iatoolkit.
12
+
13
+ Permite a los clientes registrar sus clases de empresa de forma explícita
14
+ en lugar de usar autodiscovery.
15
+ """
16
+
17
+ def __init__(self):
18
+ self._company_classes: Dict[str, Type[BaseCompany]] = {}
19
+ self._company_instances: Dict[str, BaseCompany] = {}
20
+ self._injector = None
21
+
22
+ def register_company(self, name: str, company_class: Type[BaseCompany]) -> None:
23
+ """
24
+ Registra una clase de empresa.
25
+
26
+ Args:
27
+ name: Nombre de la empresa (ej: 'maxxa')
28
+ company_class: Clase que hereda de BaseCompany
29
+ """
30
+ if not issubclass(company_class, BaseCompany):
31
+ raise ValueError(f"La clase {company_class.__name__} debe heredar de BaseCompany")
32
+
33
+ company_key = name.lower()
34
+ self._company_classes[company_key] = company_class
35
+
36
+ logging.info(f"Empresa registrada: {company_key} -> {company_class.__name__}")
37
+
38
+ def set_injector(self, injector) -> None:
39
+ """Establece el injector para crear instancias con dependencias"""
40
+ self._injector = injector
41
+
42
+ def instantiate_companies(self) -> Dict[str, BaseCompany]:
43
+ """
44
+ Instancia todas las empresas registradas con inyección de dependencias.
45
+
46
+ Returns:
47
+ Dict con instancias de empresas {name: instance}
48
+ """
49
+ if not self._injector:
50
+ raise RuntimeError("Injector no configurado. Llame a set_injector() primero.")
51
+
52
+ for company_key, company_class in self._company_classes.items():
53
+ if company_key not in self._company_instances:
54
+ try:
55
+ # use de injector to create the instance
56
+ company_instance = self._injector.get(company_class)
57
+ self._company_instances[company_key] = company_instance
58
+ logging.info(f"company '{company_key}' created in dispatcher")
59
+
60
+ except Exception as e:
61
+ logging.error(f"Error instanciando empresa {company_key}: {e}")
62
+ logging.exception(e)
63
+ raise
64
+
65
+ return self._company_instances.copy()
66
+
67
+ def get_registered_companies(self) -> Dict[str, Type[BaseCompany]]:
68
+ """Retorna las clases registradas"""
69
+ return self._company_classes.copy()
70
+
71
+ def get_company_instances(self) -> Dict[str, BaseCompany]:
72
+ """Retorna las instancias de empresas"""
73
+ return self._company_instances.copy()
74
+
75
+ def clear(self) -> None:
76
+ """Limpia el registro (útil para tests)"""
77
+ self._company_classes.clear()
78
+ self._company_instances.clear()
79
+
80
+
81
+ # Instancia global del registry
82
+ _company_registry = CompanyRegistry()
83
+
84
+
85
+ def register_company(name: str, company_class: Type[BaseCompany]) -> None:
86
+ """
87
+ Función pública para registrar empresas.
88
+
89
+ Args:
90
+ name: Nombre de la empresa
91
+ company_class: Clase que hereda de BaseCompany
92
+ """
93
+ _company_registry.register_company(name, company_class)
94
+
95
+
96
+ def get_company_registry() -> CompanyRegistry:
97
+ """Obtiene el registry global"""
98
+ return _company_registry
iatoolkit/iatoolkit.py ADDED
@@ -0,0 +1,405 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Producto: IAToolkit Core
3
+ # Framework opensource para chatbots empresariales con IA
4
+
5
+ from flask import Flask, url_for, current_app
6
+ from flask_session import Session
7
+ from flask_injector import FlaskInjector
8
+ from flask_bcrypt import Bcrypt
9
+ from flask_cors import CORS
10
+ from common.auth import IAuthentication
11
+ from common.util import Utility
12
+ from common.exceptions import IAToolkitException
13
+ from common.session_manager import SessionManager
14
+ from urllib.parse import urlparse
15
+ import redis
16
+ import logging
17
+ import os
18
+ import click
19
+ from typing import Optional, Dict, Any
20
+ from repositories.database_manager import DatabaseManager
21
+ from injector import Binder, singleton, Injector
22
+ from .toolkit_config import IAToolkitConfig
23
+
24
+ VERSION = "2.0.0"
25
+
26
+ # global variable for yhe unique instance of IAToolkit
27
+ _iatoolkit_instance: Optional['IAToolkit'] = None
28
+
29
+
30
+ class IAToolkit:
31
+ """
32
+ IAToolkit main class
33
+ """
34
+ def __new__(cls, config: Optional[Dict[str, Any]] = None):
35
+ """
36
+ Implementa el patrón Singleton
37
+ """
38
+ global _iatoolkit_instance
39
+ if _iatoolkit_instance is None:
40
+ _iatoolkit_instance = super().__new__(cls)
41
+ _iatoolkit_instance._initialized = False
42
+ return _iatoolkit_instance
43
+
44
+
45
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
46
+ """
47
+ Args:
48
+ config: Diccionario opcional de configuración que sobrescribe variables de entorno
49
+ """
50
+ if self._initialized:
51
+ return
52
+
53
+ self.config = config or {}
54
+ self.app: Optional[Flask] = None
55
+ self.db_manager: Optional[DatabaseManager] = None
56
+ self._injector: Optional[Injector] = None
57
+
58
+ @classmethod
59
+ def get_instance(cls) -> 'IAToolkit':
60
+ """
61
+ Obtiene la instancia única de IAToolkit
62
+ """
63
+ global _iatoolkit_instance
64
+ if _iatoolkit_instance is None:
65
+ _iatoolkit_instance = cls()
66
+ return _iatoolkit_instance
67
+
68
+ def create_iatoolkit(self):
69
+ """
70
+ Creates, configures, and returns the Flask application instance.
71
+ his is the main entry point for the application factory.
72
+ """
73
+ self._setup_logging()
74
+
75
+ # Step 1: Create the Flask app instance
76
+ self._create_flask_instance()
77
+
78
+ # Step 2: Set up the core components that DI depends on
79
+ self._setup_database()
80
+
81
+ # Step 3: Create the Injector with CORE dependencies (NO VIEWS)
82
+ toolkit_config_module = IAToolkitConfig(app=self.app, db_manager=self.db_manager)
83
+ self._injector = Injector([
84
+ toolkit_config_module,
85
+ self._configure_core_dependencies # This method binds services, repos, etc.
86
+ ])
87
+
88
+ # Step 4: Register routes using the fully configured injector
89
+ self._register_routes()
90
+
91
+ # Step 5: Initialize FlaskInjector. This is now primarily for request-scoped injections
92
+ # and other integrations, as views are handled manually.
93
+ FlaskInjector(app=self.app, injector=self._injector)
94
+
95
+ # Step 6: Finalize setup within the application context
96
+ self._setup_redis_sessions()
97
+ self._setup_cors()
98
+ self._setup_additional_services()
99
+ self._setup_cli_commands()
100
+ self._setup_context_processors()
101
+
102
+ logging.info(f"🎉 IAToolkit v{VERSION} inicializado correctamente")
103
+ return self.app
104
+
105
+
106
+ def _get_config_value(self, key: str, default=None):
107
+ """Obtiene un valor de configuración, primero del dict config, luego de env vars"""
108
+ return self.config.get(key, os.getenv(key, default))
109
+
110
+ def _setup_logging(self):
111
+ log_level_str = self._get_config_value('FLASK_ENV', 'production')
112
+ log_level = logging.INFO if log_level_str in ('dev', 'development') else logging.WARNING
113
+
114
+ logging.basicConfig(
115
+ level=log_level,
116
+ format="%(asctime)s - IATOOLKIT - %(name)s - %(levelname)s - %(message)s",
117
+ handlers=[logging.StreamHandler()],
118
+ force=True
119
+ )
120
+
121
+ def _register_routes(self):
122
+ """Registers routes by passing the configured injector."""
123
+ from common.routes import register_views
124
+
125
+ # Pass the injector to the view registration function
126
+ register_views(self._injector, self.app)
127
+
128
+ logging.info("✅ Routes registered.")
129
+
130
+ def _create_flask_instance(self):
131
+ static_folder = self._get_config_value('STATIC_FOLDER') or self._get_default_static_folder()
132
+ template_folder = self._get_config_value('TEMPLATE_FOLDER') or self._get_default_template_folder()
133
+
134
+ self.app = Flask(__name__,
135
+ static_folder=static_folder,
136
+ template_folder=template_folder)
137
+
138
+ is_https = self._get_config_value('USE_HTTPS', 'false').lower() == 'true'
139
+ is_dev = self._get_config_value('FLASK_ENV') == 'development'
140
+
141
+ self.app.config.update({
142
+ 'VERSION': VERSION,
143
+ 'SECRET_KEY': self._get_config_value('FLASK_SECRET_KEY', 'iatoolkit-default-secret'),
144
+ 'SESSION_COOKIE_SAMESITE': "None" if is_https else "Lax",
145
+ 'SESSION_COOKIE_SECURE': is_https,
146
+ 'SESSION_PERMANENT': False,
147
+ 'SESSION_USE_SIGNER': True,
148
+ 'JWT_SECRET_KEY': self._get_config_value('JWT_SECRET_KEY', 'iatoolkit-jwt-secret'),
149
+ 'JWT_ALGORITHM': 'HS256',
150
+ 'JWT_EXPIRATION_SECONDS_CHAT': int(self._get_config_value('JWT_EXPIRATION_SECONDS_CHAT', 3600))
151
+ })
152
+
153
+ # Configuración para tokenizers en desarrollo
154
+ if is_dev:
155
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
156
+
157
+ def _setup_database(self):
158
+ database_uri = self._get_config_value('DATABASE_URI')
159
+ if not database_uri:
160
+ raise IAToolkitException(
161
+ IAToolkitException.ErrorType.CONFIG_ERROR,
162
+ "DATABASE_URI es requerida (config dict o variable de entorno)"
163
+ )
164
+
165
+ self.db_manager = DatabaseManager(database_uri)
166
+ self.db_manager.create_all()
167
+ logging.info("✅ Base de datos configurada correctamente")
168
+
169
+ def _setup_redis_sessions(self):
170
+ redis_url = self._get_config_value('REDIS_URL')
171
+ if not redis_url:
172
+ logging.warning("⚠️ REDIS_URL no configurada, usando sesiones en memoria")
173
+ return
174
+
175
+ try:
176
+ url = urlparse(redis_url)
177
+ redis_instance = redis.Redis(
178
+ host=url.hostname,
179
+ port=url.port,
180
+ password=url.password,
181
+ ssl=(url.scheme == "rediss"),
182
+ ssl_cert_reqs=None
183
+ )
184
+
185
+ self.app.config.update({
186
+ 'SESSION_TYPE': 'redis',
187
+ 'SESSION_REDIS': redis_instance
188
+ })
189
+
190
+ Session(self.app)
191
+ logging.info("✅ Redis y sesiones configurados correctamente")
192
+
193
+ except Exception as e:
194
+ logging.error(f"❌ Error configurando Redis: {e}")
195
+ logging.warning("⚠️ Continuando sin Redis")
196
+
197
+ def _setup_cors(self):
198
+ """🌐 Configura CORS"""
199
+ # Origins por defecto para desarrollo
200
+ default_origins = [
201
+ "http://localhost:3000",
202
+ "http://localhost:5001",
203
+ "http://127.0.0.1:5001"
204
+ ]
205
+
206
+ # Obtener origins adicionales desde configuración/env
207
+ extra_origins = []
208
+ for i in range(1, 11): # Soporte para CORS_ORIGIN_1 a CORS_ORIGIN_10
209
+ origin = self._get_config_value(f'CORS_ORIGIN_{i}')
210
+ if origin:
211
+ extra_origins.append(origin)
212
+
213
+ all_origins = default_origins + extra_origins
214
+
215
+ CORS(self.app,
216
+ supports_credentials=True,
217
+ origins=all_origins,
218
+ allow_headers=[
219
+ "Content-Type", "Authorization", "X-Requested-With",
220
+ "X-Chat-Token", "x-chat-token"
221
+ ],
222
+ methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
223
+
224
+ logging.info(f"✅ CORS configurado para: {all_origins}")
225
+
226
+
227
+ def _configure_core_dependencies(self, binder: Binder):
228
+ """⚙️ Configures all system dependencies."""
229
+ try:
230
+ # Core dependencies
231
+ binder.bind(Injector, to=self._injector, scope=singleton)
232
+
233
+ # Bind all application components by calling the specific methods
234
+ self._bind_repositories(binder)
235
+ self._bind_services(binder)
236
+ self._bind_infrastructure(binder)
237
+ self._bind_views(binder)
238
+
239
+ logging.info("✅ Dependencias configuradas correctamente")
240
+
241
+ except Exception as e:
242
+ logging.error(f"❌ Error configurando dependencias: {e}")
243
+ raise IAToolkitException(
244
+ IAToolkitException.ErrorType.CONFIG_ERROR,
245
+ f"❌ Error configurando dependencias: {e}"
246
+ )
247
+
248
+ def _bind_repositories(self, binder: Binder):
249
+ from repositories.document_repo import DocumentRepo
250
+ from repositories.document_type_repo import DocumentTypeRepo
251
+ from repositories.profile_repo import ProfileRepo
252
+ from repositories.llm_query_repo import LLMQueryRepo
253
+ from repositories.vs_repo import VSRepo
254
+ from repositories.tasks_repo import TaskRepo
255
+
256
+ binder.bind(DocumentRepo, to=DocumentRepo)
257
+ binder.bind(DocumentTypeRepo, to=DocumentTypeRepo)
258
+ binder.bind(ProfileRepo, to=ProfileRepo)
259
+ binder.bind(LLMQueryRepo, to=LLMQueryRepo)
260
+ binder.bind(VSRepo, to=VSRepo)
261
+ binder.bind(TaskRepo, to=TaskRepo)
262
+
263
+ def _bind_services(self, binder: Binder):
264
+ from services.query_service import QueryService
265
+ from services.tasks_service import TaskService
266
+ from services.benchmark_service import BenchmarkService
267
+ from services.document_service import DocumentService
268
+ from services.prompt_manager_service import PromptService
269
+ from services.excel_service import ExcelService
270
+ from services.mail_service import MailService
271
+ from services.load_documents_service import LoadDocumentsService
272
+ from services.profile_service import ProfileService
273
+ from services.jwt_service import JWTService
274
+ from services.dispatcher_service import Dispatcher
275
+
276
+ binder.bind(QueryService, to=QueryService)
277
+ binder.bind(TaskService, to=TaskService)
278
+ binder.bind(BenchmarkService, to=BenchmarkService)
279
+ binder.bind(DocumentService, to=DocumentService)
280
+ binder.bind(PromptService, to=PromptService)
281
+ binder.bind(ExcelService, to=ExcelService)
282
+ binder.bind(MailService, to=MailService)
283
+ binder.bind(LoadDocumentsService, to=LoadDocumentsService)
284
+ binder.bind(ProfileService, to=ProfileService)
285
+ binder.bind(JWTService, to=JWTService)
286
+ binder.bind(Dispatcher, to=Dispatcher, scope=singleton)
287
+
288
+ def _bind_infrastructure(self, binder: Binder):
289
+ from infra.llm_client import llmClient
290
+ from infra.llm_proxy import LLMProxy
291
+ from infra.google_chat_app import GoogleChatApp
292
+ from infra.mail_app import MailApp
293
+
294
+ binder.bind(LLMProxy, to=LLMProxy, scope=singleton)
295
+ binder.bind(llmClient, to=llmClient, scope=singleton)
296
+ binder.bind(GoogleChatApp, to=GoogleChatApp)
297
+ binder.bind(MailApp, to=MailApp)
298
+ binder.bind(IAuthentication, to=IAuthentication)
299
+ binder.bind(Utility, to=Utility)
300
+
301
+ def _bind_views(self, binder: Binder):
302
+ """Vincula las vistas después de que el injector ha sido creado"""
303
+ from views.llmquery_view import LLMQueryView
304
+ from views.home_view import HomeView
305
+ from views.chat_view import ChatView
306
+ from views.change_password_view import ChangePasswordView
307
+
308
+ binder.bind(HomeView, to=HomeView)
309
+ binder.bind(ChatView, to=ChatView)
310
+ binder.bind(ChangePasswordView, to=ChangePasswordView)
311
+ binder.bind(LLMQueryView, to=LLMQueryView)
312
+
313
+ logging.info("✅ Views configuradas correctamente")
314
+
315
+ def _setup_additional_services(self):
316
+ Bcrypt(self.app)
317
+
318
+ def _setup_cli_commands(self):
319
+ """⌨️ Configura comandos CLI básicos"""
320
+
321
+ @self.app.cli.command("init-db")
322
+ def init_db():
323
+ """🗄️ Inicializa la base de datos del sistema"""
324
+ try:
325
+ from services.dispatcher_service import Dispatcher
326
+ dispatcher = self._get_injector().get(Dispatcher)
327
+
328
+ click.echo("🚀 Inicializando base de datos...")
329
+ dispatcher.init_db()
330
+ click.echo("✅ Base de datos inicializada correctamente")
331
+
332
+ except Exception as e:
333
+ logging.exception(e)
334
+ click.echo(f"❌ Error: {e}")
335
+
336
+
337
+ def _setup_context_processors(self):
338
+ # Configura context processors para templates
339
+ @self.app.context_processor
340
+ def inject_globals():
341
+ return {
342
+ 'url_for': url_for,
343
+ 'iatoolkit_version': VERSION,
344
+ 'app_name': 'IAToolkit',
345
+ 'user': SessionManager.get('user'),
346
+ 'user_company': SessionManager.get('company_short_name'),
347
+ }
348
+
349
+ def _get_default_static_folder(self) -> str:
350
+ try:
351
+ current_dir = os.path.dirname(os.path.abspath(__file__)) # .../src/iatoolkit
352
+ src_dir = os.path.dirname(current_dir) # .../src
353
+ return os.path.join(src_dir, "static")
354
+ except:
355
+ return 'static'
356
+
357
+ def _get_default_template_folder(self) -> str:
358
+ try:
359
+ current_dir = os.path.dirname(os.path.abspath(__file__)) # .../src/iatoolkit
360
+ src_dir = os.path.dirname(current_dir) # .../src
361
+ return os.path.join(src_dir, "templates")
362
+ except:
363
+ return 'templates'
364
+
365
+ def _get_injector(self) -> Injector:
366
+ """Obtiene el injector actual"""
367
+ if not self._injector:
368
+ raise IAToolkitException(
369
+ IAToolkitException.ErrorType.CONFIG_ERROR,
370
+ f"❌ injector not initialized"
371
+ )
372
+ return self._injector
373
+
374
+ def get_dispatcher(self):
375
+ from services.dispatcher_service import Dispatcher
376
+ if not self._injector:
377
+ raise IAToolkitException(
378
+ IAToolkitException.ErrorType.CONFIG_ERROR,
379
+ "App no inicializada. Llame a create_app() primero"
380
+ )
381
+ return self._injector.get(Dispatcher)
382
+
383
+ def get_database_manager(self) -> DatabaseManager:
384
+ if not self.db_manager:
385
+ raise IAToolkitException(
386
+ IAToolkitException.ErrorType.CONFIG_ERROR,
387
+ "Database manager no inicializado"
388
+ )
389
+ return self.db_manager
390
+
391
+
392
+ def current_iatoolkit() -> IAToolkit:
393
+ return IAToolkit.get_instance()
394
+
395
+ # 🚀 Función de conveniencia para inicialización rápida
396
+ def create_app(config: Optional[Dict[str, Any]] = None) -> Flask:
397
+ toolkit = IAToolkit(config)
398
+ toolkit.create_iatoolkit()
399
+
400
+ return toolkit.app
401
+
402
+ if __name__ == "__main__":
403
+ app = create_app()
404
+ if app:
405
+ app.run(debug=True)
@@ -0,0 +1,13 @@
1
+ # src/iatoolkit/toolkit_config.py
2
+ from injector import Module, Binder, singleton
3
+ from flask import Flask
4
+ from repositories.database_manager import DatabaseManager
5
+
6
+ class IAToolkitConfig(Module):
7
+ def __init__(self, app: Flask, db_manager: DatabaseManager):
8
+ self.app = app
9
+ self.db_manager = db_manager
10
+
11
+ def configure(self, binder: Binder):
12
+ binder.bind(Flask, to=self.app, scope=singleton)
13
+ binder.bind(DatabaseManager, to=self.db_manager, scope=singleton)