iatoolkit 1.4.2__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.
Files changed (29) hide show
  1. iatoolkit/__init__.py +1 -1
  2. iatoolkit/common/interfaces/database_provider.py +13 -8
  3. iatoolkit/common/routes.py +24 -6
  4. iatoolkit/common/util.py +21 -1
  5. iatoolkit/infra/connectors/file_connector_factory.py +1 -0
  6. iatoolkit/infra/connectors/s3_connector.py +4 -2
  7. iatoolkit/locales/en.yaml +72 -5
  8. iatoolkit/locales/es.yaml +71 -4
  9. iatoolkit/repositories/database_manager.py +27 -47
  10. iatoolkit/repositories/llm_query_repo.py +29 -7
  11. iatoolkit/repositories/models.py +16 -7
  12. iatoolkit/services/company_context_service.py +44 -20
  13. iatoolkit/services/configuration_service.py +227 -71
  14. iatoolkit/services/dispatcher_service.py +0 -3
  15. iatoolkit/services/knowledge_base_service.py +14 -1
  16. iatoolkit/services/load_documents_service.py +10 -3
  17. iatoolkit/services/prompt_service.py +210 -29
  18. iatoolkit/services/sql_service.py +17 -0
  19. iatoolkit/templates/chat.html +2 -1
  20. iatoolkit/views/categories_api_view.py +71 -0
  21. iatoolkit/views/configuration_api_view.py +163 -0
  22. iatoolkit/views/prompt_api_view.py +88 -7
  23. {iatoolkit-1.4.2.dist-info → iatoolkit-1.9.0.dist-info}/METADATA +1 -1
  24. {iatoolkit-1.4.2.dist-info → iatoolkit-1.9.0.dist-info}/RECORD +28 -27
  25. iatoolkit/views/load_company_configuration_api_view.py +0 -49
  26. {iatoolkit-1.4.2.dist-info → iatoolkit-1.9.0.dist-info}/WHEEL +0 -0
  27. {iatoolkit-1.4.2.dist-info → iatoolkit-1.9.0.dist-info}/licenses/LICENSE +0 -0
  28. {iatoolkit-1.4.2.dist-info → iatoolkit-1.9.0.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
  29. {iatoolkit-1.4.2.dist-info → iatoolkit-1.9.0.dist-info}/top_level.txt +0 -0
@@ -9,7 +9,8 @@ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
9
9
  from iatoolkit.services.i18n_service import I18nService
10
10
  from iatoolkit.repositories.profile_repo import ProfileRepo
11
11
  from collections import defaultdict
12
- from iatoolkit.repositories.models import Prompt, PromptCategory, Company
12
+ from iatoolkit.repositories.models import (Prompt, PromptCategory,
13
+ Company, PromptType)
13
14
  from iatoolkit.common.exceptions import IAToolkitException
14
15
  import importlib.resources
15
16
  import logging
@@ -34,14 +35,14 @@ class PromptService:
34
35
  self.profile_repo = profile_repo
35
36
  self.i18n_service = i18n_service
36
37
 
37
- def sync_company_prompts(self, company_short_name: str, prompts_config: list, categories_config: list):
38
+ def sync_company_prompts(self, company_short_name: str, prompt_list: list, categories_config: list):
38
39
  """
39
40
  Synchronizes prompt categories and prompts from YAML config to Database.
40
41
  Strategies:
41
42
  - Categories: Create or Update existing based on name.
42
43
  - Prompts: Create or Update existing based on name. Soft-delete or Delete unused.
43
44
  """
44
- if not prompts_config:
45
+ if not prompt_list:
45
46
  return
46
47
 
47
48
  company = self.profile_repo.get_company_by_short_name(company_short_name)
@@ -49,6 +50,7 @@ class PromptService:
49
50
  raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
50
51
  f'Company {company_short_name} not found')
51
52
 
53
+ self._register_system_prompts(company)
52
54
  try:
