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.
- iatoolkit/__init__.py +6 -4
- iatoolkit/base_company.py +0 -16
- iatoolkit/cli_commands.py +3 -14
- iatoolkit/common/exceptions.py +1 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +43 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +47 -5
- iatoolkit/common/util.py +32 -13
- iatoolkit/company_registry.py +5 -0
- iatoolkit/core.py +51 -20
- iatoolkit/infra/connectors/file_connector_factory.py +1 -0
- iatoolkit/infra/connectors/s3_connector.py +4 -2
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
- iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
- iatoolkit/infra/llm_proxy.py +235 -134
- iatoolkit/infra/llm_response.py +5 -0
- iatoolkit/locales/en.yaml +158 -2
- iatoolkit/locales/es.yaml +158 -0
- iatoolkit/repositories/database_manager.py +52 -47
- iatoolkit/repositories/document_repo.py +7 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +2 -0
- iatoolkit/repositories/models.py +72 -79
- iatoolkit/repositories/profile_repo.py +59 -3
- iatoolkit/repositories/vs_repo.py +22 -24
- iatoolkit/services/company_context_service.py +126 -53
- iatoolkit/services/configuration_service.py +299 -73
- iatoolkit/services/dispatcher_service.py +21 -3
- iatoolkit/services/file_processor_service.py +0 -5
- iatoolkit/services/history_manager_service.py +43 -24
- iatoolkit/services/knowledge_base_service.py +425 -0
- iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +38 -29
- iatoolkit/services/load_documents_service.py +26 -48
- iatoolkit/services/profile_service.py +32 -4
- iatoolkit/services/prompt_service.py +32 -30
- iatoolkit/services/query_service.py +51 -26
- iatoolkit/services/sql_service.py +122 -74
- iatoolkit/services/tool_service.py +26 -11
- iatoolkit/services/user_session_context_service.py +115 -63
- iatoolkit/static/js/chat_main.js +44 -4
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +1 -1
- iatoolkit/static/js/chat_reload_button.js +4 -1
- iatoolkit/static/styles/chat_iatoolkit.css +58 -2
- iatoolkit/static/styles/llm_output.css +34 -1
- iatoolkit/system_prompts/query_main.prompt +26 -2
- iatoolkit/templates/base.html +13 -0
- iatoolkit/templates/chat.html +45 -2
- iatoolkit/templates/onboarding_shell.html +0 -1
- iatoolkit/views/base_login_view.py +7 -2
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/configuration_api_view.py +163 -0
- iatoolkit/views/load_document_api_view.py +14 -10
- iatoolkit/views/login_view.py +8 -3
- iatoolkit/views/rag_api_view.py +216 -0
- iatoolkit/views/users_api_view.py +33 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/METADATA +4 -4
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/RECORD +66 -58
- iatoolkit/repositories/tasks_repo.py +0 -52
- iatoolkit/services/search_service.py +0 -55
- iatoolkit/services/tasks_service.py +0 -188
- iatoolkit/views/tasks_api_view.py +0 -72
- iatoolkit/views/tasks_review_api_view.py +0 -55
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/WHEEL +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
- {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
|
-
|
|
34
|
+
llm_proxy: LLMProxy,
|
|
35
|
+
model_registry: ModelRegistry,
|
|
34
36
|
util: Utility
|
|
35
37
|
):
|
|
36
38
|
self.llmquery_repo = llmquery_repo
|
|
37
|
-
self.
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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=
|
|
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
|
-
|
|
134
|
-
|
|
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}]
|
|
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=
|
|
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=
|
|
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.
|
|
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
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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.
|
|
134
|
-
|
|
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
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
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=
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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=
|
|
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=
|
|
83
|
+
company_id=company.id,
|
|
73
84
|
name=prompt_name,
|
|
74
|
-
description=prompt_data
|
|
75
|
-
order=prompt_data
|
|
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(
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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:
|