iatoolkit 0.91.1__py3-none-any.whl → 1.7.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.
Files changed (71) hide show
  1. iatoolkit/__init__.py +6 -4
  2. iatoolkit/base_company.py +0 -16
  3. iatoolkit/cli_commands.py +3 -14
  4. iatoolkit/common/exceptions.py +1 -0
  5. iatoolkit/common/interfaces/__init__.py +0 -0
  6. iatoolkit/common/interfaces/asset_storage.py +34 -0
  7. iatoolkit/common/interfaces/database_provider.py +43 -0
  8. iatoolkit/common/model_registry.py +159 -0
  9. iatoolkit/common/routes.py +47 -5
  10. iatoolkit/common/util.py +32 -13
  11. iatoolkit/company_registry.py +5 -0
  12. iatoolkit/core.py +51 -20
  13. iatoolkit/infra/connectors/file_connector_factory.py +1 -0
  14. iatoolkit/infra/connectors/s3_connector.py +4 -2
  15. iatoolkit/infra/llm_providers/__init__.py +0 -0
  16. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  17. iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
  18. iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
  19. iatoolkit/infra/llm_proxy.py +235 -134
  20. iatoolkit/infra/llm_response.py +5 -0
  21. iatoolkit/locales/en.yaml +158 -2
  22. iatoolkit/locales/es.yaml +158 -0
  23. iatoolkit/repositories/database_manager.py +52 -47
  24. iatoolkit/repositories/document_repo.py +7 -0
  25. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  26. iatoolkit/repositories/llm_query_repo.py +2 -0
  27. iatoolkit/repositories/models.py +72 -79
  28. iatoolkit/repositories/profile_repo.py +59 -3
  29. iatoolkit/repositories/vs_repo.py +22 -24
  30. iatoolkit/services/company_context_service.py +126 -53
  31. iatoolkit/services/configuration_service.py +299 -73
  32. iatoolkit/services/dispatcher_service.py +21 -3
  33. iatoolkit/services/file_processor_service.py +0 -5
  34. iatoolkit/services/history_manager_service.py +43 -24
  35. iatoolkit/services/knowledge_base_service.py +425 -0
  36. iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +38 -29
  37. iatoolkit/services/load_documents_service.py +26 -48
  38. iatoolkit/services/profile_service.py +32 -4
  39. iatoolkit/services/prompt_service.py +32 -30
  40. iatoolkit/services/query_service.py +51 -26
  41. iatoolkit/services/sql_service.py +122 -74
  42. iatoolkit/services/tool_service.py +26 -11
  43. iatoolkit/services/user_session_context_service.py +115 -63
  44. iatoolkit/static/js/chat_main.js +44 -4
  45. iatoolkit/static/js/chat_model_selector.js +227 -0
  46. iatoolkit/static/js/chat_onboarding_button.js +1 -1
  47. iatoolkit/static/js/chat_reload_button.js +4 -1
  48. iatoolkit/static/styles/chat_iatoolkit.css +58 -2
  49. iatoolkit/static/styles/llm_output.css +34 -1
  50. iatoolkit/system_prompts/query_main.prompt +26 -2
  51. iatoolkit/templates/base.html +13 -0
  52. iatoolkit/templates/chat.html +45 -2
  53. iatoolkit/templates/onboarding_shell.html +0 -1
  54. iatoolkit/views/base_login_view.py +7 -2
  55. iatoolkit/views/chat_view.py +76 -0
  56. iatoolkit/views/configuration_api_view.py +163 -0
  57. iatoolkit/views/load_document_api_view.py +14 -10
  58. iatoolkit/views/login_view.py +8 -3
  59. iatoolkit/views/rag_api_view.py +216 -0
  60. iatoolkit/views/users_api_view.py +33 -0
  61. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/METADATA +4 -4
  62. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/RECORD +66 -58
  63. iatoolkit/repositories/tasks_repo.py +0 -52
  64. iatoolkit/services/search_service.py +0 -55
  65. iatoolkit/services/tasks_service.py +0 -188
  66. iatoolkit/views/tasks_api_view.py +0 -72
  67. iatoolkit/views/tasks_review_api_view.py +0 -55
  68. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/WHEEL +0 -0
  69. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE +0 -0
  70. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
  71. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,10 @@
