iatoolkit 0.4.2__py3-none-any.whl → 0.66.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 +13 -35
- iatoolkit/base_company.py +74 -8
- iatoolkit/cli_commands.py +15 -23
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +46 -0
- iatoolkit/common/routes.py +141 -0
- iatoolkit/common/session_manager.py +24 -0
- iatoolkit/common/util.py +348 -0
- iatoolkit/company_registry.py +7 -8
- iatoolkit/iatoolkit.py +169 -96
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/call_service.py +140 -0
- iatoolkit/infra/connectors/__init__.py +5 -0
- iatoolkit/infra/connectors/file_connector.py +17 -0
- iatoolkit/infra/connectors/file_connector_factory.py +57 -0
- iatoolkit/infra/connectors/google_cloud_storage_connector.py +53 -0
- iatoolkit/infra/connectors/google_drive_connector.py +68 -0
- iatoolkit/infra/connectors/local_file_connector.py +46 -0
- iatoolkit/infra/connectors/s3_connector.py +33 -0
- iatoolkit/infra/gemini_adapter.py +356 -0
- iatoolkit/infra/google_chat_app.py +57 -0
- iatoolkit/infra/llm_client.py +429 -0
- iatoolkit/infra/llm_proxy.py +139 -0
- iatoolkit/infra/llm_response.py +40 -0
- iatoolkit/infra/mail_app.py +145 -0
- iatoolkit/infra/openai_adapter.py +90 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +144 -0
- iatoolkit/locales/es.yaml +140 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +110 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/llm_query_repo.py +91 -0
- iatoolkit/repositories/models.py +336 -0
- iatoolkit/repositories/profile_repo.py +123 -0
- iatoolkit/repositories/tasks_repo.py +52 -0
- iatoolkit/repositories/vs_repo.py +139 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +6 -6
- iatoolkit/services/branding_service.py +149 -0
- {services → iatoolkit/services}/dispatcher_service.py +39 -99
- {services → iatoolkit/services}/document_service.py +5 -5
- {services → iatoolkit/services}/excel_service.py +27 -21
- {services → iatoolkit/services}/file_processor_service.py +5 -5
- iatoolkit/services/help_content_service.py +30 -0
- {services → iatoolkit/services}/history_service.py +8 -16
- iatoolkit/services/i18n_service.py +104 -0
- {services → iatoolkit/services}/jwt_service.py +18 -27
- iatoolkit/services/language_service.py +77 -0
- {services → iatoolkit/services}/load_documents_service.py +19 -14
- {services → iatoolkit/services}/mail_service.py +5 -5
- iatoolkit/services/onboarding_service.py +43 -0
- {services → iatoolkit/services}/profile_service.py +155 -89
- {services → iatoolkit/services}/prompt_manager_service.py +26 -11
- {services → iatoolkit/services}/query_service.py +142 -104
- {services → iatoolkit/services}/search_service.py +21 -5
- {services → iatoolkit/services}/sql_service.py +24 -6
- {services → iatoolkit/services}/tasks_service.py +10 -10
- iatoolkit/services/user_feedback_service.py +103 -0
- iatoolkit/services/user_session_context_service.py +143 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/js/chat_feedback_button.js +80 -0
- iatoolkit/static/js/chat_filepond.js +85 -0
- iatoolkit/static/js/chat_help_content.js +124 -0
- iatoolkit/static/js/chat_history_button.js +112 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +364 -0
- iatoolkit/static/js/chat_onboarding_button.js +97 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +35 -0
- iatoolkit/static/styles/chat_iatoolkit.css +592 -0
- iatoolkit/static/styles/chat_modal.css +169 -0
- iatoolkit/static/styles/chat_public.css +107 -0
- iatoolkit/static/styles/landing_page.css +182 -0
- iatoolkit/static/styles/llm_output.css +115 -0
- iatoolkit/static/styles/onboarding.css +169 -0
- iatoolkit/system_prompts/query_main.prompt +5 -15
- iatoolkit/templates/_company_header.html +20 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/about.html +13 -0
- iatoolkit/templates/base.html +65 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +287 -0
- iatoolkit/templates/chat_modals.html +181 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +50 -0
- iatoolkit/templates/index.html +145 -0
- iatoolkit/templates/login_simulation.html +34 -0
- iatoolkit/templates/onboarding_shell.html +104 -0
- iatoolkit/templates/signup.html +76 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +92 -0
- iatoolkit/views/change_password_view.py +117 -0
- iatoolkit/views/external_login_view.py +73 -0
- iatoolkit/views/file_store_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +72 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +61 -0
- iatoolkit/views/index_view.py +14 -0
- iatoolkit/views/init_context_api_view.py +73 -0
- iatoolkit/views/llmquery_api_view.py +57 -0
- iatoolkit/views/login_simulation_view.py +81 -0
- iatoolkit/views/login_view.py +153 -0
- iatoolkit/views/logout_api_view.py +49 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/signup_view.py +94 -0
- iatoolkit/views/tasks_api_view.py +72 -0
- iatoolkit/views/tasks_review_api_view.py +55 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/verify_user_view.py +62 -0
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/METADATA +2 -2
- iatoolkit-0.66.2.dist-info/RECORD +119 -0
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/top_level.txt +0 -1
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.4.2.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {iatoolkit-0.4.2.dist-info → iatoolkit-0.66.2.dist-info}/WHEEL +0 -0
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
from common.exceptions import IAToolkitException
|
|
7
|
-
from services.prompt_manager_service import PromptService
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
from repositories.models import Company, Function
|
|
11
|
-
from services.excel_service import ExcelService
|
|
12
|
-
from services.mail_service import MailService
|
|
13
|
-
from common.
|
|
14
|
-
from common.util import Utility
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
7
|
+
from iatoolkit.services.prompt_manager_service import PromptService
|
|
8
|
+
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
9
|
+
|
|
10
|
+
from iatoolkit.repositories.models import Company, Function
|
|
11
|
+
from iatoolkit.services.excel_service import ExcelService
|
|
12
|
+
from iatoolkit.services.mail_service import MailService
|
|
13
|
+
from iatoolkit.common.util import Utility
|
|
15
14
|
from injector import inject
|
|
16
|
-
from typing import Dict
|
|
17
15
|
import logging
|
|
18
16
|
import os
|
|
19
17
|
|
|
@@ -24,12 +22,10 @@ class Dispatcher:
|
|
|
24
22
|
prompt_service: PromptService,
|
|
25
23
|
llmquery_repo: LLMQueryRepo,
|
|
26
24
|
util: Utility,
|
|
27
|
-
api_service: ApiService,
|
|
28
25
|
excel_service: ExcelService,
|
|
29
26
|
mail_service: MailService):
|
|
30
27
|
self.prompt_service = prompt_service
|
|
31
28
|
self.llmquery_repo = llmquery_repo
|
|
32
|
-
self.api_service = api_service
|
|
33
29
|
self.util = util
|
|
34
30
|
self.excel_service = excel_service
|
|
35
31
|
self.mail_service = mail_service
|
|
@@ -39,16 +35,9 @@ class Dispatcher:
|
|
|
39
35
|
self._company_registry = None
|
|
40
36
|
self._company_instances = None
|
|
41
37
|
|
|
42
|
-
# load into the dispatcher the configured companies
|
|
43
|
-
self.initialize_companies()
|
|
44
|
-
|
|
45
|
-
# run the statrtup logic for all companies
|
|
46
|
-
self.start_execution()
|
|
47
|
-
|
|
48
38
|
self.tool_handlers = {
|
|
49
39
|
"iat_generate_excel": self.excel_service.excel_generator,
|
|
50
40
|
"iat_send_email": self.mail_service.send_mail,
|
|
51
|
-
"iat_api_call": self.api_service.call_api
|
|
52
41
|
}
|
|
53
42
|
|
|
54
43
|
@property
|
|
@@ -66,30 +55,18 @@ class Dispatcher:
|
|
|
66
55
|
self._company_instances = self.company_registry.get_all_company_instances()
|
|
67
56
|
return self._company_instances
|
|
68
57
|
|
|
69
|
-
def initialize_companies(self):
|
|
70
|
-
from iatoolkit import current_iatoolkit
|
|
71
|
-
"""
|
|
72
|
-
Initializes and instantiates all registered company classes.
|
|
73
|
-
This method should be called *after* the main injector is fully configured
|
|
74
|
-
and the company registry is populated.
|
|
75
|
-
"""
|
|
76
|
-
if self.company_registry.get_all_company_instances(): # Check if already instantiated
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
# ✅ NOW it is safe to get the injector and instantiate companies.
|
|
80
|
-
injector = current_iatoolkit().get_injector()
|
|
81
|
-
self.company_registry.instantiate_companies(injector)
|
|
82
|
-
|
|
83
|
-
|
|
84
58
|
def start_execution(self):
|
|
59
|
+
# initialize the system functions and prompts
|
|
60
|
+
self.setup_iatoolkit_system()
|
|
61
|
+
|
|
85
62
|
"""Runs the startup logic for all registered companies."""
|
|
86
|
-
for
|
|
87
|
-
|
|
88
|
-
|
|
63
|
+
for company in self.company_instances.values():
|
|
64
|
+
company.register_company()
|
|
65
|
+
company.start_execution()
|
|
89
66
|
|
|
90
67
|
return True
|
|
91
68
|
|
|
92
|
-
def
|
|
69
|
+
def setup_iatoolkit_system(self):
|
|
93
70
|
# create system functions
|
|
94
71
|
for function in self.system_functions:
|
|
95
72
|
self.llmquery_repo.create_or_update_function(
|
|
@@ -102,16 +79,16 @@ class Dispatcher:
|
|
|
102
79
|
)
|
|
103
80
|
)
|
|
104
81
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
82
|
+
# create the system prompts
|
|
83
|
+
i = 1
|
|
84
|
+
for prompt in self.system_prompts:
|
|
85
|
+
self.prompt_service.create_prompt(
|
|
86
|
+
prompt_name=prompt['name'],
|
|
87
|
+
description=prompt['description'],
|
|
88
|
+
order=1,
|
|
89
|
+
is_system_prompt=True,
|
|
90
|
+
)
|
|
91
|
+
i += 1
|
|
115
92
|
|
|
116
93
|
# register in the database every company class
|
|
117
94
|
for company in self.company_instances.values():
|
|
@@ -193,51 +170,21 @@ class Dispatcher:
|
|
|
193
170
|
tools.append(ai_tool)
|
|
194
171
|
return tools
|
|
195
172
|
|
|
196
|
-
def get_user_info(self, company_name: str, user_identifier: str
|
|
173
|
+
def get_user_info(self, company_name: str, user_identifier: str) -> dict:
|
|
197
174
|
if company_name not in self.company_instances:
|
|
198
175
|
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
199
176
|
f"Empresa no configurada: {company_name}")
|
|
200
177
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
raw_user_data = company_instance.get_user_info(user_identifier)
|
|
210
|
-
except Exception as e:
|
|
211
|
-
logging.exception(e)
|
|
212
|
-
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
213
|
-
f"Error en get_user_info de {company_name}: {str(e)}") from e
|
|
214
|
-
|
|
215
|
-
# always normalize the data for consistent structure
|
|
216
|
-
return self._normalize_user_data(raw_user_data, is_local_user)
|
|
217
|
-
|
|
218
|
-
def _normalize_user_data(self, raw_data: dict, is_local: bool) -> dict:
|
|
219
|
-
"""
|
|
220
|
-
Asegura que los datos del usuario siempre tengan una estructura consistente.
|
|
221
|
-
"""
|
|
222
|
-
# Valores por defecto para un perfil robusto
|
|
223
|
-
normalized_user = {
|
|
224
|
-
"id": raw_data.get("id", 0),
|
|
225
|
-
"user_email": raw_data.get("email", ""),
|
|
226
|
-
"user_fullname": raw_data.get("user_fullname", ""),
|
|
227
|
-
"super_user": raw_data.get("super_user", False),
|
|
228
|
-
"company_id": raw_data.get("company_id", 0),
|
|
229
|
-
"company_name": raw_data.get("company", ""),
|
|
230
|
-
"company_short_name": raw_data.get("company_short_name", ""),
|
|
231
|
-
"is_local": is_local,
|
|
232
|
-
"extras": raw_data.get("extras", {})
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
# get the extras from the raw data, if any
|
|
236
|
-
extras = raw_data.get("extras", {})
|
|
237
|
-
if isinstance(extras, dict):
|
|
238
|
-
normalized_user.update(extras)
|
|
178
|
+
# source 2: external company user
|
|
179
|
+
company_instance = self.company_instances[company_name]
|
|
180
|
+
try:
|
|
181
|
+
external_user_profile = company_instance.get_user_info(user_identifier)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logging.exception(e)
|
|
184
|
+
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
185
|
+
f"Error en get_user_info de {company_name}: {str(e)}") from e
|
|
239
186
|
|
|
240
|
-
return
|
|
187
|
+
return external_user_profile
|
|
241
188
|
|
|
242
189
|
def get_metadata_from_filename(self, company_name: str, filename: str) -> dict:
|
|
243
190
|
if company_name not in self.company_instances:
|
|
@@ -256,13 +203,6 @@ class Dispatcher:
|
|
|
256
203
|
"""Returns the instance for a given company name."""
|
|
257
204
|
return self.company_instances.get(company_name)
|
|
258
205
|
|
|
259
|
-
def get_registered_companies(self) -> dict:
|
|
260
|
-
"""Gets all registered companies (for debugging/admin purposes)"""
|
|
261
|
-
return {
|
|
262
|
-
"registered_classes": list(self.company_registry.get_registered_companies().keys()),
|
|
263
|
-
"instantiated": list(self.company_instances.keys()),
|
|
264
|
-
"count": len(self.company_instances)
|
|
265
|
-
}
|
|
266
206
|
|
|
267
207
|
|
|
268
208
|
# iatoolkit system prompts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
from docx import Document
|
|
7
7
|
import fitz # PyMuPDF
|
|
@@ -10,7 +10,7 @@ import io
|
|
|
10
10
|
import os
|
|
11
11
|
import pytesseract
|
|
12
12
|
from injector import inject
|
|
13
|
-
from common.exceptions import IAToolkitException
|
|
13
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
14
14
|
|
|
15
15
|
class DocumentService:
|
|
16
16
|
@inject
|
|
@@ -22,7 +22,7 @@ class DocumentService:
|
|
|
22
22
|
try:
|
|
23
23
|
if filename.lower().endswith('.docx'):
|
|
24
24
|
return self.read_docx(file_content)
|
|
25
|
-
elif filename.lower().endswith('.txt'):
|
|
25
|
+
elif filename.lower().endswith('.txt') or filename.lower().endswith('.md'):
|
|
26
26
|
if isinstance(file_content, bytes):
|
|
27
27
|
try:
|
|
28
28
|
# decode using UTF-8
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from common.util import Utility
|
|
6
|
+
from iatoolkit.common.util import Utility
|
|
7
7
|
import pandas as pd
|
|
8
8
|
from uuid import uuid4
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from common.exceptions import IAToolkitException
|
|
10
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
11
11
|
from injector import inject
|
|
12
12
|
import os
|
|
13
13
|
import logging
|
|
@@ -23,21 +23,21 @@ class ExcelService:
|
|
|
23
23
|
|
|
24
24
|
def excel_generator(self, **kwargs) -> str:
|
|
25
25
|
"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
Genera un Excel a partir de una lista de diccionarios.
|
|
27
|
+
|
|
28
|
+
Parámetros esperados en kwargs:
|
|
29
|
+
- filename: str (nombre lógico a mostrar, ej. "reporte_clientes.xlsx") [obligatorio]
|
|
30
|
+
- data: list[dict] (filas del excel) [obligatorio]
|
|
31
|
+
- sheet_name: str = "hoja 1"
|
|
32
|
+
|
|
33
|
+
Retorna:
|
|
34
|
+
{
|
|
35
|
+
"filename": "reporte.xlsx",
|
|
36
|
+
"attachment_token": "8b7f8a66-...-c1c3.xlsx",
|
|
37
|
+
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
38
|
+
"download_link": "/download/8b7f8a66-...-c1c3.xlsx"
|
|
39
|
+
}
|
|
40
|
+
"""
|
|
41
41
|
try:
|
|
42
42
|
# get the parameters
|
|
43
43
|
fname = kwargs.get('filename')
|
|
@@ -55,7 +55,13 @@ class ExcelService:
|
|
|
55
55
|
|
|
56
56
|
# 3. create temporary name
|
|
57
57
|
token = f"{uuid4()}.xlsx"
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
# 4. check that download directory is configured
|
|
60
|
+
if 'IATOOLKIT_DOWNLOAD_DIR' not in current_app.config:
|
|
61
|
+
return 'no esta configurado el directorio temporal para guardar excels'
|
|
62
|
+
|
|
63
|
+
download_dir = current_app.config['IATOOLKIT_DOWNLOAD_DIR']
|
|
64
|
+
filepath = Path(download_dir) / token
|
|
59
65
|
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
60
66
|
|
|
61
67
|
# 4. save excel file in temporary directory
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
|
-
from infra.connectors.file_connector import FileConnector
|
|
6
|
+
from iatoolkit.infra.connectors.file_connector import FileConnector
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
9
|
from typing import Optional, Callable, Dict
|
|
10
|
-
from repositories.models import Company
|
|
10
|
+
from iatoolkit.repositories.models import Company
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class FileProcessorConfig:
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
|
+
|
|
6
|
+
from iatoolkit.common.util import Utility
|
|
7
|
+
from iatoolkit.common.exceptions import IAToolkitException
|
|
8
|
+
import os
|
|
9
|
+
from injector import inject
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HelpContentService:
|
|
14
|
+
@inject
|
|
15
|
+
def __init__(self, util: Utility):
|
|
16
|
+
self.util = util
|
|
17
|
+
|
|
18
|
+
def get_content(self, company_short_name: str | None) -> dict:
|
|
19
|
+
filepath = f'companies/{company_short_name}/help_content.yaml'
|
|
20
|
+
if not os.path.exists(filepath):
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
# read the file
|
|
24
|
+
try:
|
|
25
|
+
help_content = self.util.load_schema_from_yaml(filepath)
|
|
26
|
+
return help_content
|
|
27
|
+
except Exception as e:
|
|
28
|
+
logging.exception(e)
|
|
29
|
+
raise IAToolkitException(IAToolkitException.ErrorType.CONFIG_ERROR,
|
|
30
|
+
f"Error obteniendo help de {company_short_name}: {str(e)}") from e
|
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
from injector import inject
|
|
7
|
-
from repositories.llm_query_repo import LLMQueryRepo
|
|
8
|
-
from repositories.profile_repo import ProfileRepo
|
|
9
|
-
from common.util import Utility
|
|
7
|
+
from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
|
|
8
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
class HistoryService:
|
|
13
12
|
@inject
|
|
14
13
|
def __init__(self, llm_query_repo: LLMQueryRepo,
|
|
15
|
-
profile_repo: ProfileRepo
|
|
16
|
-
util: Utility):
|
|
14
|
+
profile_repo: ProfileRepo):
|
|
17
15
|
self.llm_query_repo = llm_query_repo
|
|
18
16
|
self.profile_repo = profile_repo
|
|
19
|
-
self.util = util
|
|
20
17
|
|
|
21
18
|
def get_history(self,
|
|
22
19
|
company_short_name: str,
|
|
23
|
-
|
|
24
|
-
local_user_id: int = 0) -> dict:
|
|
20
|
+
user_identifier: str) -> dict:
|
|
25
21
|
try:
|
|
26
|
-
user_identifier, _ = self.util.resolve_user_identifier(external_user_id, local_user_id)
|
|
27
|
-
if not user_identifier:
|
|
28
|
-
return {'error': "No se pudo resolver el identificador del usuario"}
|
|
29
|
-
|
|
30
22
|
# validate company
|
|
31
23
|
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
32
24
|
if not company:
|
|
@@ -35,7 +27,7 @@ class HistoryService:
|
|
|
35
27
|
history = self.llm_query_repo.get_history(company, user_identifier)
|
|
36
28
|
|
|
37
29
|
if not history:
|
|
38
|
-
return {'
|
|
30
|
+
return {'message': 'Historial vacio actualmente', 'history': []}
|
|
39
31
|
|
|
40
32
|
history_list = [query.to_dict() for query in history]
|
|
41
33
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# iatoolkit/services/i18n_service.py
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
from injector import inject
|
|
5
|
+
from iatoolkit.common.util import Utility
|
|
6
|
+
from iatoolkit.services.language_service import LanguageService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class I18nService:
|
|
10
|
+
"""
|
|
11
|
+
Servicio centralizado para manejar la internacionalización (i18n).
|
|
12
|
+
Carga todas las traducciones desde archivos YAML en memoria al iniciar.
|
|
13
|
+
"""
|
|
14
|
+
FALLBACK_LANGUAGE = 'es'
|
|
15
|
+
|
|
16
|
+
@inject
|
|
17
|
+
def __init__(self, util: Utility, language_service: LanguageService):
|
|
18
|
+
self.util = util
|
|
19
|
+
self.language_service = language_service
|
|
20
|
+
|
|
21
|
+
self.translations = {}
|
|
22
|
+
self._load_translations()
|
|
23
|
+
|
|
24
|
+
def _load_translations(self):
|
|
25
|
+
"""
|
|
26
|
+
Carga todos los archivos .yaml del directorio 'locales' en memoria.
|
|
27
|
+
"""
|
|
28
|
+
locales_dir = os.path.join(os.path.dirname(__file__), '..', 'locales')
|
|
29
|
+
if not os.path.exists(locales_dir):
|
|
30
|
+
logging.error("El directorio 'locales' no fue encontrado.")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
for filename in os.listdir(locales_dir):
|
|
34
|
+
if filename.endswith('.yaml'):
|
|
35
|
+
lang_code = filename.split('.')[0]
|
|
36
|
+
filepath = os.path.join(locales_dir, filename)
|
|
37
|
+
try:
|
|
38
|
+
self.translations[lang_code] = self.util.load_schema_from_yaml(filepath)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logging.error(f"Fallo al cargar el archivo de traducción {filepath}: {e}")
|
|
41
|
+
|
|
42
|
+
def _get_nested_key(self, lang: str, key: str):
|
|
43
|
+
"""
|
|
44
|
+
Obtiene un valor de un diccionario anidado usando una clave con puntos.
|
|
45
|
+
"""
|
|
46
|
+
data = self.translations.get(lang, {})
|
|
47
|
+
keys = key.split('.')
|
|
48
|
+
for k in keys:
|
|
49
|
+
if isinstance(data, dict) and k in data:
|
|
50
|
+
data = data[k]
|
|
51
|
+
else:
|
|
52
|
+
return None
|
|
53
|
+
return data
|
|
54
|
+
|
|
55
|
+
def get_translation_block(self, key: str, lang: str = None) -> dict:
|
|
56
|
+
"""
|
|
57
|
+
Gets a whole dictionary block from the translations.
|
|
58
|
+
Useful for passing a set of translations to JavaScript.
|
|
59
|
+
"""
|
|
60
|
+
if lang is None:
|
|
61
|
+
lang = self.language_service.get_current_language()
|
|
62
|
+
|
|
63
|
+
# 1. Try to get the block in the requested language
|
|
64
|
+
block = self._get_nested_key(lang, key)
|
|
65
|
+
|
|
66
|
+
# 2. If not found, try the fallback language
|
|
67
|
+
if not isinstance(block, dict):
|
|
68
|
+
block = self._get_nested_key(self.FALLBACK_LANGUAGE, key)
|
|
69
|
+
|
|
70
|
+
return block if isinstance(block, dict) else {}
|
|
71
|
+
|
|
72
|
+
def t(self, key: str, lang: str = None, **kwargs) -> str:
|
|
73
|
+
"""
|
|
74
|
+
Gets the translation for a given key.
|
|
75
|
+
If 'lang' is provided, it's used. Otherwise, it's determined automatically.
|
|
76
|
+
"""
|
|
77
|
+
# If no specific language is requested, determine it from the current context.
|
|
78
|
+
if lang is None:
|
|
79
|
+
lang = self.language_service.get_current_language()
|
|
80
|
+
|
|
81
|
+
# 1. Attempt to get the translation in the requested language
|
|
82
|
+
message = self._get_nested_key(lang, key)
|
|
83
|
+
|
|
84
|
+
# 2. If not found, try the fallback language
|
|
85
|
+
if message is None and lang != self.FALLBACK_LANGUAGE:
|
|
86
|
+
logging.warning(
|
|
87
|
+
f"Translation key '{key}' not found for language '{lang}'. Attempting fallback to '{self.FALLBACK_LANGUAGE}'.")
|
|
88
|
+
message = self._get_nested_key(self.FALLBACK_LANGUAGE, key)
|
|
89
|
+
|
|
90
|
+
# 3. If still not found, return the key itself as a last resort
|
|
91
|
+
if message is None:
|
|
92
|
+
logging.error(
|
|
93
|
+
f"Translation key '{key}' not found, even in fallback '{self.FALLBACK_LANGUAGE}'.")
|
|
94
|
+
return key
|
|
95
|
+
|
|
96
|
+
# 4. If variables are provided, format the message
|
|
97
|
+
if kwargs:
|
|
98
|
+
try:
|
|
99
|
+
return message.format(**kwargs)
|
|
100
|
+
except KeyError as e:
|
|
101
|
+
logging.error(f"Error formatting key '{key}': missing variable {e} in arguments.")
|
|
102
|
+
return message
|
|
103
|
+
|
|
104
|
+
return message
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# Product: IAToolkit
|
|
3
|
+
#
|
|
4
|
+
# IAToolkit is open source software.
|
|
5
5
|
|
|
6
6
|
import jwt
|
|
7
7
|
import time
|
|
@@ -24,16 +24,18 @@ class JWTService:
|
|
|
24
24
|
raise RuntimeError(f"Configuración JWT esencial faltante: {e}")
|
|
25
25
|
|
|
26
26
|
def generate_chat_jwt(self,
|
|
27
|
-
company_id: int,
|
|
28
27
|
company_short_name: str,
|
|
29
|
-
|
|
28
|
+
user_identifier: str,
|
|
30
29
|
expires_delta_seconds: int) -> Optional[str]:
|
|
31
30
|
# generate a JWT for a chat session
|
|
32
31
|
try:
|
|
32
|
+
if not company_short_name or not user_identifier:
|
|
33
|
+
logging.error(f"Missing token ID: {company_short_name}/{user_identifier}")
|
|
34
|
+
return None
|
|
35
|
+
|
|
33
36
|
payload = {
|
|
34
|
-
'company_id': company_id,
|
|
35
37
|
'company_short_name': company_short_name,
|
|
36
|
-
'
|
|
38
|
+
'user_identifier': user_identifier,
|
|
37
39
|
'exp': time.time() + expires_delta_seconds,
|
|
38
40
|
'iat': time.time(),
|
|
39
41
|
'type': 'chat_session' # Identificador del tipo de token
|
|
@@ -41,10 +43,10 @@ class JWTService:
|
|
|
41
43
|
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
|
42
44
|
return token
|
|
43
45
|
except Exception as e:
|
|
44
|
-
logging.error(f"Error al generar JWT para
|
|
46
|
+
logging.error(f"Error al generar JWT para {company_short_name}/{user_identifier}: {e}")
|
|
45
47
|
return None
|
|
46
48
|
|
|
47
|
-
def validate_chat_jwt(self, token: str
|
|
49
|
+
def validate_chat_jwt(self, token: str) -> Optional[Dict[str, Any]]:
|
|
48
50
|
"""
|
|
49
51
|
Valida un JWT de sesión de chat.
|
|
50
52
|
Retorna el payload decodificado si es válido y coincide con la empresa, o None.
|
|
@@ -59,33 +61,22 @@ class JWTService:
|
|
|
59
61
|
logging.warning(f"Validación JWT fallida: tipo incorrecto '{payload.get('type')}'")
|
|
60
62
|
return None
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
f"Esperado: {expected_company_short_name}, Obtenido: {payload.get('company_short_name')}"
|
|
66
|
-
)
|
|
64
|
+
# user_identifier debe estar presente
|
|
65
|
+
if not payload.get('user_identifier'):
|
|
66
|
+
logging.warning(f"Validación JWT fallida: user_identifier ausente o vacío.")
|
|
67
67
|
return None
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
logging.warning(f"Validación JWT fallida: external_user_id ausente o vacío.")
|
|
72
|
-
return None
|
|
73
|
-
|
|
74
|
-
# company_id debe estar presente
|
|
75
|
-
if 'company_id' not in payload or not isinstance(payload['company_id'], int):
|
|
76
|
-
logging.warning(f"Validación JWT fallida: company_id ausente o tipo incorrecto.")
|
|
69
|
+
if not payload.get('company_short_name'):
|
|
70
|
+
logging.warning(f"Validación JWT fallida: company_short_name ausente.")
|
|
77
71
|
return None
|
|
78
72
|
|
|
79
73
|
logging.debug(
|
|
80
74
|
f"JWT validado exitosamente para company: {payload.get('company_short_name')}, user: {payload.get('external_user_id')}")
|
|
81
75
|
return payload
|
|
82
76
|
|
|
83
|
-
except jwt.ExpiredSignatureError:
|
|
84
|
-
logging.info(f"Validación JWT fallida: token expirado para {expected_company_short_name}")
|
|
85
|
-
return None
|
|
86
77
|
except jwt.InvalidTokenError as e:
|
|
87
|
-
logging.warning(f"Validación JWT fallida: token inválido
|
|
78
|
+
logging.warning(f"Validación JWT fallida: token inválido . Error: {e}")
|
|
88
79
|
return None
|
|
89
80
|
except Exception as e:
|
|
90
|
-
logging.error(f"Error inesperado durante validación de JWT
|
|
81
|
+
logging.error(f"Error inesperado durante validación de JWT : {e}")
|
|
91
82
|
return None
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# iatoolkit/services/language_service.py
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from injector import inject
|
|
5
|
+
from flask import g, request
|
|
6
|
+
from iatoolkit.repositories.profile_repo import ProfileRepo
|
|
7
|
+
from iatoolkit.common.session_manager import SessionManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LanguageService:
|
|
11
|
+
"""
|
|
12
|
+
Determines the correct language for the current request
|
|
13
|
+
based on a defined priority order (session, URL, etc.)
|
|
14
|
+
and caches it in the Flask 'g' object for the request's lifecycle.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@inject
|
|
18
|
+
def __init__(self, profile_repo: ProfileRepo):
|
|
19
|
+
self.profile_repo = profile_repo
|
|
20
|
+
|
|
21
|
+
def _get_company_short_name(self) -> str | None:
|
|
22
|
+
"""
|
|
23
|
+
Gets the company_short_name from the current request context.
|
|
24
|
+
This handles different scenarios like web sessions, public URLs, and API calls.
|
|
25
|
+
|
|
26
|
+
Priority Order:
|
|
27
|
+
1. Flask Session (for logged-in web users).
|
|
28
|
+
2. URL rule variable (for public pages and API endpoints).
|
|
29
|
+
"""
|
|
30
|
+
# 1. Check session for logged-in users
|
|
31
|
+
company_short_name = SessionManager.get('company_short_name')
|
|
32
|
+
if company_short_name:
|
|
33
|
+
return company_short_name
|
|
34
|
+
|
|
35
|
+
# 2. Check URL arguments (e.g., /<company_short_name>/login)
|
|
36
|
+
# This covers public pages and most API calls.
|
|
37
|
+
if request.view_args and 'company_short_name' in request.view_args:
|
|
38
|
+
return request.view_args['company_short_name']
|
|
39
|
+
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def get_current_language(self) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Determines and caches the language for the current request using a priority order:
|
|
45
|
+
1. User's preference (from their profile).
|
|
46
|
+
2. Company's default language.
|
|
47
|
+
3. System-wide fallback language ('es').
|
|
48
|
+
"""
|
|
49
|
+
if 'lang' in g:
|
|
50
|
+
return g.lang
|
|
51
|
+
|
|
52
|
+
from iatoolkit.services.i18n_service import I18nService
|
|
53
|
+
lang = I18nService.FALLBACK_LANGUAGE
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
company_short_name = self._get_company_short_name()
|
|
57
|
+
if company_short_name:
|
|
58
|
+
# Prioridad 1: Preferencia del Usuario
|
|
59
|
+
user_identifier = SessionManager.get('user_identifier')
|
|
60
|
+
if user_identifier:
|
|
61
|
+
# Usamos el repositorio para obtener el objeto User
|
|
62
|
+
user = self.profile_repo.get_user_by_email(
|
|
63
|
+
user_identifier) # Asumiendo que el email es el identificador
|
|
64
|
+
if user and user.preferred_language:
|
|
65
|
+
g.lang = user.preferred_language
|
|
66
|
+
return g.lang
|
|
67
|
+
|
|
68
|
+
# Prioridad 2: Idioma por defecto de la Compañía (si no se encontró preferencia de usuario)
|
|
69
|
+
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
70
|
+
if company and company.default_language:
|
|
71
|
+
lang = company.default_language
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logging.debug(f"Could not determine language, falling back to default. Reason: {e}")
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
g.lang = lang
|
|
77
|
+
return lang
|