iatoolkit 0.91.1__py3-none-any.whl → 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. iatoolkit/__init__.py +6 -4
  2. iatoolkit/base_company.py +0 -16
  3. iatoolkit/cli_commands.py +3 -14
  4. iatoolkit/common/exceptions.py +1 -0
  5. iatoolkit/common/interfaces/__init__.py +0 -0
  6. iatoolkit/common/interfaces/asset_storage.py +34 -0
  7. iatoolkit/common/interfaces/database_provider.py +43 -0
  8. iatoolkit/common/model_registry.py +159 -0
  9. iatoolkit/common/routes.py +47 -5
  10. iatoolkit/common/util.py +32 -13
  11. iatoolkit/company_registry.py +5 -0
  12. iatoolkit/core.py +51 -20
  13. iatoolkit/infra/connectors/file_connector_factory.py +1 -0
  14. iatoolkit/infra/connectors/s3_connector.py +4 -2
  15. iatoolkit/infra/llm_providers/__init__.py +0 -0
  16. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  17. iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
  18. iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
  19. iatoolkit/infra/llm_proxy.py +235 -134
  20. iatoolkit/infra/llm_response.py +5 -0
  21. iatoolkit/locales/en.yaml +158 -2
  22. iatoolkit/locales/es.yaml +158 -0
  23. iatoolkit/repositories/database_manager.py +52 -47
  24. iatoolkit/repositories/document_repo.py +7 -0
  25. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  26. iatoolkit/repositories/llm_query_repo.py +2 -0
  27. iatoolkit/repositories/models.py +72 -79
  28. iatoolkit/repositories/profile_repo.py +59 -3
  29. iatoolkit/repositories/vs_repo.py +22 -24
  30. iatoolkit/services/company_context_service.py +126 -53
  31. iatoolkit/services/configuration_service.py +299 -73
  32. iatoolkit/services/dispatcher_service.py +21 -3
  33. iatoolkit/services/file_processor_service.py +0 -5
  34. iatoolkit/services/history_manager_service.py +43 -24
  35. iatoolkit/services/knowledge_base_service.py +425 -0
  36. iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +38 -29
  37. iatoolkit/services/load_documents_service.py +26 -48
  38. iatoolkit/services/profile_service.py +32 -4
  39. iatoolkit/services/prompt_service.py +32 -30
  40. iatoolkit/services/query_service.py +51 -26
  41. iatoolkit/services/sql_service.py +122 -74
  42. iatoolkit/services/tool_service.py +26 -11
  43. iatoolkit/services/user_session_context_service.py +115 -63
  44. iatoolkit/static/js/chat_main.js +44 -4
  45. iatoolkit/static/js/chat_model_selector.js +227 -0
  46. iatoolkit/static/js/chat_onboarding_button.js +1 -1
  47. iatoolkit/static/js/chat_reload_button.js +4 -1
  48. iatoolkit/static/styles/chat_iatoolkit.css +58 -2
  49. iatoolkit/static/styles/llm_output.css +34 -1
  50. iatoolkit/system_prompts/query_main.prompt +26 -2
  51. iatoolkit/templates/base.html +13 -0
  52. iatoolkit/templates/chat.html +45 -2
  53. iatoolkit/templates/onboarding_shell.html +0 -1
  54. iatoolkit/views/base_login_view.py +7 -2
  55. iatoolkit/views/chat_view.py +76 -0
  56. iatoolkit/views/configuration_api_view.py +163 -0
  57. iatoolkit/views/load_document_api_view.py +14 -10
  58. iatoolkit/views/login_view.py +8 -3
  59. iatoolkit/views/rag_api_view.py +216 -0
  60. iatoolkit/views/users_api_view.py +33 -0
  61. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/METADATA +4 -4
  62. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/RECORD +66 -58
  63. iatoolkit/repositories/tasks_repo.py +0 -52
  64. iatoolkit/services/search_service.py +0 -55
  65. iatoolkit/services/tasks_service.py +0 -188
  66. iatoolkit/views/tasks_api_view.py +0 -72
  67. iatoolkit/views/tasks_review_api_view.py +0 -55
  68. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/WHEEL +0 -0
  69. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE +0 -0
  70. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
  71. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,7 @@ from iatoolkit.repositories.models import Company, LLMQuery
