iatoolkit 0.71.4__py3-none-any.whl → 1.4.2__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 (114) hide show
  1. iatoolkit/__init__.py +19 -7
  2. iatoolkit/base_company.py +1 -71
  3. iatoolkit/cli_commands.py +9 -21
  4. iatoolkit/common/exceptions.py +2 -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 +38 -0
  8. iatoolkit/common/model_registry.py +159 -0
  9. iatoolkit/common/routes.py +53 -32
  10. iatoolkit/common/util.py +17 -12
  11. iatoolkit/company_registry.py +55 -14
  12. iatoolkit/{iatoolkit.py → core.py} +102 -72
  13. iatoolkit/infra/{mail_app.py → brevo_mail_app.py} +15 -37
  14. iatoolkit/infra/llm_providers/__init__.py +0 -0
  15. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  16. iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
  17. iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
  18. iatoolkit/infra/llm_proxy.py +235 -134
  19. iatoolkit/infra/llm_response.py +5 -0
  20. iatoolkit/locales/en.yaml +134 -4
  21. iatoolkit/locales/es.yaml +293 -162
  22. iatoolkit/repositories/database_manager.py +92 -22
  23. iatoolkit/repositories/document_repo.py +7 -0
  24. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  25. iatoolkit/repositories/llm_query_repo.py +36 -22
  26. iatoolkit/repositories/models.py +86 -95
  27. iatoolkit/repositories/profile_repo.py +64 -13
  28. iatoolkit/repositories/vs_repo.py +31 -28
  29. iatoolkit/services/auth_service.py +1 -1
  30. iatoolkit/services/branding_service.py +1 -1
  31. iatoolkit/services/company_context_service.py +96 -39
  32. iatoolkit/services/configuration_service.py +329 -67
  33. iatoolkit/services/dispatcher_service.py +51 -227
  34. iatoolkit/services/document_service.py +10 -1
  35. iatoolkit/services/embedding_service.py +9 -6
  36. iatoolkit/services/excel_service.py +50 -2
  37. iatoolkit/services/file_processor_service.py +0 -5
  38. iatoolkit/services/history_manager_service.py +208 -0
  39. iatoolkit/services/jwt_service.py +1 -1
  40. iatoolkit/services/knowledge_base_service.py +412 -0
  41. iatoolkit/services/language_service.py +8 -2
  42. iatoolkit/services/license_service.py +82 -0
  43. iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +42 -29
  44. iatoolkit/services/load_documents_service.py +18 -47
  45. iatoolkit/services/mail_service.py +171 -25
  46. iatoolkit/services/profile_service.py +69 -36
  47. iatoolkit/services/{prompt_manager_service.py → prompt_service.py} +136 -25
  48. iatoolkit/services/query_service.py +229 -203
  49. iatoolkit/services/sql_service.py +116 -34
  50. iatoolkit/services/tool_service.py +246 -0
  51. iatoolkit/services/user_feedback_service.py +18 -6
  52. iatoolkit/services/user_session_context_service.py +121 -51
  53. iatoolkit/static/images/iatoolkit_core.png +0 -0
  54. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  55. iatoolkit/static/js/chat_feedback_button.js +1 -1
  56. iatoolkit/static/js/chat_help_content.js +4 -4
  57. iatoolkit/static/js/chat_main.js +61 -9
  58. iatoolkit/static/js/chat_model_selector.js +227 -0
  59. iatoolkit/static/js/chat_onboarding_button.js +1 -1
  60. iatoolkit/static/js/chat_reload_button.js +4 -1
  61. iatoolkit/static/styles/chat_iatoolkit.css +59 -3
  62. iatoolkit/static/styles/chat_public.css +28 -0
  63. iatoolkit/static/styles/documents.css +598 -0
  64. iatoolkit/static/styles/landing_page.css +223 -7
  65. iatoolkit/static/styles/llm_output.css +34 -1
  66. iatoolkit/system_prompts/__init__.py +0 -0
  67. iatoolkit/system_prompts/query_main.prompt +28 -3
  68. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  69. iatoolkit/templates/_company_header.html +30 -5
  70. iatoolkit/templates/_login_widget.html +3 -3
  71. iatoolkit/templates/base.html +13 -0
  72. iatoolkit/templates/chat.html +45 -3
  73. iatoolkit/templates/forgot_password.html +3 -2
  74. iatoolkit/templates/onboarding_shell.html +1 -2
  75. iatoolkit/templates/signup.html +3 -0
  76. iatoolkit/views/base_login_view.py +8 -3
  77. iatoolkit/views/change_password_view.py +1 -1
  78. iatoolkit/views/chat_view.py +76 -0
  79. iatoolkit/views/forgot_password_view.py +9 -4
  80. iatoolkit/views/history_api_view.py +3 -3
  81. iatoolkit/views/home_view.py +4 -2
  82. iatoolkit/views/init_context_api_view.py +1 -1
  83. iatoolkit/views/llmquery_api_view.py +4 -3
  84. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  85. iatoolkit/views/{file_store_api_view.py → load_document_api_view.py} +15 -11
  86. iatoolkit/views/login_view.py +25 -8
  87. iatoolkit/views/logout_api_view.py +10 -2
  88. iatoolkit/views/prompt_api_view.py +1 -1
  89. iatoolkit/views/rag_api_view.py +216 -0
  90. iatoolkit/views/root_redirect_view.py +22 -0
  91. iatoolkit/views/signup_view.py +12 -4
  92. iatoolkit/views/static_page_view.py +27 -0
  93. iatoolkit/views/users_api_view.py +33 -0
  94. iatoolkit/views/verify_user_view.py +1 -1
  95. iatoolkit-1.4.2.dist-info/METADATA +268 -0
  96. iatoolkit-1.4.2.dist-info/RECORD +133 -0
  97. iatoolkit-1.4.2.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  98. iatoolkit/repositories/tasks_repo.py +0 -52
  99. iatoolkit/services/history_service.py +0 -37
  100. iatoolkit/services/search_service.py +0 -55
  101. iatoolkit/services/tasks_service.py +0 -188
  102. iatoolkit/templates/about.html +0 -13
  103. iatoolkit/templates/index.html +0 -145
  104. iatoolkit/templates/login_simulation.html +0 -45
  105. iatoolkit/views/external_login_view.py +0 -73
  106. iatoolkit/views/index_view.py +0 -14
  107. iatoolkit/views/login_simulation_view.py +0 -93
  108. iatoolkit/views/tasks_api_view.py +0 -72
  109. iatoolkit/views/tasks_review_api_view.py +0 -55
  110. iatoolkit-0.71.4.dist-info/METADATA +0 -276
  111. iatoolkit-0.71.4.dist-info/RECORD +0 -122
  112. {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/WHEEL +0 -0
  113. {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE +0 -0
  114. {iatoolkit-0.71.4.dist-info → iatoolkit-1.4.2.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,15 @@
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
+ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
+ from iatoolkit.repositories.profile_repo import ProfileRepo
7
9
  from iatoolkit.common.util import Utility
8
10
  from injector import inject
9
11
  import logging
12
+ import os
13
+
10
14
 
11
15
  class ConfigurationService:
12
16
  """
@@ -16,10 +20,24 @@ class ConfigurationService:
16
20
 
17
21
  @inject
18
22
  def __init__(self,
23
+ asset_repo: AssetRepository,
24
+ llm_query_repo: LLMQueryRepo,
25
+ profile_repo: ProfileRepo,
19
26
  utility: Utility):
27
+ self.asset_repo = asset_repo
28
+ self.llm_query_repo = llm_query_repo
29
+ self.profile_repo = profile_repo
20
30
  self.utility = utility
21
31
  self._loaded_configs = {} # cache for store loaded configurations
22
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
+
23
41
  def get_configuration(self, company_short_name: str, content_key: str):
24
42
  """
25
43
  Public method to provide a specific section of a company's configuration.
@@ -28,7 +46,30 @@ class ConfigurationService:
28
46
  self._ensure_config_loaded(company_short_name)
29
47
  return self._loaded_configs[company_short_name].get(content_key)
30
48
 
31
- 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):
32
73
  """
33
74
  Main entry point for configuring a company instance.
34
75
  This method is invoked by the dispatcher for each registered company.
@@ -37,97 +78,318 @@ class ConfigurationService:
37
78
 
38
79
  # 1. Load the main configuration file and supplementary content files
39
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)
40
84
 
41
- # 2. Register core company details and get the database object
42
- company_db_object = self._register_core_details(company_instance, config)
85
+ # 3. Register tools
86
+ self._register_tools(company_short_name, config)
43
87
 
44
- # 3. Register tools (functions)
45
- self._register_tools(company_instance, config.get('tools', []))
88
+ # 4. Register prompt categories and prompts
89
+ self._register_prompts(company_short_name, config)
46
90
 
47
- # 4. Register prompt categories and prompts
48
- self._register_prompts(company_instance, config)
91
+ # 5. Register Knowledge base information
92
+ self._register_knowledge_base(company_short_name, config)
49
93
 
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
94
+ # Final step: validate the configuration against platform
95
+ errors = self._validate_configuration(company_short_name, config)
54
96
 
55
97
  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)
98
+ return config, errors
64
99
 
65
100
  def _load_and_merge_configs(self, company_short_name: str) -> dict:
66
101
  """
67
102
  Loads the main company.yaml and merges data from supplementary files
68
- specified in the 'content_files' section.
103
+ specified in the 'content_files' section using AssetRepository.
69
104
  """
70
- config_dir = Path("companies") / company_short_name / "config"
71
- main_config_path = config_dir / "company.yaml"
105
+ main_config_filename = "company.yaml"
72
106
 
73
- if not main_config_path.exists():
74
- raise FileNotFoundError(f"Main configuration file not found: {main_config_path}")
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}")
75
111
 
76
- config = self.utility.load_schema_from_yaml(main_config_path)
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 {}
77
127
 
78
128
  # 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)
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)
83
133
  else:
84
- logging.warning(f"⚠️ Warning: Content file not found: {supplementary_path}")
85
- config[key] = None # Ensure the key exists but is empty
134
+ logging.warning(f"⚠️ Warning: Content file not found: {filename}")
135
+ config[key] = None
86
136
 
87
137
  return config
88
138
 
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
- )
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
96
143
 
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
- )
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)
105
149
 
106
- def _register_prompts(self, company_instance, config: dict):
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):
107
158
  """
108
- Creates prompt categories first, then creates each prompt and assigns
109
- it to its respective category.
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
+ 'database': db_name,
194
+ 'schema': source.get('schema', 'public'),
195
+ 'connection_type': source.get('connection_type', 'direct'),
196
+
197
+ # Pass through keys needed for Bridge or other plugins
198
+ 'bridge_id': source.get('bridge_id'),
199
+ 'timeout': source.get('timeout')
200
+ }
201
+
202
+ # Resolve URI if env var is present (Required for 'direct', optional for others)
203
+ db_env_var = source.get('connection_string_env')
204
+ if db_env_var:
205
+ db_uri = os.getenv(db_env_var)
206
+ if db_uri:
207
+ db_config['db_uri'] = db_uri
208
+
209
+ # Validation: 'direct' connections MUST have a URI
210
+ if db_config['connection_type'] == 'direct' and not db_config.get('db_uri'):
211
+ logging.error(
212
+ f"-> Skipping DB '{db_name}' for '{company_short_name}': missing URI in env '{db_env_var}'.")
213
+ continue
214
+
215
+ elif db_config['connection_type'] == 'bridge' and not db_config.get('bridge_id'):
216
+ logging.error(
217
+ f"-> Skipping DB '{db_name}' for '{company_short_name}': missing bridge_id in configuration.")
218
+ continue
219
+
220
+ # Register with the SQL service
221
+ sql_service.register_database(company_short_name, db_name, db_config)
222
+
223
+ def _register_tools(self, company_short_name: str, config: dict):
224
+ """creates in the database each tool defined in the YAML."""
225
+ # Lazy import and resolve ToolService locally
226
+ from iatoolkit import current_iatoolkit
227
+ from iatoolkit.services.tool_service import ToolService
228
+ tool_service = current_iatoolkit().get_injector().get(ToolService)
229
+
230
+ tools_config = config.get('tools', [])
231
+ tool_service.sync_company_tools(company_short_name, tools_config)
232
+
233
+ def _register_prompts(self, company_short_name: str, config: dict):
110
234
  """
235
+ Delegates prompt synchronization to PromptService.
236
+ """
237
+ # Lazy import to avoid circular dependency
238
+ from iatoolkit import current_iatoolkit
239
+ from iatoolkit.services.prompt_service import PromptService
240
+ prompt_service = current_iatoolkit().get_injector().get(PromptService)
241
+
111
242
  prompts_config = config.get('prompts', [])
112
243
  categories_config = config.get('prompt_categories', [])
113
244
 
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
245
+ prompt_service.sync_company_prompts(
246
+ company_short_name=company_short_name,
247
+ prompts_config=prompts_config,
248
+ categories_config=categories_config
249
+ )
250
+
251
+ def _register_knowledge_base(self, company_short_name: str, config: dict):
252
+ # Lazy import to avoid circular dependency
253
+ from iatoolkit import current_iatoolkit
254
+ from iatoolkit.services.knowledge_base_service import KnowledgeBaseService
255
+ knowledge_base = current_iatoolkit().get_injector().get(KnowledgeBaseService)
256
+
257
+ kb_config = config.get('knowledge_base', {})
258
+ categories_config = kb_config.get('collections', [])
259
+
260
+ # sync collection types in database
261
+ knowledge_base.sync_collection_types(company_short_name, categories_config)
262
+
263
+
264
+ def _validate_configuration(self, company_short_name: str, config: dict):
265
+ """
266
+ Validates the structure and consistency of the company.yaml configuration.
267
+ It checks for required keys, valid values, and existence of related files.
268
+ Raises IAToolkitException if any validation error is found.
269
+ """
270
+ errors = []
271
+
272
+ # Helper to collect errors
273
+ def add_error(section, message):
274
+ errors.append(f"[{section}] {message}")
118
275
 
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.")
276
+ if not config:
277
+ add_error("General", "Configuration file missing or with errors, check the application logs.")
278
+ return errors
279
+
280
+ # 1. Top-level keys
281
+ if not config.get("id"):
282
+ add_error("General", "Missing required key: 'id'")
283
+ elif config["id"] != company_short_name:
284
+ add_error("General",
285
+ f"'id' ({config['id']}) does not match the company short name ('{company_short_name}').")
286
+ if not config.get("name"):
287
+ add_error("General", "Missing required key: 'name'")
288
+
289
+ # 2. LLM section
290
+ if not isinstance(config.get("llm"), dict):
291
+ add_error("llm", "Missing or invalid 'llm' section.")
292
+ else:
293
+ if not config.get("llm", {}).get("model"):
294
+ add_error("llm", "Missing required key: 'model'")
295
+ if not config.get("llm", {}).get("provider_api_keys"):
296
+ add_error("llm", "Missing required key: 'provider_api_keys'")
297
+
298
+ # 3. Embedding Provider
299
+ if isinstance(config.get("embedding_provider"), dict):
300
+ if not config.get("embedding_provider", {}).get("provider"):
301
+ add_error("embedding_provider", "Missing required key: 'provider'")
302
+ if not config.get("embedding_provider", {}).get("model"):
303
+ add_error("embedding_provider", "Missing required key: 'model'")
304
+ if not config.get("embedding_provider", {}).get("api_key_name"):
305
+ add_error("embedding_provider", "Missing required key: 'api_key_name'")
306
+
307
+ # 4. Data Sources
308
+ for i, source in enumerate(config.get("data_sources", {}).get("sql", [])):
309
+ if not source.get("database"):
310
+ add_error(f"data_sources.sql[{i}]", "Missing required key: 'database'")
311
+
312
+ connection_type = source.get("connection_type")
313
+ if connection_type == 'direct' and not source.get("connection_string_env"):
314
+ add_error(f"data_sources.sql[{i}]", "Missing required key: 'connection_string_env'")
315
+ elif connection_type == 'bridge' and not source.get("bridge_id"):
316
+ add_error(f"data_sources.sql[{i}]", "Missing bridge_id'")
317
+
318
+ # 5. Tools
319
+ for i, tool in enumerate(config.get("tools", [])):
320
+ function_name = tool.get("function_name")
321
+ if not function_name:
322
+ add_error(f"tools[{i}]", "Missing required key: 'function_name'")
323
+
324
+ # check that function exist in dispatcher
325
+ if not tool.get("description"):
326
+ add_error(f"tools[{i}]", "Missing required key: 'description'")
327
+ if not isinstance(tool.get("params"), dict):
328
+ add_error(f"tools[{i}]", "'params' key must be a dictionary.")
329
+
330
+ # 6. Prompts
331
+ category_set = set(config.get("prompt_categories", []))
332
+ for i, prompt in enumerate(config.get("prompts", [])):
333
+ prompt_name = prompt.get("name")
334
+ if not prompt_name:
335
+ add_error(f"prompts[{i}]", "Missing required key: 'name'")
336
+ else:
337
+ prompt_filename = f"{prompt_name}.prompt"
338
+ if not self.asset_repo.exists(company_short_name, AssetType.PROMPT, prompt_filename):
339
+ add_error(f"prompts/{prompt_name}:", f"Prompt file not found: {prompt_filename}")
340
+
341
+ prompt_description = prompt.get("description")
342
+ if not prompt_description:
343
+ add_error(f"prompts[{i}]", "Missing required key: 'description'")
344
+
345
+ prompt_cat = prompt.get("category")
346
+ if not prompt_cat:
347
+ add_error(f"prompts[{i}]", "Missing required key: 'category'")
348
+ elif prompt_cat not in category_set:
349
+ add_error(f"prompts[{i}]", f"Category '{prompt_cat}' is not defined in 'prompt_categories'.")
350
+
351
+ # 7. User Feedback
352
+ feedback_config = config.get("parameters", {}).get("user_feedback", {})
353
+ if feedback_config.get("channel") == "email" and not feedback_config.get("destination"):
354
+ add_error("parameters.user_feedback", "When channel is 'email', a 'destination' is required.")
355
+
356
+ # 8. Knowledge Base
357
+ kb_config = config.get("knowledge_base", {})
358
+ if kb_config and not isinstance(kb_config, dict):
359
+ add_error("knowledge_base", "Section must be a dictionary.")
360
+ elif kb_config:
361
+ prod_connector = kb_config.get("connectors", {}).get("production", {})
362
+ if prod_connector.get("type") == "s3":
363
+ for key in ["bucket", "prefix", "aws_access_key_id_env", "aws_secret_access_key_env", "aws_region_env"]:
364
+ if not prod_connector.get(key):
365
+ add_error("knowledge_base.connectors.production", f"S3 connector is missing '{key}'.")
366
+
367
+ # 9. Mail Provider
368
+ mail_config = config.get("mail_provider", {})
369
+ if mail_config:
370
+ provider = mail_config.get("provider")
371
+ if not provider:
372
+ add_error("mail_provider", "Missing required key: 'provider'")
373
+ elif provider not in ["brevo_mail", "smtplib"]:
374
+ add_error("mail_provider", f"Unsupported provider: '{provider}'. Must be 'brevo_mail' or 'smtplib'.")
375
+
376
+ if not mail_config.get("sender_email"):
377
+ add_error("mail_provider", "Missing required key: 'sender_email'")
378
+
379
+ # 10. Help Files
380
+ for key, filename in config.get("help_files", {}).items():
381
+ if not filename:
382
+ add_error(f"help_files.{key}", "Filename cannot be empty.")
123
383
  continue
384
+ if not self.asset_repo.exists(company_short_name, AssetType.CONFIG, filename):
385
+ add_error(f"help_files.{key}", f"Help file not found: {filename}")
386
+
387
+
388
+ # If any errors were found, log all messages and raise an exception
389
+ if errors:
390
+ error_summary = f"Configuration file '{company_short_name}/config/company.yaml' for '{company_short_name}' has validation errors:\n" + "\n".join(
391
+ f" - {e}" for e in errors)
392
+ logging.error(error_summary)
393
+
394
+ return errors
124
395
 
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
- )