2
2
  # Copyright (c) 2024 Fernando Libedinsky
3
3
  # Product: IAToolkit
4
4
 
5
- from pathlib import Path
6
5
  from iatoolkit.repositories.models import Company
6
+ from iatoolkit.common.interfaces.asset_storage import AssetRepository, AssetType
7
7
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
8
  from iatoolkit.repositories.profile_repo import ProfileRepo
9
- from iatoolkit.common.exceptions import IAToolkitException
10
9
  from iatoolkit.common.util import Utility
11
10
  from injector import inject
12
11
  import logging
@@ -21,9 +20,11 @@ class ConfigurationService:
21
20
 
22
21
  @inject
23
22
  def __init__(self,
23
+ asset_repo: AssetRepository,
24
24
  llm_query_repo: LLMQueryRepo,
25
25
  profile_repo: ProfileRepo,
26
26
  utility: Utility):
27
+ self.asset_repo = asset_repo
27
28
  self.llm_query_repo = llm_query_repo
28
29
  self.profile_repo = profile_repo
29
30
  self.utility = utility
@@ -45,7 +46,30 @@ class ConfigurationService:
45
46
  self._ensure_config_loaded(company_short_name)
46
47
  return self._loaded_configs[company_short_name].get(content_key)
47
48
 
48
- def load_configuration(self, company_short_name: str, company_instance):
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):
49
73
  """
50
74
  Main entry point for configuring a company instance.
51
75
  This method is invoked by the dispatcher for each registered company.
@@ -54,71 +78,234 @@ class ConfigurationService:
54
78
 
55
79
  # 1. Load the main configuration file and supplementary content files
56
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)
57
84
 
58
- # 2. Register core company details and get the database object
59
- self._register_core_details(company_instance, config)
60
-
61
- # 3. Register databases
62
- self._register_data_sources(company_short_name, config)
63
-
64
- # 4. Register tools
65
- self._register_tools(company_instance, config)
85
+ # 3. Register tools
86
+ self._register_tools(company_short_name, config)
66
87
 
67
- # 5. Register prompt categories and prompts
68
- self._register_prompts(company_instance, config)
88
+ # 4. Register prompt categories and prompts
89
+ self._register_prompts(company_short_name, config)
69
90
 
70
- # 6. Link the persisted Company object back to the running instance
71
- company_instance.company_short_name = company_short_name
72
- company_instance.id = company_instance.company.id
91
+ # 5. Register Knowledge base information
92
+ self._register_knowledge_base(company_short_name, config)
73
93
 
74
94
  # Final step: validate the configuration against platform
75
- self._validate_configuration(company_short_name, config)
95
+ errors = self._validate_configuration(company_short_name, config)
76
96
 
77
97
  logging.info(f"✅ Company '{company_short_name}' configured successfully.")
98
+ return config, errors
78
99
 
100
+ def update_configuration_key(self, company_short_name: str, key: str, value) -> tuple[dict, list[str]]:
101
+ """
102
+ Updates a specific key in the company's configuration file, validates the result,
103
+ and saves it to the asset repository if valid.
79
104
 
