iatoolkit 0.55.2__py3-none-any.whl → 0.56.0__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/base_company.py CHANGED
@@ -88,7 +88,7 @@ class BaseCompany(ABC):
88
88
 
89
89
  @abstractmethod
90
90
  # get context specific for this company
91
- def get_user_info(self, user_identifier: str) -> str:
91
+ def get_user_info(self, user_identifier: str) -> dict:
92
92
  raise NotImplementedError("La subclase debe implementar el método get_user_info()")
93
93
 
94
94
  @abstractmethod
@@ -26,8 +26,6 @@ def register_views(injector, app):
26
26
  from iatoolkit.views.tasks_view import TaskView
27
27
  from iatoolkit.views.tasks_review_view import TaskReviewView
28
28
  from iatoolkit.views.login_simulation_view import LoginSimulationView
29
- from iatoolkit.views.login_view import LoginView, FinalizeContextView
30
- from iatoolkit.views.external_login_view import ExternalLoginView
31
29
  from iatoolkit.views.signup_view import SignupView
32
30
  from iatoolkit.views.verify_user_view import VerifyAccountView
33
31
  from iatoolkit.views.forgot_password_view import ForgotPasswordView
@@ -36,27 +34,41 @@ def register_views(injector, app):
36
34
  from iatoolkit.views.user_feedback_api_view import UserFeedbackApiView
37
35
  from iatoolkit.views.prompt_api_view import PromptApiView
38
36
  from iatoolkit.views.chat_token_request_view import ChatTokenRequestView
37
+ from iatoolkit.views.login_view import LoginView, FinalizeContextView
38
+ from iatoolkit.views.external_login_view import ExternalLoginView, RedeemTokenApiView
39
39
 
40
40
  # iatoolkit home page
41
41
  app.add_url_rule('/<company_short_name>', view_func=IndexView.as_view('index'))
42
42
 
43
- # init (reset) the company context (with api-key)
44
- app.add_url_rule('/<company_short_name>/api/init_context_api',
45
- view_func=InitContextApiView.as_view('init_context_api'))
43
+ # login for the iatoolkit integrated frontend
44
+ app.add_url_rule('/<company_short_name>/login', view_func=LoginView.as_view('login'))
46
45
 
47
- # this functions are for login external users (with api-key)
48
- # only the first one should be used from an external app
46
+ # this is the login for external users
49
47
  app.add_url_rule('/<company_short_name>/external_login',
50
48
  view_func=ExternalLoginView.as_view('external_login'))
51
49
 
50
+ # this endpoint is called when onboarding_shell finish the context load
51
+ app.add_url_rule(
52
+ '/<company_short_name>/finalize',
53
+ view_func=FinalizeContextView.as_view('finalize_no_token')
54
+ )
55
+
56
+ app.add_url_rule(
57
+ '/<company_short_name>/finalize/<token>',
58
+ view_func=FinalizeContextView.as_view('finalize_with_token')
59
+ )
60
+
61
+ # this endpoint is called by the JS for changing the token for a session
62
+ app.add_url_rule('/<string:company_short_name>/api/redeem_token',
63
+ view_func = RedeemTokenApiView.as_view('redeem_token'))
64
+
52
65
  # this endpoint is for requesting a chat token for external users
53
66
  app.add_url_rule('/auth/chat_token',
54
67
  view_func=ChatTokenRequestView.as_view('chat-token'))
55
68
 
56
- # login for the iatoolkit integrated frontend
57
- # this is the main login endpoint for the frontend
58
- app.add_url_rule('/<company_short_name>/login', view_func=LoginView.as_view('login'))
59
- app.add_url_rule('/<company_short_name>/finalize_context_load', view_func=FinalizeContextView.as_view('finalize_context_load'))
69
+ # init (reset) the company context (with api-key)
70
+ app.add_url_rule('/<company_short_name>/api/init_context_api',
71
+ view_func=InitContextApiView.as_view('init_context_api'))
60
72
 
61
73
  # register new user, account verification and forgot password
62
74
  app.add_url_rule('/<company_short_name>/signup',view_func=SignupView.as_view('signup'))
@@ -111,8 +123,8 @@ def register_views(injector, app):
111
123
  except FileNotFoundError:
112
124
  abort(404)
113
125
 
