iatoolkit 0.63.1__py3-none-any.whl → 0.67.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 +2 -0
- iatoolkit/base_company.py +1 -20
- 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 +41 -5
- iatoolkit/locales/en.yaml +167 -0
- iatoolkit/locales/es.yaml +163 -0
- iatoolkit/repositories/database_manager.py +3 -3
- iatoolkit/repositories/document_repo.py +1 -1
- iatoolkit/repositories/models.py +2 -3
- iatoolkit/repositories/profile_repo.py +0 -4
- iatoolkit/services/auth_service.py +14 -9
- iatoolkit/services/branding_service.py +32 -22
- iatoolkit/services/configuration_service.py +140 -0
- iatoolkit/services/dispatcher_service.py +20 -18
- 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 +79 -0
- iatoolkit/services/load_documents_service.py +4 -4
- iatoolkit/services/mail_service.py +9 -4
- iatoolkit/services/onboarding_service.py +10 -4
- iatoolkit/services/profile_service.py +58 -38
- iatoolkit/services/prompt_manager_service.py +20 -16
- iatoolkit/services/query_service.py +15 -14
- iatoolkit/services/sql_service.py +6 -2
- 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_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/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 +58 -27
- iatoolkit/templates/chat_modals.html +113 -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 +16 -5
- iatoolkit/templates/onboarding_shell.html +0 -1
- iatoolkit/templates/signup.html +14 -14
- iatoolkit/views/base_login_view.py +12 -1
- iatoolkit/views/change_password_view.py +49 -33
- iatoolkit/views/forgot_password_view.py +20 -19
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +13 -9
- iatoolkit/views/home_view.py +30 -38
- 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 +47 -35
- 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 +26 -24
- iatoolkit/views/user_feedback_api_view.py +19 -18
- iatoolkit/views/verify_user_view.py +30 -29
- {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/METADATA +40 -22
- iatoolkit-0.67.0.dist-info/RECORD +120 -0
- iatoolkit-0.67.0.dist-info/licenses/LICENSE +21 -0
- 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.67.0.dist-info}/WHEEL +0 -0
- {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/top_level.txt +0 -0
|
@@ -74,10 +74,6 @@ class ProfileRepo:
|
|
|
74
74
|
if company:
|
|
75
75
|
if company.parameters != new_company.parameters:
|
|
76
76
|
company.parameters = new_company.parameters
|
|
77
|
-
if company.branding != new_company.branding:
|
|
78
|
-
company.branding = new_company.branding
|
|
79
|
-
if company.onboarding_cards != new_company.onboarding_cards:
|
|
80
|
-
company.onboarding_cards = new_company.onboarding_cards
|
|
81
77
|
else:
|
|
82
78
|
# Si la compañía no existe, la añade a la sesión.
|
|
83
79
|
self.session.add(new_company)
|
|
@@ -7,6 +7,7 @@ from flask import request
|
|
|
7
7
|
from injector import inject
|
|
8
8
|
from iatoolkit.services.profile_service import ProfileService
|
|
9
9
|
from iatoolkit.services.jwt_service import JWTService
|
|
10
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
10
11
|
from iatoolkit.repositories.database_manager import DatabaseManager
|
|
11
12
|
from iatoolkit.repositories.models import AccessLog
|
|
12
13
|
from flask import request
|
|
@@ -23,18 +24,20 @@ class AuthService:
|
|
|
23
24
|
@inject
|
|
24
25
|
def __init__(self, profile_service: ProfileService,
|
|
25
26
|
jwt_service: JWTService,
|
|
26
|
-
db_manager: DatabaseManager
|
|
27
|
+
db_manager: DatabaseManager,
|
|
28
|
+
i18n_service: I18nService
|
|
27
29
|
):
|
|
28
30
|
self.profile_service = profile_service
|
|
29
31
|
self.jwt_service = jwt_service
|
|
30
32
|
self.db_manager = db_manager
|
|
33
|
+
self.i18n_service = i18n_service
|
|
31
34
|
|
|
32
35
|
def login_local_user(self, company_short_name: str, email: str, password: str) -> dict:
|
|
33
36
|
# try to autenticate a local user, register the event and return the result
|
|
34
37
|
auth_response = self.profile_service.login(
|
|
35
38
|
company_short_name=company_short_name,
|
|
36
39
|
email=email,
|
|
37
|
-
password=password
|
|
40
|
+
password=password,
|
|
38
41
|
)
|
|
39
42
|
|
|
40
43
|
if not auth_response.get('success'):
|
|
@@ -66,7 +69,7 @@ class AuthService:
|
|
|
66
69
|
outcome='failure',
|
|
67
70
|
reason_code='JWT_INVALID'
|
|
68
71
|
)
|
|
69
|
-
return {'success': False, 'error': '
|
|
72
|
+
return {'success': False, 'error': self.i18n_service.t('errors.auth.invalid_or_expired_token')}
|
|
70
73
|
|
|
71
74
|
# 2. if token is valid, extract the user_identifier
|
|
72
75
|
user_identifier = payload.get('user_identifier')
|
|
@@ -81,7 +84,7 @@ class AuthService:
|
|
|
81
84
|
)
|
|
82
85
|
return {'success': True, 'user_identifier': user_identifier}
|
|
83
86
|
except Exception as e:
|
|
84
|
-
logging.error(f"
|
|
87
|
+
logging.error(f"error creeating session for Token of {user_identifier}: {e}")
|
|
85
88
|
self.log_access(
|
|
86
89
|
company_short_name=company_short_name,
|
|
87
90
|
auth_type='redeem_token',
|
|
@@ -89,7 +92,7 @@ class AuthService:
|
|
|
89
92
|
reason_code='SESSION_CREATION_FAILED',
|
|
90
93
|
user_identifier=user_identifier
|
|
91
94
|
)
|
|
92
|
-
return {'success': False, 'error': '
|
|
95
|
+
return {'success': False, 'error': self.i18n_service.t('errors.auth.session_creation_failed')}
|
|
93
96
|
|
|
94
97
|
def verify(self, anonymous: bool = False) -> dict:
|
|
95
98
|
"""
|
|
@@ -123,14 +126,15 @@ class AuthService:
|
|
|
123
126
|
# --- Failure: No valid credentials found ---
|
|
124
127
|
logging.info(f"Authentication required. No session cookie or API Key provided.")
|
|
125
128
|
return {"success": False,
|
|
126
|
-
"error_message":
|
|
129
|
+
"error_message": self.i18n_service.t('errors.auth.authentication_required'),
|
|
127
130
|
"status_code": 401}
|
|
128
131
|
|
|
129
132
|
# check if the api-key is valid and active
|
|
130
133
|
api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
|
|
131
134
|
if not api_key_entry:
|
|
132
135
|
logging.info(f"Invalid or inactive API Key {api_key}")
|
|
133
|
-
return {"success": False,
|
|
136
|
+
return {"success": False,
|
|
137
|
+
"error_message": self.i18n_service.t('errors.auth.invalid_api_key'),
|
|
134
138
|
"status_code": 402}
|
|
135
139
|
|
|
136
140
|
# get the company from the api_key_entry
|
|
@@ -141,7 +145,8 @@ class AuthService:
|
|
|
141
145
|
user_identifier = data.get('user_identifier', '')
|
|
142
146
|
if not anonymous and not user_identifier:
|
|
143
147
|
logging.info(f"No user_identifier provided for API call.")
|
|
144
|
-
return {"success": False,
|
|
148
|
+
return {"success": False,
|
|
149
|
+
"error_message": self.i18n_service.t('errors.auth.no_user_identifier_api'),
|
|
145
150
|
"status_code": 403}
|
|
146
151
|
|
|
147
152
|
return {
|
|
@@ -184,5 +189,5 @@ class AuthService:
|
|
|
184
189
|
session.commit()
|
|
185
190
|
|
|
186
191
|
except Exception as e:
|
|
187
|
-
logging.error(f"
|
|
192
|
+
logging.error(f"error writting to AccessLog: {e}", exc_info=False)
|
|
188
193
|
session.rollback()
|
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
from iatoolkit.repositories.models import Company
|
|
7
|
+
from iatoolkit.services.configuration_service import ConfigurationService
|
|
8
|
+
from injector import inject
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class BrandingService:
|
|
10
12
|
"""
|
|
11
|
-
|
|
13
|
+
Branding configuration for IAToolkit
|
|
12
14
|
"""
|
|
13
|
-
|
|
14
|
-
def __init__(self):
|
|
15
|
+
@inject
|
|
16
|
+
def __init__(self, config_service: ConfigurationService):
|
|
17
|
+
self.config_service = config_service
|
|
15
18
|
"""
|
|
16
19
|
Define los estilos de branding por defecto para la aplicación.
|
|
17
20
|
"""
|
|
@@ -19,13 +22,16 @@ class BrandingService:
|
|
|
19
22
|
# --- Estilos del Encabezado Principal ---
|
|
20
23
|
"header_background_color": "#FFFFFF",
|
|
21
24
|
"header_text_color": "#6C757D",
|
|
22
|
-
"primary_font_weight": "
|
|
23
|
-
"primary_font_size": "
|
|
24
|
-
"secondary_font_weight": "
|
|
25
|
-
"secondary_font_size": "0.
|
|
26
|
-
"tertiary_font_weight": "
|
|
27
|
-
"tertiary_font_size": "0.
|
|
28
|
-
"tertiary_opacity": "0.
|
|
25
|
+
"primary_font_weight": "600",
|
|
26
|
+
"primary_font_size": "1.2rem",
|
|
27
|
+
"secondary_font_weight": "400",
|
|
28
|
+
"secondary_font_size": "0.9rem",
|
|
29
|
+
"tertiary_font_weight": "300",
|
|
30
|
+
"tertiary_font_size": "0.8rem",
|
|
31
|
+
"tertiary_opacity": "0.7",
|
|
32
|
+
|
|
33
|
+
# headings
|
|
34
|
+
"brand_text_heading_color": "#334155", # Gris pizarra por defecto
|
|
29
35
|
|
|
30
36
|
# Estilos Globales de la Marca ---
|
|
31
37
|
"brand_primary_color": "#0d6efd", # Azul de Bootstrap por defecto
|
|
@@ -40,25 +46,27 @@ class BrandingService:
|
|
|
40
46
|
"brand_danger_border": "#f5c2c7", # Borde rojo intermedio
|
|
41
47
|
|
|
42
48
|
# Estilos para Alertas Informativas ---
|
|
43
|
-
"brand_info_bg": "#
|
|
44
|
-
"brand_info_text": "#
|
|
45
|
-
"brand_info_border": "#
|
|
49
|
+
"brand_info_bg": "#F0F4F8", # Un fondo de gris azulado muy pálido
|
|
50
|
+
"brand_info_text": "#0d6efd", # Texto en el color primario
|
|
51
|
+
"brand_info_border": "#D9E2EC", # Borde de gris azulado pálido
|
|
46
52
|
|
|
47
53
|
# Estilos para el Asistente de Prompts ---
|
|
48
54
|
"prompt_assistant_bg": "#f8f9fa",
|
|
49
55
|
"prompt_assistant_border": "#dee2e6",
|
|
50
|
-
"prompt_assistant_icon_color": "#6c757d",
|
|
51
56
|
"prompt_assistant_button_bg": "#FFFFFF",
|
|
52
57
|
"prompt_assistant_button_text": "#495057",
|
|
53
58
|
"prompt_assistant_button_border": "#ced4da",
|
|
54
59
|
"prompt_assistant_dropdown_bg": "#f8f9fa",
|
|
55
60
|
"prompt_assistant_header_bg": "#e9ecef",
|
|
56
61
|
"prompt_assistant_header_text": "#495057",
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
|
|
63
|
+
# this use the primary by default
|
|
64
|
+
"prompt_assistant_icon_color": None,
|
|
65
|
+
"prompt_assistant_item_hover_bg": None,
|
|
66
|
+
"prompt_assistant_item_hover_text": None,
|
|
59
67
|
|
|
60
68
|
# Color para el botón de Enviar ---
|
|
61
|
-
"send_button_color": "#212529"
|
|
69
|
+
"send_button_color": "#212529" # Gris oscuro/casi negro por defecto
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
def get_company_branding(self, company: Company | None) -> dict:
|
|
@@ -68,8 +76,9 @@ class BrandingService:
|
|
|
68
76
|
"""
|
|
69
77
|
final_branding_values = self._default_branding.copy()
|
|
70
78
|
|
|
71
|
-
if company
|
|
72
|
-
|
|
79
|
+
if company:
|
|
80
|
+
branding_data = self.config_service.get_company_content(company.short_name, 'branding')
|
|
81
|
+
final_branding_values.update(branding_data)
|
|
73
82
|
|
|
74
83
|
# Función para convertir HEX a RGB
|
|
75
84
|
def hex_to_rgb(hex_color):
|
|
@@ -101,6 +110,7 @@ class BrandingService:
|
|
|
101
110
|
--brand-secondary-color: {final_branding_values['brand_secondary_color']};
|
|
102
111
|
--brand-header-bg: {final_branding_values['header_background_color']};
|
|
103
112
|
--brand-header-text: {final_branding_values['header_text_color']};
|
|
113
|
+
--brand-text-heading-color: {final_branding_values['brand_text_heading_color']};
|
|
104
114
|
|
|
105
115
|
--brand-primary-color-rgb: {', '.join(map(str, primary_rgb))};
|
|
106
116
|
--brand-secondary-color-rgb: {', '.join(map(str, secondary_rgb))};
|
|
@@ -113,11 +123,11 @@ class BrandingService:
|
|
|
113
123
|
--brand-danger-text: {final_branding_values['brand_danger_text']};
|
|
114
124
|
--brand-danger-border: {final_branding_values['brand_danger_border']};
|
|
115
125
|
--brand-info-bg: {final_branding_values['brand_info_bg']};
|
|
116
|
-
--brand-info-text: {final_branding_values['brand_info_text']};
|
|
126
|
+
--brand-info-text: {final_branding_values['brand_info_text'] or final_branding_values['brand_primary_color']};
|
|
117
127
|
--brand-info-border: {final_branding_values['brand_info_border']};
|
|
118
128
|
--brand-prompt-assistant-bg: {final_branding_values['prompt_assistant_bg']};
|
|
119
129
|
--brand-prompt-assistant-border: {final_branding_values['prompt_assistant_border']};
|
|
120
|
-
--brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color']};
|
|
130
|
+
--brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color'] or final_branding_values['brand_primary_color']};
|
|
121
131
|
--brand-prompt-assistant-button-bg: {final_branding_values['prompt_assistant_button_bg']};
|
|
122
132
|
--brand-prompt-assistant-button-text: {final_branding_values['prompt_assistant_button_text']};
|
|
123
133
|
--brand-prompt-assistant-button-border: {final_branding_values['prompt_assistant_button_border']};
|
|
@@ -137,5 +147,5 @@ class BrandingService:
|
|
|
137
147
|
"tertiary_text_style": tertiary_text_style,
|
|
138
148
|
"header_text_color": final_branding_values['header_text_color'],
|
|
139
149
|
"css_variables": css_variables,
|
|
140
|
-
"send_button_color": final_branding_values['
|
|
150
|
+
"send_button_color": final_branding_values['brand_primary_color']
|
|
141
151
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# iatoolkit/services/configuration_service.py
|
|
2
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
3
|
+
# Product: IAToolkit
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from iatoolkit import BaseCompany
|
|
7
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
8
|
+
from iatoolkit.repositories.models import Company
|
|
9
|
+
from iatoolkit.common.util import Utility
|
|
10
|
+
from injector import inject
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
class ConfigurationService:
|
|
14
|
+
"""
|
|
15
|
+
Orchestrates the configuration of a Company by reading its YAML files
|
|
16
|
+
and using the BaseCompany's protected methods to register settings.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@inject
|
|
20
|
+
def __init__(self,
|
|
21
|
+
profile_repo: ProfileRepo,
|
|
22
|
+
utility: Utility):
|
|
23
|
+
self.profile_repo = profile_repo
|
|
24
|
+
self.utility = utility
|
|
25
|
+
self._loaded_configs = {} # cache for store loaded configurations
|
|
26
|
+
|
|
27
|
+
def get_company_content(self, company_short_name: str, content_key: str) -> dict | list | None:
|
|
28
|
+
"""
|
|
29
|
+
Public method to provide a specific section of a company's configuration.
|
|
30
|
+
It uses a cache to avoid reading files from disk on every call.
|
|
31
|
+
"""
|
|
32
|
+
self._ensure_config_loaded(company_short_name)
|
|
33
|
+
return self._loaded_configs[company_short_name].get(content_key)
|
|
34
|
+
|
|
35
|
+
def register_company(self, company_short_name: str, company_instance: BaseCompany):
|
|
36
|
+
"""
|
|
37
|
+
Main entry point for configuring a company instance.
|
|
38
|
+
This method is invoked by the dispatcher for each registered company.
|
|
39
|
+
"""
|
|
40
|
+
logging.info(f"⚙️ Starting configuration for company '{company_short_name}'...")
|
|
41
|
+
|
|
42
|
+
# 1. identify the instance with his name and load info from database
|
|
43
|
+
company_instance.id = company_short_name
|
|
44
|
+
company_instance.company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
45
|
+
|
|
46
|
+
# 2. Load the main configuration file and supplementary content files
|
|
47
|
+
config = self._load_and_merge_configs(company_short_name)
|
|
48
|
+
|
|
49
|
+
# 3. Register core company details and get the database object
|
|
50
|
+
company_db_object = self._register_core_details(company_instance, config)
|
|
51
|
+
|
|
52
|
+
# 4. Register tools (functions)
|
|
53
|
+
self._register_tools(company_instance, config.get('tools', []))
|
|
54
|
+
|
|
55
|
+
# 5. Register prompt categories and prompts
|
|
56
|
+
self._register_prompts(company_instance, config)
|
|
57
|
+
|
|
58
|
+
# 6. Link the persisted Company object back to the running instance
|
|
59
|
+
company_instance.company = company_db_object
|
|
60
|
+
|
|
61
|
+
logging.info(f"✅ Company '{company_short_name}' configured successfully.")
|
|
62
|
+
|
|
63
|
+
def _ensure_config_loaded(self, company_short_name: str):
|
|
64
|
+
"""
|
|
65
|
+
Checks if the configuration for a company is in the cache.
|
|
66
|
+
If not, it loads it from files and stores it.
|
|
67
|
+
"""
|
|
68
|
+
if company_short_name not in self._loaded_configs:
|
|
69
|
+
self._loaded_configs[company_short_name] = self._load_and_merge_configs(company_short_name)
|
|
70
|
+
|
|
71
|
+
def _load_and_merge_configs(self, company_short_name: str) -> dict:
|
|
72
|
+
"""
|
|
73
|
+
Loads the main company.yaml and merges data from supplementary files
|
|
74
|
+
specified in the 'content_files' section.
|
|
75
|
+
"""
|
|
76
|
+
config_dir = Path("companies") / company_short_name / "config"
|
|
77
|
+
main_config_path = config_dir / "company.yaml"
|
|
78
|
+
|
|
79
|
+
if not main_config_path.exists():
|
|
80
|
+
raise FileNotFoundError(f"Main configuration file not found: {main_config_path}")
|
|
81
|
+
|
|
82
|
+
config = self.utility.load_schema_from_yaml(main_config_path)
|
|
83
|
+
|
|
84
|
+
# Load and merge supplementary content files (e.g., onboarding_cards)
|
|
85
|
+
for key, file_path in config.get('help_files', {}).items():
|
|
86
|
+
supplementary_path = config_dir / file_path
|
|
87
|
+
if supplementary_path.exists():
|
|
88
|
+
config[key] = self.utility.load_schema_from_yaml(supplementary_path)
|
|
89
|
+
else:
|
|
90
|
+
logging.warning(f"⚠️ Warning: Content file not found: {supplementary_path}")
|
|
91
|
+
config[key] = None # Ensure the key exists but is empty
|
|
92
|
+
|
|
93
|
+
return config
|
|
94
|
+
|
|
95
|
+
def _register_core_details(self, company_instance: BaseCompany, config: dict) -> Company:
|
|
96
|
+
"""Calls _create_company with data from the merged YAML config."""
|
|
97
|
+
return company_instance._create_company(
|
|
98
|
+
name=config['name'],
|
|
99
|
+
short_name=config['id'],
|
|
100
|
+
parameters=config.get('parameters', {})
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _register_tools(self, company_instance: BaseCompany, tools_config: list):
|
|
104
|
+
"""Calls _create_function for each tool defined in the YAML."""
|
|
105
|
+
for tool in tools_config:
|
|
106
|
+
company_instance._create_function(
|
|
107
|
+
function_name=tool['function_name'],
|
|
108
|
+
description=tool['description'],
|
|
109
|
+
params=tool['params']
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _register_prompts(self, company_instance: BaseCompany, config: dict):
|
|
113
|
+
"""
|
|
114
|
+
Creates prompt categories first, then creates each prompt and assigns
|
|
115
|
+
it to its respective category.
|
|
116
|
+
"""
|
|
117
|
+
prompts_config = config.get('prompts', [])
|
|
118
|
+
categories_config = config.get('prompt_categories', [])
|
|
119
|
+
|
|
120
|
+
created_categories = {}
|
|
121
|
+
for i, category_name in enumerate(categories_config):
|
|
122
|
+
category_obj = company_instance._create_prompt_category(name=category_name, order=i + 1)
|
|
123
|
+
created_categories[category_name] = category_obj
|
|
124
|
+
|
|
125
|
+
for prompt_data in prompts_config:
|
|
126
|
+
category_name = prompt_data.get('category')
|
|
127
|
+
if not category_name or category_name not in created_categories:
|
|
128
|
+
logging.info(f"⚠️ Warning: Prompt '{prompt_data['name']}' has an invalid or missing category. Skipping.")
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
category_obj = created_categories[category_name]
|
|
132
|
+
|
|
133
|
+
company_instance._create_prompt(
|
|
134
|
+
prompt_name=prompt_data['name'],
|
|
135
|
+
description=prompt_data['description'],
|
|
136
|
+
order=prompt_data['order'],
|
|
137
|
+
category=category_obj,
|
|
138
|
+
active=prompt_data.get('active', True),
|
|
139
|
+
custom_fields=prompt_data.get('custom_fields', [])
|
|
140
|
+
)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
from iatoolkit.common.exceptions import IAToolkitException
|
|
7
7
|
from iatoolkit.services.prompt_manager_service import PromptService
|
|
8
8
|
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
9
|
-
|
|
9
|
+
from iatoolkit.services.configuration_service import ConfigurationService
|
|
10
10
|
from iatoolkit.repositories.models import Company, Function
|
|
11
11
|
from iatoolkit.services.excel_service import ExcelService
|
|
12
12
|
from iatoolkit.services.mail_service import MailService
|
|
@@ -19,11 +19,13 @@ import os
|
|
|
19
19
|
class Dispatcher:
|
|
20
20
|
@inject
|
|
21
21
|
def __init__(self,
|
|
22
|
+
config_service: ConfigurationService,
|
|
22
23
|
prompt_service: PromptService,
|
|
23
24
|
llmquery_repo: LLMQueryRepo,
|
|
24
25
|
util: Utility,
|
|
25
26
|
excel_service: ExcelService,
|
|
26
27
|
mail_service: MailService):
|
|
28
|
+
self.config_service = config_service
|
|
27
29
|
self.prompt_service = prompt_service
|
|
28
30
|
self.llmquery_repo = llmquery_repo
|
|
29
31
|
self.util = util
|
|
@@ -55,14 +57,18 @@ class Dispatcher:
|
|
|
55
57
|
self._company_instances = self.company_registry.get_all_company_instances()
|
|
56
58
|
return self._company_instances
|
|
57
59
|
|
|
58
|
-
def
|
|
60
|
+
def load_company_configs(self):
|
|
59
61
|
# initialize the system functions and prompts
|
|
60
62
|
self.setup_iatoolkit_system()
|
|
61
63
|
|
|
62
|
-
"""
|
|
63
|
-
for
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
"""Loads the configuration of every company"""
|
|
65
|
+
for company_name, company_instance in self.company_instances.items():
|
|
66
|
+
try:
|
|
67
|
+
# register the company configuration
|
|
68
|
+
self.config_service.register_company(company_name, company_instance)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logging.error(f"❌ Failed to register configuration for '{company_name}': {e}")
|
|
71
|
+
continue
|
|
66
72
|
|
|
67
73
|
return True
|
|
68
74
|
|
|
@@ -90,9 +96,6 @@ class Dispatcher:
|
|
|
90
96
|
)
|
|
91
97
|
i += 1
|
|
92
98
|
|
|
93
|
-
# register in the database every company class
|
|
94
|
-
for company in self.company_instances.values():
|
|
95
|
-
company.register_company()
|
|
96
99
|
|
|
97
100
|
def dispatch(self, company_name: str, action: str, **kwargs) -> dict:
|
|
98
101
|
company_key = company_name.lower()
|
|
@@ -149,7 +152,7 @@ class Dispatcher:
|
|
|
149
152
|
except Exception as e:
|
|
150
153
|
logging.exception(e)
|
|
151
154
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
152
|
-
f"Error
|
|
155
|
+
f"Error getting company context of: {company_name}: {str(e)}") from e
|
|
153
156
|
|
|
154
157
|
def get_company_services(self, company: Company) -> list[dict]:
|
|
155
158
|
# create the syntax with openai response syntax, for the company function list
|
|
@@ -173,7 +176,7 @@ class Dispatcher:
|
|
|
173
176
|
def get_user_info(self, company_name: str, user_identifier: str) -> dict:
|
|
174
177
|
if company_name not in self.company_instances:
|
|
175
178
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
176
|
-
f"
|
|
179
|
+
f"company not configured: {company_name}")
|
|
177
180
|
|
|
178
181
|
# source 2: external company user
|
|
179
182
|
company_instance = self.company_instances[company_name]
|
|
@@ -182,14 +185,14 @@ class Dispatcher:
|
|
|
182
185
|
except Exception as e:
|
|
183
186
|
logging.exception(e)
|
|
184
187
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
185
|
-
f"Error
|
|
188
|
+
f"Error in get_user_info: {company_name}: {str(e)}") from e
|
|
186
189
|
|
|
187
190
|
return external_user_profile
|
|
188
191
|
|
|
189
192
|
def get_metadata_from_filename(self, company_name: str, filename: str) -> dict:
|
|
190
193
|
if company_name not in self.company_instances:
|
|
191
194
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
192
|
-
f"
|
|
195
|
+
f"company not configured: {company_name}")
|
|
193
196
|
|
|
194
197
|
company_instance = self.company_instances[company_name]
|
|
195
198
|
try:
|
|
@@ -197,7 +200,7 @@ class Dispatcher:
|
|
|
197
200
|
except Exception as e:
|
|
198
201
|
logging.exception(e)
|
|
199
202
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
200
|
-
f"Error
|
|
203
|
+
f"Error in get_metadata_from_filename: {company_name}: {str(e)}") from e
|
|
201
204
|
|
|
202
205
|
def get_company_instance(self, company_name: str):
|
|
203
206
|
"""Returns the instance for a given company name."""
|
|
@@ -207,12 +210,11 @@ class Dispatcher:
|
|
|
207
210
|
|
|
208
211
|
# iatoolkit system prompts
|
|
209
212
|
_SYSTEM_PROMPT = [
|
|
210
|
-
{'name': 'query_main', 'description':'main prompt
|
|
211
|
-
{'name': 'format_styles', 'description':'
|
|
212
|
-
{'name': 'sql_rules', 'description':'
|
|
213
|
+
{'name': 'query_main', 'description':'iatoolkit main prompt'},
|
|
214
|
+
{'name': 'format_styles', 'description':'output format styles'},
|
|
215
|
+
{'name': 'sql_rules', 'description':'instructions for SQL queries'}
|
|
213
216
|
]
|
|
214
217
|
|
|
215
|
-
|
|
216
218
|
# iatoolkit function calls
|
|
217
219
|
_FUNCTION_LIST = [
|
|
218
220
|
{
|
|
@@ -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
|
|