8
8
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
9
9
  from sqlalchemy.exc import SQLAlchemyError, OperationalError
10
10
  from iatoolkit.common.util import Utility
11
+ from iatoolkit.common.model_registry import ModelRegistry
11
12
  from injector import inject
12
13
  import time
13
14
  import markdown2
@@ -30,11 +31,13 @@ class llmClient:
30
31
  @inject
31
32
  def __init__(self,
32
33
  llmquery_repo: LLMQueryRepo,
33
- llm_proxy_factory: LLMProxy,
34
+ llm_proxy: LLMProxy,
35
+ model_registry: ModelRegistry,
34
36
  util: Utility
35
37
  ):
36
38
  self.llmquery_repo = llmquery_repo
37
- self.llm_proxy_factory = llm_proxy_factory
39
+ self.llm_proxy = llm_proxy
40
+ self.model_registry = model_registry
38
41
  self.util = util
39
42
  self._dispatcher = None # Cache for the lazy-loaded dispatcher
40
43
 
@@ -73,21 +76,15 @@ class llmClient:
73
76
  response = None
74
77
  sql_retry_count = 0
75
78
  force_tool_name = None
76
- reasoning = {}
77
79
 
78
- if model in ('gpt-5', 'gpt-5-mini'):
79
- text['verbosity'] = "low"
80
- reasoning = {"effort": 'minimal'}
81
- elif model == 'gpt-5.1':
82
- text['verbosity'] = "low"
83
- reasoning = {"effort": 'low'}
80
+ # Resolve per-model defaults and apply overrides (without mutating inputs).
81
+ request_params = self.model_registry.resolve_request_params(model=model, text=text)
82
+ text_payload = request_params["text"]
83
+ reasoning = request_params["reasoning"]
84
84
 
85
85
  try:
86
86
  start_time = time.time()
87
- logging.info(f"calling llm model '{model}' with {self.count_tokens(context)} tokens...")
88
-
89
- # get the proxy for the company
90
- llm_proxy = self.llm_proxy_factory.create_for_company(company)
87
+ logging.info(f"calling llm model '{model}' with {self.count_tokens(context, context_history)} tokens...")
91
88
 
92
89
  # this is the first call to the LLM on the iteration
93
90
  try:
@@ -96,13 +93,14 @@ class llmClient:
96
93
  "content": context
97
94
  }]
98
95
 
