iatoolkit 1.7.0__py3-none-any.whl → 1.9.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.
iatoolkit/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- __version__ = "1.7.0"
6
+ __version__ = "1.9.0"
7
7
 
8
8
  # Expose main classes and functions at the top level of the package
9
9
 
@@ -32,6 +32,7 @@ def register_views(app):
32
32
  from iatoolkit.views.root_redirect_view import RootRedirectView
33
33
  from iatoolkit.views.users_api_view import UsersApiView
34
34
  from iatoolkit.views.rag_api_view import RagApiView
35
+ from iatoolkit.views.categories_api_view import CategoriesApiView
35
36
 
36
37
  # assign root '/' to our new redirect logic
37
38
  app.add_url_rule('/home', view_func=RootRedirectView.as_view('root_redirect'))
@@ -85,9 +86,21 @@ def register_views(app):
85
86
  # can be used also for executing iatoolkit prompts
86
87
  app.add_url_rule('/<company_short_name>/api/llm_query', view_func=LLMQueryApiView.as_view('llm_query_api'))
87
88
 
88
- # open the promt directory
89
- app.add_url_rule('/<company_short_name>/api/prompts', view_func=PromptApiView.as_view('prompt'))
89
+ # Categories Endpoint
90
+ app.add_url_rule('/<company_short_name>/api/categories',
91
+ view_func=CategoriesApiView.as_view('categories_api'),
92
+ methods=['GET'])
93
+
94
+ # open the promt directory and specific prompt management
95
+ prompt_view = PromptApiView.as_view('prompt')
96
+ app.add_url_rule('/<company_short_name>/api/prompts',
97
+ view_func=prompt_view,
98
+ methods=['GET', 'POST'],
99
+ defaults={'prompt_name': None})
90
100
 
101
+ app.add_url_rule('/<company_short_name>/api/prompts/<prompt_name>',
102
+ view_func=prompt_view,
103
+ methods=['GET', 'POST','PUT', 'DELETE'])
91
104
  # toolbar buttons
92
105
  app.add_url_rule('/<company_short_name>/api/feedback', view_func=UserFeedbackApiView.as_view('feedback'))
93
106
  app.add_url_rule('/<company_short_name>/api/history', view_func=HistoryApiView.as_view('history'))
iatoolkit/locales/en.yaml CHANGED
@@ -52,8 +52,7 @@ ui:
52
52
  company_config: "Company Configuration (company.yaml)"
53
53
  prompts: "Prompts"
54
54
  prompts_description: "System Prompts"
55
- knowledge: "Knowledge Base"
56
- knowledge_rag: "RAG (Vector)"
55
+ knowledge_rag: "Knowledge Base (RAG)"
57
56
  knowledge_static: "Static Context"
58
57
  schemas: "Schemas"
59
58
  schemas_description: "Data Definitions (YAML)"
@@ -86,6 +85,10 @@ ui:
86
85
  logout: "Close session"
87
86
  error_loading: "Error loading file content"
88
87
  loading: "Loading..."
88
+ add: "Add"
89
+ create: "Create"
90
+ delete: "Delete"
91
+
89
92
 
90
93
  db_explorer:
91
94
  data_explorer: "Data Explorer"
@@ -108,6 +111,36 @@ ui:
108
111
  meta_synonyms: "Synonyms"
109
112
  pii_sesitive: "PII Sensitive"
110
113
 
114
+ prompts:
115
+ title: "Prompts Manager"
116
+ subtitle: "Manage and test your AI personas and templates"
117
+ library: "Prompt Library"
118
+ new_btn: "New"
119
+ filter_placeholder: "Filter prompts..."
120
+ select_prompt: "Select a prompt"
121
+ toggle_settings: "Toggle Variables & Config"
122
+ settings_btn: "Settings"
123
+ save_btn: "Save"
124
+ tab_editor: "Editor"
125
+ tab_playground: "Playground"
126
+ config_title: "Config"
127
+ desc_label: "Description"
128
+ desc_placeholder: "Describe this prompt..."
129
+ vars_label: "Input Variables"
130
+ no_vars: "No inputs defined"
131
+ playground_inputs: "Variables & Inputs"
132
+ no_vars_detected: "No variables detected in prompt template."
133
+ model_override: "Model Override (Optional)"
134
+ default_model: "Default (from config)"
135
+ run_btn: "Run"
136
+ output_placeholder: "Output will appear here..."
137
+ new_modal_title: "Create New Prompt"
138
+ name_label: "Name (Slug)"
139
+ name_help: "Use lowercase, numbers, and underscores only."
140
+ category_label: "Category"
141
+ delete_confirmation: "Delete Prompt?"
142
+
143
+
111
144
  config:
112
145
  editor_description: "IAToolkit configuration file"
113
146
  title: "Configuration Editor"
@@ -146,7 +179,7 @@ ui:
146
179
  delete_message: "This action cannot be undone. The file will be permanently removed."
147
180
  delete_button: "Delete"
148
181
  delete_cancel: "Cancel"
149
- target_collection: "Target collection"
182
+ target_collection: "Collection"
150
183
  select_collection_placeholder: "Select a collection"
151
184
  collection_required: "Collection is required"
152
185
  collection: "Collection"
iatoolkit/locales/es.yaml CHANGED
@@ -49,8 +49,7 @@ ui:
49
49
  workspace: "Recursos"
50
50
  configuration: "Configuración"
51
51
  company_config: "Configuración Empresa (company.yaml)"
52
- knowledge: "Conocimiento"
53
- knowledge_rag: "RAG (Vectorial)"
52
+ knowledge_rag: "Knowledge Base (RAG)"
54
53
  knowledge_static: "Contenido Estático"
55
54
  prompts: "Prompts"
56
55
  prompts_description: "Prompts de sistema"
@@ -83,6 +82,9 @@ ui:
83
82
  load_configuration: "Guardar configuración"
84
83
  goto_chat: "Ir al chat"
85
84
  logout: "Cerrar sesión"
85
+ add: "Agregar"
86
+ create: "Crear"
87
+ delete: "Borrar"
86
88
 
87
89
  db_explorer:
88
90
  data_explorer: "Explorador de datos"
@@ -105,6 +107,35 @@ ui:
105
107
  meta_synonyms: "Sinonimos"
106
108
  pii_sesitive: "IP Sensible"
107
109
 
110
+ prompts:
111
+ title: "Gestor de Prompts"
112
+ subtitle: "Gestiona y prueba tus plantillas y personas de IA"
113
+ library: "Biblioteca de Prompts"
114
+ new_btn: "Nuevo"
115
+ filter_placeholder: "Filtrar prompts..."
116
+ select_prompt: "Selecciona un prompt"
117
+ toggle_settings: "Alternar Variables y Configuración"
118
+ settings_btn: "Ajustes"
119
+ save_btn: "Guardar"
120
+ tab_editor: "Editor"
121
+ tab_playground: "Playground"
122
+ config_title: "Configuración"
123
+ desc_label: "Descripción"
124
+ desc_placeholder: "Describe este prompt..."
125
+ vars_label: "Variables de Entrada"
126
+ no_vars: "Sin variables definidas"
127
+ playground_inputs: "Variables y Entradas"
128
+ no_vars_detected: "No se detectaron variables en la plantilla."
129
+ model_override: "Modelo Específico (Opcional)"
130
+ default_model: "Por defecto (según config)"
131
+ run_btn: "Ejecutar"
132
+ output_placeholder: "El resultado aparecerá aquí..."
133
+ new_modal_title: "Crear Nuevo Prompt"
134
+ name_label: "Nombre (Slug)"
135
+ name_help: "Solo minúsculas, números y guiones bajos."
136
+ category_label: "Categoría"
137
+ delete_confirmation: "Eliminar el prompt?"
138
+
108
139
  config:
109
140
  editor_description: "Editor de configuración"
110
141
  title: "Editor de configuraciones"
@@ -143,7 +174,7 @@ ui:
143
174
  delete_message: "Esta acción no se puede deshacer. El archivo se eliminará permanentemente."
144
175
  delete_button: "Eliminar"
145
176
  delete_cancel: "Cancelar"
146
- target_collection: "Categoría seleccionada"
177
+ target_collection: "Categoría"
147
178
  select_collection_placeholder: "Selecciona una categoría"