80
- def _load_and_merge_configs(self, company_short_name: str) -> dict:
105
+ Args:
106
+ company_short_name: The company identifier.
107
+ key: The configuration key to update (supports dot notation, e.g., 'llm.model').
108
+ value: The new value for the key.
109
+
110
+ Returns:
111
+ A tuple containing the updated configuration dict and a list of error strings (if any).
81
112
  """
82
- Loads the main company.yaml and merges data from supplementary files
83
- specified in the 'content_files' section.
113
+ # 1. Load raw config from file (to avoid working with merged supplementary files if possible,
114
+ # but for simplicity we load the main yaml structure)
115
+ main_config_filename = "company.yaml"
116
+
117
+ if not self.asset_repo.exists(company_short_name, AssetType.CONFIG, main_config_filename):
118
+ raise FileNotFoundError(f"Configuration file not found for {company_short_name}")
119
+
120
+ yaml_content = self.asset_repo.read_text(company_short_name, AssetType.CONFIG, main_config_filename)
121
+ config = self.utility.load_yaml_from_string(yaml_content) or {}
122
+
123
+ # 2. Update the key in the dictionary
124
+ self._set_nested_value(config, key, value)
125
+
126
+ # 3. Validate the new configuration structure
127
+ errors = self._validate_configuration(company_short_name, config)
128
+
129
+ if errors:
130
+ logging.warning(f"Configuration update failed validation: {errors}")
131
+ return config, errors
132
+
133
+ # 4. Save back to repository
134
+ # Assuming Utility has a method to dump YAML. If not, standard yaml library would be needed.
135
+ # For this example, we assume self.utility.dump_yaml_to_string exists.
136
+ new_yaml_content = self.utility.dump_yaml_to_string(config)
137
+ self.asset_repo.write_text(company_short_name, AssetType.CONFIG, main_config_filename, new_yaml_content)
138
+
139
+ # 5. Invalidate cache so next reads get the new version
140
+ if company_short_name in self._loaded_configs:
141
+ del self._loaded_configs[company_short_name]
142
+
143
+ return config, []
144
+
145
+ def add_configuration_key(self, company_short_name: str, parent_key: str, key: str, value) -> tuple[dict, list[str]]:
146
+ """
147
+ Adds a new key-value pair under a specific parent key in the configuration.
148
+
149
+ Args:
150
+ company_short_name: The company identifier.
151
+ parent_key: The parent configuration key under which to add the new key (e.g., 'llm').
152
+ key: The new key name to add.
153
+ value: The value for the new key.
154
+
155
+ Returns:
156
+ A tuple containing the updated configuration dict and a list of error strings (if any).
84
157
  """
85
- config_dir = Path("companies") / company_short_name / "config"
86
- main_config_path = config_dir / "company.yaml"
158
+ # 1. Load raw config from file
159
+ main_config_filename = "company.yaml"
160
+
161
+ if not self.asset_repo.exists(company_short_name, AssetType.CONFIG, main_config_filename):
162
+ raise FileNotFoundError(f"Configuration file not found for {company_short_name}")
163
+
164
+ yaml_content = self.asset_repo.read_text(company_short_name, AssetType.CONFIG, main_config_filename)
165
+ config = self.utility.load_yaml_from_string(yaml_content) or {}
166
+
167
+ # 2. Construct full path and set the value
168
+ # If parent_key is provided, we append the new key to it (e.g., 'llm.new_setting')
169
+ full_path = f"{parent_key}.{key}" if parent_key else key
170
+ self._set_nested_value(config, full_path, value)
87
171
 
88
- if not main_config_path.exists():
89
- raise FileNotFoundError(f"Main configuration file not found: {main_config_path}")
172
+ # 3. Validate the new configuration structure
173
+ errors = self._validate_configuration(company_short_name, config)
90
174
 
91
- config = self.utility.load_schema_from_yaml(main_config_path)
175
+ if errors:
176
+ logging.warning(f"Configuration add failed validation: {errors}")
177
+ return config, errors
178
+
179
+ # 4. Save back to repository
180
+ new_yaml_content = self.utility.dump_yaml_to_string(config)
181
+ self.asset_repo.write_text(company_short_name, AssetType.CONFIG, main_config_filename, new_yaml_content)
182
+
183
+ # 5. Invalidate cache
184
+ if company_short_name in self._loaded_configs:
185
+ del self._loaded_configs[company_short_name]
186
+
187
+ return config, []
188
+
189
+
190
+ def validate_configuration(self, company_short_name: str) -> list[str]:
191
+ """
192
+ Public method to trigger validation of the current configuration.
193
+ """
194
+ config = self._load_and_merge_configs(company_short_name)
195
+ return self._validate_configuration(company_short_name, config)
196
+
197
+ def _set_nested_value(self, data: dict, key: str, value):
198
+ """
199
+ Helper to set a value in a nested dictionary or list using dot notation (e.g. 'llm.model', 'tools.0.name').
200
+ Handles traversal through both dictionaries and lists.
201
+ """
202
+ keys = key.split('.')
203
+ current = data
204
+
205
+ # Traverse up to the parent of the target key
206
+ for i, k in enumerate(keys[:-1]):
207
+ if isinstance(current, dict):
208
+ # If it's a dict, we can traverse or create the path
209
+ current = current.setdefault(k, {})
210
+ elif isinstance(current, list):
211
+ # If it's a list, we MUST use an integer index
212
+ try:
213
+ idx = int(k)
214
+ current = current[idx]
215
+ except (ValueError, IndexError) as e:
216
+ raise ValueError(
217
+ f"Invalid path: cannot access index '{k}' in list at '{'.'.join(keys[:i + 1])}'") from e
218
+ else:
219
+ raise ValueError(
220
+ f"Invalid path: '{k}' is not a container (got {type(current)}) at '{'.'.join(keys[:i + 1])}'")
221
+
222
+ # Set the final value
223
+ last_key = keys[-1]
224
+ if isinstance(current, dict):
225
+ current[last_key] = value
226
+ elif isinstance(current, list):
227
+ try:
228
+ idx = int(last_key)
229
+ current[idx] = value
230
+ except (ValueError, IndexError) as e:
231
+ raise ValueError(f"Invalid path: cannot assign to index '{last_key}' in list") from e
232
+ else:
233
+ raise ValueError(f"Cannot assign value to non-container type {type(current)} at '{key}'")
234
+
235
+ def _load_and_merge_configs(self, company_short_name: str) -> dict:
236
+ """
237
+ Loads the main company.yaml and merges data from supplementary files
238
+ specified in the 'content_files' section using AssetRepository.
239
+ """
240
+ main_config_filename = "company.yaml"
241
+
242
+ # verify existence of the main configuration file
243
+ if not self.asset_repo.exists(company_short_name, AssetType.CONFIG, main_config_filename):
244
+ # raise FileNotFoundError(f"Main configuration file not found: {main_config_filename}")
245
+ logging.exception(f"Main configuration file not found: {main_config_filename}")
246
+
247
+ # return the minimal configuration needed for starting the IAToolkit
248
+ # this is a for solving a chicken/egg problem when trying to migrate the configuration
249
+ # from filesystem to database in enterprise installation
250
+ # see create_assets cli command in enterprise-iatoolkit)
251
+ return {
252
+ 'id': company_short_name,
253
+ 'name': company_short_name,
254
+ 'llm': {'model': 'gpt-5', 'provider_api_keys': {'openai':''} },
255
+ }
256
+
257
+ # read text and parse
258
+ yaml_content = self.asset_repo.read_text(company_short_name, AssetType.CONFIG, main_config_filename)
259
+ config = self.utility.load_yaml_from_string(yaml_content)
260
+ if not config:
261
+ return {}
92
262
 
93
263
  # Load and merge supplementary content files (e.g., onboarding_cards)
94
- for key, file_path in config.get('help_files', {}).items():
95
- supplementary_path = config_dir / file_path
96
- if supplementary_path.exists():
97
- config[key] = self.utility.load_schema_from_yaml(supplementary_path)
264
+ for key, filename in config.get('help_files', {}).items():
265
+ if self.asset_repo.exists(company_short_name, AssetType.CONFIG, filename):
266
+ supp_content = self.asset_repo.read_text(company_short_name, AssetType.CONFIG, filename)
267
+ config[key] = self.utility.load_yaml_from_string(supp_content)
98
268
  else:
99
- logging.warning(f"⚠️ Warning: Content file not found: {supplementary_path}")
100
- config[key] = None # Ensure the key exists but is empty
269
+ logging.warning(f"⚠️ Warning: Content file not found: {filename}")
270
+ config[key] = None
101
271
 
102
272
  return config
103
273
 
104
- def _register_core_details(self, company_instance, config: dict) -> Company:
274
+ def _register_company_database(self, config: dict) -> Company:
105
275
  # register the company in the database: create_or_update logic
276
+ if not config:
277
+ return None
106
278
 
107
- company_obj = Company(short_name=config['id'],
108
- name=config['name'],
279
+ # create or update the company in database
280
+ company_obj = Company(short_name=config.get('id'),
281
+ name=config.get('name'),
109
282
  parameters=config.get('parameters', {}))
110
283
  company = self.profile_repo.create_company(company_obj)
111
284
 
112
- # save company object with the instance
113
- company_instance.company = company
285
+ # save company object with the configuration
286
+ config['company'] = company
287
+
114
288
  return company
115
289
 
116
- def _register_data_sources(self, company_short_name: str, config: dict):
290
+ def register_data_sources(self,
291
+ company_short_name: str,
292
+ config: dict = None):
117
293
  """
