iatoolkit 0.63.1__py3-none-any.whl → 0.69.0__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.
- iatoolkit/__init__.py +0 -2
- iatoolkit/base_company.py +1 -26
- iatoolkit/common/routes.py +11 -2
- iatoolkit/common/session_manager.py +2 -0
- iatoolkit/common/util.py +17 -0
- iatoolkit/company_registry.py +1 -2
- iatoolkit/iatoolkit.py +39 -6
- iatoolkit/locales/en.yaml +167 -0
- iatoolkit/locales/es.yaml +163 -0
- iatoolkit/repositories/database_manager.py +8 -3
- iatoolkit/repositories/document_repo.py +1 -1
- iatoolkit/repositories/models.py +1 -4
- iatoolkit/repositories/profile_repo.py +0 -4
- iatoolkit/services/auth_service.py +14 -9
- iatoolkit/services/branding_service.py +36 -24
- iatoolkit/services/company_context_service.py +145 -0
- iatoolkit/services/configuration_service.py +133 -0
- iatoolkit/services/dispatcher_service.py +51 -48
- iatoolkit/services/document_service.py +5 -2
- iatoolkit/services/excel_service.py +15 -11
- iatoolkit/services/file_processor_service.py +4 -12
- iatoolkit/services/history_service.py +8 -7
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +7 -9
- iatoolkit/services/language_service.py +83 -0
- iatoolkit/services/load_documents_service.py +4 -4
- iatoolkit/services/mail_service.py +9 -4
- iatoolkit/services/profile_service.py +61 -38
- iatoolkit/services/prompt_manager_service.py +20 -16
- iatoolkit/services/query_service.py +19 -15
- iatoolkit/services/search_service.py +11 -4
- iatoolkit/services/sql_service.py +55 -25
- iatoolkit/services/user_feedback_service.py +16 -14
- iatoolkit/static/js/chat_feedback_button.js +57 -87
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +48 -65
- iatoolkit/static/js/chat_main.js +27 -24
- iatoolkit/static/js/chat_onboarding_button.js +6 -0
- iatoolkit/static/js/chat_reload_button.js +28 -45
- iatoolkit/static/styles/chat_iatoolkit.css +223 -315
- iatoolkit/static/styles/chat_modal.css +63 -97
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +0 -1
- iatoolkit/static/styles/onboarding.css +7 -0
- iatoolkit/templates/_company_header.html +6 -2
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +34 -19
- iatoolkit/templates/change_password.html +22 -20
- iatoolkit/templates/chat.html +59 -27
- iatoolkit/templates/chat_modals.html +114 -74
- iatoolkit/templates/error.html +12 -13
- iatoolkit/templates/forgot_password.html +11 -7
- iatoolkit/templates/index.html +8 -3
- iatoolkit/templates/login_simulation.html +17 -6
- iatoolkit/templates/onboarding_shell.html +4 -2
- iatoolkit/templates/signup.html +14 -14
- iatoolkit/views/base_login_view.py +19 -9
- iatoolkit/views/change_password_view.py +50 -35
- iatoolkit/views/external_login_view.py +1 -1
- iatoolkit/views/forgot_password_view.py +21 -22
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +13 -9
- iatoolkit/views/home_view.py +30 -39
- iatoolkit/views/init_context_api_view.py +16 -11
- iatoolkit/views/llmquery_api_view.py +38 -26
- iatoolkit/views/login_simulation_view.py +14 -2
- iatoolkit/views/login_view.py +52 -40
- iatoolkit/views/logout_api_view.py +26 -22
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +6 -6
- iatoolkit/views/signup_view.py +27 -27
- iatoolkit/views/user_feedback_api_view.py +19 -18
- iatoolkit/views/verify_user_view.py +29 -30
- {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/METADATA +40 -22
- iatoolkit-0.69.0.dist-info/RECORD +120 -0
- iatoolkit-0.69.0.dist-info/licenses/LICENSE +21 -0
- iatoolkit/services/onboarding_service.py +0 -43
- iatoolkit/static/styles/chat_info.css +0 -53
- iatoolkit/templates/header.html +0 -31
- iatoolkit/templates/test.html +0 -9
- iatoolkit-0.63.1.dist-info/RECORD +0 -112
- {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/WHEEL +0 -0
- {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/top_level.txt +0 -0
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
from iatoolkit.common.exceptions import IAToolkitException
|
|
7
7
|
from iatoolkit.services.prompt_manager_service import PromptService
|
|
8
|
+
from iatoolkit.services.sql_service import SqlService
|
|
8
9
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
9
|
-
|
|
10
|
+
from iatoolkit.services.configuration_service import ConfigurationService
|
|
10
11
|
from iatoolkit.repositories.models import Company, Function
|
|
11
12
|
from iatoolkit.services.excel_service import ExcelService
|
|
12
13
|
from iatoolkit.services.mail_service import MailService
|
|
@@ -19,14 +20,18 @@ import os
|
|
|
19
20
|
class Dispatcher:
|
|
20
21
|
@inject
|
|
21
22
|
def __init__(self,
|
|
23
|
+
config_service: ConfigurationService,
|
|
22
24
|
prompt_service: PromptService,
|
|
23
25
|
llmquery_repo: LLMQueryRepo,
|
|
24
26
|
util: Utility,
|
|
27
|
+
sql_service: SqlService,
|
|
25
28
|
excel_service: ExcelService,
|
|
26
29
|
mail_service: MailService):
|
|
30
|
+
self.config_service = config_service
|
|
27
31
|
self.prompt_service = prompt_service
|
|
28
32
|
self.llmquery_repo = llmquery_repo
|
|
29
33
|
self.util = util
|
|
34
|
+
self.sql_service = sql_service
|
|
30
35
|
self.excel_service = excel_service
|
|
31
36
|
self.mail_service = mail_service
|
|
32
37
|
self.system_functions = _FUNCTION_LIST
|
|
@@ -55,17 +60,50 @@ class Dispatcher:
|
|
|
55
60
|
self._company_instances = self.company_registry.get_all_company_instances()
|
|
56
61
|
return self._company_instances
|
|
57
62
|
|
|
58
|
-
def
|
|
63
|
+
def load_company_configs(self):
|
|
59
64
|
# initialize the system functions and prompts
|
|
60
65
|
self.setup_iatoolkit_system()
|
|
61
66
|
|
|
62
|
-
"""
|
|
63
|
-
for
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
"""Loads the configuration of every company"""
|
|
68
|
+
for company_name, company_instance in self.company_instances.items():
|
|
69
|
+
try:
|
|
70
|
+
# read company configuration from company.yaml
|
|
71
|
+
self.config_service.load_configuration(company_name, company_instance)
|
|
72
|
+
|
|
73
|
+
# register the company databases
|
|
74
|
+
self._register_company_databases(company_name)
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logging.error(f"❌ Failed to register configuration for '{company_name}': {e}")
|
|
78
|
+
continue
|
|
66
79
|
|
|
67
80
|
return True
|
|
68
81
|
|
|
82
|
+
def _register_company_databases(self, company_name: str):
|
|
83
|
+
"""
|
|
84
|
+
Reads the data_sources config for a company and registers each
|
|
85
|
+
database with the central SqlService.
|
|
86
|
+
"""
|
|
87
|
+
logging.info(f" -> Registering databases for '{company_name}'...")
|
|
88
|
+
data_sources_config = self.config_service.get_configuration(company_name, 'data_sources')
|
|
89
|
+
|
|
90
|
+
if not data_sources_config or not data_sources_config.get('sql'):
|
|
91
|
+
logging.info(f" -> No SQL data sources to register for '{company_name}'.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
for db_config in data_sources_config['sql']:
|
|
95
|
+
db_name = db_config.get('database')
|
|
96
|
+
db_env_var = db_config.get('connection_string_env')
|
|
97
|
+
|
|
98
|
+
# resolve the URI connection string from the environment variable
|
|
99
|
+
db_uri = os.getenv(db_env_var) if db_env_var else None
|
|
100
|
+
if not db_uri:
|
|
101
|
+
logging.warning(
|
|
102
|
+
f"-> Skipping database registration for '{company_name}' due to missing 'database' name or connection URI.")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
self.sql_service.register_database(db_name, db_uri)
|
|
106
|
+
|
|
69
107
|
def setup_iatoolkit_system(self):
|
|
70
108
|
# create system functions
|
|
71
109
|
for function in self.system_functions:
|
|
@@ -90,9 +128,6 @@ class Dispatcher:
|
|
|
90
128
|
)
|
|
91
129
|
i += 1
|
|
92
130
|
|
|
93
|
-
# register in the database every company class
|
|
94
|
-
for company in self.company_instances.values():
|
|
95
|
-
company.register_company()
|
|
96
131
|
|
|
97
132
|
def dispatch(self, company_name: str, action: str, **kwargs) -> dict:
|
|
98
133
|
company_key = company_name.lower()
|
|
@@ -120,37 +155,6 @@ class Dispatcher:
|
|
|
120
155
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
121
156
|
f"Error en function call '{action}': {str(e)}") from e
|
|
122
157
|
|
|
123
|
-
def get_company_context(self, company_name: str, **kwargs) -> str:
|
|
124
|
-
if company_name not in self.company_instances:
|
|
125
|
-
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
126
|
-
f"Empresa no configurada: {company_name}")
|
|
127
|
-
|
|
128
|
-
company_context = ''
|
|
129
|
-
|
|
130
|
-
# read the company context from this list of markdown files,
|
|
131
|
-
# company brief, credits, operation description, etc.
|
|
132
|
-
context_dir = os.path.join(os.getcwd(), f'companies/{company_name}/context')
|
|
133
|
-
context_files = self.util.get_files_by_extension(context_dir, '.md', return_extension=True)
|
|
134
|
-
for file in context_files:
|
|
135
|
-
filepath = os.path.join(context_dir, file)
|
|
136
|
-
company_context += self.util.load_markdown_context(filepath)
|
|
137
|
-
|
|
138
|
-
# add the schemas for every table or function call responses
|
|
139
|
-
schema_dir = os.path.join(os.getcwd(), f'companies/{company_name}/schema')
|
|
140
|
-
schema_files = self.util.get_files_by_extension(schema_dir, '.yaml', return_extension=True)
|
|
141
|
-
for file in schema_files:
|
|
142
|
-
schema_name = file.split('_')[0]
|
|
143
|
-
filepath = os.path.join(schema_dir, file)
|
|
144
|
-
company_context += self.util.generate_context_for_schema(schema_name, filepath)
|
|
145
|
-
|
|
146
|
-
company_instance = self.company_instances[company_name]
|
|
147
|
-
try:
|
|
148
|
-
return company_context + company_instance.get_company_context(**kwargs)
|
|
149
|
-
except Exception as e:
|
|
150
|
-
logging.exception(e)
|
|
151
|
-
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
152
|
-
f"Error en get_company_context de {company_name}: {str(e)}") from e
|
|
153
|
-
|
|
154
158
|
def get_company_services(self, company: Company) -> list[dict]:
|
|
155
159
|
# create the syntax with openai response syntax, for the company function list
|
|
156
160
|
tools = []
|
|
@@ -173,7 +177,7 @@ class Dispatcher:
|
|
|
173
177
|
def get_user_info(self, company_name: str, user_identifier: str) -> dict:
|
|
174
178
|
if company_name not in self.company_instances:
|
|
175
179
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
176
|
-
f"
|
|
180
|
+
f"company not configured: {company_name}")
|
|
177
181
|
|
|
178
182
|
# source 2: external company user
|
|
179
183
|
company_instance = self.company_instances[company_name]
|
|
@@ -182,14 +186,14 @@ class Dispatcher:
|
|
|
182
186
|
except Exception as e:
|
|
183
187
|
logging.exception(e)
|
|
184
188
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
185
|
-
f"Error
|
|
189
|
+
f"Error in get_user_info: {company_name}: {str(e)}") from e
|
|
186
190
|
|
|
187
191
|
return external_user_profile
|
|
188
192
|
|
|
189
193
|
def get_metadata_from_filename(self, company_name: str, filename: str) -> dict:
|
|
190
194
|
if company_name not in self.company_instances:
|
|
191
195
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
192
|
-
f"
|
|
196
|
+
f"company not configured: {company_name}")
|
|
193
197
|
|
|
194
198
|
company_instance = self.company_instances[company_name]
|
|
195
199
|
try:
|
|
@@ -197,7 +201,7 @@ class Dispatcher:
|
|
|
197
201
|
except Exception as e:
|
|
198
202
|
logging.exception(e)
|
|
199
203
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
200
|
-
f"Error
|
|
204
|
+
f"Error in get_metadata_from_filename: {company_name}: {str(e)}") from e
|
|
201
205
|
|
|
202
206
|
def get_company_instance(self, company_name: str):
|
|
203
207
|
"""Returns the instance for a given company name."""
|
|
@@ -207,12 +211,11 @@ class Dispatcher:
|
|
|
207
211
|
|
|
208
212
|
# iatoolkit system prompts
|
|
209
213
|
_SYSTEM_PROMPT = [
|
|
210
|
-
{'name': 'query_main', 'description':'main prompt
|
|
211
|
-
{'name': 'format_styles', 'description':'
|
|
212
|
-
{'name': 'sql_rules', 'description':'
|
|
214
|
+
{'name': 'query_main', 'description':'iatoolkit main prompt'},
|
|
215
|
+
{'name': 'format_styles', 'description':'output format styles'},
|
|
216
|
+
{'name': 'sql_rules', 'description':'instructions for SQL queries'}
|
|
213
217
|
]
|
|
214
218
|
|
|
215
|
-
|
|
216
219
|
# iatoolkit function calls
|
|
217
220
|
_FUNCTION_LIST = [
|
|
218
221
|
{
|
|
@@ -11,10 +11,13 @@ import os
|
|
|
11
11
|
import pytesseract
|
|
12
12
|
from injector import inject
|
|
13
13
|
from iatoolkit.common.exceptions import IAToolkitException
|
|
14
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
14
15
|
|
|
15
16
|
class DocumentService:
|
|
16
17
|
@inject
|
|
17
|
-
def __init__(self):
|
|
18
|
+
def __init__(self, i18n_service: I18nService):
|
|
19
|
+
self.i18n_service = i18n_service
|
|
20
|
+
|
|
18
21
|
# max number of pages to load
|
|
19
22
|
self.max_doc_pages = int(os.getenv("MAX_DOC_PAGES", "200"))
|
|
20
23
|
|
|
@@ -29,7 +32,7 @@ class DocumentService:
|
|
|
29
32
|
file_content = file_content.decode('utf-8')
|
|
30
33
|
except UnicodeDecodeError:
|
|
31
34
|
raise IAToolkitException(IAToolkitException.ErrorType.FILE_FORMAT_ERROR,
|
|
32
|
-
|
|
35
|
+
self.i18n_service.t('errors.services.no_text_file'))
|
|
33
36
|
|
|
34
37
|
return file_content
|
|
35
38
|
elif filename.lower().endswith('.pdf'):
|
|
@@ -8,6 +8,7 @@ import pandas as pd
|
|
|
8
8
|
from uuid import uuid4
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from iatoolkit.common.exceptions import IAToolkitException
|
|
11
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
11
12
|
from injector import inject
|
|
12
13
|
import os
|
|
13
14
|
import logging
|
|
@@ -18,8 +19,11 @@ EXCEL_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
|
18
19
|
|
|
19
20
|
class ExcelService:
|
|
20
21
|
@inject
|
|
21
|
-
def __init__(self,
|
|
22
|
+
def __init__(self,
|
|
23
|
+
util: Utility,
|
|
24
|
+
i18n_service: I18nService):
|
|
22
25
|
self.util = util
|
|
26
|
+
self.i18n_service = i18n_service
|
|
23
27
|
|
|
24
28
|
def excel_generator(self, **kwargs) -> str:
|
|
25
29
|
"""
|
|
@@ -42,11 +46,11 @@ class ExcelService:
|
|
|
42
46
|
# get the parameters
|
|
43
47
|
fname = kwargs.get('filename')
|
|
44
48
|
if not fname:
|
|
45
|
-
return '
|
|
49
|
+
return self.i18n_service.t('errors.services.no_output_file')
|
|
46
50
|
|
|
47
51
|
data = kwargs.get('data')
|
|
48
52
|
if not data or not isinstance(data, list):
|
|
49
|
-
return '
|
|
53
|
+
return self.i18n_service.t('errors.services.no_data_for_excel')
|
|
50
54
|
|
|
51
55
|
sheet_name = kwargs.get('sheet_name', 'hoja 1')
|
|
52
56
|
|
|
@@ -58,7 +62,7 @@ class ExcelService:
|
|
|
58
62
|
|
|
59
63
|
# 4. check that download directory is configured
|
|
60
64
|
if 'IATOOLKIT_DOWNLOAD_DIR' not in current_app.config:
|
|
61
|
-
return '
|
|
65
|
+
return self.i18n_service.t('errors.services.no_download_directory')
|
|
62
66
|
|
|
63
67
|
download_dir = current_app.config['IATOOLKIT_DOWNLOAD_DIR']
|
|
64
68
|
filepath = Path(download_dir) / token
|
|
@@ -77,28 +81,28 @@ class ExcelService:
|
|
|
77
81
|
|
|
78
82
|
except Exception as e:
|
|
79
83
|
raise IAToolkitException(IAToolkitException.ErrorType.CALL_ERROR,
|
|
80
|
-
'
|
|
84
|
+
self.i18n_service.t('errors.services.cannot_create_excel')) from e
|
|
81
85
|
|
|
82
86
|
def validate_file_access(self, filename):
|
|
83
87
|
try:
|
|
84
88
|
if not filename:
|
|
85
|
-
return jsonify({"error":
|
|
89
|
+
return jsonify({"error": self.i18n_service.t('errors.services.invalid_filename')})
|
|
86
90
|
# Prevent path traversal attacks
|
|
87
91
|
if '..' in filename or filename.startswith('/') or '\\' in filename:
|
|
88
|
-
return jsonify({"error":
|
|
92
|
+
return jsonify({"error": self.i18n_service.t('errors.services.invalid_filename')})
|
|
89
93
|
|
|
90
94
|
temp_dir = os.path.join(current_app.root_path, 'static', 'temp')
|
|
91
95
|
file_path = os.path.join(temp_dir, filename)
|
|
92
96
|
|
|
93
97
|
if not os.path.exists(file_path):
|
|
94
|
-
return jsonify({"error":
|
|
98
|
+
return jsonify({"error": self.i18n_service.t('errors.services.file_not_exist')})
|
|
95
99
|
|
|
96
100
|
if not os.path.isfile(file_path):
|
|
97
|
-
return jsonify({"error":
|
|
101
|
+
return jsonify({"error": self.i18n_service.t('errors.services.path_is_not_a_file')})
|
|
98
102
|
|
|
99
103
|
return None
|
|
100
104
|
|
|
101
105
|
except Exception as e:
|
|
102
|
-
error_msg = f"
|
|
106
|
+
error_msg = f"File validation error {filename}: {str(e)}"
|
|
103
107
|
logging.error(error_msg)
|
|
104
|
-
return jsonify({"error":
|
|
108
|
+
return jsonify({"error": self.i18n_service.t('errors.services.file_validation_error')})
|
|
@@ -52,27 +52,19 @@ class FileProcessor:
|
|
|
52
52
|
logger: Optional[logging.Logger] = None):
|
|
53
53
|
self.connector = connector
|
|
54
54
|
self.config = config
|
|
55
|
-
self.logger = logger or self._setup_logger()
|
|
56
55
|
self.processed_files = 0
|
|
57
56
|
|
|
58
|
-
def _setup_logger(self):
|
|
59
|
-
logging.basicConfig(
|
|
60
|
-
filename=self.config.log_file,
|
|
61
|
-
level=logging.INFO,
|
|
62
|
-
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
63
|
-
)
|
|
64
|
-
return logging.getLogger(__name__)
|
|
65
57
|
|
|
66
58
|
def process_files(self):
|
|
67
59
|
# Fetches files from the connector, filters them, and processes them.
|
|
68
60
|
try:
|
|
69
61
|
files = self.connector.list_files()
|
|
70
62
|
except Exception as e:
|
|
71
|
-
|
|
63
|
+
logging.error(f"Error fetching files: {e}")
|
|
72
64
|
return False
|
|
73
65
|
|
|
74
66
|
if self.config.echo:
|
|
75
|
-
print(f'
|
|
67
|
+
print(f'loading {len(files)} files')
|
|
76
68
|
|
|
77
69
|
for file_info in files:
|
|
78
70
|
file_path = file_info['path']
|
|
@@ -95,10 +87,10 @@ class FileProcessor:
|
|
|
95
87
|
context=self.config.context)
|
|
96
88
|
self.processed_files += 1
|
|
97
89
|
|
|
98
|
-
|
|
90
|
+
logging.info(f"Successfully processed file: {file_path}")
|
|
99
91
|
|
|
100
92
|
except Exception as e:
|
|
101
|
-
|
|
93
|
+
logging.error(f"Error processing {file_path}: {e}")
|
|
102
94
|
if not self.config.continue_on_error:
|
|
103
95
|
raise e
|
|
104
96
|
|
|
@@ -6,32 +6,33 @@
|
|
|
6
6
|
from injector import inject
|
|
7
7
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
8
8
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class HistoryService:
|
|
12
14
|
@inject
|
|
13
15
|
def __init__(self, llm_query_repo: LLMQueryRepo,
|
|
14
|
-
profile_repo: ProfileRepo
|
|
16
|
+
profile_repo: ProfileRepo,
|
|
17
|
+
i18n_service: I18nService):
|
|
15
18
|
self.llm_query_repo = llm_query_repo
|
|
16
19
|
self.profile_repo = profile_repo
|
|
20
|
+
self.i18n_service = i18n_service
|
|
17
21
|
|
|
18
22
|
def get_history(self,
|
|
19
23
|
company_short_name: str,
|
|
20
24
|
user_identifier: str) -> dict:
|
|
21
25
|
try:
|
|
22
|
-
# validate company
|
|
23
26
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
24
27
|
if not company:
|
|
25
|
-
return {
|
|
28
|
+
return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
|
|
26
29
|
|
|
27
30
|
history = self.llm_query_repo.get_history(company, user_identifier)
|
|
28
|
-
|
|
29
31
|
if not history:
|
|
30
|
-
return {'message': '
|
|
32
|
+
return {'message': 'empty history', 'history': []}
|
|
31
33
|
|
|
32
34
|
history_list = [query.to_dict() for query in history]
|
|
33
|
-
|
|
34
|
-
return {'message': 'Historial obtenido correctamente', 'history': history_list}
|
|
35
|
+
return {'message': 'history loaded ok', 'history': history_list}
|
|
35
36
|
|
|
36
37
|
except Exception as e:
|
|
37
38
|
return {'error': str(e)}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# iatoolkit/services/i18n_service.py
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
from injector import inject, singleton
|
|
5
|
+
from iatoolkit.common.util import Utility
|
|
6
|
+
from iatoolkit.services.language_service import LanguageService
|
|
7
|
+
|
|
8
|
+
@singleton
|
|
9
|
+
class I18nService:
|
|
10
|
+
"""
|
|
11
|
+
Servicio centralizado para manejar la internacionalización (i18n).
|
|
12
|
+
Carga todas las traducciones desde archivos YAML en memoria al iniciar.
|
|
13
|
+
"""
|
|
14
|
+
FALLBACK_LANGUAGE = 'es'
|
|
15
|
+
|
|
16
|
+
@inject
|
|
17
|
+
def __init__(self, util: Utility, language_service: LanguageService):
|
|
18
|
+
self.util = util
|
|
19
|
+
self.language_service = language_service
|
|
20
|
+
|
|
21
|
+
self.translations = {}
|
|
22
|
+
self._load_translations()
|
|
23
|
+
|
|
24
|
+
def _load_translations(self):
|
|
25
|
+
"""
|
|
26
|
+
Carga todos los archivos .yaml del directorio 'locales' en memoria.
|
|
27
|
+
"""
|
|
28
|
+
locales_dir = os.path.join(os.path.dirname(__file__), '..', 'locales')
|
|
29
|
+
if not os.path.exists(locales_dir):
|
|
30
|
+
logging.error("Directory 'locales' not found.")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
for filename in os.listdir(locales_dir):
|
|
34
|
+
if filename.endswith('.yaml'):
|
|
35
|
+
lang_code = filename.split('.')[0]
|
|
36
|
+
filepath = os.path.join(locales_dir, filename)
|
|
37
|
+
try:
|
|
38
|
+
self.translations[lang_code] = self.util.load_schema_from_yaml(filepath)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logging.error(f"Error while loading the translation file {filepath}: {e}")
|
|
41
|
+
|
|
42
|
+
def _get_nested_key(self, lang: str, key: str):
|
|
43
|
+
"""
|
|
44
|
+
Obtiene un valor de un diccionario anidado usando una clave con puntos.
|
|
45
|
+
"""
|
|
46
|
+
data = self.translations.get(lang, {})
|
|
47
|
+
keys = key.split('.')
|
|
48
|
+
for k in keys:
|
|
49
|
+
if isinstance(data, dict) and k in data:
|
|
50
|
+
data = data[k]
|
|
51
|
+
else:
|
|
52
|
+
return None
|
|
53
|
+
return data
|
|
54
|
+
|
|
55
|
+
def get_translation_block(self, key: str, lang: str = None) -> dict:
|
|
56
|
+
"""
|
|
57
|
+
Gets a whole dictionary block from the translations.
|
|
58
|
+
Useful for passing a set of translations to JavaScript.
|
|
59
|
+
"""
|
|
60
|
+
if lang is None:
|
|
61
|
+
lang = self.language_service.get_current_language()
|
|
62
|
+
|
|
63
|
+
# 1. Try to get the block in the requested language
|
|
64
|
+
block = self._get_nested_key(lang, key)
|
|
65
|
+
|
|
66
|
+
# 2. If not found, try the fallback language
|
|
67
|
+
if not isinstance(block, dict):
|
|
68
|
+
block = self._get_nested_key(self.FALLBACK_LANGUAGE, key)
|
|
69
|
+
|
|
70
|
+
return block if isinstance(block, dict) else {}
|
|
71
|
+
|
|
72
|
+
def t(self, key: str, lang: str = None, **kwargs) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Gets the translation for a given key.
|
|
75
|
+
If 'lang' is provided, it's used. Otherwise, it's determined automatically.
|
|
76
|
+
"""
|
|
77
|
+
# If no specific language is requested, determine it from the current context.
|
|
78
|
+
if lang is None:
|
|
79
|
+
lang = self.language_service.get_current_language()
|
|
80
|
+
|
|
81
|
+
# 1. Attempt to get the translation in the requested language
|
|
82
|
+
message = self._get_nested_key(lang, key)
|
|
83
|
+
|
|
84
|
+
# 2. If not found, try the fallback language
|
|
85
|
+
if message is None and lang != self.FALLBACK_LANGUAGE:
|
|
86
|
+
logging.warning(
|
|
87
|
+
f"Translation key '{key}' not found for language '{lang}'. Attempting fallback to '{self.FALLBACK_LANGUAGE}'.")
|
|
88
|
+
message = self._get_nested_key(self.FALLBACK_LANGUAGE, key)
|
|
89
|
+
|
|
90
|
+
# 3. If still not found, return the key itself as a last resort
|
|
91
|
+
if message is None:
|
|
92
|
+
logging.error(
|
|
93
|
+
f"Translation key '{key}' not found, even in fallback '{self.FALLBACK_LANGUAGE}'.")
|
|
94
|
+
return key
|
|
95
|
+
|
|
96
|
+
# 4. If variables are provided, format the message
|
|
97
|
+
if kwargs:
|
|
98
|
+
try:
|
|
99
|
+
return message.format(**kwargs)
|
|
100
|
+
except KeyError as e:
|
|
101
|
+
logging.error(f"Error formatting key '{key}': missing variable {e} in arguments.")
|
|
102
|
+
return message
|
|
103
|
+
|
|
104
|
+
return message
|
|
@@ -20,8 +20,8 @@ class JWTService:
|
|
|
20
20
|
self.secret_key = app.config['JWT_SECRET_KEY']
|
|
21
21
|
self.algorithm = app.config['JWT_ALGORITHM']
|
|
22
22
|
except KeyError as e:
|
|
23
|
-
logging.error(f"
|
|
24
|
-
raise RuntimeError(f"
|
|
23
|
+
logging.error(f"missing JWT configuration: {e}.")
|
|
24
|
+
raise RuntimeError(f"missing JWT configuration variables: {e}")
|
|
25
25
|
|
|
26
26
|
def generate_chat_jwt(self,
|
|
27
27
|
company_short_name: str,
|
|
@@ -58,25 +58,23 @@ class JWTService:
|
|
|
58
58
|
|
|
59
59
|
# Validaciones adicionales
|
|
60
60
|
if payload.get('type') != 'chat_session':
|
|
61
|
-
logging.warning(f"
|
|
61
|
+
logging.warning(f"Invalid JWT type '{payload.get('type')}'")
|
|
62
62
|
return None
|
|
63
63
|
|
|
64
64
|
# user_identifier debe estar presente
|
|
65
65
|
if not payload.get('user_identifier'):
|
|
66
|
-
logging.warning(f"
|
|
66
|
+
logging.warning(f"missing user_identifier in JWT payload.")
|
|
67
67
|
return None
|
|
68
68
|
|
|
69
69
|
if not payload.get('company_short_name'):
|
|
70
|
-
logging.warning(f"
|
|
70
|
+
logging.warning(f"missing company_short_name in JWT payload.")
|
|
71
71
|
return None
|
|
72
72
|
|
|
73
|
-
logging.debug(
|
|
74
|
-
f"JWT validado exitosamente para company: {payload.get('company_short_name')}, user: {payload.get('external_user_id')}")
|
|
75
73
|
return payload
|
|
76
74
|
|
|
77
75
|
except jwt.InvalidTokenError as e:
|
|
78
|
-
logging.warning(f"
|
|
76
|
+
logging.warning(f"Invalid JWT token:: {e}")
|
|
79
77
|
return None
|
|
80
78
|
except Exception as e:
|
|
81
|
-
logging.error(f"
|
|
79
|
+
logging.error(f"unexpected error during JWT validation: {e}")
|
|
82
80
|
return None
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# iatoolkit/services/language_service.py
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from injector import inject, singleton
|
|
5
|
+
from flask import g, request
|
|
6
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
7
|
+
from iatoolkit.services.configuration_service import ConfigurationService
|
|
8
|
+
from iatoolkit.common.session_manager import SessionManager
|
|
9
|
+
|
|
10
|
+
@singleton
|
|
11
|
+
class LanguageService:
|
|
12
|
+
"""
|
|
13
|
+
Determines the correct language for the current request
|
|
14
|
+
based on a defined priority order (session, URL, etc.)
|
|
15
|
+
and caches it in the Flask 'g' object for the request's lifecycle.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
FALLBACK_LANGUAGE = 'es'
|
|
19
|
+
|
|
20
|
+
@inject
|
|
21
|
+
def __init__(self,
|
|
22
|
+
config_service: ConfigurationService,
|
|
23
|
+
profile_repo: ProfileRepo):
|
|
24
|
+
self.config_service = config_service
|
|
25
|
+
self.profile_repo = profile_repo
|
|
26
|
+
|
|
27
|
+
def _get_company_short_name(self) -> str | None:
|
|
28
|
+
"""
|
|
29
|
+
Gets the company_short_name from the current request context.
|
|
30
|
+
This handles different scenarios like web sessions, public URLs, and API calls.
|
|
31
|
+
|
|
32
|
+
Priority Order:
|
|
33
|
+
1. Flask Session (for logged-in web users).
|
|
34
|
+
2. URL rule variable (for public pages and API endpoints).
|
|
35
|
+
"""
|
|
36
|
+
# 1. Check session for logged-in users
|
|
37
|
+
company_short_name = SessionManager.get('company_short_name')
|
|
38
|
+
if company_short_name:
|
|
39
|
+
return company_short_name
|
|
40
|
+
|
|
41
|
+
# 2. Check URL arguments (e.g., /<company_short_name>/login)
|
|
42
|
+
# This covers public pages and most API calls.
|
|
43
|
+
if request.view_args and 'company_short_name' in request.view_args:
|
|
44
|
+
return request.view_args['company_short_name']
|
|
45
|
+
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def get_current_language(self) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Determines and caches the language for the current request using a priority order:
|
|
51
|
+
1. User's preference (from their profile).
|
|
52
|
+
2. Company's default language.
|
|
53
|
+
3. System-wide fallback language ('es').
|
|
54
|
+
"""
|
|
55
|
+
if 'lang' in g:
|
|
56
|
+
return g.lang
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
# Priority 1: User's preferred language
|
|
60
|
+
user_identifier = SessionManager.get('user_identifier')
|
|
61
|
+
if user_identifier:
|
|
62
|
+
user = self.profile_repo.get_user_by_email(user_identifier)
|
|
63
|
+
if user and user.preferred_language:
|
|
64
|
+
logging.debug(f"Language determined by user preference: {user.preferred_language}")
|
|
65
|
+
g.lang = user.preferred_language
|
|
66
|
+
return g.lang
|
|
67
|
+
|
|
68
|
+
# Priority 2: Company's default language
|
|
69
|
+
company_short_name = self._get_company_short_name()
|
|
70
|
+
if company_short_name:
|
|
71
|
+
locale = self.config_service.get_configuration(company_short_name, 'locale')
|
|
72
|
+
if locale:
|
|
73
|
+
company_language = locale.split('_')[0]
|
|
74
|
+
g.lang = company_language
|
|
75
|
+
return g.lang
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logging.info(f"Could not determine language, falling back to default. Reason: {e}")
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# Priority 3: System-wide fallback
|
|
81
|
+
logging.info(f"Language determined by system fallback: {self.FALLBACK_LANGUAGE}")
|
|
82
|
+
g.lang = self.FALLBACK_LANGUAGE
|
|
83
|
+
return g.lang
|
|
@@ -72,7 +72,7 @@ class LoadDocumentsService:
|
|
|
72
72
|
"""
|
|
73
73
|
if not connector_config:
|
|
74
74
|
raise IAToolkitException(IAToolkitException.ErrorType.MISSING_PARAMETER,
|
|
75
|
-
f"
|
|
75
|
+
f"Missing connector config")
|
|
76
76
|
|
|
77
77
|
try:
|
|
78
78
|
if not filters:
|
|
@@ -123,7 +123,7 @@ class LoadDocumentsService:
|
|
|
123
123
|
|
|
124
124
|
if not company:
|
|
125
125
|
raise IAToolkitException(IAToolkitException.ErrorType.MISSING_PARAMETER,
|
|
126
|
-
f"
|
|
126
|
+
f"missing company")
|
|
127
127
|
|
|
128
128
|
# check if file exist in repositories
|
|
129
129
|
if self.doc_repo.get(company_id=company.id,filename=filename):
|
|
@@ -182,6 +182,6 @@ class LoadDocumentsService:
|
|
|
182
182
|
self.doc_repo.session.rollback()
|
|
183
183
|
|
|
184
184
|
# if something fails, throw exception
|
|
185
|
-
logging.exception("Error
|
|
185
|
+
logging.exception("Error processing file %s: %s", filename, str(e))
|
|
186
186
|
raise IAToolkitException(IAToolkitException.ErrorType.LOAD_DOCUMENT_ERROR,
|
|
187
|
-
f"Error
|
|
187
|
+
f"Error while processing file: {filename}")
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
from iatoolkit.infra.mail_app import MailApp
|
|
7
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
7
8
|
from injector import inject
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from iatoolkit.common.exceptions import IAToolkitException
|
|
@@ -13,18 +14,22 @@ TEMP_DIR = Path("static/temp")
|
|
|
13
14
|
|
|
14
15
|
class MailService:
|
|
15
16
|
@inject
|
|
16
|
-
def __init__(self,
|
|
17
|
+
def __init__(self,
|
|
18
|
+
mail_app: MailApp,
|
|
19
|
+
i18n_service: I18nService):
|
|
17
20
|
self.mail_app = mail_app
|
|
21
|
+
self.i18n_service = i18n_service
|
|
22
|
+
|
|
18
23
|
|
|
19
24
|
def _read_token_bytes(self, token: str) -> bytes:
|
|
20
25
|
# Defensa simple contra path traversal
|
|
21
26
|
if not token or "/" in token or "\\" in token or token.startswith("."):
|
|
22
27
|
raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
|
|
23
|
-
"attachment_token
|
|
28
|
+
"attachment_token invalid")
|
|
24
29
|
path = TEMP_DIR / token
|
|
25
30
|
if not path.is_file():
|
|
26
31
|
raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
|
|
27
|
-
f"
|
|
32
|
+
f"attach file not found: {token}")
|
|
28
33
|
return path.read_bytes()
|
|
29
34
|
|
|
30
35
|
def send_mail(self, **kwargs):
|
|
@@ -59,4 +64,4 @@ class MailService:
|
|
|
59
64
|
body=body,
|
|
60
65
|
attachments=norm_attachments)
|
|
61
66
|
|
|
62
|
-
return '
|
|
67
|
+
return self.i18n_service.t('services.mail_sent')
|