iatoolkit 0.59.2__py3-none-any.whl → 0.60.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of iatoolkit might be problematic. Click here for more details.

Files changed (30) hide show
  1. iatoolkit/common/routes.py +12 -21
  2. iatoolkit/iatoolkit.py +5 -5
  3. iatoolkit/services/auth_service.py +32 -25
  4. iatoolkit/services/profile_service.py +1 -0
  5. iatoolkit/static/js/{chat_feedback.js → chat_feedback_button.js} +4 -7
  6. iatoolkit/static/js/chat_logout_button.js +36 -0
  7. iatoolkit/static/js/chat_main.js +0 -8
  8. iatoolkit/static/js/{chat_onboarding.js → chat_onboarding_button.js} +0 -1
  9. iatoolkit/static/js/{chat_context_reload.js → chat_reload_button.js} +2 -4
  10. iatoolkit/static/styles/chat_iatoolkit.css +44 -0
  11. iatoolkit/templates/chat.html +18 -10
  12. iatoolkit/templates/onboarding_shell.html +1 -1
  13. iatoolkit/views/change_password_view.py +2 -2
  14. iatoolkit/views/external_login_view.py +5 -11
  15. iatoolkit/views/file_store_api_view.py +7 -9
  16. iatoolkit/views/history_api_view.py +8 -8
  17. iatoolkit/views/init_context_api_view.py +2 -4
  18. iatoolkit/views/llmquery_api_view.py +1 -3
  19. iatoolkit/views/logout_api_view.py +45 -0
  20. iatoolkit/views/prompt_api_view.py +5 -5
  21. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  22. iatoolkit/views/tasks_review_api_view.py +55 -0
  23. iatoolkit/views/user_feedback_api_view.py +6 -8
  24. {iatoolkit-0.59.2.dist-info → iatoolkit-0.60.1.dist-info}/METADATA +1 -1
  25. {iatoolkit-0.59.2.dist-info → iatoolkit-0.60.1.dist-info}/RECORD +28 -27
  26. iatoolkit/views/chat_token_request_view.py +0 -98
  27. iatoolkit/views/tasks_review_view.py +0 -83
  28. /iatoolkit/static/js/{chat_history.js → chat_history_button.js} +0 -0
  29. {iatoolkit-0.59.2.dist-info → iatoolkit-0.60.1.dist-info}/WHEEL +0 -0
  30. {iatoolkit-0.59.2.dist-info → iatoolkit-0.60.1.dist-info}/top_level.txt +0 -0
@@ -3,17 +3,9 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from flask import render_template, redirect, flash, url_for,send_from_directory, current_app, abort
7
- from iatoolkit.common.session_manager import SessionManager
6
+ from flask import render_template, redirect, url_for,send_from_directory, current_app, abort
8
7
  from flask import jsonify
9
8
  from iatoolkit.views.history_api_view import HistoryApiView
10
- import os
11
-
12
-
13
- def logout(company_short_name: str):
14
- SessionManager.clear()
15
- flash("Has cerrado sesión correctamente", "info")
16
- return redirect(url_for('index', company_short_name=company_short_name))
17
9
 
18
10
 
19
11
  # this function register all the views
@@ -22,8 +14,8 @@ def register_views(injector, app):
22
14
  from iatoolkit.views.index_view import IndexView
23
15
  from iatoolkit.views.init_context_api_view import InitContextApiView
24
16
  from iatoolkit.views.llmquery_api_view import LLMQueryApiView
25
- from iatoolkit.views.tasks_view import TaskView
26
- from iatoolkit.views.tasks_review_view import TaskReviewView
17
+ from iatoolkit.views.tasks_api_view import TaskApiView
18
+ from iatoolkit.views.tasks_review_api_view import TaskReviewApiView
27
19
  from iatoolkit.views.login_simulation_view import LoginSimulationView
28
20
  from iatoolkit.views.signup_view import SignupView
29
21
  from iatoolkit.views.verify_user_view import VerifyAccountView
@@ -32,9 +24,10 @@ def register_views(injector, app):
32
24
  from iatoolkit.views.file_store_api_view import FileStoreApiView
33
25
  from iatoolkit.views.user_feedback_api_view import UserFeedbackApiView
34
26
  from iatoolkit.views.prompt_api_view import PromptApiView
35
- from iatoolkit.views.chat_token_request_view import ChatTokenRequestView
36
27
  from iatoolkit.views.login_view import LoginView, FinalizeContextView
37
28
  from iatoolkit.views.external_login_view import ExternalLoginView, RedeemTokenApiView
29
+ from iatoolkit.views.logout_api_view import LogoutApiView
30
+
38
31
 
39
32
  # iatoolkit home page
40
33
  app.add_url_rule('/<company_short_name>', view_func=IndexView.as_view('index'))
@@ -57,23 +50,21 @@ def register_views(injector, app):
57
50
  view_func=FinalizeContextView.as_view('finalize_with_token')
58
51
  )
59
52
 
53
+ # logout
54
+ app.add_url_rule('/<company_short_name>/api/logout',
55
+ view_func=LogoutApiView.as_view('logout'))
56
+
60
57
  # this endpoint is called by the JS for changing the token for a session
61
58
  app.add_url_rule('/<string:company_short_name>/api/redeem_token',
62
59
  view_func = RedeemTokenApiView.as_view('redeem_token'))
63
60
 
64
- # this endpoint is for requesting a chat token for external users
65
- app.add_url_rule('/auth/chat_token',
66
- view_func=ChatTokenRequestView.as_view('chat-token'))
67
-
68
- # init (reset) the company context (with api-key)
61
+ # init (reset) the company context
69
62
  app.add_url_rule('/<company_short_name>/api/init-context',
70
63
  view_func=InitContextApiView.as_view('init-context'),
71
64
  methods=['POST', 'OPTIONS'])
72
65
 
73
66
  # register new user, account verification and forgot password
74
67
  app.add_url_rule('/<company_short_name>/signup',view_func=SignupView.as_view('signup'))
75
- app.add_url_rule('/<company_short_name>/logout', 'logout', logout)
76
- app.add_url_rule('/logout', 'logout', logout)
77
68
  app.add_url_rule('/<company_short_name>/verify/<token>', view_func=VerifyAccountView.as_view('verify_account'))
78
69
  app.add_url_rule('/<company_short_name>/forgot-password', view_func=ForgotPasswordView.as_view('forgot_password'))
79
70
  app.add_url_rule('/<company_short_name>/change-password/<token>', view_func=ChangePasswordView.as_view('change_password'))
@@ -90,8 +81,8 @@ def register_views(injector, app):
90
81
  app.add_url_rule('/<company_short_name>/api/history', view_func=HistoryApiView.as_view('history'))
91
82
 
92
83
  # tasks management endpoints: create task, and review answer
93
- app.add_url_rule('/tasks', view_func=TaskView.as_view('tasks'))
94
- app.add_url_rule('/tasks/review/<int:task_id>', view_func=TaskReviewView.as_view('tasks-review'))
84
+ app.add_url_rule('/tasks', view_func=TaskApiView.as_view('tasks'))
85
+ app.add_url_rule('/tasks/review/<int:task_id>', view_func=TaskReviewApiView.as_view('tasks-review'))
95
86
 
96
87
  # this endpoint is for upload documents into the vector store (api-key)
97
88
  app.add_url_rule('/api/load', view_func=FileStoreApiView.as_view('load_api'))
iatoolkit/iatoolkit.py CHANGED
@@ -16,10 +16,10 @@ import os
16
16
  from typing import Optional, Dict, Any
17
17
  from iatoolkit.repositories.database_manager import DatabaseManager
18
18
  from werkzeug.middleware.proxy_fix import ProxyFix