53
55
  # 1. Sync Categories
54
56
  category_map = {}
@@ -66,7 +68,7 @@ class PromptService:
66
68
  # 2. Sync Prompts
67
69
  defined_prompt_names = set()
68
70
 
69
- for prompt_data in prompts_config:
71
+ for prompt_data in prompt_list:
70
72
  category_name = prompt_data.get('category')
71
73
  if not category_name or category_name not in category_map:
72
74
  logging.warning(
@@ -86,7 +88,7 @@ class PromptService:
86
88
  order=prompt_data.get('order'),
87
89
  category_id=category_obj.id,
88
90
  active=prompt_data.get('active', True),
89
- is_system_prompt=False,
91
+ prompt_type=PromptType.COMPANY.value,
90
92
  filename=filename,
91
93
  custom_fields=prompt_data.get('custom_fields', [])
92
94
  )
@@ -106,7 +108,7 @@ class PromptService:
106
108
  self.llm_query_repo.rollback()
107
109
  raise IAToolkitException(IAToolkitException.ErrorType.DATABASE_ERROR, str(e))
108
110
 
109
- def register_system_prompts(self):
111
+ def _register_system_prompts(self, company: Company):
110
112
  """
111
113
  Synchronizes system prompts defined in Dispatcher/Code to Database.
112
114
  """
@@ -116,20 +118,26 @@ class PromptService:
116
118
  for i, prompt_data in enumerate(_SYSTEM_PROMPTS):
117
119
  prompt_name = prompt_data['name']
118
120
  defined_names.add(prompt_name)
121
+ prompt_filename = f"{prompt_name}.prompt"
119
122
 
120
123
  new_prompt = Prompt(
121
- company_id=None, # System prompts have no company
124
+ company_id=company.id,
122
125
  name=prompt_name,
123
126
  description=prompt_data['description'],
124
127
  order=i + 1,
125
128
  category_id=None,
126
129
  active=True,
127
- is_system_prompt=True,
128
- filename=f"{prompt_name}.prompt",
130
+ prompt_type=PromptType.SYSTEM.value,
131
+ filename=prompt_filename,
129
132
  custom_fields=[]
130
133
  )
131
134
  self.llm_query_repo.create_or_update_prompt(new_prompt)
132
135
 
136
+ # add prompt to company assets
137
+ if not self.asset_repo.exists(company.short_name, AssetType.PROMPT, prompt_filename):
138
+ prompt_content = importlib.resources.read_text('iatoolkit.system_prompts', prompt_filename)
139
+ self.asset_repo.write_text(company.short_name, AssetType.PROMPT, prompt_filename, prompt_content)
140
+
133
141
  # Cleanup old system prompts
134
142
  existing_sys_prompts = self.llm_query_repo.get_system_prompts()
135
143
  for p in existing_sys_prompts:
@@ -149,7 +157,7 @@ class PromptService:
149
157
  company: Company = None,
150
158
  category: PromptCategory = None,
151
159
  active: bool = True,
152
- is_system_prompt: bool = False,
160
+ prompt_type: PromptType = PromptType.COMPANY,
153
161
  custom_fields: list = []
154
162
  ):
155
163
  """
@@ -157,7 +165,7 @@ class PromptService:
157
165
  Validates file existence before creating DB entry.
158
166
  """
159
167
  prompt_filename = prompt_name.lower() + '.prompt'
160
- if is_system_prompt:
168
+ if prompt_type == PromptType.SYSTEM:
161
169
  if not importlib.resources.files('iatoolkit.system_prompts').joinpath(prompt_filename).is_file():
162
170
  raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
163
171
  f'missing system prompt file: {prompt_filename}')
@@ -181,10 +189,10 @@ class PromptService:
181
189
  name=prompt_name,
182
190
  description=description,
183
191
  order=order,
184
- category_id=category.id if category and not is_system_prompt else None,
192
+ category_id=category.id if category and prompt_type != PromptType.SYSTEM else None,
185
193
  active=active,