118
294
  Reads the data_sources config and registers databases with SqlService.
119
295
  Uses Lazy Loading to avoid circular dependency.
296
+
297
+ Public method: Can be called externally after initialization (e.g. by Enterprise)
298
+ to re-register sources once new factories (like 'bridge') are available.
120
299
  """
121
- # Lazy import to avoid circular dependency: ConfigService -> SqlService -> I18n -> ConfigService
300
+
301
+ # If config is not provided, try to load it from cache
302
+ if config is None:
303
+ self._ensure_config_loaded(company_short_name)
304
+ config = self._loaded_configs.get(company_short_name)
305
+
306
+ if not config:
307
+ return
308
+
122
309
  from iatoolkit import current_iatoolkit
123
310
  from iatoolkit.services.sql_service import SqlService
124
311
  sql_service = current_iatoolkit().get_injector().get(SqlService)
@@ -129,25 +316,46 @@ class ConfigurationService:
129
316
  if not sql_sources:
130
317
  return
131
318
 
132
- logging.info(f"🛢️ Registering databases for '{company_short_name}'...")
133
-
134
- for db_config in sql_sources:
135
- db_name = db_config.get('database')
136
- db_schema = db_config.get('schema', 'public')
137
- db_env_var = db_config.get('connection_string_env')
319
+ logging.info(f"🛢️ Registering databases for '{company_short_name}'...")
138
320
 
139
- # resolve the URI
140
- db_uri = os.getenv(db_env_var) if db_env_var else None
321
+ for source in sql_sources:
322
+ db_name = source.get('database')
323
+ if not db_name:
324
+ continue
141
325
 
142
- if not db_uri:
326
+ # Prepare the config dictionary for the factory
327
+ db_config = {
328
+ 'database': db_name,
329
+ 'schema': source.get('schema', 'public'),
330
+ 'connection_type': source.get('connection_type', 'direct'),
331
+
332
+ # Pass through keys needed for Bridge or other plugins
333
+ 'bridge_id': source.get('bridge_id'),
334
+ 'timeout': source.get('timeout')
335
+ }
336
+
337
+ # Resolve URI if env var is present (Required for 'direct', optional for others)
338
+ db_env_var = source.get('connection_string_env')
339
+ if db_env_var:
340
+ db_uri = os.getenv(db_env_var)
341
+ if db_uri:
342
+ db_config['db_uri'] = db_uri
343
+
344
+ # Validation: 'direct' connections MUST have a URI
345
+ if db_config['connection_type'] == 'direct' and not db_config.get('db_uri'):
143
346
  logging.error(
144
347
  f"-> Skipping DB '{db_name}' for '{company_short_name}': missing URI in env '{db_env_var}'.")
145
348
  continue
146
349
 
350
+ elif db_config['connection_type'] == 'bridge' and not db_config.get('bridge_id'):
351
+ logging.error(
352
+ f"-> Skipping DB '{db_name}' for '{company_short_name}': missing bridge_id in configuration.")
353
+ continue
354
+
147
355
  # Register with the SQL service
148
- sql_service.register_database(db_uri, db_name, db_schema)
356
+ sql_service.register_database(company_short_name, db_name, db_config)
149
357
 
150
- def _register_tools(self, company_instance, config: dict):
358
+ def _register_tools(self, company_short_name: str, config: dict):
151
359
  """creates in the database each tool defined in the YAML."""
152
360
  # Lazy import and resolve ToolService locally
153
361
  from iatoolkit import current_iatoolkit
@@ -155,9 +363,9 @@ class ConfigurationService:
155
363
  tool_service = current_iatoolkit().get_injector().get(ToolService)
156
364
 
157
365
  tools_config = config.get('tools', [])
158
- tool_service.sync_company_tools(company_instance, tools_config)
366
+ tool_service.sync_company_tools(company_short_name, tools_config)
159
367
 
160
- def _register_prompts(self, company_instance, config: dict):
368
+ def _register_prompts(self, company_short_name: str, config: dict):
161
369
  """