19
- from injector import Binder, singleton, Injector
19
+ from injector import Binder, Injector, singleton
20
20
  from importlib.metadata import version as _pkg_version, PackageNotFoundError
21
21
 
22
- IATOOLKIT_VERSION = "0.59.2"
22
+ IATOOLKIT_VERSION = "0.60.1"
23
23
 
24
24
  # global variable for the unique instance of IAToolkit
25
25
  _iatoolkit_instance: Optional['IAToolkit'] = None
@@ -52,7 +52,7 @@ class IAToolkit:
52
52
  self.app = None
53
53
  self.db_manager = None
54
54
  self._injector = None
55
- self.version = IATOOLKIT_VERSION
55
+ self.version = IATOOLKIT_VERSION # default version
56
56
 
57
57
  @classmethod
58
58
  def get_instance(cls) -> 'IAToolkit':
@@ -324,8 +324,8 @@ class IAToolkit:
324
324
  from iatoolkit.services.auth_service import AuthService
325
325
  from iatoolkit.common.util import Utility
326
326
 
327
- binder.bind(LLMProxy, to=LLMProxy, scope=singleton)
328
- binder.bind(llmClient, to=llmClient, scope=singleton)
327
+ binder.bind(LLMProxy, to=LLMProxy)
328
+ binder.bind(llmClient, to=llmClient)
329
329
  binder.bind(GoogleChatApp, to=GoogleChatApp)
330
330
  binder.bind(MailApp, to=MailApp)
331
331
  binder.bind(AuthService, to=AuthService)
@@ -91,9 +91,10 @@ class AuthService:
91
91
  )
92
92
  return {'success': False, 'error': 'No se pudo crear la sesión del usuario'}
93
93
 
94
- def verify(self) -> dict:
94
+ def verify(self, anonymous: bool = False) -> dict:
95
95
  """
96
96
  Verifies the current request and identifies the user.
97
+ If anonymous is True the non-presence of use_identifier is ignored
97
98
 
98
99
  Returns a dictionary with:
99
100
  - success: bool
@@ -118,31 +119,37 @@ class AuthService:
118
119
  if isinstance(auth, str) and auth.lower().startswith('bearer '):
119
120
  api_key = auth.split(' ', 1)[1].strip()
120
121
 
121
- if api_key:
122
- api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
123
- if not api_key_entry:
124
- logging.info(f"Invalid or inactive API Key {api_key}")
125
- return {"success": False, "error_message": "Invalid or inactive API Key", "status_code": 401}
122
+ if not api_key:
123
+ # --- Failure: No valid credentials found ---
124
+ logging.info(f"Authentication required. No session cookie or API Key provided.")
125
+ return {"success": False,
126
+ "error_message": "Authentication required. No session cookie or API Key provided.",
127
+ "status_code": 401}
128
+
129
+ # check if the api-key is valid and active
130
+ api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
131
+ if not api_key_entry:
132
+ logging.info(f"Invalid or inactive API Key {api_key}")
133
+ return {"success": False, "error_message": "Invalid or inactive API Key",
134
+ "status_code": 402}
135
+
136
+ # get the company from the api_key_entry
137
+ company = api_key_entry.company
138
+
139
+ # For API calls, the external_user_id must be provided in the request.
140
+ data = request.get_json(silent=True) or {}
141
+ user_identifier = data.get('user_identifier', '')
142
+ if not anonymous and not user_identifier:
143
+ logging.info(f"No user_identifier provided for API call.")
144
+ return {"success": False, "error_message": "No user_identifier provided for API call.",
145
+ "status_code": 403}
146
+
147
+ return {
148
+ "success": True,
149
+ "company_short_name": company.short_name,
150
+ "user_identifier": user_identifier
151
+ }
126
152
 
127
- # obtain the company from the api_key_entry
128
- company = api_key_entry.company
129
-
130
- # For API calls, the external_user_id must be provided in the request.
131
- user_identifier = ''
132
- if request.is_json:
133
- data = request.get_json() or {}
134
- user_identifier = data.get('user_identifier', '')
135
-
136
- return {
137
- "success": True,
138
- "company_short_name": company.short_name,
139
- "user_identifier": user_identifier
140
- }
141
-
142
- # --- Failure: No valid credentials found ---
143
- logging.info(f"Authentication required. No session cookie or API Key provided. session: {str(session_info)}")
144
- return {"success": False, "error_message": "Authentication required. No session cookie or API Key provided.",
145
- "status_code": 402}
146
153
 
147
154
  def log_access(self,
148
155
  company_short_name: str,
@@ -131,6 +131,7 @@ class ProfileService:
131
131
  "profile": profile
132
132
  }
133
133
 
134
+
134
135
  def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
135
136
  """
136
137
  Fetches a user profile directly by their identifier, bypassing the Flask session.
@@ -42,14 +42,11 @@ $(document).ready(function () {
42
42
  submitButton.html('<i class="bi bi-send me-1 icon-spaced"></i>Enviando...');
43
43
 
44
44
  const response = await sendFeedback(feedbackText);
45
-
46
45
  $('#feedbackModal').modal('hide');
47
-
48
- if (response) {
49
- Swal.fire({ icon: 'success', title: 'Feedback enviado', text: 'Gracias por tu comentario.' });
50
- } else {
51
- Swal.fire({ icon: 'error', title: 'Error', text: 'No se pudo enviar el feedback, intente nuevamente.' });
52
- }
46
+ if (response)
47
+ toastr.success('¡Gracias por tu comentario!', 'Feedback Enviado');
48
+ else
49
+ toastr.error('No se pudo enviar el feedback, por favor intente nuevamente.', 'Error');
53
50
  });
54
51
 
55
52
  // Evento para abrir el modal de feedback
@@ -0,0 +1,36 @@
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ const logoutButton = document.getElementById('logout-button');
3
+ if (!logoutButton) {
4
+ console.warn('El botón de logout con id "logout-button" no fue encontrado.');
5
+ return;
6
+ }
7
+
8
+ if (window.toastr) {
9
+ toastr.options = { "positionClass": "toast-bottom-right", "preventDuplicates": true };
10
+ }
11
+
12
+ logoutButton.addEventListener('click', async function(event) {
13
+ event.preventDefault();
14
+
15
+ try {
16
+ const apiPath = '/api/logout';
17
+ const data = await callToolkit(apiPath, null, 'GET');
18
+
19
+ // Procesar la respuesta
20
+ if (data && data.status === 'success' && data.url) {
21
+ window.top.location.href = data.url;
22
+ } else {
23
+ // Si algo falla, callToolkit usualmente muestra un error.
24
+ // Mostramos un toast como fallback.
25
+ if (window.toastr) {
26
+ toastr.error('No se pudo procesar el cierre de sesión. Por favor, intente de nuevo.');
27
+ }
28
+ }
29
+ } catch (error) {
30
+ console.error('Error durante el logout:', error);
31
+ if (window.toastr) {
32
+ toastr.error('Ocurrió un error de red al intentar cerrar sesión.');
33
+ }
34
+ }
35
+ });
36
+ });
@@ -1,7 +1,6 @@
1
1
  // Global variables for request management
2
2
  let isRequestInProgress = false;
3
3
  let abortController = null;
4
-
5
4
  let selectedPrompt = null; // Will hold a lightweight prompt object
6
5
 
