iatoolkit 1.7.0__py3-none-any.whl → 1.15.3__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 +1 -1
- iatoolkit/common/routes.py +16 -3
- iatoolkit/common/util.py +8 -123
- iatoolkit/core.py +1 -0
- iatoolkit/infra/connectors/file_connector.py +10 -2
- iatoolkit/infra/connectors/google_drive_connector.py +3 -0
- iatoolkit/infra/connectors/local_file_connector.py +3 -0
- iatoolkit/infra/connectors/s3_connector.py +24 -1
- iatoolkit/infra/llm_providers/deepseek_adapter.py +17 -1
- iatoolkit/infra/llm_providers/gemini_adapter.py +117 -18
- iatoolkit/infra/llm_providers/openai_adapter.py +175 -18
- iatoolkit/infra/llm_response.py +13 -0
- iatoolkit/locales/en.yaml +82 -4
- iatoolkit/locales/es.yaml +79 -4
- iatoolkit/repositories/llm_query_repo.py +51 -18
- iatoolkit/repositories/models.py +16 -7
- iatoolkit/services/company_context_service.py +294 -133
- iatoolkit/services/configuration_service.py +140 -121
- iatoolkit/services/dispatcher_service.py +1 -4
- iatoolkit/services/knowledge_base_service.py +26 -4
- iatoolkit/services/llm_client_service.py +58 -2
- iatoolkit/services/prompt_service.py +251 -164
- iatoolkit/services/query_service.py +37 -18
- iatoolkit/services/storage_service.py +92 -0
- iatoolkit/static/js/chat_filepond.js +188 -63
- iatoolkit/static/js/chat_main.js +105 -52
- iatoolkit/static/styles/chat_iatoolkit.css +96 -0
- iatoolkit/system_prompts/query_main.prompt +24 -41
- iatoolkit/templates/chat.html +15 -6
- iatoolkit/views/base_login_view.py +1 -1
- iatoolkit/views/categories_api_view.py +111 -0
- iatoolkit/views/chat_view.py +1 -1
- iatoolkit/views/configuration_api_view.py +1 -1
- iatoolkit/views/login_view.py +1 -1
- iatoolkit/views/prompt_api_view.py +88 -7
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/METADATA +1 -1
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/RECORD +41 -39
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/WHEEL +0 -0
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.15.3.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
382
|
-
categories_config=
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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))
|
|
@@ -109,7 +106,7 @@ class Dispatcher:
|
|
|
109
106
|
if self.tool_service.is_system_tool(function_name):
|
|
110
107
|
# this is the system function to be executed.
|
|
111
108
|
handler = self.tool_service.get_system_handler(function_name)
|
|
112
|
-
logging.
|
|
109
|
+
logging.debug(
|
|
113
110
|
f"Calling system handler [{function_name}] "
|
|
114
111
|
f"with company_short_name={company_short_name} "
|
|
115
112
|
f"and kwargs={kwargs}"
|
|
@@ -382,27 +382,49 @@ class KnowledgeBaseService:
|
|
|
382
382
|
def sync_collection_types(self, company_short_name: str, categories_config: list):
|
|
383
383
|
"""
|
|
384
384
|
This should be called during company initialization or configuration reload.
|
|
385
|
+
Syncs DB collection types with the provided list.
|
|
386
|
+
Also updates the configuration YAML.
|
|
385
387
|
"""
|
|
386
388
|
company = self.profile_service.get_company_by_short_name(company_short_name)
|
|
387
389
|
if not company:
|
|
388
390
|
raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
|
|
389
|
-
|
|
390
|
-
|
|
391
|
+
f'Company {company_short_name} not found')
|
|
391
392
|
|
|
392
393
|
session = self.document_repo.session
|
|
394
|
+
|
|
395
|
+
# 1. Get existing types
|
|
393
396
|
existing_types = session.query(CollectionType).filter_by(company_id=company.id).all()
|
|
394
397
|
existing_names = {ct.name: ct for ct in existing_types}
|
|
395
398
|
|
|
399
|
+
# 2. Add new types
|
|
400
|
+
current_config_names = set()
|
|
396
401
|
for cat_name in categories_config:
|
|
402
|
+
current_config_names.add(cat_name)
|
|
397
403
|
if cat_name not in existing_names:
|
|
398
404
|
new_type = CollectionType(company_id=company.id, name=cat_name)
|
|
399
405
|
session.add(new_type)
|
|
400
406
|
|
|
401
|
-
#
|
|
402
|
-
#
|
|
407
|
+
# 3. Delete types not in config
|
|
408
|
+
# Note: This might cascade delete documents depending on FK setup.
|
|
409
|
+
# Assuming safe deletion is desired here to match "Sync" behavior.
|
|
410
|
+
for existing_ct in existing_types:
|
|
411
|
+
if existing_ct.name not in current_config_names:
|
|
412
|
+
session.delete(existing_ct)
|
|
403
413
|
|
|
404
414
|
session.commit()
|
|
405
415
|
|
|
416
|
+
# 4. Update Configuration YAML
|
|
417
|
+
# Lazy import to avoid circular dependency
|
|
418
|
+
from iatoolkit import current_iatoolkit
|
|
419
|
+
from iatoolkit.services.configuration_service import ConfigurationService
|
|
420
|
+
config_service = current_iatoolkit().get_injector().get(ConfigurationService)
|
|
421
|
+
|
|
422
|
+
config_service.update_configuration_key(
|
|
423
|
+
company_short_name,
|
|
424
|
+
"knowledge_base.collections",
|
|
425
|
+
categories_config
|
|
426
|
+
)
|
|
427
|
+
|
|
406
428
|
def get_collection_names(self, company_short_name: str) -> List[str]:
|
|
407
429
|
"""
|
|
408
430
|
Retrieves the names of all collections defined for a specific company.
|
|
@@ -21,6 +21,7 @@ import re
|
|
|
21
21
|
import tiktoken
|
|
22
22
|
from typing import Dict, Optional, List
|
|
23
23
|
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
24
|
+
from iatoolkit.services.storage_service import StorageService
|
|
24
25
|
|
|
25
26
|
CONTEXT_ERROR_MESSAGE = 'Tu consulta supera el límite de contexto, utiliza el boton de recarga de contexto.'
|
|
26
27
|
|
|
@@ -33,11 +34,13 @@ class llmClient:
|
|
|
33
34
|
llmquery_repo: LLMQueryRepo,
|
|
34
35
|
llm_proxy: LLMProxy,
|
|
35
36
|
model_registry: ModelRegistry,
|
|
37
|
+
storage_service: StorageService,
|
|
36
38
|
util: Utility
|
|
37
39
|
):
|
|
38
40
|
self.llmquery_repo = llmquery_repo
|
|
39
41
|
self.llm_proxy = llm_proxy
|
|
40
42
|
self.model_registry = model_registry
|
|
43
|
+
self.storage_service = storage_service
|
|
41
44
|
self.util = util
|
|
42
45
|
self._dispatcher = None # Cache for the lazy-loaded dispatcher
|
|
43
46
|
|
|
@@ -69,8 +72,10 @@ class llmClient:
|
|
|
69
72
|
text: dict,
|
|
70
73
|
model: str,
|
|
71
74
|
context_history: Optional[List[Dict]] = None,
|
|
75
|
+
images: list = None,
|
|
72
76
|
) -> dict:
|
|
73
77
|
|
|
78
|
+
images = images or []
|
|
74
79
|
f_calls = [] # keep track of the function calls executed by the LLM
|
|
75
80
|
f_call_time = 0
|
|
76
81
|
response = None
|
|
@@ -84,7 +89,7 @@ class llmClient:
|
|
|
84
89
|
|
|
85
90
|
try:
|
|
86
91
|
start_time = time.time()
|
|
87
|
-
logging.info(f"calling llm model '{model}' with {self.count_tokens(context, context_history)} tokens...")
|
|
92
|
+
logging.info(f"calling llm model '{model}' with {self.count_tokens(context, context_history)} tokens...and {len(images)} images...")
|
|
88
93
|
|
|
89
94
|
# this is the first call to the LLM on the iteration
|
|
90
95
|
try:
|
|
@@ -102,6 +107,7 @@ class llmClient:
|
|
|
102
107
|
tools=tools,
|
|
103
108
|
text=text_payload,
|
|
104
109
|
reasoning=reasoning,
|
|
110
|
+
images=images,
|
|
105
111
|
)
|
|
106
112
|
stats = self.get_stats(response)
|
|
107
113
|
|
|
@@ -163,7 +169,7 @@ class llmClient:
|
|
|
163
169
|
error_message = f"Dispatch error en {function_name} con args {args} -******- {str(e)}"
|
|
164
170
|
raise IAToolkitException(IAToolkitException.ErrorType.CALL_ERROR, error_message)
|
|
165
171
|
|
|
166
|
-
# add
|
|
172
|
+
# add the return value into the list of messages
|
|
167
173
|
input_messages.append({
|
|
168
174
|
"type": "function_call_output",
|
|
169
175
|
"call_id": tool_call.call_id,
|
|
@@ -198,9 +204,14 @@ class llmClient:
|
|
|
198
204
|
tool_choice=tool_choice_value,
|
|
199
205
|
tools=tools,
|
|
200
206
|
text=text_payload,
|
|
207
|
+
images=images,
|
|
201
208
|
)
|
|
202
209
|
stats_fcall = self.add_stats(stats_fcall, self.get_stats(response))
|
|
203
210
|
|
|
211
|
+
# --- IMAGE PROCESSING ---
|
|
212
|
+
# before save or respond, upload the images to S3 and clean content_parts
|
|
213
|
+
self._process_generated_images(response, company.short_name)
|
|
214
|
+
|
|
204
215
|
# save the statistices
|
|
205
216
|
stats['response_time']=int(time.time() - start_time)
|
|
206
217
|
stats['sql_retry_count'] = sql_retry_count
|
|
@@ -239,6 +250,7 @@ class llmClient:
|
|
|
239
250
|
'query_id': query.id,
|
|
240
251
|
'model': model,
|
|
241
252
|
'reasoning_content': final_reasoning,
|
|
253
|
+
'content_parts': response.content_parts
|
|
242
254
|
}
|
|
243
255
|
except SQLAlchemyError as db_error:
|
|
244
256
|
# rollback
|
|
@@ -295,6 +307,50 @@ class llmClient:
|
|
|
295
307
|
|
|
296
308
|
return response.id
|
|
297
309
|
|
|
310
|
+
def _process_generated_images(self, response, company_short_name: str):
|
|
311
|
+
"""
|
|
312
|
+
Traverse content_parts, detect images in Base64, upload to S3 and update content_parts.
|
|
313
|
+
"""
|
|
314
|
+
if not response.content_parts:
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
for part in response.content_parts:
|
|
318
|
+
if part.get('type') == 'image':
|
|
319
|
+
source = part.get('source', {})
|
|
320
|
+
if source.get('type') in ['base64', 'url']:
|
|
321
|
+
try:
|
|
322
|
+
if source.get('type') == 'url':
|
|
323
|
+
url = source.get('url')
|
|
324
|
+
storage_key = None
|
|
325
|
+
else:
|
|
326
|
+
# upload image to S3
|
|
327
|
+
result = self.storage_service.store_generated_image(
|
|
328
|
+
company_short_name,
|
|
329
|
+
source.get('data'),
|
|
330
|
+
source.get('media_type', 'image/png')
|
|
331
|
+
)
|
|
332
|
+
url = result['url']
|
|
333
|
+
storage_key = result['storage_key']
|
|
334
|
+
|
|
335
|
+
# Update content_part: Now it's a remote reference, not base64 anymore.
|
|
336
|
+
# We keep 'url' for the frontend to display it itself, and storage_key for internal reference.
|
|
337
|
+
part['source'] = {
|
|
338
|
+
'type': 'url',
|
|
339
|
+
'url': url,
|
|
340
|
+
'storage_key': storage_key,
|
|
341
|
+
'media_type': source.get('media_type')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# clean data
|
|
345
|
+
logging.info(f"Imagen procesada y subida: {url}")
|
|
346
|
+
|
|
347
|
+
except Exception as e:
|
|
348
|
+
logging.error(f"Fallo al subir imagen generada: {e}")
|
|
349
|
+
|
|
350
|
+
# Fallback: keep the base64 and signal the error
|
|
351
|
+
part['error'] = "Failed to upload image"
|
|
352
|
+
|
|
353
|
+
|
|
298
354
|
def decode_response(self, response) -> dict:
|
|
299
355
|
message = response.output_text
|
|
300
356
|
decoded_response = {
|