99
- response = llm_proxy.create_response(
96
+ response = self.llm_proxy.create_response(
97
+ company_short_name=company.short_name,
100
98
  model=model,
99
+ input=input_messages,
101
100
  previous_response_id=previous_response_id,
102
101
  context_history=context_history,
103
- input=input_messages,
104
102
  tools=tools,
105
- text=text,
103
+ text=text_payload,
106
104
  reasoning=reasoning,
107
105
  )
108
106
  stats = self.get_stats(response)
@@ -130,8 +128,14 @@ class llmClient:
130
128
  # execute the function call through the dispatcher
131
129
  fcall_time = time.time()
132
130
  function_name = tool_call.name
133
- args = json.loads(tool_call.arguments)
134
- logging.info(f"start execution fcall: {function_name}")
131
+
132
+ try:
133
+ args = json.loads(tool_call.arguments)
134
+ except Exception as e:
135
+ logging.error(f"[Dispatcher] json.loads failed: {e}")
136
+ raise
137
+ logging.debug(f"[Dispatcher] Parsed args = {args}")
138
+
135
139
  try:
136
140
  result = self.dispatcher.dispatch(
137
141
  company_short_name=company.short_name,
@@ -163,6 +167,7 @@ class llmClient:
163
167
  input_messages.append({
164
168
  "type": "function_call_output",
165
169
  "call_id": tool_call.call_id,
170
+ "status": "completed",
166
171
  "output": str(result)
167
172
  })
168
173
  function_calls = True
@@ -173,7 +178,7 @@ class llmClient:
173
178
  f_calls.append(f_call_identity)
174
179
  f_call_time += elapsed
175
180
 
176
- logging.info(f"[{company.short_name}] Tool end execution: {function_name} in {elapsed:.1f} secs.")
181
+ logging.info(f"[{company.short_name}] end execution of tool: {function_name} in {elapsed:.1f} secs.")
177
182
 
178
183
  if not function_calls:
179
184
  break # no more function calls, the answer to send back to llm
@@ -183,7 +188,8 @@ class llmClient:
183
188
  if force_tool_name:
184
189
  tool_choice_value = "required"
185
190
 
186
- response = llm_proxy.create_response(
191
+ response = self.llm_proxy.create_response(
192
+ company_short_name=company.short_name,
187
193
  model=model,
188
194
  input=input_messages,
189
195
  previous_response_id=response.id,
@@ -191,7 +197,7 @@ class llmClient:
191
197
  reasoning=reasoning,
192
198
  tool_choice=tool_choice_value,
193
199
  tools=tools,
194
- text=text
200
+ text=text_payload,
195
201
  )
196
202
  stats_fcall = self.add_stats(stats_fcall, self.get_stats(response))
197
203
 
@@ -203,9 +209,11 @@ class llmClient:
203
209
  # decode the LLM response
204
210
  decoded_response = self.decode_response(response)
205
211
 
212
+ # Extract reasoning from the final response object
213
+ final_reasoning = getattr(response, 'reasoning_content', '')
214
+
206
215
  # save the query and response
207
216
  query = LLMQuery(user_identifier=user_identifier,
208
- task_id=0,
209
217
  company_id=company.id,
210
218
  query=question,
211
219
  output=decoded_response.get('answer', ''),
@@ -230,6 +238,7 @@ class llmClient:
230
238
  'response_id': response.id,
231
239
  'query_id': query.id,
232
240
  'model': model,
241
+ 'reasoning_content': final_reasoning,
233
242
  }
234
243
  except SQLAlchemyError as db_error:
235
244
  # rollback
@@ -244,11 +253,10 @@ class llmClient:
244
253
 
245
254
  # log the error in the llm_query table
246
255
  query = LLMQuery(user_identifier=user_identifier,
247
- task_id=0,
248
256
  company_id=company.id,
249
257
  query=question,
250
258
  output=error_message,
251
- response=response.output_text if response else {},
259
+ response={},
252
260
  valid_response=False,
253
261
  function_calls=f_calls,
254
262
  )
@@ -269,14 +277,15 @@ class llmClient:
269
277
 
270
278
  logging.info(f"initializing model '{model}' with company context: {self.count_tokens(company_base_context)} tokens...")
271
279
 
272
- llm_proxy = self.llm_proxy_factory.create_for_company(company)
273
280
  try:
274
- response = llm_proxy.create_response(
281
+ response = self.llm_proxy.create_response(
282
+ company_short_name=company.short_name,
275
283
  model=model,
276
284
  input=[{
277
285
  "role": "system",
278
286
  "content": company_base_context
279
- }]
287
+ }],
288
+
280
289
  )
281
290
 
282
291
  except Exception as e:
@@ -423,7 +432,7 @@ class llmClient:
423
432
  html_answer = markdown2.markdown(answer).replace("\n", "")
424
433
  return html_answer
425
434
 
426
- def count_tokens(self, text):
435
+ def count_tokens(self, text, history = []):
427
436
  # Codifica el texto y cuenta la cantidad de tokens
428
- tokens = self.encoding.encode(text)
437
+ tokens = self.encoding.encode(text + json.dumps(history))
429
438
  return len(tokens)
@@ -1,17 +1,13 @@
1
1
  # Copyright (c) 2024 Fernando Libedinsky
2
2
  # Product: IAToolkit
3
3
 
4
- from iatoolkit.repositories.vs_repo import VSRepo
5
- from iatoolkit.repositories.document_repo import DocumentRepo
6
- from iatoolkit.repositories.models import Document, VSDoc, Company
7
- from iatoolkit.services.document_service import DocumentService
4
+ from iatoolkit.repositories.models import Company
8
5
  from iatoolkit.services.configuration_service import ConfigurationService
9
- from langchain.text_splitter import RecursiveCharacterTextSplitter
6
+ from iatoolkit.services.knowledge_base_service import KnowledgeBaseService
10
7
  from iatoolkit.infra.connectors.file_connector_factory import FileConnectorFactory
11
8
  from iatoolkit.services.file_processor_service import FileProcessorConfig, FileProcessor
12
9
  from iatoolkit.common.exceptions import IAToolkitException
13
10
  import logging
14
- import base64
15
11
  from injector import inject, singleton
16
12
  import os
17
13
 
@@ -19,31 +15,21 @@ import os
19
15
  @singleton
20
16
  class LoadDocumentsService:
21
17
  """
22
- Orchestrates the process of loading, processing, and storing documents
23
- from various sources defined in the company's configuration.
18
+ Orchestrates the discovery and loading of documents from configured sources.
19
+ Delegates the processing and ingestion logic to KnowledgeBaseService.
24
20
  """
25
21
  @inject
26
22
  def __init__(self,
27
23
  config_service: ConfigurationService,
28
24
  file_connector_factory: FileConnectorFactory,
29
- doc_service: DocumentService,
30
- doc_repo: DocumentRepo,
31
- vector_store: VSRepo,
25
+ knowledge_base_service: KnowledgeBaseService
32
26
  ):
33
27
  self.config_service = config_service
34
- self.doc_service = doc_service
35
- self.doc_repo = doc_repo
36
- self.vector_store = vector_store
37
28
  self.file_connector_factory = file_connector_factory
29
+ self.knowledge_base_service = knowledge_base_service
38
30
 
39
31
  logging.getLogger().setLevel(logging.ERROR)
40
32
 
41
- self.splitter = RecursiveCharacterTextSplitter(
42
- chunk_size=1000,
43
- chunk_overlap=100,
44
- separators=["\n\n", "\n", "."]
45
- )
46
-
47
33
  def load_sources(self,
48
34
  company: Company,
49
35
  sources_to_load: list[str] = None,
@@ -67,7 +53,7 @@ class LoadDocumentsService:
67
53
 
68
54
  if not sources_to_load:
69
55
  raise IAToolkitException(IAToolkitException.ErrorType.PARAM_NOT_FILLED,
70
- f"Missing sources to load for company '{company.short_name}'.")
56
+ f"Missing sources to load for company '{company.short_name}'.")
71
57
 
72
58
  base_connector_config = self._get_base_connector_config(knowledge_base_config)
73
59
  all_sources = knowledge_base_config.get('document_sources', {})
@@ -79,16 +65,24 @@ class LoadDocumentsService:
79
65
  logging.warning(f"Source '{source_name}' not found in configuration for company '{company.short_name}'. Skipping.")
80
66
  continue
81
67
 
68
+ collection = source_config.get('collection')
69
+ if not collection:
70
+ logging.warning(
71
+ f"Document Source '{source_name}' missing collection definition en company.yaml, Skipping.")
72
+ continue
73
+
82
74
  try:
83
- logging.info(f"Processing source '{source_name}' for company '{company.short_name}'...")
75
+ logging.info(f"company {company.short_name}: loading source '{source_name}' into collection '{collection}'...")
84
76
 
85
77
  # Combine the base connector configuration with the specific path from the source.
86
78
  full_connector_config = base_connector_config.copy()
87
79
  full_connector_config['path'] = source_config.get('path')
80
+ full_connector_config['folder'] = source_config.get('folder')
88
81
 
89
82
  # Prepare the context for the callback function.
90
83
  context = {
91
84
  'company': company,
85
+ 'collection': collection,
92
86
  'metadata': source_config.get('metadata', {})
93
87
  }
94
88
 
@@ -130,45 +124,29 @@ class LoadDocumentsService:
130
124
 
131
125
  def _file_processing_callback(self, company: Company, filename: str, content: bytes, context: dict = None):
132
126
  """
133
- Callback method to process a single file. It extracts text, merges metadata,
134
- and saves the document to both relational and vector stores.
127
+ Callback method to process a single file.
128
+ Delegates the actual ingestion (storage, vectorization) to KnowledgeBaseService.
135
129
  """
136
130
  if not company:
137
131
  raise IAToolkitException(IAToolkitException.ErrorType.MISSING_PARAMETER, "Missing company object in callback.")
138
132
 
139
- if self.doc_repo.get(company_id=company.id, filename=filename):
140
- logging.debug(f"File '{filename}' already exists for company '{company.id}'. Skipping.")
141
- return
142
-
143
133
  try:
144
- document_content = self.doc_service.file_to_txt(filename, content)
145
-
146
134
  # Get predefined metadata from the context passed by the processor.
147
135
  predefined_metadata = context.get('metadata', {}) if context else {}
148
136
 
149
- # Save the document to the relational database.
150
- session = self.doc_repo.session
151
- new_document = Document(
152
- company_id=company.id,
137
+ # Delegate heavy lifting to KnowledgeBaseService
138
+ new_document = self.knowledge_base_service.ingest_document_sync(
139
+ company=company,
153
140
  filename=filename,
154
- content=document_content,
155
- content_b64=base64.b64encode(content).decode('utf-8'),
156
- meta=predefined_metadata
141
+ content=content,
142
+ collection=context.get('collection'),
143
+ metadata=predefined_metadata
157
144
  )
158
- session.add(new_document)
159
- session.flush() # Flush to get the new_document.id without committing.
160
-
161
- # Split into chunks and prepare for vector store.
162
- chunks = self.splitter.split_text(document_content)
163
- vs_docs = [VSDoc(company_id=company.id, document_id=new_document.id, text=text) for text in chunks]
164
145
 
165
- # Add document chunks to the vector store.
166
- self.vector_store.add_document(company.short_name, vs_docs)
167
-
168
- session.commit()
169
146
  return new_document
147
+
170
148
  except Exception as e:
171
- self.doc_repo.session.rollback()
149
+ # We log here but re-raise to let FileProcessor handle the error counting/continue logic
172
150
  logging.exception(f"Error processing file '{filename}': {e}")
173
151
  raise IAToolkitException(IAToolkitException.ErrorType.LOAD_DOCUMENT_ERROR,
174
152
  f"Error while processing file: {filename}")
@@ -9,6 +9,7 @@ from iatoolkit.services.i18n_service import I18nService
9
9
  from iatoolkit.repositories.models import User, Company, ApiKey
10
10
  from flask_bcrypt import check_password_hash
11
11
  from iatoolkit.common.session_manager import SessionManager
12
+ from iatoolkit.services.dispatcher_service import Dispatcher
12
13
  from iatoolkit.services.language_service import LanguageService
13
14
  from iatoolkit.services.user_session_context_service import UserSessionContextService
14
15
  from iatoolkit.services.configuration_service import ConfigurationService
@@ -19,7 +20,7 @@ import re
19
20
  import secrets
20
21
  import string
21
22
  import logging
22
- from iatoolkit.services.dispatcher_service import Dispatcher
23
+ from typing import List, Dict
23
24
 
24
25
 
25
26
  class ProfileService:
@@ -41,7 +42,6 @@ class ProfileService:
41
42
  self.mail_service = mail_service
42
43
  self.bcrypt = Bcrypt()
43
44
 
44
-
45
45
  def login(self, company_short_name: str, email: str, password: str) -> dict:
46
46
  try:
47
47
  # check if user exists
@@ -65,6 +65,8 @@ class ProfileService:
65
65
  return {'success': False,
66
66
  "message": self.i18n_service.t('errors.services.account_not_verified')}
67
67
 
68
+ user_role = self.profile_repo.get_user_role_in_company(company.id, user.id)
69
+
68
70
  # 1. Build the local user profile dictionary here.
69
71
  # the user_profile variables are used on the LLM templates also (see in query_main.prompt)
70
72
  user_identifier = user.email
@@ -73,6 +75,7 @@ class ProfileService:
73
75
  "user_fullname": f'{user.first_name} {user.last_name}',
74
76
  "user_is_local": True,
75
77
  "user_id": user.id,
78
+ "user_role": user_role,
76
79
  "extras": {}
77
80
  }
78
81
 
@@ -320,19 +323,44 @@ class ProfileService:
320
323
  def get_company_by_short_name(self, short_name: str) -> Company:
321
324
  return self.profile_repo.get_company_by_short_name(short_name)
322
325
 
326
+ def get_company_users(self, company_short_name: str) -> List[Dict]:
327
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
328
+ if not company:
329
+ return []
330
+
331
+ # get the company users from the repo
332
+ company_users = self.profile_repo.get_company_users_with_details(company_short_name)
333
+
334
+ users_data = []
335
+ for user, role, last_access in company_users:
336
+ users_data.append({
337
+ "first_name": user.first_name,
338
+ "last_name": user.last_name,
339
+ "email": user.email,
340
+ "created": user.created_at,
341
+ "verified": user.verified,
342
+ "role": role or "user",
343
+ "last_access": last_access
344
+ })
345
+
346
+ return users_data
347
+
323
348
  def get_active_api_key_entry(self, api_key_value: str) -> ApiKey | None:
324
349
  return self.profile_repo.get_active_api_key_entry(api_key_value)
325
350
 
326
- def new_api_key(self, company_short_name: str):
351
+ def new_api_key(self, company_short_name: str, key_name: str):
327
352
  company = self.get_company_by_short_name(company_short_name)
328
353
  if not company:
329
354
  return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
330
355
 
356
+ if not key_name:
357
+ return {"error": self.i18n_service.t('errors.auth.api_key_name_required')}
358
+
331
359
  length = 40 # lenght of the api key
332
360
  alphabet = string.ascii_letters + string.digits
333
361
  key = ''.join(secrets.choice(alphabet) for i in range(length))
334
362
 
335
- api_key = ApiKey(key=key, company_id=company.id)
363
+ api_key = ApiKey(key=key, company_id=company.id, key_name=key_name)
336
364
  self.profile_repo.create_api_key(api_key)
337
365
  return {"api-key": key}
338
366
 
@@ -4,15 +4,16 @@
4
4
  # IAToolkit is open source software.
5
5
 
6
6
  from injector import inject
7
+ from iatoolkit.common.interfaces.asset_storage import AssetRepository, AssetType
7
8
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
9
  from iatoolkit.services.i18n_service import I18nService
9
10
  from iatoolkit.repositories.profile_repo import ProfileRepo
10
11
  from collections import defaultdict
11
12
  from iatoolkit.repositories.models import Prompt, PromptCategory, Company
12
- import os
13
13
  from iatoolkit.common.exceptions import IAToolkitException
14
14
  import importlib.resources
15
15
  import logging
16
+ import os
16
17
 
17
18
  # iatoolkit system prompts definitions
18
19
  _SYSTEM_PROMPTS = [
@@ -24,27 +25,37 @@ _SYSTEM_PROMPTS = [
24
25
  class PromptService:
25
26
  @inject
26
27
  def __init__(self,
28
+ asset_repo: AssetRepository,
27
29
  llm_query_repo: LLMQueryRepo,
28
30
  profile_repo: ProfileRepo,
29
31
  i18n_service: I18nService):
32
+ self.asset_repo = asset_repo
30
33
  self.llm_query_repo = llm_query_repo
31
34
  self.profile_repo = profile_repo
32
35
  self.i18n_service = i18n_service
33
36
 
34
- def sync_company_prompts(self, company_instance, prompts_config: list, categories_config: list):
37
+ def sync_company_prompts(self, company_short_name: str, prompts_config: list, categories_config: list):
35
38
  """
36
39
  Synchronizes prompt categories and prompts from YAML config to Database.
37
40
  Strategies:
38
41
  - Categories: Create or Update existing based on name.
39
42
  - Prompts: Create or Update existing based on name. Soft-delete or Delete unused.
40
43
  """
44
+ if not prompts_config:
45
+ return
46
+
47
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
48
+ if not company:
49
+ raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
50
+ f'Company {company_short_name} not found')
51
+
41
52
  try:
42
53
  # 1. Sync Categories
43
54
  category_map = {}
44
55
 
45
56
  for i, category_name in enumerate(categories_config):
46
57
  category_obj = PromptCategory(
47
- company_id=company_instance.company.id,
58
+ company_id=company.id,
48
59
  name=category_name,
49
60
  order=i + 1
50
61
  )
@@ -69,10 +80,10 @@ class PromptService:
69
80
  filename = f"{prompt_name}.prompt"
70
81
 
71
82
  new_prompt = Prompt(
72
- company_id=company_instance.company.id,
83
+ company_id=company.id,
73
84
  name=prompt_name,
74
- description=prompt_data['description'],
75
- order=prompt_data['order'],
85
+ description=prompt_data.get('description'),
86
+ order=prompt_data.get('order'),
76
87
  category_id=category_obj.id,
77
88
  active=prompt_data.get('active', True),
78
89
  is_system_prompt=False,
@@ -83,7 +94,7 @@ class PromptService:
83
94
  self.llm_query_repo.create_or_update_prompt(new_prompt)
84
95
 
85
96
  # 3. Cleanup: Delete prompts present in DB but not in Config
86
- existing_prompts = self.llm_query_repo.get_prompts(company_instance.company)
97
+ existing_prompts = self.llm_query_repo.get_prompts(company)
87
98
  for p in existing_prompts:
88
99
  if p.name not in defined_prompt_names:
89
100
  # Using hard delete to keep consistent with previous "refresh" behavior
@@ -151,12 +162,9 @@ class PromptService:
151
162
  raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
152
163
  f'missing system prompt file: {prompt_filename}')
153
164
  else:
154
- template_dir = f'companies/{company.short_name}/prompts'
155
-
156
- relative_prompt_path = os.path.join(template_dir, prompt_filename)
157
- if not os.path.exists(relative_prompt_path):
165
+ if not self.asset_repo.exists(company.short_name, AssetType.PROMPT, prompt_filename):
158
166
  raise IAToolkitException(IAToolkitException.ErrorType.INVALID_NAME,
159
- f'missing prompt file: {relative_prompt_path}')
167
+ f'missing prompt file: {prompt_filename} in prompts/')
160
168
 
161
169
  if custom_fields:
162
170
  for f in custom_fields:
@@ -188,33 +196,28 @@ class PromptService:
188
196
 
189
197
  def get_prompt_content(self, company: Company, prompt_name: str):
190
198
  try:
191
- user_prompt_content = []
192
- execution_dir = os.getcwd()
193
-
194
199
  # get the user prompt
195
200
  user_prompt = self.llm_query_repo.get_prompt_by_name(company, prompt_name)
196
201
  if not user_prompt:
197
202
  raise IAToolkitException(IAToolkitException.ErrorType.DOCUMENT_NOT_FOUND,
198
203
  f"prompt not found '{prompt_name}' for company '{company.short_name}'")
199
204
 
200
- prompt_file = f'companies/{company.short_name}/prompts/{user_prompt.filename}'
201
- absolute_filepath = os.path.join(execution_dir, prompt_file)
202
- if not os.path.exists(absolute_filepath):
203
- raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
204
- f"prompt file '{prompt_name}' does not exist: {absolute_filepath}")
205
-
206
205
  try:
207
- with open(absolute_filepath, 'r', encoding='utf-8') as f:
208
- user_prompt_content = f.read()
206
+ user_prompt_content = self.asset_repo.read_text(
207
+ company.short_name,
208
+ AssetType.PROMPT,
209
+ user_prompt.filename
210
+ )
211
+ except FileNotFoundError:
212
+ raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
213
+ f"prompt file '{user_prompt.filename}' does not exist for company '{company.short_name}'")
209
214
  except Exception as e:
