iatoolkit 0.11.0__py3-none-any.whl → 0.71.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. iatoolkit/__init__.py +2 -6
  2. iatoolkit/base_company.py +9 -29
  3. iatoolkit/cli_commands.py +1 -1
  4. iatoolkit/common/routes.py +96 -52
  5. iatoolkit/common/session_manager.py +2 -1
  6. iatoolkit/common/util.py +17 -27
  7. iatoolkit/company_registry.py +1 -2
  8. iatoolkit/iatoolkit.py +97 -53
  9. iatoolkit/infra/llm_client.py +15 -20
  10. iatoolkit/infra/llm_proxy.py +38 -10
  11. iatoolkit/infra/openai_adapter.py +1 -1
  12. iatoolkit/infra/redis_session_manager.py +48 -2
  13. iatoolkit/locales/en.yaml +167 -0
  14. iatoolkit/locales/es.yaml +163 -0
  15. iatoolkit/repositories/database_manager.py +23 -3
  16. iatoolkit/repositories/document_repo.py +1 -1
  17. iatoolkit/repositories/models.py +35 -10
  18. iatoolkit/repositories/profile_repo.py +3 -2
  19. iatoolkit/repositories/vs_repo.py +26 -20
  20. iatoolkit/services/auth_service.py +193 -0
  21. iatoolkit/services/branding_service.py +70 -25
  22. iatoolkit/services/company_context_service.py +155 -0
  23. iatoolkit/services/configuration_service.py +133 -0
  24. iatoolkit/services/dispatcher_service.py +80 -105
  25. iatoolkit/services/document_service.py +5 -2
  26. iatoolkit/services/embedding_service.py +146 -0
  27. iatoolkit/services/excel_service.py +30 -26
  28. iatoolkit/services/file_processor_service.py +4 -12
  29. iatoolkit/services/history_service.py +7 -16
  30. iatoolkit/services/i18n_service.py +104 -0
  31. iatoolkit/services/jwt_service.py +18 -29
  32. iatoolkit/services/language_service.py +83 -0
  33. iatoolkit/services/load_documents_service.py +100 -113
  34. iatoolkit/services/mail_service.py +9 -4
  35. iatoolkit/services/profile_service.py +152 -76
  36. iatoolkit/services/prompt_manager_service.py +20 -16
  37. iatoolkit/services/query_service.py +208 -96
  38. iatoolkit/services/search_service.py +11 -4
  39. iatoolkit/services/sql_service.py +57 -25
  40. iatoolkit/services/tasks_service.py +1 -1
  41. iatoolkit/services/user_feedback_service.py +72 -34
  42. iatoolkit/services/user_session_context_service.py +112 -54
  43. iatoolkit/static/images/fernando.jpeg +0 -0
  44. iatoolkit/static/js/chat_feedback_button.js +80 -0
  45. iatoolkit/static/js/chat_help_content.js +124 -0
  46. iatoolkit/static/js/chat_history_button.js +110 -0
  47. iatoolkit/static/js/chat_logout_button.js +36 -0
  48. iatoolkit/static/js/chat_main.js +135 -222
  49. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  50. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  51. iatoolkit/static/js/chat_reload_button.js +35 -0
  52. iatoolkit/static/styles/chat_iatoolkit.css +289 -210
  53. iatoolkit/static/styles/chat_modal.css +63 -77
  54. iatoolkit/static/styles/chat_public.css +107 -0
  55. iatoolkit/static/styles/landing_page.css +182 -0
  56. iatoolkit/static/styles/onboarding.css +176 -0
  57. iatoolkit/system_prompts/query_main.prompt +5 -22
  58. iatoolkit/templates/_company_header.html +20 -0
  59. iatoolkit/templates/_login_widget.html +42 -0
  60. iatoolkit/templates/base.html +40 -20
  61. iatoolkit/templates/change_password.html +57 -36
  62. iatoolkit/templates/chat.html +180 -86
  63. iatoolkit/templates/chat_modals.html +138 -68
  64. iatoolkit/templates/error.html +44 -8
  65. iatoolkit/templates/forgot_password.html +40 -23
  66. iatoolkit/templates/index.html +145 -0
  67. iatoolkit/templates/login_simulation.html +45 -0
  68. iatoolkit/templates/onboarding_shell.html +107 -0
  69. iatoolkit/templates/signup.html +63 -65
  70. iatoolkit/views/base_login_view.py +91 -0
  71. iatoolkit/views/change_password_view.py +56 -31
  72. iatoolkit/views/embedding_api_view.py +65 -0
  73. iatoolkit/views/external_login_view.py +61 -28
  74. iatoolkit/views/{file_store_view.py → file_store_api_view.py} +10 -3
  75. iatoolkit/views/forgot_password_view.py +27 -21
  76. iatoolkit/views/help_content_api_view.py +54 -0
  77. iatoolkit/views/history_api_view.py +56 -0
  78. iatoolkit/views/home_view.py +50 -23
  79. iatoolkit/views/index_view.py +14 -0
  80. iatoolkit/views/init_context_api_view.py +74 -0
  81. iatoolkit/views/llmquery_api_view.py +58 -0
  82. iatoolkit/views/login_simulation_view.py +93 -0
  83. iatoolkit/views/login_view.py +130 -37
  84. iatoolkit/views/logout_api_view.py +49 -0
  85. iatoolkit/views/profile_api_view.py +46 -0
  86. iatoolkit/views/{prompt_view.py → prompt_api_view.py} +10 -10
  87. iatoolkit/views/signup_view.py +41 -36
  88. iatoolkit/views/{tasks_view.py → tasks_api_view.py} +10 -36
  89. iatoolkit/views/tasks_review_api_view.py +55 -0
  90. iatoolkit/views/user_feedback_api_view.py +60 -0
  91. iatoolkit/views/verify_user_view.py +34 -29
  92. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/METADATA +41 -23
  93. iatoolkit-0.71.2.dist-info/RECORD +122 -0
  94. iatoolkit-0.71.2.dist-info/licenses/LICENSE +21 -0
  95. iatoolkit/common/auth.py +0 -200
  96. iatoolkit/static/images/arrow_up.png +0 -0
  97. iatoolkit/static/images/diagrama_iatoolkit.jpg +0 -0
  98. iatoolkit/static/images/logo_clinica.png +0 -0
  99. iatoolkit/static/images/logo_iatoolkit.png +0 -0
  100. iatoolkit/static/images/logo_maxxa.png +0 -0
  101. iatoolkit/static/images/logo_notaria.png +0 -0
  102. iatoolkit/static/images/logo_tarjeta.png +0 -0
  103. iatoolkit/static/images/logo_umayor.png +0 -0
  104. iatoolkit/static/images/upload.png +0 -0
  105. iatoolkit/static/js/chat_feedback.js +0 -115
  106. iatoolkit/static/js/chat_history.js +0 -117
  107. iatoolkit/static/styles/chat_info.css +0 -53
  108. iatoolkit/templates/header.html +0 -31
  109. iatoolkit/templates/home.html +0 -199
  110. iatoolkit/templates/login.html +0 -43
  111. iatoolkit/templates/test.html +0 -9
  112. iatoolkit/views/chat_token_request_view.py +0 -98
  113. iatoolkit/views/chat_view.py +0 -58
  114. iatoolkit/views/download_file_view.py +0 -58
  115. iatoolkit/views/external_chat_login_view.py +0 -95
  116. iatoolkit/views/history_view.py +0 -57
  117. iatoolkit/views/llmquery_view.py +0 -65
  118. iatoolkit/views/tasks_review_view.py +0 -83
  119. iatoolkit/views/user_feedback_view.py +0 -74
  120. iatoolkit-0.11.0.dist-info/RECORD +0 -110
  121. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/WHEEL +0 -0
  122. {iatoolkit-0.11.0.dist-info → iatoolkit-0.71.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,107 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Iniciando {{ branding.name }} IA...{% endblock %}
4
+
5
+ {% block styles %}
6
+ {% endblock %}
7
+
8
+ {% block content %}
9
+
10
+ <link rel="stylesheet" href="{{ url_for('static', filename='styles/llm_output.css', _external=True) }}?v=6">
11
+ <link rel="stylesheet" href="{{ url_for('static', filename='styles/onboarding.css', _external=True) }}?v=6">
12
+
13
+ <style>
14
+ {% if branding and branding.css_variables %}
15
+ {{ branding.css_variables|safe }}
16
+ {% endif %}
17
+
18
+ {# 2. Ahora definimos las reglas que usan esas variables #}
19
+ .onboarding-shell-root { position: relative; height: 100vh; }
20
+ .onboarding-shell-root #loader-wrapper { position: absolute; inset: 0; background: #f4f7f6; z-index: 1000; display:flex; align-items:center; justify-content:center; padding:20px; box-sizing:border-box; transition: opacity .5s; }
21
+ .onboarding-shell-root .ob-stack { width:100%; max-width:520px; display:flex; flex-direction:column; gap:16px; }
22
+ .onboarding-shell-root .ob-loading-band { display:flex; align-items:center; justify-content:center; gap:12px; padding:12px 14px; background:#fff; border-radius:12px; box-shadow:0 4px 20px rgba(0,0,0,.08); }
23
+ .onboarding-shell-root .spinner { width:28px; height:28px; border:4px solid rgba(0,0,0,.1); border-top-color: var(--brand-primary-color, #FF5100); border-radius:50%; animation: spin 1s linear infinite; }
24
+ @keyframes spin { to { transform: rotate(360deg); } }
25
+ .onboarding-shell-root #content-container { width:100%; height:100%; }
26
+ .onboarding-shell-root #content-container iframe { width:100%; height:100%; border:none; }
27
+
28
+ /* Forzar colores de marca (sube especificidad y evita overrides) */
29
+ .onboarding-shell-root .ob-brand-header { color: var(--brand-secondary-color, #06326B) !important; }
30
+ .onboarding-shell-root .ob-brand-header .brand-name { color: var(--brand-primary-color, #FF5100) !important; }
31
+ .onboarding-shell-root .ob-icon { color: var(--brand-primary-color, #FF5100) !important; }
32
+ .onboarding-shell-root .ob-btn { background-color: var(--brand-secondary-color, #06326B) !important; color: var(--brand-text-on-secondary, #FFFFFF) !important; border:none !important; }
33
+ .onboarding-shell-root .ob-dots > div.active { background-color: var(--brand-primary-color, #FF5100) !important; }
34
+ </style>
35
+
36
+ <div class="onboarding-shell-root ob-root">
37
+ <div id="loader-wrapper">
38
+ <div class="ob-stack">
39
+ <h1 id="ob-brand-header" class="ob-brand-header">
40
+ <span class="brand-name text-brand-primary">{{ branding.name | default('IAToolkit') }}</span>
41
+ <span class="brand-rest"> IA</span>
42
+ </h1>
43
+
44
+ <div id="card-container" class="ob-card">
45
+ <div id="card-icon" class="ob-icon"><i class="fas fa-lightbulb"></i></div>
46
+ <h3 id="card-title" class="ob-title">Título de la Tarjeta</h3>
47
+ <p id="card-text" class="ob-text">Descripción de la tarjeta de capacitación.</p>
48
+ <p id="card-example" class="ob-example"></p>
49
+
50
+ <div class="ob-nav">
51
+ <button id="prev-card" class="ob-btn btn-branded-primary" aria-label="Anterior">
52
+ <i class="fas fa-chevron-left"></i>
53
+ </button>
54
+ <div id="progress-dots" class="ob-dots"></div>
55
+ <button id="next-card" class="ob-btn btn-branded-primary" aria-label="Siguiente">
56
+ <i class="fas fa-chevron-right"></i>
57
+ </button>
58
+ </div>
59
+ </div>
60
+
61
+ <div id="loading-status" class="ob-loading-band">
62
+ <div class="spinner"></div>
63
+ <p>Inicializando el contexto de {{ branding.name }} para la IA...</p>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <div id="content-container"></div>
69
+ </div>
70
+ {% endblock %}
71
+
72
+ {% block scripts %}
73
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
74
+
75
+ <script src="{{ url_for('static', filename='js/chat_onboarding_button.js', _external=True) }}"></script>
76
+ <script>
77
+ (function() {
78
+ const cardsData = {{ onboarding_cards | tojson }};
79
+ const iframeSrc = "{{ iframe_src_url }}";
80
+
81
+ if (!window.initOnboarding) {
82
+ console.error('initOnboarding no está disponible. Verifica la carga de chat_onboarding.js');
83
+ return;
84
+ }
85
+
86
+ const onboarding = initOnboarding({
87
+ mode: 'shell',
88
+ cards: cardsData,
89
+ ui: {
90
+ icon: '#card-icon',
91
+ title: '#card-title',
92
+ text: '#card-text',
93
+ example: '#card-example',
94
+ dots: '#progress-dots',
95
+ prev: '#prev-card',
96
+ next: '#next-card',
97
+ loader: '#loader-wrapper',
98
+ container: '#content-container'
99
+ },
100
+ autoRotateMs: 5000,
101
+ shell: { iframeSrc: iframeSrc }
102
+ });
103
+
104
+ onboarding.start();
105
+ })();
106
+ </script>
107
+ {% endblock %}
@@ -1,78 +1,76 @@
1
1
  {% extends "base.html" %}
2
2
 
3
- {% block title %}Registro de Usuario{% endblock %}
3
+ {% block title %}{{ t('ui.signup.title') }} - {{ branding.name }}{% endblock %}
4
+
5
+ {% block styles %}
6
+ {# ¡Importante! Añadimos los estilos para el branding #}
7
+ <style>
8
+ {{ branding.css_variables | safe }}
9
+ </style>
10
+ <link rel="stylesheet" href="{{ url_for('static', filename='styles/chat_public.css') }}">
11
+ {% endblock %}
4
12
 
5
13
  {% block content %}
14
+ <div class="container mt-4">
6
15
 
16
+ {% include '_company_header.html' %}
7
17
 
8
- <div class="d-flex align-items-center p-3 border-bottom border-light-subtle">
9
- <div class="col-11 col-md-8 col-lg-5 p-3">
10
- <h4 class="text-muted fw-semibold mb-2 text-start">Crea tu cuenta</h4>
11
- <p class="text-muted mb-3 text-start" style="text-align: justify;">
12
- Regístrate para acceder a todas las funcionalidades de nuestra plataforma.
13
- </p>
14
- <form action="{{ url_for('signup', company_short_name=company_short_name) }}" method="post" >
15
- <div class="mb-3 ">
16
- <label for="email" class="form-label text-secondary">Correo Electrónico</label>
17
- <input type="email" autocomplete="off" id="email" name="email"
18
- class="form-control" required
19
- value="{{ form_data.email if form_data else '' }}">
20
- </div>
18
+ <section class="hero-section">
19
+ <div class="container">
20
+ <div class="row justify-content-center">
21
+ <!-- Se ha reducido el ancho de la columna a lg-6 y md-8 -->
22
+ <div class="col-lg-6 col-md-8">
23
+ <div class="branded-form-container">
24
+ <h4 class="branded-form-title">{{ t('ui.signup.title') }}</h4>
25
+ <form action="{{ url_for('signup', company_short_name=company_short_name) }}" method="post">
26
+ <div class="mb-3">
27
+ <label for="email" class="form-label text-secondary">{{ t('ui.signup.email_label') }}</label>
28
+ <input type="email" autocomplete="off" id="email" name="email"
29
+ class="form-control" required
30
+ value="{{ form_data.email if form_data else '' }}">
31
+ </div>
21
32
 
22
- {% if is_mobile %}
23
- <!-- En móvil, cada input ocupa una fila -->
24
- <div class="mb-3">
25
- <label for="first_name" class="form-label text-secondary">Nombre</label>
26
- <input type="text" id="first_name" name="first_name"
27
- class="form-control" required
28
- value="{{ form_data.first_name if form_data else '' }}">
29
- </div>
30
- <div class="mb-3">
31
- <label for="last_name" class="form-label text-secondary">Apellido</label>
32
- <input type="text" id="last_name" name="last_name"
33
- class="form-control" required
34
- value="{{ form_data.last_name if form_data else '' }}">
35
- </div>
36
- {% else %}
37
- <div class="row">
38
- <!-- En escritorio, los inputs están en la misma fila -->
39
- <div class="col-md-6 mb-3">
40
- <label for="first_name" class="form-label text-secondary">Nombre</label>
41
- <input type="text" id="first_name" name="first_name"
42
- class="form-control" required
43
- value="{{ form_data.first_name if form_data else '' }}">
44
- </div>
45
- <div class="col-md-6 mb-3">
46
- <label for="last_name" class="form-label text-secondary">Apellido</label>
47
- <input type="text" id="last_name" name="last_name"
48
- class="form-control" required
49
- value="{{ form_data.last_name if form_data else '' }}">
50
- </div>
51
- </div>
33
+ <div class="row">
34
+ <div class="col-md-6 mb-3">
35
+ <label for="first_name" class="form-label text-secondary">{{ t('ui.signup.first_name_label') }}</label>
36
+ <input type="text" id="first_name" name="first_name"
37
+ class="form-control" required
38
+ value="{{ form_data.first_name if form_data else '' }}">
39
+ </div>
40
+ <div class="col-md-6 mb-3">
41
+ <label for="last_name" class="form-label text-secondary">{{ t('ui.signup.last_name_label') }}</label>
42
+ <input type="text" id="last_name" name="last_name"
43
+ class="form-control" required
44
+ value="{{ form_data.last_name if form_data else '' }}">
45
+ </div>
46
+ </div>
52
47
 
53
- {% endif %}
48
+ <div class="mb-3">
49
+ <label for="password" class="form-label text-secondary">{{ t('ui.signup.password_label') }}</label>
50
+ <input type="password" id="password" name="password" class="form-control" required>
51
+ <div class="d-flex align-items-start text-muted mt-2" style="font-size: 0.8rem;">
52
+ <i class="bi bi-info-circle me-2" style="font-size: 0.9rem; line-height: 1.4;"></i>
53
+ <span>{{ t('ui.change_password.password_instructions') }}</span>
54
+ </div>
55
+ </div>
54
56
 
55
- <div class="mb-3">
56
- <label for="password" class="form-label text-secondary">Contraseña</label>
57
- <input type="password" id="password" name="password" class="form-control" required>
58
- <small class="form-text text-muted">
59
- La contraseña debe contener al menos 8 caracteres, una letra mayúscula, una letra minúscula, un número y un carácter especial.
60
- </small>
57
+ <div class="mb-3">
58
+ <label for="confirm_password" class="form-label text-secondary">{{ t('ui.signup.confirm_password_label') }}</label>
59
+ <input type="password" id="confirm_password" name="confirm_password" class="form-control" required>
60
+ </div>
61
61
 
62
+ <!-- Botón actualizado con la clase de branding -->
63
+ <button type="submit" class="btn btn-branded-primary w-100 fw-bold py-2 mt-3">{{ t('ui.signup.signup_button') }}</button>
64
+ </form>
65
+ <!-- Nota de privacidad -->
66
+ <p class="text-muted small mb-0 text-center mt-4">
67
+ {{ t('ui.signup.disclaimer') }}
68
+ </p>
69
+ </div>
70
+ </div>
62
71
  </div>
63
- <div class="mb-3">
64
- <label for="confirm_password" class="form-label text-secondary">Confirmar Contraseña</label>
65
- <input type="password" id="confirm_password" name="confirm_password" class="form-control" required>
66
- </div>
67
- <button type="submit" class="btn btn-primary w-100">Registrarse</button>
68
- </form>
69
- <!-- Nota sobre privacidad -->
70
- <p class="text-muted small mb-0 text-start mt-3" style="text-align: justify;">
71
- 🔒 Valoramos tu privacidad. Tus datos serán utilizados exclusivamente para brindarte una mejor experiencia
72
- y no serán compartidos con terceros sin tu consentimiento. Para más información, consulta nuestra
73
- <a href="#" class="text-decoration-none fw-semibold">Política de Privacidad</a>.
74
- </p>
75
- </p>
76
- </div>
72
+ </div>
73
+ </section>
77
74
  </div>
75
+
78
76
  {% endblock %}
@@ -0,0 +1,91 @@
1
+ # iatoolkit/views/base_login_view.py
2
+ # Copyright (c) 2024 Fernando Libedinsky
3
+ # Product: IAToolkit
4
+ #
5
+ # IAToolkit is open source software.
6
+
7
+ from flask.views import MethodView
8
+ from flask import render_template, url_for
9
+ from injector import inject
10
+ from iatoolkit.services.profile_service import ProfileService
11
+ from iatoolkit.services.auth_service import AuthService
12
+ from iatoolkit.services.query_service import QueryService
13
+ from iatoolkit.services.branding_service import BrandingService
14
+ from iatoolkit.services.configuration_service import ConfigurationService
15
+ from iatoolkit.services.prompt_manager_service import PromptService
16
+ from iatoolkit.services.i18n_service import I18nService
17
+ from iatoolkit.common.util import Utility
18
+ from iatoolkit.services.jwt_service import JWTService
19
+ from iatoolkit.repositories.models import Company
20
+
21
+
22
+ class BaseLoginView(MethodView):
23
+ """
24
+ Base class for views that initiate a session and decide the context
25
+ loading path (fast or slow).
26
+ """
27
+ @inject
28
+ def __init__(self,
29
+ profile_service: ProfileService,
30
+ auth_service: AuthService,
31
+ jwt_service: JWTService,
32
+ branding_service: BrandingService,
33
+ prompt_service: PromptService,
34
+ config_service: ConfigurationService,
35
+ query_service: QueryService,
36
+ i18n_service: I18nService,
37
+ utility: Utility
38
+ ):
39
+ self.profile_service = profile_service
40
+ self.auth_service = auth_service
41
+ self.jwt_service = jwt_service
42
+ self.branding_service = branding_service
43
+ self.prompt_service = prompt_service
44
+ self.config_service = config_service
45
+ self.query_service = query_service
46
+ self.i18n_service = i18n_service
47
+ self.utility = utility
48
+
49
+
50
+ def _handle_login_path(self,
51
+ company_short_name: str,
52
+ user_identifier: str,
53
+ target_url: str,
54
+ redeem_token: str = None):
55
+ """
56
+ Centralized logic to decide between the fast path and the slow path.
57
+ """
58
+ # --- Get the company branding and onboarding_cards
59
+ branding_data = self.branding_service.get_company_branding(company_short_name)
60
+ onboarding_cards = self.config_service.get_configuration(company_short_name, 'onboarding_cards')
61
+
62
+ # this service decides is the context needs to be rebuilt or not
63
+ prep_result = self.query_service.prepare_context(
64
+ company_short_name=company_short_name, user_identifier=user_identifier
65
+ )
66
+
67
+ if prep_result.get('rebuild_needed'):
68
+ # --- SLOW PATH: Render the loading shell ---
69
+ return render_template(
70
+ "onboarding_shell.html",
71
+ iframe_src_url=target_url,
72
+ branding=branding_data,
73
+ onboarding_cards=onboarding_cards
74
+ )
75
+ else:
76
+ # --- FAST PATH: Render the chat page directly ---
77
+ prompts = self.prompt_service.get_user_prompts(company_short_name)
78
+
79
+ # Get the entire 'js_messages' block in the correct language.
80
+ js_translations = self.i18n_service.get_translation_block('js_messages')
81
+
82
+ return render_template(
83
+ "chat.html",
84
+ company_short_name=company_short_name,
85
+ user_identifier=user_identifier,
86
+ prompts=prompts,
87
+ branding=branding_data,
88
+ onboarding_cards=onboarding_cards,
89
+ js_translations=js_translations,
90
+ redeem_token=redeem_token
91
+ )
@@ -4,8 +4,10 @@
4
4
  # IAToolkit is open source software.
5
5
 
6
6
  from flask.views import MethodView
7
- from flask import render_template, request
7
+ from flask import render_template, request, url_for, session, redirect, flash
8
8
  from iatoolkit.services.profile_service import ProfileService
9
+ from iatoolkit.services.branding_service import BrandingService
10
+ from iatoolkit.services.i18n_service import I18nService
9
11
  from itsdangerous import URLSafeTimedSerializer, SignatureExpired
10
12
  from flask_bcrypt import Bcrypt
11
13
  from injector import inject
@@ -14,44 +16,67 @@ import os
14
16
 
15
17
  class ChangePasswordView(MethodView):
16
18
  @inject
17
- def __init__(self, profile_service: ProfileService):
19
+ def __init__(self,
20
+ profile_service: ProfileService,
21
+ branding_service: BrandingService,
22
+ i18n_service: I18nService):
18
23
  self.profile_service = profile_service
24
+ self.branding_service = branding_service
25
+ self.i18n_service = i18n_service
19
26
 
20
27
  self.serializer = URLSafeTimedSerializer(os.getenv("PASS_RESET_KEY"))
21
28
  self.bcrypt = Bcrypt()
22
29
 
23
30
  def get(self, company_short_name: str, token: str):
24
- # get company info
25
- company = self.profile_service.get_company_by_short_name(company_short_name)
26
- if not company:
27
- return render_template('error.html', message=f"Empresa no encontrada: {company_short_name}"), 404
28
-
29
31
  try:
30
- # Decodificar el token
31
- email = self.serializer.loads(token, salt='password-reset', max_age=3600)
32
- except SignatureExpired as e:
33
- return render_template('forgot_password.html',
34
- alert_message="El enlace de cambio de contraseña ha expirado. Por favor, solicita uno nuevo.")
32
+ company = self.profile_service.get_company_by_short_name(company_short_name)
33
+ if not company:
34
+ return render_template('error.html',
35
+ message=self.i18n_service.t('errors.templates.company_not_found')), 404
36
+
37
+ branding_data = self.branding_service.get_company_branding(company_short_name)
35
38
 
36
- return render_template('change_password.html',
37
- company_short_name=company_short_name,
38
- company=company,
39
- token=token, email=email)
39
+ try:
40
+ # Decodificar el token
41
+ email = self.serializer.loads(token, salt='password-reset', max_age=3600)
42
+ except SignatureExpired as e:
43
+ flash(self.i18n_service.t('errors.change_password.token_expired'), 'error')
44
+ return render_template('forgot_password.html',
45
+ branding=branding_data)
46
+
47
+ return render_template('change_password.html',
48
+ company_short_name=company_short_name,
49
+ company=company,
50
+ branding=branding_data,
51
+ token=token,
52
+ email=email)
53
+ except Exception as e:
54
+ message = self.i18n_service.t('errors.templates.processing_error', error=str(e))
55
+ return render_template(
56
+ "error.html",
57
+ company_short_name=company_short_name,
58
+ branding=branding_data,
59
+ message=message
60
+ ), 500
40
61
 
41
62
  def post(self, company_short_name: str, token: str):
42
63
  # get company info
43
64
  company = self.profile_service.get_company_by_short_name(company_short_name)
44
65
  if not company:
45
- return render_template('error.html', message=f"Empresa no encontrada: {company_short_name}"), 404
66
+ return render_template('error.html',
67
+ message=self.i18n_service.t('errors.templates.company_not_found')), 404
46
68
 
69
+ branding_data = self.branding_service.get_company_branding(company_short_name)
47
70
  try:
48
71
  # Decodificar el token
49
72
  email = self.serializer.loads(token, salt='password-reset', max_age=3600)
50
73
  except SignatureExpired:
74
+ flash(self.i18n_service.t('errors.change_password.token_expired'), 'error')
75
+
51
76
  return render_template('forgot_password.html',
52
77
  company_short_name=company_short_name,
53
78
  company=company,
54
- alert_message="El enlace de cambio de contraseña ha expirado. Por favor, solicita uno nuevo.")
79
+ branding=branding_data)
55
80
 
56
81
  try:
57
82
  # Obtener datos del formulario
@@ -67,25 +92,25 @@ class ChangePasswordView(MethodView):
67
92
  )
68
93
 
69
94
  if "error" in response:
95
+ flash(response["error"], 'error')
96
+
70
97
  return render_template(
71
98
  'change_password.html',
72
99
  token=token,
73
100
  company_short_name=company_short_name,
74
- company=company,
101
+ branding=branding_data,
75
102
  form_data={"temp_code": temp_code,
76
103
  "new_password": new_password,
77
- "confirm_password": confirm_password},
78
- alert_message=response["error"]), 400
104
+ "confirm_password": confirm_password}), 400
79
105
 
80
-
81
- return render_template('login.html',
82
- company_short_name=company_short_name,
83
- company=company,
84
- alert_icon='success',
85
- alert_message="Tu contraseña ha sido restablecida exitosamente. Ahora puedes iniciar sesión.")
106
+ flash(self.i18n_service.t('flash_messages.password_changed_success'), 'success')
107
+ return redirect(url_for('home', company_short_name=company_short_name))
86
108
 
87
109
  except Exception as e:
88
- return render_template("error.html",
89
- company=company,
90
- company_short_name=company_short_name,
91
- message="Ha ocurrido un error inesperado."), 500
110
+ message = self.i18n_service.t('errors.templates.processing_error', error=str(e))
111
+ return render_template(
112
+ "error.html",
113
+ company_short_name=company_short_name,
114
+ branding=branding_data,
115
+ message=message
116
+ ), 500
@@ -0,0 +1,65 @@
1
+ # iatoolkit/views/embedding_api_view.py
2
+ # Copyright (c) 2024 Fernando Libedinsky
3
+ # Product: IAToolkit
4
+ #
5
+ # IAToolkit is open source software.
6
+
7
+ from flask import request, jsonify
8
+ from flask.views import MethodView
9
+ from iatoolkit.services.embedding_service import EmbeddingService
10
+ from iatoolkit.services.auth_service import AuthService
11
+ from injector import inject
12
+ import logging
13
+
14
+ class EmbeddingApiView(MethodView):
15
+ """
16
+ Handles API requests to generate an embedding for a given text.
17
+ Authentication is based on the active Flask session.
18
+ """
19
+
20
+ @inject
21
+ def __init__(self,
22
+ auth_service: AuthService,
23
+ embedding_service: EmbeddingService):
24
+ self.auth_service = auth_service
25
+ self.embedding_service = embedding_service
26
+
27
+ def post(self, company_short_name: str):
28
+ """
29
+ Generates an embedding for the text provided in the request body.
30
+ Expects a JSON payload with a "text" key.
31
+ """
32
+ try:
33
+ # 1. Authenticate the user from the current session
34
+ auth_result = self.auth_service.verify(anonymous=True)
35
+ if not auth_result.get("success"):
36
+ return jsonify(auth_result), auth_result.get("status_code", 401)
37
+
38
+ # 2. Validate incoming request data
39
+ if not request.is_json:
40
+ return jsonify({"error": "Request must be JSON"}), 400
41
+
42
+ data = request.get_json()
43
+ text = data.get('text')
44
+
45
+ if not text:
46
+ return jsonify({"error": "The 'text' key is required."}), 400
47
+
48
+ # 3. Call the embedding service, now passing the company_short_name
49
+ embedding_b64 = self.embedding_service.embed_text(
50
+ company_short_name=company_short_name,
51
+ text=text,
52
+ to_base64=True
53
+ )
54
+
55
+ model_name = self.embedding_service.get_model_name(company_short_name)
56
+ response = {
57
+ "embedding": embedding_b64,
58
+ "model": model_name
59
+ }
60
+ return jsonify(response), 200
61
+
62
+ except Exception as e:
63
+ logging.exception(f"Unexpected error in EmbeddingApiView: {e}")
64
+ # Return a generic error message to the client
65
+ return jsonify({"error": "An internal error occurred while generating the embedding."}), 500
@@ -3,38 +3,71 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from flask.views import MethodView
7
- from injector import inject
8
- from iatoolkit.common.auth import IAuthentication
9
- from iatoolkit.services.query_service import QueryService
10
- from flask import jsonify
6
+ import os
11
7
  import logging
8
+ from flask import request, jsonify, url_for
9
+ from iatoolkit.views.base_login_view import BaseLoginView
12
10
 
13
- class ExternalLoginView(MethodView):
14
11
 
15
- @inject
16
- def __init__(self,
17
- iauthentication: IAuthentication,
18
- query_service: QueryService
19
- ):
20
- self.iauthentication = iauthentication
21
- self.query_service = query_service
12
+ class ExternalLoginView(BaseLoginView):
13
+ """
14
+ Handles login for external users via API.
15
+ Authenticates and then delegates the path decision (fast/slow) to the base class.
16
+ """
17
+ def post(self, company_short_name: str):
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")
22
22
 
23
- def get(self, company_short_name: str, external_user_id: str):
24
- # 1. get access credentials
25
- iaut = self.iauthentication.verify(company_short_name, external_user_id)
26
- if not iaut.get("success"):
27
- return jsonify(iaut), 401
23
+ company = self.profile_service.get_company_by_short_name(company_short_name)
24
+ if not company:
25
+ return jsonify({"error": "Empresa no encontrada"}), 404
28
26
 
29
- try:
30
- # initialize the context
31
- self.query_service.llm_init_context(
32
- company_short_name=company_short_name,
33
- external_user_id=external_user_id
34
- )
27
+ user_identifier = auth_result.get('user_identifier')
28
+
29
+ # 2. Create the external user session.
30
+ self.profile_service.create_external_user_profile_context(company, user_identifier)
31
+
32
+ # 3. create a redeem_token for create session at the end of the process
33
+ redeem_token = self.jwt_service.generate_chat_jwt(
34
+ company_short_name=company_short_name,
35
+ user_identifier=user_identifier,
36
+ expires_delta_seconds=300
37
+ )
35
38
 
36
- return {'status': 'OK'}, 200
39
+ if not redeem_token:
40
+ return jsonify({"error": "Error al generar el redeem_token para login externo."}), 403
41
+
42
+ # 4. define URL to call when slow path is finished
43
+ target_url = url_for('finalize_with_token',
44
+ company_short_name=company_short_name,
45
+ token=redeem_token,
46
+ _external=True)
47
+
48
+ # 5. Delegate the path decision to the centralized logic.
49
+ try:
50
+ return self._handle_login_path(company_short_name, user_identifier, target_url, redeem_token)
37
51
  except Exception as e:
38
- logging.exception(
39
- f"Error inesperado al inicializar el contexto durante el login para company {company_short_name}: {e}")
40
- return jsonify({"error_message": str(e)}), 500
52
+ logging.exception(f"Error processing external login path for {company_short_name}/{user_identifier}: {e}")
53
+ return jsonify({"error": f"Internal server error while starting chat. {str(e)}"}), 500
54
+
55
+
56
+ class RedeemTokenApiView(BaseLoginView):
57
+ # this endpoint is only used ONLY by chat_main.js to redeem a chat token
58
+ def post(self, company_short_name: str):
59
+ data = request.get_json()
60
+ if not data or 'token' not in data:
61
+ return jsonify({"error": "Falta token de validación"}), 400
62
+
63
+ # get the token and validate with auth service
64
+ token = data.get('token')
65
+ redeem_result = self.auth_service.redeem_token_for_session(
66
+ company_short_name=company_short_name,
67
+ token=token
68
+ )
69
+
70
+ if not redeem_result['success']:
71
+ return {"error": redeem_result['error']}, 401
72
+
73
+ return {"status": "ok"}, 200