iatoolkit 0.71.4__py3-none-any.whl → 0.91.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.
Files changed (86) hide show
  1. iatoolkit/__init__.py +15 -5
  2. iatoolkit/base_company.py +4 -58
  3. iatoolkit/cli_commands.py +6 -7
  4. iatoolkit/common/exceptions.py +1 -0
  5. iatoolkit/common/routes.py +12 -28
  6. iatoolkit/common/util.py +7 -1
  7. iatoolkit/company_registry.py +50 -14
  8. iatoolkit/{iatoolkit.py → core.py} +54 -55
  9. iatoolkit/infra/{mail_app.py → brevo_mail_app.py} +15 -37
  10. iatoolkit/infra/llm_client.py +9 -5
  11. iatoolkit/locales/en.yaml +10 -2
  12. iatoolkit/locales/es.yaml +171 -162
  13. iatoolkit/repositories/database_manager.py +59 -14
  14. iatoolkit/repositories/llm_query_repo.py +34 -22
  15. iatoolkit/repositories/models.py +16 -18
  16. iatoolkit/repositories/profile_repo.py +5 -10
  17. iatoolkit/repositories/vs_repo.py +9 -4
  18. iatoolkit/services/auth_service.py +1 -1
  19. iatoolkit/services/branding_service.py +1 -1
  20. iatoolkit/services/company_context_service.py +19 -11
  21. iatoolkit/services/configuration_service.py +219 -46
  22. iatoolkit/services/dispatcher_service.py +31 -225
  23. iatoolkit/services/document_service.py +10 -1
  24. iatoolkit/services/embedding_service.py +9 -6
  25. iatoolkit/services/excel_service.py +50 -2
  26. iatoolkit/services/history_manager_service.py +189 -0
  27. iatoolkit/services/jwt_service.py +1 -1
  28. iatoolkit/services/language_service.py +8 -2
  29. iatoolkit/services/license_service.py +82 -0
  30. iatoolkit/services/mail_service.py +171 -25
  31. iatoolkit/services/profile_service.py +37 -32
  32. iatoolkit/services/{prompt_manager_service.py → prompt_service.py} +110 -1
  33. iatoolkit/services/query_service.py +192 -191
  34. iatoolkit/services/sql_service.py +63 -12
  35. iatoolkit/services/tool_service.py +231 -0
  36. iatoolkit/services/user_feedback_service.py +18 -6
  37. iatoolkit/services/user_session_context_service.py +18 -0
  38. iatoolkit/static/images/iatoolkit_core.png +0 -0
  39. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  40. iatoolkit/static/js/chat_feedback_button.js +1 -1
  41. iatoolkit/static/js/chat_help_content.js +4 -4
  42. iatoolkit/static/js/chat_main.js +17 -5
  43. iatoolkit/static/js/chat_onboarding_button.js +1 -1
  44. iatoolkit/static/styles/chat_iatoolkit.css +1 -1
  45. iatoolkit/static/styles/chat_public.css +28 -0
  46. iatoolkit/static/styles/documents.css +598 -0
  47. iatoolkit/static/styles/landing_page.css +223 -7
  48. iatoolkit/system_prompts/__init__.py +0 -0
  49. iatoolkit/system_prompts/query_main.prompt +2 -1
  50. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  51. iatoolkit/templates/_company_header.html +30 -5
  52. iatoolkit/templates/_login_widget.html +3 -3
  53. iatoolkit/templates/chat.html +1 -1
  54. iatoolkit/templates/forgot_password.html +3 -2
  55. iatoolkit/templates/onboarding_shell.html +1 -1
  56. iatoolkit/templates/signup.html +3 -0
  57. iatoolkit/views/base_login_view.py +1 -1
  58. iatoolkit/views/change_password_view.py +1 -1
  59. iatoolkit/views/forgot_password_view.py +9 -4
  60. iatoolkit/views/history_api_view.py +3 -3
  61. iatoolkit/views/home_view.py +4 -2
  62. iatoolkit/views/init_context_api_view.py +1 -1
  63. iatoolkit/views/llmquery_api_view.py +4 -3
  64. iatoolkit/views/{file_store_api_view.py → load_document_api_view.py} +1 -1
  65. iatoolkit/views/login_view.py +17 -5
  66. iatoolkit/views/logout_api_view.py +10 -2
  67. iatoolkit/views/prompt_api_view.py +1 -1
  68. iatoolkit/views/root_redirect_view.py +22 -0
  69. iatoolkit/views/signup_view.py +12 -4
  70. iatoolkit/views/static_page_view.py +27 -0
  71. iatoolkit/views/verify_user_view.py +1 -1
  72. iatoolkit-0.91.1.dist-info/METADATA +268 -0
  73. iatoolkit-0.91.1.dist-info/RECORD +125 -0
  74. iatoolkit-0.91.1.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  75. iatoolkit/services/history_service.py +0 -37
  76. iatoolkit/templates/about.html +0 -13
  77. iatoolkit/templates/index.html +0 -145
  78. iatoolkit/templates/login_simulation.html +0 -45
  79. iatoolkit/views/external_login_view.py +0 -73
  80. iatoolkit/views/index_view.py +0 -14
  81. iatoolkit/views/login_simulation_view.py +0 -93
  82. iatoolkit-0.71.4.dist-info/METADATA +0 -276
  83. iatoolkit-0.71.4.dist-info/RECORD +0 -122
  84. {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/WHEEL +0 -0
  85. {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/licenses/LICENSE +0 -0
  86. {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/top_level.txt +0 -0
@@ -17,7 +17,7 @@ class JWTService:
17
17
  def __init__(self, app: Flask):
18
18
  # Acceder a la configuración directamente desde app.config
19
19
  try:
20
- self.secret_key = app.config['JWT_SECRET_KEY']
20
+ self.secret_key = app.config['IATOOLKIT_SECRET_KEY']
21
21
  self.algorithm = app.config['JWT_ALGORITHM']
22
22
  except KeyError as e:
23
23
  logging.error(f"missing JWT configuration: {e}.")
@@ -48,6 +48,7 @@ class LanguageService:
48
48
  def get_current_language(self) -> str:
49
49
  """
50
50
  Determines and caches the language for the current request using a priority order:
51
+ 0. Query parameter '?lang=<code>' (highest priority; e.g., 'en', 'es').
51
52
  1. User's preference (from their profile).
52
53
  2. Company's default language.
53
54
  3. System-wide fallback language ('es').
@@ -56,6 +57,12 @@ class LanguageService:
56
57
  return g.lang
57
58
 
58
59
  try:
60
+ # Priority 0: Explicit query parameter (?lang=)
61
+ lang_arg = request.args.get('lang')
62
+ if lang_arg:
63
+ g.lang = lang_arg
64
+ return g.lang
65
+
59
66
  # Priority 1: User's preferred language
60
67
  user_identifier = SessionManager.get('user_identifier')
61
68
  if user_identifier:
@@ -74,10 +81,9 @@ class LanguageService:
74
81
  g.lang = company_language
75
82
  return g.lang
76
83
  except Exception as e:
77
- logging.info(f"Could not determine language, falling back to default. Reason: {e}")
78
84
  pass
79
85
 
80
86
  # Priority 3: System-wide fallback
81
- logging.info(f"Language determined by system fallback: {self.FALLBACK_LANGUAGE}")
87
+ logging.debug(f"Language determined by system fallback: {self.FALLBACK_LANGUAGE}")
82
88
  g.lang = self.FALLBACK_LANGUAGE
83
89
  return g.lang
@@ -0,0 +1,82 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ import jwt
7
+ import os
8
+ import logging
9
+ from pathlib import Path
10
+ from iatoolkit.common.exceptions import IAToolkitException
11
+ from injector import inject, singleton
12
+
13
+
14
+ @singleton
15
+ class LicenseService:
16
+ """
17
+ Manages system restrictions and features based on a license (JWT).
18
+ If no license or an invalid license is provided, Community Edition limits apply.
19
+ """
20
+ @inject
21
+ def __init__(self):
22
+ self.limits = self._load_limits()
23
+
24
+ def _load_limits(self):
25
+ # 1. Define default limits (Community Edition)
26
+ default_limits = {
27
+ "license_type": "Community Edition",
28
+ "plan": "Open Source (Community Edition)",
29
+ "max_companies": 1,
30
+ "max_tools": 3,
31
+ "features": {
32
+ "multi_tenant": False,
33
+ "rag_advanced": False,
34
+ }
35
+ }
36
+ return default_limits
37
+
38
+
39
+ # --- Information Getters ---
40
+ def get_license_type(self) -> str:
41
+ return self.limits.get("license_type", "Community Edition")
42
+
43
+ def get_plan_name(self) -> str:
44
+ return self.limits.get("plan", "Unknown")
45
+
46
+ def get_max_companies(self) -> int:
47
+ return self.limits.get("max_companies", 1)
48
+
49
+ def get_max_tools_per_company(self) -> int:
50
+ return self.limits.get("max_tools", 3)
51
+
52
+ def get_license_info(self) -> str:
53
+ return f"Plan: {self.get_plan_name()}, Companies: {self.get_max_companies()}, Tools: {self.get_max_tools_per_company()}"
54
+
55
+ # --- Restriction Validators ---
56
+
57
+ def validate_company_limit(self, current_count: int):
58
+ """Raises exception if the limit of active companies is exceeded."""
59
+ limit = self.get_max_companies()
60
+ # -1 means unlimited
61
+ if limit != -1 and current_count > limit:
62
+ raise IAToolkitException(
63
+ IAToolkitException.ErrorType.PERMISSION,
64
+ f"Company limit ({limit}) reached for plan '{self.get_plan_name()}'."
65
+ )
66
+
67
+
68
+ def validate_tool_config_limit(self, tools_config: list):
69
+ """Validates a configuration list before processing it."""
70
+ limit = self.get_max_tools_per_company()
71
+ if limit != -1 and len(tools_config) > limit:
72
+ raise IAToolkitException(
73
+ IAToolkitException.ErrorType.PERMISSION,
74
+ f"Configuration defines {len(tools_config)} tools, but limit is {limit}."
75
+ )
76
+
77
+ # --- Feature Gating Validators ---
78
+
79
+ def has_feature(self, feature_key: str) -> bool:
80
+ """Checks if a specific feature is enabled in the license."""
81
+ features = self.limits.get("features", {})
82
+ return features.get(feature_key, False)
@@ -3,43 +3,40 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from iatoolkit.infra.mail_app import MailApp
6
+ from iatoolkit.services.configuration_service import ConfigurationService
7
7
  from iatoolkit.services.i18n_service import I18nService
8
+ from iatoolkit.infra.brevo_mail_app import BrevoMailApp
8
9
  from injector import inject
9
10
  from pathlib import Path
10
- from iatoolkit.common.exceptions import IAToolkitException
11
11
  import base64
12
+ import os
13
+ import smtplib
14
+ from email.message import EmailMessage
15
+ from iatoolkit.common.exceptions import IAToolkitException
16
+
12
17
 
13
18
  TEMP_DIR = Path("static/temp")
14
19
 
15
20
  class MailService:
16
21
  @inject
17
22
  def __init__(self,
18
- mail_app: MailApp,
19
- i18n_service: I18nService):
23
+ config_service: ConfigurationService,
24
+ mail_app: BrevoMailApp,
25
+ i18n_service: I18nService,
26
+ brevo_mail_app: BrevoMailApp):
20
27
  self.mail_app = mail_app
28
+ self.config_service = config_service
21
29
  self.i18n_service = i18n_service
30
+ self.brevo_mail_app = brevo_mail_app
22
31
 
23
32
 
24
- def _read_token_bytes(self, token: str) -> bytes:
25
- # Defensa simple contra path traversal
26
- if not token or "/" in token or "\\" in token or token.startswith("."):
27
- raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
28
- "attachment_token invalid")
29
- path = TEMP_DIR / token
30
- if not path.is_file():
31
- raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
32
- f"attach file not found: {token}")
33
- return path.read_bytes()
34
-
35
- def send_mail(self, **kwargs):
36
- from_email = kwargs.get('from_email', 'iatoolkit@iatoolkit.com')
33
+ def send_mail(self, company_short_name: str, **kwargs):
37
34
  recipient = kwargs.get('recipient')
38
35
  subject = kwargs.get('subject')
39
36
  body = kwargs.get('body')
40
37
  attachments = kwargs.get('attachments')
41
38
 
42
- # Normalizar a payload de MailApp (name + base64 content)
39
+ # Normalizar a payload de BrevoMailApp (name + base64 content)
43
40
  norm_attachments = []
44
41
  for a in attachments or []:
45
42
  if a.get("attachment_token"):
@@ -55,13 +52,162 @@ class MailService:
55
52
  "content": a["content"]
56
53
  })
57
54
 
58
- self.sender = {"email": from_email, "name": "IAToolkit"}
55
+ # build provider configuration from company.yaml
56
+ provider, provider_config = self._build_provider_config(company_short_name)
57
+
58
+ # define the email sender
59
+ sender = {
60
+ "email": provider_config.get("sender_email"),
61
+ "name": provider_config.get("sender_name"),
62
+ }
59
63
 
60
- response = self.mail_app.send_email(
61
- sender=self.sender,
62
- to=recipient,
63
- subject=subject,
64
- body=body,
65
- attachments=norm_attachments)
64
+ # select provider and send the email through it
65
+ if provider == "brevo_mail":
66
+ response = self.brevo_mail_app.send_email(
67
+ provider_config=provider_config,
68
+ sender=sender,
69
+ to=recipient,
70
+ subject=subject,
71
+ body=body,
72
+ attachments=norm_attachments
73
+ )
74
+ elif provider == "smtplib":
75
+ response = self._send_with_smtplib(
76
+ provider_config=provider_config,
77
+ sender=sender,
78
+ recipient=recipient,
79
+ subject=subject,
80
+ body=body,
81
+ attachments=norm_attachments,
82
+ )
83
+ response = None
84
+ else:
85
+ raise IAToolkitException(
86
+ IAToolkitException.ErrorType.MAIL_ERROR,
87
+ f"Unknown mail provider '{provider}'"
88
+ )
66
89
 
67
90
  return self.i18n_service.t('services.mail_sent')
91
+
92
+ def _build_provider_config(self, company_short_name: str) -> tuple[str, dict]:
93
+ """
94
+ Determina el provider activo (brevo_mail / smtplib) y construye
95
+ el diccionario de configuración a partir de las variables de entorno
96
+ cuyos nombres están en company.yaml (mail_provider).
97
+ """
98
+ # get company mail configuration and provider
99
+ mail_config = self.config_service.get_configuration(company_short_name, "mail_provider")
100
+ provider = mail_config.get("provider", "brevo_mail")
101
+
102
+ # get mail common parameteres
103
+ sender_email = mail_config.get("sender_email")
104
+ sender_name = mail_config.get("sender_name")
105
+
106
+ # get parameters depending on provider
107
+ if provider == "brevo_mail":
108
+ brevo_cfg = mail_config.get("brevo_mail", {})
109
+ api_key_env = brevo_cfg.get("brevo_api", "BREVO_API_KEY")
110
+ return provider, {
111
+ "api_key": os.getenv(api_key_env),
112
+ "sender_name": sender_name,
113
+ "sender_email": sender_email,
114
+ }
115
+
116
+ if provider == "smtplib":
117
+ smtp_cfg = mail_config.get("smtplib", {})
118
+ host = os.getenv(smtp_cfg.get("host_env", "SMTP_HOST"))
119
+ port = os.getenv(smtp_cfg.get("port_env", "SMTP_PORT"))
120
+ username = os.getenv(smtp_cfg.get("username_env", "SMTP_USERNAME"))
121
+ password = os.getenv(smtp_cfg.get("password_env", "SMTP_PASSWORD"))
122
+ use_tls = os.getenv(smtp_cfg.get("use_tls_env", "SMTP_USE_TLS"))
123
+ use_ssl = os.getenv(smtp_cfg.get("use_ssl_env", "SMTP_USE_SSL"))
124
+
125
+ return provider, {
126
+ "host": host,
127
+ "port": int(port) if port is not None else None,
128
+ "username": username,
129
+ "password": password,
130
+ "use_tls": str(use_tls).lower() == "true",
131
+ "use_ssl": str(use_ssl).lower() == "true",
132
+ "sender_name": sender_name,
133
+ "sender_email": sender_email,
134
+ }
135
+
136
+ # Fallback simple si el provider no es reconocido
137
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
138
+ f"missing mail provider in mail configuration for company '{company_short_name}'")
139
+
140
+ def _send_with_smtplib(self,
141
+ provider_config: dict,
142
+ sender: dict,
143
+ recipient: str,
144
+ subject: str,
145
+ body: str,
146
+ attachments: list[dict] | None):
147
+ """
148
+ Envía correo usando smtplib, utilizando la configuración normalizada
149
+ en provider_config.
150
+ """
151
+ host = provider_config.get("host")
152
+ port = provider_config.get("port")
153
+ username = provider_config.get("username")
154
+ password = provider_config.get("password")
155
+ use_tls = provider_config.get("use_tls")
156
+ use_ssl = provider_config.get("use_ssl")
157
+
158
+ if not host or not port:
159
+ raise IAToolkitException(
160
+ IAToolkitException.ErrorType.MAIL_ERROR,
161
+ "smtplib configuration is incomplete (host/port missing)"
162
+ )
163
+
164
+ msg = EmailMessage()
165
+ msg["From"] = f"{sender.get('name', '')} <{sender.get('email')}>"
166
+ msg["To"] = recipient
167
+ msg["Subject"] = subject
168
+ msg.set_content(body, subtype="html")
169
+
170
+ # Adjuntos: ya vienen como filename + base64 content
171
+ for a in attachments or []:
172
+ filename = a.get("filename")
173
+ content_b64 = a.get("content")
174
+ if not filename or not content_b64:
175
+ continue
176
+ try:
177
+ raw = base64.b64decode(content_b64, validate=True)
178
+ except Exception:
179
+ raise IAToolkitException(
180
+ IAToolkitException.ErrorType.MAIL_ERROR,
181
+ f"Invalid base64 for attachment '{filename}'"
182
+ )
183
+ msg.add_attachment(
184
+ raw,
185
+ maintype="application",
186
+ subtype="octet-stream",
187
+ filename=filename,
188
+ )
189
+
190
+ if use_ssl:
191
+ with smtplib.SMTP_SSL(host, port) as server:
192
+ if username and password:
193
+ server.login(username, password)
194
+ server.send_message(msg)
195
+ else:
196
+ with smtplib.SMTP(host, port) as server:
197
+ if use_tls:
198
+ server.starttls()
199
+ if username and password:
200
+ server.login(username, password)
201
+ server.send_message(msg)
202
+
203
+
204
+ def _read_token_bytes(self, token: str) -> bytes:
205
+ # Defensa simple contra path traversal
206
+ if not token or "/" in token or "\\" in token or token.startswith("."):
207
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
208
+ "attachment_token invalid")
209
+ path = TEMP_DIR / token
210
+ if not path.is_file():
211
+ raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
212
+ f"attach file not found: {token}")
213
+ return path.read_bytes()
@@ -9,9 +9,11 @@ from iatoolkit.services.i18n_service import I18nService
9
9
  from iatoolkit.repositories.models import User, Company, ApiKey
10
10
  from flask_bcrypt import check_password_hash
11
11
  from iatoolkit.common.session_manager import SessionManager
12
+ from iatoolkit.services.language_service import LanguageService
12
13
  from iatoolkit.services.user_session_context_service import UserSessionContextService
14
+ from iatoolkit.services.configuration_service import ConfigurationService
13
15
  from flask_bcrypt import Bcrypt
14
- from iatoolkit.infra.mail_app import MailApp
16
+ from iatoolkit.services.mail_service import MailService
15
17
  import random
16
18
  import re
17
19
  import secrets
@@ -26,13 +28,17 @@ class ProfileService:
26
28
  i18n_service: I18nService,
27
29
  profile_repo: ProfileRepo,
28
30
  session_context_service: UserSessionContextService,
31
+ config_service: ConfigurationService,
32
+ lang_service: LanguageService,
29
33
  dispatcher: Dispatcher,
30
- mail_app: MailApp):
34
+ mail_service: MailService):
31
35
  self.i18n_service = i18n_service
32
36
  self.profile_repo = profile_repo
33
37
  self.dispatcher = dispatcher
34
38
  self.session_context = session_context_service
35
- self.mail_app = mail_app
39
+ self.config_service = config_service
40
+ self.lang_service = lang_service
41
+ self.mail_service = mail_service
36
42
  self.bcrypt = Bcrypt()
37
43
 
38
44
 
@@ -61,11 +67,12 @@ class ProfileService:
61
67
 
62
68
  # 1. Build the local user profile dictionary here.
63
69
  # 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
70
+ user_identifier = user.email
65
71
  user_profile = {
66
72
  "user_email": user.email,
67
73
  "user_fullname": f'{user.first_name} {user.last_name}',
68
74
  "user_is_local": True,
75
+ "user_id": user.id,
69
76
  "extras": {}
70
77
  }
71
78
 
@@ -79,25 +86,6 @@ class ProfileService:
79
86
  logging.error(f"Error in login: {e}")
80
87
  return {'success': False, "message": str(e)}
81
88
 
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
- # 3. make sure the flask session is clean
99
- SessionManager.clear()
100
-
101
89
  def save_user_profile(self, company: Company, user_identifier: str, user_profile: dict):
102
90
  """
103
91
  Private helper: Takes a pre-built profile, saves it to Redis, and sets the Flask cookie.
@@ -107,6 +95,7 @@ class ProfileService:
107
95
  user_profile['id'] = user_identifier
108
96
  user_profile['company_id'] = company.id
109
97
  user_profile['company'] = company.name
98
+ user_profile['language'] = self.lang_service.get_current_language()
110
99
 
111
100
  # save user_profile in Redis session
112
101
  self.session_context.save_profile_data(company.short_name, user_identifier, user_profile)
@@ -214,24 +203,34 @@ class ProfileService:
214
203
  # encrypt the password
215
204
  hashed_password = self.bcrypt.generate_password_hash(password).decode('utf-8')
216
205
 
206
+ # account verification can be skiped with this security parameter
207
+ verified = False
208
+ cfg = self.config_service.get_configuration(company_short_name, 'parameters')
209
+ if cfg and not cfg.get('verify_account', True):
210
+ verified = True
211
+ message = self.i18n_service.t('flash_messages.signup_success_no_verification')
212
+
217
213
  # create the new user
218
214
  new_user = User(email=email,
219
215
  password=hashed_password,
220
216
  first_name=first_name.lower(),
221
217
  last_name=last_name.lower(),
222
- verified=False,
218
+ verified=verified,
223
219
  verification_url=verification_url
224
220
  )
225
221
 
226
222
  # associate new company to user
227
223
  new_user.companies.append(company)
228
224
 
225
+ # and create in the database
229
226
  self.profile_repo.create_user(new_user)
230
227
 
231
228
  # send email with verification
232
- self.send_verification_email(new_user, company_short_name)
229
+ if not cfg or cfg.get('verify_account', True):
230
+ self.send_verification_email(new_user, company_short_name)
231
+ message = self.i18n_service.t('flash_messages.signup_success')
233
232
 
234
- return {"message": self.i18n_service.t('flash_messages.signup_success')}
233
+ return {"message": message}
235
234
  except Exception as e:
236
235
  return {"error": self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
237
236
 
@@ -275,7 +274,7 @@ class ProfileService:
275
274
  except Exception as e:
276
275
  return {"error": self.i18n_service.t('errors.general.unexpected_error')}
277
276
 
278
- def forgot_password(self, email: str, reset_url: str):
277
+ def forgot_password(self, company_short_name: str, email: str, reset_url: str):
279
278
  try:
280
279
  # Verificar si el usuario existe
281
280
  user = self.profile_repo.get_user_by_email(email)
@@ -287,7 +286,7 @@ class ProfileService:
287
286
  self.profile_repo.set_temp_code(email, temp_code)
288
287
 
289
288
  # send email to the user
290
- self.send_forgot_password_email(user, reset_url)
289
+ self.send_forgot_password_email(company_short_name, user, reset_url)
291
290
 
292
291
  return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
293
292
  except Exception as e:
@@ -388,9 +387,12 @@ class ProfileService:
388
387
  </body>
389
388
  </html>
390
389
  """
391
- self.mail_app.send_email(to=new_user.email, subject=subject, body=body)
390
+ self.mail_service.send_mail(company_short_name=company_short_name,
391
+ recipient=new_user.email,
392
+ subject=subject,
393
+ body=body)
392
394
 
393
- def send_forgot_password_email(self, user: User, reset_url: str):
395
+ def send_forgot_password_email(self, company_short_name: str, user: User, reset_url: str):
394
396
  # send email to the user
395
397
  subject = f"Recuperación de Contraseña "
396
398
  body = f"""
@@ -441,5 +443,8 @@ class ProfileService:
441
443
  </html>
442
444
  """
443
445
 
444
- self.mail_app.send_email(to=user.email, subject=subject, body=body)
445
- return {"message": "se envio mail para cambio de clave"}
446
+ self.mail_service.send_mail(company_short_name=company_short_name,
447
+ recipient=user.email,
448
+ subject=subject,
449
+ body=body)
450
+ return {"message": self.i18n_service.t('services.mail_change_password') }
@@ -14,6 +14,12 @@ from iatoolkit.common.exceptions import IAToolkitException
14
14
  import importlib.resources
15
15
  import logging
16
16
 
17
+ # iatoolkit system prompts definitions
18
+ _SYSTEM_PROMPTS = [
19
+ {'name': 'query_main', 'description': 'iatoolkit main prompt'},
20
+ {'name': 'format_styles', 'description': 'output format styles'},
21
+ {'name': 'sql_rules', 'description': 'instructions for SQL queries'}
22
+ ]
17
23
 
18
24
  class PromptService:
19
25
  @inject
@@ -25,6 +31,106 @@ class PromptService:
25
31
  self.profile_repo = profile_repo
26
32
  self.i18n_service = i18n_service
27
33
 
34
+ def sync_company_prompts(self, company_instance, prompts_config: list, categories_config: list):
35
+ """
36
+ Synchronizes prompt categories and prompts from YAML config to Database.
37
+ Strategies:
38
+ - Categories: Create or Update existing based on name.
39
+ - Prompts: Create or Update existing based on name. Soft-delete or Delete unused.
40
+ """
41
+ try:
42
+ # 1. Sync Categories
43
+ category_map = {}
44
+
45
+ for i, category_name in enumerate(categories_config):
46
+ category_obj = PromptCategory(
47
+ company_id=company_instance.company.id,
48
+ name=category_name,
49
+ order=i + 1
50
+ )
51
+ # Persist and get back the object with ID
52
+ persisted_cat = self.llm_query_repo.create_or_update_prompt_category(category_obj)
53
+ category_map[category_name] = persisted_cat
54
+
55
+ # 2. Sync Prompts
56
+ defined_prompt_names = set()
57
+
58
+ for prompt_data in prompts_config:
59
+ category_name = prompt_data.get('category')
60
+ if not category_name or category_name not in category_map:
61
+ logging.warning(
62
+ f"⚠️ Warning: Prompt '{prompt_data['name']}' has an invalid or missing category. Skipping.")
63
+ continue
64
+
65
+ prompt_name = prompt_data['name']
66
+ defined_prompt_names.add(prompt_name)
67
+
68
+ category_obj = category_map[category_name]
69
+ filename = f"{prompt_name}.prompt"
70
+
71
+ new_prompt = Prompt(
72
+ company_id=company_instance.company.id,
73
+ name=prompt_name,
74
+ description=prompt_data['description'],
75
+ order=prompt_data['order'],
76
+ category_id=category_obj.id,
77
+ active=prompt_data.get('active', True),
78
+ is_system_prompt=False,
79
+ filename=filename,
80
+ custom_fields=prompt_data.get('custom_fields', [])
81
+ )
82
+
83
+ self.llm_query_repo.create_or_update_prompt(new_prompt)
84
+
85
+ # 3. Cleanup: Delete prompts present in DB but not in Config
86
+ existing_prompts = self.llm_query_repo.get_prompts(company_instance.company)
87
+ for p in existing_prompts:
88
+ if p.name not in defined_prompt_names:
89
+ # Using hard delete to keep consistent with previous "refresh" behavior
90
+ self.llm_query_repo.session.delete(p)
91
+
92
+ self.llm_query_repo.commit()
93
+
94
+ except Exception as e:
95
+ self.llm_query_repo.rollback()
96
+ raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR, str(e))
97
+
98
+ def register_system_prompts(self):
99
+ """
100
+ Synchronizes system prompts defined in Dispatcher/Code to Database.
101
+ """
102
+ try:
103
+ defined_names = set()
104
+
105
+ for i, prompt_data in enumerate(_SYSTEM_PROMPTS):
106
+ prompt_name = prompt_data['name']
107
+ defined_names.add(prompt_name)
108
+
109
+ new_prompt = Prompt(
110
+ company_id=None, # System prompts have no company
111
+ name=prompt_name,
112
+ description=prompt_data['description'],
113
+ order=i + 1,
114
+ category_id=None,
115
+ active=True,
116
+ is_system_prompt=True,
117
+ filename=f"{prompt_name}.prompt",
118
+ custom_fields=[]
119
+ )
120
+ self.llm_query_repo.create_or_update_prompt(new_prompt)
121
+
122
+ # Cleanup old system prompts
123
+ existing_sys_prompts = self.llm_query_repo.get_system_prompts()
124
+ for p in existing_sys_prompts:
125
+ if p.name not in defined_names:
126
+ self.llm_query_repo.session.delete(p)
127
+
128
+ self.llm_query_repo.commit()
129
+
130
+ except Exception as e:
131
+ self.llm_query_repo.rollback()
132
+ raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR, str(e))
133
+
28
134
  def create_prompt(self,
29
135
  prompt_name: str,
30
136
  description: str,
@@ -35,7 +141,10 @@ class PromptService:
35
141
  is_system_prompt: bool = False,
36
142
  custom_fields: list = []
37
143
  ):
38
-
144
+ """
145
+ Direct creation method (used by sync or direct calls).
146
+ Validates file existence before creating DB entry.
147
+ """
39
148
  prompt_filename = prompt_name.lower() + '.prompt'
40
149
  if is_system_prompt:
41
150
  if not importlib.resources.files('iatoolkit.system_prompts').joinpath(prompt_filename).is_file():