From c13240b481922be9fc597f953aeaf9bbde62cadd Mon Sep 17 00:00:00 2001 From: egutierrez Date: Wed, 14 May 2025 02:06:33 +0200 Subject: [PATCH] Notas y bibliotecas funcionando --- .../api/v1/endpoints/text_manager_endpoint.py | 18 ++- backend/services/text_manager_srvc.py | 49 +++++-- frontend/src/pages/Biblioteca.tsx | 134 ++++++++++++++---- Apikeys.ipynb => notebooks/Apikeys.ipynb | 11 +- .../Apikeys_embedding.ipynb | 0 .../Credenciales.ipynb | 0 .../Encriptacion.ipynb | 0 .../Pruebas_notas.ipynb | 0 .../github_tutorial.ipynb | 0 prueba_loop_agente.py | 2 +- src/ArquitectureLayer/Model.py | 10 +- src/Llms/MCPs/MCPStdioServer.py | 65 --------- src/Llms/MCPs/McpServers/cliente_prueba.py | 15 ++ src/Llms/MCPs/McpServers/server_prueba.py | 30 ++++ src/Llms/Memory/postgres_MemoryConv.py | 2 +- src/Llms/Modelos/Openai_model.py | 12 +- src/TextManager/biblioteca.py | 20 +-- src/TextManager/notas_mmr.py | 66 +++++---- 18 files changed, 266 insertions(+), 168 deletions(-) rename Apikeys.ipynb => notebooks/Apikeys.ipynb (97%) rename Apikeys_embedding.ipynb => notebooks/Apikeys_embedding.ipynb (100%) rename Credenciales.ipynb => notebooks/Credenciales.ipynb (100%) rename Encriptacion.ipynb => notebooks/Encriptacion.ipynb (100%) rename Pruebas_notas.ipynb => notebooks/Pruebas_notas.ipynb (100%) rename github_tutorial.ipynb => notebooks/github_tutorial.ipynb (100%) delete mode 100644 src/Llms/MCPs/MCPStdioServer.py create mode 100644 src/Llms/MCPs/McpServers/cliente_prueba.py create mode 100644 src/Llms/MCPs/McpServers/server_prueba.py diff --git a/backend/api/v1/endpoints/text_manager_endpoint.py b/backend/api/v1/endpoints/text_manager_endpoint.py index 3ca3188..3670ab2 100644 --- a/backend/api/v1/endpoints/text_manager_endpoint.py +++ b/backend/api/v1/endpoints/text_manager_endpoint.py @@ -3,6 +3,7 @@ from fastapi import Path from backend.schemas.text_manager_schema import BibliotecaInput, NotaInput +from fastapi.concurrency import run_in_threadpool from backend.db.conexion import get_conexion from backend.services.text_manager_srvc import * from src.ConexionSql.Postgres_conexion import PostgresConexion @@ -10,21 +11,24 @@ from src.ConexionSql.Postgres_conexion import PostgresConexion router = APIRouter() -@router.post("/", summary="Crear una nueva biblioteca") -def crear_biblioteca_endpoint( +@router.post("/biblioteca", summary="Crear una nueva biblioteca") +async def crear_biblioteca_endpoint( data: BibliotecaInput, conexion: PostgresConexion = Depends(get_conexion) ): try: - return crear_biblioteca( - nombre_biblioteca=data.nombre_biblioteca, - descripcion=data.descripcion, - conexion=conexion + return await run_in_threadpool( + crear_biblioteca, + data.nombre_biblioteca, + conexion, + data.descripcion, ) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) except Exception as e: - raise HTTPException(status_code=500, detail="Error interno al crear la biblioteca") + raise HTTPException(status_code=500, detail=f"Error interno al crear la biblioteca: {str(e)}") + + @router.get("/list", summary="Listar todas las bibliotecas") def listar_todas_bibliotecas( diff --git a/backend/services/text_manager_srvc.py b/backend/services/text_manager_srvc.py index fbc0e29..2a00117 100644 --- a/backend/services/text_manager_srvc.py +++ b/backend/services/text_manager_srvc.py @@ -10,24 +10,45 @@ from backend.schemas.text_manager_schema import NotaInput -def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str): - cred_repo = OpenAICredencialRepo(conexion) - credencial = cred_repo.get_by_id("OPAK20250510-ac2cea8af3110632314") +def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str = None): + print("[INICIO] Creando biblioteca...") - embedder = OpenAIEmbedder(credencial, model="text-embedding-3-large") - biblioteca = Biblioteca(nombre=nombre_biblioteca, - embedder=embedder, - descripcion=descripcion) + try: + print("[Paso 1] Obteniendo credencial...") + cred_repo = OpenAICredencialRepo(conexion) + credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014") + print("[OK] Credencial obtenida:", credencial.titulo if credencial else "❌ None") - repo = BibliotecaRepo(conexion) - repo.add(biblioteca=biblioteca) + print("[Paso 2] Instanciando embedder...") + embedder = OpenAIEmbedder(credencial, model="text-embedding-3-large") + print("[OK] Embedder instanciado") - biblioteca.generar_modelo_notas(conexion) + print("[Paso 3] Instanciando biblioteca...") + biblioteca = Biblioteca( + nombre=nombre_biblioteca, + embedder=embedder, + descripcion=descripcion + ) + print(f"[OK] Biblioteca instanciada con ID: {biblioteca.id}") - return { - "mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.", - "id": biblioteca.id - } + print("[Paso 4] Guardando en base de datos...") + repo = BibliotecaRepo(conexion) + repo.add(biblioteca=biblioteca) + print("[OK] Biblioteca guardada") + + print("[Paso 5] Generando modelo de notas...") + biblioteca.generar_modelo_notas(conexion) + print("[OK] Modelo de notas generado") + + print("[FIN] Biblioteca creada correctamente") + return { + "mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.", + "id": biblioteca.id + } + + except Exception as e: + print("[ERROR] Ocurrió una excepción:", str(e)) + raise def listar_bibliotecas(conexion: PostgresConexion) -> list[dict]: diff --git a/frontend/src/pages/Biblioteca.tsx b/frontend/src/pages/Biblioteca.tsx index 515e664..2b941bc 100644 --- a/frontend/src/pages/Biblioteca.tsx +++ b/frontend/src/pages/Biblioteca.tsx @@ -12,6 +12,7 @@ import { Modal, Box, Loader, + Textarea } from '@mantine/core'; import { AppShellWithMenu } from '../components/Appshell/Appshell'; import axios from 'axios'; @@ -38,6 +39,10 @@ export function Biblioteca() { const [loadingNotas, setLoadingNotas] = useState(false); const [notaEnEdicion, setNotaEnEdicion] = useState(null); const [modalEditarAbierto, setModalEditarAbierto] = useState(false); + const [modalNuevaBiblio, setModalNuevaBiblio] = useState(false); + const [nombreBiblio, setNombreBiblio] = useState(''); + const [descripcionBiblio, setDescripcionBiblio] = useState(''); + const [loadingNuevaBiblio, setLoadingNuevaBiblio] = useState(false); const fetchBibliotecas = async () => { @@ -63,6 +68,31 @@ export function Biblioteca() { } }; +const crearBiblioteca = async () => { + setLoadingNuevaBiblio(true); // 🔄 Activa el loader en el botón + try { + // Llamada a backend + await axios.post('/api/v1/text_manager/biblioteca', { + nombre_biblioteca: nombreBiblio, + descripcion: descripcionBiblio, + }); + + // 🧼 Limpia formularios + setNombreBiblio(''); + setDescripcionBiblio(''); + + // 🔒 Cierra el modal + setModalNuevaBiblio(false); + + // 🔄 Refresca la lista de bibliotecas + await fetchBibliotecas(); + } catch (error) { + console.error('❌ Error al crear biblioteca:', error); + } finally { + setLoadingNuevaBiblio(false); // ✅ Apaga el loader + } +}; + const agregarNota = async () => { if (!bibliotecaSeleccionada) return; @@ -101,7 +131,14 @@ export function Biblioteca() { if (!bibliotecaSeleccionada) return; try { await axios.delete(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaId}`); - await fetchBibliotecas(); + + // Solo actualiza la biblioteca actual + const nuevasNotas = await axios.get(`/api/v1/text_manager/nota/list/${bibliotecaSeleccionada.id}`); + const nuevasBibliotecas = bibliotecas.map((b) => + b.id === bibliotecaSeleccionada.id ? { ...b, notas: nuevasNotas.data as Nota[] } : b + ); + setBibliotecas(nuevasBibliotecas); + setBibliotecaSeleccionada(nuevasBibliotecas.find(b => b.id === bibliotecaSeleccionada.id) || null); } catch (error) { console.error("Error al eliminar nota:", error); } @@ -140,9 +177,9 @@ export function Biblioteca() { - + + + {bibliotecas.map((biblio) => ( - - - {nota.titulo} - {nota.texto} // Fin de notas en cards @@ -218,9 +252,7 @@ export function Biblioteca() { ) : ( Selecciona una biblioteca - + )} @@ -234,8 +266,10 @@ export function Biblioteca() { value={tituloNota} onChange={(event) => setTituloNota(event.currentTarget.value)} /> - setContenidoNota(event.currentTarget.value)} /> @@ -253,16 +287,60 @@ export function Biblioteca() { setNotaEnEdicion((prev) => (prev ? { ...prev, titulo: e.currentTarget.value } : null)) } /> - setNotaEnEdicion((prev) => (prev ? { ...prev, texto: e.currentTarget.value } : null)) } /> - + + + + + + {/* Modal para crear una biblioteca */} + setModalNuevaBiblio(false)} + title="Crear nueva biblioteca" + > + + setNombreBiblio(e.currentTarget.value)} + disabled={loadingNuevaBiblio} + /> + setDescripcionBiblio(e.currentTarget.value)} + disabled={loadingNuevaBiblio} + /> + + + + + ); diff --git a/Apikeys.ipynb b/notebooks/Apikeys.ipynb similarity index 97% rename from Apikeys.ipynb rename to notebooks/Apikeys.ipynb index afc566b..4a96cec 100644 --- a/Apikeys.ipynb +++ b/notebooks/Apikeys.ipynb @@ -28,7 +28,7 @@ { "data": { "text/plain": [ - "'OPAK20250510-ac2cea8af3110632314'" + "'OPAK20250513-61b29978b7604031014'" ] }, "execution_count": 2, @@ -37,8 +37,11 @@ } ], "source": [ - "# apikey_gpt = OpenAICredencial(titulo=\"Credencial_enmanuel_gpt\")\n", - "# repo.add(apikey_gpt)" + "apikey_gpt = OpenAICredencial(titulo=\"Credencial_enmanuel_gpt\",\n", + " api_key=\"\")\n", + "\n", + "\n", + "repo.add(apikey_gpt)" ] }, { @@ -56,7 +59,7 @@ } ], "source": [ - "credencial_openai = repo.get_by_id('OPAK20250510-ac2cea8af3110632314')\n", + "credencial_openai = repo.get_by_id('OPAK20250513-61b29978b7604031014')\n", "print(f\"✅ Credencial: {credencial_openai.titulo}\")" ] }, diff --git a/Apikeys_embedding.ipynb b/notebooks/Apikeys_embedding.ipynb similarity index 100% rename from Apikeys_embedding.ipynb rename to notebooks/Apikeys_embedding.ipynb diff --git a/Credenciales.ipynb b/notebooks/Credenciales.ipynb similarity index 100% rename from Credenciales.ipynb rename to notebooks/Credenciales.ipynb diff --git a/Encriptacion.ipynb b/notebooks/Encriptacion.ipynb similarity index 100% rename from Encriptacion.ipynb rename to notebooks/Encriptacion.ipynb diff --git a/Pruebas_notas.ipynb b/notebooks/Pruebas_notas.ipynb similarity index 100% rename from Pruebas_notas.ipynb rename to notebooks/Pruebas_notas.ipynb diff --git a/github_tutorial.ipynb b/notebooks/github_tutorial.ipynb similarity index 100% rename from github_tutorial.ipynb rename to notebooks/github_tutorial.ipynb diff --git a/prueba_loop_agente.py b/prueba_loop_agente.py index 614e536..f894132 100644 --- a/prueba_loop_agente.py +++ b/prueba_loop_agente.py @@ -13,7 +13,7 @@ from src.Llms.Memory.postgres_MemoryConv import MemoryConvPostgres conexion_admin = PostgresConexion(db_credencial) repo = OpenAICredencialRepo(conexion_admin) -credencial_openai = repo.get_by_id(1) +credencial_openai = repo.get_by_id("OPAK20250513-61b29978b7604031014") cliente = OpenAICliente(credencial_openai) diff --git a/src/ArquitectureLayer/Model.py b/src/ArquitectureLayer/Model.py index 24d46a5..47dbd4c 100644 --- a/src/ArquitectureLayer/Model.py +++ b/src/ArquitectureLayer/Model.py @@ -48,10 +48,16 @@ class Model_base: def __json__(self) -> dict: """Devuelve una representación JSON serializable (dict plano).""" out = {} + + # Prevención de error: solo ejecuta si __table__ existe + if not hasattr(self, "__table__"): + return out + for attr in self.__table__.columns: - val = getattr(self, attr.name) + val = getattr(self, attr.name, None) if isinstance(val, datetime): out[attr.name] = val.isoformat() else: out[attr.name] = val - return out + + return out \ No newline at end of file diff --git a/src/Llms/MCPs/MCPStdioServer.py b/src/Llms/MCPs/MCPStdioServer.py deleted file mode 100644 index 8977df6..0000000 --- a/src/Llms/MCPs/MCPStdioServer.py +++ /dev/null @@ -1,65 +0,0 @@ -import asyncio -import os -from typing import Optional, List, Dict -from contextlib import AsyncExitStack -from mcp import ClientSession, StdioServerParameters -from mcp.client.stdio import stdio_client -from mcp.types import Tool - -class MCPStdioServer: - def __init__( - self, - name: str, - command: str, - args: List[str], - env: Optional[Dict[str, str]] = None - ): - self.name = name - self.command = command - self.args = args - self.env = env or os.environ.copy() - self.exit_stack = AsyncExitStack() - self.session: Optional[ClientSession] = None - self.tools: List[Tool] = [] - - async def start(self): - # Configurar el bucle de eventos Proactor en Windows si es necesario - if os.name == "nt": - asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) - - server_params = StdioServerParameters( - command=self.command, - args=self.args, - env=self.env - ) - - # Iniciar el transporte y establecer la sesión - stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) - read, write = stdio_transport - - self.session = await self.exit_stack.enter_async_context(ClientSession(read, write)) - await self.session.initialize() - response = await self.session.list_tools() - self.tools = response.tools - - if self.tools: - print(f"[{self.name}] Servidor iniciado con herramientas:") - for tool in self.tools: - nombre = getattr(tool, "name", "[sin nombre]") - descripcion = getattr(tool, "description", "[sin descripción]") - print(f" - {nombre} - {descripcion}") - else: - print(f"[{self.name}] Servidor iniciado, pero no se detectaron herramientas.") - - async def call_tool(self, tool_name: str, arguments: Dict): - if not self.session: - raise RuntimeError("La sesión no está inicializada.") - result = await self.session.call_tool(tool_name, arguments) - return result.content - - def get_tool_names(self) -> List[str]: - return [tool.name for tool in self.tools] - - async def stop(self): - await self.exit_stack.aclose() - print(f"[{self.name}] Servidor detenido.") diff --git a/src/Llms/MCPs/McpServers/cliente_prueba.py b/src/Llms/MCPs/McpServers/cliente_prueba.py new file mode 100644 index 0000000..27ae088 --- /dev/null +++ b/src/Llms/MCPs/McpServers/cliente_prueba.py @@ -0,0 +1,15 @@ +from fastmcp import Client +import asyncio + +async def main(): + + async with Client("http://127.0.0.1:8080/mcp") as client: + tools = await client.list_tools() + for tool in tools: + print(f"🔧 {tool.name} - {tool.description or 'sin descripción'}") + + + client.call_tool_mcp() + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/src/Llms/MCPs/McpServers/server_prueba.py b/src/Llms/MCPs/McpServers/server_prueba.py new file mode 100644 index 0000000..93828b7 --- /dev/null +++ b/src/Llms/MCPs/McpServers/server_prueba.py @@ -0,0 +1,30 @@ +# archivo: sse_server.py + +from fastmcp.server import FastMCP +import asyncio +from fastmcp import Client + +# Crear la instancia del servidor +server = FastMCP( + name="ServidorSSE", + instructions="Este servidor expone herramientas de prueba.", +) + +# Herramienta 1: saludar +@server.tool(name="saludar", description="Saluda a una persona por su nombre.") +def saludar(nombre: str) -> str: + return f"¡Hola, {nombre}!" + +# Herramienta 2: espera asíncrona +@server.tool(name="esperar", description="Espera N segundos y responde.") +async def esperar(segundos: int) -> str: + await asyncio.sleep(segundos) + return f"Esperé {segundos} segundos como me pediste." + +# Punto de entrada para ejecutarlo por SSE +if __name__ == "__main__": + server.run( + transport="streamable-http", # <-- cambio aquí + host="0.0.0.0", + port=8080, + ) \ No newline at end of file diff --git a/src/Llms/Memory/postgres_MemoryConv.py b/src/Llms/Memory/postgres_MemoryConv.py index c5689ba..ef1ca90 100644 --- a/src/Llms/Memory/postgres_MemoryConv.py +++ b/src/Llms/Memory/postgres_MemoryConv.py @@ -27,7 +27,7 @@ class MemoryConvPostgres(MemoryConvABC): ) # Crea la tabla si no existe - self.metadata.create_all(self.conexion.engine) + self.metadata.create_all(self.conexion._engine) def guardar_turno(self, rol: Literal["user", "assistant"], contenido: str) -> None: stmt = insert(self.tabla).values(rol=rol, contenido=contenido) diff --git a/src/Llms/Modelos/Openai_model.py b/src/Llms/Modelos/Openai_model.py index 5f16f7f..6375d6b 100644 --- a/src/Llms/Modelos/Openai_model.py +++ b/src/Llms/Modelos/Openai_model.py @@ -17,9 +17,11 @@ class ModeloOpenAI(ModeloABC): num_tokens_maximos: int = 512, use_legacy: bool = False ): - id = id if id is not None else GeneradorIDUnico("MOPA").generar() + # Generar ID con prefijo MOPA si no fue proporcionado + self.id = id if id is not None else GeneradorIDUnico("MOPA").generar() + + # Inicializar resto del modelo base super().__init__( - id=id, model=model, temperature=temperature, top_p=top_p, @@ -27,6 +29,8 @@ class ModeloOpenAI(ModeloABC): frecuencia_penalizacion=frecuencia_penalizacion, num_tokens_maximos=num_tokens_maximos ) + + # Asignar cliente e indicadores adicionales self.cliente = cliente self.use_legacy = use_legacy @@ -79,8 +83,8 @@ class ModeloOpenAI(ModeloABC): if stream: async def generador(): - for token in resultado: # ya es un generador del cliente + for token in resultado: yield token return generador() else: - return resultado.choices[0].message.content \ No newline at end of file + return resultado.choices[0].message.content diff --git a/src/TextManager/biblioteca.py b/src/TextManager/biblioteca.py index 538085b..385ff81 100644 --- a/src/TextManager/biblioteca.py +++ b/src/TextManager/biblioteca.py @@ -5,6 +5,7 @@ from src.ConexionSql.Base_conexion import ConexionBase from sqlalchemy import MetaData # Asegúrate de importar esto from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca # Ajusta si es necesario from sqlalchemy import inspect +from src.base import Base class Biblioteca: @@ -38,18 +39,21 @@ class Biblioteca: raise ValueError("Debes proporcionar un 'embedder' o un 'vector_dim' explícito.") def generar_modelo_notas(self, conexion: ConexionBase): - """ - Genera dinámicamente un modelo de notas asociado a esta biblioteca y lo crea en la base de datos. - Previene la creación si la tabla ya existe. - """ nombre_tabla = f"{self.nombre}" + print(f"[Notas] Generando tabla: {nombre_tabla}") + engine = conexion.get_engine() - inspector = inspect(engine) + if inspector.has_table(nombre_tabla): + print(f"[Notas] ❌ Ya existe la tabla {nombre_tabla}") raise ValueError(f"Ya existe una tabla con el nombre '{nombre_tabla}' en la base de datos.") - metadata = MetaData() - tabla, NotaModel = generar_tabla_nota_para_biblioteca(nombre_tabla, self.vector_dim, metadata) - metadata.create_all(engine) + print("[Notas] Generando definición SQL...") + tabla, NotaModel = generar_tabla_nota_para_biblioteca(nombre_tabla, self.vector_dim, Base.metadata) + + print("[Notas] Creando tabla en base de datos...") + Base.metadata.create_all(engine) + print("[Notas] ✔️ Tabla creada") + return NotaModel \ No newline at end of file diff --git a/src/TextManager/notas_mmr.py b/src/TextManager/notas_mmr.py index 341cc5c..d892d80 100644 --- a/src/TextManager/notas_mmr.py +++ b/src/TextManager/notas_mmr.py @@ -1,6 +1,6 @@ import os from dotenv import load_dotenv -from sqlalchemy import Column, String, Table, MetaData +from sqlalchemy import Table, Column, String, Text, MetaData from pgvector.sqlalchemy import Vector from sqlalchemy.orm import registry, Session from src.TextManager.nota import Nota @@ -13,6 +13,9 @@ from src.ArquitectureLayer.Mapper import Mapper_base from src.ArquitectureLayer.Model import Model_base from src.ArquitectureLayer.Repo import Repo_base + +from src.base import Base # Este es tu declarative_base() + # ---------------------- # Cargar .env # ---------------------- @@ -28,47 +31,42 @@ mapper_registry = registry() # FUNCIONES AUXILIARES # ---------------------- + def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int, metadata: MetaData) -> Tuple[Table, type]: """ - Genera una tabla dinámica y modelo ORM para una biblioteca dada, con campos vectoriales. + Genera una tabla dinámica y modelo ORM para una biblioteca dada, con campos vectoriales y campos del sistema. """ - # Normalización robusta del nombre de la tabla - nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower()) + try: + print(f"[INFO] Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}") - tabla = Table( - nombre_tabla, - metadata, - Column("id", String, primary_key=True), - Column("titulo", String, nullable=False), - Column("tags", String), - Column("conexiones", String), - Column("texto", String), - Column("resumen", String), - Column("vector", Vector(vector_dim), nullable=True), - Column("vector_resumen", Vector(vector_dim), nullable=True), - *Model_base.__table__.columns # Añade columnas del sistema si Model_base es declarativo - ) + # Nombre SQL-safe + nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower()) + print(f"[DEBUG] Nombre de tabla SQL-safe: '{nombre_tabla}'") - class NotaModel(Model_base): - """ - Modelo ORM generado dinámicamente para notas de una biblioteca. - """ - def __init__(self, nota): - self.id = nota.id - self.titulo = nota.titulo - self.tags = ",".join(nota.tags) - self.conexiones = ",".join(nota.conexiones) - self.texto = nota.texto - self.resumen = nota.resumen - self.vector = nota.vector - self.vector_resumen = getattr(nota, "vector_resumen", None) + # Modelo ORM dinámico + class NotaModel(Base, Model_base): + __tablename__ = nombre_tabla + __table_args__ = {"extend_existing": True} - # Registrar solo si aún no se ha hecho - if NotaModel.__name__ not in mapper_registry._class_registry: - mapper_registry.map_imperatively(NotaModel, tabla) + id = Column(String, primary_key=True) + titulo = Column(String, nullable=False) + tags = Column(String) + conexiones = Column(String) + texto = Column(Text) + resumen = Column(Text) + vector = Column(Vector(vector_dim), nullable=True) + vector_resumen = Column(Vector(vector_dim), nullable=True) - return tabla, NotaModel + print(f"[INFO] Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'") + print(f"[DEBUG] Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}") + print(f"[DEBUG] Tipos de columnas: {[str(c.type) for c in NotaModel.__table__.columns]}") + print(f"[DEBUG] Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}") + return NotaModel.__table__, NotaModel + + except Exception as e: + print(f"[ERROR] Error al generar la tabla y modelo ORM para '{biblioteca_nombre}': {e}") + raise # ---------------------- # MAPPER