iatoolkit 0.91.1__py3-none-any.whl → 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iatoolkit/__init__.py +6 -4
- iatoolkit/base_company.py +0 -16
- iatoolkit/cli_commands.py +3 -14
- iatoolkit/common/exceptions.py +1 -0
- iatoolkit/common/interfaces/__init__.py +0 -0
- iatoolkit/common/interfaces/asset_storage.py +34 -0
- iatoolkit/common/interfaces/database_provider.py +43 -0
- iatoolkit/common/model_registry.py +159 -0
- iatoolkit/common/routes.py +47 -5
- iatoolkit/common/util.py +32 -13
- iatoolkit/company_registry.py +5 -0
- iatoolkit/core.py +51 -20
- iatoolkit/infra/connectors/file_connector_factory.py +1 -0
- iatoolkit/infra/connectors/s3_connector.py +4 -2
- iatoolkit/infra/llm_providers/__init__.py +0 -0
- iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
- iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
- iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
- iatoolkit/infra/llm_proxy.py +235 -134
- iatoolkit/infra/llm_response.py +5 -0
- iatoolkit/locales/en.yaml +158 -2
- iatoolkit/locales/es.yaml +158 -0
- iatoolkit/repositories/database_manager.py +52 -47
- iatoolkit/repositories/document_repo.py +7 -0
- iatoolkit/repositories/filesystem_asset_repository.py +36 -0
- iatoolkit/repositories/llm_query_repo.py +2 -0
- iatoolkit/repositories/models.py +72 -79
- iatoolkit/repositories/profile_repo.py +59 -3
- iatoolkit/repositories/vs_repo.py +22 -24
- iatoolkit/services/company_context_service.py +126 -53
- iatoolkit/services/configuration_service.py +299 -73
- iatoolkit/services/dispatcher_service.py +21 -3
- iatoolkit/services/file_processor_service.py +0 -5
- iatoolkit/services/history_manager_service.py +43 -24
- iatoolkit/services/knowledge_base_service.py +425 -0
- iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +38 -29
- iatoolkit/services/load_documents_service.py +26 -48
- iatoolkit/services/profile_service.py +32 -4
- iatoolkit/services/prompt_service.py +32 -30
- iatoolkit/services/query_service.py +51 -26
- iatoolkit/services/sql_service.py +122 -74
- iatoolkit/services/tool_service.py +26 -11
- iatoolkit/services/user_session_context_service.py +115 -63
- iatoolkit/static/js/chat_main.js +44 -4
- iatoolkit/static/js/chat_model_selector.js +227 -0
- iatoolkit/static/js/chat_onboarding_button.js +1 -1
- iatoolkit/static/js/chat_reload_button.js +4 -1
- iatoolkit/static/styles/chat_iatoolkit.css +58 -2
- iatoolkit/static/styles/llm_output.css +34 -1
- iatoolkit/system_prompts/query_main.prompt +26 -2
- iatoolkit/templates/base.html +13 -0
- iatoolkit/templates/chat.html +45 -2
- iatoolkit/templates/onboarding_shell.html +0 -1
- iatoolkit/views/base_login_view.py +7 -2
- iatoolkit/views/chat_view.py +76 -0
- iatoolkit/views/configuration_api_view.py +163 -0
- iatoolkit/views/load_document_api_view.py +14 -10
- iatoolkit/views/login_view.py +8 -3
- iatoolkit/views/rag_api_view.py +216 -0
- iatoolkit/views/users_api_view.py +33 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/METADATA +4 -4
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/RECORD +66 -58
- iatoolkit/repositories/tasks_repo.py +0 -52
- iatoolkit/services/search_service.py +0 -55
- iatoolkit/services/tasks_service.py +0 -188
- iatoolkit/views/tasks_api_view.py +0 -72
- iatoolkit/views/tasks_review_api_view.py +0 -55
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/WHEEL +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
- {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/top_level.txt +0 -0
iatoolkit/locales/es.yaml
CHANGED
|
@@ -45,6 +45,110 @@ ui:
|
|
|
45
45
|
prompts_available: "Prompts disponibles"
|
|
46
46
|
init_context: "Inicializando el contexto de la IA ..."
|
|
47
47
|
|
|
48
|
+
admin:
|
|
49
|
+
workspace: "Recursos"
|
|
50
|
+
configuration: "Configuración"
|
|
51
|
+
company_config: "Configuración Empresa (company.yaml)"
|
|
52
|
+
knowledge: "Conocimiento"
|
|
53
|
+
knowledge_rag: "RAG (Vectorial)"
|
|
54
|
+
knowledge_static: "Contenido Estático"
|
|
55
|
+
prompts: "Prompts"
|
|
56
|
+
prompts_description: "Prompts de sistema"
|
|
57
|
+
schemas: "Schemas"
|
|
58
|
+
schemas_description: "Definiciones de entidades"
|
|
59
|
+
context: "Contexto"
|
|
60
|
+
context_description: "Contenido estatico (Markdown)"
|
|
61
|
+
administration: "Administración"
|
|
62
|
+
monitoring: "Monitoreo"
|
|
63
|
+
teams: "Usuarios"
|
|
64
|
+
billing: "Facturación"
|
|
65
|
+
files: "Archivos"
|
|
66
|
+
select_file: "Seleccione un archivo a editar"
|
|
67
|
+
select_category: "Seleccione una categoría"
|
|
68
|
+
editing: "Editando"
|
|
69
|
+
no_file_selected: "Seleccione un archivo"
|
|
70
|
+
new: "Nuevo"
|
|
71
|
+
confirm: "Confirmar"
|
|
72
|
+
cancel: "Cancelar"
|
|
73
|
+
new_file: "Nuevo archivo"
|
|
74
|
+
delete_file: "Borrar archivo"
|
|
75
|
+
rename_file: "Renombrar archivo"
|
|
76
|
+
save_file: "Guardar"
|
|
77
|
+
credentials: "Email y password son requeridos."
|
|
78
|
+
saved_ok: "Guardado correctamente"
|
|
79
|
+
deleted_ok: "Eliminado correctamente"
|
|
80
|
+
company: "Empresa"
|
|
81
|
+
admin_page_title: "Acceso de administración"
|
|
82
|
+
user_manager: "Administración de usuarios"
|
|
83
|
+
load_configuration: "Guardar configuración"
|
|
84
|
+
goto_chat: "Ir al chat"
|
|
85
|
+
logout: "Cerrar sesión"
|
|
86
|
+
|
|
87
|
+
db_explorer:
|
|
88
|
+
data_explorer: "Explorador de datos"
|
|
89
|
+
database_connection: "Bases de datos"
|
|
90
|
+
tables: "Tablas"
|
|
91
|
+
data_explorer_description: "Explora tus tablas de base de datos."
|
|
92
|
+
select_database: "Seleccione una base de datos para ver sus tablas."
|
|
93
|
+
not_table_selected: "No hay tabla seleccionada."
|
|
94
|
+
view_yaml: "Ver YAML"
|
|
95
|
+
save_schema: "Guardar esquema"
|
|
96
|
+
select_table: "Seleccionar una tabla del panel izquierdo"
|
|
97
|
+
table_semantics: "Significado de la tabla"
|
|
98
|
+
table_description: "Descripción de la tabla (Contexto IA)"
|
|
99
|
+
table_placeholder: "Describe el significado de la tabla, ejemplo: tabla de usuarios."
|
|
100
|
+
column_metadata: "Metadatos de columna"
|
|
101
|
+
auto_detect: "Auto-detectar desde la BD"
|
|
102
|
+
meta_column: "Columna"
|
|
103
|
+
meta_type: "Tipo"
|
|
104
|
+
meta_description: "Descripción"
|
|
105
|
+
meta_synonyms: "Sinonimos"
|
|
106
|
+
pii_sesitive: "IP Sensible"
|
|
107
|
+
|
|
108
|
+
config:
|
|
109
|
+
editor_description: "Editor de configuración"
|
|
110
|
+
title: "Editor de configuraciones"
|
|
111
|
+
sections: "Secciones"
|
|
112
|
+
refresh: "Refrescar"
|
|
113
|
+
validate: "Validar"
|
|
114
|
+
select_section: "Seleccione una sección del panel izquierdo"
|
|
115
|
+
no_section_selected: "No hay sección seleccionada."
|
|
116
|
+
view_yaml: "Ver YAML"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
rag:
|
|
120
|
+
ingestion: "Ingesta"
|
|
121
|
+
ingestion_description: "Ingestar documentos en la base de conocimiento."
|
|
122
|
+
workbench: "Area de trabajo"
|
|
123
|
+
documents: "Documentos"
|
|
124
|
+
retrieval_lab: "Laboratorio de recuperación"
|
|
125
|
+
retrieval_description: "Probar la búsqueda semántica y la recuperación de contexto."
|
|
126
|
+
query_placeholder: "Introduce una pregunta para consultar la base de conocimiento..."
|
|
127
|
+
search_button: "Buscar"
|
|
128
|
+
filter: "Filtrar"
|
|
129
|
+
search_results_title: "Listo para probar"
|
|
130
|
+
search_results_description: "Los resultados aparecerán aquí"
|
|
131
|
+
filename: "Nombre de archivo"
|
|
132
|
+
filename_placeholder: "Contiene..."
|
|
133
|
+
user: "Usuario"
|
|
134
|
+
status: "Estado"
|
|
135
|
+
all_status: "Estados"
|
|
136
|
+
status_active: "Activo"
|
|
137
|
+
status_pending: "Pendiente"
|
|
138
|
+
status_processing: "Procesando"
|
|
139
|
+
status_failed: "Fallido"
|
|
140
|
+
created_at: "Creado"
|
|
141
|
+
date_range: "Rango de fechas"
|
|
142
|
+
delete_confirmation: "¿Eliminar archivo?"
|
|
143
|
+
delete_message: "Esta acción no se puede deshacer. El archivo se eliminará permanentemente."
|
|
144
|
+
delete_button: "Eliminar"
|
|
145
|
+
delete_cancel: "Cancelar"
|
|
146
|
+
target_collection: "Categoría seleccionada"
|
|
147
|
+
select_collection_placeholder: "Selecciona una categoría"
|
|
148
|
+
collection_required: "Debe seleccionar una categoría"
|
|
149
|
+
all_collections: "Todas las categorías"
|
|
150
|
+
collection: "Categoría"
|
|
151
|
+
|
|
48
152
|
|
|
49
153
|
tooltips:
|
|
50
154
|
history: "Historial con mis consultas"
|
|
@@ -52,6 +156,7 @@ ui:
|
|
|
52
156
|
feedback: "Tu feedback es muy importante"
|
|
53
157
|
usage_guide: "Guía de Uso"
|
|
54
158
|
onboarding: "Cómo preguntar mejor"
|
|
159
|
+
preferences: "Preferencias"
|
|
55
160
|
logout: "Cerrar sesión"
|
|
56
161
|
use_prompt_assistant: "Usar Asistente de Prompts"
|
|
57
162
|
attach_files: "Adjuntar archivos"
|
|
@@ -87,6 +192,9 @@ errors:
|
|
|
87
192
|
authentication_required: "Autenticación requerida. No se proporcionó cookie de sesión o clave de API."
|
|
88
193
|
invalid_api_key: "Clave de API inválida o inactiva."
|
|
89
194
|
no_user_identifier_api: "No se proporcionó user_identifier para la llamada a la API."
|
|
195
|
+
no_company_permissions: "No tiene permisos para administrar esta empresa."
|
|
196
|
+
api_key_name_required: "el parametro api_key_name es requerido."
|
|
197
|
+
|
|
90
198
|
templates:
|
|
91
199
|
company_not_found: "Empresa no encontrada."
|
|
92
200
|
home_template_not_found: "La plantilla de la página de inicio para la empresa '{company_short_name}' no está configurada."
|
|
@@ -143,6 +251,20 @@ services:
|
|
|
143
251
|
start_query: "Hola, cual es tu pregunta?"
|
|
144
252
|
mail_change_password: "se envio mail para cambio de clave"
|
|
145
253
|
|
|
254
|
+
rag:
|
|
255
|
+
ingestion:
|
|
256
|
+
duplicate: "El documento '{filename}' ya existe para la empresa '{company_short_name}'. Omitiendo ingesta."
|
|
257
|
+
failed: "Error al ingerir el documento: {error}"
|
|
258
|
+
processing_failed: "El procesamiento falló: {error}"
|
|
259
|
+
empty_text: "El texto extraído está vacío."
|
|
260
|
+
search:
|
|
261
|
+
query_required: "La consulta (query) es obligatoria."
|
|
262
|
+
company_not_found: "Empresa '{company_short_name}' no encontrada."
|
|
263
|
+
management:
|
|
264
|
+
delete_success: "Documento eliminado."
|
|
265
|
+
not_found: "Documento no encontrado."
|
|
266
|
+
action_not_found: "Acción '{action}' no encontrada."
|
|
267
|
+
|
|
146
268
|
flash_messages:
|
|
147
269
|
password_changed_success: "Tu contraseña ha sido restablecida exitosamente. Ahora puedes iniciar sesión."
|
|
148
270
|
signup_success: "Registro exitoso. Por favor, revisa tu correo para verificar tu cuenta."
|
|
@@ -169,4 +291,40 @@ js_messages:
|
|
|
169
291
|
reload_init: "Iniciando recarga de contexto en segundo plano..."
|
|
170
292
|
no_history_found: "No existe historial de consultas."
|
|
171
293
|
example: "Ejemplo:"
|
|
294
|
+
show_reasoning: "Ver razonamiento"
|
|
295
|
+
unsaved: "Modificado (no guardado)"
|
|
296
|
+
saved_ok: "Archivo guadado correctamente"
|
|
297
|
+
error_saving: "Error al guardar archivo"
|
|
298
|
+
select_file: "Seleccione un archivo de la lista"
|
|
299
|
+
no_file_selected: "No hay archivo seleccionado"
|
|
300
|
+
select_company: "Seleccione empresa"
|
|
301
|
+
file_created: "Archivo creado correctamente"
|
|
302
|
+
delete_ok: "Archivo eliminado correctamente"
|
|
303
|
+
delete_failed: "Error al eliminar archivo"
|
|
304
|
+
rename_ok: "Archivo renombrado correctamente"
|
|
305
|
+
not_saved: 'No puede guardar'
|
|
306
|
+
invalid_file_name: "Nombre de archivo no válido. Usa solo letras, números, guiones bajos, guiones y puntos."
|
|
307
|
+
config_loaded: "Configuración cargada correctamente."
|
|
308
|
+
config_load_error: "Error cargando confguración."
|
|
309
|
+
search_placeholder: "Buscar usuarios..."
|
|
310
|
+
showing: "Mostrando"
|
|
311
|
+
records: "Registros"
|
|
312
|
+
db_user: "Usuario"
|
|
313
|
+
db_role: "Rol"
|
|
314
|
+
db_verified: "Verificado"
|
|
315
|
+
db_collection: "Colección"
|
|
316
|
+
db_last_access: "ultimo acceso"
|
|
317
|
+
db_filename: "Nombre de archivo"
|
|
318
|
+
db_user: "Usuario"
|
|
319
|
+
db_status: "Estado"
|
|
320
|
+
db_created: "Creado"
|
|
321
|
+
editor_no_file_selected: "No hay archivo seleccionado"
|
|
322
|
+
error_loading: "Error cargando el contenido del archivo"
|
|
323
|
+
cant_load_company: "No puede cargarcompany.yaml"
|
|
324
|
+
config_saved: "Configuración guardada correctamente."
|
|
325
|
+
config_error: "Error guardando configuración"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
172
330
|
|
|
@@ -10,13 +10,15 @@ from sqlalchemy.engine.url import make_url
|
|
|
10
10
|
from iatoolkit.repositories.models import Base
|
|
11
11
|
from injector import inject
|
|
12
12
|
from pgvector.psycopg2 import register_vector
|
|
13
|
+
from iatoolkit.common.interfaces.database_provider import DatabaseProvider
|
|
14
|
+
import logging
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
class DatabaseManager:
|
|
17
|
+
class DatabaseManager(DatabaseProvider):
|
|
16
18
|
@inject
|
|
17
19
|
def __init__(self,
|
|
18
20
|
database_url: str,
|
|
19
|
-
schema: str
|
|
21
|
+
schema: str = 'public',
|
|
20
22
|
register_pgvector: bool = True):
|
|
21
23
|
"""
|
|
22
24
|
Inicializa el gestor de la base de datos.
|
|
@@ -91,9 +93,6 @@ class DatabaseManager:
|
|
|
91
93
|
def get_connection(self):
|
|
92
94
|
return self._engine.connect()
|
|
93
95
|
|
|
94
|
-
def get_engine(self):
|
|
95
|
-
return self._engine
|
|
96
|
-
|
|
97
96
|
def create_all(self):
|
|
98
97
|
# if there is a schema defined, make sure it exists before creating tables
|
|
99
98
|
backend = self.url.get_backend_name()
|
|
@@ -109,52 +108,58 @@ class DatabaseManager:
|
|
|
109
108
|
def remove_session(self):
|
|
110
109
|
self.scoped_session.remove()
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
# Returns a list of all table names in the database
|
|
114
|
-
inspector = inspect(self._engine)
|
|
115
|
-
return inspector.get_table_names(schema=self.schema)
|
|
116
|
-
|
|
117
|
-
def get_table_schema(self,
|
|
118
|
-
table_name: str,
|
|
119
|
-
db_schema: str,
|
|
120
|
-
schema_object_name: str | None = None,
|
|
121
|
-
exclude_columns: list[str] | None = None) -> str:
|
|
122
|
-
inspector = inspect(self._engine)
|
|
123
|
-
|
|
124
|
-
# search the table in the specified schema
|
|
125
|
-
if table_name not in inspector.get_table_names(schema=db_schema):
|
|
126
|
-
raise RuntimeError(f"Table '{table_name}' does not exist in database schema '{db_schema}'.")
|
|
111
|
+
# -- execution methods ----
|
|
127
112
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
113
|
+
def execute_query(self, query: str, commit: bool = False) -> list[dict] | dict:
|
|
114
|
+
"""
|
|
115
|
+
Implementation for Direct SQLAlchemy connection.
|
|
116
|
+
"""
|
|
117
|
+
session = self.get_session()
|
|
118
|
+
if self.schema:
|
|
119
|
+
session.execute(text(f"SET search_path TO {self.schema}"))
|
|
133
120
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
"description": f"Definición de la tabla {table_name}.",
|
|
138
|
-
"fields": []
|
|
139
|
-
}
|
|
140
|
-
if schema_object_name:
|
|
141
|
-
json_dict["description"] += f"Los detalles de cada campo están en el objeto **`{schema_object_name}`**."
|
|
121
|
+
result = session.execute(text(query))
|
|
122
|
+
if commit:
|
|
123
|
+
session.commit()
|
|
142
124
|
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
|
|
125
|
+
if result.returns_rows:
|
|
126
|
+
# Convert SQLAlchemy rows to list of dicts immediately
|
|
127
|
+
cols = result.keys()
|
|
128
|
+
return [dict(zip(cols, row)) for row in result.fetchall()]
|
|
146
129
|
|
|
147
|
-
|
|
148
|
-
for col in columns:
|
|
149
|
-
name = col["name"]
|
|
130
|
+
return {'rowcount': result.rowcount}
|
|
150
131
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
continue
|
|
132
|
+
def commit(self):
|
|
133
|
+
self.get_session().commit()
|
|
154
134
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"type": str(col["type"]),
|
|
158
|
-
})
|
|
135
|
+
def rollback(self):
|
|
136
|
+
self.get_session().rollback()
|
|
159
137
|
|
|
160
|
-
|
|
138
|
+
# -- schema methods ----
|
|
139
|
+
def get_database_structure(self) -> dict:
|
|
140
|
+
inspector = inspect(self._engine)
|
|
141
|
+
structure = {}
|
|
142
|
+
for table in inspector.get_table_names(schema=self.schema):
|
|
143
|
+
columns_data = []
|
|
144
|
+
|
|
145
|
+
# get columns
|
|
146
|
+
try:
|
|
147
|
+
columns = inspector.get_columns(table, schema=self.schema)
|
|
148
|
+
# Obtener PKs para marcarlas
|
|
149
|
+
pks = inspector.get_pk_constraint(table, schema=self.schema).get('constrained_columns', [])
|
|
150
|
+
|
|
151
|
+
for col in columns:
|
|
152
|
+
columns_data.append({
|
|
153
|
+
"name": col['name'],
|
|
154
|
+
"type": str(col['type']),
|
|
155
|
+
"nullable": col.get('nullable', True),
|
|
156
|
+
"pk": col['name'] in pks
|
|
157
|
+
})
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logging.warning(f"Could not inspect columns for table {table}: {e}")
|
|
160
|
+
|
|
161
|
+
structure[table] = {
|
|
162
|
+
"columns": columns_data
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return structure
|
|
@@ -26,6 +26,13 @@ class DocumentRepo:
|
|
|
26
26
|
|
|
27
27
|
return self.session.query(Document).filter_by(company_id=company_id, filename=filename).first()
|
|
28
28
|
|
|
29
|
+
def get_by_hash(self, company_id: int, file_hash: str) -> Document:
|
|
30
|
+
"""Find a document by its content hash within a company."""
|
|
31
|
+
if not company_id or not file_hash:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
return self.session.query(Document).filter_by(company_id=company_id, hash=file_hash).first()
|
|
35
|
+
|
|
29
36
|
def get_by_id(self, document_id: int) -> Document:
|
|
30
37
|
if not document_id:
|
|
31
38
|
return None
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from iatoolkit.common.interfaces.asset_storage import AssetRepository, AssetType
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FileSystemAssetRepository(AssetRepository):
|
|
6
|
+
def _get_path(self, company_short_name: str, asset_type: AssetType, filename: str = "") -> Path:
|
|
7
|
+
return Path("companies") / company_short_name / asset_type.value / filename
|
|
8
|
+
|
|
9
|
+
def exists(self, company_short_name: str, asset_type: AssetType, filename: str) -> bool:
|
|
10
|
+
return self._get_path(company_short_name, asset_type, filename).is_file()
|
|
11
|
+
|
|
12
|
+
def read_text(self, company_short_name: str, asset_type: AssetType, filename: str) -> str:
|
|
13
|
+
path = self._get_path(company_short_name, asset_type, filename)
|
|
14
|
+
if not path.is_file():
|
|
15
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
16
|
+
return path.read_text(encoding="utf-8")
|
|
17
|
+
|
|
18
|
+
def list_files(self, company_short_name: str, asset_type: AssetType, extension: str = None) -> list[str]:
|
|
19
|
+
directory = self._get_path(company_short_name, asset_type)
|
|
20
|
+
if not directory.exists():
|
|
21
|
+
return []
|
|
22
|
+
files = [f.name for f in directory.iterdir() if f.is_file()]
|
|
23
|
+
if extension:
|
|
24
|
+
files = [f for f in files if f.endswith(extension)]
|
|
25
|
+
return files
|
|
26
|
+
|
|
27
|
+
def write_text(self, company_short_name: str, asset_type: AssetType, filename: str, content: str) -> None:
|
|
28
|
+
path = self._get_path(company_short_name, asset_type, filename)
|
|
29
|
+
# Ensure the directory exists (e.g. creating a new company structure)
|
|
30
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
path.write_text(content, encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
def delete(self, company_short_name: str, asset_type: AssetType, filename: str) -> None:
|
|
34
|
+
path = self._get_path(company_short_name, asset_type, filename)
|
|
35
|
+
if path.exists():
|
|
36
|
+
path.unlink()
|
iatoolkit/repositories/models.py
CHANGED
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
from sqlalchemy import Column, Integer, BigInteger, String, DateTime, Enum, Text, JSON, Boolean, ForeignKey, Table
|
|
7
7
|
from sqlalchemy.orm import DeclarativeBase
|
|
8
|
-
from sqlalchemy.orm import relationship, class_mapper
|
|
8
|
+
from sqlalchemy.orm import relationship, class_mapper
|
|
9
9
|
from sqlalchemy.sql import func
|
|
10
|
+
from sqlalchemy import UniqueConstraint
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
from pgvector.sqlalchemy import Vector
|
|
12
|
-
from enum import Enum as PyEnum
|
|
13
|
-
import secrets
|
|
14
13
|
import enum
|
|
15
14
|
|
|
16
15
|
|
|
@@ -27,6 +26,7 @@ user_company = Table('iat_user_company',
|
|
|
27
26
|
Column('company_id', Integer,
|
|
28
27
|
ForeignKey('iat_companies.id',ondelete='CASCADE'),
|
|
29
28
|
primary_key=True),
|
|
29
|
+
Column('role', String, nullable=True, default='user'),
|
|
30
30
|
Column('created_at', DateTime, default=datetime.now)
|
|
31
31
|
)
|
|
32
32
|
|
|
@@ -36,7 +36,8 @@ class ApiKey(Base):
|
|
|
36
36
|
|
|
37
37
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
38
38
|
company_id = Column(Integer, ForeignKey('iat_companies.id', ondelete='CASCADE'), nullable=False)
|
|
39
|
-
|
|
39
|
+
key_name = Column(String, nullable=False)
|
|
40
|
+
key = Column(String, unique=True, nullable=False, index=True) # La API Key en sí
|
|
40
41
|
is_active = Column(Boolean, default=True, nullable=False)
|
|
41
42
|
created_at = Column(DateTime, default=datetime.now)
|
|
42
43
|
last_used_at = Column(DateTime, nullable=True) # Opcional: para rastrear uso
|
|
@@ -49,12 +50,9 @@ class Company(Base):
|
|
|
49
50
|
__tablename__ = 'iat_companies'
|
|
50
51
|
|
|
51
52
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
52
|
-
short_name = Column(String
|
|
53
|
-
name = Column(String
|
|
53
|
+
short_name = Column(String, nullable=False, unique=True, index=True)
|
|
54
|
+
name = Column(String, nullable=False)
|
|
54
55
|
|
|
55
|
-
# encrypted api-key
|
|
56
|
-
openai_api_key = Column(String, nullable=True)
|
|
57
|
-
gemini_api_key = Column(String, nullable=True)
|
|
58
56
|
parameters = Column(JSON, nullable=True)
|
|
59
57
|
created_at = Column(DateTime, default=datetime.now)
|
|
60
58
|
|
|
@@ -78,13 +76,18 @@ class Company(Base):
|
|
|
78
76
|
back_populates="company",
|
|
79
77
|
cascade="all, delete-orphan")
|
|
80
78
|
|
|
81
|
-
tasks = relationship("Task", back_populates="company")
|
|
82
79
|
feedbacks = relationship("UserFeedback",
|
|
83
80
|
back_populates="company",
|
|
84
81
|
cascade="all, delete-orphan")
|
|
85
82
|
prompts = relationship("Prompt",
|
|
86
83
|
back_populates="company",
|
|
87
84
|
cascade="all, delete-orphan")
|
|
85
|
+
collection_types = relationship(
|
|
86
|
+
"CollectionType",
|
|
87
|
+
back_populates="company",
|
|
88
|
+
cascade="all, delete-orphan"
|
|
89
|
+
)
|
|
90
|
+
|
|
88
91
|
|
|
89
92
|
def to_dict(self):
|
|
90
93
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
@@ -95,13 +98,13 @@ class User(Base):
|
|
|
95
98
|
__tablename__ = 'iat_users'
|
|
96
99
|
|
|
97
100
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
98
|
-
email = Column(String
|
|
99
|
-
first_name = Column(String
|
|
100
|
-
last_name = Column(String
|
|
101
|
+
email = Column(String, unique=True, nullable=False)
|
|
102
|
+
first_name = Column(String, nullable=False)
|
|
103
|
+
last_name = Column(String, nullable=False)
|
|
101
104
|
created_at = Column(DateTime, default=datetime.now)
|
|
102
105
|
password = Column(String, nullable=False)
|
|
103
106
|
verified = Column(Boolean, nullable=False, default=False)
|
|
104
|
-
preferred_language = Column(String
|
|
107
|
+
preferred_language = Column(String, nullable=True)
|
|
105
108
|
verification_url = Column(String, nullable=True)
|
|
106
109
|
temp_code = Column(String, nullable=True)
|
|
107
110
|
|
|
@@ -133,7 +136,7 @@ class Tool(Base):
|
|
|
133
136
|
company_id = Column(Integer,
|
|
134
137
|
ForeignKey('iat_companies.id',ondelete='CASCADE'),
|
|
135
138
|
nullable=True)
|
|
136
|
-
name = Column(String
|
|
139
|
+
name = Column(String, nullable=False)
|
|
137
140
|
system_function = Column(Boolean, default=False)
|
|
138
141
|
description = Column(Text, nullable=False)
|
|
139
142
|
parameters = Column(JSON, nullable=False)
|
|
@@ -146,6 +149,29 @@ class Tool(Base):
|
|
|
146
149
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
147
150
|
|
|
148
151
|
|
|
152
|
+
class DocumentStatus(str, enum.Enum):
|
|
153
|
+
PENDING = "pending"
|
|
154
|
+
PROCESSING = "processing"
|
|
155
|
+
ACTIVE = "active"
|
|
156
|
+
FAILED = "failed"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class CollectionType(Base):
|
|
160
|
+
"""Defines the available document collections/categories for a company."""
|
|
161
|
+
__tablename__ = 'iat_collection_types'
|
|
162
|
+
|
|
163
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
164
|
+
company_id = Column(Integer, ForeignKey('iat_companies.id', ondelete='CASCADE'), nullable=False)
|
|
165
|
+
name = Column(String, nullable=False) # e.g., "Contracts", "Manuals"
|
|
166
|
+
|
|
167
|
+
# description - optional for the LLM to understand what's inside'
|
|
168
|
+
description = Column(Text, nullable=True)
|
|
169
|
+
|
|
170
|
+
__table_args__ = (UniqueConstraint('company_id', 'name', name='uix_company_collection_name'),)
|
|
171
|
+
|
|
172
|
+
company = relationship("Company", back_populates="collection_types")
|
|
173
|
+
documents = relationship("Document", back_populates="collection_type")
|
|
174
|
+
|
|
149
175
|
class Document(Base):
|
|
150
176
|
"""Represents a file or document uploaded by a company for context."""
|
|
151
177
|
__tablename__ = 'iat_documents'
|
|
@@ -153,17 +179,32 @@ class Document(Base):
|
|
|
153
179
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
154
180
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
155
181
|
ondelete='CASCADE'), nullable=False)
|
|
156
|
-
|
|
182
|
+
collection_type_id = Column(Integer, ForeignKey('iat_collection_types.id', ondelete='SET NULL'), nullable=True)
|
|
183
|
+
|
|
184
|
+
user_identifier = Column(String, nullable=True)
|
|
185
|
+
filename = Column(String, nullable=False, index=True)
|
|
186
|
+
status = Column(Enum(DocumentStatus), default=DocumentStatus.PENDING, nullable=False)
|
|
157
187
|
meta = Column(JSON, nullable=True)
|
|
158
188
|
created_at = Column(DateTime, default=datetime.now)
|
|
159
189
|
content = Column(Text, nullable=False)
|
|
160
190
|
content_b64 = Column(Text, nullable=False)
|
|
161
191
|
|
|
192
|
+
# For feedback if OCR or embedding fails
|
|
193
|
+
error_message = Column(Text, nullable=True)
|
|
194
|
+
|
|
195
|
+
# Hash column for deduplication (SHA-256 hex digest)
|
|
196
|
+
hash = Column(String(64), index=True, nullable=True)
|
|
197
|
+
|
|
162
198
|
company = relationship("Company", back_populates="documents")
|
|
199
|
+
collection_type = relationship("CollectionType", back_populates="documents")
|
|
163
200
|
|
|
164
201
|
def to_dict(self):
|
|
165
202
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
166
203
|
|
|
204
|
+
@property
|
|
205
|
+
def description(self):
|
|
206
|
+
collection_type = self.collection_type.name if self.collection_type else None
|
|
207
|
+
return f"Document ID {self.id}: {self.filename} ({collection_type})"
|
|
167
208
|
|
|
168
209
|
class LLMQuery(Base):
|
|
169
210
|
"""Logs a query made to the LLM, including input, output, and metadata."""
|
|
@@ -172,8 +213,7 @@ class LLMQuery(Base):
|
|
|
172
213
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
173
214
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
174
215
|
ondelete='CASCADE'), nullable=False)
|
|
175
|
-
user_identifier = Column(String
|
|
176
|
-
task_id = Column(Integer, default=0, nullable=True)
|
|
216
|
+
user_identifier = Column(String, nullable=False)
|
|
177
217
|
query = Column(Text, nullable=False)
|
|
178
218
|
output = Column(Text, nullable=False)
|
|
179
219
|
response = Column(JSON, nullable=True, default={})
|
|
@@ -184,7 +224,6 @@ class LLMQuery(Base):
|
|
|
184
224
|
created_at = Column(DateTime, default=datetime.now)
|
|
185
225
|
|
|
186
226
|
company = relationship("Company", back_populates="llm_queries")
|
|
187
|
-
tasks = relationship("Task", back_populates="llm_query")
|
|
188
227
|
|
|
189
228
|
def to_dict(self):
|
|
190
229
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
@@ -203,59 +242,13 @@ class VSDoc(Base):
|
|
|
203
242
|
|
|
204
243
|
# the size of this vector should be set depending on the embedding model used
|
|
205
244
|
# for OpenAI is 1536, and for huggingface is 384
|
|
206
|
-
embedding = Column(Vector(
|
|
245
|
+
embedding = Column(Vector(384), nullable=False)
|
|
207
246
|
|
|
208
247
|
company = relationship("Company", back_populates="vsdocs")
|
|
209
248
|
|
|
210
249
|
def to_dict(self):
|
|
211
250
|
return {column.key: getattr(self, column.key) for column in class_mapper(self.__class__).columns}
|
|
212
251
|
|
|
213
|
-
class TaskStatus(PyEnum):
|
|
214
|
-
"""Enumeration for the possible statuses of a Task."""
|
|
215
|
-
pendiente = "pendiente" # task created and waiting to be executed.
|
|
216
|
-
ejecutado = "ejecutado" # the IA algorithm has been executed.
|
|
217
|
-
aprobada = "aprobada" # validated and approved by human.
|
|
218
|
-
rechazada = "rechazada" # validated and rejected by human.
|
|
219
|
-
fallida = "fallida" # error executing the IA algorithm.
|
|
220
|
-
|
|
221
|
-
class TaskType(Base):
|
|
222
|
-
"""Defines a type of task that can be executed, including its prompt template."""
|
|
223
|
-
__tablename__ = 'iat_task_types'
|
|
224
|
-
|
|
225
|
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
226
|
-
name = Column(String(100), unique=True, nullable=False)
|
|
227
|
-
prompt_template = Column(String(100), nullable=True) # Plantilla de prompt por defecto.
|
|
228
|
-
template_args = Column(JSON, nullable=True) # Argumentos/prefijos de configuración para el template.
|
|
229
|
-
|
|
230
|
-
class Task(Base):
|
|
231
|
-
"""Represents an asynchronous task to be executed by the system, often involving an LLM."""
|
|
232
|
-
__tablename__ = 'iat_tasks'
|
|
233
|
-
|
|
234
|
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
235
|
-
company_id = Column(Integer, ForeignKey("iat_companies.id"))
|
|
236
|
-
|
|
237
|
-
user_id = Column(Integer, nullable=True, default=0)
|
|
238
|
-
task_type_id = Column(Integer, ForeignKey('iat_task_types.id'), nullable=False)
|
|
239
|
-
status = Column(Enum(TaskStatus, name="task_status_enum"),
|
|
240
|
-
default=TaskStatus.pendiente, nullable=False)
|
|
241
|
-
client_data = Column(JSON, nullable=True, default={})
|
|
242
|
-
company_task_id = Column(Integer, nullable=True, default=0)
|
|
243
|
-
execute_at = Column(DateTime, default=datetime.now, nullable=True)
|
|
244
|
-
llm_query_id = Column(Integer, ForeignKey('iat_queries.id'), nullable=True)
|
|
245
|
-
callback_url = Column(String(512), default=None, nullable=True)
|
|
246
|
-
files = Column(JSON, default=[], nullable=True)
|
|
247
|
-
|
|
248
|
-
review_user = Column(String(128), nullable=True, default='')
|
|
249
|
-
review_date = Column(DateTime, nullable=True)
|
|
250
|
-
comment = Column(Text, nullable=True)
|
|
251
|
-
approved = Column(Boolean, nullable=False, default=False)
|
|
252
|
-
|
|
253
|
-
created_at = Column(DateTime, default=datetime.now)
|
|
254
|
-
updated_at = Column(DateTime, default=datetime.now)
|
|
255
|
-
|
|
256
|
-
task_type = relationship("TaskType")
|
|
257
|
-
llm_query = relationship("LLMQuery", back_populates="tasks", uselist=False)
|
|
258
|
-
company = relationship("Company", back_populates="tasks")
|
|
259
252
|
|
|
260
253
|
class UserFeedback(Base):
|
|
261
254
|
"""Stores feedback and ratings submitted by users for specific interactions."""
|
|
@@ -264,7 +257,7 @@ class UserFeedback(Base):
|
|
|
264
257
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
265
258
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
266
259
|
ondelete='CASCADE'), nullable=False)
|
|
267
|
-
user_identifier = Column(String
|
|
260
|
+
user_identifier = Column(String, default='', nullable=True)
|
|
268
261
|
message = Column(Text, nullable=False)
|
|
269
262
|
rating = Column(Integer, nullable=False)
|
|
270
263
|
created_at = Column(DateTime, default=datetime.now)
|
|
@@ -293,12 +286,12 @@ class Prompt(Base):
|
|
|
293
286
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
294
287
|
company_id = Column(Integer, ForeignKey('iat_companies.id',
|
|
295
288
|
ondelete='CASCADE'), nullable=True)
|
|
296
|
-
name = Column(String
|
|
297
|
-
description = Column(String
|
|
298
|
-
filename = Column(String
|
|
289
|
+
name = Column(String, nullable=False)
|
|
290
|
+
description = Column(String, nullable=False)
|
|
291
|
+
filename = Column(String, nullable=False)
|
|
299
292
|
active = Column(Boolean, default=True)
|
|
300
293
|
is_system_prompt = Column(Boolean, default=False)
|
|
301
|
-
order = Column(Integer, nullable=
|
|
294
|
+
order = Column(Integer, nullable=True, default=0)
|
|
302
295
|
category_id = Column(Integer, ForeignKey('iat_prompt_categories.id'), nullable=True)
|
|
303
296
|
custom_fields = Column(JSON, nullable=False, default=[])
|
|
304
297
|
|
|
@@ -314,18 +307,18 @@ class AccessLog(Base):
|
|
|
314
307
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
315
308
|
|
|
316
309
|
timestamp = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
|
317
|
-
company_short_name = Column(String
|
|
318
|
-
user_identifier = Column(String
|
|
310
|
+
company_short_name = Column(String, nullable=False, index=True)
|
|
311
|
+
user_identifier = Column(String, index=True)
|
|
319
312
|
|
|
320
313
|
# Cómo y el Resultado
|
|
321
|
-
auth_type = Column(String
|
|
322
|
-
outcome = Column(String
|
|
323
|
-
reason_code = Column(String
|
|
314
|
+
auth_type = Column(String, nullable=False) # 'local', 'external_api', 'redeem_token', etc.
|
|
315
|
+
outcome = Column(String, nullable=False) # 'success' o 'failure'
|
|
316
|
+
reason_code = Column(String) # Causa de fallo, ej: 'INVALID_CREDENTIALS'
|
|
324
317
|
|
|
325
318
|
# Contexto de la Petición
|
|
326
|
-
source_ip = Column(String
|
|
327
|
-
user_agent_hash = Column(String
|
|
328
|
-
request_path = Column(String
|
|
319
|
+
source_ip = Column(String, nullable=False)
|
|
320
|
+
user_agent_hash = Column(String) # Hash corto del User-Agent
|
|
321
|
+
request_path = Column(String, nullable=False)
|
|
329
322
|
|
|
330
323
|
def __repr__(self):
|
|
331
324
|
return (f"<AccessLog(id={self.id}, company='{self.company_short_name}', "
|