186
194
  filename=prompt_filename,
187
- is_system_prompt=is_system_prompt,
195
+ prompt_type=prompt_type.value,
188
196
  custom_fields=custom_fields
189
197
  )
190
198
 
@@ -196,21 +204,25 @@ class PromptService:
196
204
 
197
205
  def get_prompt_content(self, company: Company, prompt_name: str):
198
206
  try:
199
- # get the user prompt
200
- user_prompt = self.llm_query_repo.get_prompt_by_name(company, prompt_name)
201
- if not user_prompt:
207
+ # get the prompt
208
+ prompt = self.llm_query_repo.get_prompt_by_name(company, prompt_name)
209
+ if not prompt:
202
210
  raise IAToolkitException(IAToolkitException.ErrorType.DOCUMENT_NOT_FOUND,
203
- f"prompt not found '{prompt_name}' for company '{company.short_name}'")
211
+ f"prompt not found '{prompt}' for company '{company.short_name}'")
204
212
 
205
213
  try:
206
- user_prompt_content = self.asset_repo.read_text(
207
- company.short_name,
208
- AssetType.PROMPT,
209
- user_prompt.filename
210
- )
214
+ if (prompt.prompt_type == PromptType.SYSTEM.value and
215
+ not self.asset_repo.exists(company.short_name, AssetType.PROMPT, prompt.filename)):
216
+ user_prompt_content = importlib.resources.read_text('iatoolkit.system_prompts', prompt.filename)
217
+ else:
218
+ user_prompt_content = self.asset_repo.read_text(
219
+ company.short_name,
220
+ AssetType.PROMPT,
221
+ prompt.filename
222
+ )
211
223
  except FileNotFoundError:
212
224
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
213
- f"prompt file '{user_prompt.filename}' does not exist for company '{company.short_name}'")
225
+ f"prompt file '{prompt.filename}' does not exist for company '{company.short_name}'")
214
226
  except Exception as e:
215
227
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
216
228
  f"error while reading prompt: '{prompt_name}': {e}")
@@ -225,6 +237,114 @@ class PromptService:
225
237
  raise IAToolkitException(IAToolkitException.ErrorType.PROMPT_ERROR,
226
238
  f'error loading prompt "{prompt_name}" content for company {company.short_name}: {str(e)}')
227
239
 