148
179
  collection_required: "Debe seleccionar una categoría"
149
180
  all_collections: "Todas las categorías"
@@ -3,10 +3,13 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- from iatoolkit.repositories.models import LLMQuery, Tool, Company, Prompt, PromptCategory
6
+ from iatoolkit.repositories.models import (LLMQuery, Tool,
7
+ Company, Prompt, PromptCategory, PromptType)
7
8
  from injector import inject
8
9
  from iatoolkit.repositories.database_manager import DatabaseManager
9
10
  from sqlalchemy import or_
11
+ from typing import List
12
+
10
13
 
11
14
  class LLMQueryRepo:
12
15
  @inject
@@ -54,7 +57,7 @@ class LLMQueryRepo:
54
57
  self.session.add(new_tool)
55
58
  tool = new_tool
56
59
 
57
- self.session.flush()
60
+ self.session.commit()
58
61
  return tool
59
62
 
60
63
  def delete_tool(self, tool: Tool):
@@ -67,14 +70,14 @@ class LLMQueryRepo:
67
70
  prompt.category_id = new_prompt.category_id
68
71
  prompt.description = new_prompt.description
69
72
  prompt.order = new_prompt.order
70
- prompt.is_system_prompt = new_prompt.is_system_prompt
73
+ prompt.prompt_type = new_prompt.prompt_type
71
74
  prompt.filename = new_prompt.filename
72
75
  prompt.custom_fields = new_prompt.custom_fields
73
76
  else:
74
77
  self.session.add(new_prompt)
75
78
  prompt = new_prompt
76
79
 
77
- self.session.flush()
80
+ self.session.commit()
78
81
  return prompt
79
82
 
80
83
  def create_or_update_prompt_category(self, new_category: PromptCategory):
@@ -94,12 +97,31 @@ class LLMQueryRepo:
94
97
  LLMQuery.user_identifier == user_identifier,
95
98
  ).filter_by(company_id=company.id).order_by(LLMQuery.created_at.desc()).limit(100).all()
96
99
 
97
- def get_prompts(self, company: Company) -> list[Prompt]:
98
- return self.session.query(Prompt).filter_by(company_id=company.id, is_system_prompt=False).all()
100
+ def get_prompts(self, company: Company, include_all: bool = False) -> list[Prompt]:
101
+ if include_all:
102
+ # Include all prompts: company, system, agent
103
+ return self.session.query(Prompt).filter(
104
+ Prompt.company_id == company.id,
105
+ ).all()
106
+ else:
107
+ # Only company prompts, excluding system (default behavior for end users)
108
+ return self.session.query(Prompt).filter(
109
+ Prompt.company_id == company.id,
110
+ Prompt.prompt_type == PromptType.COMPANY.value
111
+ ).all()
99
112
 
100
113
  def get_prompt_by_name(self, company: Company, prompt_name: str):
101
114
  return self.session.query(Prompt).filter_by(company_id=company.id, name=prompt_name).first()
102
115
 
116
+ def get_category_by_name(self, company_id: int, name: str) -> PromptCategory:
117
+ return self.session.query(PromptCategory).filter_by(company_id=company_id, name=name).first()
118
+
119
+ def get_all_categories(self, company_id: int) -> List[PromptCategory]:
120
+ return self.session.query(PromptCategory).filter_by(company_id=company_id).order_by(PromptCategory.order).all()
121
+
103
122
  def get_system_prompts(self) -> list[Prompt]:
104
- return self.session.query(Prompt).filter_by(is_system_prompt=True, active=True).order_by(Prompt.order).all()
123
+ return self.session.query(Prompt).filter_by(prompt_type=PromptType.SYSTEM.value, active=True).order_by(Prompt.order).all()
105
124
 
125
+ def delete_prompt(self, prompt: Prompt):
126
+ self.session.delete(prompt)
127
+ self.session.commit()
@@ -17,6 +17,19 @@ import enum
17
17
  class Base(DeclarativeBase):
18
18
  pass
19
19
 
