iatoolkit 0.71.4__py3-none-any.whl → 0.91.1__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 +15 -5
- iatoolkit/base_company.py +4 -58
- iatoolkit/cli_commands.py +6 -7
- iatoolkit/common/exceptions.py +1 -0
- iatoolkit/common/routes.py +12 -28
- iatoolkit/common/util.py +7 -1
- iatoolkit/company_registry.py +50 -14
- iatoolkit/{iatoolkit.py → core.py} +54 -55
- iatoolkit/infra/{mail_app.py → brevo_mail_app.py} +15 -37
- iatoolkit/infra/llm_client.py +9 -5
- iatoolkit/locales/en.yaml +10 -2
- iatoolkit/locales/es.yaml +171 -162
- iatoolkit/repositories/database_manager.py +59 -14
- iatoolkit/repositories/llm_query_repo.py +34 -22
- iatoolkit/repositories/models.py +16 -18
- iatoolkit/repositories/profile_repo.py +5 -10
- iatoolkit/repositories/vs_repo.py +9 -4
- iatoolkit/services/auth_service.py +1 -1
- iatoolkit/services/branding_service.py +1 -1
- iatoolkit/services/company_context_service.py +19 -11
- iatoolkit/services/configuration_service.py +219 -46
- iatoolkit/services/dispatcher_service.py +31 -225
- iatoolkit/services/document_service.py +10 -1
- iatoolkit/services/embedding_service.py +9 -6
- iatoolkit/services/excel_service.py +50 -2
- iatoolkit/services/history_manager_service.py +189 -0
- iatoolkit/services/jwt_service.py +1 -1
- iatoolkit/services/language_service.py +8 -2
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/services/mail_service.py +171 -25
- iatoolkit/services/profile_service.py +37 -32
- iatoolkit/services/{prompt_manager_service.py → prompt_service.py} +110 -1
- iatoolkit/services/query_service.py +192 -191
- iatoolkit/services/sql_service.py +63 -12
- iatoolkit/services/tool_service.py +231 -0
- iatoolkit/services/user_feedback_service.py +18 -6
- iatoolkit/services/user_session_context_service.py +18 -0
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +0 -0
- iatoolkit/static/js/chat_feedback_button.js +1 -1
- iatoolkit/static/js/chat_help_content.js +4 -4
- iatoolkit/static/js/chat_main.js +17 -5
- iatoolkit/static/js/chat_onboarding_button.js +1 -1
- iatoolkit/static/styles/chat_iatoolkit.css +1 -1
- iatoolkit/static/styles/chat_public.css +28 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +223 -7
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +2 -1
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +30 -5
- iatoolkit/templates/_login_widget.html +3 -3
- iatoolkit/templates/chat.html +1 -1
- iatoolkit/templates/forgot_password.html +3 -2
- iatoolkit/templates/onboarding_shell.html +1 -1
- iatoolkit/templates/signup.html +3 -0
- iatoolkit/views/base_login_view.py +1 -1
- iatoolkit/views/change_password_view.py +1 -1
- iatoolkit/views/forgot_password_view.py +9 -4
- iatoolkit/views/history_api_view.py +3 -3
- iatoolkit/views/home_view.py +4 -2
- iatoolkit/views/init_context_api_view.py +1 -1
- iatoolkit/views/llmquery_api_view.py +4 -3
- iatoolkit/views/{file_store_api_view.py → load_document_api_view.py} +1 -1
- iatoolkit/views/login_view.py +17 -5
- iatoolkit/views/logout_api_view.py +10 -2
- iatoolkit/views/prompt_api_view.py +1 -1
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +12 -4
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/verify_user_view.py +1 -1
- iatoolkit-0.91.1.dist-info/METADATA +268 -0
- iatoolkit-0.91.1.dist-info/RECORD +125 -0
- iatoolkit-0.91.1.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- iatoolkit/services/history_service.py +0 -37
- iatoolkit/templates/about.html +0 -13
- iatoolkit/templates/index.html +0 -145
- iatoolkit/templates/login_simulation.html +0 -45
- iatoolkit/views/external_login_view.py +0 -73
- iatoolkit/views/index_view.py +0 -14
- iatoolkit/views/login_simulation_view.py +0 -93
- iatoolkit-0.71.4.dist-info/METADATA +0 -276
- iatoolkit-0.71.4.dist-info/RECORD +0 -122
- {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/WHEEL +0 -0
- {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/top_level.txt +0 -0
iatoolkit/repositories/models.py
CHANGED
|
@@ -27,8 +27,6 @@ user_company = Table('iat_user_company',
|
|
|
27
27
|
Column('company_id', Integer,
|
|
28
28
|
ForeignKey('iat_companies.id',ondelete='CASCADE'),
|
|
29
29
|
primary_key=True),
|
|
30
|
-
Column('is_active', Boolean, default=True),
|
|
31
|
-
Column('role', String(50), default='user'), # Para manejar roles por empresa
|
|
32
30
|
Column('created_at', DateTime, default=datetime.now)
|
|
33
31
|
)
|
|
34
32
|
|
|
@@ -36,7 +34,7 @@ class ApiKey(Base):
|
|
|
36
34
|
"""Represents an API key for a company to authenticate against the system."""
|
|
37
35
|
__tablename__ = 'iat_api_keys'
|
|
38
36
|
|
|
39
|
-
id = Column(Integer, primary_key=True)
|
|
37
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
40
38
|
company_id = Column(Integer, ForeignKey('iat_companies.id', ondelete='CASCADE'), nullable=False)
|
|
41
39
|
key = Column(String(128), unique=True, nullable=False, index=True) # La API Key en sí
|
|
42
40
|
is_active = Column(Boolean, default=True, nullable=False)
|
|
@@ -50,7 +48,7 @@ class Company(Base):
|
|
|
50
48
|
"""Represents a company or tenant in the multi-tenant system."""
|
|
51
49
|
__tablename__ = 'iat_companies'
|
|
52
50
|
|
|
53
|
-
id = Column(Integer, primary_key=True)
|
|
51
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
54
52
|
short_name = Column(String(20), nullable=False, unique=True, index=True)
|
|
55
53
|
name = Column(String(256), nullable=False)
|
|
56
54
|
|
|
@@ -64,7 +62,7 @@ class Company(Base):
|
|
|
64
62
|
back_populates="company",
|
|
65
63
|
cascade="all, delete-orphan",
|
|
66
64
|
lazy='dynamic')
|
|
67
|
-
|
|
65
|
+
tools = relationship("Tool",
|
|
68
66
|
back_populates="company",
|
|
69
67
|
cascade="all, delete-orphan")
|
|
70
68
|
vsdocs = relationship("VSDoc",
|
|
@@ -96,7 +94,7 @@ class User(Base):
|
|
|
96
94
|
"""Represents an IAToolkit user who can be associated with multiple companies."""
|
|
97
95
|
__tablename__ = 'iat_users'
|
|
98
96
|
|
|
99
|
-
id = Column(Integer, primary_key=True)
|
|
97
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
100
98
|
email = Column(String(80), unique=True, nullable=False)
|
|
101
99
|
first_name = Column(String(50), nullable=False)
|
|
102
100
|
last_name = Column(String(50), nullable=False)
|
|
@@ -127,11 +125,11 @@ class User(Base):
|
|
|
127
125
|
'companies': [company.to_dict() for company in self.companies]
|
|
128
126
|
}
|
|
129
127
|
|
|
130
|
-
class
|
|
128
|
+
class Tool(Base):
|
|
131
129
|
"""Represents a custom or system function that the LLM can call (tool)."""
|
|
132
|
-
__tablename__ = '
|
|
130
|
+
__tablename__ = 'iat_tools'
|
|
133
131
|
|
|
134
|
-
id = Column(Integer, primary_key=True)
|
|
132
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
135
133
|
company_id = Column(Integer,
|
|
136
134
|
ForeignKey('iat_companies.id',ondelete='CASCADE'),
|
|
137
135
|
nullable=True)
|
|
@@ -142,7 +140,7 @@ class Function(Base):
|
|
|
142
140
|
is_active = Column(Boolean, default=True)
|
|
143
141
|
created_at = Column(DateTime, default=datetime.now)
|
|
144
142
|
|
|
145
|
-
company = relationship('Company', back_populates='
|
|
143
|
+
company = relationship('Company', back_populates='tools')
|
|
146
144
|
|
|
147
145
|
def to_dict(self):
|
|
148
146
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
@@ -171,7 +169,7 @@ class LLMQuery(Base):
|
|
|
171
169
|
"""Logs a query made to the LLM, including input, output, and metadata."""
|
|
172
170
|
__tablename__ = 'iat_queries'
|
|
173
171
|
|
|
174
|
-
id = Column(Integer, primary_key=True)
|
|
172
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
175
173
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
176
174
|
ondelete='CASCADE'), nullable=False)
|
|
177
175
|
user_identifier = Column(String(128), nullable=False)
|
|
@@ -196,7 +194,7 @@ class VSDoc(Base):
|
|
|
196
194
|
"""Stores a text chunk and its corresponding vector embedding for similarity search."""
|
|
197
195
|
__tablename__ = "iat_vsdocs"
|
|
198
196
|
|
|
199
|
-
id = Column(Integer, primary_key=True)
|
|
197
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
200
198
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
201
199
|
ondelete='CASCADE'), nullable=False)
|
|
202
200
|
document_id = Column(Integer, ForeignKey('iat_documents.id',
|
|
@@ -224,7 +222,7 @@ class TaskType(Base):
|
|
|
224
222
|
"""Defines a type of task that can be executed, including its prompt template."""
|
|
225
223
|
__tablename__ = 'iat_task_types'
|
|
226
224
|
|
|
227
|
-
id = Column(Integer, primary_key=True)
|
|
225
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
228
226
|
name = Column(String(100), unique=True, nullable=False)
|
|
229
227
|
prompt_template = Column(String(100), nullable=True) # Plantilla de prompt por defecto.
|
|
230
228
|
template_args = Column(JSON, nullable=True) # Argumentos/prefijos de configuración para el template.
|
|
@@ -233,7 +231,7 @@ class Task(Base):
|
|
|
233
231
|
"""Represents an asynchronous task to be executed by the system, often involving an LLM."""
|
|
234
232
|
__tablename__ = 'iat_tasks'
|
|
235
233
|
|
|
236
|
-
id = Column(Integer, primary_key=True)
|
|
234
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
237
235
|
company_id = Column(Integer, ForeignKey("iat_companies.id"))
|
|
238
236
|
|
|
239
237
|
user_id = Column(Integer, nullable=True, default=0)
|
|
@@ -263,7 +261,7 @@ class UserFeedback(Base):
|
|
|
263
261
|
"""Stores feedback and ratings submitted by users for specific interactions."""
|
|
264
262
|
__tablename__ = 'iat_feedback'
|
|
265
263
|
|
|
266
|
-
id = Column(Integer, primary_key=True)
|
|
264
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
267
265
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
268
266
|
ondelete='CASCADE'), nullable=False)
|
|
269
267
|
user_identifier = Column(String(128), default='', nullable=True)
|
|
@@ -277,7 +275,7 @@ class UserFeedback(Base):
|
|
|
277
275
|
class PromptCategory(Base):
|
|
278
276
|
"""Represents a category to group and organize prompts."""
|
|
279
277
|
__tablename__ = 'iat_prompt_categories'
|
|
280
|
-
id = Column(Integer, primary_key=True)
|
|
278
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
281
279
|
name = Column(String, nullable=False)
|
|
282
280
|
order = Column(Integer, nullable=False, default=0)
|
|
283
281
|
company_id = Column(Integer, ForeignKey('iat_companies.id'), nullable=False)
|
|
@@ -292,7 +290,7 @@ class Prompt(Base):
|
|
|
292
290
|
"""Represents a system or user-defined prompt template for the LLM."""
|
|
293
291
|
__tablename__ = 'iat_prompt'
|
|
294
292
|
|
|
295
|
-
id = Column(Integer, primary_key=True)
|
|
293
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
296
294
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
297
295
|
ondelete='CASCADE'), nullable=True)
|
|
298
296
|
name = Column(String(64), nullable=False)
|
|
@@ -313,7 +311,7 @@ class AccessLog(Base):
|
|
|
313
311
|
# Modelo ORM para registrar cada intento de acceso a la plataforma.
|
|
314
312
|
__tablename__ = 'iat_access_log'
|
|
315
313
|
|
|
316
|
-
id = Column(BigInteger, primary_key=True)
|
|
314
|
+
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
317
315
|
|
|
318
316
|
timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
|
319
317
|
company_short_name = Column(String(100), nullable=False, index=True)
|
|
@@ -98,16 +98,11 @@ class ProfileRepo:
|
|
|
98
98
|
search for an active API Key by its value.
|
|
99
99
|
returns the entry if found and is active, None otherwise.
|
|
100
100
|
"""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.first()
|
|
107
|
-
return api_key_entry
|
|
108
|
-
except Exception:
|
|
109
|
-
self.session.rollback() # Asegura que la sesión esté limpia tras un error
|
|
110
|
-
return None
|
|
101
|
+
api_key_entry = (self.session.query(ApiKey).filter
|
|
102
|
+
(ApiKey.key == api_key_value, ApiKey.is_active == True).first())
|
|
103
|
+
|
|
104
|
+
return api_key_entry
|
|
105
|
+
|
|
111
106
|
|
|
112
107
|
def get_active_api_key_by_company(self, company: Company) -> ApiKey | None:
|
|
113
108
|
return self.session.query(ApiKey)\
|
|
@@ -29,10 +29,10 @@ class VSRepo:
|
|
|
29
29
|
self.session.add(doc)
|
|
30
30
|
self.session.commit()
|
|
31
31
|
except Exception as e:
|
|
32
|
-
logging.error(f"Error inserting
|
|
32
|
+
logging.error(f"Error while inserting embedding chunk list: {str(e)}")
|
|
33
33
|
self.session.rollback()
|
|
34
34
|
raise IAToolkitException(IAToolkitException.ErrorType.VECTOR_STORE_ERROR,
|
|
35
|
-
f"Error inserting
|
|
35
|
+
f"Error while inserting embedding chunk list: {str(e)}")
|
|
36
36
|
|
|
37
37
|
def query(self,
|
|
38
38
|
company_short_name: str,
|
|
@@ -53,7 +53,12 @@ class VSRepo:
|
|
|
53
53
|
list of documents matching the query and filters
|
|
54
54
|
"""
|
|
55
55
|
# Generate the embedding with the query text for the specific company
|
|
56
|
-
|
|
56
|
+
try:
|
|
57
|
+
query_embedding = self.embedding_service.embed_text(company_short_name, query_text)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logging.error(f"error while creating text embedding: {str(e)}")
|
|
60
|
+
raise IAToolkitException(IAToolkitException.ErrorType.EMBEDDING_ERROR,
|
|
61
|
+
f"embedding error: {str(e)}")
|
|
57
62
|
|
|
58
63
|
sql_query, params = None, None
|
|
59
64
|
try:
|
|
@@ -98,7 +103,7 @@ class VSRepo:
|
|
|
98
103
|
sql_query = "".join(sql_query_parts)
|
|
99
104
|
|
|
100
105
|
# add sorting and limit of results
|
|
101
|
-
sql_query += " ORDER BY embedding <-> :query_embedding LIMIT :n_results"
|
|
106
|
+
sql_query += " ORDER BY embedding <-> CAST(:query_embedding AS VECTOR) LIMIT :n_results"
|
|
102
107
|
|
|
103
108
|
logging.debug(f"Executing SQL query: {sql_query}")
|
|
104
109
|
logging.debug(f"With parameters: {params}")
|
|
@@ -132,7 +132,7 @@ class AuthService:
|
|
|
132
132
|
# check if the api-key is valid and active
|
|
133
133
|
api_key_entry = self.profile_service.get_active_api_key_entry(api_key)
|
|
134
134
|
if not api_key_entry:
|
|
135
|
-
logging.
|
|
135
|
+
logging.error(f"Invalid or inactive IAToolkit API Key: {api_key}")
|
|
136
136
|
return {"success": False,
|
|
137
137
|
"error_message": self.i18n_service.t('errors.auth.invalid_api_key'),
|
|
138
138
|
"status_code": 402}
|
|
@@ -42,7 +42,7 @@ class BrandingService:
|
|
|
42
42
|
# Estilos para Alertas de Error ---
|
|
43
43
|
"brand_danger_color": "#dc3545", # Rojo principal para alertas
|
|
44
44
|
"brand_danger_bg": "#f8d7da", # Fondo rojo pálido
|
|
45
|
-
"brand_danger_text": "#
|
|
45
|
+
"brand_danger_text": "#000000",
|
|
46
46
|
"brand_danger_border": "#f5c2c7", # Borde rojo intermedio
|
|
47
47
|
|
|
48
48
|
# Estilos para Alertas Informativas ---
|
|
@@ -31,8 +31,7 @@ class CompanyContextService:
|
|
|
31
31
|
"""
|
|
32
32
|
Builds the full context by aggregating three sources:
|
|
33
33
|
1. Static context files (Markdown).
|
|
34
|
-
2. Static schema files (YAML for
|
|
35
|
-
3. Dynamic SQL database schema from the live connection.
|
|
34
|
+
2. Static schema files (YAML files for SQL data sources).
|
|
36
35
|
"""
|
|
37
36
|
context_parts = []
|
|
38
37
|
|
|
@@ -44,7 +43,7 @@ class CompanyContextService:
|
|
|
44
43
|
except Exception as e:
|
|
45
44
|
logging.warning(f"Could not load Markdown context for '{company_short_name}': {e}")
|
|
46
45
|
|
|
47
|
-
# 2. Context from company-specific
|
|
46
|
+
# 2. Context from company-specific database schemas (schema/*.yaml files)
|
|
48
47
|
try:
|
|
49
48
|
sql_context = self._get_sql_schema_context(company_short_name)
|
|
50
49
|
if sql_context:
|
|
@@ -118,6 +117,10 @@ class CompanyContextService:
|
|
|
118
117
|
# 2. get the global settings and overrides.
|
|
119
118
|
global_exclude_columns = source.get('exclude_columns', [])
|
|
120
119
|
table_prefix = source.get('table_prefix')
|
|
120
|
+
|
|
121
|
+
# get the global schema definition, for this source.
|
|
122
|
+
global_schema_name = source.get('schema')
|
|
123
|
+
|
|
121
124
|
table_overrides = source.get('tables', {})
|
|
122
125
|
|
|
123
126
|
# 3. iterate over the tables.
|
|
@@ -126,17 +129,21 @@ class CompanyContextService:
|
|
|
126
129
|
# 4. get the table specific configuration.
|
|
127
130
|
table_config = table_overrides.get(table_name, {})
|
|
128
131
|
|
|
129
|
-
# 5. define the schema name, using the override if it exists.
|
|
132
|
+
# 5. define the schema object name, using the override if it exists.
|
|
130
133
|
# Priority 1: Explicit override from the 'tables' map.
|
|
131
|
-
|
|
134
|
+
schema_object_name = table_config.get('schema_object_name')
|
|
135
|
+
|
|
136
|
+
# Priority 2: Global schema defined in the source.
|
|
137
|
+
if not schema_object_name and global_schema_name:
|
|
138
|
+
schema_object_name = global_schema_name
|
|
132
139
|
|
|
133
|
-
if not
|
|
134
|
-
# Priority
|
|
140
|
+
if not schema_object_name:
|
|
141
|
+
# Priority 3: Automatic prefix stripping.
|
|
135
142
|
if table_prefix and table_name.startswith(table_prefix):
|
|
136
|
-
|
|
143
|
+
schema_object_name = table_name[len(table_prefix):]
|
|
137
144
|
else:
|
|
138
|
-
# Priority
|
|
139
|
-
|
|
145
|
+
# Priority 4: Default to the table name itself.
|
|
146
|
+
schema_object_name = table_name
|
|
140
147
|
|
|
141
148
|
# 6. define the list of columns to exclude, (local vs. global).
|
|
142
149
|
local_exclude_columns = table_config.get('exclude_columns')
|
|
@@ -145,7 +152,8 @@ class CompanyContextService:
|
|
|
145
152
|
# 7. get the table schema definition.
|
|
146
153
|
table_definition = db_manager.get_table_schema(
|
|
147
154
|
table_name=table_name,
|
|
148
|
-
|
|
155
|
+
db_schema=db_manager.schema,
|
|
156
|
+
schema_object_name=schema_object_name,
|
|
149
157
|
exclude_columns=final_exclude_columns
|
|
150
158
|
)
|
|
151
159
|
sql_context += table_definition
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from iatoolkit.repositories.models import Company
|
|
7
|
+
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
8
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
7
10
|
from iatoolkit.common.util import Utility
|
|
8
11
|
from injector import inject
|
|
9
12
|
import logging
|
|
13
|
+
import os
|
|
14
|
+
|
|
10
15
|
|
|
11
16
|
class ConfigurationService:
|
|
12
17
|
"""
|
|
@@ -16,10 +21,22 @@ class ConfigurationService:
|
|
|
16
21
|
|
|
17
22
|
@inject
|
|
18
23
|
def __init__(self,
|
|
24
|
+
llm_query_repo: LLMQueryRepo,
|
|
25
|
+
profile_repo: ProfileRepo,
|
|
19
26
|
utility: Utility):
|
|
27
|
+
self.llm_query_repo = llm_query_repo
|
|
28
|
+
self.profile_repo = profile_repo
|
|
20
29
|
self.utility = utility
|
|
21
30
|
self._loaded_configs = {} # cache for store loaded configurations
|
|
22
31
|
|
|
32
|
+
def _ensure_config_loaded(self, company_short_name: str):
|
|
33
|
+
"""
|
|
34
|
+
Checks if the configuration for a company is in the cache.
|
|
35
|
+
If not, it loads it from files and stores it.
|
|
36
|
+
"""
|
|
37
|
+
if company_short_name not in self._loaded_configs:
|
|
38
|
+
self._loaded_configs[company_short_name] = self._load_and_merge_configs(company_short_name)
|
|
39
|
+
|
|
23
40
|
def get_configuration(self, company_short_name: str, content_key: str):
|
|
24
41
|
"""
|
|
25
42
|
Public method to provide a specific section of a company's configuration.
|
|
@@ -39,28 +56,26 @@ class ConfigurationService:
|
|
|
39
56
|
config = self._load_and_merge_configs(company_short_name)
|
|
40
57
|
|
|
41
58
|
# 2. Register core company details and get the database object
|
|
42
|
-
|
|
59
|
+
self._register_core_details(company_instance, config)
|
|
60
|
+
|
|
61
|
+
# 3. Register databases
|
|
62
|
+
self._register_data_sources(company_short_name, config)
|
|
43
63
|
|
|
44
|
-
#
|
|
45
|
-
self._register_tools(company_instance, config
|
|
64
|
+
# 4. Register tools
|
|
65
|
+
self._register_tools(company_instance, config)
|
|
46
66
|
|
|
47
|
-
#
|
|
67
|
+
# 5. Register prompt categories and prompts
|
|
48
68
|
self._register_prompts(company_instance, config)
|
|
49
69
|
|
|
50
|
-
#
|
|
70
|
+
# 6. Link the persisted Company object back to the running instance
|
|
51
71
|
company_instance.company_short_name = company_short_name
|
|
52
|
-
company_instance.company = company_db_object
|
|
53
72
|
company_instance.id = company_instance.company.id
|
|
54
73
|
|
|
74
|
+
# Final step: validate the configuration against platform
|
|
75
|
+
self._validate_configuration(company_short_name, config)
|
|
76
|
+
|
|
55
77
|
logging.info(f"✅ Company '{company_short_name}' configured successfully.")
|
|
56
78
|
|
|
57
|
-
def _ensure_config_loaded(self, company_short_name: str):
|
|
58
|
-
"""
|
|
59
|
-
Checks if the configuration for a company is in the cache.
|
|
60
|
-
If not, it loads it from files and stores it.
|
|
61
|
-
"""
|
|
62
|
-
if company_short_name not in self._loaded_configs:
|
|
63
|
-
self._loaded_configs[company_short_name] = self._load_and_merge_configs(company_short_name)
|
|
64
79
|
|
|
65
80
|
def _load_and_merge_configs(self, company_short_name: str) -> dict:
|
|
66
81
|
"""
|
|
@@ -87,47 +102,205 @@ class ConfigurationService:
|
|
|
87
102
|
return config
|
|
88
103
|
|
|
89
104
|
def _register_core_details(self, company_instance, config: dict) -> Company:
|
|
90
|
-
|
|
91
|
-
return company_instance._create_company(
|
|
92
|
-
short_name=config['id'],
|
|
93
|
-
name=config['name'],
|
|
94
|
-
parameters=config.get('parameters', {})
|
|
95
|
-
)
|
|
105
|
+
# register the company in the database: create_or_update logic
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
function_name=tool['function_name'],
|
|
102
|
-
description=tool['description'],
|
|
103
|
-
params=tool['params']
|
|
104
|
-
)
|
|
107
|
+
company_obj = Company(short_name=config['id'],
|
|
108
|
+
name=config['name'],
|
|
109
|
+
parameters=config.get('parameters', {}))
|
|
110
|
+
company = self.profile_repo.create_company(company_obj)
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
# save company object with the instance
|
|
113
|
+
company_instance.company = company
|
|
114
|
+
return company
|
|
115
|
+
|
|
116
|
+
def _register_data_sources(self, company_short_name: str, config: dict):
|
|
107
117
|
"""
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
Reads the data_sources config and registers databases with SqlService.
|
|
119
|
+
Uses Lazy Loading to avoid circular dependency.
|
|
120
|
+
"""
|
|
121
|
+
# Lazy import to avoid circular dependency: ConfigService -> SqlService -> I18n -> ConfigService
|
|
122
|
+
from iatoolkit import current_iatoolkit
|
|
123
|
+
from iatoolkit.services.sql_service import SqlService
|
|
124
|
+
sql_service = current_iatoolkit().get_injector().get(SqlService)
|
|
125
|
+
|
|
126
|
+
data_sources = config.get('data_sources', {})
|
|
127
|
+
sql_sources = data_sources.get('sql', [])
|
|
128
|
+
|
|
129
|
+
if not sql_sources:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
logging.info(f"🛢️ Registering databases for '{company_short_name}'...")
|
|
133
|
+
|
|
134
|
+
for db_config in sql_sources:
|
|
135
|
+
db_name = db_config.get('database')
|
|
136
|
+
db_schema = db_config.get('schema', 'public')
|
|
137
|
+
db_env_var = db_config.get('connection_string_env')
|
|
138
|
+
|
|
139
|
+
# resolve the URI
|
|
140
|
+
db_uri = os.getenv(db_env_var) if db_env_var else None
|
|
141
|
+
|
|
142
|
+
if not db_uri:
|
|
143
|
+
logging.error(
|
|
144
|
+
f"-> Skipping DB '{db_name}' for '{company_short_name}': missing URI in env '{db_env_var}'.")
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# Register with the SQL service
|
|
148
|
+
sql_service.register_database(db_uri, db_name, db_schema)
|
|
149
|
+
|
|
150
|
+
def _register_tools(self, company_instance, config: dict):
|
|
151
|
+
"""creates in the database each tool defined in the YAML."""
|
|
152
|
+
# Lazy import and resolve ToolService locally
|
|
153
|
+
from iatoolkit import current_iatoolkit
|
|
154
|
+
from iatoolkit.services.tool_service import ToolService
|
|
155
|
+
tool_service = current_iatoolkit().get_injector().get(ToolService)
|
|
156
|
+
|
|
157
|
+
tools_config = config.get('tools', [])
|
|
158
|
+
tool_service.sync_company_tools(company_instance, tools_config)
|
|
159
|
+
|
|
160
|
+
def _register_prompts(self, company_instance, config: dict):
|
|
110
161
|
"""
|
|
162
|
+
Delegates prompt synchronization to PromptService.
|
|
163
|
+
"""
|
|
164
|
+
# Lazy import to avoid circular dependency
|
|
165
|
+
from iatoolkit import current_iatoolkit
|
|
166
|
+
from iatoolkit.services.prompt_service import PromptService
|
|
167
|
+
prompt_service = current_iatoolkit().get_injector().get(PromptService)
|
|
168
|
+
|
|
111
169
|
prompts_config = config.get('prompts', [])
|
|
112
170
|
categories_config = config.get('prompt_categories', [])
|
|
113
171
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
172
|
+
prompt_service.sync_company_prompts(
|
|
173
|
+
company_instance=company_instance,
|
|
174
|
+
prompts_config=prompts_config,
|
|
175
|
+
categories_config=categories_config
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _validate_configuration(self, company_short_name: str, config: dict):
|
|
179
|
+
"""
|
|
180
|
+
Validates the structure and consistency of the company.yaml configuration.
|
|
181
|
+
It checks for required keys, valid values, and existence of related files.
|
|
182
|
+
Raises IAToolkitException if any validation error is found.
|
|
183
|
+
"""
|
|
184
|
+
errors = []
|
|
185
|
+
config_dir = Path("companies") / company_short_name / "config"
|
|
186
|
+
prompts_dir = Path("companies") / company_short_name / "prompts"
|
|
187
|
+
|
|
188
|
+
# Helper to collect errors
|
|
189
|
+
def add_error(section, message):
|
|
190
|
+
errors.append(f"[{section}] {message}")
|
|
191
|
+
|
|
192
|
+
# 1. Top-level keys
|
|
193
|
+
if not config.get("id"):
|
|
194
|
+
add_error("General", "Missing required key: 'id'")
|
|
195
|
+
elif config["id"] != company_short_name:
|
|
196
|
+
add_error("General",
|
|
197
|
+
f"'id' ({config['id']}) does not match the company short name ('{company_short_name}').")
|
|
198
|
+
if not config.get("name"):
|
|
199
|
+
add_error("General", "Missing required key: 'name'")
|
|
200
|
+
|
|
201
|
+
# 2. LLM section
|
|
202
|
+
if not isinstance(config.get("llm"), dict):
|
|
203
|
+
add_error("llm", "Missing or invalid 'llm' section.")
|
|
204
|
+
else:
|
|
205
|
+
if not config.get("llm", {}).get("model"):
|
|
206
|
+
add_error("llm", "Missing required key: 'model'")
|
|
207
|
+
if not config.get("llm", {}).get("api-key"):
|
|
208
|
+
add_error("llm", "Missing required key: 'api-key'")
|
|
118
209
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if not
|
|
122
|
-
|
|
210
|
+
# 3. Embedding Provider
|
|
211
|
+
if isinstance(config.get("embedding_provider"), dict):
|
|
212
|
+
if not config.get("embedding_provider", {}).get("provider"):
|
|
213
|
+
add_error("embedding_provider", "Missing required key: 'provider'")
|
|
214
|
+
if not config.get("embedding_provider", {}).get("model"):
|
|
215
|
+
add_error("embedding_provider", "Missing required key: 'model'")
|
|
216
|
+
if not config.get("embedding_provider", {}).get("api_key_name"):
|
|
217
|
+
add_error("embedding_provider", "Missing required key: 'api_key_name'")
|
|
218
|
+
|
|
219
|
+
# 4. Data Sources
|
|
220
|
+
for i, source in enumerate(config.get("data_sources", {}).get("sql", [])):
|
|
221
|
+
if not source.get("database"):
|
|
222
|
+
add_error(f"data_sources.sql[{i}]", "Missing required key: 'database'")
|
|
223
|
+
if not source.get("connection_string_env"):
|
|
224
|
+
add_error(f"data_sources.sql[{i}]", "Missing required key: 'connection_string_env'")
|
|
225
|
+
|
|
226
|
+
# 5. Tools
|
|
227
|
+
for i, tool in enumerate(config.get("tools", [])):
|
|
228
|
+
function_name = tool.get("function_name")
|
|
229
|
+
if not function_name:
|
|
230
|
+
add_error(f"tools[{i}]", "Missing required key: 'function_name'")
|
|
231
|
+
|
|
232
|
+
# check that function exist in dispatcher
|
|
233
|
+
if not tool.get("description"):
|
|
234
|
+
add_error(f"tools[{i}]", "Missing required key: 'description'")
|
|
235
|
+
if not isinstance(tool.get("params"), dict):
|
|
236
|
+
add_error(f"tools[{i}]", "'params' key must be a dictionary.")
|
|
237
|
+
|
|
238
|
+
# 6. Prompts
|
|
239
|
+
category_set = set(config.get("prompt_categories", []))
|
|
240
|
+
for i, prompt in enumerate(config.get("prompts", [])):
|
|
241
|
+
prompt_name = prompt.get("name")
|
|
242
|
+
if not prompt_name:
|
|
243
|
+
add_error(f"prompts[{i}]", "Missing required key: 'name'")
|
|
244
|
+
else:
|
|
245
|
+
prompt_file = prompts_dir / f"{prompt_name}.prompt"
|
|
246
|
+
if not prompt_file.is_file():
|
|
247
|
+
add_error(f"prompts/{prompt_name}:", f"Prompt file not found: {prompt_file}")
|
|
248
|
+
|
|
249
|
+
prompt_description = prompt.get("description")
|
|
250
|
+
if not prompt_description:
|
|
251
|
+
add_error(f"prompts[{i}]", "Missing required key: 'description'")
|
|
252
|
+
|
|
253
|
+
prompt_cat = prompt.get("category")
|
|
254
|
+
if not prompt_cat:
|
|
255
|
+
add_error(f"prompts[{i}]", "Missing required key: 'category'")
|
|
256
|
+
elif prompt_cat not in category_set:
|
|
257
|
+
add_error(f"prompts[{i}]", f"Category '{prompt_cat}' is not defined in 'prompt_categories'.")
|
|
258
|
+
|
|
259
|
+
# 7. User Feedback
|
|
260
|
+
feedback_config = config.get("parameters", {}).get("user_feedback", {})
|
|
261
|
+
if feedback_config.get("channel") == "email" and not feedback_config.get("destination"):
|
|
262
|
+
add_error("parameters.user_feedback", "When channel is 'email', a 'destination' is required.")
|
|
263
|
+
|
|
264
|
+
# 8. Knowledge Base
|
|
265
|
+
kb_config = config.get("knowledge_base", {})
|
|
266
|
+
if kb_config and not isinstance(kb_config, dict):
|
|
267
|
+
add_error("knowledge_base", "Section must be a dictionary.")
|
|
268
|
+
elif kb_config:
|
|
269
|
+
prod_connector = kb_config.get("connectors", {}).get("production", {})
|
|
270
|
+
if prod_connector.get("type") == "s3":
|
|
271
|
+
for key in ["bucket", "prefix", "aws_access_key_id_env", "aws_secret_access_key_env", "aws_region_env"]:
|
|
272
|
+
if not prod_connector.get(key):
|
|
273
|
+
add_error("knowledge_base.connectors.production", f"S3 connector is missing '{key}'.")
|
|
274
|
+
|
|
275
|
+
# 9. Mail Provider
|
|
276
|
+
mail_config = config.get("mail_provider", {})
|
|
277
|
+
if mail_config:
|
|
278
|
+
provider = mail_config.get("provider")
|
|
279
|
+
if not provider:
|
|
280
|
+
add_error("mail_provider", "Missing required key: 'provider'")
|
|
281
|
+
elif provider not in ["brevo_mail", "smtplib"]:
|
|
282
|
+
add_error("mail_provider", f"Unsupported provider: '{provider}'. Must be 'brevo_mail' or 'smtplib'.")
|
|
283
|
+
|
|
284
|
+
if not mail_config.get("sender_email"):
|
|
285
|
+
add_error("mail_provider", "Missing required key: 'sender_email'")
|
|
286
|
+
|
|
287
|
+
# 10. Help Files
|
|
288
|
+
for key, filename in config.get("help_files", {}).items():
|
|
289
|
+
if not filename:
|
|
290
|
+
add_error(f"help_files.{key}", "Filename cannot be empty.")
|
|
123
291
|
continue
|
|
292
|
+
help_file_path = config_dir / filename
|
|
293
|
+
if not help_file_path.is_file():
|
|
294
|
+
add_error(f"help_files.{key}", f"Help file not found: {help_file_path}")
|
|
295
|
+
|
|
296
|
+
# If any errors were found, log all messages and raise an exception
|
|
297
|
+
if errors:
|
|
298
|
+
error_summary = f"Configuration file '{company_short_name}/config/company.yaml' for '{company_short_name}' has validation errors:\n" + "\n".join(
|
|
299
|
+
f" - {e}" for e in errors)
|
|
300
|
+
logging.error(error_summary)
|
|
124
301
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
description=prompt_data['description'],
|
|
129
|
-
order=prompt_data['order'],
|
|
130
|
-
category=category_obj,
|
|
131
|
-
active=prompt_data.get('active', True),
|
|
132
|
-
custom_fields=prompt_data.get('custom_fields', [])
|
|
302
|
+
raise IAToolkitException(
|
|
303
|
+
IAToolkitException.ErrorType.CONFIG_ERROR,
|
|
304
|
+
'company.yaml validation errors'
|
|
133
305
|
)
|
|
306
|
+
|