iatoolkit 0.91.1__py3-none-any.whl → 1.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) 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 +38 -0
  8. iatoolkit/common/model_registry.py +159 -0
  9. iatoolkit/common/routes.py +42 -5
  10. iatoolkit/common/util.py +11 -12
  11. iatoolkit/company_registry.py +5 -0
  12. iatoolkit/core.py +51 -20
  13. iatoolkit/infra/llm_providers/__init__.py +0 -0
  14. iatoolkit/infra/llm_providers/deepseek_adapter.py +278 -0
  15. iatoolkit/infra/{gemini_adapter.py → llm_providers/gemini_adapter.py} +11 -17
  16. iatoolkit/infra/{openai_adapter.py → llm_providers/openai_adapter.py} +41 -7
  17. iatoolkit/infra/llm_proxy.py +235 -134
  18. iatoolkit/infra/llm_response.py +5 -0
  19. iatoolkit/locales/en.yaml +124 -2
  20. iatoolkit/locales/es.yaml +122 -0
  21. iatoolkit/repositories/database_manager.py +44 -19
  22. iatoolkit/repositories/document_repo.py +7 -0
  23. iatoolkit/repositories/filesystem_asset_repository.py +36 -0
  24. iatoolkit/repositories/llm_query_repo.py +2 -0
  25. iatoolkit/repositories/models.py +72 -79
  26. iatoolkit/repositories/profile_repo.py +59 -3
  27. iatoolkit/repositories/vs_repo.py +22 -24
  28. iatoolkit/services/company_context_service.py +88 -39
  29. iatoolkit/services/configuration_service.py +157 -68
  30. iatoolkit/services/dispatcher_service.py +21 -3
  31. iatoolkit/services/file_processor_service.py +0 -5
  32. iatoolkit/services/history_manager_service.py +43 -24
  33. iatoolkit/services/knowledge_base_service.py +412 -0
  34. iatoolkit/{infra/llm_client.py → services/llm_client_service.py} +38 -29
  35. iatoolkit/services/load_documents_service.py +18 -47
  36. iatoolkit/services/profile_service.py +32 -4
  37. iatoolkit/services/prompt_service.py +32 -30
  38. iatoolkit/services/query_service.py +51 -26
  39. iatoolkit/services/sql_service.py +105 -74
  40. iatoolkit/services/tool_service.py +26 -11
  41. iatoolkit/services/user_session_context_service.py +115 -63
  42. iatoolkit/static/js/chat_main.js +44 -4
  43. iatoolkit/static/js/chat_model_selector.js +227 -0
  44. iatoolkit/static/js/chat_onboarding_button.js +1 -1
  45. iatoolkit/static/js/chat_reload_button.js +4 -1
  46. iatoolkit/static/styles/chat_iatoolkit.css +58 -2
  47. iatoolkit/static/styles/llm_output.css +34 -1
  48. iatoolkit/system_prompts/query_main.prompt +26 -2
  49. iatoolkit/templates/base.html +13 -0
  50. iatoolkit/templates/chat.html +44 -2
  51. iatoolkit/templates/onboarding_shell.html +0 -1
  52. iatoolkit/views/base_login_view.py +7 -2
  53. iatoolkit/views/chat_view.py +76 -0
  54. iatoolkit/views/load_company_configuration_api_view.py +49 -0
  55. iatoolkit/views/load_document_api_view.py +14 -10
  56. iatoolkit/views/login_view.py +8 -3
  57. iatoolkit/views/rag_api_view.py +216 -0
  58. iatoolkit/views/users_api_view.py +33 -0
  59. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/METADATA +4 -4
  60. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/RECORD +64 -56
  61. iatoolkit/repositories/tasks_repo.py +0 -52
  62. iatoolkit/services/search_service.py +0 -55
  63. iatoolkit/services/tasks_service.py +0 -188
  64. iatoolkit/views/tasks_api_view.py +0 -72
  65. iatoolkit/views/tasks_review_api_view.py +0 -55
  66. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/WHEEL +0 -0
  67. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE +0 -0
  68. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/licenses/LICENSE_COMMUNITY.md +0 -0
  69. {iatoolkit-0.91.1.dist-info → iatoolkit-1.4.2.dist-info}/top_level.txt +0 -0