20
+
21
+ class DocumentStatus(str, enum.Enum):
22
+ PENDING = "pending"
23
+ PROCESSING = "processing"
24
+ ACTIVE = "active"
25
+ FAILED = "failed"
26
+
27
+ class PromptType(str, enum.Enum):
28
+ SYSTEM = "system"
29
+ COMPANY = "company"
30
+ AGENT = "agent"
31
+
32
+
20
33
  # relation table for many-to-many relationship between companies and users
21
34
  user_company = Table('iat_user_company',
22
35
  Base.metadata,
@@ -149,11 +162,6 @@ class Tool(Base):
149
162
  return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
150
163
 
151
164
 
152
- class DocumentStatus(str, enum.Enum):
153
- PENDING = "pending"
154
- PROCESSING = "processing"
155
- ACTIVE = "active"
156
- FAILED = "failed"
157
165
 
158
166
 
159
167
  class CollectionType(Base):
@@ -290,12 +298,13 @@ class Prompt(Base):
290
298
  description = Column(String, nullable=False)
291
299
  filename = Column(String, nullable=False)
292
300
  active = Column(Boolean, default=True)
293
- is_system_prompt = Column(Boolean, default=False)
301
+ prompt_type = Column(String, default=PromptType.COMPANY.value, nullable=False)
294
302
  order = Column(Integer, nullable=True, default=0)
295
303
  category_id = Column(Integer, ForeignKey('iat_prompt_categories.id'), nullable=True)
296
304
  custom_fields = Column(JSON, nullable=False, default=[])
297
-
298
305
  created_at = Column(DateTime, default=datetime.now)
306
+ def to_dict(self):
307
+ return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
299
308
 
300
309
  company = relationship("Company", back_populates="prompts")
301
310
  category = relationship("PromptCategory", back_populates="prompts")
@@ -38,41 +38,11 @@ class ConfigurationService:
38
38
  if company_short_name not in self._loaded_configs:
39
39
  self._loaded_configs[company_short_name] = self._load_and_merge_configs(company_short_name)
40
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
41
  def load_configuration(self, company_short_name: str):
73
42
  """
74
43
  Main entry point for configuring a company instance.
75
44
  This method is invoked by the dispatcher for each registered company.
45
+ And for the configurator, for editing the configuration of a company.
76
46
  """
77
47
  logging.info(f"⚙️ Starting configuration for company '{company_short_name}'...")
78
48
 
@@ -97,6 +67,14 @@ class ConfigurationService:
97
67
  logging.info(f"✅ Company '{company_short_name}' configured successfully.")
98
68
  return config, errors
99
69
 
70
+ def get_configuration(self, company_short_name: str, content_key: str):
71
+ """
72
+ Public method to provide a specific section of a company's configuration.
73
+ It uses a cache to avoid reading files from disk on every call.
74
+ """
75
+ self._ensure_config_loaded(company_short_name)
76
+ return self._loaded_configs[company_short_name].get(content_key)
77
+
100
78
  def update_configuration_key(self, company_short_name: str, key: str, value) -> tuple[dict, list[str]]:
101
79
  """
102
80
  Updates a specific key in the company's configuration file, validates the result,
@@ -186,7 +164,6 @@ class ConfigurationService:
186
164
 
187
165
  return config, []
188
166
 
189
-
190
167
  def validate_configuration(self, company_short_name: str) -> list[str]:
191
168
  """
192
169
  Public method to trigger validation of the current configuration.
@@ -194,83 +171,6 @@ class ConfigurationService:
194
171
  config = self._load_and_merge_configs(company_short_name)
195
172
  return self._validate_configuration(company_short_name, config)
196
173
 
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 {}
262
-
263
- # Load and merge supplementary content files (e.g., onboarding_cards)
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)
268
- else:
269
- logging.warning(f"⚠️ Warning: Content file not found: {filename}")
270
- config[key] = None
271
-
272
- return config
273
-
274
174
  def _register_company_database(self, config: dict) -> Company:
275
175
  # register the company in the database: create_or_update logic
276
176
  if not config:
@@ -374,12 +274,11 @@ class ConfigurationService:
374
274
  from iatoolkit.services.prompt_service import PromptService
375
275
  prompt_service = current_iatoolkit().get_injector().get(PromptService)
376
276
 
