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.

Files changed (83) hide show
  1. iatoolkit/__init__.py +0 -2
  2. iatoolkit/base_company.py +1 -26
  3. iatoolkit/common/routes.py +11 -2
  4. iatoolkit/common/session_manager.py +2 -0
  5. iatoolkit/common/util.py +17 -0
  6. iatoolkit/company_registry.py +1 -2
  7. iatoolkit/iatoolkit.py +39 -6
  8. iatoolkit/locales/en.yaml +167 -0
  9. iatoolkit/locales/es.yaml +163 -0
  10. iatoolkit/repositories/database_manager.py +8 -3
  11. iatoolkit/repositories/document_repo.py +1 -1
  12. iatoolkit/repositories/models.py +1 -4
  13. iatoolkit/repositories/profile_repo.py +0 -4
  14. iatoolkit/services/auth_service.py +14 -9
  15. iatoolkit/services/branding_service.py +36 -24
  16. iatoolkit/services/company_context_service.py +145 -0
  17. iatoolkit/services/configuration_service.py +133 -0
  18. iatoolkit/services/dispatcher_service.py +51 -48
  19. iatoolkit/services/document_service.py +5 -2
  20. iatoolkit/services/excel_service.py +15 -11
  21. iatoolkit/services/file_processor_service.py +4 -12
  22. iatoolkit/services/history_service.py +8 -7
  23. iatoolkit/services/i18n_service.py +104 -0
  24. iatoolkit/services/jwt_service.py +7 -9
  25. iatoolkit/services/language_service.py +83 -0
  26. iatoolkit/services/load_documents_service.py +4 -4
  27. iatoolkit/services/mail_service.py +9 -4
  28. iatoolkit/services/profile_service.py +61 -38
  29. iatoolkit/services/prompt_manager_service.py +20 -16
  30. iatoolkit/services/query_service.py +19 -15
  31. iatoolkit/services/search_service.py +11 -4
  32. iatoolkit/services/sql_service.py +55 -25
  33. iatoolkit/services/user_feedback_service.py +16 -14
  34. iatoolkit/static/js/chat_feedback_button.js +57 -87
  35. iatoolkit/static/js/chat_help_content.js +124 -0
  36. iatoolkit/static/js/chat_history_button.js +48 -65
  37. iatoolkit/static/js/chat_main.js +27 -24
  38. iatoolkit/static/js/chat_onboarding_button.js +6 -0
  39. iatoolkit/static/js/chat_reload_button.js +28 -45
  40. iatoolkit/static/styles/chat_iatoolkit.css +223 -315
  41. iatoolkit/static/styles/chat_modal.css +63 -97
  42. iatoolkit/static/styles/chat_public.css +107 -0
  43. iatoolkit/static/styles/landing_page.css +0 -1
  44. iatoolkit/static/styles/onboarding.css +7 -0
  45. iatoolkit/templates/_company_header.html +6 -2
  46. iatoolkit/templates/_login_widget.html +42 -0
  47. iatoolkit/templates/base.html +34 -19
  48. iatoolkit/templates/change_password.html +22 -20
  49. iatoolkit/templates/chat.html +59 -27
  50. iatoolkit/templates/chat_modals.html +114 -74
  51. iatoolkit/templates/error.html +12 -13
  52. iatoolkit/templates/forgot_password.html +11 -7
  53. iatoolkit/templates/index.html +8 -3
  54. iatoolkit/templates/login_simulation.html +17 -6
  55. iatoolkit/templates/onboarding_shell.html +4 -2
  56. iatoolkit/templates/signup.html +14 -14
  57. iatoolkit/views/base_login_view.py +19 -9
  58. iatoolkit/views/change_password_view.py +50 -35
  59. iatoolkit/views/external_login_view.py +1 -1
  60. iatoolkit/views/forgot_password_view.py +21 -22
  61. iatoolkit/views/help_content_api_view.py +54 -0
  62. iatoolkit/views/history_api_view.py +13 -9
  63. iatoolkit/views/home_view.py +30 -39
  64. iatoolkit/views/init_context_api_view.py +16 -11
  65. iatoolkit/views/llmquery_api_view.py +38 -26
  66. iatoolkit/views/login_simulation_view.py +14 -2
  67. iatoolkit/views/login_view.py +52 -40
  68. iatoolkit/views/logout_api_view.py +26 -22
  69. iatoolkit/views/profile_api_view.py +46 -0
  70. iatoolkit/views/prompt_api_view.py +6 -6
  71. iatoolkit/views/signup_view.py +27 -27
  72. iatoolkit/views/user_feedback_api_view.py +19 -18
  73. iatoolkit/views/verify_user_view.py +29 -30
  74. {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/METADATA +40 -22
  75. iatoolkit-0.69.0.dist-info/RECORD +120 -0
  76. iatoolkit-0.69.0.dist-info/licenses/LICENSE +21 -0
  77. iatoolkit/services/onboarding_service.py +0 -43
  78. iatoolkit/static/styles/chat_info.css +0 -53
  79. iatoolkit/templates/header.html +0 -31
  80. iatoolkit/templates/test.html +0 -9
  81. iatoolkit-0.63.1.dist-info/RECORD +0 -112
  82. {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/WHEEL +0 -0
  83. {iatoolkit-0.63.1.dist-info → iatoolkit-0.69.0.dist-info}/top_level.txt +0 -0
@@ -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': 'Token inválido o expirado'}
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"Error al crear la sesión desde token para {user_identifier}: {e}")
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': 'No se pudo crear la sesión del usuario'}
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": "Authentication required. No session cookie or API Key provided.",
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, "error_message": "Invalid or inactive API Key",
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, "error_message": "No user_identifier provided for API call.",
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"Fallo al escribir en AccessLog: {e}", exc_info=False)
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
- Servicio centralizado que gestiona la configuración de branding.
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": "bold",
23
- "primary_font_size": "1rem",
24
- "secondary_font_weight": "600",
25
- "secondary_font_size": "0.875rem",
26
- "tertiary_font_weight": "normal",
27
- "tertiary_font_size": "0.75rem",
28
- "tertiary_opacity": "0.8",
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": "#cff4fc", # Fondo celeste pálido
44
- "brand_info_text": "#055160", # Texto azul oscuro
45
- "brand_info_border": "#b6effb",
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
- "prompt_assistant_item_hover_bg": None, # Usará el primario por defecto
58
- "prompt_assistant_item_hover_text": None, # Usará el texto sobre primario
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" # Gris oscuro/casi negro por defecto
69
+ "send_button_color": "#212529" # Gris oscuro/casi negro por defecto
62
70
  }
63
71
 
64
- def get_company_branding(self, company: Company | None) -> dict:
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": company.name if company else "IAToolkit",
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['send_button_color']
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
+ )