iatoolkit 0.4.2__py3-none-any.whl → 0.66.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.
- iatoolkit/__init__.py +13 -35
- iatoolkit/base_company.py +74 -8
- iatoolkit/cli_commands.py +15 -23
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +46 -0
- iatoolkit/common/routes.py +141 -0
- iatoolkit/common/session_manager.py +24 -0
- iatoolkit/common/util.py +348 -0
- iatoolkit/company_registry.py +7 -8
- iatoolkit/iatoolkit.py +169 -96
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/call_service.py +140 -0
- iatoolkit/infra/connectors/__init__.py +5 -0
- iatoolkit/infra/connectors/file_connector.py +17 -0
- iatoolkit/infra/connectors/file_connector_factory.py +57 -0
- iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
- iatoolkit/infra/connectors/google_drive_connector.py +68 -0
- iatoolkit/infra/connectors/local_file_connector.py +46 -0
- iatoolkit/infra/connectors/s3_connector.py +33 -0
- iatoolkit/infra/gemini_adapter.py +356 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_client.py +429 -0
- iatoolkit/infra/llm_proxy.py +139 -0
- iatoolkit/infra/llm_response.py +40 -0
- iatoolkit/infra/mail_app.py +145 -0
- iatoolkit/infra/openai_adapter.py +90 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +144 -0
- iatoolkit/locales/es.yaml +140 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +110 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/llm_query_repo.py +91 -0
- iatoolkit/repositories/models.py +336 -0
- iatoolkit/repositories/profile_repo.py +123 -0
- iatoolkit/repositories/tasks_repo.py +52 -0
- iatoolkit/repositories/vs_repo.py +139 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +6 -6
- iatoolkit/services/branding_service.py +149 -0
- {services → iatoolkit/services}/dispatcher_service.py +39 -99
- {services → iatoolkit/services}/document_service.py +5 -5
- {services → iatoolkit/services}/excel_service.py +27 -21
- {services → iatoolkit/services}/file_processor_service.py +5 -5
- iatoolkit/services/help_content_service.py +30 -0
- {services → iatoolkit/services}/history_service.py +8 -16
- iatoolkit/services/i18n_service.py +104 -0
- {services → iatoolkit/services}/jwt_service.py +18 -27
- iatoolkit/services/language_service.py +77 -0
- {services → iatoolkit/services}/load_documents_service.py +19 -14
- {services → iatoolkit/services}/mail_service.py +5 -5
- iatoolkit/services/onboarding_service.py +43 -0
- {services → iatoolkit/services}/profile_service.py +155 -89
- {services → iatoolkit/services}/prompt_manager_service.py +26 -11
- {services → iatoolkit/services}/query_service.py +142 -104
- {services → iatoolkit/services}/search_service.py +21 -5
- {services → iatoolkit/services}/sql_service.py +24 -6
- {services → iatoolkit/services}/tasks_service.py +10 -10
- iatoolkit/services/user_feedback_service.py +103 -0
- iatoolkit/services/user_session_context_service.py +143 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_filepond.js +85 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +112 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +364 -0
- iatoolkit/static/js/chat_onboarding_button.js +97 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +35 -0
- iatoolkit/static/styles/chat_iatoolkit.css +592 -0
- iatoolkit/static/styles/chat_modal.css +169 -0
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/llm_output.css +115 -0
- iatoolkit/static/styles/onboarding.css +169 -0
- iatoolkit/system_prompts/query_main.prompt +5 -15
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/about.html +13 -0
- iatoolkit/templates/base.html +65 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +287 -0
- iatoolkit/templates/chat_modals.html +181 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +50 -0
- iatoolkit/templates/index.html +145 -0
- iatoolkit/templates/login_simulation.html +34 -0
- iatoolkit/templates/onboarding_shell.html +104 -0
- iatoolkit/templates/signup.html +76 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +92 -0
- iatoolkit/views/change_password_view.py +117 -0
- iatoolkit/views/external_login_view.py +73 -0
- iatoolkit/views/file_store_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +72 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +61 -0
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +73 -0
- iatoolkit/views/llmquery_api_view.py +57 -0
- iatoolkit/views/login_simulation_view.py +81 -0
- iatoolkit/views/login_view.py +153 -0
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/signup_view.py +94 -0
- iatoolkit/views/tasks_api_view.py +72 -0
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/verify_user_view.py +62 -0
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
- iatoolkit-0.66.2.dist-info/RECORD +119 -0
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -1
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.4.2.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/WHEEL +0 -0
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
from repositories.vs_repo import VSRepo
|
|
7
|
-
from repositories.document_repo import DocumentRepo
|
|
8
|
-
from repositories.profile_repo import ProfileRepo
|
|
9
|
-
from repositories.llm_query_repo import LLMQueryRepo
|
|
10
|
-
|
|
11
|
-
from
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.repositories.vs_repo import VSRepo
|
|
7
|
+
from iatoolkit.repositories.document_repo import DocumentRepo
|
|
8
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
|
+
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
10
|
+
|
|
11
|
+
from iatoolkit.repositories.models import Document, VSDoc, Company
|
|
12
|
+
from iatoolkit.services.document_service import DocumentService
|
|
12
13
|
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
|
13
|
-
from infra.connectors.file_connector_factory import FileConnectorFactory
|
|
14
|
-
from services.file_processor_service import FileProcessorConfig, FileProcessor
|
|
15
|
-
from services.dispatcher_service import Dispatcher
|
|
16
|
-
from common.exceptions import IAToolkitException
|
|
14
|
+
from iatoolkit.infra.connectors.file_connector_factory import FileConnectorFactory
|
|
15
|
+
from iatoolkit.services.file_processor_service import FileProcessorConfig, FileProcessor
|
|
16
|
+
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
17
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
17
18
|
import logging
|
|
18
19
|
import base64
|
|
19
20
|
from injector import inject
|
|
@@ -120,6 +121,10 @@ class LoadDocumentsService:
|
|
|
120
121
|
context (dict, optional): A context dictionary, may contain predefined metadata.
|
|
121
122
|
"""
|
|
122
123
|
|
|
124
|
+
if not company:
|
|
125
|
+
raise IAToolkitException(IAToolkitException.ErrorType.MISSING_PARAMETER,
|
|
126
|
+
f"Falta configurar empresa")
|
|
127
|
+
|
|
123
128
|
# check if file exist in repositories
|
|
124
129
|
if self.doc_repo.get(company_id=company.id,filename=filename):
|
|
125
130
|
return
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from infra.mail_app import MailApp
|
|
6
|
+
from iatoolkit.infra.mail_app import MailApp
|
|
7
7
|
from injector import inject
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from common.exceptions import IAToolkitException
|
|
9
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
10
10
|
import base64
|
|
11
11
|
|
|
12
12
|
TEMP_DIR = Path("static/temp")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.repositories.models import Company
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OnboardingService:
|
|
11
|
+
"""
|
|
12
|
+
Servicio para gestionar las tarjetas de contenido que se muestran
|
|
13
|
+
durante la pantalla de carga (onboarding).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""
|
|
18
|
+
Define el conjunto de tarjetas de onboarding por defecto.
|
|
19
|
+
"""
|
|
20
|
+
self._default_cards = [
|
|
21
|
+
{'icon': 'fas fa-users', 'title': 'Clientes',
|
|
22
|
+
'text': 'Conozco en detalle a nuestros clientes: antigüedad, contactos, historial de operaciones.<br><br><strong>Ejemplo:</strong> ¿cuántos clientes nuevos se incorporaron a mi cartera este año?'},
|
|
23
|
+
{'icon': 'fas fa-cubes', 'title': 'Productos',
|
|
24
|
+
'text': 'Productos: características, condiciones, historial.'},
|
|
25
|
+
|
|
26
|
+
{'icon': 'fas fa-cogs', 'title': 'Personaliza tus Prompts',
|
|
27
|
+
'text': 'Utiliza la varita mágica y podrás explorar los prompts predefinidos que he preparado para ti.'},
|
|
28
|
+
{'icon': 'fas fa-table', 'title': 'Tablas y Excel',
|
|
29
|
+
'text': 'Puedes pedirme la respuesta en formato de tablas o excel.<br><br><strong>Ejemplo:</strong> dame una tabla con los 10 certificados más grandes este año.'},
|
|
30
|
+
{'icon': 'fas fa-shield-alt', 'title': 'Seguridad y Confidencialidad',
|
|
31
|
+
'text': 'Toda tu información es procesada de forma segura y confidencial dentro de nuestro entorno protegido.'}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def get_onboarding_cards(self, company: Company | None) -> List[Dict[str, Any]]:
|
|
35
|
+
"""
|
|
36
|
+
Retorna la lista de tarjetas de onboarding para una compañía.
|
|
37
|
+
Si la compañía tiene tarjetas personalizadas, las devuelve.
|
|
38
|
+
De lo contrario, devuelve las tarjetas por defecto.
|
|
39
|
+
"""
|
|
40
|
+
if company and company.onboarding_cards:
|
|
41
|
+
return company.onboarding_cards
|
|
42
|
+
|
|
43
|
+
return self._default_cards
|
|
@@ -1,96 +1,167 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
from injector import inject
|
|
7
|
-
from repositories.profile_repo import ProfileRepo
|
|
8
|
-
from
|
|
7
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
8
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
9
|
+
from iatoolkit.repositories.models import User, Company, ApiKey
|
|
9
10
|
from flask_bcrypt import check_password_hash
|
|
10
|
-
from common.session_manager import SessionManager
|
|
11
|
+
from iatoolkit.common.session_manager import SessionManager
|
|
12
|
+
from iatoolkit.services.user_session_context_service import UserSessionContextService
|
|
11
13
|
from flask_bcrypt import Bcrypt
|
|
12
|
-
from infra.mail_app import MailApp
|
|
14
|
+
from iatoolkit.infra.mail_app import MailApp
|
|
13
15
|
import random
|
|
14
|
-
import logging
|
|
15
16
|
import re
|
|
16
17
|
import secrets
|
|
17
18
|
import string
|
|
18
|
-
|
|
19
|
-
from services.
|
|
20
|
-
from services.query_service import QueryService
|
|
19
|
+
import logging
|
|
20
|
+
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class ProfileService:
|
|
24
24
|
@inject
|
|
25
25
|
def __init__(self,
|
|
26
|
+
i18n_service: I18nService,
|
|
26
27
|
profile_repo: ProfileRepo,
|
|
27
28
|
session_context_service: UserSessionContextService,
|
|
28
|
-
|
|
29
|
+
dispatcher: Dispatcher,
|
|
29
30
|
mail_app: MailApp):
|
|
31
|
+
self.i18n_service = i18n_service
|
|
30
32
|
self.profile_repo = profile_repo
|
|
33
|
+
self.dispatcher = dispatcher
|
|
31
34
|
self.session_context = session_context_service
|
|
32
|
-
self.query_service = query_service
|
|
33
35
|
self.mail_app = mail_app
|
|
34
36
|
self.bcrypt = Bcrypt()
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
def login(self, company_short_name: str, email: str, password: str) -> dict:
|
|
38
40
|
try:
|
|
39
|
-
# check if
|
|
41
|
+
# check if user exists
|
|
40
42
|
user = self.profile_repo.get_user_by_email(email)
|
|
41
43
|
if not user:
|
|
42
|
-
return {
|
|
44
|
+
return {'success': False, 'message': self.i18n_service.t('errors.auth.user_not_found')}
|
|
43
45
|
|
|
44
46
|
# check the encrypted password
|
|
45
47
|
if not check_password_hash(user.password, password):
|
|
46
|
-
return {
|
|
48
|
+
return {'success': False, 'message': self.i18n_service.t('errors.auth.invalid_password')}
|
|
47
49
|
|
|
48
|
-
company = self.get_company_by_short_name(company_short_name)
|
|
50
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
49
51
|
if not company:
|
|
50
|
-
return {"
|
|
52
|
+
return {'success': False, "message": "Empresa no encontrada"}
|
|
51
53
|
|
|
52
|
-
# check that user belongs to
|
|
54
|
+
# check that user belongs to company
|
|
53
55
|
if company not in user.companies:
|
|
54
|
-
return {"
|
|
56
|
+
return {'success': False, "message": "Usuario no esta autorizado para esta empresa"}
|
|
55
57
|
|
|
56
58
|
if not user.verified:
|
|
57
|
-
return {
|
|
59
|
+
return {'success': False,
|
|
60
|
+
"message": "Tu cuenta no ha sido verificada. Por favor, revisa tu correo."}
|
|
61
|
+
|
|
62
|
+
# 1. Build the local user profile dictionary here.
|
|
63
|
+
# the user_profile variables are used on the LLM templates also (see in query_main.prompt)
|
|
64
|
+
user_identifier = user.email # no longer de ID
|
|
65
|
+
user_profile = {
|
|
66
|
+
"user_email": user.email,
|
|
67
|
+
"user_fullname": f'{user.first_name} {user.last_name}',
|
|
68
|
+
"user_is_local": True,
|
|
69
|
+
"extras": {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# 2. create user_profile in context
|
|
73
|
+
self.save_user_profile(company, user_identifier, user_profile)
|
|
74
|
+
|
|
75
|
+
# 3. create the web session
|
|
76
|
+
self.set_session_for_user(company.short_name, user_identifier)
|
|
77
|
+
return {'success': True, "user_identifier": user_identifier, "message": "Login exitoso"}
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logging.error(f"Error in login: {e}")
|
|
80
|
+
return {'success': False, "message": str(e)}
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
def create_external_user_profile_context(self, company: Company, user_identifier: str):
|
|
83
|
+
"""
|
|
84
|
+
Public method for views to create a user profile context for an external user.
|
|
85
|
+
"""
|
|
86
|
+
# 1. Fetch the external user profile via Dispatcher.
|
|
87
|
+
external_user_profile = self.dispatcher.get_user_info(
|
|
88
|
+
company_name=company.short_name,
|
|
89
|
+
user_identifier=user_identifier
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# 2. Call the session creation helper with external_user_id as user_identifier
|
|
93
|
+
self.save_user_profile(
|
|
94
|
+
company=company,
|
|
95
|
+
user_identifier=user_identifier,
|
|
96
|
+
user_profile=external_user_profile)
|
|
97
|
+
|
|
98
|
+
def save_user_profile(self, company: Company, user_identifier: str, user_profile: dict):
|
|
99
|
+
"""
|
|
100
|
+
Private helper: Takes a pre-built profile, saves it to Redis, and sets the Flask cookie.
|
|
101
|
+
"""
|
|
102
|
+
user_profile['company_short_name'] = company.short_name
|
|
103
|
+
user_profile['user_identifier'] = user_identifier
|
|
104
|
+
user_profile['id'] = user_identifier
|
|
105
|
+
user_profile['company_id'] = company.id
|
|
106
|
+
user_profile['company'] = company.name
|
|
61
107
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
company_short_name=company_short_name,
|
|
65
|
-
local_user_id=user.id
|
|
66
|
-
)
|
|
108
|
+
# save user_profile in Redis session
|
|
109
|
+
self.session_context.save_profile_data(company.short_name, user_identifier, user_profile)
|
|
67
110
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
def set_session_for_user(self, company_short_name: str, user_identifier:str ):
|
|
112
|
+
# save a min Flask session cookie for this user
|
|
113
|
+
SessionManager.set('company_short_name', company_short_name)
|
|
114
|
+
SessionManager.set('user_identifier', user_identifier)
|
|
115
|
+
|
|
116
|
+
def get_current_session_info(self) -> dict:
|
|
117
|
+
"""
|
|
118
|
+
Gets the current web user's profile from the unified session.
|
|
119
|
+
This is the standard way to access user data for web requests.
|
|
120
|
+
"""
|
|
121
|
+
# 1. Get identifiers from the simple Flask session cookie.
|
|
122
|
+
user_identifier = SessionManager.get('user_identifier')
|
|
123
|
+
company_short_name = SessionManager.get('company_short_name')
|
|
124
|
+
|
|
125
|
+
if not user_identifier or not company_short_name:
|
|
126
|
+
# No authenticated web user.
|
|
127
|
+
return {}
|
|
128
|
+
|
|
129
|
+
# 2. Use the identifiers to fetch the full, authoritative profile from Redis.
|
|
130
|
+
profile = self.session_context.get_profile_data(company_short_name, user_identifier)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"user_identifier": user_identifier,
|
|
134
|
+
"company_short_name": company_short_name,
|
|
135
|
+
"profile": profile
|
|
89
136
|
}
|
|
90
|
-
SessionManager.set('user', user_data)
|
|
91
137
|
|
|
92
|
-
|
|
93
|
-
|
|
138
|
+
def update_user_language(self, user_identifier: str, new_lang: str) -> dict:
|
|
139
|
+
"""
|
|
140
|
+
Business logic to update a user's preferred language.
|
|
141
|
+
It validates the language and then calls the generic update method.
|
|
142
|
+
"""
|
|
143
|
+
# 1. Validate that the language is supported by checking the loaded translations.
|
|
144
|
+
if new_lang not in self.i18n_service.translations:
|
|
145
|
+
return {'success': False, 'error_message': self.i18n_service.t('errors.general.unsupported_language')}
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
# 2. Call the generic update_user method, passing the specific field to update.
|
|
149
|
+
self.update_user(user_identifier, preferred_language=new_lang)
|
|
150
|
+
return {'success': True, 'message': 'Language updated successfully.'}
|
|
151
|
+
except Exception as e:
|
|
152
|
+
# Log the error and return a generic failure message.
|
|
153
|
+
logging.error(f"Failed to update language for {user_identifier}: {e}")
|
|
154
|
+
return {'success': False, 'error_message': self.i18n_service.t('errors.general.unexpected_error')}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
|
|
158
|
+
"""
|
|
159
|
+
Fetches a user profile directly by their identifier, bypassing the Flask session.
|
|
160
|
+
This is ideal for API-side checks.
|
|
161
|
+
"""
|
|
162
|
+
if not company_short_name or not user_identifier:
|
|
163
|
+
return {}
|
|
164
|
+
return self.session_context.get_profile_data(company_short_name, user_identifier)
|
|
94
165
|
|
|
95
166
|
|
|
96
167
|
def signup(self,
|
|
@@ -98,19 +169,18 @@ class ProfileService:
|
|
|
98
169
|
email: str,
|
|
99
170
|
first_name: str,
|
|
100
171
|
last_name: str,
|
|
101
|
-
rut: str,
|
|
102
172
|
password: str,
|
|
103
173
|
confirm_password: str,
|
|
104
174
|
verification_url: str) -> dict:
|
|
105
175
|
try:
|
|
106
176
|
|
|
107
177
|
# get company info
|
|
108
|
-
company = self.get_company_by_short_name(company_short_name)
|
|
178
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
109
179
|
if not company:
|
|
110
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
"error": self.i18n_service.t('errors.signup.company_not_found', company_name=company_short_name)}
|
|
111
182
|
|
|
112
183
|
# normalize format's
|
|
113
|
-
rut = rut.lower().replace(" ", "")
|
|
114
184
|
email = email.lower()
|
|
115
185
|
|
|
116
186
|
# check if user exists
|
|
@@ -118,34 +188,31 @@ class ProfileService:
|
|
|
118
188
|
if existing_user:
|
|
119
189
|
# validate password
|
|
120
190
|
if not self.bcrypt.check_password_hash(existing_user.password, password):
|
|
121
|
-
return {"error":
|
|
122
|
-
|
|
123
|
-
if rut != existing_user.rut:
|
|
124
|
-
return {"error": "El RUT ingresado no corresponde al email existente."}
|
|
191
|
+
return {"error": self.i18n_service.t('errors.signup.incorrect_password_for_existing_user', email=email)}
|
|
125
192
|
|
|
126
193
|
# check if register
|
|
127
194
|
if company in existing_user.companies:
|
|
128
|
-
return {"error":
|
|
195
|
+
return {"error": self.i18n_service.t('errors.signup.user_already_registered', email=email)}
|
|
129
196
|
else:
|
|
130
197
|
# add new company to existing user
|
|
131
198
|
existing_user.companies.append(company)
|
|
132
199
|
self.profile_repo.save_user(existing_user)
|
|
133
|
-
return {"message":
|
|
200
|
+
return {"message": self.i18n_service.t('flash_messages.user_associated_success')}
|
|
134
201
|
|
|
135
202
|
# add the new user
|
|
136
203
|
if password != confirm_password:
|
|
137
|
-
return {"error":
|
|
204
|
+
return {"error": self.i18n_service.t('errors.signup.password_mismatch')}
|
|
138
205
|
|
|
139
206
|
is_valid, message = self.validate_password(password)
|
|
140
207
|
if not is_valid:
|
|
141
|
-
|
|
208
|
+
# Translate the key returned by validate_password
|
|
209
|
+
return {"error": self.i18n_service.t(message)}
|
|
142
210
|
|
|
143
211
|
# encrypt the password
|
|
144
212
|
hashed_password = self.bcrypt.generate_password_hash(password).decode('utf-8')
|
|
145
213
|
|
|
146
214
|
# create the new user
|
|
147
215
|
new_user = User(email=email,
|
|
148
|
-
rut=rut,
|
|
149
216
|
password=hashed_password,
|
|
150
217
|
first_name=first_name.lower(),
|
|
151
218
|
last_name=last_name.lower(),
|
|
@@ -161,9 +228,9 @@ class ProfileService:
|
|
|
161
228
|
# send email with verification
|
|
162
229
|
self.send_verification_email(new_user, company_short_name)
|
|
163
230
|
|
|
164
|
-
return {"message":
|
|
231
|
+
return {"message": self.i18n_service.t('flash_messages.signup_success')}
|
|
165
232
|
except Exception as e:
|
|
166
|
-
return {"error":
|
|
233
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
167
234
|
|
|
168
235
|
def update_user(self, email: str, **kwargs) -> User:
|
|
169
236
|
return self.profile_repo.update_user(email, **kwargs)
|
|
@@ -173,14 +240,14 @@ class ProfileService:
|
|
|
173
240
|
# check if user exist
|
|
174
241
|
user = self.profile_repo.get_user_by_email(email)
|
|
175
242
|
if not user:
|
|
176
|
-
return {"error":
|
|
243
|
+
return {"error": self.i18n_service.t('errors.verification.user_not_found')}
|
|
177
244
|
|
|
178
245
|
# activate the user account
|
|
179
246
|
self.profile_repo.verify_user(email)
|
|
180
|
-
return {"message":
|
|
247
|
+
return {"message": self.i18n_service.t('flash_messages.account_verified_success')}
|
|
181
248
|
|
|
182
249
|
except Exception as e:
|
|
183
|
-
return {"error":
|
|
250
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
184
251
|
|
|
185
252
|
def change_password(self,
|
|
186
253
|
email: str,
|
|
@@ -189,28 +256,28 @@ class ProfileService:
|
|
|
189
256
|
confirm_password: str):
|
|
190
257
|
try:
|
|
191
258
|
if new_password != confirm_password:
|
|
192
|
-
return {"error":
|
|
259
|
+
return {"error": self.i18n_service.t('errors.change_password.password_mismatch')}
|
|
193
260
|
|
|
194
261
|
# check the temporary code
|
|
195
262
|
user = self.profile_repo.get_user_by_email(email)
|
|
196
263
|
if not user or user.temp_code != temp_code:
|
|
197
|
-
return {"error":
|
|
264
|
+
return {"error": self.i18n_service.t('errors.change_password.invalid_temp_code')}
|
|
198
265
|
|
|
199
266
|
# encrypt and save the password, make the temporary code invalid
|
|
200
267
|
hashed_password = self.bcrypt.generate_password_hash(new_password).decode('utf-8')
|
|
201
268
|
self.profile_repo.update_password(email, hashed_password)
|
|
202
269
|
self.profile_repo.reset_temp_code(email)
|
|
203
270
|
|
|
204
|
-
return {"message":
|
|
271
|
+
return {"message": self.i18n_service.t('flash_messages.password_changed_success')}
|
|
205
272
|
except Exception as e:
|
|
206
|
-
return {"error":
|
|
273
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
207
274
|
|
|
208
275
|
def forgot_password(self, email: str, reset_url: str):
|
|
209
276
|
try:
|
|
210
277
|
# Verificar si el usuario existe
|
|
211
278
|
user = self.profile_repo.get_user_by_email(email)
|
|
212
279
|
if not user:
|
|
213
|
-
return {"error":
|
|
280
|
+
return {"error": self.i18n_service.t('errors.forgot_password.user_not_registered', email=email)}
|
|
214
281
|
|
|
215
282
|
# Gen a temporary code and store in the repositories
|
|
216
283
|
temp_code = ''.join(random.choices(string.ascii_letters + string.digits, k=6)).upper()
|
|
@@ -219,35 +286,31 @@ class ProfileService:
|
|
|
219
286
|
# send email to the user
|
|
220
287
|
self.send_forgot_password_email(user, reset_url)
|
|
221
288
|
|
|
222
|
-
return {"message":
|
|
289
|
+
return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
|
|
223
290
|
except Exception as e:
|
|
224
|
-
return {"error":
|
|
291
|
+
return {"error": self.i18n_service.t('errors.general.unexpected_error')}
|
|
225
292
|
|
|
226
293
|
def validate_password(self, password):
|
|
227
294
|
"""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
- Contiene al menos una letra mayúscula
|
|
231
|
-
- Contiene al menos una letra minúscula
|
|
232
|
-
- Contiene al menos un número
|
|
233
|
-
- Contiene al menos un carácter especial
|
|
295
|
+
Validates that a password meets all requirements.
|
|
296
|
+
Returns (True, "...") on success, or (False, "translation.key") on failure.
|
|
234
297
|
"""
|
|
235
298
|
if len(password) < 8:
|
|
236
|
-
return False, "
|
|
299
|
+
return False, "errors.validation.password_too_short"
|
|
237
300
|
|
|
238
301
|
if not any(char.isupper() for char in password):
|
|
239
|
-
return False, "
|
|
302
|
+
return False, "errors.validation.password_no_uppercase"
|
|
240
303
|
|
|
241
304
|
if not any(char.islower() for char in password):
|
|
242
|
-
return False, "
|
|
305
|
+
return False, "errors.validation.password_no_lowercase"
|
|
243
306
|
|
|
244
307
|
if not any(char.isdigit() for char in password):
|
|
245
|
-
return False, "
|
|
308
|
+
return False, "errors.validation.password_no_digit"
|
|
246
309
|
|
|
247
310
|
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
|
|
248
|
-
return False, "
|
|
311
|
+
return False, "errors.validation.password_no_special_char"
|
|
249
312
|
|
|
250
|
-
return True, "
|
|
313
|
+
return True, "Password is valid."
|
|
251
314
|
|
|
252
315
|
def get_companies(self):
|
|
253
316
|
return self.profile_repo.get_companies()
|
|
@@ -255,6 +318,9 @@ class ProfileService:
|
|
|
255
318
|
def get_company_by_short_name(self, short_name: str) -> Company:
|
|
256
319
|
return self.profile_repo.get_company_by_short_name(short_name)
|
|
257
320
|
|
|
321
|
+
def get_active_api_key_entry(self, api_key_value: str) -> ApiKey | None:
|
|
322
|
+
return self.profile_repo.get_active_api_key_entry(api_key_value)
|
|
323
|
+
|
|
258
324
|
def new_api_key(self, company_short_name: str):
|
|
259
325
|
company = self.get_company_by_short_name(company_short_name)
|
|
260
326
|
if not company:
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
from injector import inject
|
|
7
|
-
from repositories.llm_query_repo import LLMQueryRepo
|
|
7
|
+
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
8
|
+
|
|
8
9
|
import logging
|
|
9
|
-
from repositories.profile_repo import ProfileRepo
|
|
10
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
10
11
|
from collections import defaultdict
|
|
11
|
-
from repositories.models import Prompt, PromptCategory, Company
|
|
12
|
+
from iatoolkit.repositories.models import Prompt, PromptCategory, Company
|
|
12
13
|
import os
|
|
13
|
-
from common.exceptions import IAToolkitException
|
|
14
|
-
from pathlib import Path
|
|
14
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
15
15
|
import importlib.resources
|
|
16
16
|
|
|
17
17
|
|
|
@@ -29,7 +29,7 @@ class PromptService:
|
|
|
29
29
|
category: PromptCategory = None,
|
|
30
30
|
active: bool = True,
|
|
31
31
|
is_system_prompt: bool = False,
|
|
32
|
-
|
|
32
|
+
custom_fields: list = []
|
|
33
33
|
):
|
|
34
34
|
|
|
35
35
|
prompt_filename = prompt_name.lower() + '.prompt'
|
|
@@ -45,6 +45,16 @@ class PromptService:
|
|
|
45
45
|
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
|
|
46
46
|
f'No existe el archivo de prompt: {relative_prompt_path}')
|
|
47
47
|
|
|
48
|
+
if custom_fields:
|
|
49
|
+
for f in custom_fields:
|
|
50
|
+
if ('data_key' not in f) or ('label' not in f):
|
|
51
|
+
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_PARAMETER,
|
|
52
|
+
f'El campo custom_fields debe contener los campos: data_key y label')
|
|
53
|
+
|
|
54
|
+
# add default value for data_type
|
|
55
|
+
if 'type' not in f:
|
|
56
|
+
f['type'] = 'text'
|
|
57
|
+
|
|
48
58
|
prompt = Prompt(
|
|
49
59
|
company_id=company.id if company else None,
|
|
50
60
|
name=prompt_name,
|
|
@@ -54,7 +64,7 @@ class PromptService:
|
|
|
54
64
|
active=active,
|
|
55
65
|
filename=prompt_filename,
|
|
56
66
|
is_system_prompt=is_system_prompt,
|
|
57
|
-
|
|
67
|
+
custom_fields=custom_fields
|
|
58
68
|
)
|
|
59
69
|
|
|
60
70
|
try:
|
|
@@ -160,7 +170,12 @@ class PromptService:
|
|
|
160
170
|
'category_name': cat_name,
|
|
161
171
|
'category_order': cat_order,
|
|
162
172
|
'prompts': [
|
|
163
|
-
{
|
|
173
|
+
{
|
|
174
|
+
'prompt': p.name,
|
|
175
|
+
'description': p.description,
|
|
176
|
+
'custom_fields': p.custom_fields,
|
|
177
|
+
'order': p.order
|
|
178
|
+
}
|
|
164
179
|
for p in prompts
|
|
165
180
|
]
|
|
166
181
|
})
|