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.
- iatoolkit/__init__.py +15 -5
- iatoolkit/base_company.py +4 -58
- iatoolkit/cli_commands.py +6 -7
- iatoolkit/common/exceptions.py +1 -0
- iatoolkit/common/routes.py +12 -28
- iatoolkit/common/util.py +7 -1
- iatoolkit/company_registry.py +50 -14
- iatoolkit/{iatoolkit.py → core.py} +54 -55
- iatoolkit/infra/{mail_app.py → brevo_mail_app.py} +15 -37
- iatoolkit/infra/llm_client.py +9 -5
- iatoolkit/locales/en.yaml +10 -2
- iatoolkit/locales/es.yaml +171 -162
- iatoolkit/repositories/database_manager.py +59 -14
- iatoolkit/repositories/llm_query_repo.py +34 -22
- iatoolkit/repositories/models.py +16 -18
- iatoolkit/repositories/profile_repo.py +5 -10
- iatoolkit/repositories/vs_repo.py +9 -4
- iatoolkit/services/auth_service.py +1 -1
- iatoolkit/services/branding_service.py +1 -1
- iatoolkit/services/company_context_service.py +19 -11
- iatoolkit/services/configuration_service.py +219 -46
- iatoolkit/services/dispatcher_service.py +31 -225
- iatoolkit/services/document_service.py +10 -1
- iatoolkit/services/embedding_service.py +9 -6
- iatoolkit/services/excel_service.py +50 -2
- iatoolkit/services/history_manager_service.py +189 -0
- iatoolkit/services/jwt_service.py +1 -1
- iatoolkit/services/language_service.py +8 -2
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/services/mail_service.py +171 -25
- iatoolkit/services/profile_service.py +37 -32
- iatoolkit/services/{prompt_manager_service.py → prompt_service.py} +110 -1
- iatoolkit/services/query_service.py +192 -191
- iatoolkit/services/sql_service.py +63 -12
- iatoolkit/services/tool_service.py +231 -0
- iatoolkit/services/user_feedback_service.py +18 -6
- iatoolkit/services/user_session_context_service.py +18 -0
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +1 -1
- iatoolkit/static/js/chat_help_content.js +4 -4
- iatoolkit/static/js/chat_main.js +17 -5
- iatoolkit/static/js/chat_onboarding_button.js +1 -1
- iatoolkit/static/styles/chat_iatoolkit.css +1 -1
- iatoolkit/static/styles/chat_public.css +28 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +223 -7
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +2 -1
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +30 -5
- iatoolkit/templates/_login_widget.html +3 -3
- iatoolkit/templates/chat.html +1 -1
- iatoolkit/templates/forgot_password.html +3 -2
- iatoolkit/templates/onboarding_shell.html +1 -1
- iatoolkit/templates/signup.html +3 -0
- iatoolkit/views/base_login_view.py +1 -1
- iatoolkit/views/change_password_view.py +1 -1
- iatoolkit/views/forgot_password_view.py +9 -4
- iatoolkit/views/history_api_view.py +3 -3
- iatoolkit/views/home_view.py +4 -2
- iatoolkit/views/init_context_api_view.py +1 -1
- iatoolkit/views/llmquery_api_view.py +4 -3
- iatoolkit/views/{file_store_api_view.py → load_document_api_view.py} +1 -1
- iatoolkit/views/login_view.py +17 -5
- iatoolkit/views/logout_api_view.py +10 -2
- iatoolkit/views/prompt_api_view.py +1 -1
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +12 -4
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/verify_user_view.py +1 -1
- iatoolkit-0.91.1.dist-info/METADATA +268 -0
- iatoolkit-0.91.1.dist-info/RECORD +125 -0
- iatoolkit-0.91.1.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- iatoolkit/services/history_service.py +0 -37
- iatoolkit/templates/about.html +0 -13
- iatoolkit/templates/index.html +0 -145
- iatoolkit/templates/login_simulation.html +0 -45
- iatoolkit/views/external_login_view.py +0 -73
- iatoolkit/views/index_view.py +0 -14
- iatoolkit/views/login_simulation_view.py +0 -93
- iatoolkit-0.71.4.dist-info/METADATA +0 -276
- iatoolkit-0.71.4.dist-info/RECORD +0 -122
- {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/WHEEL +0 -0
- {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/licenses/LICENSE +0 -0
- {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['
|
|
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.
|
|
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.
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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=
|
|
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
|
-
|
|
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":
|
|
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.
|
|
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.
|
|
445
|
-
|
|
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():
|