iatoolkit 0.3.9__py3-none-any.whl → 0.107.4__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.
Potentially problematic release.
This version of iatoolkit might be problematic. Click here for more details.
- iatoolkit/__init__.py +27 -35
- iatoolkit/base_company.py +3 -35
- iatoolkit/cli_commands.py +18 -47
- iatoolkit/common/__init__.py +0 -0
- iatoolkit/common/exceptions.py +48 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +39 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +138 -0
- iatoolkit/common/session_manager.py +26 -0
- iatoolkit/common/util.py +353 -0
- iatoolkit/company_registry.py +66 -29
- iatoolkit/core.py +514 -0
- iatoolkit/infra/__init__.py +5 -0
- iatoolkit/infra/brevo_mail_app.py +123 -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/google_chat_app.py +57 -0
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/llm_providers/gemini_adapter.py +350 -0
- iatoolkit/infra/llm_providers/openai_adapter.py +124 -0
- iatoolkit/infra/llm_proxy.py +268 -0
- iatoolkit/infra/llm_response.py +45 -0
- iatoolkit/infra/redis_session_manager.py +122 -0
- iatoolkit/locales/en.yaml +222 -0
- iatoolkit/locales/es.yaml +225 -0
- iatoolkit/repositories/__init__.py +5 -0
- iatoolkit/repositories/database_manager.py +187 -0
- iatoolkit/repositories/document_repo.py +33 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +105 -0
- iatoolkit/repositories/models.py +279 -0
- iatoolkit/repositories/profile_repo.py +171 -0
- iatoolkit/repositories/vs_repo.py +150 -0
- iatoolkit/services/__init__.py +5 -0
- iatoolkit/services/auth_service.py +193 -0
- {services → iatoolkit/services}/benchmark_service.py +7 -7
- iatoolkit/services/branding_service.py +153 -0
- iatoolkit/services/company_context_service.py +214 -0
- iatoolkit/services/configuration_service.py +375 -0
- iatoolkit/services/dispatcher_service.py +134 -0
- {services → iatoolkit/services}/document_service.py +20 -8
- iatoolkit/services/embedding_service.py +148 -0
- iatoolkit/services/excel_service.py +156 -0
- {services → iatoolkit/services}/file_processor_service.py +36 -21
- iatoolkit/services/history_manager_service.py +208 -0
- iatoolkit/services/i18n_service.py +104 -0
- iatoolkit/services/jwt_service.py +80 -0
- iatoolkit/services/language_service.py +89 -0
- iatoolkit/services/license_service.py +82 -0
- iatoolkit/services/llm_client_service.py +438 -0
- iatoolkit/services/load_documents_service.py +174 -0
- iatoolkit/services/mail_service.py +213 -0
- {services → iatoolkit/services}/profile_service.py +200 -101
- iatoolkit/services/prompt_service.py +303 -0
- iatoolkit/services/query_service.py +467 -0
- iatoolkit/services/search_service.py +55 -0
- iatoolkit/services/sql_service.py +169 -0
- iatoolkit/services/tool_service.py +246 -0
- iatoolkit/services/user_feedback_service.py +117 -0
- iatoolkit/services/user_session_context_service.py +213 -0
- iatoolkit/static/images/fernando.jpeg +0 -0
- iatoolkit/static/images/iatoolkit_core.png +0 -0
- iatoolkit/static/images/iatoolkit_logo.png +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 +110 -0
- iatoolkit/static/js/chat_logout_button.js +36 -0
- iatoolkit/static/js/chat_main.js +401 -0
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +103 -0
- iatoolkit/static/js/chat_prompt_manager.js +94 -0
- iatoolkit/static/js/chat_reload_button.js +38 -0
- iatoolkit/static/styles/chat_iatoolkit.css +559 -0
- iatoolkit/static/styles/chat_modal.css +133 -0
- iatoolkit/static/styles/chat_public.css +135 -0
- iatoolkit/static/styles/documents.css +598 -0
- iatoolkit/static/styles/landing_page.css +398 -0
- iatoolkit/static/styles/llm_output.css +148 -0
- iatoolkit/static/styles/onboarding.css +176 -0
- iatoolkit/system_prompts/__init__.py +0 -0
- iatoolkit/system_prompts/query_main.prompt +30 -23
- iatoolkit/system_prompts/sql_rules.prompt +47 -12
- iatoolkit/templates/_company_header.html +45 -0
- iatoolkit/templates/_login_widget.html +42 -0
- iatoolkit/templates/base.html +78 -0
- iatoolkit/templates/change_password.html +66 -0
- iatoolkit/templates/chat.html +337 -0
- iatoolkit/templates/chat_modals.html +185 -0
- iatoolkit/templates/error.html +51 -0
- iatoolkit/templates/forgot_password.html +51 -0
- iatoolkit/templates/onboarding_shell.html +106 -0
- iatoolkit/templates/signup.html +79 -0
- iatoolkit/views/__init__.py +5 -0
- iatoolkit/views/base_login_view.py +96 -0
- iatoolkit/views/change_password_view.py +116 -0
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/embedding_api_view.py +65 -0
- iatoolkit/views/forgot_password_view.py +75 -0
- iatoolkit/views/help_content_api_view.py +54 -0
- iatoolkit/views/history_api_view.py +56 -0
- iatoolkit/views/home_view.py +63 -0
- iatoolkit/views/init_context_api_view.py +74 -0
- iatoolkit/views/llmquery_api_view.py +59 -0
- iatoolkit/views/load_company_configuration_api_view.py +49 -0
- iatoolkit/views/load_document_api_view.py +65 -0
- iatoolkit/views/login_view.py +170 -0
- iatoolkit/views/logout_api_view.py +57 -0
- iatoolkit/views/profile_api_view.py +46 -0
- iatoolkit/views/prompt_api_view.py +37 -0
- iatoolkit/views/root_redirect_view.py +22 -0
- iatoolkit/views/signup_view.py +100 -0
- iatoolkit/views/static_page_view.py +27 -0
- iatoolkit/views/user_feedback_api_view.py +60 -0
- iatoolkit/views/users_api_view.py +33 -0
- iatoolkit/views/verify_user_view.py +60 -0
- iatoolkit-0.107.4.dist-info/METADATA +268 -0
- iatoolkit-0.107.4.dist-info/RECORD +132 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE +21 -0
- iatoolkit-0.107.4.dist-info/licenses/LICENSE_COMMUNITY.md +15 -0
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/top_level.txt +0 -1
- iatoolkit/iatoolkit.py +0 -413
- iatoolkit/system_prompts/arquitectura.prompt +0 -32
- iatoolkit-0.3.9.dist-info/METADATA +0 -252
- iatoolkit-0.3.9.dist-info/RECORD +0 -32
- services/__init__.py +0 -5
- services/api_service.py +0 -75
- services/dispatcher_service.py +0 -351
- services/excel_service.py +0 -98
- services/history_service.py +0 -45
- services/jwt_service.py +0 -91
- services/load_documents_service.py +0 -212
- services/mail_service.py +0 -62
- services/prompt_manager_service.py +0 -172
- services/query_service.py +0 -334
- services/search_service.py +0 -32
- services/sql_service.py +0 -42
- services/tasks_service.py +0 -188
- services/user_feedback_service.py +0 -67
- services/user_session_context_service.py +0 -85
- {iatoolkit-0.3.9.dist-info → iatoolkit-0.107.4.dist-info}/WHEEL +0 -0
services/dispatcher_service.py
DELETED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Producto: IAToolkit
|
|
3
|
-
# Todos los derechos reservados.
|
|
4
|
-
# En trámite de registro en el Registro de Propiedad Intelectual de Chile.
|
|
5
|
-
|
|
6
|
-
from iatoolkit import current_iatoolkit
|
|
7
|
-
from common.exceptions import IAToolkitException
|
|
8
|
-
from services.prompt_manager_service import PromptService
|
|
9
|
-
from services.api_service import ApiService
|
|
10
|
-
from repositories.llm_query_repo import LLMQueryRepo
|
|
11
|
-
from repositories.models import Company, Function
|
|
12
|
-
from services.excel_service import ExcelService
|
|
13
|
-
from services.mail_service import MailService
|
|
14
|
-
from iatoolkit.company_registry import get_company_registry
|
|
15
|
-
from common.session_manager import SessionManager
|
|
16
|
-
from common.util import Utility
|
|
17
|
-
from injector import inject
|
|
18
|
-
import logging
|
|
19
|
-
import os
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class Dispatcher:
|
|
23
|
-
@inject
|
|
24
|
-
def __init__(self,
|
|
25
|
-
prompt_service: PromptService,
|
|
26
|
-
llmquery_repo: LLMQueryRepo,
|
|
27
|
-
util: Utility,
|
|
28
|
-
api_service: ApiService,
|
|
29
|
-
excel_service: ExcelService,
|
|
30
|
-
mail_service: MailService):
|
|
31
|
-
self.prompt_service = prompt_service
|
|
32
|
-
self.llmquery_repo = llmquery_repo
|
|
33
|
-
self.api_service = api_service
|
|
34
|
-
self.util = util
|
|
35
|
-
self.excel_service = excel_service
|
|
36
|
-
self.mail_service = mail_service
|
|
37
|
-
self.system_functions = _FUNCTION_LIST
|
|
38
|
-
self.system_prompts = _SYSTEM_PROMPT
|
|
39
|
-
|
|
40
|
-
# Use the global registry
|
|
41
|
-
self.company_registry = get_company_registry()
|
|
42
|
-
|
|
43
|
-
# load into the dispatcher the configured companies
|
|
44
|
-
self.company_classes = {}
|
|
45
|
-
self.initialize_companies()
|
|
46
|
-
|
|
47
|
-
# run the statrtup logic for all companies
|
|
48
|
-
self.start_execution()
|
|
49
|
-
|
|
50
|
-
self.tool_handlers = {
|
|
51
|
-
"iat_generate_excel": self.excel_service.excel_generator,
|
|
52
|
-
"iat_send_email": self.mail_service.send_mail,
|
|
53
|
-
"iat_api_call": self.api_service.call_api
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
def initialize_companies(self):
|
|
57
|
-
"""
|
|
58
|
-
Initializes and instantiates all registered company classes.
|
|
59
|
-
This method should be called *after* the main injector is fully configured
|
|
60
|
-
and the company registry is populated.
|
|
61
|
-
"""
|
|
62
|
-
if self.company_classes: # Prevent re-initialization
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
# ✅ NOW it is safe to get the injector and instantiate companies.
|
|
66
|
-
injector = current_iatoolkit().get_injector()
|
|
67
|
-
self.company_registry.set_injector(injector)
|
|
68
|
-
self.company_classes = self.company_registry.instantiate_companies()
|
|
69
|
-
|
|
70
|
-
def start_execution(self):
|
|
71
|
-
"""Runs the startup logic for all registered companies."""
|
|
72
|
-
for company_name, company_instance in self.company_classes.items():
|
|
73
|
-
logging.info(f'Starting execution for company: {company_name}')
|
|
74
|
-
company_instance.start_execution()
|
|
75
|
-
|
|
76
|
-
return True
|
|
77
|
-
|
|
78
|
-
def setup_all_companies(self):
|
|
79
|
-
# create system functions
|
|
80
|
-
for function in self.system_functions:
|
|
81
|
-
self.llmquery_repo.create_or_update_function(
|
|
82
|
-
Function(
|
|
83
|
-
company_id=None,
|
|
84
|
-
system_function=True,
|
|
85
|
-
name=function['function_name'],
|
|
86
|
-
description= function['description'],
|
|
87
|
-
parameters=function['parameters']
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
# create the system prompts
|
|
92
|
-
i = 1
|
|
93
|
-
for prompt in self.system_prompts:
|
|
94
|
-
self.prompt_service.create_prompt(
|
|
95
|
-
prompt_name=prompt['name'],
|
|
96
|
-
description=prompt['description'],
|
|
97
|
-
order=1,
|
|
98
|
-
is_system_prompt=True
|
|
99
|
-
)
|
|
100
|
-
i += 1
|
|
101
|
-
|
|
102
|
-
# register in the database every company class
|
|
103
|
-
for company in self.company_classes.values():
|
|
104
|
-
company.register_company()
|
|
105
|
-
|
|
106
|
-
def dispatch(self, company_name: str, action: str, **kwargs) -> str:
|
|
107
|
-
company_key = company_name.lower()
|
|
108
|
-
|
|
109
|
-
if company_key not in self.company_classes:
|
|
110
|
-
available_companies = list(self.company_classes.keys())
|
|
111
|
-
raise IAToolkitException(
|
|
112
|
-
IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
113
|
-
f"Empresa '{company_name}' no configurada. Empresas disponibles: {available_companies}"
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
# check if action is a system function
|
|
117
|
-
if action in self.tool_handlers:
|
|
118
|
-
return self.tool_handlers[action](**kwargs)
|
|
119
|
-
|
|
120
|
-
company_instance = self.company_classes[company_name]
|
|
121
|
-
try:
|
|
122
|
-
return company_instance.handle_request(action, **kwargs)
|
|
123
|
-
except IAToolkitException as e:
|
|
124
|
-
# Si ya es una IAToolkitException, la relanzamos para preservar el tipo de error original.
|
|
125
|
-
raise e
|
|
126
|
-
|
|
127
|
-
except Exception as e:
|
|
128
|
-
logging.exception(e)
|
|
129
|
-
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
130
|
-
f"Error en function call '{action}': {str(e)}") from e
|
|
131
|
-
|
|
132
|
-
def get_company_context(self, company_name: str, **kwargs) -> str:
|
|
133
|
-
if company_name not in self.company_classes:
|
|
134
|
-
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
135
|
-
f"Empresa no configurada: {company_name}")
|
|
136
|
-
|
|
137
|
-
company_context = ''
|
|
138
|
-
|
|
139
|
-
# read the company context from this list of markdown files,
|
|
140
|
-
# company brief, credits, operation description, etc.
|
|
141
|
-
context_dir = os.path.join(os.getcwd(), f'companies/{company_name}/context')
|
|
142
|
-
context_files = self.util.get_files_by_extension(context_dir, '.md', return_extension=True)
|
|
143
|
-
for file in context_files:
|
|
144
|
-
filepath = os.path.join(context_dir, file)
|
|
145
|
-
company_context += self.util.load_markdown_context(filepath)
|
|
146
|
-
|
|
147
|
-
# add the schemas for every table or function call responses
|
|
148
|
-
schema_dir = os.path.join(os.getcwd(), f'companies/{company_name}/schema')
|
|
149
|
-
schema_files = self.util.get_files_by_extension(schema_dir, '.yaml', return_extension=True)
|
|
150
|
-
for file in schema_files:
|
|
151
|
-
schema_name = file.split('_')[0]
|
|
152
|
-
filepath = os.path.join(schema_dir, file)
|
|
153
|
-
company_context += self.util.generate_context_for_schema(schema_name, filepath)
|
|
154
|
-
|
|
155
|
-
company_instance = self.company_classes[company_name]
|
|
156
|
-
try:
|
|
157
|
-
return company_context + company_instance.get_company_context(**kwargs)
|
|
158
|
-
except Exception as e:
|
|
159
|
-
logging.exception(e)
|
|
160
|
-
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
161
|
-
f"Error en get_company_context de {company_name}: {str(e)}") from e
|
|
162
|
-
|
|
163
|
-
def get_company_services(self, company: Company) -> list[dict]:
|
|
164
|
-
# create the syntax with openai response syntax, for the company function list
|
|
165
|
-
tools = []
|
|
166
|
-
functions = self.llmquery_repo.get_company_functions(company)
|
|
167
|
-
|
|
168
|
-
for function in functions:
|
|
169
|
-
# make sure is always on
|
|
170
|
-
function.parameters["additionalProperties"] = False
|
|
171
|
-
|
|
172
|
-
ai_tool = {
|
|
173
|
-
"type": "function",
|
|
174
|
-
"name": function.name,
|
|
175
|
-
"description": function.description,
|
|
176
|
-
"parameters": function.parameters,
|
|
177
|
-
"strict": True
|
|
178
|
-
}
|
|
179
|
-
tools.append(ai_tool)
|
|
180
|
-
return tools
|
|
181
|
-
|
|
182
|
-
def get_user_info(self, company_name: str, user_identifier: str, is_local_user: bool) -> dict:
|
|
183
|
-
if company_name not in self.company_classes:
|
|
184
|
-
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
185
|
-
f"Empresa no configurada: {company_name}")
|
|
186
|
-
|
|
187
|
-
raw_user_data = {}
|
|
188
|
-
if is_local_user:
|
|
189
|
-
# source 1: local user login into IAToolkit
|
|
190
|
-
raw_user_data = SessionManager.get('user', {})
|
|
191
|
-
else:
|
|
192
|
-
# source 2: external company user
|
|
193
|
-
company_instance = self.company_classes[company_name]
|
|
194
|
-
try:
|
|
195
|
-
raw_user_data = company_instance.get_user_info(user_identifier)
|
|
196
|
-
except Exception as e:
|
|
197
|
-
logging.exception(e)
|
|
198
|
-
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
199
|
-
f"Error en get_user_info de {company_name}: {str(e)}") from e
|
|
200
|
-
|
|
201
|
-
# always normalize the data for consistent structure
|
|
202
|
-
return self._normalize_user_data(raw_user_data, is_local_user)
|
|
203
|
-
|
|
204
|
-
def _normalize_user_data(self, raw_data: dict, is_local: bool) -> dict:
|
|
205
|
-
"""
|
|
206
|
-
Asegura que los datos del usuario siempre tengan una estructura consistente.
|
|
207
|
-
"""
|
|
208
|
-
# Valores por defecto para un perfil robusto
|
|
209
|
-
normalized_user = {
|
|
210
|
-
"id": raw_data.get("id", 0),
|
|
211
|
-
"user_email": raw_data.get("email", ""),
|
|
212
|
-
"user_fullname": raw_data.get("user_fullname", ""),
|
|
213
|
-
"super_user": raw_data.get("super_user", False),
|
|
214
|
-
"company_id": raw_data.get("company_id", 0),
|
|
215
|
-
"company_name": raw_data.get("company", ""),
|
|
216
|
-
"company_short_name": raw_data.get("company_short_name", ""),
|
|
217
|
-
"is_local": is_local,
|
|
218
|
-
"extras": raw_data.get("extras", {})
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
# get the extras from the raw data, if any
|
|
222
|
-
extras = raw_data.get("extras", {})
|
|
223
|
-
if isinstance(extras, dict):
|
|
224
|
-
normalized_user.update(extras)
|
|
225
|
-
|
|
226
|
-
return normalized_user
|
|
227
|
-
|
|
228
|
-
def get_metadata_from_filename(self, company_name: str, filename: str) -> dict:
|
|
229
|
-
if company_name not in self.company_classes:
|
|
230
|
-
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
231
|
-
f"Empresa no configurada: {company_name}")
|
|
232
|
-
|
|
233
|
-
company_instance = self.company_classes[company_name]
|
|
234
|
-
try:
|
|
235
|
-
return company_instance.get_metadata_from_filename(filename)
|
|
236
|
-
except Exception as e:
|
|
237
|
-
logging.exception(e)
|
|
238
|
-
raise IAToolkitException(IAToolkitException.ErrorType.EXTERNAL_SOURCE_ERROR,
|
|
239
|
-
f"Error en get_metadata_from_filename de {company_name}: {str(e)}") from e
|
|
240
|
-
|
|
241
|
-
def get_company_instance(self, company_name: str):
|
|
242
|
-
"""Returns the instance for a given company name."""
|
|
243
|
-
return self.company_classes.get(company_name)
|
|
244
|
-
|
|
245
|
-
def get_registered_companies(self) -> dict:
|
|
246
|
-
"""Obtiene todas las empresas registradas (para debugging/admin)"""
|
|
247
|
-
return {
|
|
248
|
-
"registered_classes": list(self.company_registry.get_registered_companies().keys()),
|
|
249
|
-
"instantiated": list(self.company_classes.keys()),
|
|
250
|
-
"count": len(self.company_classes)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
# iatoolkit system prompts
|
|
255
|
-
_SYSTEM_PROMPT = [
|
|
256
|
-
{'name': 'query_main', 'description':'main prompt de iatoolkit'},
|
|
257
|
-
{'name': 'format_styles', 'description':'formatos y estilos de salida'},
|
|
258
|
-
{'name': 'sql_rules', 'description':'instrucciones para generar sql'}
|
|
259
|
-
]
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
# iatoolkit function calls
|
|
263
|
-
_FUNCTION_LIST = [
|
|
264
|
-
{
|
|
265
|
-
"name": "iat_generate_excel",
|
|
266
|
-
"description": "Generador de Excel."
|
|
267
|
-
"Genera un archivo Excel (.xlsx) a partir de una lista de diccionarios. "
|
|
268
|
-
"Cada diccionario representa una fila del archivo. "
|
|
269
|
-
"el archivo se guarda en directorio de descargas."
|
|
270
|
-
"retorna diccionario con filename, attachment_token (para enviar archivo por mail)"
|
|
271
|
-
"content_type y download_link",
|
|
272
|
-
"function_name": "iat_generate_excel",
|
|
273
|
-
"parameters": {
|
|
274
|
-
"type": "object",
|
|
275
|
-
"properties": {
|
|
276
|
-
"filename": {
|
|
277
|
-
"type": "string",
|
|
278
|
-
"description": "Nombre del archivo de salida (ejemplo: 'reporte.xlsx')",
|
|
279
|
-
"pattern": "^.+\\.xlsx?$"
|
|
280
|
-
},
|
|
281
|
-
"sheet_name": {
|
|
282
|
-
"type": "string",
|
|
283
|
-
"description": "Nombre de la hoja dentro del Excel",
|
|
284
|
-
"minLength": 1
|
|
285
|
-
},
|
|
286
|
-
"data": {
|
|
287
|
-
"type": "array",
|
|
288
|
-
"description": "Lista de diccionarios. Cada diccionario representa una fila.",
|
|
289
|
-
"minItems": 1,
|
|
290
|
-
"items": {
|
|
291
|
-
"type": "object",
|
|
292
|
-
"properties": {},
|
|
293
|
-
"additionalProperties": {
|
|
294
|
-
"anyOf": [
|
|
295
|
-
{"type": "string"},
|
|
296
|
-
{"type": "number"},
|
|
297
|
-
{"type": "boolean"},
|
|
298
|
-
{"type": "null"},
|
|
299
|
-
{
|
|
300
|
-
"type": "string",
|
|
301
|
-
"format": "date"
|
|
302
|
-
}
|
|
303
|
-
]
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
"required": ["filename", "sheet_name", "data"]
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
'name': 'Envio de mails',
|
|
313
|
-
'description': "iatoolkit mail system. "
|
|
314
|
-
"envia mails cuando un usuario lo solicita."
|
|
315
|
-
"Si no te indican quien envia el correo utiliza la dirección iatoolkit@iatoolkit.com",
|
|
316
|
-
'function_name': "iat_send_email",
|
|
317
|
-
'parameters': {
|
|
318
|
-
"type": "object",
|
|
319
|
-
"properties": {
|
|
320
|
-
"from_email": {"type": "string","description": "dirección de correo electrónico que esta enviando el email."},
|
|
321
|
-
"recipient": {"type": "string", "description": "email del destinatario"},
|
|
322
|
-
"subject": {"type": "string", "description": "asunto del email"},
|
|
323
|
-
"body": {"type": "string", "description": "HTML del email"},
|
|
324
|
-
"attachments": {
|
|
325
|
-
"type": "array",
|
|
326
|
-
"description": "Lista de archivos adjuntos codificados en base64",
|
|
327
|
-
"items": {
|
|
328
|
-
"type": "object",
|
|
329
|
-
"properties": {
|
|
330
|
-
"filename": {
|
|
331
|
-
"type": "string",
|
|
332
|
-
"description": "Nombre del archivo con su extensión (ej. informe.pdf)"
|
|
333
|
-
},
|
|
334
|
-
"content": {
|
|
335
|
-
"type": "string",
|
|
336
|
-
"description": "Contenido del archivo en b64."
|
|
337
|
-
},
|
|
338
|
-
"attachment_token": {
|
|
339
|
-
"type": "string",
|
|
340
|
-
"description": "token para descargar el archivo."
|
|
341
|
-
}
|
|
342
|
-
},
|
|
343
|
-
"required": ["filename", "content", "attachment_token"],
|
|
344
|
-
"additionalProperties": False
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
},
|
|
348
|
-
"required": ["from_email","recipient", "subject", "body", "attachments"]
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
]
|
services/excel_service.py
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Producto: IAToolkit
|
|
3
|
-
# Todos los derechos reservados.
|
|
4
|
-
# En trámite de registro en el Registro de Propiedad Intelectual de Chile.
|
|
5
|
-
|
|
6
|
-
from common.util import Utility
|
|
7
|
-
import pandas as pd
|
|
8
|
-
from uuid import uuid4
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from common.exceptions import IAToolkitException
|
|
11
|
-
from injector import inject
|
|
12
|
-
import os
|
|
13
|
-
import logging
|
|
14
|
-
from flask import current_app, jsonify
|
|
15
|
-
|
|
16
|
-
EXCEL_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ExcelService:
|
|
20
|
-
@inject
|
|
21
|
-
def __init__(self,util: Utility):
|
|
22
|
-
self.util = util
|
|
23
|
-
|
|
24
|
-
def excel_generator(self, **kwargs) -> str:
|
|
25
|
-
"""
|
|
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
|
-
try:
|
|
42
|
-
# get the parameters
|
|
43
|
-
fname = kwargs.get('filename')
|
|
44
|
-
if not fname:
|
|
45
|
-
return 'falta el nombre del archivo de salida'
|
|
46
|
-
|
|
47
|
-
data = kwargs.get('data')
|
|
48
|
-
if not data or not isinstance(data, list):
|
|
49
|
-
return 'faltan los datos o no es una lista de diccionarios'
|
|
50
|
-
|
|
51
|
-
sheet_name = kwargs.get('sheet_name', 'hoja 1')
|
|
52
|
-
|
|
53
|
-
# 1. convert dictionary to dataframe
|
|
54
|
-
df = pd.DataFrame(data)
|
|
55
|
-
|
|
56
|
-
# 3. create temporary name
|
|
57
|
-
token = f"{uuid4()}.xlsx"
|
|
58
|
-
filepath = Path("static/temp") / token
|
|
59
|
-
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
-
|
|
61
|
-
# 4. save excel file in temporary directory
|
|
62
|
-
df.to_excel(filepath, index=False, sheet_name=sheet_name)
|
|
63
|
-
|
|
64
|
-
# 5. return the link to the LLM
|
|
65
|
-
return {
|
|
66
|
-
"filename": fname,
|
|
67
|
-
"attachment_token": token,
|
|
68
|
-
"content_type": EXCEL_MIME,
|
|
69
|
-
"download_link": f"/download/{token}"
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
except Exception as e:
|
|
73
|
-
raise IAToolkitException(IAToolkitException.ErrorType.CALL_ERROR,
|
|
74
|
-
'error generating excel file') from e
|
|
75
|
-
|
|
76
|
-
def validate_file_access(self, filename):
|
|
77
|
-
try:
|
|
78
|
-
if not filename:
|
|
79
|
-
return jsonify({"error": "Nombre de archivo inválido"})
|
|
80
|
-
# Prevent path traversal attacks
|
|
81
|
-
if '..' in filename or filename.startswith('/') or '\\' in filename:
|
|
82
|
-
return jsonify({"error": "Nombre de archivo inválido"})
|
|
83
|
-
|
|
84
|
-
temp_dir = os.path.join(current_app.root_path, 'static', 'temp')
|
|
85
|
-
file_path = os.path.join(temp_dir, filename)
|
|
86
|
-
|
|
87
|
-
if not os.path.exists(file_path):
|
|
88
|
-
return jsonify({"error": "Archivo no encontrado"})
|
|
89
|
-
|
|
90
|
-
if not os.path.isfile(file_path):
|
|
91
|
-
return jsonify({"error": "La ruta no corresponde a un archivo"})
|
|
92
|
-
|
|
93
|
-
return None
|
|
94
|
-
|
|
95
|
-
except Exception as e:
|
|
96
|
-
error_msg = f"Error validando acceso al archivo {filename}: {str(e)}"
|
|
97
|
-
logging.error(error_msg)
|
|
98
|
-
return jsonify({"error": "Error validando archivo"})
|
services/history_service.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Producto: IAToolkit
|
|
3
|
-
# Todos los derechos reservados.
|
|
4
|
-
# En trámite de registro en el Registro de Propiedad Intelectual de Chile.
|
|
5
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class HistoryService:
|
|
13
|
-
@inject
|
|
14
|
-
def __init__(self, llm_query_repo: LLMQueryRepo,
|
|
15
|
-
profile_repo: ProfileRepo,
|
|
16
|
-
util: Utility):
|
|
17
|
-
self.llm_query_repo = llm_query_repo
|
|
18
|
-
self.profile_repo = profile_repo
|
|
19
|
-
self.util = util
|
|
20
|
-
|
|
21
|
-
def get_history(self,
|
|
22
|
-
company_short_name: str,
|
|
23
|
-
external_user_id: str = None,
|
|
24
|
-
local_user_id: int = 0) -> dict:
|
|
25
|
-
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
|
-
# validate company
|
|
31
|
-
company = self.profile_repo.get_company_by_short_name(company_short_name)
|
|
32
|
-
if not company:
|
|
33
|
-
return {'error': f'No existe la empresa: {company_short_name}'}
|
|
34
|
-
|
|
35
|
-
history = self.llm_query_repo.get_history(company, user_identifier)
|
|
36
|
-
|
|
37
|
-
if not history:
|
|
38
|
-
return {'error': 'No se pudo obtener el historial'}
|
|
39
|
-
|
|
40
|
-
history_list = [query.to_dict() for query in history]
|
|
41
|
-
|
|
42
|
-
return {'message': 'Historial obtenido correctamente', 'history': history_list}
|
|
43
|
-
|
|
44
|
-
except Exception as e:
|
|
45
|
-
return {'error': str(e)}
|
services/jwt_service.py
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2024 Fernando Libedinsky
|
|
2
|
-
# Producto: IAToolkit
|
|
3
|
-
# Todos los derechos reservados.
|
|
4
|
-
# En trámite de registro en el Registro de Propiedad Intelectual de Chile.
|
|
5
|
-
|
|
6
|
-
import jwt
|
|
7
|
-
import time
|
|
8
|
-
import logging
|
|
9
|
-
from injector import singleton, inject
|
|
10
|
-
from typing import Optional, Dict, Any
|
|
11
|
-
from flask import Flask
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@singleton
|
|
15
|
-
class JWTService:
|
|
16
|
-
@inject
|
|
17
|
-
def __init__(self, app: Flask):
|
|
18
|
-
# Acceder a la configuración directamente desde app.config
|
|
19
|
-
try:
|
|
20
|
-
self.secret_key = app.config['JWT_SECRET_KEY']
|
|
21
|
-
self.algorithm = app.config['JWT_ALGORITHM']
|
|
22
|
-
except KeyError as e:
|
|
23
|
-
logging.error(f"Configuración JWT faltante en app.config: {e}. JWTService no funcionará correctamente.")
|
|
24
|
-
raise RuntimeError(f"Configuración JWT esencial faltante: {e}")
|
|
25
|
-
|
|
26
|
-
def generate_chat_jwt(self,
|
|
27
|
-
company_id: int,
|
|
28
|
-
company_short_name: str,
|
|
29
|
-
external_user_id: str,
|
|
30
|
-
expires_delta_seconds: int) -> Optional[str]:
|
|
31
|
-
# generate a JWT for a chat session
|
|
32
|
-
try:
|
|
33
|
-
payload = {
|
|
34
|
-
'company_id': company_id,
|
|
35
|
-
'company_short_name': company_short_name,
|
|
36
|
-
'external_user_id': external_user_id,
|
|
37
|
-
'exp': time.time() + expires_delta_seconds,
|
|
38
|
-
'iat': time.time(),
|
|
39
|
-
'type': 'chat_session' # Identificador del tipo de token
|
|
40
|
-
}
|
|
41
|
-
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
|
42
|
-
return token
|
|
43
|
-
except Exception as e:
|
|
44
|
-
logging.error(f"Error al generar JWT para company {company_id}, user {external_user_id}: {e}")
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
def validate_chat_jwt(self, token: str, expected_company_short_name: str) -> Optional[Dict[str, Any]]:
|
|
48
|
-
"""
|
|
49
|
-
Valida un JWT de sesión de chat.
|
|
50
|
-
Retorna el payload decodificado si es válido y coincide con la empresa, o None.
|
|
51
|
-
"""
|
|
52
|
-
if not token:
|
|
53
|
-
return None
|
|
54
|
-
try:
|
|
55
|
-
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
|
|
56
|
-
|
|
57
|
-
# Validaciones adicionales
|
|
58
|
-
if payload.get('type') != 'chat_session':
|
|
59
|
-
logging.warning(f"Validación JWT fallida: tipo incorrecto '{payload.get('type')}'")
|
|
60
|
-
return None
|
|
61
|
-
|
|
62
|
-
if payload.get('company_short_name') != expected_company_short_name:
|
|
63
|
-
logging.warning(
|
|
64
|
-
f"Validación JWT fallida: company_short_name no coincide. "
|
|
65
|
-
f"Esperado: {expected_company_short_name}, Obtenido: {payload.get('company_short_name')}"
|
|
66
|
-
)
|
|
67
|
-
return None
|
|
68
|
-
|
|
69
|
-
# external_user_id debe estar presente
|
|
70
|
-
if 'external_user_id' not in payload or not payload['external_user_id']:
|
|
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.")
|
|
77
|
-
return None
|
|
78
|
-
|
|
79
|
-
logging.debug(
|
|
80
|
-
f"JWT validado exitosamente para company: {payload.get('company_short_name')}, user: {payload.get('external_user_id')}")
|
|
81
|
-
return payload
|
|
82
|
-
|
|
83
|
-
except jwt.ExpiredSignatureError:
|
|
84
|
-
logging.info(f"Validación JWT fallida: token expirado para {expected_company_short_name}")
|
|
85
|
-
return None
|
|
86
|
-
except jwt.InvalidTokenError as e:
|
|
87
|
-
logging.warning(f"Validación JWT fallida: token inválido para {expected_company_short_name}. Error: {e}")
|
|
88
|
-
return None
|
|
89
|
-
except Exception as e:
|
|
90
|
-
logging.error(f"Error inesperado durante validación de JWT para {expected_company_short_name}: {e}")
|
|
91
|
-
return None
|