iatoolkit/locales/es.yaml CHANGED
@@ -45,6 +45,74 @@ 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
+ company: "Empresa"
66
+ files: "Archivos"
67
+ select_file: "Seleccione un archivo a editar"
68
+ select_category: "Seleccione una categoría"
69
+ editing: "Editando"
70
+ no_file_selected: "Seleccione un archivo"
71
+ new: "Nuevo"
72
+ confirm: "Confirmar"
73
+ cancel: "Cancelar"
74
+ new_file: "Nuevo archivo"
75
+ delete_file: "Borrar archivo"
76
+ rename_file: "Renombrar archivo"
77
+ save_file: "Guardar"
78
+ credentials: "Email y password son requeridos."
79
+ saved_ok: "Guardado correctamente"
80
+ deleted_ok: "Eliminado correctamente"
81
+ company: "Empresa"
82
+ admin_page_title: "Acceso de administración"
83
+ user_manager: "Administración de usuarios"
84
+ load_configuration: "Guardar configuración"
85
+ goto_chat: "Ir al chat"
86
+ logout: "Cerrar sesión"
87
+
88
+ rag:
89
+ ingestion: "Ingesta"
90
+ ingestion_description: "Ingestar documentos en la base de conocimiento."
91
+ workbench: "Area de trabajo"
92
+ documents: "Documentos"
93
+ retrieval_lab: "Laboratorio de recuperación"
94
+ retrieval_description: "Probar la búsqueda semántica y la recuperación de contexto."
95
+ query_placeholder: "Introduce una pregunta para consultar la base de conocimiento..."
96
+ search_button: "Buscar"
97
+ filter: "Filtrar"
98
+ search_results_title: "Listo para probar"
99
+ search_results_description: "Los resultados aparecerán aquí"
100
+ filename: "Nombre de archivo"
101
+ filename_placeholder: "Contiene..."
102
+ user: "Usuario"
103
+ status: "Estado"
104
+ all_status: "Todos los estados"
105
+ status_active: "Activo"
106
+ status_pending: "Pendiente"
107
+ status_processing: "Procesando"
108
+ status_failed: "Fallido"
109
+ created_at: "Creado"
110
+ date_range: "Rango de fechas"
111
+ delete_confirmation: "¿Eliminar archivo?"
112
+ delete_message: "Esta acción no se puede deshacer. El archivo se eliminará permanentemente."
113
+ delete_button: "Eliminar"
114
+ delete_cancel: "Cancelar"
115
+
48
116
 
49
117
  tooltips:
50
118
  history: "Historial con mis consultas"
@@ -52,6 +120,7 @@ ui:
52
120
  feedback: "Tu feedback es muy importante"
53
121
  usage_guide: "Guía de Uso"
54
122
  onboarding: "Cómo preguntar mejor"
123
+ preferences: "Preferencias"
55
124
  logout: "Cerrar sesión"
56
125
  use_prompt_assistant: "Usar Asistente de Prompts"
57
126
  attach_files: "Adjuntar archivos"
@@ -87,6 +156,9 @@ errors:
87
156
  authentication_required: "Autenticación requerida. No se proporcionó cookie de sesión o clave de API."
88
157
  invalid_api_key: "Clave de API inválida o inactiva."
89
158
  no_user_identifier_api: "No se proporcionó user_identifier para la llamada a la API."
159
+ no_company_permissions: "No tiene permisos para administrar esta empresa."
160
+ api_key_name_required: "el parametro api_key_name es requerido."
161
+
90
162
  templates:
91
163
  company_not_found: "Empresa no encontrada."
92
164
  home_template_not_found: "La plantilla de la página de inicio para la empresa '{company_short_name}' no está configurada."
@@ -143,6 +215,20 @@ services:
143
215
  start_query: "Hola, cual es tu pregunta?"
144
216
  mail_change_password: "se envio mail para cambio de clave"
145
217
 
218
+ rag:
219
+ ingestion:
220
+ duplicate: "El documento '{filename}' ya existe para la empresa '{company_short_name}'. Omitiendo ingesta."
221
+ failed: "Error al ingerir el documento: {error}"
222
+ processing_failed: "El procesamiento falló: {error}"
223
+ empty_text: "El texto extraído está vacío."
224
+ search:
225
+ query_required: "La consulta (query) es obligatoria."
226
+ company_not_found: "Empresa '{company_short_name}' no encontrada."
227
+ management:
228
+ delete_success: "Documento eliminado."
229
+ not_found: "Documento no encontrado."
230
+ action_not_found: "Acción '{action}' no encontrada."
231
+
146
232
  flash_messages:
147
233
  password_changed_success: "Tu contraseña ha sido restablecida exitosamente. Ahora puedes iniciar sesión."
148
234
  signup_success: "Registro exitoso. Por favor, revisa tu correo para verificar tu cuenta."
@@ -169,4 +255,40 @@ js_messages:
169
255
  reload_init: "Iniciando recarga de contexto en segundo plano..."
170
256
  no_history_found: "No existe historial de consultas."
171
257
  example: "Ejemplo:"
258
+ show_reasoning: "Ver razonamiento"
259
+ unsaved: "Modificado (no guardado)"
260
+ saved_ok: "Archivo guadado correctamente"
261
+ error_saving: "Error al guardar archivo"
262
+ select_file: "Seleccione un archivo de la lista"
263
+ no_file_selected: "No hay archivo seleccionado"
264
+ select_company: "Seleccione empresa"
265
+ file_created: "Archivo creado correctamente"
266
+ delete_ok: "Archivo eliminado correctamente"
267
+ delete_failed: "Error al eliminar archivo"
268
+ rename_ok: "Archivo renombrado correctamente"
269
+ not_saved: 'No puede guardar'
270
+ invalid_file_name: "Nombre de archivo no válido. Usa solo letras, números, guiones bajos, guiones y puntos."
271
+ config_loaded: "Configuración cargada correctamente."
272
+ config_load_error: "Error cargando confguración."
273
+ search_placeholder: "Buscar usuarios..."
274
+ showing: "Mostrando"
275
+ records: "Registros"
276
+ db_user: "Usuario"
277
+ db_role: "Rol"
278
+ db_verified: "Verificado"
279
+ db_collection: "Colección"
280
+ db_last_access: "ultimo acceso"
281
+ db_filename: "Nombre de archivo"
282
+ db_user: "Usuario"
283
+ db_status: "Estado"
284
+ db_created: "Creado"
285
+ editor_no_file_selected: "No hay archivo seleccionado"
286
+ error_loading: "Error cargando el contenido del archivo"
287
+ cant_load_company: "No puede cargarcompany.yaml"
288
+ config_saved: "Configuración guardada correctamente."
289
+ config_error: "Error guardando configuración"
290
+
291
+
292
+
293
+
172
294
 
@@ -10,13 +10,14 @@ 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
13
14
 
14
15
 
15
- class DatabaseManager:
16
+ class DatabaseManager(DatabaseProvider):
16
17
  @inject
17
18
  def __init__(self,
18
19
  database_url: str,
19
- schema: str | None = None,
20
+ schema: str = 'public',
20
21
  register_pgvector: bool = True):
21
22
  """
22
23
  Inicializa el gestor de la base de datos.
@@ -91,9 +92,6 @@ class DatabaseManager:
91
92
  def get_connection(self):
92
93
  return self._engine.connect()
93
94
 
94
- def get_engine(self):
95
- return self._engine
96
-
97
95
  def create_all(self):
98
96
  # if there is a schema defined, make sure it exists before creating tables
99
97
  backend = self.url.get_backend_name()
@@ -109,40 +107,67 @@ class DatabaseManager:
109
107
  def remove_session(self):
110
108
  self.scoped_session.remove()
111
109
 
110
+ # -- execution methods ----
111
+
112
+ def execute_query(self, query: str, commit: bool = False) -> list[dict] | dict:
113
+ """
114
+ Implementation for Direct SQLAlchemy connection.
115
+ """
116
+ session = self.get_session()
117
+ if self.schema:
118
+ session.execute(text(f"SET search_path TO {self.schema}"))
119
+
120
+ result = session.execute(text(query))
121
+ if commit:
122
+ session.commit()
123
+
124
+ if result.returns_rows:
125
+ # Convert SQLAlchemy rows to list of dicts immediately
126
+ cols = result.keys()
127
+ return [dict(zip(cols, row)) for row in result.fetchall()]
128
+
129
+ return {'rowcount': result.rowcount}
130
+
131
+ def commit(self):
132
+ self.get_session().commit()
133
+
134
+ def rollback(self):
135
+ self.get_session().rollback()
136
+
137
+ # -- schema methods ----
112
138
  def get_all_table_names(self) -> list[str]:
113
139
  # Returns a list of all table names in the database
114
140
  inspector = inspect(self._engine)
115
141
  return inspector.get_table_names(schema=self.schema)
116
142
 
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:
143
+ def get_table_description(self,
144
+ table_name: str,
145
+ schema_object_name: str | None = None,
146
+ exclude_columns: list[str] | None = None) -> str:
122
147
  inspector = inspect(self._engine)
123
148
 
124
149
  # 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}'.")
150
+ if table_name not in inspector.get_table_names(schema=self.schema):
151
+ raise RuntimeError(f"Table '{table_name}' does not exist in database schema '{self.schema}'.")
127
152
 
128
153
  if exclude_columns is None:
129
154
  exclude_columns = []
130
155
 
131
156
  # get all the table columns
132
- columns = inspector.get_columns(table_name, schema=db_schema)
157
+ columns = inspector.get_columns(table_name, schema=self.schema)
133
158
 
134
159
  # construct a json dictionary with the table definition
135
160
  json_dict = {
136
161
  "table": table_name,
137
- "description": f"Definición de la tabla {table_name}.",
162
+ "schema": self.schema,
163
+ "description": f"The table belongs to the **`{self.schema}`** schema.",
138
164
  "fields": []
139
165
  }
140
- if schema_object_name:
141
- json_dict["description"] += f"Los detalles de cada campo están en el objeto **`{schema_object_name}`**."
142
166
 
143
- if db_schema:
144
- json_dict["schema"] = db_schema
145
- json_dict["description"] += f"Pertenece al esquema **`{db_schema}`**."
167
+ if schema_object_name:
168
+ json_dict["description"] += (
169
+ f"The meaning of each field in this table is detailed in the **`{schema_object_name}`** object."
170
+ )
146
171
 
147
172
  # now add every column to the json dictionary
148
173
  for col in columns:
@@ -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}', "