iatoolkit 0.3.9__py3-none-any.whl → 0.107.4__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 (150) hide show
  1. iatoolkit/__init__.py +27 -35
  2. iatoolkit/base_company.py +3 -35
  3. iatoolkit/cli_commands.py +18 -47
  4. iatoolkit/common/__init__.py +0 -0
  5. iatoolkit/common/exceptions.py +48 -0
  6. iatoolkit/common/interfaces/__init__.py +0 -0
  7. iatoolkit/common/interfaces/asset_storage.py +34 -0
  8. iatoolkit/common/interfaces/database_provider.py +39 -0
  9. iatoolkit/common/model_registry.py +159 -0
  10. iatoolkit/common/routes.py +138 -0
  11. iatoolkit/common/session_manager.py +26 -0
  12. iatoolkit/common/util.py +353 -0
  13. iatoolkit/company_registry.py +66 -29
  14. iatoolkit/core.py +514 -0
  15. iatoolkit/infra/__init__.py +5 -0
  16. iatoolkit/infra/brevo_mail_app.py +123 -0
  17. iatoolkit/infra/call_service.py +140 -0
  18. iatoolkit/infra/connectors/__init__.py +5 -0
  19. iatoolkit/infra/connectors/file_connector.py +17 -0
  20. iatoolkit/infra/connectors/file_connector_factory.py +57 -0
  21. iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
  22. iatoolkit/infra/connectors/google_drive_connector.py +68 -0
  23. iatoolkit/infra/connectors/local_file_connector.py +46 -0
  24. iatoolkit/infra/connectors/s3_connector.py +33 -0
  25. iatoolkit/infra/google_chat_app.py +57 -0
  26. iatoolkit/infra/llm_providers/__init__.py +0 -0
  27. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  28. iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
  29. iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
  30. iatoolkit/infra/llm_proxy.py +268 -0
  31. iatoolkit/infra/llm_response.py +45 -0
  32. iatoolkit/infra/redis_session_manager.py +122 -0
  33. iatoolkit/locales/en.yaml +222 -0
  34. iatoolkit/locales/es.yaml +225 -0
  35. iatoolkit/repositories/__init__.py +5 -0
  36. iatoolkit/repositories/database_manager.py +187 -0
  37. iatoolkit/repositories/document_repo.py +33 -0
  38. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  39. iatoolkit/repositories/llm_query_repo.py +105 -0
  40. iatoolkit/repositories/models.py +279 -0
  41. iatoolkit/repositories/profile_repo.py +171 -0
  42. iatoolkit/repositories/vs_repo.py +150 -0
  43. iatoolkit/services/__init__.py +5 -0
  44. iatoolkit/services/auth_service.py +193 -0
  45. {services → iatoolkit/services}/benchmark_service.py +7 -7
  46. iatoolkit/services/branding_service.py +153 -0
  47. iatoolkit/services/company_context_service.py +214 -0
  48. iatoolkit/services/configuration_service.py +375 -0
  49. iatoolkit/services/dispatcher_service.py +134 -0
  50. {services → iatoolkit/services}/document_service.py +20 -8
  51. iatoolkit/services/embedding_service.py +148 -0
  52. iatoolkit/services/excel_service.py +156 -0
  53. {services → iatoolkit/services}/file_processor_service.py +36 -21
  54. iatoolkit/services/history_manager_service.py +208 -0
  55. iatoolkit/services/i18n_service.py +104 -0
  56. iatoolkit/services/jwt_service.py +80 -0
  57. iatoolkit/services/language_service.py +89 -0
  58. iatoolkit/services/license_service.py +82 -0
  59. iatoolkit/services/llm_client_service.py +438 -0
  60. iatoolkit/services/load_documents_service.py +174 -0
  61. iatoolkit/services/mail_service.py +213 -0
  62. {services → iatoolkit/services}/profile_service.py +200 -101
  63. iatoolkit/services/prompt_service.py +303 -0
  64. iatoolkit/services/query_service.py +467 -0
  65. iatoolkit/services/search_service.py +55 -0
  66. iatoolkit/services/sql_service.py +169 -0
  67. iatoolkit/services/tool_service.py +246 -0
  68. iatoolkit/services/user_feedback_service.py +117 -0
  69. iatoolkit/services/user_session_context_service.py +213 -0
  70. iatoolkit/static/images/fernando.jpeg +0 -0
  71. iatoolkit/static/images/iatoolkit_core.png +0 -0
  72. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  73. iatoolkit/static/js/chat_feedback_button.js +80 -0
  74. iatoolkit/static/js/chat_filepond.js +85 -0
  75. iatoolkit/static/js/chat_help_content.js +124 -0
  76. iatoolkit/static/js/chat_history_button.js +110 -0
  77. iatoolkit/static/js/chat_logout_button.js +36 -0
  78. iatoolkit/static/js/chat_main.js +401 -0
  79. iatoolkit/static/js/chat_model_selector.js +227 -0
  80. iatoolkit/static/js/chat_onboarding_button.js +103 -0
  81. iatoolkit/static/js/chat_prompt_manager.js +94 -0
  82. iatoolkit/static/js/chat_reload_button.js +38 -0
  83. iatoolkit/static/styles/chat_iatoolkit.css +559 -0
  84. iatoolkit/static/styles/chat_modal.css +133 -0
  85. iatoolkit/static/styles/chat_public.css +135 -0
  86. iatoolkit/static/styles/documents.css +598 -0
  87. iatoolkit/static/styles/landing_page.css +398 -0
  88. iatoolkit/static/styles/llm_output.css +148 -0
  89. iatoolkit/static/styles/onboarding.css +176 -0
  90. iatoolkit/system_prompts/__init__.py +0 -0
  91. iatoolkit/system_prompts/query_main.prompt +30 -23
  92. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  93. iatoolkit/templates/_company_header.html +45 -0
  94. iatoolkit/templates/_login_widget.html +42 -0
  95. iatoolkit/templates/base.html +78 -0
  96. iatoolkit/templates/change_password.html +66 -0
  97. iatoolkit/templates/chat.html +337 -0
  98. iatoolkit/templates/chat_modals.html +185 -0
  99. iatoolkit/templates/error.html +51 -0
  100. iatoolkit/templates/forgot_password.html +51 -0
  101. iatoolkit/templates/onboarding_shell.html +106 -0
  102. iatoolkit/templates/signup.html +79 -0
  103. iatoolkit/views/__init__.py +5 -0
  104. iatoolkit/views/base_login_view.py +96 -0
  105. iatoolkit/views/change_password_view.py +116 -0
  106. iatoolkit/views/chat_view.py +76 -0
  107. iatoolkit/views/embedding_api_view.py +65 -0
  108. iatoolkit/views/forgot_password_view.py +75 -0
  109. iatoolkit/views/help_content_api_view.py +54 -0
  110. iatoolkit/views/history_api_view.py +56 -0
  111. iatoolkit/views/home_view.py +63 -0
  112. iatoolkit/views/init_context_api_view.py +74 -0
  113. iatoolkit/views/llmquery_api_view.py +59 -0
  114. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  115. iatoolkit/views/load_document_api_view.py +65 -0
  116. iatoolkit/views/login_view.py +170 -0
  117. iatoolkit/views/logout_api_view.py +57 -0
  118. iatoolkit/views/profile_api_view.py +46 -0
  119. iatoolkit/views/prompt_api_view.py +37 -0
  120. iatoolkit/views/root_redirect_view.py +22 -0
  121. iatoolkit/views/signup_view.py +100 -0
  122. iatoolkit/views/static_page_view.py +27 -0
  123. iatoolkit/views/user_feedback_api_view.py +60 -0
  124. iatoolkit/views/users_api_view.py +33 -0
  125. iatoolkit/views/verify_user_view.py +60 -0
  126. iatoolkit-0.107.4.dist-info/METADATA +268 -0
  127. iatoolkit-0.107.4.dist-info/RECORD +132 -0
  128. iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
  129. iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  130. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
  131. iatoolkit/iatoolkit.py +0 -413
  132. iatoolkit/system_prompts/arquitectura.prompt +0 -32
  133. iatoolkit-0.3.9.dist-info/METADATA +0 -252
  134. iatoolkit-0.3.9.dist-info/RECORD +0 -32
  135. services/__init__.py +0 -5
  136. services/api_service.py +0 -75
  137. services/dispatcher_service.py +0 -351
  138. services/excel_service.py +0 -98
  139. services/history_service.py +0 -45
  140. services/jwt_service.py +0 -91
  141. services/load_documents_service.py +0 -212
  142. services/mail_service.py +0 -62
  143. services/prompt_manager_service.py +0 -172
  144. services/query_service.py +0 -334
  145. services/search_service.py +0 -32
  146. services/sql_service.py +0 -42
  147. services/tasks_service.py +0 -188
  148. services/user_feedback_service.py +0 -67
  149. services/user_session_context_service.py +0 -85
  150. {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,214 @@
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.common.interfaces.asset_storage import AssetRepository, AssetType
9
+ from iatoolkit.services.sql_service import SqlService
10
+ from iatoolkit.common.exceptions import IAToolkitException
11
+ import logging
12
+ from injector import inject
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
+ asset_repo: AssetRepository):
27
+ self.sql_service = sql_service
28
+ self.utility = utility
29
+ self.config_service = config_service
30
+ self.asset_repo = asset_repo
31
+
32
+ def get_company_context(self, company_short_name: str) -> str:
33
+ """
34
+ Builds the full context by aggregating three sources:
35
+ 1. Static context files (Markdown).
36
+ 2. Static schema files (YAML files for SQL data sources).
37
+ """
38
+ context_parts = []
39
+
40
+ # 1. Context from Markdown (context/*.md) files
41
+ try:
42
+ md_context = self._get_static_file_context(company_short_name)
43
+ if md_context:
44
+ context_parts.append(md_context)
45
+ except Exception as e:
46
+ logging.warning(f"Could not load Markdown context for '{company_short_name}': {e}")
47
+
48
+ # 2. Context from company-specific SQL databases
49
+ try:
50
+ sql_context = self._get_sql_schema_context(company_short_name)
51
+ if sql_context:
52
+ context_parts.append(sql_context)
53
+ except Exception as e:
54
+ logging.warning(f"Could not generate SQL context for '{company_short_name}': {e}")
55
+
56
+ # 3. Context from yaml (schema/*.yaml) files
57
+ try:
58
+ yaml_schema_context = self._get_yaml_schema_context(company_short_name)
59
+ if yaml_schema_context:
60
+ context_parts.append(yaml_schema_context)
61
+ except Exception as e:
62
+ logging.warning(f"Could not load Yaml context for '{company_short_name}': {e}")
63
+
64
+ # Join all parts with a clear separator
65
+ return "\n\n---\n\n".join(context_parts)
66
+
67
+ def _get_static_file_context(self, company_short_name: str) -> str:
68
+ # Get context from .md files using the repository
69
+ static_context = ''
70
+
71
+ try:
72
+ # 1. List markdown files in the context "folder"
73
+ # Note: The repo handles where this folder actually is (FS or DB)
74
+ md_files = self.asset_repo.list_files(company_short_name, AssetType.CONTEXT, extension='.md')
75
+
76
+ for filename in md_files:
77
+ try:
78
+ # 2. Read content
79
+ content = self.asset_repo.read_text(company_short_name, AssetType.CONTEXT, filename)
80
+ static_context += content + "\n" # Append content
81
+ except Exception as e:
82
+ logging.warning(f"Error reading context file {filename}: {e}")
83
+
84
+ except Exception as e:
85
+ # If listing fails (e.g. folder doesn't exist), just log and return empty
86
+ logging.warning(f"Error listing context files for {company_short_name}: {e}")
87
+
88
+ return static_context
89
+
90
+ def _get_sql_schema_context(self, company_short_name: str) -> str:
91
+ """
92
+ Generates the SQL schema context by inspecting live database connections
93
+ based on the flexible company.yaml configuration.
94
+ It supports including all tables and providing specific overrides for a subset of them.
95
+ """
96
+ data_sources_config = self.config_service.get_configuration(company_short_name, 'data_sources')
97
+ if not data_sources_config or not data_sources_config.get('sql'):
98
+ return ''
99
+
100
+ sql_context = ''
101
+ for source in data_sources_config.get('sql', []):
102
+ db_name = source.get('database')
103
+ if not db_name:
104
+ continue
105
+
106
+ try:
107
+ db_provider = self.sql_service.get_database_provider(company_short_name, db_name)
108
+ except IAToolkitException as e:
109
+ logging.warning(f"Could not get DB provider for '{db_name}': {e}")
110
+ continue
111
+
112
+ db_description = source.get('description', '')
113
+ sql_context = f"***Database (`database_key`)***: {db_name}\n"
114
+
115
+ if db_description:
116
+ sql_context += (
117
+ f"**Description:** : {db_description}\n"
118
+ )
119
+
120
+ sql_context += (
121
+ f"IMPORTANT: To query this database you MUST use the service/tool "
122
+ f"**iat_sql_query**, with `database_key={db_name}`.\n"
123
+ )
124
+
125
+ sql_context += (
126
+ f"IMPORTANT: The value of **database_key** is ALWAYS the literal string "
127
+ f"'{db_name}'. Do not invent or infer alternative names. "
128
+ f"Use exactly: `database_key='{db_name}'`.\n"
129
+ )
130
+
131
+ # 1. get the list of tables to process.
132
+ tables_to_process = []
133
+ if source.get('include_all_tables', False):
134
+ all_tables = db_provider.get_all_table_names()
135
+ tables_to_exclude = set(source.get('exclude_tables', []))
136
+ tables_to_process = [t for t in all_tables if t not in tables_to_exclude]
137
+ elif 'tables' in source:
138
+ # if not include_all_tables, use the list of tables explicitly specified in the map.
139
+ tables_to_process = list(source['tables'].keys())
140
+
141
+ # 2. get the global settings and overrides.
142
+ global_exclude_columns = source.get('exclude_columns', [])
143
+ table_prefix = source.get('table_prefix')
144
+
145
+ # get database schema definition, for this source.
146
+ database_schema_name = source.get('schema')
147
+
148
+ table_overrides = source.get('tables', {})
149
+
150
+ # 3. iterate over the tables.
151
+ for table_name in tables_to_process:
152
+ try:
153
+ # 4. get the table specific configuration.
154
+ table_config = table_overrides.get(table_name, {})
155
+
156
+ # 5. define the schema object name, using the override if it exists.
157
+ # Priority 1: Explicit override from the 'tables' map.
158
+ schema_object_name = table_config.get('schema_name')
159
+
160
+ if not schema_object_name:
161
+ # Priority 3: Automatic prefix stripping.
162
+ if table_prefix and table_name.startswith(table_prefix):
163
+ schema_object_name = table_name[len(table_prefix):]
164
+ else:
165
+ # Priority 4: Default to the table name itself.
166
+ schema_object_name = table_name
167
+
168
+ # 6. define the list of columns to exclude, (local vs. global).
169
+ local_exclude_columns = table_config.get('exclude_columns')
170
+ final_exclude_columns = local_exclude_columns if local_exclude_columns is not None else global_exclude_columns
171
+
172
+ # 7. get the table schema definition.
173
+ table_definition = db_provider.get_table_schema(
174
+ table_name=table_name,
175
+ db_schema=db_provider.schema,
176
+ schema_object_name=schema_object_name,
177
+ exclude_columns=final_exclude_columns
178
+ )
179
+ sql_context += table_definition
180
+ except (KeyError, RuntimeError) as e:
181
+ logging.warning(f"Could not generate schema for table '{table_name}': {e}")
182
+
183
+ if sql_context:
184
+ sql_context = "These are the SQL databases you can query using the **`iat_sql_service`**: \n" + sql_context
185
+ return sql_context
186
+
187
+ def _get_yaml_schema_context(self, company_short_name: str) -> str:
188
+ # Get context from .yaml schema files using the repository
189
+ yaml_schema_context = ''
190
+
191
+ try:
192
+ # 1. List yaml files in the schema "folder"
193
+ schema_files = self.asset_repo.list_files(company_short_name, AssetType.SCHEMA, extension='.yaml')
194
+
195
+ for filename in schema_files:
196
+ try:
197
+ # 2. Read content
198
+ content = self.asset_repo.read_text(company_short_name, AssetType.SCHEMA, filename)
199
+
200
+ # 3. Parse YAML content into a dict
201
+ schema_dict = self.utility.load_yaml_from_string(content)
202
+
203
+ # 4. Generate markdown description from the dict
204
+ if schema_dict:
205
+ # We use generate_schema_table which accepts a dict directly
206
+ yaml_schema_context += self.utility.generate_schema_table(schema_dict)
207
+
208
+ except Exception as e:
209
+ logging.warning(f"Error processing schema file {filename}: {e}")
210
+
211
+ except Exception as e:
212
+ logging.warning(f"Error listing schema files for {company_short_name}: {e}")
213
+
214
+ return yaml_schema_context
@@ -0,0 +1,375 @@
1
+ # iatoolkit/services/configuration_service.py
2
+ # Copyright (c) 2024 Fernando Libedinsky
3
+ # Product: IAToolkit
4
+
5
+ from iatoolkit.repositories.models import Company
6
+ from iatoolkit.common.interfaces.asset_storage import AssetRepository, AssetType
7
+ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
+ from iatoolkit.repositories.profile_repo import ProfileRepo
9
+ from iatoolkit.common.util import Utility
10
+ from injector import inject
11
+ import logging
12
+ import os
13
+
14
+
15
+ class ConfigurationService:
16
+ """
17
+ Orchestrates the configuration of a Company by reading its YAML files
18
+ and using the BaseCompany's protected methods to register settings.
19
+ """
20
+
21
+ @inject
22
+ def __init__(self,
23
+ asset_repo: AssetRepository,
24
+ llm_query_repo: LLMQueryRepo,
25
+ profile_repo: ProfileRepo,
26
+ utility: Utility):
27
+ self.asset_repo = asset_repo
28
+ self.llm_query_repo = llm_query_repo
29
+ self.profile_repo = profile_repo
30
+ self.utility = utility
31
+ self._loaded_configs = {} # cache for store loaded configurations
32
+
33
+ def _ensure_config_loaded(self, company_short_name: str):
34
+ """
35
+ Checks if the configuration for a company is in the cache.
36
+ If not, it loads it from files and stores it.
37
+ """
38
+ if company_short_name not in self._loaded_configs:
39
+ self._loaded_configs[company_short_name] = self._load_and_merge_configs(company_short_name)
40
+
41
+ def get_configuration(self, company_short_name: str, content_key: str):
42
+ """
43
+ Public method to provide a specific section of a company's configuration.
44
+ It uses a cache to avoid reading files from disk on every call.
45
+ """
46
+ self._ensure_config_loaded(company_short_name)
47
+ return self._loaded_configs[company_short_name].get(content_key)
48
+
49
+ def get_llm_configuration(self, company_short_name: str):
50
+ """
51
+ Convenience helper to obtain the 'llm' configuration block for a company.
52
+ Kept separate from get_configuration() to avoid coupling tests that
53
+ assert the number of calls to get_configuration().
54
+ """
55
+ default_llm_model = None
56
+ available_llm_models = []
57
+ self._ensure_config_loaded(company_short_name)
58
+ llm_config = self._loaded_configs[company_short_name].get("llm")
59
+ if llm_config:
60
+ default_llm_model = llm_config.get("model")
61
+ available_llm_models = llm_config.get('available_models') or []
62
+
63
+ # fallback: if no explicit list of models is provided, use the default model
64
+ if not available_llm_models and default_llm_model:
65
+ available_llm_models = [{
66
+ "id": default_llm_model,
67
+ "label": default_llm_model,
68
+ "description": "Modelo por defecto configurado para esta compañía."
69
+ }]
70
+ return default_llm_model, available_llm_models
71
+
72
+ def load_configuration(self, company_short_name: str):
73
+ """
74
+ Main entry point for configuring a company instance.
75
+ This method is invoked by the dispatcher for each registered company.
76
+ """
77
+ logging.info(f"⚙️ Starting configuration for company '{company_short_name}'...")
78
+
79
+ # 1. Load the main configuration file and supplementary content files
80
+ config = self._load_and_merge_configs(company_short_name)
81
+ if config:
82
+ # 2. create/update company in database
83
+ self._register_company_database(config)
84
+
85
+ # 3. Register config databases with db manager
86
+ # self.register_data_sources(company_short_name, config=config)
87
+
88
+ # 4. Register tools
89
+ self._register_tools(company_short_name, config)
90
+
91
+ # 5. Register prompt categories and prompts
92
+ self._register_prompts(company_short_name, config)
93
+
94
+ # Final step: validate the configuration against platform
95
+ errors = self._validate_configuration(company_short_name, config)
96
+
97
+ logging.info(f"✅ Company '{company_short_name}' configured successfully.")
98
+ return config, errors
99
+
100
+ def _load_and_merge_configs(self, company_short_name: str) -> dict:
101
+ """
102
+ Loads the main company.yaml and merges data from supplementary files
103
+ specified in the 'content_files' section using AssetRepository.
104
+ """
105
+ main_config_filename = "company.yaml"
106
+
107
+ # verify existence of the main configuration file
108
+ if not self.asset_repo.exists(company_short_name, AssetType.CONFIG, main_config_filename):
109
+ # raise FileNotFoundError(f"Main configuration file not found: {main_config_filename}")
110
+ logging.exception(f"Main configuration file not found: {main_config_filename}")
111
+
112
+ # return the minimal configuration needed for starting the IAToolkit
113
+ # this is a for solving a chicken/egg problem when trying to migrate the configuration
114
+ # from filesystem to database in enterprise installation
115
+ # see create_assets cli command in enterprise-iatoolkit)
116
+ return {
117
+ 'id': company_short_name,
118
+ 'name': company_short_name,
119
+ 'llm': {'model': 'gpt-5', 'provider_api_keys': {'openai':''} },
120
+ }
121
+
122
+ # read text and parse
123
+ yaml_content = self.asset_repo.read_text(company_short_name, AssetType.CONFIG, main_config_filename)
124
+ config = self.utility.load_yaml_from_string(yaml_content)
125
+ if not config:
126
+ return {}
127
+
128
+ # Load and merge supplementary content files (e.g., onboarding_cards)
129
+ for key, filename in config.get('help_files', {}).items():
130
+ if self.asset_repo.exists(company_short_name, AssetType.CONFIG, filename):
131
+ supp_content = self.asset_repo.read_text(company_short_name, AssetType.CONFIG, filename)
132
+ config[key] = self.utility.load_yaml_from_string(supp_content)
133
+ else:
134
+ logging.warning(f"⚠️ Warning: Content file not found: {filename}")
135
+ config[key] = None
136
+
137
+ return config
138
+
139
+ def _register_company_database(self, config: dict) -> Company:
140
+ # register the company in the database: create_or_update logic
141
+ if not config:
142
+ return None
143
+
144
+ # create or update the company in database
145
+ company_obj = Company(short_name=config.get('id'),
146
+ name=config.get('name'),
147
+ parameters=config.get('parameters', {}))
148
+ company = self.profile_repo.create_company(company_obj)
149
+
150
+ # save company object with the configuration
151
+ config['company'] = company
152
+
153
+ return company
154
+
155
+ def register_data_sources(self,
156
+ company_short_name: str,
157
+ config: dict = None):
158
+ """
159
+ Reads the data_sources config and registers databases with SqlService.
160
+ Uses Lazy Loading to avoid circular dependency.
161
+
162
+ Public method: Can be called externally after initialization (e.g. by Enterprise)
163
+ to re-register sources once new factories (like 'bridge') are available.
164
+ """
165
+
166
+ # If config is not provided, try to load it from cache
167
+ if config is None:
168
+ self._ensure_config_loaded(company_short_name)
169
+ config = self._loaded_configs.get(company_short_name)
170
+
171
+ if not config:
172
+ return
173
+
174
+ from iatoolkit import current_iatoolkit
175
+ from iatoolkit.services.sql_service import SqlService
176
+ sql_service = current_iatoolkit().get_injector().get(SqlService)
177
+
178
+ data_sources = config.get('data_sources', {})
179
+ sql_sources = data_sources.get('sql', [])
180
+
181
+ if not sql_sources:
182
+ return
183
+
184
+ logging.info(f"🛢️ Registering databases for '{company_short_name}'...")
185
+
186
+ for source in sql_sources:
187
+ db_name = source.get('database')
188
+ if not db_name:
189
+ continue
190
+
191
+ # Prepare the config dictionary for the factory
192
+ db_config = {
193
+ 'schema': source.get('schema', 'public'),
194
+ 'connection_type': source.get('connection_type', 'direct'),
195
+ # Pass through keys needed for Bridge or other plugins
196
+ 'bridge_id': source.get('bridge_id'),
197
+ 'timeout': source.get('timeout')
198
+ }
199
+
200
+ # Resolve URI if env var is present (Required for 'direct', optional for others)
201
+ db_env_var = source.get('connection_string_env')
202
+ if db_env_var:
203
+ db_uri = os.getenv(db_env_var)
204
+ if db_uri:
205
+ db_config['db_uri'] = db_uri
206
+
207
+ # Validation: 'direct' connections MUST have a URI
208
+ if db_config['connection_type'] == 'direct' and not db_config.get('db_uri'):
209
+ logging.error(
210
+ f"-> Skipping DB '{db_name}' for '{company_short_name}': missing URI in env '{db_env_var}'.")
211
+ continue
212
+
213
+ # Register with the SQL service
214
+ sql_service.register_database(company_short_name, db_name, db_config)
215
+
216
+ def _register_tools(self, company_short_name: str, config: dict):
217
+ """creates in the database each tool defined in the YAML."""
218
+ # Lazy import and resolve ToolService locally
219
+ from iatoolkit import current_iatoolkit
220
+ from iatoolkit.services.tool_service import ToolService
221
+ tool_service = current_iatoolkit().get_injector().get(ToolService)
222
+
223
+ tools_config = config.get('tools', [])
224
+ tool_service.sync_company_tools(company_short_name, tools_config)
225
+
226
+ def _register_prompts(self, company_short_name: str, config: dict):
227
+ """
228
+ Delegates prompt synchronization to PromptService.
229
+ """
230
+ # Lazy import to avoid circular dependency
231
+ from iatoolkit import current_iatoolkit
232
+ from iatoolkit.services.prompt_service import PromptService
233
+ prompt_service = current_iatoolkit().get_injector().get(PromptService)
234
+
235
+ prompts_config = config.get('prompts', [])
236
+ categories_config = config.get('prompt_categories', [])
237
+
238
+ prompt_service.sync_company_prompts(
239
+ company_short_name=company_short_name,
240
+ prompts_config=prompts_config,
241
+ categories_config=categories_config
242
+ )
243
+
244
+ def _validate_configuration(self, company_short_name: str, config: dict):
245
+ """
246
+ Validates the structure and consistency of the company.yaml configuration.
247
+ It checks for required keys, valid values, and existence of related files.
248
+ Raises IAToolkitException if any validation error is found.
249
+ """
250
+ errors = []
251
+
252
+ # Helper to collect errors
253
+ def add_error(section, message):
254
+ errors.append(f"[{section}] {message}")
255
+
256
+ if not config:
257
+ add_error("General", "Configuration file missing or with errors, check the application logs.")
258
+ return errors
259
+
260
+ # 1. Top-level keys
261
+ if not config.get("id"):
262
+ add_error("General", "Missing required key: 'id'")
263
+ elif config["id"] != company_short_name:
264
+ add_error("General",
265
+ f"'id' ({config['id']}) does not match the company short name ('{company_short_name}').")
266
+ if not config.get("name"):
267
+ add_error("General", "Missing required key: 'name'")
268
+
269
+ # 2. LLM section
270
+ if not isinstance(config.get("llm"), dict):
271
+ add_error("llm", "Missing or invalid 'llm' section.")
272
+ else:
273
+ if not config.get("llm", {}).get("model"):
274
+ add_error("llm", "Missing required key: 'model'")
275
+ if not config.get("llm", {}).get("provider_api_keys"):
276
+ add_error("llm", "Missing required key: 'provider_api_keys'")
277
+
278
+ # 3. Embedding Provider
279
+ if isinstance(config.get("embedding_provider"), dict):
280
+ if not config.get("embedding_provider", {}).get("provider"):
281
+ add_error("embedding_provider", "Missing required key: 'provider'")
282
+ if not config.get("embedding_provider", {}).get("model"):
283
+ add_error("embedding_provider", "Missing required key: 'model'")
284
+ if not config.get("embedding_provider", {}).get("api_key_name"):
285
+ add_error("embedding_provider", "Missing required key: 'api_key_name'")
286
+
287
+ # 4. Data Sources
288
+ for i, source in enumerate(config.get("data_sources", {}).get("sql", [])):
289
+ if not source.get("database"):
290
+ add_error(f"data_sources.sql[{i}]", "Missing required key: 'database'")
291
+
292
+ connection_type = source.get("connection_type")
293
+ if connection_type == 'direct' and not source.get("connection_string_env"):
294
+ add_error(f"data_sources.sql[{i}]", "Missing required key: 'connection_string_env'")
295
+ elif connection_type == 'bridge' and not source.get("bridge_id"):
296
+ add_error(f"data_sources.sql[{i}]", "Missing bridge_id'")
297
+
298
+ # 5. Tools
299
+ for i, tool in enumerate(config.get("tools", [])):
300
+ function_name = tool.get("function_name")
301
+ if not function_name:
302
+ add_error(f"tools[{i}]", "Missing required key: 'function_name'")
303
+
304
+ # check that function exist in dispatcher
305
+ if not tool.get("description"):
306
+ add_error(f"tools[{i}]", "Missing required key: 'description'")
307
+ if not isinstance(tool.get("params"), dict):
308
+ add_error(f"tools[{i}]", "'params' key must be a dictionary.")
309
+
310
+ # 6. Prompts
311
+ category_set = set(config.get("prompt_categories", []))
312
+ for i, prompt in enumerate(config.get("prompts", [])):
313
+ prompt_name = prompt.get("name")
314
+ if not prompt_name:
315
+ add_error(f"prompts[{i}]", "Missing required key: 'name'")
316
+ else:
317
+ prompt_filename = f"{prompt_name}.prompt"
318
+ if not self.asset_repo.exists(company_short_name, AssetType.PROMPT, prompt_filename):
319
+ add_error(f"prompts/{prompt_name}:", f"Prompt file not found: {prompt_filename}")
320
+
321
+ prompt_description = prompt.get("description")
322
+ if not prompt_description:
323
+ add_error(f"prompts[{i}]", "Missing required key: 'description'")
324
+
325
+ prompt_cat = prompt.get("category")
326
+ if not prompt_cat:
327
+ add_error(f"prompts[{i}]", "Missing required key: 'category'")
328
+ elif prompt_cat not in category_set:
329
+ add_error(f"prompts[{i}]", f"Category '{prompt_cat}' is not defined in 'prompt_categories'.")
330
+
331
+ # 7. User Feedback
332
+ feedback_config = config.get("parameters", {}).get("user_feedback", {})
333
+ if feedback_config.get("channel") == "email" and not feedback_config.get("destination"):
334
+ add_error("parameters.user_feedback", "When channel is 'email', a 'destination' is required.")
335
+
336
+ # 8. Knowledge Base
337
+ kb_config = config.get("knowledge_base", {})
338
+ if kb_config and not isinstance(kb_config, dict):
339
+ add_error("knowledge_base", "Section must be a dictionary.")
340
+ elif kb_config:
341
+ prod_connector = kb_config.get("connectors", {}).get("production", {})
342
+ if prod_connector.get("type") == "s3":
343
+ for key in ["bucket", "prefix", "aws_access_key_id_env", "aws_secret_access_key_env", "aws_region_env"]:
344
+ if not prod_connector.get(key):
345
+ add_error("knowledge_base.connectors.production", f"S3 connector is missing '{key}'.")
346
+
347
+ # 9. Mail Provider
348
+ mail_config = config.get("mail_provider", {})
349
+ if mail_config:
350
+ provider = mail_config.get("provider")
351
+ if not provider:
352
+ add_error("mail_provider", "Missing required key: 'provider'")
353
+ elif provider not in ["brevo_mail", "smtplib"]:
354
+ add_error("mail_provider", f"Unsupported provider: '{provider}'. Must be 'brevo_mail' or 'smtplib'.")
355
+
356
+ if not mail_config.get("sender_email"):
357
+ add_error("mail_provider", "Missing required key: 'sender_email'")
358
+
359
+ # 10. Help Files
360
+ for key, filename in config.get("help_files", {}).items():
361
+ if not filename:
362
+ add_error(f"help_files.{key}", "Filename cannot be empty.")
363
+ continue
364
+ if not self.asset_repo.exists(company_short_name, AssetType.CONFIG, filename):
365
+ add_error(f"help_files.{key}", f"Help file not found: {filename}")
366
+
367
+
368
+ # If any errors were found, log all messages and raise an exception
369
+ if errors:
370
+ error_summary = f"Configuration file '{company_short_name}/config/company.yaml' for '{company_short_name}' has validation errors:\n" + "\n".join(
371
+ f" - {e}" for e in errors)
372
+ logging.error(error_summary)
373
+
374
+ return errors
375
+