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.
Files changed (123) hide show
  1. iatoolkit/__init__.py +13 -35
  2. iatoolkit/base_company.py +74 -8
  3. iatoolkit/cli_commands.py +15 -23
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +46 -0
  6. iatoolkit/common/routes.py +141 -0
  7. iatoolkit/common/session_manager.py +24 -0
  8. iatoolkit/common/util.py +348 -0
  9. iatoolkit/company_registry.py +7 -8
  10. iatoolkit/iatoolkit.py +169 -96
  11. iatoolkit/infra/__init__.py +5 -0
  12. iatoolkit/infra/call_service.py +140 -0
  13. iatoolkit/infra/connectors/__init__.py +5 -0
  14. iatoolkit/infra/connectors/file_connector.py +17 -0
  15. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  16. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  17. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  18. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  19. iatoolkit/infra/connectors/s3_connector.py +33 -0
  20. iatoolkit/infra/gemini_adapter.py +356 -0
  21. iatoolkit/infra/google_chat_app.py +57 -0
  22. iatoolkit/infra/llm_client.py +429 -0
  23. iatoolkit/infra/llm_proxy.py +139 -0
  24. iatoolkit/infra/llm_response.py +40 -0
  25. iatoolkit/infra/mail_app.py +145 -0
  26. iatoolkit/infra/openai_adapter.py +90 -0
  27. iatoolkit/infra/redis_session_manager.py +122 -0
  28. iatoolkit/locales/en.yaml +144 -0
  29. iatoolkit/locales/es.yaml +140 -0
  30. iatoolkit/repositories/__init__.py +5 -0
  31. iatoolkit/repositories/database_manager.py +110 -0
  32. iatoolkit/repositories/document_repo.py +33 -0
  33. iatoolkit/repositories/llm_query_repo.py +91 -0
  34. iatoolkit/repositories/models.py +336 -0
  35. iatoolkit/repositories/profile_repo.py +123 -0
  36. iatoolkit/repositories/tasks_repo.py +52 -0
  37. iatoolkit/repositories/vs_repo.py +139 -0
  38. iatoolkit/services/__init__.py +5 -0
  39. iatoolkit/services/auth_service.py +193 -0
  40. {services → iatoolkit/services}/benchmark_service.py +6 -6
  41. iatoolkit/services/branding_service.py +149 -0
  42. {services → iatoolkit/services}/dispatcher_service.py +39 -99
  43. {services → iatoolkit/services}/document_service.py +5 -5
  44. {services → iatoolkit/services}/excel_service.py +27 -21
  45. {services → iatoolkit/services}/file_processor_service.py +5 -5
  46. iatoolkit/services/help_content_service.py +30 -0
  47. {services → iatoolkit/services}/history_service.py +8 -16
  48. iatoolkit/services/i18n_service.py +104 -0
  49. {services → iatoolkit/services}/jwt_service.py +18 -27
  50. iatoolkit/services/language_service.py +77 -0
  51. {services → iatoolkit/services}/load_documents_service.py +19 -14
  52. {services → iatoolkit/services}/mail_service.py +5 -5
  53. iatoolkit/services/onboarding_service.py +43 -0
  54. {services → iatoolkit/services}/profile_service.py +155 -89
  55. {services → iatoolkit/services}/prompt_manager_service.py +26 -11
  56. {services → iatoolkit/services}/query_service.py +142 -104
  57. {services → iatoolkit/services}/search_service.py +21 -5
  58. {services → iatoolkit/services}/sql_service.py +24 -6
  59. {services → iatoolkit/services}/tasks_service.py +10 -10
  60. iatoolkit/services/user_feedback_service.py +103 -0
  61. iatoolkit/services/user_session_context_service.py +143 -0
  62. iatoolkit/static/images/fernando.jpeg +0 -0
  63. iatoolkit/static/js/chat_feedback_button.js +80 -0
  64. iatoolkit/static/js/chat_filepond.js +85 -0
  65. iatoolkit/static/js/chat_help_content.js +124 -0
  66. iatoolkit/static/js/chat_history_button.js +112 -0
  67. iatoolkit/static/js/chat_logout_button.js +36 -0
  68. iatoolkit/static/js/chat_main.js +364 -0
  69. iatoolkit/static/js/chat_onboarding_button.js +97 -0
  70. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  71. iatoolkit/static/js/chat_reload_button.js +35 -0
  72. iatoolkit/static/styles/chat_iatoolkit.css +592 -0
  73. iatoolkit/static/styles/chat_modal.css +169 -0
  74. iatoolkit/static/styles/chat_public.css +107 -0
  75. iatoolkit/static/styles/landing_page.css +182 -0
  76. iatoolkit/static/styles/llm_output.css +115 -0
  77. iatoolkit/static/styles/onboarding.css +169 -0
  78. iatoolkit/system_prompts/query_main.prompt +5 -15
  79. iatoolkit/templates/_company_header.html +20 -0
  80. iatoolkit/templates/_login_widget.html +42 -0
  81. iatoolkit/templates/about.html +13 -0
  82. iatoolkit/templates/base.html +65 -0
  83. iatoolkit/templates/change_password.html +66 -0
  84. iatoolkit/templates/chat.html +287 -0
  85. iatoolkit/templates/chat_modals.html +181 -0
  86. iatoolkit/templates/error.html +51 -0
  87. iatoolkit/templates/forgot_password.html +50 -0
  88. iatoolkit/templates/index.html +145 -0
  89. iatoolkit/templates/login_simulation.html +34 -0
  90. iatoolkit/templates/onboarding_shell.html +104 -0
  91. iatoolkit/templates/signup.html +76 -0
  92. iatoolkit/views/__init__.py +5 -0
  93. iatoolkit/views/base_login_view.py +92 -0
  94. iatoolkit/views/change_password_view.py +117 -0
  95. iatoolkit/views/external_login_view.py +73 -0
  96. iatoolkit/views/file_store_api_view.py +65 -0
  97. iatoolkit/views/forgot_password_view.py +72 -0
  98. iatoolkit/views/help_content_api_view.py +54 -0
  99. iatoolkit/views/history_api_view.py +56 -0
  100. iatoolkit/views/home_view.py +61 -0
  101. iatoolkit/views/index_view.py +14 -0
  102. iatoolkit/views/init_context_api_view.py +73 -0
  103. iatoolkit/views/llmquery_api_view.py +57 -0
  104. iatoolkit/views/login_simulation_view.py +81 -0
  105. iatoolkit/views/login_view.py +153 -0
  106. iatoolkit/views/logout_api_view.py +49 -0
  107. iatoolkit/views/profile_api_view.py +46 -0
  108. iatoolkit/views/prompt_api_view.py +37 -0
  109. iatoolkit/views/signup_view.py +94 -0
  110. iatoolkit/views/tasks_api_view.py +72 -0
  111. iatoolkit/views/tasks_review_api_view.py +55 -0
  112. iatoolkit/views/user_feedback_api_view.py +60 -0
  113. iatoolkit/views/verify_user_view.py +62 -0
  114. {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
  115. iatoolkit-0.66.2.dist-info/RECORD +119 -0
  116. {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -1
  117. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  118. iatoolkit-0.4.2.dist-info/RECORD +0 -32
  119. services/__init__.py +0 -5
  120. services/api_service.py +0 -75
  121. services/user_feedback_service.py +0 -67
  122. services/user_session_context_service.py +0 -85
  123. {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
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
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
- from repositories.models import Document, VSDoc, Company
11
- from services.document_service import DocumentService
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
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
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
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
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 repositories.models import User, Company, ApiKey
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
- from datetime import datetime, timezone
19
- from services.user_session_context_service import UserSessionContextService
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
- query_service: QueryService,
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 exits
41
+ # check if user exists
40
42
  user = self.profile_repo.get_user_by_email(email)
41
43
  if not user:
42
- return {"error": "Usuario no encontrado"}
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 {"error": "Contraseña inválida"}
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 {"error": "Empresa no encontrada"}
52
+ return {'success': False, "message": "Empresa no encontrada"}
51
53
 
52
- # check that user belongs to company
54
+ # check that user belongs to company
53
55
  if company not in user.companies:
54
- return {"error": "Usuario no esta autorizado para esta empresa"}
56
+ return {'success': False, "message": "Usuario no esta autorizado para esta empresa"}
55
57
 
56
58
  if not user.verified:
57
- return {"error": "Tu cuenta no ha sido verificada. Por favor, revisa tu correo."}
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
- # clear history save user data into session manager
60
- self.set_user_session(user=user, company=company)
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
- # initialize company context
63
- self.query_service.llm_init_context(
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
- return {"message": "Login exitoso"}
69
- except Exception as e:
70
- logging.exception(f"login error: {str(e)}")
71
- return {"error": str(e)}
72
-
73
- def set_user_session(self, user: User, company: Company):
74
- SessionManager.set('user_id', user.id)
75
- SessionManager.set('company_id', company.id)
76
- SessionManager.set('company_short_name', company.short_name)
77
-
78
- # save user data into session manager
79
- user_data = {
80
- "id": user.id,
81
- "email": user.email,
82
- "user_fullname": f'{user.first_name} {user.last_name}',
83
- "super_user": user.super_user,
84
- "company_id": company.id,
85
- "company": company.name,
86
- "company_short_name": company.short_name,
87
- "user_is_local": True, # origin of data
88
- "extras": {} # company specific data
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
- # save time session was activated (in timestamp format)
93
- SessionManager.set('last_activity', datetime.now(timezone.utc).timestamp())
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 {"error": f"la empresa {company_short_name} no existe"}
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": "La contraseña es incorrecta. No se puede agregar a la nueva empresa."}
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": "Usuario ya registrado en esta empresa"}
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": "Usuario asociado a nueva empresa"}
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": "Las contraseñas no coinciden. Por favor, inténtalo de nuevo."}
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
- return {"error": message}
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": "Registro exitoso. Por favor, revisa tu correo para verificar tu cuenta."}
231
+ return {"message": self.i18n_service.t('flash_messages.signup_success')}
165
232
  except Exception as e:
166
- return {"error": str(e)}
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": "El usuario no existe."}
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": "Tu cuenta ha sido verificada exitosamente. Ahora puedes iniciar sesión."}
247
+ return {"message": self.i18n_service.t('flash_messages.account_verified_success')}
181
248
 
182
249
  except Exception as e:
183
- return {"error": str(e)}
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": "Las contraseñas no coinciden. Por favor, inténtalo nuevamente."}
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": "El código temporal no es válido. Por favor, verifica o solicita uno nuevo."}
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": "La clave se cambio correctamente"}
271
+ return {"message": self.i18n_service.t('flash_messages.password_changed_success')}
205
272
  except Exception as e:
206
- return {"error": str(e)}
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": "El usuario no existe."}
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": "se envio mail para cambio de clave"}
289
+ return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
223
290
  except Exception as e:
224
- return {"error": str(e)}
291
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
225
292
 
226
293
  def validate_password(self, password):
227
294
  """
228
- Valida que una contraseña cumpla con los siguientes requisitos:
229
- - Al menos 8 caracteres de longitud
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, "La contraseña debe tener al menos 8 caracteres."
299
+ return False, "errors.validation.password_too_short"
237
300
 
238
301
  if not any(char.isupper() for char in password):
239
- return False, "La contraseña debe tener al menos una letra mayúscula."
302
+ return False, "errors.validation.password_no_uppercase"
240
303
 
241
304
  if not any(char.islower() for char in password):
242
- return False, "La contraseña debe tener al menos una letra minúscula."
305
+ return False, "errors.validation.password_no_lowercase"
243
306
 
244
307
  if not any(char.isdigit() for char in password):
245
- return False, "La contraseña debe tener al menos un número."
308
+ return False, "errors.validation.password_no_digit"
246
309
 
247
310
  if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
248
- return False, "La contraseña debe tener al menos un carácter especial."
311
+ return False, "errors.validation.password_no_special_char"
249
312
 
250
- return True, "La contraseña es válida."
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
- # Producto: IAToolkit
3
- # Todos los derechos reservados.
4
- # En trámite de registro en el Registro de Propiedad Intelectual de Chile.
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
- params: dict = {}
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
- parameters=params
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
- {'prompt': p.name, 'description': p.description, 'order': p.order}
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
  })