7
6
  $(document).ready(function () {
@@ -325,13 +324,6 @@ const callToolkit = async function(apiPath, data, method, timeoutMs = 500000) {
325
324
  clearTimeout(timeoutId);
326
325
 
327
326
  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
- }
335
327
  try {
336
328
  // Intentamos leer el error como JSON, que es el formato esperado de nuestra API.
337
329
  const errorData = await response.json();
@@ -1,4 +1,3 @@
1
- // static/js/chat_onboarding.js
2
1
  (function (global) {
3
2
  function qs(root, sel) { return (typeof sel === 'string') ? root.querySelector(sel) : sel; }
4
3
 
@@ -30,12 +30,10 @@ document.addEventListener('DOMContentLoaded', function() {
30
30
  // 4. Procesar la respuesta
31
31
  // callToolkit devuelve null si hubo un error que ya mostró en el chat.
32
32
  if (data) {
33
- if (data.status === 'OK') {
33
+ if (data.status === 'OK')
34
34
  toastr.success(data.message || 'Contexto recargado exitosamente.');
35
- } else {
36
- // El servidor respondió 200 OK pero con un mensaje de error en el cuerpo
35
+ else
37
36
  toastr.error(data.error_message || 'Ocurrió un error desconocido durante la recarga.');
38
- }
39
37
  } else {
40
38
  // Si data es null, callToolkit ya manejó el error (mostrando un mensaje en el chat).
41
39
  // Añadimos un toast para notificar al usuario que algo falló.
@@ -455,3 +455,47 @@
455
455
  #send-button i {
456
456
  color: var(--brand-send-button-color);
457
457
  }
458
+
459
+ /* Estilos personalizados para Toastr usando las variables de Branding */
460
+
461
+ /* --- Toast de Información (Usa el color primario de la marca) --- */
462
+ .toast-info {
463
+ /* ¡Importante! Usamos las variables CSS de tu BrandingService */
464
+ background-color: var(--brand-primary-color) !important;
465
+ color: var(--brand-text-on-primary) !important;
466
+ opacity: 0.95 !important;
467
+ }
468
+ .toast-info .toast-progress {
469
+ /* Usamos un color ligeramente más oscuro o una variante,
470
+ pero para simplificar, podemos empezar con el mismo */
471
+ background-color: rgba(0, 0, 0, 0.2) !important;
472
+ }
473
+
474
+ /* --- Toast de Éxito (Usa el verde por defecto o uno de marca si lo defines) --- */
475
+ .toast-success {
476
+ background-color: #198754 !important; /* Puedes crear una variable --brand-success-color si quieres */
477
+ color: #ffffff !important;
478
+ opacity: 0.95 !important;
479
+ }
480
+ .toast-success .toast-progress {
481
+ background-color: rgba(0, 0, 0, 0.2) !important;
482
+ }
483
+
484
+ /* --- Toast de Error (Usa el color de peligro de la marca) --- */
485
+ .toast-error {
486
+ background-color: var(--brand-danger-color) !important;
487
+ color: var(--brand-text-on-primary) !important; /* Asumimos texto blanco sobre el color de peligro */
488
+ opacity: 0.95 !important;
489
+ }
490
+ .toast-error .toast-progress {
491
+ background-color: rgba(0, 0, 0, 0.2) !important;
492
+ }
493
+
494
+ /* Opcional: Estilo para el botón de cierre para que contraste bien */
495
+ .toast-close-button {
496
+ color: var(--brand-text-on-primary) !important;
497
+ text-shadow: none !important;
498
+ }
499
+ .toast-close-button:hover {
500
+ opacity: 0.8;
501
+ }
@@ -40,7 +40,8 @@
40
40
  <div class="vr mx-3"></div>
41
41
 
42
42
  <!-- 3. Iconos de Acción -->
43
- <a href="javascript:void(0);" id="history-button"
43
+ <a href="javascript:void(0);"
44
+ id="history-button"
44
45
  class="action-icon-style" title="Historial con mis consultas" style="color: {{ branding.header_text_color }};">
45
46
  <i class="bi bi-clock-history"></i>
46
47
  </a>
@@ -51,19 +52,23 @@
51
52
  style="color: {{ branding.header_text_color }};">
52
53
  <i class="bi bi-arrow-clockwise"></i>
53
54
  </a>
54
- <a href="javascript:void(0);" id="send-feedback-button"
55
+ <a href="javascript:void(0);"
56
+ id="send-feedback-button"
55
57
  class="ms-3 action-icon-style" title="Tu feedback es muy importante" style="color: {{ branding.header_text_color }};">
56
58
  <i class="bi bi-emoji-smile"></i>
57
59
  </a>
58
- <a href="javascript:void(0);" id="onboarding-button"
60
+ <a href="javascript:void(0);"
61
+ id="onboarding-button"
59
62
  class="ms-3 action-icon-style" title="Ver onboarding"
60
63
  style="color: {{ branding.header_text_color }};">
61
64
  <i class="bi bi-lightbulb"></i>
62
65
  </a>
63
66
 
64
67
  <!-- Icono de cerrar sesión (al final) -->
65
- <a href="{{ url_for('logout', company_short_name=company_short_name, _external=True) }}"
66
- class="ms-3 action-icon-style" title="Cerrar sesión" style="color: {{ branding.header_text_color }} !important;">
68
+ <a href="javascript:void(0);"
69
+ id="logout-button"
70
+ class="ms-3 action-icon-style" title="Cerrar sesión"
71
+ style="color: {{ branding.header_text_color }}; !important;">
67
72
  <i class="bi bi-box-arrow-right"></i>
68
73
  </a>
69
74
  </div>
@@ -192,13 +197,16 @@
192
197
  </script>
193
198
 
194
199
  <!-- Carga de los scripts JS externos después de definir las variables globales -->
195
- <script src="{{ url_for('static', filename='js/chat_onboarding.js', _external=True) }}"></script>
200
+ <script src="{{ url_for('static', filename='js/chat_onboarding_button.js', _external=True) }}"></script>
196
201
  <script src="{{ url_for('static', filename='js/chat_filepond.js', _external=True) }}"></script>
197
- <script src="{{ url_for('static', filename='js/chat_history.js', _external=True) }}"></script>
198
- <script src="{{ url_for('static', filename='js/chat_feedback.js', _external=True) }}"></script>
199
- <script src="{{ url_for('static', filename='js/chat_context_reload.js', _external=True) }}"></script><script src="{{ url_for('static', filename='js/chat_main.js', _external=True) }}"></script>
202
+ <script src="{{ url_for('static', filename='js/chat_history_button.js', _external=True) }}"></script>
203
+ <script src="{{ url_for('static', filename='js/chat_feedback_button.js', _external=True) }}"></script>
204
+ <script src="{{ url_for('static', filename='js/chat_reload_button.js', _external=True) }}"></script>
205
+ <script src="{{ url_for('static', filename='js/chat_main.js', _external=True) }}"></script>
206
+ <script src="{{ url_for('static', filename='js/chat_logout_button.js', _external=True) }}"></script>
207
+ <script src="{{ url_for('static', filename='js/chat_main.js', _external=True) }}"></script>
200
208
 
201
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
209
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
202
210
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
203
211
  <script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
204
212
 
@@ -71,7 +71,7 @@
71
71
  {% block scripts %}
72
72
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
73
73
 
74
- <script src="{{ url_for('static', filename='js/chat_onboarding.js', _external=True) }}"></script>
74
+ <script src="{{ url_for('static', filename='js/chat_onboarding_button.js', _external=True) }}"></script>
75
75
  <script>
76
76
  (function() {
77
77
  const cardsData = {{ onboarding_cards | tojson }};
@@ -37,8 +37,8 @@ class ChangePasswordView(MethodView):
37
37
  email = self.serializer.loads(token, salt='password-reset', max_age=3600)
38
38
  except SignatureExpired as e:
39
39
  return render_template('forgot_password.html',
40
- branding=branding_data,
41
- alert_message="El enlace de cambio de contraseña ha expirado. Por favor, solicita uno nuevo.")
40
+ branding=branding_data,
41
+ alert_message="El enlace de cambio de contraseña ha expirado. Por favor, solicita uno nuevo.")
42
42
 
43
43
  return render_template('change_password.html',
44
44
  company_short_name=company_short_name,
@@ -15,22 +15,16 @@ class ExternalLoginView(BaseLoginView):
15
15
  Authenticates and then delegates the path decision (fast/slow) to the base class.
16
16
  """
17
17
  def post(self, company_short_name: str):
18
- data = request.get_json()
19
- if not data or 'user_identifier' not in data:
20
- return jsonify({"error": "Falta user_identifier"}), 400
18
+ # Authenticate the API call.
19
+ auth_result = self.auth_service.verify()
20
+ if not auth_result.get("success"):
21
+ return jsonify(auth_result), auth_result.get("status_code")
21
22
 
22
23
  company = self.profile_service.get_company_by_short_name(company_short_name)
23
24
  if not company:
24
25
  return jsonify({"error": "Empresa no encontrada"}), 404
25
26
 
26
- user_identifier = data.get('user_identifier')
27
- if not user_identifier:
28
- return jsonify({"error": "missing user_identifier"}), 404
29
-
30
- # 1. Authenticate the API call.
31
- auth_response = self.auth_service.verify()
32
- if not auth_response.get("success"):
33
- return jsonify(auth_response), 401
27
+ user_identifier = auth_result.get('user_identifier')
34
28
 
35
29
  # 2. Create the external user session.
36
30
  self.profile_service.create_external_user_profile_context(company, user_identifier)
@@ -15,24 +15,27 @@ import base64
15
15
  class FileStoreApiView(MethodView):
16
16
  @inject
17
17
  def __init__(self,
18
- iauthentication: AuthService,
18
+ auth_service: AuthService,
19
19
  doc_service: LoadDocumentsService,
20
20
  profile_repo: ProfileRepo,):
21
- self.iauthentication = iauthentication
21
+ self.auth_service = auth_service
22
22
  self.doc_service = doc_service
23
23
  self.profile_repo = profile_repo
24
24
 
25
25
  def post(self):
26
26
  try:
27
- req_data = request.get_json()
27
+ # 1. Authenticate the API request.
28
+ auth_result = self.auth_service.verify()
29
+ if not auth_result.get("success"):
30
+ return jsonify(auth_result), auth_result.get("status_code")
28
31
 
32
+ req_data = request.get_json()
29
33
  required_fields = ['company', 'filename', 'content']
30
34
  for field in required_fields:
31
35
  if field not in req_data:
32
36
  return jsonify({"error": f"El campo {field} es requerido"}), 400
33
37
 
34
38
  company_short_name = req_data.get('company', '')
35
- requested_name = req_data.get('username', 'external_user')
36
39
  filename = req_data.get('filename', False)
37
40
  base64_content = req_data.get('content', '')
38
41
  metadata = req_data.get('metadata', {})
@@ -42,11 +45,6 @@ class FileStoreApiView(MethodView):
42
45
  if not company:
43
46
  return jsonify({"error": f"La empresa {company_short_name} no existe"}), 400
44
47
 
45
- # get access credentials
46
- iaut = self.iauthentication.verify()
47
- if not iaut.get("success"):
48
- return jsonify(iaut), 401
49
-
50
48
  # get the file content from base64
51
49
  content = base64.b64decode(base64_content)
52
50
 
@@ -6,7 +6,7 @@
6
6
  from flask import request, jsonify
7
7
  from flask.views import MethodView
8
8
  from iatoolkit.services.history_service import HistoryService
9
- from iatoolkit.services.profile_service import ProfileService
9
+ from iatoolkit.services.auth_service import AuthService
10
10
  from injector import inject
11
11
  import logging
12
12
 
@@ -19,18 +19,18 @@ class HistoryApiView(MethodView):
19
19
 
20
20
  @inject
21
21
  def __init__(self,
22
- profile_service: ProfileService,
22
+ auth_service: AuthService,
23
23
  history_service: HistoryService):
24
- self.profile_service = profile_service
24
+ self.auth_service = auth_service
25
25
  self.history_service = history_service
26
26
 
27
27
  def post(self, company_short_name: str):
28
- # 1. Get the authenticated user's info from the unified session.
29
- session_info = self.profile_service.get_current_session_info()
30
- user_identifier = session_info.get("user_identifier")
28
+ # 1. Get the authenticated user's
29
+ auth_result = self.auth_service.verify()
30
+ if not auth_result.get("success"):
31
+ return jsonify(auth_result), auth_result.get("status_code")
31
32
 
32
- if not user_identifier:
33
- return jsonify({'error_message': 'Usuario no autenticado o sesión inválida'}), 401
33
+ user_identifier = auth_result.get('user_identifier')
34
34
 
35
35
  try:
36
36
  # 2. Call the history service with the unified identifier.
@@ -31,11 +31,9 @@ class InitContextApiView(MethodView):
31
31
  # 1. Authenticate the request. This handles both session and API Key.
32
32
  auth_result = self.auth_service.verify()
33
33
  if not auth_result.get("success"):
34
- return jsonify({"error": auth_result.get("error_message")}), auth_result.get("status_code", 401)
34
+ return jsonify(auth_result), auth_result.get("status_code")
35
35
 
36
36
  user_identifier = auth_result.get('user_identifier')
37
- if not user_identifier:
38
- return jsonify({"error": "Could not identify user from session or payload"}), 400
39
37
 
40
38
  try:
41
39
  # 2. Execute the forced rebuild sequence using the unified identifier.
@@ -54,7 +52,7 @@ class InitContextApiView(MethodView):
54
52
  )
55
53
 
56
54
  # 3. Respond with JSON, as this is an API endpoint.
57
- return jsonify({'status': 'OK', 'message': 'El context se ha recargado con éxito.'}), 200
55
+ return jsonify({'status': 'OK', 'message': 'El contexto se ha recargado con éxito.'}), 200
58
56
 
59
57
  except Exception as e:
60
58
  logging.exception(f"Error durante la recarga de contexto {user_identifier}: {e}")
@@ -21,12 +21,10 @@ class LLMQueryApiView(MethodView):
21
21
  # 1. Authenticate the API request.
22
22
  auth_result = self.auth_service.verify()
23
23
  if not auth_result.get("success"):
24
- return jsonify({"error": auth_result.get("error_message")}), auth_result.get("status_code", 401)
24
+ return jsonify(auth_result), auth_result.get("status_code")
25
25
 
26
26
  # 2. Get the user identifier from the payload.
27
27
  user_identifier = auth_result.get('user_identifier')
28
- if not user_identifier:
29
- return jsonify({"error": "Payload must include 'user_identifier'"}), 400
30
28
 
31
29
  data = request.get_json()
32
30
  if not data:
@@ -0,0 +1,45 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from flask.views import MethodView
7
+ from flask import redirect, url_for, jsonify
8
+ from injector import inject
9
+ from iatoolkit.services.auth_service import AuthService
10
+ from iatoolkit.services.profile_service import ProfileService
11
+ from iatoolkit.common.session_manager import SessionManager
12
+
13
+
14
+ class LogoutApiView(MethodView):
15
+ @inject
16
+ def __init__(self,
17
+ profile_service: ProfileService,
18
+ auth_service: AuthService):
19
+ self.profile_service = profile_service
20
+ self.auth_service = auth_service
21
+
22
+ def get(self, company_short_name: str = None):
23
+ # 1. Get the authenticated user's
24
+ auth_result = self.auth_service.verify(anonymous=True)
25
+ if not auth_result.get("success"):
26
+ return jsonify(auth_result), auth_result.get("status_code", 401)
27
+
28
+ company = self.profile_service.get_company_by_short_name(company_short_name)
29
+ if not company:
30
+ return jsonify({"error": "Empresa no encontrada"}), 404
31
+
32
+ # get URL for redirection
33
+ url_for_redirect = company.parameters.get('external_urls', {}).get('logout_url')
34
+ if not url_for_redirect:
35
+ url_for_redirect = url_for('index', company_short_name=company_short_name)
36
+
37
+ # clear de session cookie
38
+ SessionManager.clear()
39
+
40
+ return {
41
+ 'status': 'success',
42
+ 'url': url_for_redirect,
43
+ }
44
+
45
+
@@ -14,16 +14,16 @@ import logging
14
14
  class PromptApiView(MethodView):
15
15
  @inject
16
16
  def __init__(self,
17
- iauthentication: AuthService,
17
+ auth_service: AuthService,
18
18
  prompt_service: PromptService ):
19
- self.iauthentication = iauthentication
19
+ self.auth_service = auth_service
20
20
  self.prompt_service = prompt_service
21
21
 
22
22
  def get(self, company_short_name):
23
23
  # get access credentials
24
- iaut = self.iauthentication.verify()
25
- if not iaut.get("success"):
26
- return jsonify(iaut), 401
24
+ auth_result = self.auth_service.verify(anonymous=True)
25
+ if not auth_result.get("success"):
26
+ return jsonify(auth_result), auth_result.get('status_code')
27
27
 
28
28
  try:
29
29
  response = self.prompt_service.get_user_prompts(company_short_name)
@@ -7,54 +7,28 @@ from flask.views import MethodView
7
7
  from flask import request, jsonify
8
8
  from iatoolkit.services.tasks_service import TaskService
9
9
  from iatoolkit.repositories.profile_repo import ProfileRepo
10
+ from iatoolkit.services.auth_service import AuthService
10
11
  from injector import inject
11
12
  from datetime import datetime
12
13
  import logging
13
14
  from typing import Optional
14
15
 
15
16
 
16
- class TaskView(MethodView):
17
+ class TaskApiView(MethodView):
17
18
  @inject
18
- def __init__(self, task_service: TaskService, profile_repo: ProfileRepo):
19
+ def __init__(self,
20
+ auth_service: AuthService,
21
+ task_service: TaskService,
22
+ profile_repo: ProfileRepo):
23
+ self.auth_service = auth_service
19
24
  self.task_service = task_service
20
25
  self.profile_repo = profile_repo
21
26
 
22
- def _authenticate_requesting_company_via_api_key(self) -> tuple[
23
- Optional[int], Optional[str], Optional[tuple[dict, int]]]:
24
- """
25
- Autentica a la compañía usando su API Key.
26
- Retorna (company_id, company_short_name, None) en éxito.
27
- Retorna (None, None, (error_json, status_code)) en fallo.
28
- """
29
- api_key_header = request.headers.get('Authorization')
30
- if not api_key_header or not api_key_header.startswith('Bearer '):
31
- return None, None, ({"error": "API Key faltante o mal formada en el header Authorization"}, 401)
32
-
33
- api_key_value = api_key_header.split('Bearer ')[1]
34
- try:
35
- api_key_entry = self.profile_repo.get_active_api_key_entry(api_key_value)
36
- if not api_key_entry:
37
- return None, None, ({"error": "API Key inválida o inactiva"}, 401)
38
-
39
- # api_key_entry.company ya está cargado por joinedload en get_active_api_key_entry
40
- if not api_key_entry.company: # Sanity check
41
- logging.error(
42
- f"ChatTokenRequest: API Key {api_key_value[:5]}... no tiene compañía asociada a pesar de ser válida.")
43
- return None, None, ({"error": "Error interno del servidor al verificar API Key"}, 500)
44
-
45
- return api_key_entry.company_id, api_key_entry.company.short_name, None
46
-
47
- except Exception as e:
48
- logging.exception(f"ChatTokenRequest: Error interno durante validación de API Key: {e}")
49
- return None, None, ({"error": "Error interno del servidor al validar API Key"}, 500)
50
-
51
-
52
27
  def post(self):
53
28
  try:
54
- # only requests with valid api-key are allowed
55
- auth_company_id, auth_company_short_name, error = self._authenticate_requesting_company_via_api_key()
56
- if error:
57
- return jsonify(error[0]), error[1]
29
+ auth_result = self.auth_service.verify(anonymous=True)
30
+ if not auth_result.get("success"):
31
+ return jsonify(auth_result), auth_result.get("status_code")
58
32
 
59
33
  req_data = request.get_json()
60
34
  files = request.files.getlist('files')
@@ -0,0 +1,55 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from flask.views import MethodView
7
+ from flask import request, jsonify
8
+ from iatoolkit.services.tasks_service import TaskService
9
+ from iatoolkit.repositories.profile_repo import ProfileRepo
10
+ from iatoolkit.services.auth_service import AuthService
11
+ from injector import inject
12
+ import logging
13
+ from typing import Optional
14
+
15
+
16
+ class TaskReviewApiView(MethodView):
17
+ @inject
18
+ def __init__(self,
19
+ auth_service: AuthService,
20
+ task_service: TaskService,
21
+ profile_repo: ProfileRepo):
22
+ self.auth_service = auth_service
23
+ self.task_service = task_service
24
+ self.profile_repo = profile_repo
25
+
26
+ def post(self, task_id: int):
27
+ auth_result = self.auth_service.verify(anonymous=True)
28
+ if not auth_result.get("success"):
29
+ return jsonify(auth_result), auth_result.get("status_code")
30
+
31
+ try:
32
+ req_data = request.get_json()
33
+ required_fields = ['review_user', 'approved']
34
+ for field in required_fields:
35
+ if field not in req_data:
36
+ return jsonify({"error": f"El campo {field} es requerido"}), 400
37
+
38
+ review_user = req_data.get('review_user', '')
39
+ approved = req_data.get('approved', False)
40
+ comment = req_data.get('comment', '')
41
+
42
+ new_task = self.task_service.review_task(
43
+ task_id=task_id,
44
+ review_user=review_user,
45
+ approved=approved,
46
+ comment=comment)
47
+
48
+ return jsonify({
49
+ "task_id": new_task.id,
50
+ "status": new_task.status.name
51
+ }), 200
52
+
53
+ except Exception as e:
54
+ logging.exception("Error al revisar la tarea: %s", str(e))
55
+ return jsonify({"error": str(e)}), 500
@@ -14,20 +14,18 @@ import logging
14
14
  class UserFeedbackApiView(MethodView):
15
15
  @inject
16
16
  def __init__(self,
17
- iauthentication: AuthService,
17
+ auth_service: AuthService,
18
18
  user_feedback_service: UserFeedbackService ):
19
- self.iauthentication = iauthentication
19
+ self.auth_service = auth_service
20
20
  self.user_feedback_service = user_feedback_service
21
21
 
22
22
  def post(self, company_short_name):
23
23
  # get access credentials
24
- iaut = self.iauthentication.verify()
25
- if not iaut.get("success"):
26
- return jsonify(iaut), 401
24
+ auth_result = self.auth_service.verify()
25
+ if not auth_result.get("success"):
26
+ return jsonify(auth_result), auth_result.get("status_code")
27
27
 
28
- user_identifier = iaut.get('user_identifier')
29
- if not user_identifier:
30
- return jsonify({"error": "Could not identify user from session or payload"}), 400
28
+ user_identifier = auth_result.get('user_identifier')
31
29
 
32
30
  data = request.get_json()
33
31
  if not data:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iatoolkit
3
- Version: 0.59.2
3
+ Version: 0.60.1
4
4
  Summary: IAToolkit
5
5
  Author: Fernando Libedinsky
6
6
  License-Expression: MIT
@@ -2,10 +2,10 @@ iatoolkit/__init__.py,sha256=4PWjMJjktixtrxF6BY405qyA50Sv967kEP2x-oil6qk,1120
2
2
  iatoolkit/base_company.py,sha256=vU4ki-wB3PWIn3_Bvehfh0TfBH_XNC614tRBKNmEd84,4718
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=N922fz-tHZYZSpu5_PupW8p4x1yi66wnRq7UUgpPCfs,17583
5
+ iatoolkit/iatoolkit.py,sha256=9mzOWrmZq4TpJlb7cVsi0N4DewcZlUDqtiuggMFaSbE,17570
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=en9LNxQ3oj7wPUA19okmauGVqdA1yIB_YjPo_-CV-UQ,6195
8
+ iatoolkit/common/routes.py,sha256=L8UTwbE-cNSYl5s09mMckO2J9on0N1F6p8ZXMFbjrDM,5738
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
@@ -34,7 +34,7 @@ iatoolkit/repositories/profile_repo.py,sha256=21am3GP7XCG0nq6i3pArQ7mfGsrRn8rdcW
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=1_YfoEflzV7UboOO9xIduwXuwb4I8XHC8mZLgYyDEyw,7171
37
+ iatoolkit/services/auth_service.py,sha256=PPaa_zB0L2i574O6VQzKvTmzHbmzlWK3vvMQl2TYjOA,7494
38
38
  iatoolkit/services/benchmark_service.py,sha256=CdbFYyS3FHFhNzWQEa9ZNjUlmON10DT1nKNbZQ1EUi8,5880
39
39
  iatoolkit/services/branding_service.py,sha256=gXj9Lj6EIFNIHT6wAHia5lr4_2a2sD-ExMbewno5YD8,7505
40
40
  iatoolkit/services/dispatcher_service.py,sha256=Qdn2x4cozpgpKg2448sUxkhO6tuplzb8xPWUxdTTFBE,12772
@@ -46,7 +46,7 @@ iatoolkit/services/jwt_service.py,sha256=W2kQVNQheQSLkNLS7RZ4jd3hmySBPLbHAS5hvBr
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=TuWV5wVR7gUYylOPKcUR0KdxHAglRv0cXxO18kCgng0,20300
49
+ iatoolkit/services/profile_service.py,sha256=cOjGJvrH9f3DRoXrhs4vZEqG7dzwrl_GnOcwxyaSX18,20301
50
50
  iatoolkit/services/prompt_manager_service.py,sha256=U-XmSpkeXvv1KRN4dytdMxSYBMRSB7y-UHcb18mk0nA,8342
51
51
  iatoolkit/services/query_service.py,sha256=gtMEAQ7IRVrFAMq4h_Pc_lHlMlXFI6heLSuBYHenkfs,17502
52
52
  iatoolkit/services/search_service.py,sha256=i1xGWu7ORKIIDH0aAQBkF86dVVbLQ0Yrooz5TiZ6aGo,1823
@@ -55,13 +55,14 @@ iatoolkit/services/tasks_service.py,sha256=itREO5rDnUIgsqtyCOBKDtH30QL5v1egs4qPT
55
55
  iatoolkit/services/user_feedback_service.py,sha256=Bb6PVWcxzoQ3awev_k4MI0BQhkMh6iLPGC8_CK02t5g,4986
56
56
  iatoolkit/services/user_session_context_service.py,sha256=vYF_vWM37tPB_ZyPBJ6f6WTJVjT2j-4L8JfZbqbI93k,6775
57
57
  iatoolkit/static/images/fernando.jpeg,sha256=W68TYMuo5hZVpbP-evwH6Nu4xWFv2bc8pJzSKDoLTeQ,100612
58
- iatoolkit/static/js/chat_context_reload.js,sha256=DCOGEf7-t_YLw9wuqkP-kFpiFHt3UyaesFfePp9Cs_k,2512
59
- iatoolkit/static/js/chat_feedback.js,sha256=j2LieutME4RrtMmHbyi3ptfRLefcxechule2NLxzCm8,4284
58
+ iatoolkit/static/js/chat_feedback_button.js,sha256=j3BIyxcgyDuDDBn3vxxe0DV4evQabj3xb6XF28OWwCs,4220
60
59
  iatoolkit/static/js/chat_filepond.js,sha256=mzXafm7a506EpM37KATTK3zvAswO1E0KSUY1vKbwuRc,3163
61
- iatoolkit/static/js/chat_history.js,sha256=4h6ldU7cDvgkW84fMKB8JReoxCX0NKSQAir_4CzAF9I,4382
62
- iatoolkit/static/js/chat_main.js,sha256=o1ZNkeaBgGG9d07e5BxTL9OI0aJ26z1bvca1lGoQ-Uo,18535
63
- iatoolkit/static/js/chat_onboarding.js,sha256=b6ofiFcPhuCaPmSFIvDQZqcMUVvbI7LpIsjZOZJUSAU,3185
64
- iatoolkit/static/styles/chat_iatoolkit.css,sha256=aA-PZ2TGl_k82JSVVBC2-CJT0NiZAuLOGoiaJhdeVUU,11416
60
+ iatoolkit/static/js/chat_history_button.js,sha256=4h6ldU7cDvgkW84fMKB8JReoxCX0NKSQAir_4CzAF9I,4382
61
+ iatoolkit/static/js/chat_logout_button.js,sha256=Of9H6IbAboSBmeqRaurEVW6_dL752L0UeDcDLNBD5Z0,1335
62
+ iatoolkit/static/js/chat_main.js,sha256=GPPM3euqT_cGcrmsb7DGr0Aa6BpeQRTpuf2zW2VEvMM,18118
63
+ iatoolkit/static/js/chat_onboarding_button.js,sha256=vjvEloJ9PA9D7jOGca0QjS3J_7bU97R71L7kSyY3wOI,3153
64
+ iatoolkit/static/js/chat_reload_button.js,sha256=f8f_qRnZTNr_DwbcmafTHIuBLmiCoypYAKGQc472AOs,2393
65
+ iatoolkit/static/styles/chat_iatoolkit.css,sha256=w9O_4slQuls3R5UH30oUrgA5BetA5BVctzSPpelMN4Y,12934
65
66
  iatoolkit/static/styles/chat_info.css,sha256=17DbgoNYE21VYWfb5L9-QLCpD2R1idK4imKRLwXtJLY,1058
66
67
  iatoolkit/static/styles/chat_modal.css,sha256=mdfjrJtmUn3O9rKwIGjJc-oSNmJGnzUY1aAJqEfPh38,4301
67
68
  iatoolkit/static/styles/landing_page.css,sha256=5MHlXkmgZVv9uHE7rZTGYzZeynya3ONY4hp7e2uPXwk,4898
@@ -76,36 +77,36 @@ iatoolkit/templates/_navbar.html,sha256=o1PvZE5ueLmVpGUAmsjtu-vS_WPROTlJc2sTXl6A
76
77
  iatoolkit/templates/about.html,sha256=ciC08grUVz5qLzdzDDqDX31xirg5PrJIRYabWpV9oA8,294
77
78
  iatoolkit/templates/base.html,sha256=hHfBqZJsPcZGlb4BxAbHvpJFcSjckLIAVTYTmoyXrz0,2323
78
79
  iatoolkit/templates/change_password.html,sha256=G5a3hYLTpz_5Q_eZ4LNcYSqLeW-CuT4NCHD8bkhAd9k,3573
79
- iatoolkit/templates/chat.html,sha256=NdKj1cDqQWSLrjMjfne32IZcwBdg_DV9apm8BMeTQDI,11952
80
+ iatoolkit/templates/chat.html,sha256=kq9juQ1LhXxG-uERr8FdK80KtwI43DABmSS3z66qNfQ,12182
80
81
  iatoolkit/templates/chat_modals.html,sha256=NwwgPoOmVbjy4aO2eHsy1TUMXRiOfTOC5Jx_F2ehhcs,6947
81
82
  iatoolkit/templates/error.html,sha256=c3dxieMygsvdjQMiQu_sn6kqqag9zFtVu-z5FunX6so,580
82
83
  iatoolkit/templates/forgot_password.html,sha256=NRZqbNHJXSLNArF_KLbzuem-U57v07awS0ikI_DJbfM,2360
83
84
  iatoolkit/templates/header.html,sha256=179agI7rnYwP_rvJNXIiVde5E8Ec5649_XKq6eew2Hk,1263
84
85
  iatoolkit/templates/index.html,sha256=ZeDBmZGZ_GlxQ8p_NA2Fdf4syTU5kXrJBO6q2P77wKY,7907
85
86
  iatoolkit/templates/login_simulation.html,sha256=1svwCBPrJ3Gy6bD9WMuz25NBSdFgZt4j8_sC7HE6MFU,1270
86
- iatoolkit/templates/onboarding_shell.html,sha256=r1ivSR2ci8GrDSm1uaD-cf78rfO1bKT5gXa-v5aHLAk,4659
87
+ iatoolkit/templates/onboarding_shell.html,sha256=exSGckoPeE-ID9ym3B4TLh5hULpR7N1X6LeuSNmiUL0,4666
87
88
  iatoolkit/templates/signup.html,sha256=9ArDvcNQgHFR2dwxy-37AXzGUOeOsT7Nz5u0y6fAB3U,4385
88
89
  iatoolkit/templates/test.html,sha256=rwNtxC83tbCl5COZFXYvmRBxxmgFJtPNuVBd_nq9KWY,133
89
90
  iatoolkit/views/__init__.py,sha256=5JqK9sZ6jBuK83zDQokUhxQ0wuJJJ9DXB8pYCLkX7X4,102
90
91
  iatoolkit/views/base_login_view.py,sha256=qoMMrAezCJvzjcfNzIbd2vwHhksALIQfNK8f_9E8m0o,3241
91
- iatoolkit/views/change_password_view.py,sha256=tM0woZyKdhY4XYjS_YXg2sKq3RYkXGfcq_eVAKrNvNM,4498
92
- iatoolkit/views/chat_token_request_view.py,sha256=wf32_A2Sq8NHYWshCwL10Tovd1znLoD0jQjzutR3sVE,4408
93
- iatoolkit/views/external_login_view.py,sha256=U_1k8AB1u6hB7twsdtQr6yS0xcihDxksstg-IiMZB30,3087
94
- iatoolkit/views/file_store_api_view.py,sha256=Uz9f6sey3_F5K8zuyQz6SwYRKAalCjD1ekf-Mkl_Kfo,2326
92
+ iatoolkit/views/change_password_view.py,sha256=DRfyvD-zgX-zidoIrvYnWg6V0LOeeugCZTxgk7wNntM,4503
93
+ iatoolkit/views/external_login_view.py,sha256=d4gUQbxFsThGbbAUdtFn7AMgPJjeb7_8KFFoH3vspVA,2853
94
+ iatoolkit/views/file_store_api_view.py,sha256=UvtZWOG-rLQMLfs8igOIYoQ-tkkEg5baMjqCJdKxaRQ,2300
95
95
  iatoolkit/views/forgot_password_view.py,sha256=-qKJeeOBqJFdvDUk7rCNg1E1cDQnJQkozPpb0T0FgwA,3159
96
- iatoolkit/views/history_api_view.py,sha256=x-tZhB8UzqrD2n-WDIfmHK9iVhGZ9f0yncsGs9mxwt0,1953
96
+ iatoolkit/views/history_api_view.py,sha256=0YChbss0ae05KHzni2p3d4bGS2_yKAbjALk1OBeQk50,1867
97
97
  iatoolkit/views/index_view.py,sha256=P5aVdEWxsYOZGbzcXd6WFE733qZ7YXIoeqriUMAM6V8,1527
98
- iatoolkit/views/init_context_api_view.py,sha256=NbJdGD4BDTkmDhGO5jkrGihdkpdIvuqUsl_Qg7MDZvA,2878
99
- iatoolkit/views/llmquery_api_view.py,sha256=ihRtZygDLAamz8DIJ0UEN73MG_SHzjgvQIznXvCPyxg,1816
98
+ iatoolkit/views/init_context_api_view.py,sha256=YTjpT4xdtm1knUhelDj-VbV4EK6o_qGAgwwDhFmIOlg,2716
99
+ iatoolkit/views/llmquery_api_view.py,sha256=v_KxR6w-TrCVR2fMFHZCz3_v4o42CXb1Yvd-R1hSJHg,1662
100
100
  iatoolkit/views/login_simulation_view.py,sha256=0Qt-puRnltI2HZxlfdyJmOf26-hQp3xjknGV_jkwV7E,3484
101
101
  iatoolkit/views/login_view.py,sha256=ESJLKHGUKQw71STHK2AoxugQUJmxnPYlI13n7ZawLLg,5663
102
- iatoolkit/views/prompt_api_view.py,sha256=MP0r-MiswwKcbNc_5KY7aVbHkrR218I8XCiCX1D0yTA,1244
102
+ iatoolkit/views/logout_api_view.py,sha256=V1HkfUfTQr_IrHFqjmeA0-qN2qugk3bmtLRHtZd7VE8,1506
103
+ iatoolkit/views/prompt_api_view.py,sha256=S_4-qAD5knh8Esae1AczEYGdXy_AuU7LMOmnUPej4jQ,1294
103
104
  iatoolkit/views/signup_view.py,sha256=BCjhM2lMiDPwYrlW_eEwPl-ZLupblbFfsonWtq0E4vU,3922
104
- iatoolkit/views/tasks_review_view.py,sha256=keLsLCyOTTlcoIapnB_lbuSvLwrPVZVpBiFC_7ChbLg,3388
105
- iatoolkit/views/tasks_view.py,sha256=a3anTXrJTTvbQuc6PSpOzidLKQFL4hWa7PI2Cppcz8w,4110
106
- iatoolkit/views/user_feedback_api_view.py,sha256=PNtXs3G26COw3QeZ9WlX14lYizualGMZ4wVfv77IUoU,2075
105
+ iatoolkit/views/tasks_api_view.py,sha256=wGnuwuuL83ByQ1Yre6ytRVztA0OGQjGrwMjB1_G830U,2630
106
+ iatoolkit/views/tasks_review_api_view.py,sha256=wsCpzqyRyUdCXWAhyGlBe3eNZZ6A1DQG7TblN_GZNfM,1894
107
+ iatoolkit/views/user_feedback_api_view.py,sha256=-Ngex8SPf0mPvPNqwE_GUcRErLpOL49yJ43o5Y4Qhqo,1992
107
108
  iatoolkit/views/verify_user_view.py,sha256=7XLSaxvs8LjBr3cYOUDa9B8DqW_50IGlq0IvmOQcD0Y,2340
108
- iatoolkit-0.59.2.dist-info/METADATA,sha256=XUACxsD5jRzps23Y-avo13tfLAXlF23w7v2jrq-Pr4Q,9301
109
- iatoolkit-0.59.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
110
- iatoolkit-0.59.2.dist-info/top_level.txt,sha256=V_w4QvDx0b1RXiy8zTCrD1Bp7AZkFe3_O0-9fMiwogg,10
111
- iatoolkit-0.59.2.dist-info/RECORD,,
109
+ iatoolkit-0.60.1.dist-info/METADATA,sha256=kl3WrHpU-XIlw1_m_QDFNlNsvmKDpnEjW4j5zHbC9Tw,9301
110
+ iatoolkit-0.60.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
111
+ iatoolkit-0.60.1.dist-info/top_level.txt,sha256=V_w4QvDx0b1RXiy8zTCrD1Bp7AZkFe3_O0-9fMiwogg,10
112
+ iatoolkit-0.60.1.dist-info/RECORD,,
@@ -1,98 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- from flask import request, jsonify, current_app
7
- from flask.views import MethodView
8
- from injector import inject
9
- import logging
10
- from iatoolkit.repositories.profile_repo import ProfileRepo
11
- from iatoolkit.services.jwt_service import JWTService
12
- from typing import Optional
13
-
14
-
15
- # Necesitaremos JWT_EXPIRATION_SECONDS_CHAT de la configuración de la app
16
- # Se podría inyectar o acceder globalmente.
17
-
18
- class ChatTokenRequestView(MethodView):
19
- @inject
20
- def __init__(self, profile_repo: ProfileRepo, jwt_service: JWTService):
21
- self.profile_repo = profile_repo
22
- self.jwt_service = jwt_service
23
-
24
- def _authenticate_requesting_company_via_api_key(self) -> tuple[
25
- Optional[int], Optional[str], Optional[tuple[dict, int]]]:
26
- """
27
- Autentica a la compañía que solicita el token JWT usando su API Key.
28
- Retorna (company_id, company_short_name, None) en éxito.
29
- Retorna (None, None, (error_json, status_code)) en fallo.
30
- """
31
- api_key_header = request.headers.get('Authorization')
32
- if not api_key_header or not api_key_header.startswith('Bearer '):
33
- return None, None, ({"error": "API Key faltante o mal formada en el header Authorization"}, 401)
34
-
35
- api_key_value = api_key_header.split('Bearer ')[1]
36
- try:
37
- api_key_entry = self.profile_repo.get_active_api_key_entry(api_key_value)
38
- if not api_key_entry:
39
- return None, None, ({"error": "API Key inválida o inactiva"}, 401)
40
-
41
- # api_key_entry.company ya está cargado por joinedload en get_active_api_key_entry
42
- if not api_key_entry.company: # Sanity check
43
- logging.error(
44
- f"ChatTokenRequest: API Key {api_key_value[:5]}... no tiene compañía asociada a pesar de ser válida.")
45
- return None, None, ({"error": "Error interno del servidor al verificar API Key"}, 500)
46
-
47
- return api_key_entry.company_id, api_key_entry.company.short_name, None
48
-
49
- except Exception as e:
50
- logging.exception(f"ChatTokenRequest: Error interno durante validación de API Key: {e}")
51
- return None, None, ({"error": "Error interno del servidor al validar API Key"}, 500)
52
-
53
- def post(self):
54
- """
55
- Genera un JWT para una sesión de chat.
56
- Autenticado por API Key de la empresa.
57
- Requiere JSON body:
58
- {"company_short_name": "target_company_name",
59
- "external_user_id": "user_abc"
60
- }
61
- """
62
- # only requests with valid api-key are allowed
63
- auth_company_id, auth_company_short_name, error = self._authenticate_requesting_company_via_api_key()
64
- if error:
65
- return jsonify(error[0]), error[1]
66
-
67
- # get the json fields from the request body
68
- data = request.get_json()
69
- if not data:
70
- return jsonify({"error": "Cuerpo de la solicitud JSON faltante"}), 400
71
-
72
- target_company_short_name = data.get('company_short_name')
73
- external_user_id = data.get('external_user_id')
74
-
75
- if not target_company_short_name or not external_user_id:
76
- return jsonify(
77
- {"error": "Faltan 'company_short_name' o 'external_user_id' en el cuerpo de la solicitud"}), 401
78
-
79
- # Verificar que la API Key usada pertenezca a la empresa para la cual se solicita el token
80
- if auth_company_short_name != target_company_short_name:
81
- return jsonify({
82
- "error": f"API Key no autorizada para generar tokens para la compañía '{target_company_short_name}'"}), 403
83
-
84
- jwt_expiration_seconds = current_app.config.get('JWT_EXPIRATION_SECONDS_CHAT', 3600)
85
-
86
- # Aquí, auth_company_id es el ID de la compañía para la que se generará el token.
87
- # auth_company_short_name es su nombre corto.
88
- token = self.jwt_service.generate_chat_jwt(
89
- company_id=auth_company_id,
90
- company_short_name=auth_company_short_name, # Usamos el short_name autenticado
91
- external_user_id=external_user_id,
92
- expires_delta_seconds=jwt_expiration_seconds
93
- )
94
-
95
- if token:
96
- return jsonify({"chat_jwt": token}), 200
97
- else:
98
- return jsonify({"error": "No se pudo generar el token de chat"}), 500
@@ -1,83 +0,0 @@
1
- # Copyright (c) 2024 Fernando Libedinsky
2
- # Product: IAToolkit
3
- #
4
- # IAToolkit is open source software.
5
-
6
- from flask.views import MethodView
7
- from flask import request, jsonify
8
- from iatoolkit.services.tasks_service import TaskService
9
- from iatoolkit.repositories.profile_repo import ProfileRepo
10
- from injector import inject
11
- import logging
12
- from typing import Optional
13
-
14
-
15
- class TaskReviewView(MethodView):
16
- @inject
17
- def __init__(self, task_service: TaskService, profile_repo: ProfileRepo):
18
- self.task_service = task_service
19
- self.profile_repo = profile_repo
20
-
21
-
22
- def _authenticate_requesting_company_via_api_key(self) -> tuple[
23
- Optional[int], Optional[str], Optional[tuple[dict, int]]]:
24
- """
25
- Autentica a la compañía usando su API Key.
26
- Retorna (company_id, company_short_name, None) en éxito.
27
- Retorna (None, None, (error_json, status_code)) en fallo.
28
- """
29
- api_key_header = request.headers.get('Authorization')
30
- if not api_key_header or not api_key_header.startswith('Bearer '):
31
- return None, None, ({"error": "API Key faltante o mal formada en el header Authorization"}, 401)
32
-
33
- api_key_value = api_key_header.split('Bearer ')[1]
34
- try:
35
- api_key_entry = self.profile_repo.get_active_api_key_entry(api_key_value)
36
- if not api_key_entry:
37
- return None, None, ({"error": "API Key inválida o inactiva"}, 401)
38
-
39
- # api_key_entry.company ya está cargado por joinedload en get_active_api_key_entry
40
- if not api_key_entry.company: # Sanity check
41
- logging.error(
42
- f"ChatTokenRequest: API Key {api_key_value[:5]}... no tiene compañía asociada a pesar de ser válida.")
43
- return None, None, ({"error": "Error interno del servidor al verificar API Key"}, 500)
44
-
45
- return api_key_entry.company_id, api_key_entry.company.short_name, None
46
-
47
- except Exception as e:
48
- logging.exception(f"ChatTokenRequest: Error interno durante validación de API Key: {e}")
49
- return None, None, ({"error": "Error interno del servidor al validar API Key"}, 500)
50
-
51
-
52
- def post(self, task_id: int):
53
- try:
54
- # only requests with valid api-key are allowed
55
- auth_company_id, auth_company_short_name, error = self._authenticate_requesting_company_via_api_key()
56
- if error:
57
- return jsonify(error[0]), error[1]
58
-
59
- req_data = request.get_json()
60
-
61
- required_fields = ['review_user', 'approved']
62
- for field in required_fields:
63
- if field not in req_data:
64
- return jsonify({"error": f"El campo {field} es requerido"}), 400
65
-
66
- review_user = req_data.get('review_user', '')
67
- approved = req_data.get('approved', False)
68
- comment = req_data.get('comment', '')
69
-
70
- new_task = self.task_service.review_task(
71
- task_id=task_id,
72
- review_user=review_user,
73
- approved=approved,
74
- comment=comment)
75
-
76
- return jsonify({
77
- "task_id": new_task.id,
78
- "status": new_task.status.name
79
- }), 200
80
-
81
- except Exception as e:
82
- logging.exception("Error al revisar la tarea: %s", str(e))
83
- return jsonify({"error": str(e)}), 500