240
+ def save_prompt(self, company_short_name: str, prompt_name: str, data: dict):
241
+ """
242
+ Create or Update a prompt.
243
+ 1. Saves the Jinja content to the .prompt file.
244
+ 2. Updates the Metadata (params, description) in company.yaml using ConfigurationService.
245
+ 3. Updates the Database.
246
+ """
247
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
248
+ if not company:
249
+ raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
250
+ f"Company {company_short_name} not found")
251
+
252
+ # Validate category if present
253
+ category_id = None
254
+ if 'category' in data:
255
+ # simple lookup, assuming category names are unique per company
256
+ cat = self.llm_query_repo.get_category_by_name(company.id, data['category'])
257
+ if cat:
258
+ category_id = cat.id
259
+
260
+ # 1. save the phisical part of the prompt (content)
261
+ if 'content' in data:
262
+ filename = f"{prompt_name}.prompt"
263
+ filename = filename.lower().replace(' ', '_')
264
+ self.asset_repo.write_text(company_short_name, AssetType.PROMPT, filename, data['content'])
265
+
266
+ # 2. Sync the metadata with company.yaml (lazy import here)
267
+ # Extract the fields that go to the YAML
268
+ yaml_metadata = {
269
+ 'name': prompt_name,
270
+ 'description': data.get('description', ''),
271
+ 'category': data.get('category'),
272
+ 'prompt_type': data.get('prompt_type', 'company'),
273
+ 'order': data.get('order', 1),
274
+ 'active': data.get('active', True),
275
+ 'custom_fields': data.get('custom_fields', [])
276
+ }
277
+
278
+ self._sync_to_configuration(company_short_name, yaml_metadata)
279
+
280
+ # 3. Reflejar cambios en la BD inmediatamente (para no esperar recarga)
281
+ # Esto es opcional si confías en que _sync_to_configuration recargará la config,
282
+ # pero es más seguro actualizar la entidad actual.
283
+ prompt_db = self.llm_query_repo.get_prompt_by_name(company, prompt_name)
284
+ if not prompt_db:
285
+ # Create new prompt in DB immediately for responsiveness
286
+ new_prompt = Prompt(
287
+ company_id=company.id,
288
+ name=prompt_name,
289
+ description=yaml_metadata['description'],
290
+ order=yaml_metadata['order'],
291
+ category_id=category_id,
292
+ active=yaml_metadata['active'],
293
+ prompt_type=yaml_metadata['prompt_type'],
294
+ filename=f"{prompt_name.lower().replace(' ', '_')}.prompt",
295
+ custom_fields=yaml_metadata['custom_fields']
296
+ )
297
+ self.llm_query_repo.create_or_update_prompt(new_prompt)
298
+ else:
299
+ prompt_db.description = yaml_metadata['description']
300
+ prompt_db.category_id = category_id
301
+ prompt_db.order = yaml_metadata['order']
302
+ prompt_db.custom_fields = yaml_metadata['custom_fields']
303
+ prompt_db.active = yaml_metadata['active']
304
+ self.llm_query_repo.create_or_update_prompt(prompt_db)
305
+
306
+ def _sync_to_configuration(self, company_short_name: str, prompt_data: dict):
307
+ """
308
+ Usa ConfigurationService para inyectar este prompt en la lista 'prompts.prompt_list' del YAML.
309
+ """
310
+ # --- LAZY IMPORT para evitar Circular Dependency ---
311
+ from iatoolkit import current_iatoolkit
312
+ from iatoolkit.services.configuration_service import ConfigurationService
313
+
314
+ config_service = current_iatoolkit().get_injector().get(ConfigurationService)
315
+
316
+ # 1. Obtenemos la configuración actual cruda (sin objetos Python)
317
+ # Necesitamos leer la estructura para encontrar si el prompt ya existe en la lista.
318
+ full_config = config_service._load_and_merge_configs(company_short_name)
319
+
320
+ prompts_config = full_config.get('prompts', {})
321
+ # Normalizar estructura si prompts es una lista o un dict
322
+ if isinstance(prompts_config, list):
323
+ # Estructura antigua o simple, la convertimos a dict
324
+ prompts_config = {'prompt_list': prompts_config, 'prompt_categories': []}
325
+
326
+ prompt_list = prompts_config.get('prompt_list', [])
327
+
328
+ # 2. Buscar si el prompt ya existe en la lista
329
+ found_index = -1
330
+ for i, p in enumerate(prompt_list):
331
+ if p.get('name') == prompt_data['name']:
332
+ found_index = i
333
+ break
334
+
335
+ # 3. Construir la ruta de actualización (key path)
336
+ if found_index >= 0:
337
+ # Actualizar existente: "prompts.prompt_list.3"
338
+ # Nota: prompt_data contiene keys como 'description', 'custom_fields', etc.
339
+ # ConfigurationService.update_configuration_key espera una clave y un valor.
340
+ # Podríamos actualizar todo el objeto del prompt en la lista.
341
+ key_path = f"prompts.prompt_list.{found_index}"
342
+ config_service.update_configuration_key(company_short_name, key_path, prompt_data)
343
+ else:
344
+ # Crear nuevo: Agregar a la lista
345
+ # Usamos el método add_configuration_key que creaste anteriormente
346
+ config_service.add_configuration_key(company_short_name, "prompts.prompt_list", str(len(prompt_list)), prompt_data)
347
+
228
348
  def get_system_prompt(self):
229
349
  try:
230
350
  system_prompt_content = []
@@ -253,7 +373,7 @@ class PromptService:
253
373
  raise IAToolkitException(IAToolkitException.ErrorType.PROMPT_ERROR,
254
374
  f'error reading the system prompts": {str(e)}')
255
375
 
256
- def get_user_prompts(self, company_short_name: str) -> dict:
376
+ def get_user_prompts(self, company_short_name: str, include_all: bool = False) -> dict:
257
377
  try:
258
378
  # validate company
259
379
  company = self.profile_repo.get_company_by_short_name(company_short_name)
@@ -261,15 +381,31 @@ class PromptService:
261
381
  return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
262
382
 
263
383
  # get all the prompts
264
- all_prompts = self.llm_query_repo.get_prompts(company)
384
+ # If include_all is True, repo should return everything for the company
385
+ all_prompts = self.llm_query_repo.get_prompts(company, include_all=include_all)
386
+
387
+ # Deduplicate prompts by id
388
+ all_prompts = list({p.id: p for p in all_prompts}.values())
265
389
 
266
390
  # group by category
267
391
  prompts_by_category = defaultdict(list)
268
392
  for prompt in all_prompts:
269
- if prompt.active:
270
- if prompt.category:
271
- cat_key = (prompt.category.order, prompt.category.name)
272
- prompts_by_category[cat_key].append(prompt)
393
+ # Filter logic moved here or in repo.
394
+ # If include_all is False, we only want active prompts (and maybe only specific types)
395
+ if not include_all:
396
+ if not prompt.active:
397
+ continue
398
+ # Standard user view: usually excludes system/agent hidden prompts if any?
399
+ # Current requirement: "solo los de tipo company, activos" for end users
400
+ if prompt.prompt_type != PromptType.COMPANY.value:
401
+ continue
402
+
403
+ # Grouping logic
404
+ cat_key = (0, "Uncategorized") # Default
405
+ if prompt.category:
406
+ cat_key = (prompt.category.order, prompt.category.name)
407
+
408
+ prompts_by_category[cat_key].append(prompt)
273
409
 
274
410
  # sort each category by order
275
411
  for cat_key in prompts_by_category:
@@ -288,6 +424,8 @@ class PromptService:
288
424
  {
289
425
  'prompt': p.name,
290
426
  'description': p.description,
427
+ 'type': p.prompt_type,
428
+ 'active': p.active,
291
429
  'custom_fields': p.custom_fields,
292
430
  'order': p.order
293
431
  }
@@ -301,3 +439,46 @@ class PromptService:
301
439
  logging.error(f"error in get_prompts: {e}")
302
440
  return {'error': str(e)}
303
441
 
442
+ def delete_prompt(self, company_short_name: str, prompt_name: str):
443
+ """
444
+ Deletes a prompt:
445
+ 1. Removes from DB.
446
+ 2. Removes from YAML config.
447
+ 3. (Optional) Deletes/Archives physical file.
448
+ """
449
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
450
+ if not company:
451
+ raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME, f"Company not found")
452
+
453
+ prompt_db = self.llm_query_repo.get_prompt_by_name(company, prompt_name)
454
+ if not prompt_db:
455
+ raise IAToolkitException(IAToolkitException.ErrorType.DOCUMENT_NOT_FOUND, f"Prompt {prompt_name} not found")
456
+
457
+ # 1. Remove from DB
458
+ self.llm_query_repo.delete_prompt(prompt_db)
459
+
460
+ # 2. Remove from Configuration (Lazy import)
461
+ from iatoolkit import current_iatoolkit
462
+ from iatoolkit.services.configuration_service import ConfigurationService
463
+ config_service = current_iatoolkit().get_injector().get(ConfigurationService)
464
+
465
+ # We need to find the index to remove it from the list in YAML
466
+ full_config = config_service._load_and_merge_configs(company_short_name)
467
+ prompts_list = full_config.get('prompts', {}).get('prompt_list', [])
468
+
469
+ found_index = -1
470
+ for i, p in enumerate(prompts_list):
471
+ if p.get('name') == prompt_name:
472
+ found_index = i
473
+ break
474
+
475
+ if found_index >= 0:
476
+ # This is tricky with current ConfigService if it doesn't support list item deletion easily.
477
+ # Assuming we might need to implement a 'delete_configuration_key' or similar,
478
+ # OR just leave it in config but update DB. For now, let's assume manual config cleanup or
479
+ # implement a specific removal if ConfigService supports it.
480
+ # If ConfigService doesn't support removal, we might just mark it inactive in config.
481
+ pass
482
+ # config_service.remove_list_item(company_short_name, "prompts.prompt_list", found_index)
483
+
484
+
@@ -171,4 +171,21 @@ class SqlService:
171
171
  logging.error(f"Error while committing sql: '{str(e)}'")