377
- prompts_config = config.get('prompts', {})
378
-
277
+ prompt_list, categories_config = self._get_prompt_config(config)
379
278
  prompt_service.sync_company_prompts(
380
279
  company_short_name=company_short_name,
381
- prompts_config=prompts_config.get('prompt_list', []),
382
- categories_config=prompts_config.get('prompt_categories', []),
280
+ prompt_list=prompt_list,
281
+ categories_config=categories_config,
383
282
  )
384
283
 
385
284
  def _register_knowledge_base(self, company_short_name: str, config: dict):
@@ -394,7 +293,6 @@ class ConfigurationService:
394
293
  # sync collection types in database
395
294
  knowledge_base.sync_collection_types(company_short_name, categories_config)
396
295
 
397
-
398
296
  def _validate_configuration(self, company_short_name: str, config: dict):
399
297
  """
400
298
  Validates the structure and consistency of the company.yaml configuration.
@@ -462,10 +360,9 @@ class ConfigurationService:
462
360
  add_error(f"tools[{i}]", "'params' key must be a dictionary.")
463
361
 
464
362
  # 6. Prompts
465
- prompt_list = config.get("prompts", {}).get("prompt_list", [])
466
- prompt_categories = config.get("prompts", {}).get("prompt_categories", [])
363
+ prompt_list, categories_config = self._get_prompt_config(config)
467
364
 
468
- category_set = set(prompt_categories)
365
+ category_set = set(categories_config)
469
366
  for i, prompt in enumerate(prompt_list):
470
367
  prompt_name = prompt.get("name")
471
368
  if not prompt_name:
@@ -480,10 +377,12 @@ class ConfigurationService:
480
377
  add_error(f"prompts[{i}]", "Missing required key: 'description'")
481
378
 
482
379
  prompt_cat = prompt.get("category")
483
- if not prompt_cat:
484
- add_error(f"prompts[{i}]", "Missing required key: 'category'")
485
- elif prompt_cat not in category_set:
486
- add_error(f"prompts[{i}]", f"Category '{prompt_cat}' is not defined in 'prompt_categories'.")
380
+ prompt_type = prompt.get("prompt_type", 'company').lower()
381
+ if prompt_type == 'company':
382
+ if not prompt_cat:
383
+ add_error(f"prompts[{i}]", "Missing required key: 'category'")
384
+ elif prompt_cat not in category_set:
385
+ add_error(f"prompts[{i}]", f"Category '{prompt_cat}' is not defined in 'prompt_categories'.")
487
386
 
488
387
  # 7. User Feedback
489
388
  feedback_config = config.get("parameters", {}).get("user_feedback", {})
@@ -530,3 +429,123 @@ class ConfigurationService:
530
429
 
531
430
  return errors
532
431
 
432
+
433
+ def _set_nested_value(self, data: dict, key: str, value):
434
+ """
435
+ Helper to set a value in a nested dictionary or list using dot notation (e.g. 'llm.model', 'tools.0.name').
436
+ Handles traversal through both dictionaries and lists.
437
+ """
438
+ keys = key.split('.')
439
+ current = data
440
+
441
+ # Traverse up to the parent of the target key
442
+ for i, k in enumerate(keys[:-1]):
443
+ if isinstance(current, dict):
444
+ # If it's a dict, we can traverse or create the path
445
+ current = current.setdefault(k, {})
446
+ elif isinstance(current, list):
447
+ # If it's a list, we MUST use an integer index
448
+ try:
449
+ idx = int(k)
450
+ # Allow accessing existing index
451
+ current = current[idx]
452
+ except (ValueError, IndexError) as e:
453
+ raise ValueError(
454
+ f"Invalid path: cannot access index '{k}' in list at '{'.'.join(keys[:i + 1])}'") from e
455
+ else:
456
+ raise ValueError(
457
+ f"Invalid path: '{k}' is not a container (got {type(current)}) at '{'.'.join(keys[:i + 1])}'")
458
+
459
+ # Set the final value
460
+ last_key = keys[-1]
461
+ if isinstance(current, dict):
462
+ current[last_key] = value
463
+ elif isinstance(current, list):
464
+ try:
465
+ idx = int(last_key)
466
+ # If index equals length, it means append
467
+ if idx == len(current):
468
+ current.append(value)
469
+ elif 0 <= idx < len(current):
470
+ current[idx] = value
471
+ else:
472
+ raise IndexError(f"Index {idx} out of range for list of size {len(current)}")
473
+ except (ValueError, IndexError) as e:
474
+ raise ValueError(f"Invalid path: cannot assign to index '{last_key}' in list") from e
475
+ else:
476
+ raise ValueError(f"Cannot assign value to non-container type {type(current)} at '{key}'")
477
+
478
+ def get_llm_configuration(self, company_short_name: str):
479
+ """
480
+ Convenience helper to obtain the 'llm' configuration block for a company.
481
+ Kept separate from get_configuration() to avoid coupling tests that
482
+ assert the number of calls to get_configuration().
483
+ """
484
+ default_llm_model = None
485
+ available_llm_models = []
486
+ self._ensure_config_loaded(company_short_name)
487
+ llm_config = self._loaded_configs[company_short_name].get("llm")
488
+ if llm_config:
489
+ default_llm_model = llm_config.get("model")
490
+ available_llm_models = llm_config.get('available_models') or []
491
+
492
+ # fallback: if no explicit list of models is provided, use the default model
493
+ if not available_llm_models and default_llm_model:
494
+ available_llm_models = [{
495
+ "id": default_llm_model,
496
+ "label": default_llm_model,
497
+ "description": "Modelo por defecto configurado para esta compañía."
498
+ }]
499
+ return default_llm_model, available_llm_models
500
+
501
+
502
+ def _load_and_merge_configs(self, company_short_name: str) -> dict:
503
+ """
504
+ Loads the main company.yaml and merges data from supplementary files
505
+ specified in the 'content_files' section using AssetRepository.
506
+ """
507
+ main_config_filename = "company.yaml"
508
+
509
+ # verify existence of the main configuration file
510
+ if not self.asset_repo.exists(company_short_name, AssetType.CONFIG, main_config_filename):
511
+ # raise FileNotFoundError(f"Main configuration file not found: {main_config_filename}")
512
+ logging.exception(f"Main configuration file not found: {main_config_filename}")
513
+
514
+ # return the minimal configuration needed for starting the IAToolkit
515
+ # this is a for solving a chicken/egg problem when trying to migrate the configuration
516
+ # from filesystem to database in enterprise installation
517
+ # see create_assets cli command in enterprise-iatoolkit)
518
+ return {
519
+ 'id': company_short_name,
520
+ 'name': company_short_name,
521
+ 'llm': {'model': 'gpt-5', 'provider_api_keys': {'openai':''} },
522
+ }
523
+
524
+ # read text and parse
525
+ yaml_content = self.asset_repo.read_text(company_short_name, AssetType.CONFIG, main_config_filename)
526
+ config = self.utility.load_yaml_from_string(yaml_content)
527
+ if not config:
528
+ return {}
529
+
530
+ # Load and merge supplementary content files (e.g., onboarding_cards)
531
+ for key, filename in config.get('help_files', {}).items():
532
+ if self.asset_repo.exists(company_short_name, AssetType.CONFIG, filename):
533
+ supp_content = self.asset_repo.read_text(company_short_name, AssetType.CONFIG, filename)
534
+ config[key] = self.utility.load_yaml_from_string(supp_content)
535
+ else:
536
+ logging.warning(f"⚠️ Warning: Content file not found: {filename}")
537
+ config[key] = None
538
+
539
+ return config
540
+
541
+ def _get_prompt_config(self, config):
542
+ prompts_config = config.get('prompts', {})
543
+ if isinstance(prompts_config, dict):
544
+ prompt_list = prompts_config.get('prompt_list', [])
545
+ categories_config = prompts_config.get('prompt_categories', [])
546
+ else:
547
+ prompt_list = config.get('prompts', [])
548
+ categories_config = config.get('prompt_categories', [])
549
+
550
+ return prompt_list, categories_config
551
+
@@ -87,9 +87,6 @@ class Dispatcher:
87
87
  # system tools registration
88
88
  self.tool_service.register_system_tools()
89
89
 
90
- # system prompts registration
91
- self.prompt_service.register_system_prompts()
92
-
93
90
  except Exception as e:
94
91
  self.llmquery_repo.rollback()
95
92
  raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR, str(e))