114
- # login testing (old home page)
115
- app.add_url_rule('/login_test/<company_short_name>/<external_user_id>',
126
+ # login testing
127
+ app.add_url_rule('/<company_short_name>/login_test',
116
128
  view_func=LoginSimulationView.as_view('login_test'))
117
129
 
118
130
  app.add_url_rule(
iatoolkit/iatoolkit.py CHANGED
@@ -19,7 +19,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
19
19
  from injector import Binder, singleton, Injector
20
20
  from importlib.metadata import version as _pkg_version, PackageNotFoundError
21
21
 
22
- IATOOLKIT_VERSION = "0.55.1"
22
+ IATOOLKIT_VERSION = "0.56.0"
23
23
 
24
24
  # global variable for the unique instance of IAToolkit
25
25
  _iatoolkit_instance: Optional['IAToolkit'] = None
@@ -155,10 +155,9 @@ class IAToolkit:
155
155
 
156
156
  self.app.config.update({
157
157
  'VERSION': self.version,
158
- 'SERVER_NAME': domain,
159
158
  'SECRET_KEY': self._get_config_value('FLASK_SECRET_KEY', 'iatoolkit-default-secret'),
160
- 'SESSION_COOKIE_SAMESITE': "None" if is_https else "Lax",
161
- 'SESSION_COOKIE_SECURE': is_https,
159
+ 'SESSION_COOKIE_SAMESITE': "None",
160
+ 'SESSION_COOKIE_SECURE': True,
162
161
  'SESSION_PERMANENT': False,
163
162
  'SESSION_USE_SIGNER': True,
164
163
  'JWT_SECRET_KEY': self._get_config_value('JWT_SECRET_KEY', 'iatoolkit-jwt-secret'),
@@ -21,16 +21,19 @@ class DatabaseManager:
21
21
  :param echo: Si True, habilita logs de SQL.
22
22
  """
23
23
  self.url = make_url(database_url)
24
- self._engine = create_engine(
25
- database_url,
26
- echo=False,
27
- pool_size=2, # per worker
28
- max_overflow=3,
29
- pool_timeout=30,
30
- pool_recycle=1800,
31
- pool_pre_ping=True,
32
- future=True,
33
- )
24
+ if database_url.startswith('sqlite'): # for tests
25
+ self._engine = create_engine(database_url, echo=False)
26
+ else:
27
+ self._engine = create_engine(
28
+ database_url,
29
+ echo=False,
30
+ pool_size=2, # per worker
31
+ max_overflow=3,
32
+ pool_timeout=30,
33
+ pool_recycle=1800,
34
+ pool_pre_ping=True,
35
+ future=True,
36
+ )
34
37
  self.SessionFactory = sessionmaker(bind=self._engine,
35
38
  autoflush=False,
36
39
  autocommit=False,
@@ -72,10 +72,14 @@ class ProfileRepo:
72
72
  def create_company(self, new_company: Company):
73
73
  company = self.session.query(Company).filter_by(name=new_company.name).first()
74
74
  if company:
75
- company.parameters = new_company.parameters
76
- company.branding = new_company.branding
77
- company.onboarding_cards = new_company.onboarding_cards
75
+ if company.parameters != new_company.parameters:
76
+ company.parameters = new_company.parameters
77
+ if company.branding != new_company.branding:
78
+ company.branding = new_company.branding
79
+ if company.onboarding_cards != new_company.onboarding_cards:
80
+ company.onboarding_cards = new_company.onboarding_cards
78
81
  else:
82
+ # Si la compañía no existe, la añade a la sesión.
79
83
  self.session.add(new_company)
80
84
  company = new_company
81
85
 
@@ -7,6 +7,7 @@ from flask import request
7
7
  from injector import inject
8
8
  from iatoolkit.services.profile_service import ProfileService
9
9
  from flask import request
10
+ import logging
10
11
 
11
12
 
12
13
  class AuthService:
@@ -52,6 +53,7 @@ class AuthService:
52
53
  if api_key:
53
54
  api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
54
55
  if not api_key_entry:
56
+ logging.info(f"Invalid or inactive API Key {api_key}")
55
57
  return {"success": False, "error": "Invalid or inactive API Key", "status_code": 401}
56
58
 
57
59
  # obtain the company from the api_key_entry
@@ -61,7 +63,7 @@ class AuthService:
61
63
  user_identifier = ''
62
64
  if request.is_json:
63
65
  data = request.get_json() or {}
64
- user_identifier = data.get('external_user_id', '')
66
+ user_identifier = data.get('user_identifier', '')
65
67
 
66
68
  return {
67
69
  "success": True,
@@ -70,5 +72,6 @@ class AuthService:
70
72
  }
71
73
 
72
74
  # --- Failure: No valid credentials found ---
75
+ logging.info(f"Authentication required. No session cookie or API Key provided.")
73
76
  return {"success": False, "error": "Authentication required. No session cookie or API Key provided.",
74
- "status_code": 401}
77
+ "status_code": 402}
@@ -178,35 +178,13 @@ class Dispatcher:
178
178
  # source 2: external company user
179
179
  company_instance = self.company_instances[company_name]
180
180
  try:
181
- raw_user_data = company_instance.get_user_info(user_identifier)
181
+ external_user_profile = company_instance.get_user_info(user_identifier)
182
182
  except Exception as e:
183
183
  logging.exception(e)
184
184
  raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
185
185
  f"Error en get_user_info de {company_name}: {str(e)}") from e
186
186
 
187
- # always normalize the data for consistent structure
188
- return self._normalize_user_data(raw_user_data)
189
-
190
- def _normalize_user_data(self, raw_data: dict) -> dict:
191
- """
192
- Asegura que los datos del usuario siempre tengan una estructura consistente.
193
- """
194
- # default values
195
- normalized_user = {
196
- "id": raw_data.get("id", 0),
197
- "username": raw_data.get("id", 0),
198
- "user_email": raw_data.get("email", ""),
199
- "user_fullname": raw_data.get("user_fullname", ""),
200
- "is_local": False,
201
- "extras": raw_data.get("extras", {})
202
- }
203
-
204
- # get the extras from the raw data, if any
205
- extras = raw_data.get("extras", {})
206
- if isinstance(extras, dict):
207
- normalized_user.update(extras)
208
-
209
- return normalized_user
187
+ return external_user_profile
210
188
 
211
189
  def get_metadata_from_filename(self, company_name: str, filename: str) -> dict:
212
190
  if company_name not in self.company_instances:
@@ -24,16 +24,18 @@ class JWTService:
24
24
  raise RuntimeError(f"Configuración JWT esencial faltante: {e}")
25
25
 
26
26
  def generate_chat_jwt(self,
27
- company_id: int,
28
27
  company_short_name: str,
29
- external_user_id: str,
28
+ user_identifier: str,
30
29
  expires_delta_seconds: int) -> Optional[str]:
31
30
  # generate a JWT for a chat session
32
31
  try:
32
+ if not company_short_name or not user_identifier:
33
+ logging.error(f"Missing token ID: {company_short_name}/{user_identifier}")
34
+ return None
35
+
33
36
  payload = {
34
- 'company_id': company_id,
35
37
  'company_short_name': company_short_name,
36
- 'external_user_id': external_user_id,
38
+ 'user_identifier': user_identifier,
37
39
  'exp': time.time() + expires_delta_seconds,
38
40
  'iat': time.time(),
39
41
  'type': 'chat_session' # Identificador del tipo de token
@@ -41,10 +43,10 @@ class JWTService:
41
43
  token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
42
44
  return token
43
45
  except Exception as e:
44
- logging.error(f"Error al generar JWT para company {company_id}, user {external_user_id}: {e}")
46
+ logging.error(f"Error al generar JWT para {company_short_name}/{user_identifier}: {e}")
45
47
  return None
46
48
 
47
- def validate_chat_jwt(self, token: str, expected_company_short_name: str) -> Optional[Dict[str, Any]]:
49
+ def validate_chat_jwt(self, token: str) -> Optional[Dict[str, Any]]:
48
50
  """
49
51
  Valida un JWT de sesión de chat.
50
52
  Retorna el payload decodificado si es válido y coincide con la empresa, o None.
@@ -59,33 +61,22 @@ class JWTService:
59
61
  logging.warning(f"Validación JWT fallida: tipo incorrecto '{payload.get('type')}'")
60
62
  return None
61
63
 
62
- if payload.get('company_short_name') != expected_company_short_name:
63
- logging.warning(
64
- f"Validación JWT fallida: company_short_name no coincide. "
65
- f"Esperado: {expected_company_short_name}, Obtenido: {payload.get('company_short_name')}"
66
- )
64
+ # user_identifier debe estar presente
65
+ if not payload.get('user_identifier'):
66
+ logging.warning(f"Validación JWT fallida: user_identifier ausente o vacío.")
67
67
  return None
68
68
 
69
- # external_user_id debe estar presente
70
- if 'external_user_id' not in payload or not payload['external_user_id']:
71
- logging.warning(f"Validación JWT fallida: external_user_id ausente o vacío.")
72
- return None
73
-
74
- # company_id debe estar presente
75
- if 'company_id' not in payload or not isinstance(payload['company_id'], int):
76
- logging.warning(f"Validación JWT fallida: company_id ausente o tipo incorrecto.")
69
+ if not payload.get('company_short_name'):
70
+ logging.warning(f"Validación JWT fallida: company_short_name ausente.")
77
71
  return None
78
72
 
79
73
  logging.debug(
80
74
  f"JWT validado exitosamente para company: {payload.get('company_short_name')}, user: {payload.get('external_user_id')}")
81
75
  return payload
82
76
 
83
- except jwt.ExpiredSignatureError:
84
- logging.info(f"Validación JWT fallida: token expirado para {expected_company_short_name}")
85
- return None
86
77
  except jwt.InvalidTokenError as e:
87
- logging.warning(f"Validación JWT fallida: token inválido para {expected_company_short_name}. Error: {e}")
78
+ logging.warning(f"Validación JWT fallida: token inválido . Error: {e}")
88
79
  return None
89
80
  except Exception as e:
90
- logging.error(f"Error inesperado durante validación de JWT para {expected_company_short_name}: {e}")
81
+ logging.error(f"Error inesperado durante validación de JWT : {e}")
91
82
  return None
@@ -60,7 +60,6 @@ class ProfileService:
60
60
  # the user_profile variables are used on the LLM templates also (see in query_main.prompt)
61
61
  user_identifier = user.email # no longer de ID
62
62
  user_profile = {
63
- "id": user_identifier,
64
63
  "user_email": user.email,
65
64
  "user_fullname": f'{user.first_name} {user.last_name}',
66
65
  "user_is_local": True,
@@ -74,21 +73,21 @@ class ProfileService:
74
73
  except Exception as e:
75
74
  return {'success': False, "message": str(e)}
76
75
 
77
- def create_external_user_session(self, company: Company, external_user_id: str):
76
+ def create_external_user_session(self, company: Company, user_identifier: str):
78
77
  """
79
78
  Public method for views to create a web session for an external user.
80
79
  """
81
- # 1. Fetch the profile from the external system via Dispatcher.
82
- user_profile = self.dispatcher.get_user_info(
80
+ # 1. Fetch the external user profile via Dispatcher.
81
+ external_user_profile = self.dispatcher.get_user_info(
83
82
  company_name=company.short_name,
84
- user_identifier=external_user_id
83
+ user_identifier=user_identifier
85
84
  )
86
85
 
87
86
  # 2. Call the session creation helper with external_user_id as user_identifier
88
87
  self.create_web_session(
89
88
  company=company,
90
- user_identifier=external_user_id,
91
- user_profile=user_profile)
89
+ user_identifier=user_identifier,
90
+ user_profile=external_user_profile)
92
91
 
93
92
  def create_web_session(self, company: Company, user_identifier: str, user_profile: dict):
94
93
  """
@@ -96,16 +95,19 @@ class ProfileService:
96
95
  """
97
96
  user_profile['company_short_name'] = company.short_name
98
97
  user_profile['user_identifier'] = user_identifier
99
- user_profile['user_id'] = user_identifier
98
+ user_profile['id'] = user_identifier
100
99
  user_profile['company_id'] = company.id
101
100
  user_profile['company'] = company.name
102
101
 
103
- # user_profile['last_activity'] = datetime.now(timezone.utc).timestamp()
102
+ # save user_profile in Redis session
104
103
  self.session_context.save_profile_data(company.short_name, user_identifier, user_profile)
105
104
 
106
- # save this values into Flask session cookie
105
+ # save a min Flask session cookie for this user
106
+ self.set_session_for_user(company.short_name, user_identifier)
107
+
108
+ def set_session_for_user(self, company_short_name: str, user_identifier:str ):
109
+ SessionManager.set('company_short_name', company_short_name)
107
110
  SessionManager.set('user_identifier', user_identifier)
108
- SessionManager.set('company_short_name', company.short_name)
109
111
 
110
112
  def get_current_session_info(self) -> dict:
111
113
  """
@@ -114,10 +114,6 @@ class QueryService:
114
114
  self._has_valid_cached_context(company_short_name, user_identifier))
115
115
 
116
116
  if rebuild_is_needed:
117
- logging.info(
118
- f"Se necesita reconstrucción de contexto para {company_short_name}/{user_identifier}. Preparando...")
119
-
120
-
121
117
  # Guardar el contexto preparado y su versión para que `finalize_context_rebuild` los use.
122
118
  self.session_context.save_prepared_context(company_short_name, user_identifier, final_system_context,
123
119
  current_version)
@@ -106,7 +106,7 @@ const sendFeedback = async function(message) {
106
106
  };
107
107
  try {
108
108
  // Asumiendo que callLLMAPI está definido globalmente en otro archivo (ej. chat_main.js)
109
- const responseData = await callLLMAPI('/feedback', data, "POST");
109
+ const responseData = await callToolkit('/feedback', data, "POST");
110
110
  return responseData;
111
111
  } catch (error) {
112
112
  console.error("Error al enviar feedback:", error);
@@ -40,7 +40,7 @@ $(document).ready(function () {
40
40
  historyContent.hide();
41
41
 
42
42
  try {
43
- const responseData = await callLLMAPI("/api/history", {}, "POST");
43
+ const responseData = await callToolkit("/api/history", {}, "POST");
44
44
 
45
45
  if (responseData && responseData.history) {
46
46
  // Guardar datos globalmente
@@ -5,6 +5,13 @@ let abortController = null;
5
5
  let selectedPrompt = null; // Will hold a lightweight prompt object
6
6
 
7
7
  $(document).ready(function () {
8
+ // Gatilla el redeem sin esperar ni manejar respuesta aquí
9
+ if (window.redeemToken !== '') {
10
+ const url = `/api/redeem_token`;
11
+ // No await: dejamos que callToolkit maneje todo internamente
12
+ callToolkit(url, {'token': window.redeemToken}, "POST").catch(() => {});
13
+ }
14
+
8
15
  // --- MAIN EVENT HANDLERS ---
9
16
  $('#send-button').on('click', handleChatMessage);
10
17
  $('#stop-button').on('click', abortCurrentRequest);
@@ -176,7 +183,7 @@ const handleChatMessage = async function () {
176
183
  user_identifier: window.user_identifier
177
184
  };
178
185
 
179
- const responseData = await callLLMAPI("/llm_query", data, "POST");
186
+ const responseData = await callToolkit("/llm_query", data, "POST");
180
187
  if (responseData && responseData.answer) {
181
188
  const answerSection = $('<div>').addClass('answer-section llm-output').append(responseData.answer);
182
189
  displayBotMessage(answerSection);
@@ -292,28 +299,39 @@ function resetSpecificDataInput() {
292
299
  * @param {number} timeoutMs - Timeout in milliseconds.
293
300
  * @returns {Promise<object|null>} The response data or null on error.
294
301
  */
295
- const callLLMAPI = async function(apiPath, data, method, timeoutMs = 500000) {
302
+ const callToolkit = async function(apiPath, data, method, timeoutMs = 500000) {
296
303
  const url = `${window.iatoolkit_base_url}/${window.companyShortName}${apiPath}`;
297
304
 
298
- const headers = {"Content-Type": "application/json"};
299
- if (window.sessionJWT) {
300
- headers['X-Chat-Token'] = window.sessionJWT;
301
- }
302
-
303
305
  abortController = new AbortController();
304
306
  const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
305
307
 
306
308
  try {
307
- const response = await fetch(url, {
308
- method: method,
309
- headers: headers,
310
- body: JSON.stringify(data),
311
- signal: abortController.signal, // Se usa el signal del controlador global
312
- credentials: 'include'
313
- });
309
+ const fetchOptions = {
310
+ method: method,
311
+ signal: abortController.signal,
312
+ credentials: 'include'
313
+ };
314
+
315
+ // Solo agrega body si el método lo soporta y hay datos
316
+ const methodUpper = (method || '').toUpperCase();
317
+ const canHaveBody = !['GET', 'HEAD'].includes(methodUpper);
318
+ if (canHaveBody && data !== undefined && data !== null) {
319
+ fetchOptions.body = JSON.stringify(data);
320
+ fetchOptions.headers = {"Content-Type": "application/json"};
321
+
322
+ }
323
+ const response = await fetch(url, fetchOptions);
324
+
314
325
  clearTimeout(timeoutId);
315
326
 
316
327
  if (!response.ok) {
328
+ if (response.status === 401) {
329
+ const errorMessage = `Tu sesión ha expirado. `;
330
+ const errorIcon = '<i class="bi bi-exclamation-triangle"></i>';
331
+ const infrastructureError = $('<div>').addClass('error-section').html(errorIcon + `<p>${errorMessage}</p>`);
332
+ displayBotMessage(infrastructureError);
333
+ return null;
334
+ }
317
335
  try {
318
336
  // Intentamos leer el error como JSON, que es el formato esperado de nuestra API.
319
337
  const errorData = await response.json();
@@ -337,6 +355,14 @@ const callLLMAPI = async function(apiPath, data, method, timeoutMs = 500000) {
337
355
  if (error.name === 'AbortError') {
338
356
  throw error; // Re-throw to be handled by handleChatMessage
339
357
  } else {
358
+ // Log detallado en consola
359
+ console.error('Error de red en callToolkit:', {
360
+ url,
361
+ method,
362
+ error,
363
+ message: error?.message,
364
+ stack: error?.stack,
365
+ });
340
366
  const friendlyMessage = "Ocurrió un error de red. Por favor, inténtalo de nuevo en unos momentos.";
341
367
  const errorIcon = '<i class="bi bi-exclamation-triangle"></i>';
342
368
  const commError = $('<div>').addClass('error-section').html(errorIcon + `<p>${friendlyMessage}</p>`);
@@ -111,8 +111,11 @@
111
111
  data-custom-fields='{{ prompt.custom_fields | tojson }}'>
112
112
  {{ prompt.description }}
113
113
  </a>
114
- </li> {% endfor %}
115
- {% if not loop.last %}<li><hr class="dropdown-divider"></li>{% endif %}
114
+ </li>
115
+ {% endfor %}
116
+ {% if not loop.last %}
117
+ <li><hr class="dropdown-divider"></li>
118
+ {% endif %}
116
119
  {% endfor %}
117
120
  {% endif %}
118
121
  </ul>
@@ -180,10 +183,11 @@
180
183
  // --- Global Configuration from Backend ---
181
184
  window.companyShortName = "{{ company_short_name }}";
182
185
  window.user_identifier = "{{ user_identifier }}";
186
+ window.redeemToken = "{{ redeem_token }}";
183
187
  window.iatoolkit_base_url = "{{ iatoolkit_base_url }}";
184
188
  window.availablePrompts = {{ prompts.message | tojson }};
185
- window.sendButtonColor = "{{ branding.send_button_color }}";
186
189
  window.onboardingCards = {{ onboarding_cards | tojson }};
190
+ window.sendButtonColor = "{{ branding.send_button_color }}";
187
191
  </script>
188
192
 
189
193
  <!-- Carga de los scripts JS externos después de definir las variables globales -->
@@ -0,0 +1,34 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Prueba de Login para {{ company_short_name }}{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container-fluid">
7
+ <div class="row flex-fill mt-5 justify-content-center">
8
+ <div class="col-12 col-lg-6">
9
+ <div class="border rounded p-4 p-md-5 shadow-sm bg-light">
10
+ <h3 class="text-muted fw-semibold text-start mb-3">
11
+ Login Externo para <span style="color:#0d6efd;">{{ company_short_name }}</span>
12
+ </h3>
13
+ <div class="text-center mb-4">
14
+ <p class="text-muted widget-intro-text">
15
+ Este formulario simula el inicio de una sesión externa. Al enviar, serás redirigido a la URL de login final.
16
+ </p>
17
+ </div>
18
+
19
+ <!-- Formulario HTML estándar que hace un POST a la misma URL -->
20
+ <form method="POST" action="">
21
+ <div class="mb-3">
22
+ <label for="external_user_id" class="form-label d-block">External user ID</label>
23
+ <input type="text" id="external_user_id" name="external_user_id" class="form-control" required>
24
+ </div>
25
+ <button type="submit" class="btn btn-primary">
26
+ Redirigir a External Login
27
+ </button>
28
+ </form>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ {% endblock %}
34
+
@@ -12,6 +12,7 @@ from iatoolkit.services.query_service import QueryService
12
12
  from iatoolkit.services.branding_service import BrandingService
13
13
  from iatoolkit.services.onboarding_service import OnboardingService
14
14
  from iatoolkit.services.prompt_manager_service import PromptService
15
+ from iatoolkit.services.jwt_service import JWTService
15
16
 
16
17
 
17
18
  class BaseLoginView(MethodView):
@@ -22,16 +23,20 @@ class BaseLoginView(MethodView):
22
23
  @inject
23
24
  def __init__(self,
24
25
  profile_service: ProfileService,
26
+ jwt_service: JWTService,
25
27
  branding_service: BrandingService,
26
28
  prompt_service: PromptService,
27
29
  onboarding_service: OnboardingService,
28
- query_service: QueryService):
30
+ query_service: QueryService
31
+ ):
29
32
  self.profile_service = profile_service
33
+ self.jwt_service = jwt_service
30
34
  self.branding_service = branding_service
31
35
  self.prompt_service = prompt_service
32
36
  self.onboarding_service = onboarding_service
33
37
  self.query_service = query_service
34
38
 
39
+
35
40
  def _handle_login_path(self, company_short_name: str, user_identifier: str, company):
36
41
  """
37
42
  Centralized logic to decide between the fast path and the slow path.
@@ -43,13 +48,33 @@ class BaseLoginView(MethodView):
43
48
  prep_result = self.query_service.prepare_context(
44
49
  company_short_name=company_short_name, user_identifier=user_identifier
45
50
  )
51
+
52
+ # generate continuation token for external login
53
+ redeem_token = ''
54
+ if self.__class__.__name__ == 'ExternalLoginView':
55
+ redeem_token = self.jwt_service.generate_chat_jwt(
56
+ company_short_name=company_short_name,
57
+ user_identifier=user_identifier,
58
+ expires_delta_seconds=300
59
+ )
60
+
61
+ if not redeem_token:
62
+ return "Error al generar el redeem_token para login externo.", 500
63
+
46
64
  if prep_result.get('rebuild_needed'):
47
65
  # --- SLOW PATH: Render the loading shell ---
48
66
  onboarding_cards = self.onboarding_service.get_onboarding_cards(company)
49
67
 
50
68
  # callback url to call when the context finish loading
51
- target_url = url_for('finalize_context_load', company_short_name=company_short_name, _external=True)
52
-
69
+ if redeem_token:
70
+ target_url = url_for('finalize_with_token',
71
+ company_short_name=company_short_name,
72
+ token=redeem_token,
73
+ _external=True)
74
+ else:
75
+ target_url = url_for('finalize_no_token',
76
+ company_short_name=company_short_name,
77
+ _external=True)
53
78
  return render_template(
54
79
  "onboarding_shell.html",
55
80
  iframe_src_url=target_url,
@@ -62,7 +87,10 @@ class BaseLoginView(MethodView):
62
87
  onboarding_cards = self.onboarding_service.get_onboarding_cards(company)
63
88
  return render_template(
64
89
  "chat.html",
90
+ company_short_name=company_short_name,
91
+ user_identifier=user_identifier,
65
92
  branding=branding_data,
66
93
  prompts=prompts,
67
- onboarding_cards=onboarding_cards
94
+ onboarding_cards=onboarding_cards,
95
+ redeem_token=redeem_token
68
96
  )
@@ -6,12 +6,14 @@
6
6
  import os
7
7
  import logging
8
8
  from flask import request, jsonify
9
+ from flask.views import MethodView
9
10
  from injector import inject
10
11
  from iatoolkit.services.auth_service import AuthService
11
12
  from iatoolkit.views.base_login_view import BaseLoginView
12
13
 
13
14
  # Importar los servicios que necesita la clase base
14
15
  from iatoolkit.services.profile_service import ProfileService
16
+ from iatoolkit.services.jwt_service import JWTService
15
17
  from iatoolkit.services.branding_service import BrandingService
16
18
  from iatoolkit.services.onboarding_service import OnboardingService
17
19
  from iatoolkit.services.query_service import QueryService
@@ -25,6 +27,7 @@ class ExternalLoginView(BaseLoginView):
25
27
  @inject
26
28
  def __init__(self,
27
29
  iauthentication: AuthService,
30
+ jwt_service: JWTService,
28
31
  profile_service: ProfileService,
29
32
  branding_service: BrandingService,
30
33
  prompt_service: PromptService,
@@ -33,26 +36,27 @@ class ExternalLoginView(BaseLoginView):
33
36
  # Pass the dependencies for the base class to its __init__
34
37
  super().__init__(
35
38
  profile_service=profile_service,
39
+ jwt_service=jwt_service,
36
40
  branding_service=branding_service,
37
41
  onboarding_service=onboarding_service,
38
42
  query_service=query_service,
39
- prompt_service=prompt_service
43
+ prompt_service=prompt_service,
40
44
  )
41
45
  # Handle the dependency specific to this child class
42
46
  self.iauthentication = iauthentication
43
47
 
44
48
  def post(self, company_short_name: str):
45
49
  data = request.get_json()
46
- if not data or 'external_user_id' not in data:
47
- return jsonify({"error": "Falta external_user_id"}), 400
50
+ if not data or 'user_identifier' not in data:
51
+ return jsonify({"error": "Falta user_identifier"}), 400
48
52
 
49
53
  company = self.profile_service.get_company_by_short_name(company_short_name)
50
54
  if not company:
51
55
  return jsonify({"error": "Empresa no encontrada"}), 404
52
56
 
53
- external_user_id = data['external_user_id']
54
- if not external_user_id:
55
- return jsonify({"error": "missing external_user_id"}), 404
57
+ user_identifier = data.get('user_identifier')
58
+ if not user_identifier:
59
+ return jsonify({"error": "missing user_identifier"}), 404
56
60
 
57
61
  # 1. Authenticate the API call.
58
62
  iaut = self.iauthentication.verify()
@@ -60,11 +64,47 @@ class ExternalLoginView(BaseLoginView):
60
64
  return jsonify(iaut), 401
61
65
 
62
66
  # 2. Create the external user session.
63
- self.profile_service.create_external_user_session(company, external_user_id)
67
+ self.profile_service.create_external_user_session(company, user_identifier)
64
68
 
65
69
  # 3. Delegate the path decision to the centralized logic.
66
70
  try:
67
- return self._handle_login_path(company_short_name, external_user_id, company)
71
+ return self._handle_login_path(company_short_name, user_identifier, company)
68
72
  except Exception as e:
69
- logging.exception(f"Error processing external login path for {company_short_name}/{external_user_id}: {e}")
70
- return jsonify({"error": f"Internal server error while starting chat. {str(e)}"}), 500
73
+ logging.exception(f"Error processing external login path for {company_short_name}/{user_identifier}: {e}")
74
+ return jsonify({"error": f"Internal server error while starting chat. {str(e)}"}), 500
75
+
76
+
77
+ class RedeemTokenApiView(MethodView):
78
+ # this endpoint is only used ONLY by chat_main.js to redeem a chat token
79
+ @inject
80
+ def __init__(self,
81
+ profile_service: ProfileService,
82
+ jwt_service: JWTService):
83
+ self.profile_service = profile_service
84
+ self.jwt_service = jwt_service
85
+
86
+ def post(self, company_short_name: str):
87
+ data = request.get_json()
88
+ if not data or 'token' not in data:
89
+ return jsonify({"error": "Falta token de validación"}), 400
90
+
91
+ # 1. validate the token
92
+ token = data.get('token')
93
+ payload = self.jwt_service.validate_chat_jwt(token)
94
+ if not payload:
95
+ logging.warning("Intento de canjear un token inválido o expirado.")
96
+ return {"error": "Token inválido o expirado."}, 401
97
+
98
+ # 2. if token is valid, extract the user_identifier
99
+ user_identifier = payload.get('user_identifier')
100
+
101
+ try:
102
+ # 3. create the Flask session
103
+ self.profile_service.set_session_for_user(company_short_name, user_identifier)
104
+ logging.info(f"Token de sesión canjeado exitosamente para {user_identifier}.")
105
+
106
+ return {"status": "ok"}, 200
107
+
108
+ except Exception as e:
109
+ logging.error(f"Error al crear la sesión desde token para {user_identifier}: {e}")
110
+ return {"error": "No se pudo crear la sesión del usuario."}, 500
@@ -1,53 +1,74 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
1
6
  import requests
2
7
  import json
3
8
  import os
4
9
  from flask.views import MethodView
5
- from flask import Response, abort, request, make_response
10
+ from flask import render_template, request, Response
11
+ from injector import inject
12
+ from iatoolkit.services.profile_service import ProfileService
6
13
 
7
14
 
8
15
  class LoginSimulationView(MethodView):
9
- """
10
- Simula un portal externo que llama a la API de IAToolkit de servidor a servidor,
11
- replicando el flujo real de `dispatch_request`.
12
- Para usarlo, visita /login_test/<company_short_name>/<external_user_id>
13
- """
16
+ @inject
17
+ def __init__(self,
18
+ profile_service: ProfileService):
19
+ self.profile_service = profile_service
20
+
21
+ def get(self, company_short_name: str = None):
22
+ """Muestra el formulario para iniciar la simulación."""
23
+ return render_template('login_simulation.html',
24
+ company_short_name=company_short_name
25
+ )
14
26
 
15
- def get(self, company_short_name: str, external_user_id: str):
27
+ def post(self, company_short_name: str):
28
+ """
29
+ Recibe el POST del formulario y actúa como un proxy servidor-a-servidor.
30
+ Llama al endpoint 'external_login' y devuelve su respuesta (HTML y headers).
31
+ """
16
32
  api_key = os.getenv("IATOOLKIT_API_KEY")
33
+ # Obtenemos la URL base de la petición actual para construir la URL interna
17
34
  base_url = request.host_url.rstrip('/')
18
35
 
19
- if not api_key:
20
- abort(500, "Error: IATOOLKIT_API_KEY no está configurada en el servidor de prueba.")
21
- if not external_user_id:
22
- abort(400, "Error: Debes proporcionar un external_user_id en la URL.")
36
+ # 1. Obtener el user_identifier del formulario
37
+ user_identifier = request.form.get('external_user_id')
23
38
 
24
- target_url = f"{base_url}/{company_short_name}/external_login"
39
+ if not user_identifier:
40
+ return Response("Error: El campo 'external_user_id' es requerido.", status=400)
25
41
 
26
- # --- INICIO DE LA CORRECCIÓN ---
27
- # Usamos el formato de header 'Authorization: Bearer' como solicitaste.
42
+ # 2. Preparar la llamada a la API real de external_login
43
+ target_url = f"{base_url}/{company_short_name}/external_login"
28
44
  headers = {
29
45
  'Content-Type': 'application/json',
30
46
  'Authorization': f'Bearer {api_key}'
31
47
  }
32
- # --- FIN DE LA CORRECCIÓN ---
33
-
34
- payload = {'external_user_id': external_user_id}
48
+ # El payload debe ser un diccionario que se convertirá a JSON
49
+ payload = {'user_identifier': user_identifier}
35
50
 
36
51
  try:
37
- # Llamada POST interna. 'stream=True' es importante para manejar la respuesta.
38
- internal_response = requests.post(target_url, headers=headers, data=json.dumps(payload), timeout=120,
39
- stream=True)
52
+ # 3. Llamada POST segura desde este servidor al endpoint de IAToolkit
53
+ internal_response = requests.post(
54
+ target_url,
55
+ headers=headers,
56
+ data=json.dumps(payload),
57
+ timeout=120,
58
+ stream=True # Usamos stream para manejar la respuesta eficientemente
59
+ )
40
60
  internal_response.raise_for_status()
41
61
 
42
- # Creamos una nueva Response de Flask para el navegador del usuario.
62
+ # 4. Creamos una nueva Response de Flask para el navegador del usuario.
43
63
  user_response = Response(
44
64
  internal_response.iter_content(chunk_size=1024),
45
65
  status=internal_response.status_code
46
66
  )
47
67
 
48
- # Copiamos TODAS las cabeceras de la respuesta interna a la respuesta final,
49
- # incluyendo 'Content-Type' y, crucialmente, 'Set-Cookie'.
68
+ # 5. Copiamos TODAS las cabeceras de la respuesta interna a la respuesta final.
69
+ # Esto es CRUCIAL para que las cookies ('Set-Cookie') lleguen al navegador.
50
70
  for key, value in internal_response.headers.items():
71
+ # Excluimos cabeceras que no debemos pasar (controladas por el servidor WSGI)
51
72
  if key.lower() not in ['content-encoding', 'content-length', 'transfer-encoding', 'connection']:
52
73
  user_response.headers[key] = value
53
74
 
@@ -57,4 +78,4 @@ class LoginSimulationView(MethodView):
57
78
  error_text = f"Error en la llamada interna a la API: {e.response.status_code}. Respuesta: {e.response.text}"
58
79
  return Response(error_text, status=e.response.status_code, mimetype='text/plain')
59
80
  except requests.exceptions.RequestException as e:
60
- return Response(f'Error de conexión con el servicio de IA: {str(e)}', status=502, mimetype='text/plain')
81
+ return Response(f'Error de conexión con el servicio de IA: {str(e)}', status=502, mimetype='text/plain')
@@ -7,11 +7,13 @@ from flask.views import MethodView
7
7
  from flask import request, redirect, render_template, url_for
8
8
  from injector import inject
9
9
  from iatoolkit.services.profile_service import ProfileService
10
+ from iatoolkit.services.jwt_service import JWTService
10
11
  from iatoolkit.services.query_service import QueryService
11
12
  from iatoolkit.services.prompt_manager_service import PromptService
12
13
  from iatoolkit.services.branding_service import BrandingService
13
14
  from iatoolkit.services.onboarding_service import OnboardingService
14
15
  from iatoolkit.views.base_login_view import BaseLoginView
16
+ import logging
15
17
 
16
18
 
17
19
  class LoginView(BaseLoginView):
@@ -28,7 +30,7 @@ class LoginView(BaseLoginView):
28
30
  email = request.form.get('email')
29
31
  password = request.form.get('password')
30
32
 
31
- # 1. Authenticate user and create the unified session.
33
+ # 1. Authenticate user and create the session for internal user
32
34
  auth_response = self.profile_service.login(
33
35
  company_short_name=company_short_name,
34
36
  email=email,
@@ -62,28 +64,38 @@ class FinalizeContextView(MethodView):
62
64
  Finalizes context loading in the slow path.
63
65
  This view is invoked by the iframe inside onboarding_shell.html.
64
66
  """
65
-
66
67
  @inject
67
68
  def __init__(self,
68
69
  profile_service: ProfileService,
69
70
  query_service: QueryService,
70
71
  prompt_service: PromptService,
71
72
  branding_service: BrandingService,
72
- onboarding_service: OnboardingService
73
+ onboarding_service: OnboardingService,
74
+ jwt_service: JWTService,
73
75
  ):
74
76
  self.profile_service = profile_service
77
+ self.jwt_service = jwt_service
75
78
  self.query_service = query_service
76
79
  self.prompt_service = prompt_service
77
80
  self.branding_service = branding_service
78
81
  self.onboarding_service = onboarding_service
79
82
 
80
- def get(self, company_short_name: str):
81
- # 1. Use the centralized method to get session info.
83
+ def get(self, company_short_name: str, token: str = None):
82
84
  session_info = self.profile_service.get_current_session_info()
83
- user_identifier = session_info.get('user_identifier')
84
-
85
- if not user_identifier:
86
- # This can happen if the session expires or is invalid.
85
+ if session_info:
86
+ # session exists, internal user
87
+ user_identifier = session_info.get('user_identifier')
88
+ token = ''
89
+ elif token:
90
+ # user identified by api-key
91
+ payload = self.jwt_service.validate_chat_jwt(token)
92
+ if not payload:
93
+ logging.warning("Fallo crítico: No se pudo leer el auth token.")
94
+ return redirect(url_for('index', company_short_name=company_short_name))
95
+
96
+ user_identifier = payload.get('user_identifier')
97
+ else:
98
+ logging.warning("Fallo crítico: missing session information or auth token")
87
99
  return redirect(url_for('index', company_short_name=company_short_name))
88
100
 
89
101
  company = self.profile_service.get_company_by_short_name(company_short_name)
@@ -104,13 +116,17 @@ class FinalizeContextView(MethodView):
104
116
 
105
117
  return render_template(
106
118
  "chat.html",
119
+ company_short_name=company_short_name,
120
+ user_identifier=user_identifier,
107
121
  branding=branding_data,
108
122
  prompts=prompts,
109
- onboarding_cards=onboarding_cards
123
+ onboarding_cards=onboarding_cards,
124
+ redeem_token=token
110
125
  )
111
126
 
112
127
  except Exception as e:
113
128
  return render_template("error.html",
114
129
  company=company,
115
130
  company_short_name=company_short_name,
116
- message=f"An unexpected error occurred during context loading: {str(e)}"), 500
131
+ message=f"An unexpected error occurred during context loading: {str(e)}"), 500
132
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iatoolkit
3
- Version: 0.55.2
3
+ Version: 0.56.0
4
4
  Summary: IAToolkit
5
5
  Author: Fernando Libedinsky
6
6
  License-Expression: MIT
@@ -1,11 +1,11 @@
1
1
  iatoolkit/__init__.py,sha256=4PWjMJjktixtrxF6BY405qyA50Sv967kEP2x-oil6qk,1120
2
- iatoolkit/base_company.py,sha256=uFJmy77LPAceVqkTeuJqo15-auDiq4aTwvC_bbBD0mQ,4607
2
+ iatoolkit/base_company.py,sha256=nfF-G0h63jy3Qh9kCnvx8Ozx76IjG2p7a34HpweWhOk,4608
3
3
  iatoolkit/cli_commands.py,sha256=G5L9xQXZ0lVFXQWBaE_KEZHyfuiT6PL1nTQRoSdnBzc,2302
4
4
  iatoolkit/company_registry.py,sha256=tduqt3oV8iDX_IB1eA7KIgvIxE4edTcy-3qZIXh3Lzw,2549
5
- iatoolkit/iatoolkit.py,sha256=G_4plhotRBuf-u6OsFZhov_MoFbdvyd--u1vVc3hz18,17645
5
+ iatoolkit/iatoolkit.py,sha256=ve63cb-1L-m_RLPsU7KRG9IPhhMREb1fDQjqOIgHx9w,17583
6
6
  iatoolkit/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  iatoolkit/common/exceptions.py,sha256=EXx40n5htp7UiOM6P1xfJ9U6NMcADqm62dlFaKz7ICU,1154
8
- iatoolkit/common/routes.py,sha256=LniD_xsVZ6iHx1Z2_daOChhXZ9AQwsYFtqWSuFkQVyk,6117
8
+ iatoolkit/common/routes.py,sha256=c8kRk8pEK_nV-2P1LiYXRERmW_QoW21Hk6CZkkizv6Y,6419
9
9
  iatoolkit/common/session_manager.py,sha256=UeKfD15bcEA3P5e0WSURfotLqpsiIMp3AXxAMhtgHs0,471
10
10
  iatoolkit/common/util.py,sha256=w9dTd3csK0gKtFSp-a4t7XmCPZiYDhiON92uXRbTT8A,14624
11
11
  iatoolkit/infra/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
@@ -26,39 +26,39 @@ iatoolkit/infra/connectors/google_drive_connector.py,sha256=WR1AlO5-Bl3W89opdja0
26
26
  iatoolkit/infra/connectors/local_file_connector.py,sha256=hrzIgpMJOTuwTqzlQeTIU_50ZbZ6yl8lcWPv6hMnoqI,1739
27
27
  iatoolkit/infra/connectors/s3_connector.py,sha256=Nj4_YaLobjfcnbZewJf21_K2EXohgcc3mJll1Pzn4zg,1123
28
28
  iatoolkit/repositories/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
29
- iatoolkit/repositories/database_manager.py,sha256=nrBSAPuFkTYlvAht_3Kp2XeIDiqjq-mBTeqfCoMfn3A,3515
29
+ iatoolkit/repositories/database_manager.py,sha256=QgV8hNnVv9RmeOvUdomdj_mfk0bf3Rl8Ti41a-5zIAY,3700
30
30
  iatoolkit/repositories/document_repo.py,sha256=Y7bF1kZB1HWJsAGjWdF7P2aVYeTYNufq9ngQXp7mDkY,1124
31
31
  iatoolkit/repositories/llm_query_repo.py,sha256=YT_t7cYGQk8rwzH_17-28aTzO-e2jUfa2rvXy8tugvA,3612
32
32
  iatoolkit/repositories/models.py,sha256=3YbIJXNMZiTkMbPyiSOiyzqUKEQF0JIfN4VSWYzwr44,13146
33
- iatoolkit/repositories/profile_repo.py,sha256=vwDuVec9IDUCSyM7ecf790oqdpV36cpM2bgqP8POkHQ,4128
33
+ iatoolkit/repositories/profile_repo.py,sha256=21am3GP7XCG0nq6i3pArQ7mfGsrRn8rdcWT98fsdwlU,4397
34
34
  iatoolkit/repositories/tasks_repo.py,sha256=icVO_r2oPagGnnBhwVFzznnvEEU2EAx-2dlWuWvoDC4,1745
35
35
  iatoolkit/repositories/vs_repo.py,sha256=UkpmQQiocgM5IwRBmmWhw3HHzHP6zK1nN3J3TcQgjhc,5300
36
36
  iatoolkit/services/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
37
- iatoolkit/services/auth_service.py,sha256=9vK09ozxdjcSwM8H8HVdsSb3JXx00GnFs4o7tyfOXLs,2762
37
+ iatoolkit/services/auth_service.py,sha256=XHG2F0Vf2wDFjylc_OdDEIJL8I4cdhxeUIZIltgE4DU,2936
38
38
  iatoolkit/services/benchmark_service.py,sha256=CdbFYyS3FHFhNzWQEa9ZNjUlmON10DT1nKNbZQ1EUi8,5880
39
39
  iatoolkit/services/branding_service.py,sha256=gXj9Lj6EIFNIHT6wAHia5lr4_2a2sD-ExMbewno5YD8,7505
40
- iatoolkit/services/dispatcher_service.py,sha256=7tltTH-r-Bg2J2xs597Vyhzflw736dOpPG_soPZFokI,13570
40
+ iatoolkit/services/dispatcher_service.py,sha256=Qdn2x4cozpgpKg2448sUxkhO6tuplzb8xPWUxdTTFBE,12772
41
41
  iatoolkit/services/document_service.py,sha256=nMXrNtbHQuc9pSaten0LvKY0kT8_WngBDmZJUP3jNPw,5936
42
42
  iatoolkit/services/excel_service.py,sha256=JdAcg7_vPz3J16cf2chC6j7WYVpiT55tDX9667tfMUc,3764
43
43
  iatoolkit/services/file_processor_service.py,sha256=B1sUUhZNFf-rT4_1wrD38GKNoBFMp2g0dYrXYMCWe2E,4122
44
44
  iatoolkit/services/history_service.py,sha256=3IxcdpKV1mHBGIiv2KIYV3LsVQJ0GPdFuCOiGYRszMU,1255
45
- iatoolkit/services/jwt_service.py,sha256=YoZ9h7_o9xBko-arNQv4MbcwnxoSWVNj4VbZmMo_QGY,3908
45
+ iatoolkit/services/jwt_service.py,sha256=W2kQVNQheQSLkNLS7RZ4jd3hmySBPLbHAS5hvBrUI10,3244
46
46
  iatoolkit/services/load_documents_service.py,sha256=ZpB0BZ3qX1fGJGBtZtMLbFdWWx0hkPoeCS3OqJKwCTs,7291
47
47
  iatoolkit/services/mail_service.py,sha256=2h-fcF3swZDya_o7IpgXkmuj3iEVHVCiHi7oVxU99sQ,2182
48
48
  iatoolkit/services/onboarding_service.py,sha256=cMO2Ho1-G3wAeVNl-j25LwCMJjRwj3yKHpYKnZUFLDE,2001
49
- iatoolkit/services/profile_service.py,sha256=Y1EbTDNFDWrUQ4QqiaM2OfluZqiJfL9hhbeqJ7nsbtw,20235
49
+ iatoolkit/services/profile_service.py,sha256=uWlCsoGAPZaJkvVTBZVBWdh6Rk5LHcpDHWgYqi5vTp4,20317
50
50
  iatoolkit/services/prompt_manager_service.py,sha256=U-XmSpkeXvv1KRN4dytdMxSYBMRSB7y-UHcb18mk0nA,8342
51
- iatoolkit/services/query_service.py,sha256=sEmdBDLTpdZbNu43KAUO6vXqqWBBxTWKWlQm4oFGtSk,17796
51
+ iatoolkit/services/query_service.py,sha256=J42KzthYIIsUX8cUS2E7vcXA6IZOJ28Y8I637m-9za4,17649
52
52
  iatoolkit/services/search_service.py,sha256=i1xGWu7ORKIIDH0aAQBkF86dVVbLQ0Yrooz5TiZ6aGo,1823
53
53
  iatoolkit/services/sql_service.py,sha256=MIslAtpJWnTMgSD74nnqTvQj27p-lHiyRXc6OiA2C_c,2172
54
54
  iatoolkit/services/tasks_service.py,sha256=itREO5rDnUIgsqtyCOBKDtH30QL5v1egs4qPTiBK8xU,6865
55
55
  iatoolkit/services/user_feedback_service.py,sha256=ooy750qWmYOeJi-IJQofu8pLG4svGjGU_JKpKMURZkw,2353
56
56
  iatoolkit/services/user_session_context_service.py,sha256=TeYi4xF04Xk-4Qnp6cVTmxuNzA4B1nMVUuFDjTHeiZQ,6764
57
57
  iatoolkit/static/images/fernando.jpeg,sha256=W68TYMuo5hZVpbP-evwH6Nu4xWFv2bc8pJzSKDoLTeQ,100612
58
- iatoolkit/static/js/chat_feedback.js,sha256=DDT2NPgglrLnW75vtEAVXS72MNt7vlMcavzr94q80qQ,4398
58
+ iatoolkit/static/js/chat_feedback.js,sha256=zlLEDQfEocGK7RKG2baqI-9fyQlqe6hVuAHOKTPmWek,4399
59
59
  iatoolkit/static/js/chat_filepond.js,sha256=mzXafm7a506EpM37KATTK3zvAswO1E0KSUY1vKbwuRc,3163
60
- iatoolkit/static/js/chat_history.js,sha256=4hYNODIwYNd5vaQqkR28HZyXYIFKgSayrnmOuT_DUac,4381
61
- iatoolkit/static/js/chat_main.js,sha256=j3rbJjWWCEAM1XUXPv6K2SW4S3kBrPAwEzVtLTjHslI,17314
60
+ iatoolkit/static/js/chat_history.js,sha256=4h6ldU7cDvgkW84fMKB8JReoxCX0NKSQAir_4CzAF9I,4382
61
+ iatoolkit/static/js/chat_main.js,sha256=3ZsMy6NauobPRaYDvSJAws26l16FDslXGm8kGfXLt_E,18538
62
62
  iatoolkit/static/js/chat_onboarding.js,sha256=b6ofiFcPhuCaPmSFIvDQZqcMUVvbI7LpIsjZOZJUSAU,3185
63
63
  iatoolkit/static/styles/chat_iatoolkit.css,sha256=aA-PZ2TGl_k82JSVVBC2-CJT0NiZAuLOGoiaJhdeVUU,11416
64
64
  iatoolkit/static/styles/chat_info.css,sha256=17DbgoNYE21VYWfb5L9-QLCpD2R1idK4imKRLwXtJLY,1058
@@ -75,21 +75,21 @@ iatoolkit/templates/_navbar.html,sha256=o1PvZE5ueLmVpGUAmsjtu-vS_WPROTlJc2sTXl6A
75
75
  iatoolkit/templates/about.html,sha256=ciC08grUVz5qLzdzDDqDX31xirg5PrJIRYabWpV9oA8,294
76
76
  iatoolkit/templates/base.html,sha256=hHfBqZJsPcZGlb4BxAbHvpJFcSjckLIAVTYTmoyXrz0,2323
77
77
  iatoolkit/templates/change_password.html,sha256=G5a3hYLTpz_5Q_eZ4LNcYSqLeW-CuT4NCHD8bkhAd9k,3573
78
- iatoolkit/templates/chat.html,sha256=AgVynrJzUlBr8aG334YKg3U35Nas0-RoVfzFAx9Ntfk,13961
78
+ iatoolkit/templates/chat.html,sha256=usmyuke0Moh6jNMWvxK2p_N15V6UzHNeXyXDd45XWIU,14087
79
79
  iatoolkit/templates/chat_modals.html,sha256=NwwgPoOmVbjy4aO2eHsy1TUMXRiOfTOC5Jx_F2ehhcs,6947
80
80
  iatoolkit/templates/error.html,sha256=c3dxieMygsvdjQMiQu_sn6kqqag9zFtVu-z5FunX6so,580
81
81
  iatoolkit/templates/forgot_password.html,sha256=NRZqbNHJXSLNArF_KLbzuem-U57v07awS0ikI_DJbfM,2360
82
82
  iatoolkit/templates/header.html,sha256=179agI7rnYwP_rvJNXIiVde5E8Ec5649_XKq6eew2Hk,1263
83
83
  iatoolkit/templates/index.html,sha256=Q2wBHDv3HTBt-jURVLggye7XOqoqDY8E0LnrmLdc3SQ,7910
84
- iatoolkit/templates/login_test.html,sha256=ApvW4l0UmcjJ9pBl3jm-0BojWwiFV6ZyCWz3caM1DN4,4722
84
+ iatoolkit/templates/login_simulation.html,sha256=1svwCBPrJ3Gy6bD9WMuz25NBSdFgZt4j8_sC7HE6MFU,1270
85
85
  iatoolkit/templates/onboarding_shell.html,sha256=r1ivSR2ci8GrDSm1uaD-cf78rfO1bKT5gXa-v5aHLAk,4659
86
86
  iatoolkit/templates/signup.html,sha256=9ArDvcNQgHFR2dwxy-37AXzGUOeOsT7Nz5u0y6fAB3U,4385
87
87
  iatoolkit/templates/test.html,sha256=rwNtxC83tbCl5COZFXYvmRBxxmgFJtPNuVBd_nq9KWY,133
88
88
  iatoolkit/views/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
89
- iatoolkit/views/base_login_view.py,sha256=6IWJXEg_6K9cPYoKnG_-Kpt_hIdYnv5ZNC5anIrytXk,2819
89
+ iatoolkit/views/base_login_view.py,sha256=6AANVhwhs5MyT3WVjJqKZLoS1TwyErsrKnb1xhpqP4Y,3970
90
90
  iatoolkit/views/change_password_view.py,sha256=tM0woZyKdhY4XYjS_YXg2sKq3RYkXGfcq_eVAKrNvNM,4498
91
91
  iatoolkit/views/chat_token_request_view.py,sha256=wf32_A2Sq8NHYWshCwL10Tovd1znLoD0jQjzutR3sVE,4408
92
- iatoolkit/views/external_login_view.py,sha256=qY5wxh98HyKI0Q1Phf6pkOzRGD7PvbH27GUZn0ccYu4,2889
92
+ iatoolkit/views/external_login_view.py,sha256=nuWebrl2Opnhj6H-UW6dUOHwi8pj4s1_-dN3VEKNQdY,4483
93
93
  iatoolkit/views/file_store_api_view.py,sha256=Uz9f6sey3_F5K8zuyQz6SwYRKAalCjD1ekf-Mkl_Kfo,2326
94
94
  iatoolkit/views/forgot_password_view.py,sha256=-qKJeeOBqJFdvDUk7rCNg1E1cDQnJQkozPpb0T0FgwA,3159
95
95
  iatoolkit/views/history_api_view.py,sha256=x-tZhB8UzqrD2n-WDIfmHK9iVhGZ9f0yncsGs9mxwt0,1953
@@ -97,15 +97,15 @@ iatoolkit/views/index_view.py,sha256=P5aVdEWxsYOZGbzcXd6WFE733qZ7YXIoeqriUMAM6V8
97
97
  iatoolkit/views/init_context_api_view.py,sha256=1j8NKfODfPrffbA5YO8TPMHh-ildlLNzReIxv_qO-W4,2586
98
98
  iatoolkit/views/llmquery_api_view.py,sha256=Rh-y-VENwwtNsDrYAD_SWKwjK16fW-pFRWlEvI-OYwY,2120
99
99
  iatoolkit/views/llmquery_web_view.py,sha256=WhjlA1mfsoL8hL9tlKQfjCUcaTzT43odlp_uQKmT314,1500
100
- iatoolkit/views/login_simulation_view.py,sha256=hujduAuq84YyHXfG1iVVbypkwYOreKr9FnQNvIXBDyQ,2675
101
- iatoolkit/views/login_view.py,sha256=3mV-I1KWrVCi4GBmSWCPmIIKj0reZEBMmyTZdBKFCFM,4630
100
+ iatoolkit/views/login_simulation_view.py,sha256=0Qt-puRnltI2HZxlfdyJmOf26-hQp3xjknGV_jkwV7E,3484
101
+ iatoolkit/views/login_view.py,sha256=sjM8ki_F7C9S0a9pukLd5jn5KnM2sbRabnAIrtIK8KQ,5373
102
102
  iatoolkit/views/prompt_api_view.py,sha256=MP0r-MiswwKcbNc_5KY7aVbHkrR218I8XCiCX1D0yTA,1244
103
103
  iatoolkit/views/signup_view.py,sha256=BCjhM2lMiDPwYrlW_eEwPl-ZLupblbFfsonWtq0E4vU,3922
104
104
  iatoolkit/views/tasks_review_view.py,sha256=keLsLCyOTTlcoIapnB_lbuSvLwrPVZVpBiFC_7ChbLg,3388
105
105
  iatoolkit/views/tasks_view.py,sha256=a3anTXrJTTvbQuc6PSpOzidLKQFL4hWa7PI2Cppcz8w,4110
106
106
  iatoolkit/views/user_feedback_api_view.py,sha256=59XB9uQLHI4Q6QA4_XhK787HzfXb-c6EY7k1Ccyr4hI,2424
107
107
  iatoolkit/views/verify_user_view.py,sha256=7XLSaxvs8LjBr3cYOUDa9B8DqW_50IGlq0IvmOQcD0Y,2340
108
- iatoolkit-0.55.2.dist-info/METADATA,sha256=hR0pzM6CSYaGhZCLbwA_G4A-_85DUV3ttYIojwpDkY4,9301
109
- iatoolkit-0.55.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
110
- iatoolkit-0.55.2.dist-info/top_level.txt,sha256=V_w4QvDx0b1RXiy8zTCrD1Bp7AZkFe3_O0-9fMiwogg,10
111
- iatoolkit-0.55.2.dist-info/RECORD,,
108
+ iatoolkit-0.56.0.dist-info/METADATA,sha256=Td4cobpNkkpPaIPZALITKGwwBHowzfG3klOYoGANjsk,9301
109
+ iatoolkit-0.56.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
110
+ iatoolkit-0.56.0.dist-info/top_level.txt,sha256=V_w4QvDx0b1RXiy8zTCrD1Bp7AZkFe3_O0-9fMiwogg,10
111
+ iatoolkit-0.56.0.dist-info/RECORD,,
@@ -1,118 +0,0 @@
1
- {% extends "base.html" %}
2
-
3
- {% block title %}Prueba de Login para {{ branding.name }}{% endblock %}
4
-
5
- {% block styles %}
6
- {# Este bloque asegura que los colores de la marca estén disponibles como variables CSS #}
7
- {% if branding and branding.css_variables %}
8
- <style>
9
- {{ branding.css_variables|safe }}
10
- /* Usa la variable de CSS para el color primario del botón */
11
- #initiateJwtChatButton {
12
- background-color: var(--brand-primary-color, #0d6efd);
13
- border-color: var(--brand-primary-color, #0d6efd);
14
- }
15
- </style>
16
- {% endif %}
17
- {% endblock %}
18
-
19
- {% block content %}
20
- <div class="container-fluid">
21
- <div class="row flex-fill mt-5 justify-content-center">
22
- <!-- login desde sistema externo -->
23
- <div class="col-12 col-lg-6">
24
- <div class="border rounded p-4 p-md-5 shadow-sm bg-light">
25
- {# El título ahora muestra dinámicamente el nombre de la empresa #}
26
- <h3 class="text-muted fw-semibold text-start mb-3">
27
- Login Externo para <span style="color: var(--brand-primary-color, #0d6efd);">{{ branding.name }}</span>
28
- </h3>
29
- <div class="text-center mb-4">
30
- <p class="text-muted widget-intro-text">
31
- Este formulario permite testear el acceso a IAToolkit desde un portal externo utilizando una api-key. El external user ID es el nombre del usuario ya autentificado en algún portal interno de la empresa.
32
- </p>
33
- </div>
34
-
35
- {# El 'action' y 'method' son manejados por JS, pero el id es crucial #}
36
- <form id="jwt-form">
37
-
38
- <div class="mb-3">
39
- <label for="external_user_id" class="form-label d-block">External user ID</label>
40
- <input type="text" id="external_user_id" name="external_user_id" class="form-control" required>
41
- </div>
42
-
43
- <button type="submit" id="initiateJwtChatButton" class="btn btn-primary">
44
- <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
45
- Iniciar Sesión
46
- </button>
47
- </form>
48
- </div>
49
- </div>
50
- </div>
51
- </div>
52
- {% endblock %}
53
-
54
- {% block scripts %}
55
- <script>
56
- document.addEventListener('DOMContentLoaded', function() {
57
- const companyShortName = "{{ company_short_name }}";
58
- const apiKey = "{{ api_key }}";
59
- const loginForm = document.getElementById('jwt-form');
60
- const userIdInput = document.getElementById('external_user_id');
61
- const submitButton = document.getElementById('initiateJwtChatButton');
62
- const spinner = submitButton.querySelector('.spinner-border');
63
-
64
- loginForm.addEventListener('submit', function(event) {
65
- event.preventDefault();
66
- const userIdentifier = userIdInput.value.trim();
67
- if (!userIdentifier) return;
68
-
69
- // Deshabilitar botón y mostrar spinner
70
- submitButton.disabled = true;
71
- spinner.classList.remove('d-none');
72
-
73
- const newWindow = window.open('', '_blank');
74
- const apiUrl = `/${companyShortName}/external_login`;
75
-
76
- fetch(apiUrl, {
77
- method: 'POST',
78
- headers: {
79
- 'Content-Type': 'application/json',
80
- 'Authorization': `Bearer ${apiKey}`
81
- },
82
- body: JSON.stringify({ external_user_id: userIdentifier })
83
- })
84
- .then(response => response.ok ? response.text() : response.text().then(text => { throw new Error(text) }))
85
- .then(html => {
86
- newWindow.document.documentElement.innerHTML = html;
87
- // Buscamos todos los scripts en el HTML inyectado y los re-ejecutamos.
88
- const scripts = newWindow.document.querySelectorAll('script');
89
- scripts.forEach(oldScript => {
90
- const newScript = newWindow.document.createElement('script');
91
-
92
- // Copiamos los atributos (como 'src' para archivos externos)
93
- Array.from(oldScript.attributes).forEach(attr => {
94
- newScript.setAttribute(attr.name, attr.value);
95
- });
96
-
97
- // Copiamos el contenido para scripts inline
98
- newScript.appendChild(newWindow.document.createTextNode(oldScript.innerHTML));
99
-
100
- // Reemplazamos el script viejo por el nuevo para que se ejecute.
101
- oldScript.parentNode.replaceChild(newScript, oldScript);
102
- });
103
-
104
- })
105
- .catch(error => {
106
- console.error("Falló la prueba de login externo:", error);
107
- const errorMessage = error.message || "Error desconocido.";
108
- newWindow.document.body.innerHTML = `<div style="padding: 20px; font-family: monospace;"><h2>Error</h2><pre>${errorMessage}</pre></div>`;
109
- })
110
- .finally(() => {
111
- // Volver a habilitar el botón y ocultar el spinner
112
- submitButton.disabled = false;
113
- spinner.classList.add('d-none');
114
- });
115
- });
116
- });
117
- </script>
118
- {% endblock %}