iatoolkit 0.11.0__py3-none-any.whl → 0.71.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iatoolkit/__init__.py +2 -6
- iatoolkit/base_company.py +9 -29
- iatoolkit/cli_commands.py +1 -1
- iatoolkit/common/routes.py +96 -52
- iatoolkit/common/session_manager.py +2 -1
- iatoolkit/common/util.py +17 -27
- iatoolkit/company_registry.py +1 -2
- iatoolkit/iatoolkit.py +97 -53
- iatoolkit/infra/llm_client.py +15 -20
- iatoolkit/infra/llm_proxy.py +38 -10
- iatoolkit/infra/openai_adapter.py +1 -1
- iatoolkit/infra/redis_session_manager.py +48 -2
- iatoolkit/locales/en.yaml +167 -0
- iatoolkit/locales/es.yaml +163 -0
- iatoolkit/repositories/database_manager.py +23 -3
- iatoolkit/repositories/document_repo.py +1 -1
- iatoolkit/repositories/models.py +35 -10
- iatoolkit/repositories/profile_repo.py +3 -2
- iatoolkit/repositories/vs_repo.py +26 -20
- iatoolkit/services/auth_service.py +193 -0
- iatoolkit/services/branding_service.py +70 -25
- iatoolkit/services/company_context_service.py +155 -0
- iatoolkit/services/configuration_service.py +133 -0
- iatoolkit/services/dispatcher_service.py +80 -105
- iatoolkit/services/document_service.py +5 -2
- iatoolkit/services/embedding_service.py +146 -0
- iatoolkit/services/excel_service.py +30 -26
- iatoolkit/services/file_processor_service.py +4 -12
- iatoolkit/services/history_service.py +7 -16
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +18 -29
- iatoolkit/services/language_service.py +83 -0
- iatoolkit/services/load_documents_service.py +100 -113
- iatoolkit/services/mail_service.py +9 -4
- iatoolkit/services/profile_service.py +152 -76
- iatoolkit/services/prompt_manager_service.py +20 -16
- iatoolkit/services/query_service.py +208 -96
- iatoolkit/services/search_service.py +11 -4
- iatoolkit/services/sql_service.py +57 -25
- iatoolkit/services/tasks_service.py +1 -1
- iatoolkit/services/user_feedback_service.py +72 -34
- iatoolkit/services/user_session_context_service.py +112 -54
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -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 +135 -222
- 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 +35 -0
- iatoolkit/static/styles/chat_iatoolkit.css +289 -210
- iatoolkit/static/styles/chat_modal.css +63 -77
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/query_main.prompt +5 -22
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +40 -20
- iatoolkit/templates/change_password.html +57 -36
- iatoolkit/templates/chat.html +180 -86
- iatoolkit/templates/chat_modals.html +138 -68
- iatoolkit/templates/error.html +44 -8
- iatoolkit/templates/forgot_password.html +40 -23
- iatoolkit/templates/index.html +145 -0
- iatoolkit/templates/login_simulation.html +45 -0
- iatoolkit/templates/onboarding_shell.html +107 -0
- iatoolkit/templates/signup.html +63 -65
- iatoolkit/views/base_login_view.py +91 -0
- iatoolkit/views/change_password_view.py +56 -31
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/external_login_view.py +61 -28
- iatoolkit/views/{file_store_view.py → file_store_api_view.py} +10 -3
- iatoolkit/views/forgot_password_view.py +27 -21
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +50 -23
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +58 -0
- iatoolkit/views/login_simulation_view.py +93 -0
- iatoolkit/views/login_view.py +130 -37
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/{prompt_view.py → prompt_api_view.py} +10 -10
- iatoolkit/views/signup_view.py +41 -36
- iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/verify_user_view.py +34 -29
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/METADATA +41 -23
- iatoolkit-0.71.2.dist-info/RECORD +122 -0
- iatoolkit-0.71.2.dist-info/licenses/LICENSE +21 -0
- iatoolkit/common/auth.py +0 -200
- iatoolkit/static/images/arrow_up.png +0 -0
- iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
- iatoolkit/static/images/logo_clinica.png +0 -0
- iatoolkit/static/images/logo_iatoolkit.png +0 -0
- iatoolkit/static/images/logo_maxxa.png +0 -0
- iatoolkit/static/images/logo_notaria.png +0 -0
- iatoolkit/static/images/logo_tarjeta.png +0 -0
- iatoolkit/static/images/logo_umayor.png +0 -0
- iatoolkit/static/images/upload.png +0 -0
- iatoolkit/static/js/chat_feedback.js +0 -115
- iatoolkit/static/js/chat_history.js +0 -117
- iatoolkit/static/styles/chat_info.css +0 -53
- iatoolkit/templates/header.html +0 -31
- iatoolkit/templates/home.html +0 -199
- iatoolkit/templates/login.html +0 -43
- iatoolkit/templates/test.html +0 -9
- iatoolkit/views/chat_token_request_view.py +0 -98
- iatoolkit/views/chat_view.py +0 -58
- iatoolkit/views/download_file_view.py +0 -58
- iatoolkit/views/external_chat_login_view.py +0 -95
- iatoolkit/views/history_view.py +0 -57
- iatoolkit/views/llmquery_view.py +0 -65
- iatoolkit/views/tasks_review_view.py +0 -83
- iatoolkit/views/user_feedback_view.py +0 -74
- iatoolkit-0.11.0.dist-info/RECORD +0 -110
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/WHEEL +0 -0
- {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/top_level.txt +0 -0
iatoolkit/iatoolkit.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from flask import Flask, url_for
|
|
6
|
+
from flask import Flask, url_for, get_flashed_messages
|
|
7
7
|
from flask_session import Session
|
|
8
8
|
from flask_injector import FlaskInjector
|
|
9
9
|
from flask_bcrypt import Bcrypt
|
|
@@ -15,10 +15,11 @@ import logging
|
|
|
15
15
|
import os
|
|
16
16
|
from typing import Optional, Dict, Any
|
|
17
17
|
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
18
|
-
|
|
19
|
-
from injector import Binder,
|
|
18
|
+
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
19
|
+
from injector import Binder, Injector, singleton
|
|
20
20
|
from importlib.metadata import version as _pkg_version, PackageNotFoundError
|
|
21
21
|
|
|
22
|
+
IATOOLKIT_VERSION = "0.71.2"
|
|
22
23
|
|
|
23
24
|
# global variable for the unique instance of IAToolkit
|
|
24
25
|
_iatoolkit_instance: Optional['IAToolkit'] = None
|
|
@@ -51,7 +52,7 @@ class IAToolkit:
|
|
|
51
52
|
self.app = None
|
|
52
53
|
self.db_manager = None
|
|
53
54
|
self._injector = None
|
|
54
|
-
self.version =
|
|
55
|
+
self.version = IATOOLKIT_VERSION # default version
|
|
55
56
|
|
|
56
57
|
@classmethod
|
|
57
58
|
def get_instance(cls) -> 'IAToolkit':
|
|
@@ -87,24 +88,23 @@ class IAToolkit:
|
|
|
87
88
|
# and other integrations, as views are handled manually.
|
|
88
89
|
FlaskInjector(app=self.app, injector=self._injector)
|
|
89
90
|
|
|
90
|
-
# Step 6: initialize dispatcher and registered
|
|
91
|
+
# Step 6: initialize dispatcher and registered companies
|
|
91
92
|
self._init_dispatcher_and_company_instances()
|
|
92
93
|
|
|
94
|
+
# Re-apply logging configuration in case it was modified by company-specific code
|
|
95
|
+
self._setup_logging()
|
|
96
|
+
|
|
93
97
|
# Step 7: Finalize setup within the application context
|
|
94
98
|
self._setup_redis_sessions()
|
|
95
99
|
self._setup_cors()
|
|
96
100
|
self._setup_additional_services()
|
|
97
101
|
self._setup_cli_commands()
|
|
102
|
+
self._setup_request_globals()
|
|
98
103
|
self._setup_context_processors()
|
|
99
104
|
|
|
100
105
|
# Step 8: define the download_dir for excel's
|
|
101
106
|
self._setup_download_dir()
|
|
102
107
|
|
|
103
|
-
try:
|
|
104
|
-
self.version = _pkg_version("iatoolkit")
|
|
105
|
-
except PackageNotFoundError:
|
|
106
|
-
pass
|
|
107
|
-
|
|
108
108
|
logging.info(f"🎉 IAToolkit v{self.version} inicializado correctamente")
|
|
109
109
|
self._initialized = True
|
|
110
110
|
return self.app
|
|
@@ -113,9 +113,26 @@ class IAToolkit:
|
|
|
113
113
|
# get a value from the config dict or the environment variable
|
|
114
114
|
return self.config.get(key, os.getenv(key, default))
|
|
115
115
|
|
|
116
|
+
def _setup_request_globals(self):
|
|
117
|
+
"""
|
|
118
|
+
Configures functions to run before each request to set up
|
|
119
|
+
request-global variables, such as language.
|
|
120
|
+
"""
|
|
121
|
+
injector = self._injector
|
|
122
|
+
|
|
123
|
+
@self.app.before_request
|
|
124
|
+
def set_request_language():
|
|
125
|
+
"""
|
|
126
|
+
Determines and caches the language for the current request in g.lang.
|
|
127
|
+
"""
|
|
128
|
+
from iatoolkit.services.language_service import LanguageService
|
|
129
|
+
language_service = injector.get(LanguageService)
|
|
130
|
+
language_service.get_current_language()
|
|
131
|
+
|
|
116
132
|
def _setup_logging(self):
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
# Lee el nivel de log desde una variable de entorno, con 'INFO' como valor por defecto.
|
|
134
|
+
log_level_name = os.getenv('LOG_LEVEL', 'INFO').upper()
|
|
135
|
+
log_level = getattr(logging, log_level_name, logging.INFO)
|
|
119
136
|
|
|
120
137
|
logging.basicConfig(
|
|
121
138
|
level=log_level,
|
|
@@ -141,14 +158,17 @@ class IAToolkit:
|
|
|
141
158
|
static_folder=static_folder,
|
|
142
159
|
template_folder=template_folder)
|
|
143
160
|
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
# get the IATOOLKIT_VERSION from the package metadata
|
|
162
|
+
try:
|
|
163
|
+
self.version = _pkg_version("iatoolkit")
|
|
164
|
+
except PackageNotFoundError:
|
|
165
|
+
pass
|
|
146
166
|
|
|
147
167
|
self.app.config.update({
|
|
148
168
|
'VERSION': self.version,
|
|
149
169
|
'SECRET_KEY': self._get_config_value('FLASK_SECRET_KEY', 'iatoolkit-default-secret'),
|
|
150
|
-
'SESSION_COOKIE_SAMESITE': "None"
|
|
151
|
-
'SESSION_COOKIE_SECURE':
|
|
170
|
+
'SESSION_COOKIE_SAMESITE': "None",
|
|
171
|
+
'SESSION_COOKIE_SECURE': True,
|
|
152
172
|
'SESSION_PERMANENT': False,
|
|
153
173
|
'SESSION_USE_SIGNER': True,
|
|
154
174
|
'JWT_SECRET_KEY': self._get_config_value('JWT_SECRET_KEY', 'iatoolkit-jwt-secret'),
|
|
@@ -156,8 +176,15 @@ class IAToolkit:
|
|
|
156
176
|
'JWT_EXPIRATION_SECONDS_CHAT': int(self._get_config_value('JWT_EXPIRATION_SECONDS_CHAT', 3600))
|
|
157
177
|
})
|
|
158
178
|
|
|
179
|
+
parsed_url = urlparse(os.getenv('IATOOLKIT_BASE_URL'))
|
|
180
|
+
if parsed_url.scheme == 'https':
|
|
181
|
+
self.app.config['PREFERRED_URL_SCHEME'] = 'https'
|
|
182
|
+
|
|
183
|
+
# 2. ProxyFix para no tener problemas con iframes y rutas
|
|
184
|
+
self.app.wsgi_app = ProxyFix(self.app.wsgi_app, x_proto=1)
|
|
185
|
+
|
|
159
186
|
# Configuración para tokenizers en desarrollo
|
|
160
|
-
if
|
|
187
|
+
if self._get_config_value('FLASK_ENV') == 'dev':
|
|
161
188
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
162
189
|
|
|
163
190
|
def _setup_database(self):
|
|
@@ -172,6 +199,15 @@ class IAToolkit:
|
|
|
172
199
|
self.db_manager.create_all()
|
|
173
200
|
logging.info("✅ Base de datos configurada correctamente")
|
|
174
201
|
|
|
202
|
+
@self.app.teardown_appcontext
|
|
203
|
+
def remove_session(exception=None):
|
|
204
|
+
"""
|
|
205
|
+
Flask calls this after each request.
|
|
206
|
+
It ensures the SQLAlchemy session is properly closed
|
|
207
|
+
and the DB connection is returned to the pool.
|
|
208
|
+
"""
|
|
209
|
+
self.db_manager.scoped_session.remove()
|
|
210
|
+
|
|
175
211
|
def _setup_redis_sessions(self):
|
|
176
212
|
redis_url = self._get_config_value('REDIS_URL')
|
|
177
213
|
if not redis_url:
|
|
@@ -202,19 +238,19 @@ class IAToolkit:
|
|
|
202
238
|
|
|
203
239
|
def _setup_cors(self):
|
|
204
240
|
"""🌐 Configura CORS"""
|
|
205
|
-
|
|
241
|
+
from iatoolkit.company_registry import get_company_registry
|
|
242
|
+
|
|
243
|
+
# default CORS origin
|
|
206
244
|
default_origins = [
|
|
207
|
-
"http://localhost:5001",
|
|
208
|
-
"http://127.0.0.1:5001",
|
|
209
245
|
os.getenv('IATOOLKIT_BASE_URL')
|
|
210
246
|
]
|
|
211
247
|
|
|
212
|
-
#
|
|
248
|
+
# Iterate through the registered company names
|
|
213
249
|
extra_origins = []
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
250
|
+
all_company_instances = get_company_registry().get_all_company_instances()
|
|
251
|
+
for company_name, company_instance in all_company_instances.items():
|
|
252
|
+
cors_origin = company_instance.company.parameters.get('cors_origin', [])
|
|
253
|
+
extra_origins += cors_origin
|
|
218
254
|
|
|
219
255
|
all_origins = default_origins + extra_origins
|
|
220
256
|
|
|
@@ -229,7 +265,6 @@ class IAToolkit:
|
|
|
229
265
|
|
|
230
266
|
logging.info(f"✅ CORS configurado para: {all_origins}")
|
|
231
267
|
|
|
232
|
-
|
|
233
268
|
def _configure_core_dependencies(self, binder: Binder):
|
|
234
269
|
"""⚙️ Configures all system dependencies."""
|
|
235
270
|
try:
|
|
@@ -241,7 +276,6 @@ class IAToolkit:
|
|
|
241
276
|
self._bind_repositories(binder)
|
|
242
277
|
self._bind_services(binder)
|
|
243
278
|
self._bind_infrastructure(binder)
|
|
244
|
-
self._bind_views(binder)
|
|
245
279
|
|
|
246
280
|
logging.info("✅ Dependencias configuradas correctamente")
|
|
247
281
|
|
|
@@ -256,7 +290,6 @@ class IAToolkit:
|
|
|
256
290
|
from iatoolkit.repositories.document_repo import DocumentRepo
|
|
257
291
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
258
292
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
259
|
-
|
|
260
293
|
from iatoolkit.repositories.vs_repo import VSRepo
|
|
261
294
|
from iatoolkit.repositories.tasks_repo import TaskRepo
|
|
262
295
|
|
|
@@ -279,6 +312,10 @@ class IAToolkit:
|
|
|
279
312
|
from iatoolkit.services.jwt_service import JWTService
|
|
280
313
|
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
281
314
|
from iatoolkit.services.branding_service import BrandingService
|
|
315
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
316
|
+
from iatoolkit.services.language_service import LanguageService
|
|
317
|
+
from iatoolkit.services.configuration_service import ConfigurationService
|
|
318
|
+
from iatoolkit.services.embedding_service import EmbeddingService
|
|
282
319
|
|
|
283
320
|
binder.bind(QueryService, to=QueryService)
|
|
284
321
|
binder.bind(TaskService, to=TaskService)
|
|
@@ -292,39 +329,26 @@ class IAToolkit:
|
|
|
292
329
|
binder.bind(JWTService, to=JWTService)
|
|
293
330
|
binder.bind(Dispatcher, to=Dispatcher)
|
|
294
331
|
binder.bind(BrandingService, to=BrandingService)
|
|
295
|
-
|
|
332
|
+
binder.bind(I18nService, to=I18nService)
|
|
333
|
+
binder.bind(LanguageService, to=LanguageService)
|
|
334
|
+
binder.bind(ConfigurationService, to=ConfigurationService)
|
|
335
|
+
binder.bind(EmbeddingService, to=EmbeddingService)
|
|
296
336
|
|
|
297
337
|
def _bind_infrastructure(self, binder: Binder):
|
|
298
338
|
from iatoolkit.infra.llm_client import llmClient
|
|
299
339
|
from iatoolkit.infra.llm_proxy import LLMProxy
|
|
300
340
|
from iatoolkit.infra.google_chat_app import GoogleChatApp
|
|
301
341
|
from iatoolkit.infra.mail_app import MailApp
|
|
302
|
-
from iatoolkit.
|
|
342
|
+
from iatoolkit.services.auth_service import AuthService
|
|
303
343
|
from iatoolkit.common.util import Utility
|
|
304
344
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
binder.bind(LLMProxy, to=LLMProxy, scope=singleton)
|
|
308
|
-
binder.bind(llmClient, to=llmClient, scope=singleton)
|
|
345
|
+
binder.bind(LLMProxy, to=LLMProxy)
|
|
346
|
+
binder.bind(llmClient, to=llmClient)
|
|
309
347
|
binder.bind(GoogleChatApp, to=GoogleChatApp)
|
|
310
348
|
binder.bind(MailApp, to=MailApp)
|
|
311
|
-
binder.bind(
|
|
349
|
+
binder.bind(AuthService, to=AuthService)
|
|
312
350
|
binder.bind(Utility, to=Utility)
|
|
313
351
|
|
|
314
|
-
def _bind_views(self, binder: Binder):
|
|
315
|
-
"""Vincula las vistas después de que el injector ha sido creado"""
|
|
316
|
-
from iatoolkit.views.llmquery_view import LLMQueryView
|
|
317
|
-
from iatoolkit.views.home_view import HomeView
|
|
318
|
-
from iatoolkit.views.chat_view import ChatView
|
|
319
|
-
from iatoolkit.views.change_password_view import ChangePasswordView
|
|
320
|
-
|
|
321
|
-
binder.bind(HomeView, to=HomeView)
|
|
322
|
-
binder.bind(ChatView, to=ChatView)
|
|
323
|
-
binder.bind(ChangePasswordView, to=ChangePasswordView)
|
|
324
|
-
binder.bind(LLMQueryView, to=LLMQueryView)
|
|
325
|
-
|
|
326
|
-
logging.info("✅ Views configuradas correctamente")
|
|
327
|
-
|
|
328
352
|
def _setup_additional_services(self):
|
|
329
353
|
Bcrypt(self.app)
|
|
330
354
|
|
|
@@ -335,9 +359,9 @@ class IAToolkit:
|
|
|
335
359
|
# instantiate all the registered companies
|
|
336
360
|
get_company_registry().instantiate_companies(self._injector)
|
|
337
361
|
|
|
338
|
-
# use the dispatcher to
|
|
362
|
+
# use the dispatcher to load the company config.yaml file and prepare the execution
|
|
339
363
|
dispatcher = self._injector.get(Dispatcher)
|
|
340
|
-
dispatcher.
|
|
364
|
+
dispatcher.load_company_configs()
|
|
341
365
|
|
|
342
366
|
def _setup_cli_commands(self):
|
|
343
367
|
from iatoolkit.cli_commands import register_core_commands
|
|
@@ -362,12 +386,32 @@ class IAToolkit:
|
|
|
362
386
|
@self.app.context_processor
|
|
363
387
|
def inject_globals():
|
|
364
388
|
from iatoolkit.common.session_manager import SessionManager
|
|
389
|
+
from iatoolkit.services.profile_service import ProfileService
|
|
390
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
391
|
+
|
|
392
|
+
# Get services from the injector
|
|
393
|
+
profile_service = self._injector.get(ProfileService)
|
|
394
|
+
i18n_service = self._injector.get(I18nService)
|
|
395
|
+
|
|
396
|
+
# The 't' function wrapper no longer needs to determine the language itself.
|
|
397
|
+
# It will be automatically handled by the refactored I18nService.
|
|
398
|
+
def translate_for_template(key: str, **kwargs):
|
|
399
|
+
return i18n_service.t(key, **kwargs)
|
|
400
|
+
|
|
401
|
+
# Get user profile if a session exists
|
|
402
|
+
user_profile = profile_service.get_current_session_info().get('profile', {})
|
|
403
|
+
|
|
365
404
|
return {
|
|
366
405
|
'url_for': url_for,
|
|
367
406
|
'iatoolkit_version': self.version,
|
|
368
407
|
'app_name': 'IAToolkit',
|
|
369
|
-
'
|
|
370
|
-
'
|
|
408
|
+
'user_identifier': SessionManager.get('user_identifier'),
|
|
409
|
+
'company_short_name': SessionManager.get('company_short_name'),
|
|
410
|
+
'user_is_local': user_profile.get('user_is_local'),
|
|
411
|
+
'user_email': user_profile.get('user_email'),
|
|
412
|
+
'iatoolkit_base_url': os.environ.get('IATOOLKIT_BASE_URL', ''),
|
|
413
|
+
'flashed_messages': get_flashed_messages(with_categories=True),
|
|
414
|
+
't': translate_for_template
|
|
371
415
|
}
|
|
372
416
|
|
|
373
417
|
def _get_default_static_folder(self) -> str:
|
iatoolkit/infra/llm_client.py
CHANGED
|
@@ -21,6 +21,7 @@ import tiktoken
|
|
|
21
21
|
from typing import Dict, Optional, List
|
|
22
22
|
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
23
23
|
|
|
24
|
+
CONTEXT_ERROR_MESSAGE = 'Tu consulta supera el límite de contexto, utiliza el boton de recarga de contexto.'
|
|
24
25
|
|
|
25
26
|
class llmClient:
|
|
26
27
|
_llm_clients_cache = {} # class attribute, for the clients cache
|
|
@@ -37,12 +38,6 @@ class llmClient:
|
|
|
37
38
|
self.util = util
|
|
38
39
|
self._dispatcher = None # Cache for the lazy-loaded dispatcher
|
|
39
40
|
|
|
40
|
-
# get the model from the environment variable
|
|
41
|
-
self.model = os.getenv("LLM_MODEL", "")
|
|
42
|
-
if not self.model:
|
|
43
|
-
raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
|
|
44
|
-
"La variable de entorno 'LLM_MODEL' no está configurada.")
|
|
45
|
-
|
|
46
41
|
# library for counting tokens
|
|
47
42
|
self.encoding = tiktoken.encoding_for_model("gpt-4o")
|
|
48
43
|
|
|
@@ -69,6 +64,7 @@ class llmClient:
|
|
|
69
64
|
context: str,
|
|
70
65
|
tools: list[dict],
|
|
71
66
|
text: dict,
|
|
67
|
+
model: str,
|
|
72
68
|
context_history: Optional[List[Dict]] = None,
|
|
73
69
|
) -> dict:
|
|
74
70
|
|
|
@@ -79,13 +75,13 @@ class llmClient:
|
|
|
79
75
|
force_tool_name = None
|
|
80
76
|
reasoning = {}
|
|
81
77
|
|
|
82
|
-
if 'gpt-5' in
|
|
78
|
+
if 'gpt-5' in model:
|
|
83
79
|
text['verbosity'] = "low"
|
|
84
80
|
reasoning = {"effort": 'minimal'}
|
|
85
81
|
|
|
86
82
|
try:
|
|
87
83
|
start_time = time.time()
|
|
88
|
-
logging.info(f"calling llm model '{
|
|
84
|
+
logging.info(f"calling llm model '{model}' with {self.count_tokens(context)} tokens...")
|
|
89
85
|
|
|
90
86
|
# get the proxy for the company
|
|
91
87
|
llm_proxy = self.llm_proxy_factory.create_for_company(company)
|
|
@@ -98,7 +94,7 @@ class llmClient:
|
|
|
98
94
|
}]
|
|
99
95
|
|
|
100
96
|
response = llm_proxy.create_response(
|
|
101
|
-
model=
|
|
97
|
+
model=model,
|
|
102
98
|
previous_response_id=previous_response_id,
|
|
103
99
|
context_history=context_history,
|
|
104
100
|
input=input_messages,
|
|
@@ -116,7 +112,7 @@ class llmClient:
|
|
|
116
112
|
|
|
117
113
|
# in case of context error
|
|
118
114
|
if "context_length_exceeded" in str(e):
|
|
119
|
-
error_message =
|
|
115
|
+
error_message = CONTEXT_ERROR_MESSAGE
|
|
120
116
|
|
|
121
117
|
raise IAToolkitException(IAToolkitException.ErrorType.LLM_ERROR, error_message)
|
|
122
118
|
|
|
@@ -135,7 +131,7 @@ class llmClient:
|
|
|
135
131
|
logging.info(f"start execution fcall: {function_name}")
|
|
136
132
|
try:
|
|
137
133
|
result = self.dispatcher.dispatch(
|
|
138
|
-
|
|
134
|
+
company_short_name=company.short_name,
|
|
139
135
|
action=function_name,
|
|
140
136
|
**args
|
|
141
137
|
)
|
|
@@ -185,7 +181,7 @@ class llmClient:
|
|
|
185
181
|
tool_choice_value = "required"
|
|
186
182
|
|
|
187
183
|
response = llm_proxy.create_response(
|
|
188
|
-
model=
|
|
184
|
+
model=model,
|
|
189
185
|
input=input_messages,
|
|
190
186
|
previous_response_id=response.id,
|
|
191
187
|
context_history=context_history,
|
|
@@ -199,7 +195,7 @@ class llmClient:
|
|
|
199
195
|
# save the statistices
|
|
200
196
|
stats['response_time']=int(time.time() - start_time)
|
|
201
197
|
stats['sql_retry_count'] = sql_retry_count
|
|
202
|
-
stats['model'] =
|
|
198
|
+
stats['model'] = model
|
|
203
199
|
|
|
204
200
|
# decode the LLM response
|
|
205
201
|
decoded_response = self.decode_response(response)
|
|
@@ -256,25 +252,23 @@ class llmClient:
|
|
|
256
252
|
|
|
257
253
|
# in case of context error
|
|
258
254
|
if "context_length_exceeded" in str(e):
|
|
259
|
-
error_message =
|
|
255
|
+
error_message = CONTEXT_ERROR_MESSAGE
|
|
260
256
|
elif "string_above_max_length" in str(e):
|
|
261
|
-
error_message = 'La respuesta es muy
|
|
257
|
+
error_message = 'La respuesta es muy extensa, trata de filtrar/restringuir tu consulta'
|
|
262
258
|
|
|
263
259
|
raise IAToolkitException(IAToolkitException.ErrorType.LLM_ERROR, error_message)
|
|
264
260
|
|
|
265
261
|
def set_company_context(self,
|
|
266
262
|
company: Company,
|
|
267
263
|
company_base_context: str,
|
|
268
|
-
model
|
|
264
|
+
model) -> str:
|
|
269
265
|
|
|
270
|
-
|
|
271
|
-
self.model = model
|
|
272
|
-
logging.info(f"initializing model '{self.model}' with company context: {self.count_tokens(company_base_context)} tokens...")
|
|
266
|
+
logging.info(f"initializing model '{model}' with company context: {self.count_tokens(company_base_context)} tokens...")
|
|
273
267
|
|
|
274
268
|
llm_proxy = self.llm_proxy_factory.create_for_company(company)
|
|
275
269
|
try:
|
|
276
270
|
response = llm_proxy.create_response(
|
|
277
|
-
model=
|
|
271
|
+
model=model,
|
|
278
272
|
input=[{
|
|
279
273
|
"role": "system",
|
|
280
274
|
"content": company_base_context
|
|
@@ -393,6 +387,7 @@ class llmClient:
|
|
|
393
387
|
|
|
394
388
|
def add_stats(self, stats1: dict, stats2: dict) -> dict:
|
|
395
389
|
stats_dict = {
|
|
390
|
+
"model": stats1.get('model', ''),
|
|
396
391
|
"input_tokens": stats1.get('input_tokens', 0) + stats2.get('input_tokens', 0),
|
|
397
392
|
"output_tokens": stats1.get('output_tokens', 0) + stats2.get('output_tokens', 0),
|
|
398
393
|
"total_tokens": stats1.get('total_tokens', 0) + stats2.get('total_tokens', 0),
|
iatoolkit/infra/llm_proxy.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import Dict, List, Any
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from iatoolkit.common.util import Utility
|
|
9
9
|
from iatoolkit.infra.llm_response import LLMResponse
|
|
10
|
+
from iatoolkit.services.configuration_service import ConfigurationService
|
|
10
11
|
from iatoolkit.infra.openai_adapter import OpenAIAdapter
|
|
11
12
|
from iatoolkit.infra.gemini_adapter import GeminiAdapter
|
|
12
13
|
from iatoolkit.common.exceptions import IAToolkitException
|
|
@@ -41,12 +42,16 @@ class LLMProxy:
|
|
|
41
42
|
_clients_cache_lock = threading.Lock()
|
|
42
43
|
|
|
43
44
|
@inject
|
|
44
|
-
def __init__(self, util: Utility,
|
|
45
|
+
def __init__(self, util: Utility,
|
|
46
|
+
configuration_service: ConfigurationService,
|
|
47
|
+
openai_client = None,
|
|
48
|
+
gemini_client = None):
|
|
45
49
|
"""
|
|
46
50
|
Inicializa una instancia del proxy. Puede ser una instancia "base" (fábrica)
|
|
47
51
|
o una instancia de "trabajo" con clientes configurados.
|
|
48
52
|
"""
|
|
49
53
|
self.util = util
|
|
54
|
+
self.configuration_service = configuration_service
|
|
50
55
|
self.openai_adapter = OpenAIAdapter(openai_client) if openai_client else None
|
|
51
56
|
self.gemini_adapter = GeminiAdapter(gemini_client) if gemini_client else None
|
|
52
57
|
|
|
@@ -71,7 +76,11 @@ class LLMProxy:
|
|
|
71
76
|
)
|
|
72
77
|
|
|
73
78
|
# Devuelve una NUEVA instancia con los clientes configurados
|
|
74
|
-
return LLMProxy(
|
|
79
|
+
return LLMProxy(
|
|
80
|
+
util=self.util,
|
|
81
|
+
configuration_service=self.configuration_service,
|
|
82
|
+
openai_client=openai_client,
|
|
83
|
+
gemini_client=gemini_client)
|
|
75
84
|
|
|
76
85
|
def create_response(self, model: str, input: List[Dict], **kwargs) -> LLMResponse:
|
|
77
86
|
"""Enruta la llamada al adaptador correcto basado en el modelo."""
|
|
@@ -103,7 +112,7 @@ class LLMProxy:
|
|
|
103
112
|
elif provider == LLMProvider.GEMINI:
|
|
104
113
|
client = self._create_gemini_client(company)
|
|
105
114
|
else:
|
|
106
|
-
raise IAToolkitException(f"
|
|
115
|
+
raise IAToolkitException(f"provider not supported: {provider.value}")
|
|
107
116
|
|
|
108
117
|
if client:
|
|
109
118
|
LLMProxy._clients_cache[cache_key] = client
|
|
@@ -115,22 +124,41 @@ class LLMProxy:
|
|
|
115
124
|
|
|
116
125
|
def _create_openai_client(self, company: Company) -> OpenAI:
|
|
117
126
|
"""Crea un cliente de OpenAI con la API key."""
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
decrypted_api_key = ''
|
|
128
|
+
llm_config = self.configuration_service.get_configuration(company.short_name, 'llm')
|
|
129
|
+
|
|
130
|
+
# Try to get API key name from config first
|
|
131
|
+
if llm_config and llm_config.get('api-key'):
|
|
132
|
+
api_key_env_var = llm_config['api-key']
|
|
133
|
+
decrypted_api_key = os.getenv(api_key_env_var, '')
|
|
120
134
|
else:
|
|
121
|
-
|
|
135
|
+
# Fallback to old logic
|
|
136
|
+
if company.openai_api_key:
|
|
137
|
+
decrypted_api_key = self.util.decrypt_key(company.openai_api_key)
|
|
138
|
+
else:
|
|
139
|
+
decrypted_api_key = os.getenv("OPENAI_API_KEY", '')
|
|
140
|
+
|
|
122
141
|
if not decrypted_api_key:
|
|
123
142
|
raise IAToolkitException(IAToolkitException.ErrorType.API_KEY,
|
|
124
|
-
|
|
143
|
+
f"La empresa '{company.name}' no tiene API key de OpenAI.")
|
|
125
144
|
return OpenAI(api_key=decrypted_api_key)
|
|
126
145
|
|
|
127
146
|
def _create_gemini_client(self, company: Company) -> Any:
|
|
128
147
|
"""Configura y devuelve el cliente de Gemini."""
|
|
129
148
|
|
|
130
|
-
|
|
131
|
-
|
|
149
|
+
decrypted_api_key = ''
|
|
150
|
+
llm_config = self.configuration_service.get_configuration(company.short_name, 'llm')
|
|
151
|
+
|
|
152
|
+
# Try to get API key name from config first
|
|
153
|
+
if llm_config and llm_config.get('api-key'):
|
|
154
|
+
api_key_env_var = llm_config['api-key']
|
|
155
|
+
decrypted_api_key = os.getenv(api_key_env_var, '')
|
|
132
156
|
else:
|
|
133
|
-
|
|
157
|
+
# Fallback to old logic
|
|
158
|
+
if company.gemini_api_key:
|
|
159
|
+
decrypted_api_key = self.util.decrypt_key(company.gemini_api_key)
|
|
160
|
+
else:
|
|
161
|
+
decrypted_api_key = os.getenv("GEMINI_API_KEY", '')
|
|
134
162
|
|
|
135
163
|
if not decrypted_api_key:
|
|
136
164
|
return None
|
|
@@ -55,7 +55,7 @@ class OpenAIAdapter:
|
|
|
55
55
|
|
|
56
56
|
# En caso de error de contexto
|
|
57
57
|
if "context_length_exceeded" in str(e):
|
|
58
|
-
error_message = 'Tu consulta supera el limite de contexto
|
|
58
|
+
error_message = 'Tu consulta supera el limite de contexto. Reinicia el contexto con el boton de la barra superior.'
|
|
59
59
|
|
|
60
60
|
raise IAToolkitException(IAToolkitException.ErrorType.LLM_ERROR, error_message)
|
|
61
61
|
|
|
@@ -37,9 +37,14 @@ class RedisSessionManager:
|
|
|
37
37
|
return cls._client
|
|
38
38
|
|
|
39
39
|
@classmethod
|
|
40
|
-
def set(cls, key: str, value: str,
|
|
40
|
+
def set(cls, key: str, value: str, **kwargs):
|
|
41
|
+
"""
|
|
42
|
+
Método set flexible que pasa argumentos opcionales (como ex, nx)
|
|
43
|
+
directamente al cliente de redis.
|
|
44
|
+
"""
|
|
41
45
|
client = cls._get_client()
|
|
42
|
-
|
|
46
|
+
# Pasa todos los argumentos de palabra clave adicionales al cliente real
|
|
47
|
+
result = client.set(key, value, **kwargs)
|
|
43
48
|
return result
|
|
44
49
|
|
|
45
50
|
@classmethod
|
|
@@ -49,12 +54,53 @@ class RedisSessionManager:
|
|
|
49
54
|
result = value if value is not None else default
|
|
50
55
|
return result
|
|
51
56
|
|
|
57
|
+
@classmethod
|
|
58
|
+
def hset(cls, key: str, field: str, value: str):
|
|
59
|
+
"""
|
|
60
|
+
Establece un campo en un Hash de Redis.
|
|
61
|
+
"""
|
|
62
|
+
client = cls._get_client()
|
|
63
|
+
return client.hset(key, field, value)
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def hget(cls, key: str, field: str):
|
|
67
|
+
"""
|
|
68
|
+
Obtiene el valor de un campo de un Hash de Redis.
|
|
69
|
+
Devuelve None si la clave o el campo no existen.
|
|
70
|
+
"""
|
|
71
|
+
client = cls._get_client()
|
|
72
|
+
return client.hget(key, field)
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def hdel(cls, key: str, *fields):
|
|
76
|
+
"""
|
|
77
|
+
Elimina uno o más campos de un Hash de Redis.
|
|
78
|
+
"""
|
|
79
|
+
client = cls._get_client()
|
|
80
|
+
return client.hdel(key, *fields)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def pipeline(cls):
|
|
84
|
+
"""
|
|
85
|
+
Inicia una transacción (pipeline) de Redis.
|
|
86
|
+
"""
|
|
87
|
+
client = cls._get_client()
|
|
88
|
+
return client.pipeline()
|
|
89
|
+
|
|
90
|
+
|
|
52
91
|
@classmethod
|
|
53
92
|
def remove(cls, key: str):
|
|
54
93
|
client = cls._get_client()
|
|
55
94
|
result = client.delete(key)
|
|
56
95
|
return result
|
|
57
96
|
|
|
97
|
+
@classmethod
|
|
98
|
+
def exists(cls, key: str) -> bool:
|
|
99
|
+
"""Verifica si una clave existe en Redis."""
|
|
100
|
+
client = cls._get_client()
|
|
101
|
+
# El comando EXISTS de Redis devuelve un entero (0 o 1). Lo convertimos a booleano.
|
|
102
|
+
return bool(client.exists(key))
|
|
103
|
+
|
|
58
104
|
@classmethod
|
|
59
105
|
def set_json(cls, key: str, value: dict, ex: int = None):
|
|
60
106
|
json_str = json.dumps(value)
|