iatoolkit 0.91.1__py3-none-any.whl → 1.4.2__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 +38 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +42 -5
- iatoolkit/common/util.py +11 -12
- iatoolkit/company_registry.py +5 -0
- iatoolkit/core.py +51 -20
- 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 +124 -2
- iatoolkit/locales/es.yaml +122 -0
- iatoolkit/repositories/database_manager.py +44 -19
- 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 +88 -39
- iatoolkit/services/configuration_service.py +157 -68
- 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 +412 -0
- iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +38 -29
- iatoolkit/services/load_documents_service.py +18 -47
- 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 +105 -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 +44 -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/load_company_configuration_api_view.py +49 -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.4.2.dist-info}/METADATA +4 -4
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/RECORD +64 -56
- 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.4.2.dist-info}/WHEEL +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/top_level.txt +0 -0
|
@@ -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', {})
|
|
@@ -89,6 +75,7 @@ class LoadDocumentsService:
|
|
|
89
75
|
# Prepare the context for the callback function.
|
|
90
76
|
context = {
|
|
91
77
|
'company': company,
|
|
78
|
+
'collection': source_config.get('metadata', {}).get('collection'),
|
|
92
79
|
'metadata': source_config.get('metadata', {})
|
|
93
80
|
}
|
|
94
81
|
|
|
@@ -130,45 +117,29 @@ class LoadDocumentsService:
|
|
|
130
117
|
|
|
131
118
|
def _file_processing_callback(self, company: Company, filename: str, content: bytes, context: dict = None):
|
|
132
119
|
"""
|
|
133
|
-
Callback method to process a single file.
|
|
134
|
-
|
|
120
|
+
Callback method to process a single file.
|
|
121
|
+
Delegates the actual ingestion (storage, vectorization) to KnowledgeBaseService.
|
|
135
122
|
"""
|
|
136
123
|
if not company:
|
|
137
124
|
raise IAToolkitException(IAToolkitException.ErrorType.MISSING_PARAMETER, "Missing company object in callback.")
|
|
138
125
|
|
|
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
126
|
try:
|
|
144
|
-
document_content = self.doc_service.file_to_txt(filename, content)
|
|
145
|
-
|
|
146
127
|
# Get predefined metadata from the context passed by the processor.
|
|
147
128
|
predefined_metadata = context.get('metadata', {}) if context else {}
|
|
148
129
|
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
company_id=company.id,
|
|
130
|
+
# Delegate heavy lifting to KnowledgeBaseService
|
|
131
|
+
new_document = self.knowledge_base_service.ingest_document_sync(
|
|
132
|
+
company=company,
|
|
153
133
|
filename=filename,
|
|
154
|
-
content=
|
|
155
|
-
|
|
156
|
-
|
|
134
|
+
content=content,
|
|
135
|
+
collection=predefined_metadata.get('collection'),
|
|
136
|
+
metadata=predefined_metadata
|
|
157
137
|
)
|
|
158
|
-
session.add(new_document)
|
|
159
|
-
session.flush() # Flush to get the new_document.id without committing.
|
|
160
138
|
|
|
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
|
-
|
|
165
|
-
# Add document chunks to the vector store.
|
|
166
|
-
self.vector_store.add_document(company.short_name, vs_docs)
|
|
167
|
-
|
|
168
|
-
session.commit()
|
|
169
139
|
return new_document
|
|
140
|
+
|
|
170
141
|
except Exception as e:
|
|
171
|
-
|
|
142
|
+
# We log here but re-raise to let FileProcessor handle the error counting/continue logic
|
|
172
143
|
logging.exception(f"Error processing file '{filename}': {e}")
|
|
173
144
|
raise IAToolkitException(IAToolkitException.ErrorType.LOAD_DOCUMENT_ERROR,
|
|
174
145
|
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:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from iatoolkit.
|
|
6
|
+
from iatoolkit.services.llm_client_service import llmClient
|
|
7
7
|
from iatoolkit.services.profile_service import ProfileService
|
|
8
8
|
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
9
9
|
from iatoolkit.services.tool_service import ToolService
|
|
@@ -11,11 +11,11 @@ from iatoolkit.services.document_service import DocumentService
|
|
|
11
11
|
from iatoolkit.services.company_context_service import CompanyContextService
|
|
12
12
|
from iatoolkit.services.i18n_service import I18nService
|
|
13
13
|
from iatoolkit.services.configuration_service import ConfigurationService
|
|
14
|
-
from iatoolkit.repositories.models import Task
|
|
15
14
|
from iatoolkit.services.dispatcher_service import Dispatcher
|
|
16
15
|
from iatoolkit.services.prompt_service import PromptService
|
|
17
16
|
from iatoolkit.services.user_session_context_service import UserSessionContextService
|
|
18
17
|
from iatoolkit.services.history_manager_service import HistoryManagerService
|
|
18
|
+
from iatoolkit.common.model_registry import ModelRegistry
|
|
19
19
|
from iatoolkit.common.util import Utility
|
|
20
20
|
from injector import inject
|
|
21
21
|
import base64
|
|
@@ -33,6 +33,7 @@ class HistoryHandle:
|
|
|
33
33
|
company_short_name: str
|
|
34
34
|
user_identifier: str
|
|
35
35
|
type: str
|
|
36
|
+
model: str | None = None
|
|
36
37
|
request_params: dict = None
|
|
37
38
|
|
|
38
39
|
|
|
@@ -52,6 +53,7 @@ class QueryService:
|
|
|
52
53
|
configuration_service: ConfigurationService,
|
|
53
54
|
history_manager: HistoryManagerService,
|
|
54
55
|
util: Utility,
|
|
56
|
+
model_registry: ModelRegistry
|
|
55
57
|
):
|
|
56
58
|
self.profile_service = profile_service
|
|
57
59
|
self.company_context_service = company_context_service
|
|
@@ -66,6 +68,7 @@ class QueryService:
|
|
|
66
68
|
self.configuration_service = configuration_service
|
|
67
69
|
self.llm_client = llm_client
|
|
68
70
|
self.history_manager = history_manager
|
|
71
|
+
self.model_registry = model_registry
|
|
69
72
|
|
|
70
73
|
|
|
71
74
|
def _resolve_model(self, company_short_name: str, model: Optional[str]) -> str:
|
|
@@ -78,13 +81,16 @@ class QueryService:
|
|
|
78
81
|
return effective_model
|
|
79
82
|
|
|
80
83
|
def _get_history_type(self, model: str) -> str:
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
history_type_str = self.model_registry.get_history_type(model)
|
|
85
|
+
if history_type_str == "server_side":
|
|
86
|
+
return HistoryManagerService.TYPE_SERVER_SIDE
|
|
87
|
+
else:
|
|
88
|
+
return HistoryManagerService.TYPE_CLIENT_SIDE
|
|
83
89
|
|
|
84
90
|
|
|
85
91
|
def _build_user_facing_prompt(self, company, user_identifier: str,
|
|
86
92
|
client_data: dict, files: list,
|
|
87
|
-
prompt_name: Optional[str], question: str)
|
|
93
|
+
prompt_name: Optional[str], question: str):
|
|
88
94
|
# get the user profile data from the session context
|
|
89
95
|
user_profile = self.profile_service.get_profile_by_identifier(company.short_name, user_identifier)
|
|
90
96
|
|
|
@@ -124,9 +130,12 @@ class QueryService:
|
|
|
124
130
|
|
|
125
131
|
return user_turn_prompt, effective_question
|
|
126
132
|
|
|
127
|
-
def _ensure_valid_history(self, company,
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
def _ensure_valid_history(self, company,
|
|
134
|
+
user_identifier: str,
|
|
135
|
+
effective_model: str,
|
|
136
|
+
user_turn_prompt: str,
|
|
137
|
+
ignore_history: bool
|
|
138
|
+
) -> tuple[Optional[HistoryHandle], Optional[dict]]:
|
|
130
139
|
"""
|
|
131
140
|
Manages the history strategy and rebuilds context if necessary.
|
|
132
141
|
Returns: (HistoryHandle, error_response)
|
|
@@ -137,7 +146,8 @@ class QueryService:
|
|
|
137
146
|
handle = HistoryHandle(
|
|
138
147
|
company_short_name=company.short_name,
|
|
139
148
|
user_identifier=user_identifier,
|
|
140
|
-
type=history_type
|
|
149
|
+
type=history_type,
|
|
150
|
+
model=effective_model
|
|
141
151
|
)
|
|
142
152
|
|
|
143
153
|
# pass the handle to populate request_params
|
|
@@ -197,22 +207,35 @@ class QueryService:
|
|
|
197
207
|
def init_context(self, company_short_name: str,
|
|
198
208
|
user_identifier: str,
|
|
199
209
|
model: str = None) -> dict:
|
|
210
|
+
"""
|
|
211
|
+
Forces a context rebuild for a given user and (optionally) model.
|
|
212
|
+
|
|
213
|
+
- Clears LLM-related context for the resolved model.
|
|
214
|
+
- Regenerates the static company/user context.
|
|
215
|
+
- Sends the context to the LLM for that model.
|
|
216
|
+
"""
|
|
200
217
|
|
|
201
|
-
# 1.
|
|
202
|
-
self.
|
|
203
|
-
logging.info(f"Context for {company_short_name}/{user_identifier} has been cleared.")
|
|
218
|
+
# 1. Resolve the effective model for this user/company
|
|
219
|
+
effective_model = self._resolve_model(company_short_name, model)
|
|
204
220
|
|
|
205
|
-
# 2.
|
|
221
|
+
# 2. Clear only the LLM-related context for this model
|
|
222
|
+
self.session_context.clear_all_context(company_short_name, user_identifier,model=effective_model)
|
|
223
|
+
logging.info(
|
|
224
|
+
f"Context for {company_short_name}/{user_identifier} "
|
|
225
|
+
f"(model={effective_model}) has been cleared."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# 3. Static LLM context is now clean, we can prepare it again (model-agnostic)
|
|
206
229
|
self.prepare_context(
|
|
207
230
|
company_short_name=company_short_name,
|
|
208
231
|
user_identifier=user_identifier
|
|
209
232
|
)
|
|
210
233
|
|
|
211
|
-
#
|
|
234
|
+
# 4. Communicate the new context to the specific LLM model
|
|
212
235
|
response = self.set_context_for_llm(
|
|
213
236
|
company_short_name=company_short_name,
|
|
214
237
|
user_identifier=user_identifier,
|
|
215
|
-
model=
|
|
238
|
+
model=effective_model
|
|
216
239
|
)
|
|
217
240
|
|
|
218
241
|
return response
|
|
@@ -257,8 +280,10 @@ class QueryService:
|
|
|
257
280
|
company_short_name: str,
|
|
258
281
|
user_identifier: str,
|
|
259
282
|
model: str = ''):
|
|
260
|
-
|
|
261
|
-
|
|
283
|
+
"""
|
|
284
|
+
Takes a pre-built static context and sends it to the LLM for the given model.
|
|
285
|
+
Also initializes the model-specific history through HistoryManagerService.
|
|
286
|
+
"""
|
|
262
287
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
263
288
|
if not company:
|
|
264
289
|
logging.error(f"Company not found: {company_short_name} in set_context_for_llm")
|
|
@@ -267,8 +292,8 @@ class QueryService:
|
|
|
267
292
|
# --- Model Resolution ---
|
|
268
293
|
effective_model = self._resolve_model(company_short_name, model)
|
|
269
294
|
|
|
270
|
-
#
|
|
271
|
-
lock_key = f"lock:context:{company_short_name}/{user_identifier}"
|
|
295
|
+
# Lock per (company, user, model) to avoid concurrent rebuilds for the same model
|
|
296
|
+
lock_key = f"lock:context:{company_short_name}/{user_identifier}/{effective_model}"
|
|
272
297
|
if not self.session_context.acquire_lock(lock_key, expire_seconds=60):
|
|
273
298
|
logging.warning(
|
|
274
299
|
f"try to rebuild context for user {user_identifier} while is still in process, ignored.")
|
|
@@ -310,13 +335,13 @@ class QueryService:
|
|
|
310
335
|
def llm_query(self,
|
|
311
336
|
company_short_name: str,
|
|
312
337
|
user_identifier: str,
|
|
313
|
-
|
|
338
|
+
model: Optional[str] = None,
|
|
314
339
|
prompt_name: str = None,
|
|
315
340
|
question: str = '',
|
|
316
341
|
client_data: dict = {},
|
|
317
342
|
ignore_history: bool = False,
|
|
318
|
-
files: list = []
|
|
319
|
-
|
|
343
|
+
files: list = []
|
|
344
|
+
) -> dict:
|
|
320
345
|
try:
|
|
321
346
|
company = self.profile_repo.get_company_by_short_name(short_name=company_short_name)
|
|
322
347
|
if not company:
|
|
@@ -378,10 +403,10 @@ class QueryService:
|
|
|
378
403
|
if not response.get('valid_response'):
|
|
379
404
|
response['error'] = True
|
|
380
405
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
406
|
+
# save history using the manager passing the handle
|
|
407
|
+
self.history_manager.update_history(
|
|
408
|
+
history_handle, user_turn_prompt, response
|
|
409
|
+
)
|
|
385
410
|
|
|
386
411
|
return response
|
|
387
412
|
except Exception as e:
|