iatoolkit 0.3.1__py3-none-any.whl

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

Potentially problematic release.


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

@@ -0,0 +1,376 @@
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 injector import inject
7
+ from repositories.profile_repo import ProfileRepo
8
+ from repositories.models import User, Company, ApiKey
9
+ from flask_bcrypt import check_password_hash
10
+ from common.session_manager import SessionManager
11
+ from flask_bcrypt import Bcrypt
12
+ from infra.mail_app import MailApp
13
+ import random
14
+ import logging
15
+ import re
16
+ import secrets
17
+ import string
18
+ from datetime import datetime, timezone
19
+ from services.user_session_context_service import UserSessionContextService
20
+ from services.query_service import QueryService
21
+
22
+
23
+ class ProfileService:
24
+ @inject
25
+ def __init__(self,
26
+ profile_repo: ProfileRepo,
27
+ session_context_service: UserSessionContextService,
28
+ query_service: QueryService,
29
+ mail_app: MailApp):
30
+ self.profile_repo = profile_repo
31
+ self.session_context = session_context_service
32
+ self.query_service = query_service
33
+ self.mail_app = mail_app
34
+ self.bcrypt = Bcrypt()
35
+
36
+
37
+ def login(self, company_short_name: str, email: str, password: str) -> dict:
38
+ try:
39
+ # check if exits
40
+ user = self.profile_repo.get_user_by_email(email)
41
+ if not user:
42
+ return {"error": "Usuario no encontrado"}
43
+
44
+ # check the encrypted password
45
+ if not check_password_hash(user.password, password):
46
+ return {"error": "Contraseña inválida"}
47
+
48
+ company = self.get_company_by_short_name(company_short_name)
49
+ if not company:
50
+ return {"error": "Empresa no encontrada"}
51
+
52
+ # check that user belongs to company
53
+ if company not in user.companies:
54
+ return {"error": "Usuario no esta autorizado para esta empresa"}
55
+
56
+ if not user.verified:
57
+ return {"error": "Tu cuenta no ha sido verificada. Por favor, revisa tu correo."}
58
+
59
+ # clear history save user data into session manager
60
+ self.set_user_session(user=user, company=company)
61
+
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
+ )
67
+
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
+ "first_name": user.first_name,
83
+ "last_name": user.last_name,
84
+ "super_user": user.super_user,
85
+ "company_id": company.id,
86
+ "company": company.name,
87
+ "company_short_name": company.short_name,
88
+ "company_logo": company.logo_file
89
+ }
90
+ SessionManager.set('user', user_data)
91
+
92
+ # save time session was activated (in timestamp format)
93
+ SessionManager.set('last_activity', datetime.now(timezone.utc).timestamp())
94
+
95
+
96
+ def signup(self,
97
+ company_short_name: str,
98
+ email: str,
99
+ first_name: str,
100
+ last_name: str,
101
+ rut: str,
102
+ password: str,
103
+ confirm_password: str,
104
+ verification_url: str) -> dict:
105
+ try:
106
+
107
+ # get company info
108
+ company = self.get_company_by_short_name(company_short_name)
109
+ if not company:
110
+ return {"error": f"la empresa {company_short_name} no existe"}
111
+
112
+ # normalize format's
113
+ rut = rut.lower().replace(" ", "")
114
+ email = email.lower()
115
+
116
+ # check if user exists
117
+ existing_user = self.profile_repo.get_user_by_email(email)
118
+ if existing_user:
119
+ # validate password
120
+ 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."}
125
+
126
+ # check if register
127
+ if company in existing_user.companies:
128
+ return {"error": "Usuario ya registrado en esta empresa"}
129
+ else:
130
+ # add new company to existing user
131
+ existing_user.companies.append(company)
132
+ self.profile_repo.save_user(existing_user)
133
+ return {"message": "Usuario asociado a nueva empresa"}
134
+
135
+ # add the new user
136
+ if password != confirm_password:
137
+ return {"error": "Las contraseñas no coinciden. Por favor, inténtalo de nuevo."}
138
+
139
+ is_valid, message = self.validate_password(password)
140
+ if not is_valid:
141
+ return {"error": message}
142
+
143
+ # encrypt the password
144
+ hashed_password = self.bcrypt.generate_password_hash(password).decode('utf-8')
145
+
146
+ # create the new user
147
+ new_user = User(email=email,
148
+ rut=rut,
149
+ password=hashed_password,
150
+ first_name=first_name.lower(),
151
+ last_name=last_name.lower(),
152
+ verified=False,
153
+ verification_url=verification_url
154
+ )
155
+
156
+ # associate new company to user
157
+ new_user.companies.append(company)
158
+
159
+ self.profile_repo.create_user(new_user)
160
+
161
+ # send email with verification
162
+ self.send_verification_email(new_user, company_short_name)
163
+
164
+ return {"message": "Registro exitoso. Por favor, revisa tu correo para verificar tu cuenta."}
165
+ except Exception as e:
166
+ return {"error": str(e)}
167
+
168
+ def update_user(self, email: str, **kwargs) -> User:
169
+ return self.profile_repo.update_user(email, **kwargs)
170
+
171
+ def verify_account(self, email: str):
172
+ try:
173
+ # check if user exist
174
+ user = self.profile_repo.get_user_by_email(email)
175
+ if not user:
176
+ return {"error": "El usuario no existe."}
177
+
178
+ # activate the user account
179
+ self.profile_repo.verify_user(email)
180
+ return {"message": "Tu cuenta ha sido verificada exitosamente. Ahora puedes iniciar sesión."}
181
+
182
+ except Exception as e:
183
+ return {"error": str(e)}
184
+
185
+ def change_password(self,
186
+ email: str,
187
+ temp_code: str,
188
+ new_password: str,
189
+ confirm_password: str):
190
+ try:
191
+ if new_password != confirm_password:
192
+ return {"error": "Las contraseñas no coinciden. Por favor, inténtalo nuevamente."}
193
+
194
+ # check the temporary code
195
+ user = self.profile_repo.get_user_by_email(email)
196
+ 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."}
198
+
199
+ # encrypt and save the password, make the temporary code invalid
200
+ hashed_password = self.bcrypt.generate_password_hash(new_password).decode('utf-8')
201
+ self.profile_repo.update_password(email, hashed_password)
202
+ self.profile_repo.reset_temp_code(email)
203
+
204
+ return {"message": "La clave se cambio correctamente"}
205
+ except Exception as e:
206
+ return {"error": str(e)}
207
+
208
+ def forgot_password(self, email: str, reset_url: str):
209
+ try:
210
+ # Verificar si el usuario existe
211
+ user = self.profile_repo.get_user_by_email(email)
212
+ if not user:
213
+ return {"error": "El usuario no existe."}
214
+
215
+ # Gen a temporary code and store in the repositories
216
+ temp_code = ''.join(random.choices(string.ascii_letters + string.digits, k=6)).upper()
217
+ self.profile_repo.set_temp_code(email, temp_code)
218
+
219
+ # send email to the user
220
+ self.send_forgot_password_email(user, reset_url)
221
+
222
+ return {"message": "se envio mail para cambio de clave"}
223
+ except Exception as e:
224
+ return {"error": str(e)}
225
+
226
+ def validate_password(self, password):
227
+ """
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
234
+ """
235
+ if len(password) < 8:
236
+ return False, "La contraseña debe tener al menos 8 caracteres."
237
+
238
+ if not any(char.isupper() for char in password):
239
+ return False, "La contraseña debe tener al menos una letra mayúscula."
240
+
241
+ if not any(char.islower() for char in password):
242
+ return False, "La contraseña debe tener al menos una letra minúscula."
243
+
244
+ if not any(char.isdigit() for char in password):
245
+ return False, "La contraseña debe tener al menos un número."
246
+
247
+ if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
248
+ return False, "La contraseña debe tener al menos un carácter especial."
249
+
250
+ return True, "La contraseña es válida."
251
+
252
+ def get_companies(self):
253
+ return self.profile_repo.get_companies()
254
+
255
+ def get_company_by_short_name(self, short_name: str) -> Company:
256
+ return self.profile_repo.get_company_by_short_name(short_name)
257
+
258
+ def new_api_key(self, company_short_name: str):
259
+ company = self.get_company_by_short_name(company_short_name)
260
+ if not company:
261
+ return {"error": f"la empresa {company_short_name} no existe"}
262
+
263
+ length = 40 # lenght of the api key
264
+ alphabet = string.ascii_letters + string.digits
265
+ key = ''.join(secrets.choice(alphabet) for i in range(length))
266
+
267
+ api_key = ApiKey(key=key, company_id=company.id)
268
+ self.profile_repo.create_api_key(api_key)
269
+ return {"message": f"La nueva clave de API para {company_short_name} es: {key}"}
270
+
271
+
272
+ def send_verification_email(self, new_user: User, company_short_name):
273
+ # send verification account email
274
+ subject = f"Verificación de Cuenta - {company_short_name}"
275
+ body = f"""
276
+ <!DOCTYPE html>
277
+ <html>
278
+ <head>
279
+ <meta charset="UTF-8">
280
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
281
+ <title>Verificación de Cuenta - {company_short_name}</title>
282
+ </head>
283
+ <body style="font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0;">
284
+ <table role="presentation" width="100%" bgcolor="#f4f4f4" cellpadding="0" cellspacing="0" border="0">
285
+ <tr>
286
+ <td align="center">
287
+ <table role="presentation" width="600" bgcolor="#ffffff" cellpadding="20" cellspacing="0" border="0" style="border-radius: 8px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
288
+
289
+ <tr>
290
+ <td style="text-align: left; font-size: 16px; color: #333;">
291
+ <p>Hola <strong>{new_user.first_name}</strong>,</p>
292
+ <p>¡Bienvenido a <strong>{company_short_name}</strong>! Estamos encantados de tenerte con nosotros.</p>
293
+ <p>Para comenzar, verifica tu cuenta haciendo clic en el siguiente botón:</p>
294
+ <p style="text-align: center; margin: 20px 0;">
295
+ <a href="{new_user.verification_url}"
296
+ style="background-color: #007bff; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 5px; font-size: 16px; display: inline-block;">
297
+ Verificar Cuenta
298
+ </a>
299
+ </p>
300
+ <p>Si no puedes hacer clic en el botón, copia y pega el siguiente enlace en tu navegador:</p>
301
+ <p style="word-break: break-word; color: #007bff;">
302
+ <a href="{new_user.verification_url}"
303
+ style="color: #007bff;">
304
+ {new_user.verification_url}
305
+ </a>
306
+ </p>
307
+ <p>Si no creaste una cuenta en {company_short_name}, simplemente ignora este correo.</p>
308
+ <p>¡Gracias por unirte a nuestra comunidad!</p>
309
+ <p style="margin-top: 20px;">Saludos,<br><strong>El equipo de {company_short_name}</strong></p>
310
+ </td>
311
+ </tr>
312
+ </table>
313
+ <p style="font-size: 12px; color: #666; margin-top: 10px;">
314
+ Este es un correo automático, por favor no respondas a este mensaje.
315
+ </p>
316
+ </td>
317
+ </tr>
318
+ </table>
319
+ </body>
320
+ </html>
321
+ """
322
+ self.mail_app.send_email(to=new_user.email, subject=subject, body=body)
323
+
324
+ def send_forgot_password_email(self, user: User, reset_url: str):
325
+ # send email to the user
326
+ subject = f"Recuperación de Contraseña "
327
+ body = f"""
328
+ <!DOCTYPE html>
329
+ <html>
330
+ <head>
331
+ <meta charset="UTF-8">
332
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
333
+ <title>Restablecer Contraseña </title>
334
+ </head>
335
+ <body style="font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0;">
336
+ <table role="presentation" width="100%" bgcolor="#f4f4f4" cellpadding="0" cellspacing="0" border="0">
337
+ <tr>
338
+ <td align="center">
339
+ <table role="presentation" width="600" bgcolor="#ffffff" cellpadding="20" cellspacing="0" border="0" style="border-radius: 8px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
340
+
341
+ <tr>
342
+ <td style="text-align: left; font-size: 16px; color: #333;">
343
+ <p>Hola <strong>{user.first_name}</strong>,</p>
344
+ <p>Hemos recibido una solicitud para restablecer tu contraseña. </p>
345
+ <p>Utiliza el siguiente botón para ingresar tu código temporal y cambiar tu contraseña:</p>
346
+ <p style="text-align: center; margin: 20px 0;">
347
+ <a href="{reset_url}"
348
+ style="background-color: #007bff; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 5px; font-size: 16px; display: inline-block;">
349
+ Restablecer Contraseña
350
+ </a>
351
+ </p>
352
+ <p><strong>Tu código temporal es:</strong></p>
353
+ <p style="font-size: 20px; font-weight: bold; text-align: center; background-color: #f8f9fa; padding: 10px; border-radius: 5px; border: 1px solid #ccc;">
354
+ {user.temp_code}
355
+ </p>
356
+ <p>Si el botón no funciona, también puedes copiar y pegar el siguiente enlace en tu navegador:</p>
357
+ <p style="word-break: break-word; color: #007bff;">
358
+ <a href="{reset_url}" style="color: #007bff;">{reset_url}</a>
359
+ </p>
360
+ <p>Si no solicitaste este cambio, ignora este correo. Tu cuenta permanecerá segura.</p>
361
+ <p style="margin-top: 20px;">Saludos,<br><strong>El equipo de TI</strong></p>
362
+ </td>
363
+ </tr>
364
+ </table>
365
+ <p style="font-size: 12px; color: #666; margin-top: 10px;">
366
+ Este es un correo automático, por favor no respondas a este mensaje.
367
+ </p>
368
+ </td>
369
+ </tr>
370
+ </table>
371
+ </body>
372
+ </html>
373
+ """
374
+
375
+ self.mail_app.send_email(to=user.email, subject=subject, body=body)
376
+ return {"message": "se envio mail para cambio de clave"}
@@ -0,0 +1,180 @@
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 injector import inject
7
+ from repositories.llm_query_repo import LLMQueryRepo
8
+ import logging
9
+ from repositories.profile_repo import ProfileRepo
10
+ from collections import defaultdict
11
+ from repositories.models import Prompt, PromptCategory, Company
12
+ import os
13
+ from common.exceptions import IAToolkitException
14
+
15
+
16
+ class PromptService:
17
+ @inject
18
+ def __init__(self, llm_query_repo: LLMQueryRepo, profile_repo: ProfileRepo):
19
+ self.llm_query_repo = llm_query_repo
20
+ self.profile_repo = profile_repo
21
+
22
+ def create_prompt(self,
23
+ prompt_name: str,
24
+ description: str,
25
+ order: int,
26
+ company: Company = None,
27
+ category: PromptCategory = None,
28
+ active: bool = True,
29
+ is_system_prompt: bool = False,
30
+ params: dict = {}
31
+ ):
32
+
33
+ prompt_filename = prompt_name.lower() + '.prompt'
34
+ if is_system_prompt:
35
+ template_dir = 'prompts'
36
+ else:
37
+ template_dir = f'companies/{company.short_name}/prompts'
38
+
39
+ # Guardar el filepath como una ruta relativa
40
+ relative_prompt_path = os.path.join(template_dir, prompt_filename)
41
+
42
+ # Validar la existencia del archivo usando la ruta absoluta
43
+ absolute_prompt_path = os.path.join(os.getcwd(), relative_prompt_path)
44
+ if not os.path.exists(absolute_prompt_path):
45
+ raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
46
+ f'No existe el archivo de prompt: {absolute_prompt_path}')
47
+
48
+ prompt = Prompt(
49
+ company_id=company.id if company else None,
50
+ name=prompt_name,
51
+ description=description,
52
+ order=order,
53
+ category_id=category.id if category and not is_system_prompt else None,
54
+ active=active,
55
+ filepath=relative_prompt_path,
56
+ is_system_prompt=is_system_prompt,
57
+ parameters=params
58
+ )
59
+
60
+ try:
61
+ self.llm_query_repo.create_or_update_prompt(prompt)
62
+ except Exception as e:
63
+ raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR,
64
+ f'error creating prompt "{prompt_name}": {str(e)}')
65
+
66
+ def get_prompt_content(self, company: Company, prompt_name: str):
67
+ try:
68
+ user_prompt_content = []
69
+ execution_dir = os.getcwd()
70
+
71
+ # get the user prompt
72
+ user_prompt = self.llm_query_repo.get_prompt_by_name(company, prompt_name)
73
+ if not user_prompt:
74
+ raise IAToolkitException(IAToolkitException.ErrorType.DOCUMENT_NOT_FOUND,
75
+ f"No se encontró el prompt '{prompt_name}' para la empresa '{company.short_name}'")
76
+
77
+ absolute_filepath = os.path.join(execution_dir, user_prompt.filepath)
78
+ if not os.path.exists(absolute_filepath):
79
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
80
+ f"El archivo para el prompt '{prompt_name}' no existe: {absolute_filepath}")
81
+
82
+ try:
83
+ with open(absolute_filepath, 'r', encoding='utf-8') as f:
84
+ user_prompt_content = f.read()
85
+ except Exception as e:
86
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
87
+ f"Error leyendo el archivo de prompt '{prompt_name}' en {absolute_filepath}: {e}")
88
+
89
+ return user_prompt_content
90
+
91
+ except IAToolkitException:
92
+ # Vuelve a lanzar las IAToolkitException que ya hemos manejado
93
+ # para que no sean capturadas por el siguiente bloque.
94
+ raise
95
+ except Exception as e:
96
+ logging.exception(
97
+ f"Error al obtener el contenido del prompt para la empresa '{company.short_name}' y prompt '{prompt_name}': {e}")
98
+ raise IAToolkitException(IAToolkitException.ErrorType.PROMPT_ERROR,
99
+ f'Error al obtener el contenido del prompt "{prompt_name}" para la empresa {company.short_name}: {str(e)}')
100
+
101
+ def get_system_prompt(self):
102
+ try:
103
+ system_prompt_content = []
104
+
105
+ # get the filepaths for all system prompts
106
+ current_dir = os.path.dirname(os.path.abspath(__file__))
107
+ src_dir = os.path.dirname(current_dir) # ../src
108
+ system_prompt_dir = os.path.join(src_dir, "prompts")
109
+
110
+ # Obtener, ordenar y leer los system prompts
111
+ system_prompts = self.llm_query_repo.get_system_prompts()
112
+
113
+ for prompt in system_prompts:
114
+ # Construir la ruta absoluta para leer el archivo
115
+ absolute_filepath = os.path.join(system_prompt_dir, prompt.filepath)
116
+ if not os.path.exists(absolute_filepath):
117
+ logging.warning(f"El archivo para el prompt de sistema no existe: {absolute_filepath}")
118
+ continue
119
+ try:
120
+ with open(absolute_filepath, 'r', encoding='utf-8') as f:
121
+ system_prompt_content.append(f.read())
122
+ except Exception as e:
123
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
124
+ f"Error leyendo el archivo de prompt del sistema {absolute_filepath}: {e}")
125
+
126
+ # Unir todo el contenido en un solo string
127
+ return "\n".join(system_prompt_content)
128
+
129
+ except IAToolkitException:
130
+ raise
131
+ except Exception as e:
132
+ logging.exception(
133
+ f"Error al obtener el contenido del prompt de sistema: {e}")
134
+ raise IAToolkitException(IAToolkitException.ErrorType.PROMPT_ERROR,
135
+ f'Error al obtener el contenido de los prompts de sistema": {str(e)}')
136
+
137
+ def get_user_prompts(self, company_short_name: str) -> dict:
138
+ try:
139
+ # validate company
140
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
141
+ if not company:
142
+ return {'error': f'No existe la empresa: {company_short_name}'}
143
+
144
+ # get all the prompts
145
+ all_prompts = self.llm_query_repo.get_prompts(company)
146
+
147
+ # Agrupar prompts por categoría
148
+ prompts_by_category = defaultdict(list)
149
+ for prompt in all_prompts:
150
+ if prompt.active:
151
+ if prompt.category:
152
+ cat_key = (prompt.category.order, prompt.category.name)
153
+ prompts_by_category[cat_key].append(prompt)
154
+
155
+ # Ordenar los prompts dentro de cada categoría
156
+ for cat_key in prompts_by_category:
157
+ prompts_by_category[cat_key].sort(key=lambda p: p.order)
158
+
159
+ # Crear la estructura de respuesta final, ordenada por la categoría
160
+ categorized_prompts = []
161
+
162
+ # Ordenar las categorías por su 'order'
163
+ sorted_categories = sorted(prompts_by_category.items(), key=lambda item: item[0][0])
164
+
165
+ for (cat_order, cat_name), prompts in sorted_categories:
166
+ categorized_prompts.append({
167
+ 'category_name': cat_name,
168
+ 'category_order': cat_order,
169
+ 'prompts': [
170
+ {'prompt': p.name, 'description': p.description, 'order': p.order}
171
+ for p in prompts
172
+ ]
173
+ })
174
+
175
+ return {'message': categorized_prompts}
176
+
177
+ except Exception as e:
178
+ logging.error(f"Error en get_prompts: {e}")
179
+ return {'error': str(e)}
180
+