210
215
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
211
- f"error while reading prompt: '{prompt_name}' in this pathname {absolute_filepath}: {e}")
216
+ f"error while reading prompt: '{prompt_name}': {e}")
212
217
 
213
218
  return user_prompt_content
214
219
 
215
220
  except IAToolkitException:
216
- # Vuelve a lanzar las IAToolkitException que ya hemos manejado
217
- # para que no sean capturadas por el siguiente bloque.
218
221
  raise
219
222
  except Exception as e:
220
223
  logging.exception(
@@ -260,7 +263,7 @@ class PromptService:
260
263
  # get all the prompts
261
264
  all_prompts = self.llm_query_repo.get_prompts(company)
262
265
 
263
- # Agrupar prompts por categoría
266
+ # group by category
264
267
  prompts_by_category = defaultdict(list)
265
268
  for prompt in all_prompts:
266
269
  if prompt.active:
@@ -268,14 +271,13 @@ class PromptService:
268
271
  cat_key = (prompt.category.order, prompt.category.name)
269
272
  prompts_by_category[cat_key].append(prompt)
270
273
 
271
- # Ordenar los prompts dentro de cada categoría
274
+ # sort each category by order
272
275
  for cat_key in prompts_by_category:
273
276
  prompts_by_category[cat_key].sort(key=lambda p: p.order)
274
277
 
275
- # Crear la estructura de respuesta final, ordenada por la categoría
276
278
  categorized_prompts = []
277
279
 
278
- # Ordenar las categorías por su 'order'
280
+ # sort categories by order
279
281
  sorted_categories = sorted(prompts_by_category.items(), key=lambda item: item[0][0])
280
282
 
281
283
  for (cat_order, cat_name), prompts in sorted_categories: