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.
Files changed (71) hide show
  1. iatoolkit/__init__.py +6 -4
  2. iatoolkit/base_company.py +0 -16
  3. iatoolkit/cli_commands.py +3 -14
  4. iatoolkit/common/exceptions.py +1 -0
  5. iatoolkit/common/interfaces/__init__.py +0 -0
  6. iatoolkit/common/interfaces/asset_storage.py +34 -0
  7. iatoolkit/common/interfaces/database_provider.py +43 -0
  8. iatoolkit/common/model_registry.py +159 -0
  9. iatoolkit/common/routes.py +47 -5
  10. iatoolkit/common/util.py +32 -13
  11. iatoolkit/company_registry.py +5 -0
  12. iatoolkit/core.py +51 -20
  13. iatoolkit/infra/connectors/file_connector_factory.py +1 -0
  14. iatoolkit/infra/connectors/s3_connector.py +4 -2
  15. iatoolkit/infra/llm_providers/__init__.py +0 -0
  16. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  17. iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
  18. iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
  19. iatoolkit/infra/llm_proxy.py +235 -134
  20. iatoolkit/infra/llm_response.py +5 -0
  21. iatoolkit/locales/en.yaml +158 -2
  22. iatoolkit/locales/es.yaml +158 -0
  23. iatoolkit/repositories/database_manager.py +52 -47
  24. iatoolkit/repositories/document_repo.py +7 -0
  25. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  26. iatoolkit/repositories/llm_query_repo.py +2 -0
  27. iatoolkit/repositories/models.py +72 -79
  28. iatoolkit/repositories/profile_repo.py +59 -3
  29. iatoolkit/repositories/vs_repo.py +22 -24
  30. iatoolkit/services/company_context_service.py +126 -53
  31. iatoolkit/services/configuration_service.py +299 -73
  32. iatoolkit/services/dispatcher_service.py +21 -3
  33. iatoolkit/services/file_processor_service.py +0 -5
  34. iatoolkit/services/history_manager_service.py +43 -24
  35. iatoolkit/services/knowledge_base_service.py +425 -0
  36. iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +38 -29
  37. iatoolkit/services/load_documents_service.py +26 -48
  38. iatoolkit/services/profile_service.py +32 -4
  39. iatoolkit/services/prompt_service.py +32 -30
  40. iatoolkit/services/query_service.py +51 -26
  41. iatoolkit/services/sql_service.py +122 -74
  42. iatoolkit/services/tool_service.py +26 -11
  43. iatoolkit/services/user_session_context_service.py +115 -63
  44. iatoolkit/static/js/chat_main.js +44 -4
  45. iatoolkit/static/js/chat_model_selector.js +227 -0
  46. iatoolkit/static/js/chat_onboarding_button.js +1 -1
  47. iatoolkit/static/js/chat_reload_button.js +4 -1
  48. iatoolkit/static/styles/chat_iatoolkit.css +58 -2
  49. iatoolkit/static/styles/llm_output.css +34 -1
  50. iatoolkit/system_prompts/query_main.prompt +26 -2
  51. iatoolkit/templates/base.html +13 -0
  52. iatoolkit/templates/chat.html +45 -2
  53. iatoolkit/templates/onboarding_shell.html +0 -1
  54. iatoolkit/views/base_login_view.py +7 -2
  55. iatoolkit/views/chat_view.py +76 -0
  56. iatoolkit/views/configuration_api_view.py +163 -0
  57. iatoolkit/views/load_document_api_view.py +14 -10
  58. iatoolkit/views/login_view.py +8 -3
  59. iatoolkit/views/rag_api_view.py +216 -0
  60. iatoolkit/views/users_api_view.py +33 -0
  61. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/METADATA +4 -4
  62. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/RECORD +66 -58
  63. iatoolkit/repositories/tasks_repo.py +0 -52
  64. iatoolkit/services/search_service.py +0 -55
  65. iatoolkit/services/tasks_service.py +0 -188
  66. iatoolkit/views/tasks_api_view.py +0 -72
  67. iatoolkit/views/tasks_review_api_view.py +0 -55
  68. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/WHEEL +0 -0
  69. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE +0 -0
  70. {iatoolkit-0.91.1.dist-info → iatoolkit-1.7.0.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
  71. {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 | None = None,
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
- def get_all_table_names(self) -> list[str]:
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
- if exclude_columns is None:
129
- exclude_columns = []
130
-
131
- # get all the table columns
132
- columns = inspector.get_columns(table_name, schema=db_schema)
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
- # construct a json dictionary with the table definition
135
- json_dict = {
136
- "table": table_name,
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 db_schema:
144
- json_dict["schema"] = db_schema
145
- json_dict["description"] += f"Pertenece al esquema **`{db_schema}`**."
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
- # now add every column to the json dictionary
148
- for col in columns:
149
- name = col["name"]
130
+ return {'rowcount': result.rowcount}
150
131
 
151
- # omit the excluded columns.
152
- if name in exclude_columns:
153
- continue
132
+ def commit(self):
133
+ self.get_session().commit()
154
134
 
155
- json_dict["fields"].append({
156
- "name": name,
157
- "type": str(col["type"]),
158
- })
135
+ def rollback(self):
136
+ self.get_session().rollback()
159
137
 
160
- return "\n\n" + str(json_dict)
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()
@@ -35,6 +35,8 @@ class LLMQueryRepo:
35
35
  Tool.system_function.is_(True)
36
36
  )
37
37
  )
38
+ # Ordenamos descendente: True (System) va primero, False (Company) va después
39
+ .order_by(Tool.system_function.desc())
38
40
  .all()
39
41
  )
40
42
 
@@ -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, declarative_base
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
- key = Column(String(128), unique=True, nullable=False, index=True) # La API Key en sí
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(20), nullable=False, unique=True, index=True)
53
- name = Column(String(256), nullable=False)
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(80), unique=True, nullable=False)
99
- first_name = Column(String(50), nullable=False)
100
- last_name = Column(String(50), nullable=False)
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(5), nullable=True)
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(255), nullable=False)
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
- filename = Column(String(256), nullable=False, index=True)
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(128), nullable=False)
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(1536), nullable=False)
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(128), default='', nullable=True)
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(64), nullable=False)
297
- description = Column(String(256), nullable=False)
298
- filename = Column(String(256), nullable=False)
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=False, default=0) # Nuevo campo para el orden
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(100), nullable=False, index=True)
318
- user_identifier = Column(String(255), index=True)
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(20), nullable=False) # 'local', 'external_api', 'redeem_token', etc.
322
- outcome = Column(String(10), nullable=False) # 'success' o 'failure'
323
- reason_code = Column(String(50)) # Causa de fallo, ej: 'INVALID_CREDENTIALS'
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(45), nullable=False)
327
- user_agent_hash = Column(String(16)) # Hash corto del User-Agent
328
- request_path = Column(String(255), nullable=False)
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}', "