Notas y bibliotecas funcionando

This commit is contained in:
2025-05-14 02:06:33 +02:00
parent bf1814bb8e
commit c13240b481
18 changed files with 266 additions and 168 deletions
@@ -3,6 +3,7 @@ from fastapi import Path
from backend.schemas.text_manager_schema import BibliotecaInput, NotaInput 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.db.conexion import get_conexion
from backend.services.text_manager_srvc import * from backend.services.text_manager_srvc import *
from src.ConexionSql.Postgres_conexion import PostgresConexion from src.ConexionSql.Postgres_conexion import PostgresConexion
@@ -10,21 +11,24 @@ from src.ConexionSql.Postgres_conexion import PostgresConexion
router = APIRouter() router = APIRouter()
@router.post("/", summary="Crear una nueva biblioteca") @router.post("/biblioteca", summary="Crear una nueva biblioteca")
def crear_biblioteca_endpoint( async def crear_biblioteca_endpoint(
data: BibliotecaInput, data: BibliotecaInput,
conexion: PostgresConexion = Depends(get_conexion) conexion: PostgresConexion = Depends(get_conexion)
): ):
try: try:
return crear_biblioteca( return await run_in_threadpool(
nombre_biblioteca=data.nombre_biblioteca, crear_biblioteca,
descripcion=data.descripcion, data.nombre_biblioteca,
conexion=conexion conexion,
data.descripcion,
) )
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as 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") @router.get("/list", summary="Listar todas las bibliotecas")
def listar_todas_bibliotecas( def listar_todas_bibliotecas(
+35 -14
View File
@@ -10,24 +10,45 @@ from backend.schemas.text_manager_schema import NotaInput
def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str): def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str = None):
cred_repo = OpenAICredencialRepo(conexion) print("[INICIO] Creando biblioteca...")
credencial = cred_repo.get_by_id("OPAK20250510-ac2cea8af3110632314")
embedder = OpenAIEmbedder(credencial, model="text-embedding-3-large") try:
biblioteca = Biblioteca(nombre=nombre_biblioteca, print("[Paso 1] Obteniendo credencial...")
embedder=embedder, cred_repo = OpenAICredencialRepo(conexion)
descripcion=descripcion) credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014")
print("[OK] Credencial obtenida:", credencial.titulo if credencial else "❌ None")
repo = BibliotecaRepo(conexion) print("[Paso 2] Instanciando embedder...")
repo.add(biblioteca=biblioteca) 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 { print("[Paso 4] Guardando en base de datos...")
"mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.", repo = BibliotecaRepo(conexion)
"id": biblioteca.id 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]: def listar_bibliotecas(conexion: PostgresConexion) -> list[dict]:
+106 -28
View File
@@ -12,6 +12,7 @@ import {
Modal, Modal,
Box, Box,
Loader, Loader,
Textarea
} from '@mantine/core'; } from '@mantine/core';
import { AppShellWithMenu } from '../components/Appshell/Appshell'; import { AppShellWithMenu } from '../components/Appshell/Appshell';
import axios from 'axios'; import axios from 'axios';
@@ -38,6 +39,10 @@ export function Biblioteca() {
const [loadingNotas, setLoadingNotas] = useState(false); const [loadingNotas, setLoadingNotas] = useState(false);
const [notaEnEdicion, setNotaEnEdicion] = useState<Nota | null>(null); const [notaEnEdicion, setNotaEnEdicion] = useState<Nota | null>(null);
const [modalEditarAbierto, setModalEditarAbierto] = useState(false); 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 () => { 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 () => { const agregarNota = async () => {
if (!bibliotecaSeleccionada) return; if (!bibliotecaSeleccionada) return;
@@ -101,7 +131,14 @@ export function Biblioteca() {
if (!bibliotecaSeleccionada) return; if (!bibliotecaSeleccionada) return;
try { try {
await axios.delete(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaId}`); 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) { } catch (error) {
console.error("Error al eliminar nota:", error); console.error("Error al eliminar nota:", error);
} }
@@ -140,9 +177,9 @@ export function Biblioteca() {
<Box w={240} p="md"> <Box w={240} p="md">
<ScrollArea h="100%"> <ScrollArea h="100%">
<Stack> <Stack>
<Button color="teal" onClick={fetchBibliotecas}> <Button color="teal" onClick={fetchBibliotecas}>🔄 Recuperar bibliotecas</Button>
🔄 Recuperar bibliotecas <Button color="grape" variant="outline" onClick={() => setModalNuevaBiblio(true)}> Nueva biblioteca</Button>
</Button>
{bibliotecas.map((biblio) => ( {bibliotecas.map((biblio) => (
<Button <Button
@@ -181,32 +218,29 @@ export function Biblioteca() {
padding="lg" padding="lg"
radius="md" radius="md"
withBorder withBorder
style={{ width: 300, position: 'relative' }} style={{
width: 300,
height: 250, // Altura fija para asegurar la separación
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}
> >
{/* Botones en esquina superior derecha */} <div>
<Box style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4, zIndex: 1 }}> <Title order={4} style={{ marginBottom: 10 }}>{nota.titulo}</Title>
<Text>{nota.texto}</Text>
</div>
<Box mt="md" style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button <Button
size="xs" size="xs"
color="blue"
variant="light" variant="light"
p={4} color="blue"
onClick={() => abrirModalEditar(nota)} onClick={() => abrirModalEditar(nota)}
> >
Editar
</Button>
<Button
size="xs"
color="red"
variant="light"
p={4}
onClick={() => eliminarNota(nota.id)}
>
🗑
</Button> </Button>
</Box> </Box>
<Title order={4} style={{ marginBottom: 10 }}>{nota.titulo}</Title>
<Text>{nota.texto}</Text>
</Card> </Card>
// Fin de notas en cards // Fin de notas en cards
@@ -218,9 +252,7 @@ export function Biblioteca() {
) : ( ) : (
<Stack> <Stack>
<Text>Selecciona una biblioteca</Text> <Text>Selecciona una biblioteca</Text>
<Button color="teal" onClick={fetchBibliotecas}>
🔄 Recuperar bibliotecas
</Button>
</Stack> </Stack>
)} )}
</Box> </Box>
@@ -234,8 +266,10 @@ export function Biblioteca() {
value={tituloNota} value={tituloNota}
onChange={(event) => setTituloNota(event.currentTarget.value)} onChange={(event) => setTituloNota(event.currentTarget.value)}
/> />
<TextInput <Textarea
label="Contenido" label="Contenido"
minRows={6}
autosize
value={contenidoNota} value={contenidoNota}
onChange={(event) => setContenidoNota(event.currentTarget.value)} onChange={(event) => setContenidoNota(event.currentTarget.value)}
/> />
@@ -253,16 +287,60 @@ export function Biblioteca() {
setNotaEnEdicion((prev) => (prev ? { ...prev, titulo: e.currentTarget.value } : null)) setNotaEnEdicion((prev) => (prev ? { ...prev, titulo: e.currentTarget.value } : null))
} }
/> />
<TextInput <Textarea
label="Contenido" label="Contenido"
minRows={6}
autosize
value={notaEnEdicion?.texto || ""} value={notaEnEdicion?.texto || ""}
onChange={(e) => onChange={(e) =>
setNotaEnEdicion((prev) => (prev ? { ...prev, texto: e.currentTarget.value } : null)) setNotaEnEdicion((prev) => (prev ? { ...prev, texto: e.currentTarget.value } : null))
} }
/> />
<Button onClick={guardarEdicionNota}>Guardar cambios</Button> <Group grow>
<Button color="blue" onClick={guardarEdicionNota}>
💾 Guardar cambios
</Button>
<Button
color="red"
onClick={async () => {
if (!notaEnEdicion || !bibliotecaSeleccionada) return;
await eliminarNota(notaEnEdicion.id);
setModalEditarAbierto(false);
setNotaEnEdicion(null);
}}
>
🗑 Eliminar nota
</Button>
</Group>
</Stack> </Stack>
</Modal> </Modal>
{/* Modal para crear una biblioteca */}
<Modal
opened={modalNuevaBiblio}
onClose={() => setModalNuevaBiblio(false)}
title="Crear nueva biblioteca"
>
<Stack>
<TextInput
label="Nombre"
value={nombreBiblio}
onChange={(e) => setNombreBiblio(e.currentTarget.value)}
disabled={loadingNuevaBiblio}
/>
<TextInput
label="Descripción"
value={descripcionBiblio}
onChange={(e) => setDescripcionBiblio(e.currentTarget.value)}
disabled={loadingNuevaBiblio}
/>
<Button onClick={crearBiblioteca} loading={loadingNuevaBiblio}>
Crear
</Button>
</Stack>
</Modal>
</AppShellWithMenu> </AppShellWithMenu>
); );
+7 -4
View File
@@ -28,7 +28,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"'OPAK20250510-ac2cea8af3110632314'" "'OPAK20250513-61b29978b7604031014'"
] ]
}, },
"execution_count": 2, "execution_count": 2,
@@ -37,8 +37,11 @@
} }
], ],
"source": [ "source": [
"# apikey_gpt = OpenAICredencial(titulo=\"Credencial_enmanuel_gpt\")\n", "apikey_gpt = OpenAICredencial(titulo=\"Credencial_enmanuel_gpt\",\n",
"# repo.add(apikey_gpt)" " api_key=\"\")\n",
"\n",
"\n",
"repo.add(apikey_gpt)"
] ]
}, },
{ {
@@ -56,7 +59,7 @@
} }
], ],
"source": [ "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}\")" "print(f\"✅ Credencial: {credencial_openai.titulo}\")"
] ]
}, },
+1 -1
View File
@@ -13,7 +13,7 @@ from src.Llms.Memory.postgres_MemoryConv import MemoryConvPostgres
conexion_admin = PostgresConexion(db_credencial) conexion_admin = PostgresConexion(db_credencial)
repo = OpenAICredencialRepo(conexion_admin) repo = OpenAICredencialRepo(conexion_admin)
credencial_openai = repo.get_by_id(1) credencial_openai = repo.get_by_id("OPAK20250513-61b29978b7604031014")
cliente = OpenAICliente(credencial_openai) cliente = OpenAICliente(credencial_openai)
+8 -2
View File
@@ -48,10 +48,16 @@ class Model_base:
def __json__(self) -> dict: def __json__(self) -> dict:
"""Devuelve una representación JSON serializable (dict plano).""" """Devuelve una representación JSON serializable (dict plano)."""
out = {} out = {}
# Prevención de error: solo ejecuta si __table__ existe
if not hasattr(self, "__table__"):
return out
for attr in self.__table__.columns: for attr in self.__table__.columns:
val = getattr(self, attr.name) val = getattr(self, attr.name, None)
if isinstance(val, datetime): if isinstance(val, datetime):
out[attr.name] = val.isoformat() out[attr.name] = val.isoformat()
else: else:
out[attr.name] = val out[attr.name] = val
return out
return out
-65
View File
@@ -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.")
@@ -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())
+30
View File
@@ -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,
)
+1 -1
View File
@@ -27,7 +27,7 @@ class MemoryConvPostgres(MemoryConvABC):
) )
# Crea la tabla si no existe # 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: def guardar_turno(self, rol: Literal["user", "assistant"], contenido: str) -> None:
stmt = insert(self.tabla).values(rol=rol, contenido=contenido) stmt = insert(self.tabla).values(rol=rol, contenido=contenido)
+8 -4
View File
@@ -17,9 +17,11 @@ class ModeloOpenAI(ModeloABC):
num_tokens_maximos: int = 512, num_tokens_maximos: int = 512,
use_legacy: bool = False 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__( super().__init__(
id=id,
model=model, model=model,
temperature=temperature, temperature=temperature,
top_p=top_p, top_p=top_p,
@@ -27,6 +29,8 @@ class ModeloOpenAI(ModeloABC):
frecuencia_penalizacion=frecuencia_penalizacion, frecuencia_penalizacion=frecuencia_penalizacion,
num_tokens_maximos=num_tokens_maximos num_tokens_maximos=num_tokens_maximos
) )
# Asignar cliente e indicadores adicionales
self.cliente = cliente self.cliente = cliente
self.use_legacy = use_legacy self.use_legacy = use_legacy
@@ -79,8 +83,8 @@ class ModeloOpenAI(ModeloABC):
if stream: if stream:
async def generador(): async def generador():
for token in resultado: # ya es un generador del cliente for token in resultado:
yield token yield token
return generador() return generador()
else: else:
return resultado.choices[0].message.content return resultado.choices[0].message.content
+12 -8
View File
@@ -5,6 +5,7 @@ from src.ConexionSql.Base_conexion import ConexionBase
from sqlalchemy import MetaData # Asegúrate de importar esto 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 src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca # Ajusta si es necesario
from sqlalchemy import inspect from sqlalchemy import inspect
from src.base import Base
class Biblioteca: class Biblioteca:
@@ -38,18 +39,21 @@ class Biblioteca:
raise ValueError("Debes proporcionar un 'embedder' o un 'vector_dim' explícito.") raise ValueError("Debes proporcionar un 'embedder' o un 'vector_dim' explícito.")
def generar_modelo_notas(self, conexion: ConexionBase): 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}" nombre_tabla = f"{self.nombre}"
print(f"[Notas] Generando tabla: {nombre_tabla}")
engine = conexion.get_engine() engine = conexion.get_engine()
inspector = inspect(engine) inspector = inspect(engine)
if inspector.has_table(nombre_tabla): 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.") raise ValueError(f"Ya existe una tabla con el nombre '{nombre_tabla}' en la base de datos.")
metadata = MetaData() print("[Notas] Generando definición SQL...")
tabla, NotaModel = generar_tabla_nota_para_biblioteca(nombre_tabla, self.vector_dim, metadata) tabla, NotaModel = generar_tabla_nota_para_biblioteca(nombre_tabla, self.vector_dim, Base.metadata)
metadata.create_all(engine)
print("[Notas] Creando tabla en base de datos...")
Base.metadata.create_all(engine)
print("[Notas] ✔️ Tabla creada")
return NotaModel return NotaModel
+32 -34
View File
@@ -1,6 +1,6 @@
import os import os
from dotenv import load_dotenv 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 pgvector.sqlalchemy import Vector
from sqlalchemy.orm import registry, Session from sqlalchemy.orm import registry, Session
from src.TextManager.nota import Nota 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.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base from src.ArquitectureLayer.Repo import Repo_base
from src.base import Base # Este es tu declarative_base()
# ---------------------- # ----------------------
# Cargar .env # Cargar .env
# ---------------------- # ----------------------
@@ -28,47 +31,42 @@ mapper_registry = registry()
# FUNCIONES AUXILIARES # FUNCIONES AUXILIARES
# ---------------------- # ----------------------
def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int, metadata: MetaData) -> Tuple[Table, type]: 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 try:
nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower()) print(f"[INFO] Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}")
tabla = Table( # Nombre SQL-safe
nombre_tabla, nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower())
metadata, print(f"[DEBUG] Nombre de tabla SQL-safe: '{nombre_tabla}'")
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
)
class NotaModel(Model_base): # Modelo ORM dinámico
""" class NotaModel(Base, Model_base):
Modelo ORM generado dinámicamente para notas de una biblioteca. __tablename__ = nombre_tabla
""" __table_args__ = {"extend_existing": True}
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)
# Registrar solo si aún no se ha hecho id = Column(String, primary_key=True)
if NotaModel.__name__ not in mapper_registry._class_registry: titulo = Column(String, nullable=False)
mapper_registry.map_imperatively(NotaModel, tabla) 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 # MAPPER