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.
Files changed (86) hide show
  1. iatoolkit/__init__.py +15 -5
  2. iatoolkit/base_company.py +4 -58
  3. iatoolkit/cli_commands.py +6 -7
  4. iatoolkit/common/exceptions.py +1 -0
  5. iatoolkit/common/routes.py +12 -28
  6. iatoolkit/common/util.py +7 -1
  7. iatoolkit/company_registry.py +50 -14
  8. iatoolkit/{iatoolkit.py → core.py} +54 -55
  9. iatoolkit/infra/{mail_app.py → brevo_mail_app.py} +15 -37
  10. iatoolkit/infra/llm_client.py +9 -5
  11. iatoolkit/locales/en.yaml +10 -2
  12. iatoolkit/locales/es.yaml +171 -162
  13. iatoolkit/repositories/database_manager.py +59 -14
  14. iatoolkit/repositories/llm_query_repo.py +34 -22
  15. iatoolkit/repositories/models.py +16 -18
  16. iatoolkit/repositories/profile_repo.py +5 -10
  17. iatoolkit/repositories/vs_repo.py +9 -4
  18. iatoolkit/services/auth_service.py +1 -1
  19. iatoolkit/services/branding_service.py +1 -1
  20. iatoolkit/services/company_context_service.py +19 -11
  21. iatoolkit/services/configuration_service.py +219 -46
  22. iatoolkit/services/dispatcher_service.py +31 -225
  23. iatoolkit/services/document_service.py +10 -1
  24. iatoolkit/services/embedding_service.py +9 -6
  25. iatoolkit/services/excel_service.py +50 -2
  26. iatoolkit/services/history_manager_service.py +189 -0
  27. iatoolkit/services/jwt_service.py +1 -1
  28. iatoolkit/services/language_service.py +8 -2
  29. iatoolkit/services/license_service.py +82 -0
  30. iatoolkit/services/mail_service.py +171 -25
  31. iatoolkit/services/profile_service.py +37 -32
  32. iatoolkit/services/{prompt_manager_service.py → prompt_service.py} +110 -1
  33. iatoolkit/services/query_service.py +192 -191
  34. iatoolkit/services/sql_service.py +63 -12
  35. iatoolkit/services/tool_service.py +231 -0
  36. iatoolkit/services/user_feedback_service.py +18 -6
  37. iatoolkit/services/user_session_context_service.py +18 -0
  38. iatoolkit/static/images/iatoolkit_core.png +0 -0
  39. iatoolkit/static/images/iatoolkit_logo.png +0 -0
  40. iatoolkit/static/js/chat_feedback_button.js +1 -1
  41. iatoolkit/static/js/chat_help_content.js +4 -4
  42. iatoolkit/static/js/chat_main.js +17 -5
  43. iatoolkit/static/js/chat_onboarding_button.js +1 -1
  44. iatoolkit/static/styles/chat_iatoolkit.css +1 -1
  45. iatoolkit/static/styles/chat_public.css +28 -0
  46. iatoolkit/static/styles/documents.css +598 -0
  47. iatoolkit/static/styles/landing_page.css +223 -7
  48. iatoolkit/system_prompts/__init__.py +0 -0
  49. iatoolkit/system_prompts/query_main.prompt +2 -1
  50. iatoolkit/system_prompts/sql_rules.prompt +47 -12
  51. iatoolkit/templates/_company_header.html +30 -5
  52. iatoolkit/templates/_login_widget.html +3 -3
  53. iatoolkit/templates/chat.html +1 -1
  54. iatoolkit/templates/forgot_password.html +3 -2
  55. iatoolkit/templates/onboarding_shell.html +1 -1
  56. iatoolkit/templates/signup.html +3 -0
  57. iatoolkit/views/base_login_view.py +1 -1
  58. iatoolkit/views/change_password_view.py +1 -1
  59. iatoolkit/views/forgot_password_view.py +9 -4
  60. iatoolkit/views/history_api_view.py +3 -3
  61. iatoolkit/views/home_view.py +4 -2
  62. iatoolkit/views/init_context_api_view.py +1 -1
  63. iatoolkit/views/llmquery_api_view.py +4 -3
  64. iatoolkit/views/{file_store_api_view.py → load_document_api_view.py} +1 -1
  65. iatoolkit/views/login_view.py +17 -5
  66. iatoolkit/views/logout_api_view.py +10 -2
  67. iatoolkit/views/prompt_api_view.py +1 -1
  68. iatoolkit/views/root_redirect_view.py +22 -0
  69. iatoolkit/views/signup_view.py +12 -4
  70. iatoolkit/views/static_page_view.py +27 -0
  71. iatoolkit/views/verify_user_view.py +1 -1
  72. iatoolkit-0.91.1.dist-info/METADATA +268 -0
  73. iatoolkit-0.91.1.dist-info/RECORD +125 -0
  74. iatoolkit-0.91.1.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
  75. iatoolkit/services/history_service.py +0 -37
  76. iatoolkit/templates/about.html +0 -13
  77. iatoolkit/templates/index.html +0 -145
  78. iatoolkit/templates/login_simulation.html +0 -45
  79. iatoolkit/views/external_login_view.py +0 -73
  80. iatoolkit/views/index_view.py +0 -14
  81. iatoolkit/views/login_simulation_view.py +0 -93
  82. iatoolkit-0.71.4.dist-info/METADATA +0 -276
  83. iatoolkit-0.71.4.dist-info/RECORD +0 -122
  84. {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/WHEEL +0 -0
  85. {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/licenses/LICENSE +0 -0
  86. {iatoolkit-0.71.4.dist-info → iatoolkit-0.91.1.dist-info}/top_level.txt +0 -0
@@ -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
- functions = relationship("Function",
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 Function(Base):
128
+ class Tool(Base):
131
129
  """Represents a custom or system function that the LLM can call (tool)."""
132
- __tablename__ = 'iat_functions'
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='functions')
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
- try:
102
- # Usamos joinedload para cargar la compañía en la misma consulta
103
- api_key_entry = self.session.query(ApiKey)\
104
- .options(joinedload(ApiKey.company))\
105
- .filter(ApiKey.key == api_key_value, ApiKey.is_active == True)\
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 documents into PostgreSQL: {str(e)}")
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 documents into PostgreSQL: {str(e)}")
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
- query_embedding = self.embedding_service.embed_text(company_short_name, query_text)
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.info(f"Invalid or inactive API Key {api_key}")
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": "#842029", # Texto rojo oscuro
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 APIs, etc.).
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 Python logic (SQL schemas)
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
- schema_name = table_config.get('schema_name')
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 schema_name:
134
- # Priority 2: Automatic prefix stripping.
140
+ if not schema_object_name:
141
+ # Priority 3: Automatic prefix stripping.
135
142
  if table_prefix and table_name.startswith(table_prefix):
136
- schema_name = table_name[len(table_prefix):]
143
+ schema_object_name = table_name[len(table_prefix):]
137
144
  else:
138
- # Priority 3: Default to the table name itself.
139
- schema_name = table_name
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
- schema_name=schema_name,
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
- company_db_object = self._register_core_details(company_instance, config)
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
- # 3. Register tools (functions)
45
- self._register_tools(company_instance, config.get('tools', []))
64
+ # 4. Register tools
65
+ self._register_tools(company_instance, config)
46
66
 
47
- # 4. Register prompt categories and prompts
67
+ # 5. Register prompt categories and prompts
48
68
  self._register_prompts(company_instance, config)
49
69
 
50
- # 5. Link the persisted Company object back to the running instance
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
- """Calls _create_company with data from the merged YAML config."""
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
- def _register_tools(self, company_instance, tools_config: list):
98
- """Calls _create_function for each tool defined in the YAML."""
99
- for tool in tools_config:
100
- company_instance._create_function(
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
- def _register_prompts(self, company_instance, config: dict):
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
- Creates prompt categories first, then creates each prompt and assigns
109
- it to its respective category.
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
- created_categories = {}
115
- for i, category_name in enumerate(categories_config):
116
- category_obj = company_instance._create_prompt_category(name=category_name, order=i + 1)
117
- created_categories[category_name] = category_obj
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
- for prompt_data in prompts_config:
120
- category_name = prompt_data.get('category')
121
- if not category_name or category_name not in created_categories:
122
- logging.info(f"⚠️ Warning: Prompt '{prompt_data['name']}' has an invalid or missing category. Skipping.")
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
- category_obj = created_categories[category_name]
126
- company_instance._create_prompt(
127
- prompt_name=prompt_data['name'],
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
+