172
172
  raise IAToolkitException(
173
173
  IAToolkitException.ErrorType.DATABASE_ERROR, str(e)
174
+ )
175
+
176
+ def get_database_structure(self, company_short_name: str, db_name: str) -> dict:
177
+ """
178
+ Introspects the specified database and returns its structure (Tables & Columns).
179
+ Used for the Schema Editor 2.0
180
+ """
181
+ try:
182
+ provider = self.get_database_provider(company_short_name, db_name)
183
+ return provider.get_database_structure()
184
+ except IAToolkitException:
185
+ raise
186
+ except Exception as e:
187
+ logging.error(f"Error introspecting database '{db_name}': {e}")
188
+ raise IAToolkitException(
189
+ IAToolkitException.ErrorType.DATABASE_ERROR,
190
+ f"Failed to introspect database: {str(e)}"
174
191
  )
@@ -8,6 +8,7 @@
8
8
  {# Movemos los estilos y los links aquí para que se rendericen en el <head> #}
9
9
  <style>
10
10
  {{ branding.css_variables | safe }}
11
+ {{ branding.css_variables | safe }}
11
12
  </style>
12
13
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
13
14
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
@@ -104,7 +105,7 @@
104
105
  style="color: {{ branding.header_text_color }};">
105
106
  <i class="bi bi-question-circle-fill"></i>
106
107
  </a>
107
- {% if user_role == "admin" and license == 'enterprise' %}
108
+ {% if user_role != "user" and license == 'enterprise' %}
108
109
  <a href="/{{ company_short_name }}/admin/dashboard"
109
110
  target="_blank"
110
111
  id="preferences-button"
@@ -0,0 +1,71 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from flask import jsonify
7
+ from flask.views import MethodView
8
+ from injector import inject
9
+ from iatoolkit.services.auth_service import AuthService
10
+ from iatoolkit.services.profile_service import ProfileService
11
+ from iatoolkit.services.configuration_service import ConfigurationService
12
+ from iatoolkit.services.knowledge_base_service import KnowledgeBaseService
13
+ from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
14
+ from iatoolkit.repositories.models import PromptType, PromptCategory
15
+ import logging
16
+
17
+ class CategoriesApiView(MethodView):
18
+ """
19
+ Endpoint to retrieve all available categories and types in the system.
20
+ Useful for populating dropdowns in the frontend.
21
+ """
22
+ @inject
23
+ def __init__(self,
24
+ auth_service: AuthService,
25
+ profile_service: ProfileService,
26
+ configuration_service: ConfigurationService,
27
+ knowledge_base_service: KnowledgeBaseService,
28
+ llm_query_repo: LLMQueryRepo):
29
+ self.auth_service = auth_service
30
+ self.profile_service = profile_service
31
+ self.knowledge_base_service = knowledge_base_service
32
+ self.llm_query_repo = llm_query_repo
33
+ self.configuration_service = configuration_service
34
+
35
+ def get(self, company_short_name):
36
+ try:
37
+ # 1. Verify Authentication
38
+ auth_result = self.auth_service.verify()
39
+ if not auth_result.get("success"):
40
+ return jsonify(auth_result), 401
41
+
42
+ # 2. Get Company
43
+ company = self.profile_service.get_company_by_short_name(company_short_name)
44
+ if not company:
45
+ return jsonify({"error": "Company not found"}), 404
46
+
47
+ # 3. Gather Categories
48
+ response_data = {
49
+ "prompt_types": [t.value for t in PromptType],
50
+ "prompt_categories": [],
51
+ "collection_types": [],
52
+ # Future categories can be added here (e.g., tool_types, user_roles)
53
+ }
54
+
55
+ # A. Prompt Categories (from DB)
56
+ prompt_cats = self.llm_query_repo.get_all_categories(company_id=company.id)
57
+ response_data["prompt_categories"] = [c.name for c in prompt_cats]
58
+
59
+ # B. Collection Types (from KnowledgeBaseService)
60
+ response_data["collection_types"] = self.knowledge_base_service.get_collection_names(company_short_name)
61
+
62
+ # C. LLM Models (from ConfigurationService)
63
+ _, llm_models = self.configuration_service.get_llm_configuration(company_short_name)
64
+ # Extract only IDs
65
+ response_data["llm_models"] = [m['id'] for m in llm_models if 'id' in m]
66
+
67
+ return jsonify(response_data)
68
+
69
+ except Exception as e:
70
+ logging.exception(f"Error fetching categories for {company_short_name}: {e}")
71
+ return jsonify({"status": "error", "message": str(e)}), 500
@@ -0,0 +1,163 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ from flask import jsonify, request
7
+ from flask.views import MethodView
8
+ from injector import inject
9
+ from iatoolkit.services.configuration_service import ConfigurationService
10
+ from iatoolkit.services.profile_service import ProfileService
11
+ from iatoolkit.services.auth_service import AuthService
12
+ import logging
13
+
14
+
15
+ class ConfigurationApiView(MethodView):
16
+ """
17
+ API View to manage company configuration.
18
+ Supports loading, updating specific keys, and validating the configuration.
19
+ """
20
+ @inject
21
+ def __init__(self,
22
+ configuration_service: ConfigurationService,
23
+ profile_service: ProfileService,
24
+ auth_service: AuthService):
25
+ self.configuration_service = configuration_service
26
+ self.profile_service = profile_service
27
+ self.auth_service = auth_service
28
+
29
+ def get(self, company_short_name: str = None):
30
+ """
31
+ Loads the current configuration for the company.
32
+ """
33
+ try:
34
+ # 1. Verify authentication
35
+ auth_result = self.auth_service.verify(anonymous=True)
36
+ if not auth_result.get("success"):
37
+ return jsonify(auth_result), auth_result.get("status_code", 401)
38
+
39
+ company = self.profile_service.get_company_by_short_name(company_short_name)
40
+ if not company:
41
+ return jsonify({"error": "company not found."}), 404
42
+
43
+ config, errors = self.configuration_service.load_configuration(company_short_name)
44
+
45
+ # Register data sources to ensure services are up to date with loaded config
46
+ if config:
47
+ self.configuration_service.register_data_sources(company_short_name)
48
+
49
+ # Remove non-serializable objects
50
+ if 'company' in config:
51
+ config.pop('company')
52
+
53
+ status_code = 200 if not errors else 400
54
+ return jsonify({'config': config, 'errors': errors}), status_code
55
+ except Exception as e:
56
+ logging.exception(f"Unexpected error loading config: {e}")
57
+ return jsonify({'status': 'error', 'message': str(e)}), 500
58
+
59
+ def patch(self, company_short_name: str):
60
+ """
61
+ Updates a specific configuration key.
62
+ Body: { "key": "llm.model", "value": "gpt-4" }
63
+ """
64
+ try:
65
+ auth_result = self.auth_service.verify()
66
+ if not auth_result.get("success"):
67
+ return jsonify(auth_result), 401
68
+
69
+ payload = request.get_json()
70
+ key = payload.get('key')
71
+ value = payload.get('value')
72
+
73
+ if not key:
74
+ return jsonify({'error': 'Missing "key" in payload'}), 400
75
+
76
+ logging.info(f"Updating config key '{key}' for company '{company_short_name}'")
77
+
78
+ updated_config, errors = self.configuration_service.update_configuration_key(
79
+ company_short_name, key, value
80
+ )
81
+
82
+ # Remove non-serializable objects
83
+ if 'company' in updated_config:
84
+ updated_config.pop('company')
85
+
86
+ if errors:
87
+ return jsonify({'status': 'invalid', 'errors': errors, 'config': updated_config}), 400
88
+
89
+ return jsonify({'status': 'success', 'config': updated_config}), 200
90
+
91
+ except FileNotFoundError:
92
+ return jsonify({'error': 'Configuration file not found'}), 404
93
+ except Exception as e:
94
+ logging.exception(f"Error updating config: {e}")
95
+ return jsonify({'status': 'error', 'message': str(e)}), 500
96
+
97
+ def post(self, company_short_name: str):
98
+ """
99
+ Adds a new configuration key.
100
+ Body: { "parent_key": "llm", "key": "max_tokens", "value": 2048 }
101
+ """
102
+ try:
103
+ auth_result = self.auth_service.verify(anonymous=False)
104
+ if not auth_result.get("success"):
105
+ return jsonify(auth_result), 401
106
+
107
+ payload = request.get_json()
108
+ parent_key = payload.get('parent_key', '') # Optional, defaults to root
109
+ key = payload.get('key')
110
+ value = payload.get('value')
111
+
112
+ if not key:
113
+ return jsonify({'error': 'Missing "key" in payload'}), 400
114
+
115
+ logging.info(f"Adding config key '{key}' under '{parent_key}' for company '{company_short_name}'")
116
+
117
+ updated_config, errors = self.configuration_service.add_configuration_key(
118
+ company_short_name, parent_key, key, value
119
+ )
120
+
121
+ # Remove non-serializable objects
122
+ if 'company' in updated_config:
123
+ updated_config.pop('company')
124
+
125
+ if errors:
126
+ return jsonify({'status': 'invalid', 'errors': errors, 'config': updated_config}), 400
127
+
128
+ return jsonify({'status': 'success', 'config': updated_config}), 200
129
+
130
+ except FileNotFoundError:
131
+ return jsonify({'error': 'Configuration file not found'}), 404
132
+ except Exception as e:
133
+ logging.exception(f"Error adding config key: {e}")
134
+ return jsonify({'status': 'error', 'message': str(e)}), 500
135
+
136
+ class ValidateConfigurationApiView(MethodView):
137
+ """
138
+ API View to trigger an explicit validation of the current configuration.
139
+ Useful for UI to check status without modifying data.
140
+ """
141
+ @inject
142
+ def __init__(self,
143
+ configuration_service: ConfigurationService,
144
+ auth_service: AuthService):
145
+ self.configuration_service = configuration_service
146
+ self.auth_service = auth_service
147
+
148
+ def get(self, company_short_name: str):
149
+ try:
150
+ auth_result = self.auth_service.verify(anonymous=False)
151
+ if not auth_result.get("success"):
152
+ return jsonify(auth_result), 401
153
+
154
+ errors = self.configuration_service.validate_configuration(company_short_name)
155
+
156
+ if errors:
157
+ return jsonify({'status': 'invalid', 'errors': errors}), 200 # 200 OK because check succeeded
158
+
159
+ return jsonify({'status': 'valid', 'errors': []}), 200
160
+
161
+ except Exception as e:
162
+ logging.exception(f"Error validating config: {e}")
163
+ return jsonify({'status': 'error', 'message': str(e)}), 500