162
370
  Delegates prompt synchronization to PromptService.
163
371
  """
@@ -166,15 +374,27 @@ class ConfigurationService:
166
374
  from iatoolkit.services.prompt_service import PromptService
167
375
  prompt_service = current_iatoolkit().get_injector().get(PromptService)
168
376
 
169
- prompts_config = config.get('prompts', [])
170
- categories_config = config.get('prompt_categories', [])
377
+ prompts_config = config.get('prompts', {})
171
378
 
172
379
  prompt_service.sync_company_prompts(
173
- company_instance=company_instance,
174
- prompts_config=prompts_config,
175
- categories_config=categories_config
380
+ company_short_name=company_short_name,
381
+ prompts_config=prompts_config.get('prompt_list', []),
382
+ categories_config=prompts_config.get('prompt_categories', []),
176
383
  )
177
384
 
385
+ def _register_knowledge_base(self, company_short_name: str, config: dict):
386
+ # Lazy import to avoid circular dependency
387
+ from iatoolkit import current_iatoolkit
388
+ from iatoolkit.services.knowledge_base_service import KnowledgeBaseService
389
+ knowledge_base = current_iatoolkit().get_injector().get(KnowledgeBaseService)
390
+
391
+ kb_config = config.get('knowledge_base', {})
392
+ categories_config = kb_config.get('collections', [])
393
+
394
+ # sync collection types in database
395
+ knowledge_base.sync_collection_types(company_short_name, categories_config)
396
+
397
+
178
398
  def _validate_configuration(self, company_short_name: str, config: dict):
179
399
  """
