iatoolkit 0.59.1__py3-none-any.whl → 0.67.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.

Files changed (93) hide show
  1. iatoolkit/__init__.py +2 -0
  2. iatoolkit/base_company.py +2 -19
  3. iatoolkit/common/routes.py +28 -25
  4. iatoolkit/common/session_manager.py +2 -0
  5. iatoolkit/common/util.py +17 -6
  6. iatoolkit/company_registry.py +1 -2
  7. iatoolkit/iatoolkit.py +54 -20
  8. iatoolkit/locales/en.yaml +167 -0
  9. iatoolkit/locales/es.yaml +163 -0
  10. iatoolkit/repositories/database_manager.py +3 -3
  11. iatoolkit/repositories/document_repo.py +1 -1
  12. iatoolkit/repositories/models.py +3 -4
  13. iatoolkit/repositories/profile_repo.py +0 -4
  14. iatoolkit/services/auth_service.py +44 -32
  15. iatoolkit/services/branding_service.py +35 -27
  16. iatoolkit/services/configuration_service.py +140 -0
  17. iatoolkit/services/dispatcher_service.py +20 -18
  18. iatoolkit/services/document_service.py +5 -2
  19. iatoolkit/services/excel_service.py +15 -11
  20. iatoolkit/services/file_processor_service.py +4 -12
  21. iatoolkit/services/history_service.py +8 -7
  22. iatoolkit/services/i18n_service.py +104 -0
  23. iatoolkit/services/jwt_service.py +7 -9
  24. iatoolkit/services/language_service.py +79 -0
  25. iatoolkit/services/load_documents_service.py +4 -4
  26. iatoolkit/services/mail_service.py +9 -4
  27. iatoolkit/services/onboarding_service.py +10 -4
  28. iatoolkit/services/profile_service.py +59 -38
  29. iatoolkit/services/prompt_manager_service.py +20 -16
  30. iatoolkit/services/query_service.py +15 -14
  31. iatoolkit/services/sql_service.py +6 -2
  32. iatoolkit/services/user_feedback_service.py +70 -29
  33. iatoolkit/static/js/chat_feedback_button.js +80 -0
  34. iatoolkit/static/js/chat_help_content.js +124 -0
  35. iatoolkit/static/js/chat_history_button.js +110 -0
  36. iatoolkit/static/js/chat_logout_button.js +36 -0
  37. iatoolkit/static/js/chat_main.js +32 -184
  38. iatoolkit/static/js/{chat_onboarding.js → chat_onboarding_button.js} +0 -1
  39. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  40. iatoolkit/static/js/chat_reload_button.js +35 -0
  41. iatoolkit/static/styles/chat_iatoolkit.css +251 -205
  42. iatoolkit/static/styles/chat_modal.css +63 -95
  43. iatoolkit/static/styles/chat_public.css +107 -0
  44. iatoolkit/static/styles/landing_page.css +121 -167
  45. iatoolkit/templates/_company_header.html +20 -0
  46. iatoolkit/templates/_login_widget.html +10 -10
  47. iatoolkit/templates/base.html +36 -19
  48. iatoolkit/templates/change_password.html +24 -22
  49. iatoolkit/templates/chat.html +121 -93
  50. iatoolkit/templates/chat_modals.html +113 -74
  51. iatoolkit/templates/error.html +44 -8
  52. iatoolkit/templates/forgot_password.html +17 -15
  53. iatoolkit/templates/index.html +66 -81
  54. iatoolkit/templates/login_simulation.html +16 -5
  55. iatoolkit/templates/onboarding_shell.html +1 -2
  56. iatoolkit/templates/signup.html +22 -20
  57. iatoolkit/views/base_login_view.py +12 -1
  58. iatoolkit/views/change_password_view.py +50 -33
  59. iatoolkit/views/external_login_view.py +5 -11
  60. iatoolkit/views/file_store_api_view.py +7 -9
  61. iatoolkit/views/forgot_password_view.py +21 -19
  62. iatoolkit/views/help_content_api_view.py +54 -0
  63. iatoolkit/views/history_api_view.py +16 -12
  64. iatoolkit/views/home_view.py +61 -0
  65. iatoolkit/views/index_view.py +5 -34
  66. iatoolkit/views/init_context_api_view.py +16 -13
  67. iatoolkit/views/llmquery_api_view.py +38 -28
  68. iatoolkit/views/login_simulation_view.py +14 -2
  69. iatoolkit/views/login_view.py +48 -33
  70. iatoolkit/views/logout_api_view.py +49 -0
  71. iatoolkit/views/profile_api_view.py +46 -0
  72. iatoolkit/views/prompt_api_view.py +8 -8
  73. iatoolkit/views/signup_view.py +27 -25
  74. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  75. iatoolkit/views/tasks_review_api_view.py +55 -0
  76. iatoolkit/views/user_feedback_api_view.py +21 -32
  77. iatoolkit/views/verify_user_view.py +33 -26
  78. {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/METADATA +40 -22
  79. iatoolkit-0.67.0.dist-info/RECORD +120 -0
  80. iatoolkit-0.67.0.dist-info/licenses/LICENSE +21 -0
  81. iatoolkit/static/js/chat_context_reload.js +0 -54
  82. iatoolkit/static/js/chat_feedback.js +0 -115
  83. iatoolkit/static/js/chat_history.js +0 -127
  84. iatoolkit/static/styles/chat_info.css +0 -53
  85. iatoolkit/templates/_branding_styles.html +0 -53
  86. iatoolkit/templates/_navbar.html +0 -9
  87. iatoolkit/templates/header.html +0 -31
  88. iatoolkit/templates/test.html +0 -9
  89. iatoolkit/views/chat_token_request_view.py +0 -98
  90. iatoolkit/views/tasks_review_view.py +0 -83
  91. iatoolkit-0.59.1.dist-info/RECORD +0 -111
  92. {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/WHEEL +0 -0
  93. {iatoolkit-0.59.1.dist-info → iatoolkit-0.67.0.dist-info}/top_level.txt +0 -0
@@ -1,54 +0,0 @@
1
- document.addEventListener('DOMContentLoaded', function() {
2
- const reloadButton = document.getElementById('force-reload-button');
3
- if (!reloadButton) return;
4
-
5
- const originalIconClass = 'bi bi-arrow-clockwise';
6
- const spinnerIconClass = 'spinner-border spinner-border-sm';
7
-
8
- // Configuración de Toastr para que aparezca abajo a la derecha
9
- toastr.options = { "positionClass": "toast-bottom-right", "preventDuplicates": true };
10
-
11
- reloadButton.addEventListener('click', async function(event) {
12
- event.preventDefault();
13
-
14
- if (reloadButton.disabled) return; // Prevenir doble clic
15
-
16
- // 1. Deshabilitar y mostrar spinner
17
- reloadButton.disabled = true;
18
- const icon = reloadButton.querySelector('i');
19
- icon.className = spinnerIconClass;
20
- toastr.info('Iniciando recarga de contexto en segundo plano...');
21
-
22
- try {
23
- // 2. Definir los parámetros para callToolkit
24
- const apiPath = '/api/init-context';
25
- const payload = { 'user_identifier': window.user_identifier };
26
-
27
- // 3. Hacer la llamada usando callToolkit
28
- const data = await callToolkit(apiPath, payload, 'POST');
29
-
30
- // 4. Procesar la respuesta
31
- // callToolkit devuelve null si hubo un error que ya mostró en el chat.
32
- if (data) {
33
- if (data.status === 'OK') {
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
37
- toastr.error(data.error_message || 'Ocurrió un error desconocido durante la recarga.');
38
- }
39
- } else {
40
- // Si data es null, callToolkit ya manejó el error (mostrando un mensaje en el chat).
41
- // Añadimos un toast para notificar al usuario que algo falló.
42
- toastr.error('Falló la recarga del contexto. Revisa el chat para más detalles.');
43
- }
44
- } catch (error) {
45
- // Este bloque se ejecutará para errores no controlados por callToolkit (como AbortError)
46
- console.error('Error durante la recarga del contexto:', error);
47
- toastr.error(error.message || 'Error de red al intentar recargar.');
48
- } finally {
49
- // 5. Restaurar el botón en cualquier caso
50
- reloadButton.disabled = false;
51
- icon.className = originalIconClass;
52
- }
53
- });
54
- });
@@ -1,115 +0,0 @@
1
- $(document).ready(function () {
2
-
3
- // Evento para enviar el feedback
4
- $('#submit-feedback').on('click', async function() {
5
- const feedbackText = $('#feedback-text').val().trim();
6
- const submitButton = $(this);
7
-
8
- // --- LÓGICA DE COMPATIBILIDAD BS3 / BS5 ---
9
- // Detecta si Bootstrap 5 está presente.
10
- const isBootstrap5 = (typeof bootstrap !== 'undefined');
11
-
12
- // Define el HTML del botón de cierre según la versión.
13
- const closeButtonHtml = isBootstrap5 ?
14
- '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>' : // Versión BS5
15
- '<button type="button" class="close" data-dismiss="alert"><span>&times;</span></button>'; // Versión BS3/BS4
16
- // --- FIN DE LA LÓGICA DE COMPATIBILIDAD ---
17
-
18
- if (!feedbackText) {
19
- const alertHtml = `
20
- <div class="alert alert-warning alert-dismissible fade show" role="alert">
21
- <strong>¡Atención!</strong> Por favor, escribe tu comentario antes de enviar.
22
- ${closeButtonHtml}
23
- </div>`;
24
- $('.modal-body .alert').remove();
25
- $('.modal-body').prepend(alertHtml);
26
- return;
27
- }
28
-
29
- const activeStars = $('.star.active').length;
30
- if (activeStars === 0) {
31
- const alertHtml = `
32
- <div class="alert alert-warning alert-dismissible fade show" role="alert">
33
- <strong>¡Atención!</strong> Por favor, califica al asistente con las estrellas.
34
- ${closeButtonHtml}
35
- </div>`;
36
- $('.modal-body .alert').remove();
37
- $('.modal-body').prepend(alertHtml);
38
- return;
39
- }
40
-
41
- submitButton.prop('disabled', true);
42
- submitButton.html('<i class="bi bi-send me-1 icon-spaced"></i>Enviando...');
43
-
44
- const response = await sendFeedback(feedbackText);
45
-
46
- $('#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
- }
53
- });
54
-
55
- // Evento para abrir el modal de feedback
56
- $('#send-feedback-button').on('click', function() {
57
- $('#submit-feedback').prop('disabled', false);
58
- $('#submit-feedback').html('<i class="bi bi-send me-1 icon-spaced"></i>Enviar');
59
- $('.star').removeClass('active hover-active'); // Resetea estrellas
60
- $('#feedback-text').val(''); // Limpia texto
61
- $('.modal-body .alert').remove(); // Quita alertas previas
62
- $('#feedbackModal').modal('show');
63
- });
64
-
65
- // Evento que se dispara DESPUÉS de que el modal se ha ocultado
66
- $('#feedbackModal').on('hidden.bs.modal', function () {
67
- $('#feedback-text').val('');
68
- $('.modal-body .alert').remove();
69
- $('.star').removeClass('active');
70
- });
71
-
72
- // Función para el sistema de estrellas
73
- window.gfg = function(rating) {
74
- $('.star').removeClass('active');
75
- $('.star').each(function(index) {
76
- if (index < rating) {
77
- $(this).addClass('active');
78
- }
79
- });
80
- };
81
-
82
- $('.star').hover(
83
- function() {
84
- const rating = $(this).data('rating');
85
- $('.star').removeClass('hover-active');
86
- $('.star').each(function(index) {
87
- if ($(this).data('rating') <= rating) {
88
- $(this).addClass('hover-active');
89
- }
90
- });
91
- },
92
- function() {
93
- $('.star').removeClass('hover-active');
94
- }
95
- );
96
- });
97
-
98
- const sendFeedback = async function(message) {
99
- const activeStars = $('.star.active').length;
100
- const data = {
101
- "user_identifier": window.user_identifier,
102
- "message": message,
103
- "rating": activeStars,
104
- "space": "spaces/AAQAupQldd4", // Este valor podría necesitar ser dinámico
105
- "type": "MESSAGE_TRIGGER"
106
- };
107
- try {
108
- // Asumiendo que callLLMAPI está definido globalmente en otro archivo (ej. chat_main.js)
109
- const responseData = await callToolkit('/feedback', data, "POST");
110
- return responseData;
111
- } catch (error) {
112
- console.error("Error al enviar feedback:", error);
113
- return null;
114
- }
115
- }
@@ -1,127 +0,0 @@
1
- $(document).ready(function () {
2
- // Evento para abrir el modal de historial
3
- $('#history-button').on('click', function() {
4
- loadHistory();
5
- $('#historyModal').modal('show');
6
- });
7
-
8
- // Evento delegado para el icono de copiar.
9
- // Se adjunta UNA SOLA VEZ al cuerpo de la tabla y funciona para todas las filas
10
- // que se añadan dinámicamente.
11
- $('#history-table-body').on('click', '.copy-query-icon', function() {
12
- const queryText = $(this).data('query');
13
-
14
- // Copiar el texto al textarea del chat
15
- if (queryText) {
16
- $('#question').val(queryText);
17
- autoResizeTextarea($('#question')[0]);
18
- $('#send-button').removeClass('disabled');
19
-
20
- // Cerrar el modal
21
- $('#historyModal').modal('hide');
22
-
23
- // Hacer focus en el textarea
24
- $('#question').focus();
25
- }
26
- });
27
-
28
- // Variables globales para el historial
29
- let historyData = [];
30
-
31
- // Función para cargar el historial
32
- async function loadHistory() {
33
- const historyLoading = $('#history-loading');
34
- const historyError = $('#history-error');
35
- const historyContent = $('#history-content');
36
-
37
- // Mostrar loading
38
- historyLoading.show();
39
- historyError.hide();
40
- historyContent.hide();
41
-
42
- try {
43
- const responseData = await callToolkit("/api/history", {}, "POST");
44
-
45
- if (responseData && responseData.history) {
46
- // Guardar datos globalmente
47
- historyData = responseData.history;
48
-
49
- // Mostrar todos los datos
50
- displayAllHistory();
51
-
52
- // Mostrar contenido
53
- historyContent.show();
54
- } else {
55
- throw new Error('La respuesta del servidor no contenía el formato esperado.');
56
- }
57
- } catch (error) {
58
- console.error("Error al cargar historial:", error);
59
-
60
- const friendlyErrorMessage = "No hemos podido cargar tu historial en este momento. Por favor, cierra esta ventana y vuelve a intentarlo en unos instantes.";
61
- const errorHtml = `
62
- <div class="text-center p-4">
63
- <i class="bi bi-exclamation-triangle text-danger" style="font-size: 2.5rem; opacity: 0.8;"></i>
64
- <h5 class="mt-3 mb-2">Ocurrió un Problema</h5>
65
- <p class="text-muted">${friendlyErrorMessage}</p>
66
- </div>
67
- `;
68
- historyError.html(errorHtml).show();
69
- } finally {
70
- historyLoading.hide();
71
- }
72
- }
73
-
74
- // Función para mostrar todo el historial
75
- function displayAllHistory() {
76
- const historyTableBody = $('#history-table-body');
77
-
78
- // Limpiar tabla
79
- historyTableBody.empty();
80
-
81
- // Filtrar solo consultas que son strings simples
82
- const filteredHistory = historyData.filter(item => {
83
- try {
84
- JSON.parse(item.query);
85
- return false;
86
- } catch (e) {
87
- return true;
88
- }
89
- });
90
-
91
- // Poblar la tabla
92
- filteredHistory.forEach((item, index) => {
93
- const icon = $('<i>').addClass('bi bi-pencil-fill');
94
-
95
- const link = $('<a>')
96
- .attr('href', 'javascript:void(0);')
97
- .addClass('copy-query-icon')
98
- .attr('title', 'Copiar consulta al chat')
99
- .data('query', item.query)
100
- .append(icon);
101
-
102
- const row = $('<tr>').append(
103
- $('<td>').text(index + 1),
104
- $('<td>').addClass('date-cell').text(formatDate(item.created_at)),
105
- $('<td>').text(item.query),
106
- $('<td>').addClass('text-center').append(link)
107
- );
108
-
109
- historyTableBody.append(row);
110
- });
111
- }
112
-
113
- // Función para formatear fecha
114
- function formatDate(dateString) {
115
- const date = new Date(dateString);
116
-
117
- const padTo2Digits = (num) => num.toString().padStart(2, '0');
118
-
119
- const day = padTo2Digits(date.getDate());
120
- const month = padTo2Digits(date.getMonth() + 1);
121
- const year = date.getFullYear();
122
- const hours = padTo2Digits(date.getHours());
123
- const minutes = padTo2Digits(date.getMinutes());
124
-
125
- return `${day}-${month}-${year} ${hours}:${minutes}`;
126
- }
127
- });
@@ -1,53 +0,0 @@
1
- /*
2
- * Estilo para el encabezado de la tabla (thead).
3
- * Se usa un fondo gris claro, texto en mayúsculas y
4
- * un espaciado de letras para un look más profesional.
5
- */
6
-
7
- .table{
8
- background-color: white;
9
- }
10
-
11
- .title-info-page{
12
- color: #495057;
13
- font-size: 24px;
14
- font-weight: bold;
15
- margin-bottom: 10px;
16
- }
17
-
18
- .table thead th {
19
- background-color: #f8f9fa;
20
- color: #495057;
21
- text-transform: uppercase;
22
- letter-spacing: 0.5px;
23
- border-bottom-width: 2px;
24
- }
25
-
26
- /*
27
- * Columna 1: Ancho mínimo, sin salto de línea y en negrita.
28
- */
29
- .table th:nth-child(1),
30
- .table td:nth-child(1) {
31
- width: 1%;
32
- white-space: nowrap;
33
- font-weight: bold;
34
- }
35
-
36
- /*
37
- * Columna 2: Ancho mínimo y sin salto de línea.
38
- */
39
- .table th:nth-child(2),
40
- .table td:nth-child(2) {
41
- width: 1%;
42
- white-space: nowrap;
43
- }
44
-
45
- /*
46
- * Columna 3 (Descripción): Limita su ancho máximo
47
- * y ajusta el texto largo para que no se desborde.
48
- */
49
- .table th:nth-child(3),
50
- .table td:nth-child(3) {
51
- max-width: 450px;
52
- word-wrap: break-word;
53
- }
@@ -1,53 +0,0 @@
1
- <!-- templates/_branding_styles.html -->
2
- <style>
3
- {{ branding.css_variables | safe }}
4
-
5
- /* Clases de botón reutilizables basadas en el branding */
6
- .btn-branded-primary {
7
- background-color: var(--brand-primary-color);
8
- color: var(--brand-text-on-primary);
9
- border-color: var(--brand-primary-color);
10
- transition: all 0.2s ease-in-out;
11
- }
12
- .btn-branded-primary:hover {
13
- color: var(--brand-text-on-primary);
14
- box-shadow: inset 0 0 0 200px rgba(0, 0, 0, 0.15);
15
- }
16
- .btn-branded-secondary {
17
- background-color: var(--brand-secondary-color);
18
- color: var(--brand-text-on-secondary);
19
- border-color: var(--brand-secondary-color);
20
- transition: all 0.2s ease-in-out;
21
- }
22
- .btn-branded-secondary:hover {
23
- color: var(--brand-text-on-secondary);
24
- box-shadow: inset 0 0 0 200px rgba(0, 0, 0, 0.15);
25
- }
26
- .form-title {
27
- color: var(--brand-primary-color);
28
- }
29
-
30
- /* --- ESTILOS CORREGIDOS Y MÁS ESPECÍFICOS PARA SWEETALERT2 --- */
31
-
32
- /* Botón de Confirmar (OK) */
33
- .swal2-confirm.custom-confirm-button {
34
- background-color: var(--brand-primary-color) !important;
35
- color: var(--brand-text-on-primary) !important;
36
- border: 1px solid var(--brand-primary-color) !important;
37
- box-shadow: none !important;
38
- }
39
- .swal2-confirm.custom-confirm-button:hover {
40
- box-shadow: inset 0 0 0 200px rgba(0, 0, 0, 0.15) !important;
41
- }
42
-
43
- /* Botón de Cancelar */
44
- .swal2-cancel.custom-cancel-button {
45
- background-color: var(--brand-secondary-color) !important;
46
- color: var(--brand-text-on-secondary) !important;
47
- border: 1px solid var(--brand-secondary-color) !important;
48
- box-shadow: none !important;
49
- }
50
- .swal2-cancel.custom-cancel-button:hover {
51
- box-shadow: inset 0 0 0 200px rgba(0, 0, 0, 0.15) !important;
52
- }
53
- </style>
@@ -1,9 +0,0 @@
1
- <!-- templates/_navbar.html -->
2
- <nav class="navbar landing-navbar">
3
- <div class="container-fluid">
4
- <!-- Convertido en un enlace a la landing page de la compañía actual -->
5
- <a class="navbar-brand text-decoration-none" href="{{ url_for('index', company_short_name=company_short_name) }}">
6
- IAToolkit
7
- </a>
8
- </div>
9
- </nav>
@@ -1,31 +0,0 @@
1
- {% if not external_login %}
2
- <header class="modern-header">
3
- {% if not is_mobile or not user %}
4
- <div class="logo-section">
5
- <a href="{{ url_for('chat', company_short_name=company_short_name) }}" title="Ir a Inicio">
6
- {% if company_short_name %}
7
- <img src="{{ url_for('static', filename='images/' + company.logo_file) }}" alt="Logo de la empresa">
8
- {% else %}
9
- <img src="{{ url_for('static', filename='images/logo_iatoolkit.png') }}" alt="Logo predeterminado">
10
- {% endif %}
11
- </a>
12
- </div>
13
- {% endif %}
14
- {% if user and user_company == company_short_name %}
15
- <div class="d-flex ms-auto align-items-center">
16
- <!-- Nombres del usuario y empresa -->
17
- <div class="d-flex flex-column {% if not is_mobile %} align-items-end {% endif %} me-3">
18
- <span class="text-white fw-semibold">
19
- {{ user.email }}
20
- </span>
21
- </div>
22
-
23
- <!-- Icono de cerrar sesión -->
24
- <a href="{{ url_for('logout', company_short_name=company_short_name) }}"
25
- | class="text-white fs-4 ms-3" title="Cerrar sesión">
26
- <i class="bi bi-box-arrow-right"></i>
27
- </a>
28
- </div>
29
- {% endif %}
30
- </header>
31
- {% endif %}
@@ -1,9 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Prueba</title>
5
- </head>
6
- <body>
7
- <h1>Prueba Básica con Flask-Injector</h1>
8
- </body>
9
- </html>
@@ -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