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.
Files changed (122) hide show
  1. iatoolkit/__init__.py +2 -6
  2. iatoolkit/base_company.py +9 -29
  3. iatoolkit/cli_commands.py +1 -1
  4. iatoolkit/common/routes.py +96 -52
  5. iatoolkit/common/session_manager.py +2 -1
  6. iatoolkit/common/util.py +17 -27
  7. iatoolkit/company_registry.py +1 -2
  8. iatoolkit/iatoolkit.py +97 -53
  9. iatoolkit/infra/llm_client.py +15 -20
  10. iatoolkit/infra/llm_proxy.py +38 -10
  11. iatoolkit/infra/openai_adapter.py +1 -1
  12. iatoolkit/infra/redis_session_manager.py +48 -2
  13. iatoolkit/locales/en.yaml +167 -0
  14. iatoolkit/locales/es.yaml +163 -0
  15. iatoolkit/repositories/database_manager.py +23 -3
  16. iatoolkit/repositories/document_repo.py +1 -1
  17. iatoolkit/repositories/models.py +35 -10
  18. iatoolkit/repositories/profile_repo.py +3 -2
  19. iatoolkit/repositories/vs_repo.py +26 -20
  20. iatoolkit/services/auth_service.py +193 -0
  21. iatoolkit/services/branding_service.py +70 -25
  22. iatoolkit/services/company_context_service.py +155 -0
  23. iatoolkit/services/configuration_service.py +133 -0
  24. iatoolkit/services/dispatcher_service.py +80 -105
  25. iatoolkit/services/document_service.py +5 -2
  26. iatoolkit/services/embedding_service.py +146 -0
  27. iatoolkit/services/excel_service.py +30 -26
  28. iatoolkit/services/file_processor_service.py +4 -12
  29. iatoolkit/services/history_service.py +7 -16
  30. iatoolkit/services/i18n_service.py +104 -0
  31. iatoolkit/services/jwt_service.py +18 -29
  32. iatoolkit/services/language_service.py +83 -0
  33. iatoolkit/services/load_documents_service.py +100 -113
  34. iatoolkit/services/mail_service.py +9 -4
  35. iatoolkit/services/profile_service.py +152 -76
  36. iatoolkit/services/prompt_manager_service.py +20 -16
  37. iatoolkit/services/query_service.py +208 -96
  38. iatoolkit/services/search_service.py +11 -4
  39. iatoolkit/services/sql_service.py +57 -25
  40. iatoolkit/services/tasks_service.py +1 -1
  41. iatoolkit/services/user_feedback_service.py +72 -34
  42. iatoolkit/services/user_session_context_service.py +112 -54
  43. iatoolkit/static/images/fernando.jpeg +0 -0
  44. iatoolkit/static/js/chat_feedback_button.js +80 -0
  45. iatoolkit/static/js/chat_help_content.js +124 -0
  46. iatoolkit/static/js/chat_history_button.js +110 -0
  47. iatoolkit/static/js/chat_logout_button.js +36 -0
  48. iatoolkit/static/js/chat_main.js +135 -222
  49. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  50. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  51. iatoolkit/static/js/chat_reload_button.js +35 -0
  52. iatoolkit/static/styles/chat_iatoolkit.css +289 -210
  53. iatoolkit/static/styles/chat_modal.css +63 -77
  54. iatoolkit/static/styles/chat_public.css +107 -0
  55. iatoolkit/static/styles/landing_page.css +182 -0
  56. iatoolkit/static/styles/onboarding.css +176 -0
  57. iatoolkit/system_prompts/query_main.prompt +5 -22
  58. iatoolkit/templates/_company_header.html +20 -0
  59. iatoolkit/templates/_login_widget.html +42 -0
  60. iatoolkit/templates/base.html +40 -20
  61. iatoolkit/templates/change_password.html +57 -36
  62. iatoolkit/templates/chat.html +180 -86
  63. iatoolkit/templates/chat_modals.html +138 -68
  64. iatoolkit/templates/error.html +44 -8
  65. iatoolkit/templates/forgot_password.html +40 -23
  66. iatoolkit/templates/index.html +145 -0
  67. iatoolkit/templates/login_simulation.html +45 -0
  68. iatoolkit/templates/onboarding_shell.html +107 -0
  69. iatoolkit/templates/signup.html +63 -65
  70. iatoolkit/views/base_login_view.py +91 -0
  71. iatoolkit/views/change_password_view.py +56 -31
  72. iatoolkit/views/embedding_api_view.py +65 -0
  73. iatoolkit/views/external_login_view.py +61 -28
  74. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +10 -3
  75. iatoolkit/views/forgot_password_view.py +27 -21
  76. iatoolkit/views/help_content_api_view.py +54 -0
  77. iatoolkit/views/history_api_view.py +56 -0
  78. iatoolkit/views/home_view.py +50 -23
  79. iatoolkit/views/index_view.py +14 -0
  80. iatoolkit/views/init_context_api_view.py +74 -0
  81. iatoolkit/views/llmquery_api_view.py +58 -0
  82. iatoolkit/views/login_simulation_view.py +93 -0
  83. iatoolkit/views/login_view.py +130 -37
  84. iatoolkit/views/logout_api_view.py +49 -0
  85. iatoolkit/views/profile_api_view.py +46 -0
  86. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +10 -10
  87. iatoolkit/views/signup_view.py +41 -36
  88. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  89. iatoolkit/views/tasks_review_api_view.py +55 -0
  90. iatoolkit/views/user_feedback_api_view.py +60 -0
  91. iatoolkit/views/verify_user_view.py +34 -29
  92. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/METADATA +41 -23
  93. iatoolkit-0.71.2.dist-info/RECORD +122 -0
  94. iatoolkit-0.71.2.dist-info/licenses/LICENSE +21 -0
  95. iatoolkit/common/auth.py +0 -200
  96. iatoolkit/static/images/arrow_up.png +0 -0
  97. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  98. iatoolkit/static/images/logo_clinica.png +0 -0
  99. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  100. iatoolkit/static/images/logo_maxxa.png +0 -0
  101. iatoolkit/static/images/logo_notaria.png +0 -0
  102. iatoolkit/static/images/logo_tarjeta.png +0 -0
  103. iatoolkit/static/images/logo_umayor.png +0 -0
  104. iatoolkit/static/images/upload.png +0 -0
  105. iatoolkit/static/js/chat_feedback.js +0 -115
  106. iatoolkit/static/js/chat_history.js +0 -117
  107. iatoolkit/static/styles/chat_info.css +0 -53
  108. iatoolkit/templates/header.html +0 -31
  109. iatoolkit/templates/home.html +0 -199
  110. iatoolkit/templates/login.html +0 -43
  111. iatoolkit/templates/test.html +0 -9
  112. iatoolkit/views/chat_token_request_view.py +0 -98
  113. iatoolkit/views/chat_view.py +0 -58
  114. iatoolkit/views/download_file_view.py +0 -58
  115. iatoolkit/views/external_chat_login_view.py +0 -95
  116. iatoolkit/views/history_view.py +0 -57
  117. iatoolkit/views/llmquery_view.py +0 -65
  118. iatoolkit/views/tasks_review_view.py +0 -83
  119. iatoolkit/views/user_feedback_view.py +0 -74
  120. iatoolkit-0.11.0.dist-info/RECORD +0 -110
  121. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/WHEEL +0 -0
  122. {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, singleton, Injector
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 = "0.0.0+dev"
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 compaies
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
- log_level_str = self._get_config_value('FLASK_ENV', 'production')
118
- log_level = logging.INFO if log_level_str in ('dev', 'development') else logging.WARNING
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
- is_https = self._get_config_value('USE_HTTPS', 'false').lower() == 'true'
145
- is_dev = self._get_config_value('FLASK_ENV') == 'development'
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" if is_https else "Lax",
151
- 'SESSION_COOKIE_SECURE': is_https,
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 is_dev:
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
- # Origins por defecto para desarrollo
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
- # Obtener origins adicionales desde configuración/env
248
+ # Iterate through the registered company names
213
249
  extra_origins = []
214
- for i in range(1, 11): # Soporte para CORS_ORIGIN_1 a CORS_ORIGIN_10
215
- origin = self._get_config_value(f'CORS_ORIGIN_{i}')
216
- if origin:
217
- extra_origins.append(origin)
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.common.auth import IAuthentication
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(IAuthentication, to=IAuthentication)
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 start the execution of every company
362
+ # use the dispatcher to load the company config.yaml file and prepare the execution
339
363
  dispatcher = self._injector.get(Dispatcher)
340
- dispatcher.start_execution()
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
- 'user': SessionManager.get('user'),
370
- 'user_company': SessionManager.get('company_short_name'),
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:
@@ -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 self.model:
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 '{self.model}' with {self.count_tokens(context)} tokens...")
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=self.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 = 'Tu consulta supera el limite de contexto, sale e ingresa de nuevo a IAToolkit'
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
- company_name=company.short_name,
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=self.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'] = response.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 = 'Tu consulta supera el limite de contexto, sale e ingresa de nuevo a IAToolkit'
255
+ error_message = CONTEXT_ERROR_MESSAGE
260
256
  elif "string_above_max_length" in str(e):
261
- error_message = 'La respuesta es muy larga, trata de filtrar/restringuir tu consulta'
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: str = None) -> str:
264
+ model) -> str:
269
265
 
270
- if model:
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=self.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),
@@ -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, openai_client = None, gemini_client = None):
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(util=self.util, openai_client=openai_client, gemini_client=gemini_client)
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"Proveedor no soportado: {provider.value}")
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
- if company.openai_api_key:
119
- decrypted_api_key = self.util.decrypt_key(company.openai_api_key)
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
- decrypted_api_key = os.getenv("OPENAI_API_KEY", '')
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
- f"La empresa '{company.name}' no tiene API key de OpenAI.")
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
- if company.gemini_api_key:
131
- decrypted_api_key = self.util.decrypt_key(company.gemini_api_key)
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
- decrypted_api_key = os.getenv("GEMINI_API_KEY", '')
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, sale e ingresa de nuevo a IAToolkit'
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, ex: int = None):
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
- result = client.set(key, value, ex=ex)
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)