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
iatoolkit/repositories/models.py
CHANGED
|
@@ -57,12 +57,8 @@ class Company(Base):
|
|
|
57
57
|
# encrypted api-key
|
|
58
58
|
openai_api_key = Column(String, nullable=True)
|
|
59
59
|
gemini_api_key = Column(String, nullable=True)
|
|
60
|
-
|
|
61
|
-
branding = Column(JSON, nullable=True)
|
|
62
|
-
onboarding_cards = Column(JSON, nullable=True)
|
|
63
60
|
parameters = Column(JSON, nullable=True)
|
|
64
61
|
created_at = Column(DateTime, default=datetime.now)
|
|
65
|
-
allow_jwt = Column(Boolean, default=True, nullable=True)
|
|
66
62
|
|
|
67
63
|
documents = relationship("Document",
|
|
68
64
|
back_populates="company",
|
|
@@ -107,6 +103,7 @@ class User(Base):
|
|
|
107
103
|
created_at = Column(DateTime, default=datetime.now)
|
|
108
104
|
password = Column(String, nullable=False)
|
|
109
105
|
verified = Column(Boolean, nullable=False, default=False)
|
|
106
|
+
preferred_language = Column(String(5), nullable=True)
|
|
110
107
|
verification_url = Column(String, nullable=True)
|
|
111
108
|
temp_code = Column(String, nullable=True)
|
|
112
109
|
|
|
@@ -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,36 +46,38 @@ 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
|
-
def get_company_branding(self,
|
|
72
|
+
def get_company_branding(self, company_short_name: str) -> dict:
|
|
65
73
|
"""
|
|
66
74
|
Retorna los estilos de branding finales para una compañía,
|
|
67
75
|
fusionando los valores por defecto con los personalizados.
|
|
68
76
|
"""
|
|
69
77
|
final_branding_values = self._default_branding.copy()
|
|
78
|
+
branding_data = self.config_service.get_configuration(company_short_name, 'branding')
|
|
79
|
+
final_branding_values.update(branding_data)
|
|
70
80
|
|
|
71
|
-
if company and company.branding:
|
|
72
|
-
final_branding_values.update(company.branding)
|
|
73
81
|
|
|
74
82
|
# Función para convertir HEX a RGB
|
|
75
83
|
def hex_to_rgb(hex_color):
|
|
@@ -101,6 +109,7 @@ class BrandingService:
|
|
|
101
109
|
--brand-secondary-color: {final_branding_values['brand_secondary_color']};
|
|
102
110
|
--brand-header-bg: {final_branding_values['header_background_color']};
|
|
103
111
|
--brand-header-text: {final_branding_values['header_text_color']};
|
|
112
|
+
--brand-text-heading-color: {final_branding_values['brand_text_heading_color']};
|
|
104
113
|
|
|
105
114
|
--brand-primary-color-rgb: {', '.join(map(str, primary_rgb))};
|
|
106
115
|
--brand-secondary-color-rgb: {', '.join(map(str, secondary_rgb))};
|
|
@@ -113,11 +122,11 @@ class BrandingService:
|
|
|
113
122
|
--brand-danger-text: {final_branding_values['brand_danger_text']};
|
|
114
123
|
--brand-danger-border: {final_branding_values['brand_danger_border']};
|
|
115
124
|
--brand-info-bg: {final_branding_values['brand_info_bg']};
|
|
116
|
-
--brand-info-text: {final_branding_values['brand_info_text']};
|
|
125
|
+
--brand-info-text: {final_branding_values['brand_info_text'] or final_branding_values['brand_primary_color']};
|
|
117
126
|
--brand-info-border: {final_branding_values['brand_info_border']};
|
|
118
127
|
--brand-prompt-assistant-bg: {final_branding_values['prompt_assistant_bg']};
|
|
119
128
|
--brand-prompt-assistant-border: {final_branding_values['prompt_assistant_border']};
|
|
120
|
-
--brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color']};
|
|
129
|
+
--brand-prompt-assistant-icon-color: {final_branding_values['prompt_assistant_icon_color'] or final_branding_values['brand_primary_color']};
|
|
121
130
|
--brand-prompt-assistant-button-bg: {final_branding_values['prompt_assistant_button_bg']};
|
|
122
131
|
--brand-prompt-assistant-button-text: {final_branding_values['prompt_assistant_button_text']};
|
|
123
132
|
--brand-prompt-assistant-button-border: {final_branding_values['prompt_assistant_button_border']};
|
|
@@ -130,12 +139,15 @@ class BrandingService:
|
|
|
130
139
|
}}
|
|
131
140
|
"""
|
|
132
141
|
|
|
142
|
+
# get the company name from configuration for the branding render
|
|
143
|
+
company_name = self.config_service.get_configuration(company_short_name, 'name')
|
|
144
|
+
|
|
133
145
|
return {
|
|
134
|
-
"name":
|
|
146
|
+
"name": company_name,
|
|
135
147
|
"primary_text_style": primary_text_style,
|
|
136
148
|
"secondary_text_style": secondary_text_style,
|
|
137
149
|
"tertiary_text_style": tertiary_text_style,
|
|
138
150
|
"header_text_color": final_branding_values['header_text_color'],
|
|
139
151
|
"css_variables": css_variables,
|
|
140
|
-
"send_button_color": final_branding_values['
|
|
152
|
+
"send_button_color": final_branding_values['brand_primary_color']
|
|
141
153
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.common.util import Utility
|
|
7
|
+
from iatoolkit.services.configuration_service import ConfigurationService
|
|
8
|
+
from iatoolkit.services.sql_service import SqlService
|
|
9
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
10
|
+
import logging
|
|
11
|
+
from injector import inject
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CompanyContextService:
|
|
16
|
+
"""
|
|
17
|
+
Responsible for building the complete context string for a given company
|
|
18
|
+
to be sent to the Language Model.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@inject
|
|
22
|
+
def __init__(self,
|
|
23
|
+
sql_service: SqlService,
|
|
24
|
+
utility: Utility,
|
|
25
|
+
config_service: ConfigurationService):
|
|
26
|
+
self.sql_service = sql_service
|
|
27
|
+
self.utility = utility
|
|
28
|
+
self.config_service = config_service
|
|
29
|
+
|
|
30
|
+
def get_company_context(self, company_short_name: str) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Builds the full context by aggregating three sources:
|
|
33
|
+
1. Static context files (Markdown).
|
|
34
|
+
2. Static schema files (YAML for APIs, etc.).
|
|
35
|
+
3. Dynamic SQL database schema from the live connection.
|
|
36
|
+
"""
|
|
37
|
+
context_parts = []
|
|
38
|
+
|
|
39
|
+
# 1. Context from Markdown (context/*.md) and yaml (schema/*.yaml) files
|
|
40
|
+
try:
|
|
41
|
+
md_context = self._get_static_file_context(company_short_name)
|
|
42
|
+
if md_context:
|
|
43
|
+
context_parts.append(md_context)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logging.warning(f"Could not load Markdown context for '{company_short_name}': {e}")
|
|
46
|
+
|
|
47
|
+
# 2. Context from company-specific Python logic (SQL schemas)
|
|
48
|
+
try:
|
|
49
|
+
sql_context = self._get_sql_schema_context(company_short_name)
|
|
50
|
+
if sql_context:
|
|
51
|
+
context_parts.append(sql_context)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logging.warning(f"Could not generate SQL context for '{company_short_name}': {e}")
|
|
54
|
+
|
|
55
|
+
# Join all parts with a clear separator
|
|
56
|
+
return "\n\n---\n\n".join(context_parts)
|
|
57
|
+
|
|
58
|
+
def _get_static_file_context(self, company_short_name: str) -> str:
|
|
59
|
+
# Get context from .md and .yaml schema files.
|
|
60
|
+
static_context = ''
|
|
61
|
+
|
|
62
|
+
# Part 1: Markdown context files
|
|
63
|
+
context_dir = f'companies/{company_short_name}/context'
|
|
64
|
+
if os.path.exists(context_dir):
|
|
65
|
+
context_files = self.utility.get_files_by_extension(context_dir, '.md', return_extension=True)
|
|
66
|
+
for file in context_files:
|
|
67
|
+
filepath = os.path.join(context_dir, file)
|
|
68
|
+
static_context += self.utility.load_markdown_context(filepath)
|
|
69
|
+
|
|
70
|
+
# Part 2: YAML schema files
|
|
71
|
+
schema_dir = f'companies/{company_short_name}/schema'
|
|
72
|
+
if os.path.exists(schema_dir):
|
|
73
|
+
schema_files = self.utility.get_files_by_extension(schema_dir, '.yaml', return_extension=True)
|
|
74
|
+
for file in schema_files:
|
|
75
|
+
schema_name = file.split('.')[0] # Use full filename as entity name
|
|
76
|
+
filepath = os.path.join(schema_dir, file)
|
|
77
|
+
static_context += self.utility.generate_context_for_schema(schema_name, filepath)
|
|
78
|
+
|
|
79
|
+
return static_context
|
|
80
|
+
|
|
81
|
+
def _get_sql_schema_context(self, company_short_name: str) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Generates the SQL schema context by inspecting live database connections
|
|
84
|
+
based on the flexible company.yaml configuration.
|
|
85
|
+
It supports including all tables and providing specific overrides for a subset of them.
|
|
86
|
+
"""
|
|
87
|
+
data_sources_config = self.config_service.get_configuration(company_short_name, 'data_sources')
|
|
88
|
+
if not data_sources_config or not data_sources_config.get('sql'):
|
|
89
|
+
return ''
|
|
90
|
+
|
|
91
|
+
sql_context = ''
|
|
92
|
+
for source in data_sources_config.get('sql', []):
|
|
93
|
+
db_name = source.get('database')
|
|
94
|
+
if not db_name:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
db_manager = self.sql_service.get_database_manager(db_name)
|
|
99
|
+
except IAToolkitException as e:
|
|
100
|
+
logging.warning(f"Could not get DB manager for '{db_name}': {e}")
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
db_description = source.get('description', '')
|
|
104
|
+
sql_context += f"{db_description}\n" if db_description else ""
|
|
105
|
+
|
|
106
|
+
# 1. get the list of tables to process.
|
|
107
|
+
tables_to_process = []
|
|
108
|
+
if source.get('include_all_tables', False):
|
|
109
|
+
all_tables = db_manager.get_all_table_names()
|
|
110
|
+
tables_to_exclude = set(source.get('exclude_tables', []))
|
|
111
|
+
tables_to_process = [t for t in all_tables if t not in tables_to_exclude]
|
|
112
|
+
elif 'tables' in source:
|
|
113
|
+
# if not include_all_tables, use the list of tables explicitly specified in the map.
|
|
114
|
+
tables_to_process = list(source['tables'].keys())
|
|
115
|
+
|
|
116
|
+
# 2. get the global list of columns to exclude.
|
|
117
|
+
global_exclude_columns = source.get('exclude_columns', [])
|
|
118
|
+
|
|
119
|
+
# 3. get the overrides for specific tables.
|
|
120
|
+
table_overrides = source.get('tables', {})
|
|
121
|
+
|
|
122
|
+
# 3. iterate over the tables.
|
|
123
|
+
for table_name in tables_to_process:
|
|
124
|
+
try:
|
|
125
|
+
# 4. get the table specific configuration.
|
|
126
|
+
table_config = table_overrides.get(table_name, {})
|
|
127
|
+
|
|
128
|
+
# 5. define the schema name, using the override if it exists.
|
|
129
|
+
schema_name = table_config.get('schema_name', table_name)
|
|
130
|
+
|
|
131
|
+
# 6. define the list of columns to exclude, (local vs. global).
|
|
132
|
+
local_exclude_columns = table_config.get('exclude_columns')
|
|
133
|
+
final_exclude_columns = local_exclude_columns if local_exclude_columns is not None else global_exclude_columns
|
|
134
|
+
|
|
135
|
+
# 7. get the table schema definition.
|
|
136
|
+
table_definition = db_manager.get_table_schema(
|
|
137
|
+
table_name=table_name,
|
|
138
|
+
schema_name=schema_name,
|
|
139
|
+
exclude_columns=final_exclude_columns
|
|
140
|
+
)
|
|
141
|
+
sql_context += table_definition
|
|
142
|
+
except (KeyError, RuntimeError) as e:
|
|
143
|
+
logging.warning(f"Could not generate schema for table '{table_name}': {e}")
|
|
144
|
+
|
|
145
|
+
return sql_context
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# iatoolkit/services/configuration_service.py
|
|
2
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
3
|
+
# Product: IAToolkit
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from iatoolkit.repositories.models import Company
|
|
7
|
+
from iatoolkit.common.util import Utility
|
|
8
|
+
from injector import inject
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
class ConfigurationService:
|
|
12
|
+
"""
|
|
13
|
+
Orchestrates the configuration of a Company by reading its YAML files
|
|
14
|
+
and using the BaseCompany's protected methods to register settings.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@inject
|
|
18
|
+
def __init__(self,
|
|
19
|
+
utility: Utility):
|
|
20
|
+
self.utility = utility
|
|
21
|
+
self._loaded_configs = {} # cache for store loaded configurations
|
|
22
|
+
|
|
23
|
+
def get_configuration(self, company_short_name: str, content_key: str):
|
|
24
|
+
"""
|
|
25
|
+
Public method to provide a specific section of a company's configuration.
|
|
26
|
+
It uses a cache to avoid reading files from disk on every call.
|
|
27
|
+
"""
|
|
28
|
+
self._ensure_config_loaded(company_short_name)
|
|
29
|
+
return self._loaded_configs[company_short_name].get(content_key)
|
|
30
|
+
|
|
31
|
+
def load_configuration(self, company_short_name: str, company_instance):
|
|
32
|
+
"""
|
|
33
|
+
Main entry point for configuring a company instance.
|
|
34
|
+
This method is invoked by the dispatcher for each registered company.
|
|
35
|
+
"""
|
|
36
|
+
logging.info(f"⚙️ Starting configuration for company '{company_short_name}'...")
|
|
37
|
+
|
|
38
|
+
# 1. Load the main configuration file and supplementary content files
|
|
39
|
+
config = self._load_and_merge_configs(company_short_name)
|
|
40
|
+
|
|
41
|
+
# 2. Register core company details and get the database object
|
|
42
|
+
company_db_object = self._register_core_details(company_instance, config)
|
|
43
|
+
|
|
44
|
+
# 3. Register tools (functions)
|
|
45
|
+
self._register_tools(company_instance, config.get('tools', []))
|
|
46
|
+
|
|
47
|
+
# 4. Register prompt categories and prompts
|
|
48
|
+
self._register_prompts(company_instance, config)
|
|
49
|
+
|
|
50
|
+
# 5. Link the persisted Company object back to the running instance
|
|
51
|
+
company_instance.company_short_name = company_short_name
|
|
52
|
+
company_instance.company = company_db_object
|
|
53
|
+
company_instance.id = company_instance.company.id
|
|
54
|
+
|
|
55
|
+
logging.info(f"✅ Company '{company_short_name}' configured successfully.")
|
|
56
|
+
|
|
57
|
+
def _ensure_config_loaded(self, company_short_name: str):
|
|
58
|
+
"""
|
|
59
|
+
Checks if the configuration for a company is in the cache.
|
|
60
|
+
If not, it loads it from files and stores it.
|
|
61
|
+
"""
|
|
62
|
+
if company_short_name not in self._loaded_configs:
|
|
63
|
+
self._loaded_configs[company_short_name] = self._load_and_merge_configs(company_short_name)
|
|
64
|
+
|
|
65
|
+
def _load_and_merge_configs(self, company_short_name: str) -> dict:
|
|
66
|
+
"""
|
|
67
|
+
Loads the main company.yaml and merges data from supplementary files
|
|
68
|
+
specified in the 'content_files' section.
|
|
69
|
+
"""
|
|
70
|
+
config_dir = Path("companies") / company_short_name / "config"
|
|
71
|
+
main_config_path = config_dir / "company.yaml"
|
|
72
|
+
|
|
73
|
+
if not main_config_path.exists():
|
|
74
|
+
raise FileNotFoundError(f"Main configuration file not found: {main_config_path}")
|
|
75
|
+
|
|
76
|
+
config = self.utility.load_schema_from_yaml(main_config_path)
|
|
77
|
+
|
|
78
|
+
# Load and merge supplementary content files (e.g., onboarding_cards)
|
|
79
|
+
for key, file_path in config.get('help_files', {}).items():
|
|
80
|
+
supplementary_path = config_dir / file_path
|
|
81
|
+
if supplementary_path.exists():
|
|
82
|
+
config[key] = self.utility.load_schema_from_yaml(supplementary_path)
|
|
83
|
+
else:
|
|
84
|
+
logging.warning(f"⚠️ Warning: Content file not found: {supplementary_path}")
|
|
85
|
+
config[key] = None # Ensure the key exists but is empty
|
|
86
|
+
|
|
87
|
+
return config
|
|
88
|
+
|
|
89
|
+
def _register_core_details(self, company_instance, config: dict) -> Company:
|
|
90
|
+
"""Calls _create_company with data from the merged YAML config."""
|
|
91
|
+
return company_instance._create_company(
|
|
92
|
+
short_name=config['id'],
|
|
93
|
+
name=config['name'],
|
|
94
|
+
parameters=config.get('parameters', {})
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _register_tools(self, company_instance, tools_config: list):
|
|
98
|
+
"""Calls _create_function for each tool defined in the YAML."""
|
|
99
|
+
for tool in tools_config:
|
|
100
|
+
company_instance._create_function(
|
|
101
|
+
function_name=tool['function_name'],
|
|
102
|
+
description=tool['description'],
|
|
103
|
+
params=tool['params']
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _register_prompts(self, company_instance, config: dict):
|
|
107
|
+
"""
|
|
108
|
+
Creates prompt categories first, then creates each prompt and assigns
|
|
109
|
+
it to its respective category.
|
|
110
|
+
"""
|
|
111
|
+
prompts_config = config.get('prompts', [])
|
|
112
|
+
categories_config = config.get('prompt_categories', [])
|
|
113
|
+
|
|
114
|
+
created_categories = {}
|
|
115
|
+
for i, category_name in enumerate(categories_config):
|
|
116
|
+
category_obj = company_instance._create_prompt_category(name=category_name, order=i + 1)
|
|
117
|
+
created_categories[category_name] = category_obj
|
|
118
|
+
|
|
119
|
+
for prompt_data in prompts_config:
|
|
120
|
+
category_name = prompt_data.get('category')
|
|
121
|
+
if not category_name or category_name not in created_categories:
|
|
122
|
+
logging.info(f"⚠️ Warning: Prompt '{prompt_data['name']}' has an invalid or missing category. Skipping.")
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
category_obj = created_categories[category_name]
|
|
126
|
+
company_instance._create_prompt(
|
|
127
|
+
prompt_name=prompt_data['name'],
|
|
128
|
+
description=prompt_data['description'],
|
|
129
|
+
order=prompt_data['order'],
|
|
130
|
+
category=category_obj,
|
|
131
|
+
active=prompt_data.get('active', True),
|
|
132
|
+
custom_fields=prompt_data.get('custom_fields', [])
|
|
133
|
+
)
|