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 +1 -1
- iatoolkit/common/routes.py +15 -2
- iatoolkit/locales/en.yaml +36 -3
- iatoolkit/locales/es.yaml +34 -3
- iatoolkit/repositories/llm_query_repo.py +29 -7
- iatoolkit/repositories/models.py +16 -7
- iatoolkit/services/configuration_service.py +140 -121
- iatoolkit/services/dispatcher_service.py +0 -3
- iatoolkit/services/prompt_service.py +210 -29
- iatoolkit/views/categories_api_view.py +71 -0
- iatoolkit/views/configuration_api_view.py +1 -1
- iatoolkit/views/prompt_api_view.py +88 -7
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.9.0.dist-info}/METADATA +1 -1
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.9.0.dist-info}/RECORD +18 -17
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.9.0.dist-info}/WHEEL +0 -0
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.9.0.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.9.0.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
- {iatoolkit-1.7.0.dist-info → iatoolkit-1.9.0.dist-info}/top_level.txt +0 -0
iatoolkit/__init__.py
CHANGED
iatoolkit/common/routes.py
CHANGED
|
@@ -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
|
-
#
|
|
89
|
-
app.add_url_rule('/<company_short_name>/api/
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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()
|
iatoolkit/repositories/models.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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))
|