180
400
  Validates the structure and consistency of the company.yaml configuration.
@@ -182,13 +402,15 @@ class ConfigurationService:
182
402
  Raises IAToolkitException if any validation error is found.
183
403
  """
184
404
  errors = []
185
- config_dir = Path("companies") / company_short_name / "config"
186
- prompts_dir = Path("companies") / company_short_name / "prompts"
187
405
 
188
406
  # Helper to collect errors
189
407
  def add_error(section, message):
190
408
  errors.append(f"[{section}] {message}")
191
409
 
410
+ if not config:
411
+ add_error("General", "Configuration file missing or with errors, check the application logs.")
412
+ return errors
413
+
192
414
  # 1. Top-level keys
193
415
  if not config.get("id"):
194
416
  add_error("General", "Missing required key: 'id'")
@@ -204,8 +426,8 @@ class ConfigurationService:
204
426
  else:
205
427
  if not config.get("llm", {}).get("model"):
206
428
  add_error("llm", "Missing required key: 'model'")
207
- if not config.get("llm", {}).get("api-key"):
208
- add_error("llm", "Missing required key: 'api-key'")
429
+ if not config.get("llm", {}).get("provider_api_keys"):
430
+ add_error("llm", "Missing required key: 'provider_api_keys'")
209
431
 
210
432
  # 3. Embedding Provider
211
433
  if isinstance(config.get("embedding_provider"), dict):
@@ -220,8 +442,12 @@ class ConfigurationService:
220
442
  for i, source in enumerate(config.get("data_sources", {}).get("sql", [])):
221
443
  if not source.get("database"):
222
444
  add_error(f"data_sources.sql[{i}]", "Missing required key: 'database'")
223
- if not source.get("connection_string_env"):
445
+
446
+ connection_type = source.get("connection_type")
447
+ if connection_type == 'direct' and not source.get("connection_string_env"):
224
448
  add_error(f"data_sources.sql[{i}]", "Missing required key: 'connection_string_env'")
449
+ elif connection_type == 'bridge' and not source.get("bridge_id"):
450
+ add_error(f"data_sources.sql[{i}]", "Missing bridge_id'")
225
451
 
226
452
  # 5. Tools
227
453
  for i, tool in enumerate(config.get("tools", [])):
@@ -236,15 +462,18 @@ class ConfigurationService:
236
462
  add_error(f"tools[{i}]", "'params' key must be a dictionary.")
237
463
 
238
464
  # 6. Prompts
239
- category_set = set(config.get("prompt_categories", []))
240
- for i, prompt in enumerate(config.get("prompts", [])):
465
+ prompt_list = config.get("prompts", {}).get("prompt_list", [])
466
+ prompt_categories = config.get("prompts", {}).get("prompt_categories", [])
467
+
468
+ category_set = set(prompt_categories)
469
+ for i, prompt in enumerate(prompt_list):
241
470
  prompt_name = prompt.get("name")
242
471
  if not prompt_name:
243
472
  add_error(f"prompts[{i}]", "Missing required key: 'name'")
244
473
  else:
245
- prompt_file = prompts_dir / f"{prompt_name}.prompt"
246
- if not prompt_file.is_file():
247
- add_error(f"prompts/{prompt_name}:", f"Prompt file not found: {prompt_file}")
474
+ prompt_filename = f"{prompt_name}.prompt"
475
+ if not self.asset_repo.exists(company_short_name, AssetType.PROMPT, prompt_filename):
476
+ add_error(f"prompts/{prompt_name}:", f"Prompt file not found: {prompt_filename}")
248
477
 
249
478
  prompt_description = prompt.get("description")
250
479
  if not prompt_description:
@@ -289,9 +518,9 @@ class ConfigurationService:
289
518
  if not filename:
290
519
  add_error(f"help_files.{key}", "Filename cannot be empty.")
291
520
  continue
292
- help_file_path = config_dir / filename
293
- if not help_file_path.is_file():
294
- add_error(f"help_files.{key}", f"Help file not found: {help_file_path}")
521
+ if not self.asset_repo.exists(company_short_name, AssetType.CONFIG, filename):
522
+ add_error(f"help_files.{key}", f"Help file not found: {filename}")
523
+
295
524
 
296
525
  # If any errors were found, log all messages and raise an exception
297
526
  if errors:
@@ -299,8 +528,5 @@ class ConfigurationService:
299
528
  f" - {e}" for e in errors)
300
529
  logging.error(error_summary)
301
530
 
302
- raise IAToolkitException(
303
- IAToolkitException.ErrorType.CONFIG_ERROR,
304
- 'company.yaml validation errors'
305
- )
531
+ return errors
306
532
 
@@ -59,13 +59,25 @@ class Dispatcher:
59
59
  self.setup_iatoolkit_system()
60
60
 
61
61
  # Loads the configuration of every company: company.yaml file
62
- for company_name, company_instance in self.company_instances.items():
62
+ for company_short_name, company_instance in self.company_instances.items():
63
63
  try:
64
64
  # read company configuration from company.yaml
65
- self.config_service.load_configuration(company_name, company_instance)
65
+ config, errors = self.config_service.load_configuration(company_short_name)
66
+
67
+ '''
68
+ if errors:
69
+ raise IAToolkitException(
70
+ IAToolkitException.ErrorType.CONFIG_ERROR,
71
+ 'company.yaml validation errors'
72
+ )
73
+ '''
74
+
75
+ # complement the instance self data
76
+ company_instance.company_short_name = company_short_name
77
+ company_instance.company = config.get('company')
66
78
 
67
79
  except Exception as e:
68
- logging.error(f"❌ Failed to register configuration for '{company_name}': {e}")
80
+ logging.error(f"❌ Failed to register configuration for '{company_short_name}': {e}")
69
81
  raise e
70
82
 
71
83
  return True
@@ -95,7 +107,13 @@ class Dispatcher:
95
107
 
96
108
  # check if action is a system function using ToolService
97
109
  if self.tool_service.is_system_tool(function_name):
110
+ # this is the system function to be executed.
98
111
  handler = self.tool_service.get_system_handler(function_name)
112
+ logging.info(
113
+ f"Calling system handler [{function_name}] "
114
+ f"with company_short_name={company_short_name} "
115
+ f"and kwargs={kwargs}"
116
+ )
99
117
  return handler(company_short_name, **kwargs)
100
118
 
101
119
  company_instance = self.company_instances[company_short_name]
@@ -74,9 +74,6 @@ class FileProcessor:
74
74
  if not self._apply_filters(file_name):
75
75
  continue
76
76
 
77
- if self.config.echo:
78
- print(f'loading: {file_name}')
79
-
80
77
  content = self.connector.get_file_content(file_path)
81
78
 
82
79
  # execute the callback function
@@ -87,8 +84,6 @@ class FileProcessor:
87
84
  context=self.config.context)
88
85
  self.processed_files += 1
89
86
 
90
- logging.info(f"Successfully processed file: {file_path}")
91
-
92
87
  except Exception as e:
93
88
  logging.error(f"Error processing {file_path}: {e}")
94
89
  if not self.config.continue_on_error: