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 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(
+26 -5
View File
@@ -10,25 +10,46 @@ 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):
print("[INICIO] Creando biblioteca...")
try:
print("[Paso 1] Obteniendo credencial...")
cred_repo = OpenAICredencialRepo(conexion)
credencial = cred_repo.get_by_id("OPAK20250510-ac2cea8af3110632314")
credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014")
print("[OK] Credencial obtenida:", credencial.titulo if credencial else "❌ None")
print("[Paso 2] Instanciando embedder...")
embedder = OpenAIEmbedder(credencial, model="text-embedding-3-large")
biblioteca = Biblioteca(nombre=nombre_biblioteca,
embedder=embedder,
descripcion=descripcion)
print("[OK] Embedder instanciado")
print("[Paso 3] Instanciando biblioteca...")
biblioteca = Biblioteca(
nombre=nombre_biblioteca,
embedder=embedder,
descripcion=descripcion
)
print(f"[OK] Biblioteca instanciada con 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]:
repo = BibliotecaRepo(conexion)
+111 -33
View File
@@ -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<Nota | null>(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() {
<Box w={240} p="md">
<ScrollArea h="100%">
<Stack>
<Button color="teal" onClick={fetchBibliotecas}>
🔄 Recuperar bibliotecas
</Button>
<Button color="teal" onClick={fetchBibliotecas}>🔄 Recuperar bibliotecas</Button>
<Button color="grape" variant="outline" onClick={() => setModalNuevaBiblio(true)}> Nueva biblioteca</Button>
{bibliotecas.map((biblio) => (
<Button
@@ -181,32 +218,29 @@ export function Biblioteca() {
padding="lg"
radius="md"
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 */}
<Box style={{ position: 'absolute', top: 8, right: 8, display: 'flex', gap: 4, zIndex: 1 }}>
<Button
size="xs"
color="blue"
variant="light"
p={4}
onClick={() => abrirModalEditar(nota)}
>
</Button>
<Button
size="xs"
color="red"
variant="light"
p={4}
onClick={() => eliminarNota(nota.id)}
>
🗑
</Button>
</Box>
<div>
<Title order={4} style={{ marginBottom: 10 }}>{nota.titulo}</Title>
<Text>{nota.texto}</Text>
</div>
<Box mt="md" style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
size="xs"
variant="light"
color="blue"
onClick={() => abrirModalEditar(nota)}
>
Editar
</Button>
</Box>
</Card>
// Fin de notas en cards
@@ -218,9 +252,7 @@ export function Biblioteca() {
) : (
<Stack>
<Text>Selecciona una biblioteca</Text>
<Button color="teal" onClick={fetchBibliotecas}>
🔄 Recuperar bibliotecas
</Button>
</Stack>
)}
</Box>
@@ -234,8 +266,10 @@ export function Biblioteca() {
value={tituloNota}
onChange={(event) => setTituloNota(event.currentTarget.value)}
/>
<TextInput
<Textarea
label="Contenido"
minRows={6}
autosize
value={contenidoNota}
onChange={(event) => setContenidoNota(event.currentTarget.value)}
/>
@@ -253,16 +287,60 @@ export function Biblioteca() {
setNotaEnEdicion((prev) => (prev ? { ...prev, titulo: e.currentTarget.value } : null))
}
/>
<TextInput
<Textarea
label="Contenido"
minRows={6}
autosize
value={notaEnEdicion?.texto || ""}
onChange={(e) =>
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>
</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>
);
+7 -4
View File
@@ -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}\")"
]
},
+1 -1
View File
@@ -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)
+7 -1
View File
@@ -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
-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
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)
+7 -3
View File
@@ -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,7 +83,7 @@ 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:
+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 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}"
engine = conexion.get_engine()
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
+31 -33
View File
@@ -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
try:
print(f"[INFO] Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}")
# 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}'")
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
)
# Modelo ORM dinámico
class NotaModel(Base, Model_base):
__tablename__ = nombre_tabla
__table_args__ = {"extend_existing": True}
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)
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)
# Registrar solo si aún no se ha hecho
if NotaModel.__name__ not in mapper_registry._class_registry:
mapper_registry.map_imperatively(NotaModel, tabla)
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 tabla, NotaModel
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