Cambios a las 3 bases Model mapper repo para que funcionen a partir de las clases heredando todos los metodos comunes

This commit is contained in:
2025-05-12 01:24:44 +02:00
parent 712bd877b8
commit bf1814bb8e
18 changed files with 992 additions and 361 deletions
+2 -2
View File
@@ -121,7 +121,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 6, "execution_count": null,
"id": "943d0deb", "id": "943d0deb",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -150,7 +150,7 @@
"from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder\n", "from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder\n",
"from src.ApiKeys.openai_apikey import OpenAICredencial\n", "from src.ApiKeys.openai_apikey import OpenAICredencial\n",
"from src.TextManager.biblioteca import Biblioteca\n", "from src.TextManager.biblioteca import Biblioteca\n",
"from src.TextManager.notas_biblioteca_mmr import NotaRepo\n", "from src.TextManager.notas_mmr import NotaRepo\n",
"\n", "\n",
"conexion = conexion_admin\n", "conexion = conexion_admin\n",
"embedder = OpenAIEmbedder(credencial_openai, model=\"text-embedding-3-large\")\n", "embedder = OpenAIEmbedder(credencial_openai, model=\"text-embedding-3-large\")\n",
+80 -34
View File
@@ -2,7 +2,7 @@
"cells": [ "cells": [
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 8, "execution_count": 1,
"id": "255345d5", "id": "255345d5",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -19,7 +19,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 9, "execution_count": 2,
"id": "b414a66c", "id": "b414a66c",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -93,7 +93,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 11, "execution_count": 9,
"id": "8e57e511", "id": "8e57e511",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -104,24 +104,24 @@
"[INFO] Iniciando el proceso de agregar nota a la biblioteca...\n", "[INFO] Iniciando el proceso de agregar nota a la biblioteca...\n",
"[INFO] Buscando biblioteca con ID: BBLI20250511-a91dbb2168172979414\n", "[INFO] Buscando biblioteca con ID: BBLI20250511-a91dbb2168172979414\n",
"[INFO] Biblioteca encontrada: biblio_Pruebas_1 (vector_dim=3072)\n", "[INFO] Biblioteca encontrada: biblio_Pruebas_1 (vector_dim=3072)\n",
"[INFO] Creando objeto Nota con título: sajdhasjdhasjdh\n", "[INFO] Creando objeto Nota con título: fdsfdsfsdfewww\n",
"[DEBUG] Nota creada: titulo='sajdhasjdhasjdh', texto_len=0, tags=0, conexiones=0, resumen_len=0\n", "[DEBUG] Nota creada: titulo='fdsfdsfsdfewww', texto_len=0, tags=0, conexiones=0, resumen_len=0\n",
"[INFO] Generando tabla y modelo de Nota para la biblioteca: biblio_Pruebas_1\n", "[INFO] Generando tabla y modelo de Nota para la biblioteca: biblio_Pruebas_1\n",
"[INFO] Creando tabla en la base de datos si no existe...\n", "[INFO] Creando tabla en la base de datos si no existe...\n",
"[INFO] Tabla 'biblio_pruebas_1' verificada/creada.\n", "[INFO] Tabla 'biblio_pruebas_1' verificada/creada.\n",
"[INFO] Guardando nota en la base de datos...\n", "[INFO] Guardando nota en la base de datos...\n",
"[INFO] Nota guardada con ID: NOTA20250511-04dbb203a9126228444\n", "[INFO] Nota guardada con ID: NOTA20250511-0cf0187e58905045667\n",
"[SUCCESS] Nota 'sajdhasjdhasjdh' agregada con éxito a la biblioteca 'biblio_Pruebas_1'.\n" "[SUCCESS] Nota 'fdsfdsfsdfewww' agregada con éxito a la biblioteca 'biblio_Pruebas_1'.\n"
] ]
}, },
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"{'mensaje': \"Nota 'sajdhasjdhasjdh' agregada con éxito a la biblioteca 'biblio_Pruebas_1'.\",\n", "{'mensaje': \"Nota 'fdsfdsfsdfewww' agregada con éxito a la biblioteca 'biblio_Pruebas_1'.\",\n",
" 'nota_id': 'NOTA20250511-04dbb203a9126228444'}" " 'nota_id': 'NOTA20250511-0cf0187e58905045667'}"
] ]
}, },
"execution_count": 11, "execution_count": 9,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@@ -133,13 +133,13 @@
"agregar_nota_a_biblioteca(\n", "agregar_nota_a_biblioteca(\n",
" conexion=conexion_admin,\n", " conexion=conexion_admin,\n",
" biblioteca_id=\"BBLI20250511-a91dbb2168172979414\",\n", " biblioteca_id=\"BBLI20250511-a91dbb2168172979414\",\n",
" titulo=\"sajdhasjdhasjdh\"\n", " titulo=\"fdsfdsfsdfewww\"\n",
")" ")"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 13, "execution_count": 12,
"id": "431f24f1", "id": "431f24f1",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -171,32 +171,78 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 14, "execution_count": 13,
"id": "ae4f2994", "id": "ae4f2994",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"name": "stderr", "data": {
"output_type": "stream", "text/plain": [
"text": [ "[{'id': 'NOTA20250511-6cf9b177a2249549641',\n",
"E:\\Fitz_Studio\\src\\TextManager\\notas_biblioteca_mmr.py:51: SAWarning: This declarative base already contains a class with the same class name and module name as src.TextManager.notas_biblioteca_mmr.NotaModel, and will be replaced in the string-lookup table.\n", " 'titulo': 'Hola que tal',\n",
" mapper_registry.map_imperatively(NotaModel, tabla)\n" " 'tags': [],\n",
] " 'texto': '',\n",
}, " 'resumen': '',\n",
{ " 'conexiones': []},\n",
"ename": "TypeError", " {'id': 'NOTA20250511-664831e1bd118315114',\n",
"evalue": "'NoneType' object is not iterable", " 'titulo': 'Holoooo',\n",
"output_type": "error", " 'tags': [],\n",
"traceback": [ " 'texto': '',\n",
"\u001b[31m---------------------------------------------------------------------------\u001b[39m", " 'resumen': '',\n",
"\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", " 'conexiones': []},\n",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[14]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43mlistar_notas_de_biblioteca\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2\u001b[39m \u001b[43m \u001b[49m\u001b[43mconexion\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconexion_admin\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3\u001b[39m \u001b[43m \u001b[49m\u001b[43mbiblioteca_id\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mBBLI20250511-a91dbb2168172979414\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\n\u001b[32m 4\u001b[39m \u001b[43m)\u001b[49m\n", " {'id': 'NOTA20250511-04dbb203a9126228444',\n",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[13]\u001b[39m\u001b[32m, line 12\u001b[39m, in \u001b[36mlistar_notas_de_biblioteca\u001b[39m\u001b[34m(conexion, biblioteca_id)\u001b[39m\n\u001b[32m 9\u001b[39m metadata.create_all(conexion.get_engine())\n\u001b[32m 11\u001b[39m repo_nota = NotaRepo(conexion.get_session(), ModeloNota)\n\u001b[32m---> \u001b[39m\u001b[32m12\u001b[39m notas = \u001b[43mrepo_nota\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget_all\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 13\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m [\n\u001b[32m 14\u001b[39m {\n\u001b[32m 15\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mid\u001b[39m\u001b[33m\"\u001b[39m: n.id,\n\u001b[32m (...)\u001b[39m\u001b[32m 22\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m n \u001b[38;5;129;01min\u001b[39;00m notas\n\u001b[32m 23\u001b[39m ]\n", " 'titulo': 'sajdhasjdhasjdh',\n",
"\u001b[36mFile \u001b[39m\u001b[32mE:\\Fitz_Studio\\src\\TextManager\\notas_biblioteca_mmr.py:109\u001b[39m, in \u001b[36mget_all\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 0\u001b[39m <Error retrieving source code with stack_data see ipython/ipython#13598>\n", " 'tags': [],\n",
"\u001b[36mFile \u001b[39m\u001b[32mE:\\Fitz_Studio\\src\\TextManager\\notas_biblioteca_mmr.py:109\u001b[39m, in \u001b[36m<listcomp>\u001b[39m\u001b[34m(.0)\u001b[39m\n\u001b[32m 0\u001b[39m <Error retrieving source code with stack_data see ipython/ipython#13598>\n", " 'texto': '',\n",
"\u001b[36mFile \u001b[39m\u001b[32mE:\\Fitz_Studio\\src\\TextManager\\notas_biblioteca_mmr.py:82\u001b[39m, in \u001b[36mfrom_model\u001b[39m\u001b[34m(model)\u001b[39m\n\u001b[32m 76\u001b[39m \u001b[38;5;129m@staticmethod\u001b[39m\n\u001b[32m 77\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mfrom_model\u001b[39m(model) -> Nota:\n\u001b[32m 78\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m Nota(\n\u001b[32m 79\u001b[39m \u001b[38;5;28mid\u001b[39m=model.id,\n\u001b[32m 80\u001b[39m titulo=model.titulo,\n\u001b[32m 81\u001b[39m tags=model.tags.split(\u001b[33m\"\u001b[39m\u001b[33m,\u001b[39m\u001b[33m\"\u001b[39m) \u001b[38;5;28;01mif\u001b[39;00m model.tags \u001b[38;5;28;01melse\u001b[39;00m [],\n\u001b[32m---> \u001b[39m\u001b[32m82\u001b[39m conexiones=model.conexiones.split(\u001b[33m\"\u001b[39m\u001b[33m,\u001b[39m\u001b[33m\"\u001b[39m) \u001b[38;5;28;01mif\u001b[39;00m model.conexiones \u001b[38;5;28;01melse\u001b[39;00m [],\n\u001b[32m 83\u001b[39m texto=model.texto,\n\u001b[32m 84\u001b[39m resumen=model.resumen,\n\u001b[32m 85\u001b[39m vector=\u001b[38;5;28mlist\u001b[39m(model.vector),\n\u001b[32m 86\u001b[39m vector_resumen=\u001b[38;5;28mlist\u001b[39m(model.vector_resumen) \u001b[38;5;28;01mif\u001b[39;00m model.vector_resumen \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[32m 87\u001b[39m )\n", " 'resumen': '',\n",
"\u001b[31mTypeError\u001b[39m: 'NoneType' object is not iterable" " 'conexiones': []},\n",
] " {'id': 'NOTA20250511-668f68e425271569668',\n",
" 'titulo': 'fsdfdf',\n",
" 'tags': [],\n",
" 'texto': '',\n",
" 'resumen': '',\n",
" 'conexiones': []},\n",
" {'id': 'NOTA20250511-a6ad7166d5660286632',\n",
" 'titulo': 'fsdfdf',\n",
" 'tags': [],\n",
" 'texto': '',\n",
" 'resumen': '',\n",
" 'conexiones': []},\n",
" {'id': 'NOTA20250511-639fa883c2266940759',\n",
" 'titulo': 'fsdfdf',\n",
" 'tags': [],\n",
" 'texto': '',\n",
" 'resumen': '',\n",
" 'conexiones': []},\n",
" {'id': 'NOTA20250511-27e86a8da6529002860',\n",
" 'titulo': 'fsdfdf',\n",
" 'tags': [],\n",
" 'texto': '',\n",
" 'resumen': '',\n",
" 'conexiones': []},\n",
" {'id': 'NOTA20250511-06479aca23973772356',\n",
" 'titulo': 'fsdfdf',\n",
" 'tags': [],\n",
" 'texto': '',\n",
" 'resumen': '',\n",
" 'conexiones': []},\n",
" {'id': 'NOTA20250511-0d577e3336174429305',\n",
" 'titulo': 'fdsfdsfsdfewww',\n",
" 'tags': [],\n",
" 'texto': '',\n",
" 'resumen': '',\n",
" 'conexiones': []},\n",
" {'id': 'NOTA20250511-0cf0187e58905045667',\n",
" 'titulo': 'fdsfdsfsdfewww',\n",
" 'tags': [],\n",
" 'texto': '',\n",
" 'resumen': '',\n",
" 'conexiones': []}]"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
} }
], ],
"source": [ "source": [
@@ -1,27 +1,14 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from fastapi import Path from fastapi import Path
from backend.schemas.text_manager_schema import BibliotecaInput, NotaInput
from backend.db.conexion import get_conexion from backend.db.conexion import get_conexion
from backend.services.text_manager import ( from backend.services.text_manager_srvc import *
crear_biblioteca,
listar_bibliotecas,
agregar_nota_a_biblioteca,
listar_notas_de_biblioteca
)
from src.ConexionSql.Postgres_conexion import PostgresConexion from src.ConexionSql.Postgres_conexion import PostgresConexion
router = APIRouter() router = APIRouter()
# ---------------------------
# MODELOS PARA BIBLIOTECAS
# ---------------------------
class BibliotecaInput(BaseModel):
nombre_biblioteca: str
descripcion: str
@router.post("/", summary="Crear una nueva biblioteca") @router.post("/", summary="Crear una nueva biblioteca")
def crear_biblioteca_endpoint( def crear_biblioteca_endpoint(
@@ -50,17 +37,6 @@ def listar_todas_bibliotecas(
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail="Error interno al listar las bibliotecas") raise HTTPException(status_code=500, detail="Error interno al listar las bibliotecas")
# ---------------------------
# MODELOS PARA NOTAS
# ---------------------------
class NotaInput(BaseModel):
titulo: str
texto: str = ""
tags: Optional[List[str]] = []
conexiones: Optional[List[str]] = []
resumen: Optional[str] = ""
@router.post("/nota/{biblioteca_id}", summary="Agregar una nota a una biblioteca") @router.post("/nota/{biblioteca_id}", summary="Agregar una nota a una biblioteca")
@@ -97,3 +73,33 @@ def listar_notas(
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 listar las notas") raise HTTPException(status_code=500, detail="Error interno al listar las notas")
@router.delete("/nota/{biblioteca_id}/{nota_id}", summary="Eliminar una nota por ID")
def eliminar_nota_endpoint(
biblioteca_id: str = Path(..., description="ID de la biblioteca"),
nota_id: str = Path(..., description="ID de la nota a eliminar"),
conexion: PostgresConexion = Depends(get_conexion)
):
try:
return eliminar_nota(conexion=conexion, biblioteca_id=biblioteca_id, nota_id=nota_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception:
raise HTTPException(status_code=500, detail="Error interno al eliminar la nota")
@router.put("/nota/{biblioteca_id}/{nota_id}", summary="Actualizar una nota por ID")
def actualizar_nota_endpoint(
biblioteca_id: str = Path(..., description="ID de la biblioteca"),
nota_id: str = Path(..., description="ID de la nota a actualizar"),
nota: NotaInput = ..., # body
conexion: PostgresConexion = Depends(get_conexion)
):
try:
return actualizar_nota(conexion=conexion, biblioteca_id=biblioteca_id, nota_id=nota_id, nota_input=nota)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception:
raise HTTPException(status_code=500, detail="Error interno al actualizar la nota")
+15
View File
@@ -0,0 +1,15 @@
from pydantic import BaseModel
from typing import List, Optional
class NotaInput(BaseModel):
titulo: str
texto: str = ""
tags: Optional[List[str]] = []
conexiones: Optional[List[str]] = []
resumen: Optional[str] = ""
class BibliotecaInput(BaseModel):
nombre_biblioteca: str
descripcion: str
@@ -4,8 +4,11 @@ from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo
from src.ConexionSql.Postgres_conexion import PostgresConexion from src.ConexionSql.Postgres_conexion import PostgresConexion
from src.TextManager.nota import Nota from src.TextManager.nota import Nota
from src.TextManager.notas_biblioteca_mmr import generar_tabla_nota_para_biblioteca, NotaRepo from src.TextManager.notas_mmr import generar_tabla_nota_para_biblioteca, NotaRepo
from sqlalchemy import MetaData from sqlalchemy import MetaData
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):
cred_repo = OpenAICredencialRepo(conexion) cred_repo = OpenAICredencialRepo(conexion)
@@ -49,19 +52,14 @@ def agregar_nota_a_biblioteca(
conexiones: list[str] = None, conexiones: list[str] = None,
resumen: str = "" resumen: str = ""
): ):
print("[INFO] Iniciando el proceso de agregar nota a la biblioteca...")
# Obtener la biblioteca # Obtener la biblioteca
print(f"[INFO] Buscando biblioteca con ID: {biblioteca_id}")
repo_biblioteca = BibliotecaRepo(conexion) repo_biblioteca = BibliotecaRepo(conexion)
biblioteca = repo_biblioteca.get_by_id(biblioteca_id) biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
if biblioteca is None: if biblioteca is None:
print(f"[ERROR] No se encontró la biblioteca con ID {biblioteca_id}")
raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}") raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}")
print(f"[INFO] Biblioteca encontrada: {biblioteca.nombre} (vector_dim={biblioteca.vector_dim})")
# Crear objeto Nota # Crear objeto Nota
print(f"[INFO] Creando objeto Nota con título: {titulo}")
nota = Nota( nota = Nota(
titulo=titulo, titulo=titulo,
texto=texto, texto=texto,
@@ -81,22 +79,17 @@ def agregar_nota_a_biblioteca(
) )
# Preparar tabla y modelo de nota # Preparar tabla y modelo de nota
print(f"[INFO] Generando tabla y modelo de Nota para la biblioteca: {biblioteca.nombre}")
metadata = MetaData() metadata = MetaData()
tabla, ModeloNota = generar_tabla_nota_para_biblioteca( tabla, ModeloNota = generar_tabla_nota_para_biblioteca(
biblioteca.nombre, biblioteca.nombre,
biblioteca.vector_dim, biblioteca.vector_dim,
metadata metadata
) )
print(f"[INFO] Creando tabla en la base de datos si no existe...")
metadata.create_all(conexion.get_engine()) metadata.create_all(conexion.get_engine())
print(f"[INFO] Tabla '{tabla.name}' verificada/creada.")
# Guardar la nota # Guardar la nota
print(f"[INFO] Guardando nota en la base de datos...")
repo_nota = NotaRepo(conexion.get_session(), ModeloNota) repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
nota_id = repo_nota.add(nota) nota_id = repo_nota.add(nota)
print(f"[INFO] Nota guardada con ID: {nota_id}")
resultado = { resultado = {
"mensaje": f"Nota '{titulo}' agregada con éxito a la biblioteca '{biblioteca.nombre}'.", "mensaje": f"Nota '{titulo}' agregada con éxito a la biblioteca '{biblioteca.nombre}'.",
@@ -129,4 +122,49 @@ def listar_notas_de_biblioteca(conexion: PostgresConexion, biblioteca_id: str) -
"conexiones": n.conexiones "conexiones": n.conexiones
} }
for n in notas for n in notas
] ]
def eliminar_nota(conexion: PostgresConexion, biblioteca_id: str, nota_id: str) -> dict:
repo_biblioteca = BibliotecaRepo(conexion)
biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
if not biblioteca:
raise ValueError(f"No se encontró la biblioteca con ID: {biblioteca_id}")
metadata = MetaData()
_, ModeloNota = generar_tabla_nota_para_biblioteca(biblioteca.nombre, biblioteca.vector_dim, metadata)
metadata.create_all(conexion.get_engine())
repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
fue_eliminada = repo_nota.delete_by_id(nota_id)
if fue_eliminada:
return {"mensaje": f"Nota '{nota_id}' eliminada correctamente."}
else:
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
def actualizar_nota(conexion: PostgresConexion, biblioteca_id: str, nota_id: str, nota_input: NotaInput) -> dict:
repo_biblioteca = BibliotecaRepo(conexion)
biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
if not biblioteca:
raise ValueError(f"No se encontró la biblioteca con ID: {biblioteca_id}")
metadata = MetaData()
_, ModeloNota = generar_tabla_nota_para_biblioteca(biblioteca.nombre, biblioteca.vector_dim, metadata)
metadata.create_all(conexion.get_engine())
repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
nota_actualizada = Nota(
titulo=nota_input.titulo,
texto=nota_input.texto,
tags=nota_input.tags or [],
conexiones=nota_input.conexiones or [],
resumen=nota_input.resumen or ""
)
fue_actualizada = repo_nota.update(nota_id, nota_actualizada)
if fue_actualizada:
return {"mensaje": f"Nota '{nota_id}' actualizada correctamente."}
else:
raise ValueError(f"No se encontró la nota con ID: {nota_id}")
+2 -1
View File
@@ -7,8 +7,9 @@ from src.Credenciales.postgres_credencial import PostgresCredencial # Asegúrat
from src.Credenciales.postgres_credencial_mmr import PostgresCredencialModel from src.Credenciales.postgres_credencial_mmr import PostgresCredencialModel
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialModel from src.ApiKeys.openai_apikey_mmr import OpenAICredencialModel
from src.Llms.Modelos.Openai_model_mmr import ModeloOpenAIConfigModel from src.Llms.Modelos.Openai_model_mmr import ModeloOpenAIConfigModel
from src.TextManager.biblioteca_mmr import BibliotecaModel
from src.Llms.Embedders.Openai_embedder_mmr import OpenAIEmbedderModel from src.Llms.Embedders.Openai_embedder_mmr import OpenAIEmbedderModel
from src.TextManager.biblioteca_mmr import BibliotecaModel
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
+107 -7
View File
@@ -36,6 +36,9 @@ export function Biblioteca() {
const [tituloNota, setTituloNota] = useState(''); const [tituloNota, setTituloNota] = useState('');
const [contenidoNota, setContenidoNota] = useState(''); const [contenidoNota, setContenidoNota] = useState('');
const [loadingNotas, setLoadingNotas] = useState(false); const [loadingNotas, setLoadingNotas] = useState(false);
const [notaEnEdicion, setNotaEnEdicion] = useState<Nota | null>(null);
const [modalEditarAbierto, setModalEditarAbierto] = useState(false);
const fetchBibliotecas = async () => { const fetchBibliotecas = async () => {
try { try {
@@ -93,12 +96,54 @@ export function Biblioteca() {
fetchBibliotecas(); fetchBibliotecas();
}, []); }, []);
// Eliminar nota
const eliminarNota = async (notaId: string) => {
if (!bibliotecaSeleccionada) return;
try {
await axios.delete(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaId}`);
await fetchBibliotecas();
} catch (error) {
console.error("Error al eliminar nota:", error);
}
};
// Editar nota
const abrirModalEditar = (nota: Nota) => {
setNotaEnEdicion(nota);
setModalEditarAbierto(true);
};
// Guardar cambios de edición
const guardarEdicionNota = async () => {
if (!notaEnEdicion || !bibliotecaSeleccionada) return;
try {
await axios.put(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaEnEdicion.id}`, {
titulo: notaEnEdicion.titulo,
texto: notaEnEdicion.texto,
tags: [],
conexiones: [],
resumen: ""
});
setModalEditarAbierto(false);
setNotaEnEdicion(null);
await fetchBibliotecas();
} catch (error) {
console.error("Error al actualizar nota:", error);
}
};
return ( return (
<AppShellWithMenu> <AppShellWithMenu>
<Box display="flex" h="100%"> <Box display="flex" h="100%">
<Box w={240} p="md"> <Box w={240} p="md">
<ScrollArea h="100%"> <ScrollArea h="100%">
<Stack> <Stack>
<Button color="teal" onClick={fetchBibliotecas}>
🔄 Recuperar bibliotecas
</Button>
{bibliotecas.map((biblio) => ( {bibliotecas.map((biblio) => (
<Button <Button
key={biblio.id} key={biblio.id}
@@ -120,9 +165,6 @@ export function Biblioteca() {
<Stack> <Stack>
<Title order={2}>{bibliotecaSeleccionada.nombre}</Title> <Title order={2}>{bibliotecaSeleccionada.nombre}</Title>
<Group> <Group>
<Button color="teal" onClick={fetchBibliotecas}>
🔄 Recuperar bibliotecas
</Button>
<Button onClick={() => setModalAbierto(true)}>Agregar nota</Button> <Button onClick={() => setModalAbierto(true)}>Agregar nota</Button>
</Group> </Group>
<Group> <Group>
@@ -130,10 +172,45 @@ export function Biblioteca() {
<Loader /> <Loader />
) : ( ) : (
bibliotecaSeleccionada.notas.map((nota) => ( bibliotecaSeleccionada.notas.map((nota) => (
<Card key={nota.id} shadow="sm" padding="lg" radius="md" withBorder style={{ width: 300 }}>
<Title order={4}>{nota.titulo}</Title> // Cards de notas
<Text>{nota.texto}</Text>
</Card> <Card
key={nota.id}
shadow="sm"
padding="lg"
radius="md"
withBorder
style={{ width: 300, position: 'relative' }}
>
{/* 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>
<Title order={4} style={{ marginBottom: 10 }}>{nota.titulo}</Title>
<Text>{nota.texto}</Text>
</Card>
// Fin de notas en cards
)) ))
)} )}
</Group> </Group>
@@ -149,6 +226,7 @@ export function Biblioteca() {
</Box> </Box>
</Box> </Box>
{/* Modal para agregar */}
<Modal opened={modalAbierto} onClose={() => setModalAbierto(false)} title="Agregar nueva nota"> <Modal opened={modalAbierto} onClose={() => setModalAbierto(false)} title="Agregar nueva nota">
<Stack> <Stack>
<TextInput <TextInput
@@ -164,6 +242,28 @@ export function Biblioteca() {
<Button onClick={agregarNota}>Guardar</Button> <Button onClick={agregarNota}>Guardar</Button>
</Stack> </Stack>
</Modal> </Modal>
{/* Modal para editar */}
<Modal opened={modalEditarAbierto} onClose={() => setModalEditarAbierto(false)} title="Editar nota">
<Stack>
<TextInput
label="Título"
value={notaEnEdicion?.titulo || ""}
onChange={(e) =>
setNotaEnEdicion((prev) => (prev ? { ...prev, titulo: e.currentTarget.value } : null))
}
/>
<TextInput
label="Contenido"
value={notaEnEdicion?.texto || ""}
onChange={(e) =>
setNotaEnEdicion((prev) => (prev ? { ...prev, texto: e.currentTarget.value } : null))
}
/>
<Button onClick={guardarEdicionNota}>Guardar cambios</Button>
</Stack>
</Modal>
</AppShellWithMenu> </AppShellWithMenu>
); );
} }
+45 -33
View File
@@ -8,6 +8,13 @@ from src.base import Base
from src.ApiKeys.openai_apikey import OpenAICredencial from src.ApiKeys.openai_apikey import OpenAICredencial
from src.Security.Encriptar import Encriptar_fernet from src.Security.Encriptar import Encriptar_fernet
from entrypoint import ENV_PATH from entrypoint import ENV_PATH
from src.ArquitectureLayer.Mapper import Mapper_base
from sqlalchemy import Column, String
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
# ---------------------- # ----------------------
# Cargar clave maestra # Cargar clave maestra
@@ -21,7 +28,7 @@ if pssword is None:
# MODELO (SQLAlchemy) # MODELO (SQLAlchemy)
# ---------------------- # ----------------------
class OpenAICredencialModel(Base): class OpenAICredencialModel(Base, Model_base):
__tablename__ = 'openai_credenciales' __tablename__ = 'openai_credenciales'
id = Column(String, primary_key=True) id = Column(String, primary_key=True)
@@ -33,7 +40,30 @@ class OpenAICredencialModel(Base):
# MAPPER # MAPPER
# ---------------------- # ----------------------
class OpenAICredencialMapper: class OpenAICredencialMapper(Mapper_base[OpenAICredencial, OpenAICredencialModel]):
@staticmethod
def to_model(obj: OpenAICredencial) -> OpenAICredencialModel:
return OpenAICredencialModel(
id=obj.id,
titulo=obj.titulo,
api_key=base64.b64encode(
Encriptar_fernet.encriptar(obj.api_key, pssword)
).decode("utf-8"),
organizacion=obj.organizacion
)
@staticmethod
def from_model(model: OpenAICredencialModel) -> OpenAICredencial:
return OpenAICredencial(
id=model.id,
titulo=model.titulo,
api_key=Encriptar_fernet.desencriptar(
base64.b64decode(model.api_key), pssword
),
organizacion=model.organizacion
)
@staticmethod @staticmethod
def to_dict(obj: OpenAICredencial) -> dict: def to_dict(obj: OpenAICredencial) -> dict:
return { return {
@@ -41,7 +71,7 @@ class OpenAICredencialMapper:
"titulo": obj.titulo, "titulo": obj.titulo,
"api_key": base64.b64encode( "api_key": base64.b64encode(
Encriptar_fernet.encriptar(obj.api_key, pssword) Encriptar_fernet.encriptar(obj.api_key, pssword)
).decode('utf-8'), ).decode("utf-8"),
"organizacion": obj.organizacion "organizacion": obj.organizacion
} }
@@ -56,40 +86,22 @@ class OpenAICredencialMapper:
organizacion=data.get("organizacion") organizacion=data.get("organizacion")
) )
@staticmethod
def from_model(model: OpenAICredencialModel) -> OpenAICredencial:
return OpenAICredencial(
id=model.id,
titulo=model.titulo,
api_key=Encriptar_fernet.desencriptar(
base64.b64decode(model.api_key), pssword
),
organizacion=model.organizacion
)
# ---------------------- # ----------------------
# REPO # REPO
# ---------------------- # ----------------------
class OpenAICredencialRepo: class OpenAICredencialRepo(Repo_base[OpenAICredencialModel, OpenAICredencial]):
def __init__(self, conexion: ConexionBase): def __init__(self, conexion: ConexionBase):
self.session = conexion.get_session() super().__init__(
session=conexion.get_session(),
def add(self, credencial: OpenAICredencial) -> str: modelo=OpenAICredencialModel,
data = OpenAICredencialMapper.to_dict(credencial) mapper=OpenAICredencialMapper
model = OpenAICredencialModel(**data) )
self.session.add(model)
self.session.commit()
return model.id
def get_all(self) -> list[OpenAICredencial]:
models = self.session.query(OpenAICredencialModel).all()
return [OpenAICredencialMapper.from_model(m) for m in models]
def get_by_titulo(self, titulo: str) -> OpenAICredencial | None: def get_by_titulo(self, titulo: str) -> OpenAICredencial | None:
model = self.session.query(OpenAICredencialModel).filter_by(titulo=titulo).first() model = (
return OpenAICredencialMapper.from_model(model) if model else None self.session.query(self.Modelo)
.filter_by(titulo=titulo, sys_deleted_at=None)
def get_by_id(self, id_: str) -> OpenAICredencial | None: .first()
model = self.session.get(OpenAICredencialModel, id_) )
return OpenAICredencialMapper.from_model(model) if model else None return self.Mapper.from_model(model) if model else None
+75
View File
@@ -0,0 +1,75 @@
# src\ArquitectureLayer\Mapper.py
from abc import ABC, abstractmethod
from typing import TypeVar, Generic, Type
import json
TDominio = TypeVar("TDominio")
TModelo = TypeVar("TModelo")
class Mapper_base(ABC, Generic[TDominio, TModelo]):
# ----------------------------
# Conversiones individuales
# ----------------------------
@staticmethod
@abstractmethod
def to_model(obj: TDominio) -> TModelo:
"""Convierte objeto de dominio a modelo ORM"""
pass
@staticmethod
@abstractmethod
def from_model(model: TModelo) -> TDominio:
"""Convierte modelo ORM a objeto de dominio"""
pass
@staticmethod
@abstractmethod
def to_dict(obj: TDominio) -> dict:
"""Convierte objeto de dominio a diccionario plano"""
pass
@staticmethod
@abstractmethod
def from_dict(d: dict) -> TDominio:
"""Convierte diccionario plano a objeto de dominio"""
pass
@classmethod
def to_json(cls, obj: TDominio) -> str:
"""Convierte objeto de dominio a JSON string"""
return json.dumps(cls.to_dict(obj), default=str)
@classmethod
def from_json(cls, json_str: str) -> TDominio:
"""Convierte JSON string a objeto de dominio"""
return cls.from_dict(json.loads(json_str))
# ----------------------------
# Conversiones en lote (bulk)
# ----------------------------
@classmethod
def to_model_list(cls, objs: list[TDominio]) -> list[TModelo]:
return [cls.to_model(o) for o in objs]
@classmethod
def from_model_list(cls, models: list[TModelo]) -> list[TDominio]:
return [cls.from_model(m) for m in models]
@classmethod
def to_dict_list(cls, objs: list[TDominio]) -> list[dict]:
return [cls.to_dict(o) for o in objs]
@classmethod
def from_dict_list(cls, dicts: list[dict]) -> list[TDominio]:
return [cls.from_dict(d) for d in dicts]
@classmethod
def to_json_list(cls, objs: list[TDominio]) -> str:
return json.dumps(cls.to_dict_list(objs), default=str)
@classmethod
def from_json_list(cls, json_str: str) -> list[TDominio]:
return cls.from_dict_list(json.loads(json_str))
+57
View File
@@ -0,0 +1,57 @@
# src\ArquitectureLayer\Model.py
from sqlalchemy import Column, DateTime, String, Integer, Text, func
from sqlalchemy.ext.declarative import declared_attr, as_declarative
from datetime import datetime
@as_declarative()
class Model_base:
__abstract__ = True
@declared_attr
def sys_created_at(cls):
return Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
@declared_attr
def sys_created_by(cls):
return Column(String, nullable=True)
@declared_attr
def sys_updated_at(cls):
return Column(DateTime(timezone=True), onupdate=func.now(), nullable=True)
@declared_attr
def sys_updated_by(cls):
return Column(String, nullable=True)
@declared_attr
def sys_version(cls):
return Column(Integer, default=1, nullable=False)
@declared_attr
def sys_notes(cls):
return Column(Text, nullable=True)
@declared_attr
def sys_deleted_at(cls):
return Column(DateTime(timezone=True), nullable=True)
def __repr__(self):
id_val = getattr(self, "id", None)
return f"<{self.__class__.__name__} id={id_val}>"
def __str__(self):
cls = self.__class__.__name__
id_val = getattr(self, "id", None)
return f"{cls}(id={id_val})"
def __json__(self) -> dict:
"""Devuelve una representación JSON serializable (dict plano)."""
out = {}
for attr in self.__table__.columns:
val = getattr(self, attr.name)
if isinstance(val, datetime):
out[attr.name] = val.isoformat()
else:
out[attr.name] = val
return out
+148
View File
@@ -0,0 +1,148 @@
# src\ArquitectureLayer\Repo.py
from abc import ABC
from typing import Type, TypeVar, Generic, Optional
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime
from src.ArquitectureLayer.Mapper import Mapper_base # Asegúrate de importar tu ABC base
TModelo = TypeVar("TModelo")
TDominio = TypeVar("TDominio")
class Repo_base(ABC, Generic[TModelo, TDominio]):
def __init__(self, session: Session, modelo: type[TModelo], mapper: type[Mapper_base[TDominio, TModelo]]):
self.session = session
self.Modelo = modelo
self.Mapper = mapper
# ----------------------
# ADD
# ----------------------
def add(self, dominio: TDominio, created_by: Optional[str] = None, notes: Optional[str] = None) -> str:
data = self.Mapper.to_dict(dominio)
data.update({
"sys_created_by": created_by,
"sys_notes": notes,
"sys_version": 1
})
model = self.Modelo(**data)
self.session.add(model)
self.session.commit()
return model.id
def add_many(self, dominios: list[TDominio], created_by: Optional[str] = None, notes: Optional[str] = None) -> list[str]:
ids = []
for dominio in dominios:
data = self.Mapper.to_dict(dominio)
data.update({
"sys_created_by": created_by,
"sys_notes": notes,
"sys_version": 1
})
model = self.Modelo(**data)
self.session.add(model)
ids.append(model.id)
self.session.commit()
return ids
# ----------------------
# GET
# ----------------------
def get_by_id(self, id_: str) -> Optional[TDominio]:
model = self.session.query(self.Modelo).filter_by(id=id_, sys_deleted_at=None).first()
return self.Mapper.from_model(model) if model else None
def get_all(self) -> list[TDominio]:
models = self.session.query(self.Modelo).filter_by(sys_deleted_at=None).all()
return self.Mapper.from_model_list(models)
def get_paginated(self, offset: int = 0, limit: int = 10) -> list[TDominio]:
models = self.session.query(self.Modelo).filter_by(sys_deleted_at=None).offset(offset).limit(limit).all()
return self.Mapper.from_model_list(models)
def get_deleted(self) -> list[TDominio]:
models = self.session.query(self.Modelo).filter(self.Modelo.sys_deleted_at.isnot(None)).all()
return self.Mapper.from_model_list(models)
# ----------------------
# UPDATE
# ----------------------
def update(self, id_: str, new_data: dict, updated_by: Optional[str] = None, notes: Optional[str] = None) -> bool:
model = self.session.query(self.Modelo).filter_by(id=id_, sys_deleted_at=None).first()
if not model:
return False
for key, value in new_data.items():
if hasattr(model, key):
setattr(model, key, value)
model.sys_updated_by = updated_by
model.sys_notes = notes
model.sys_version = (model.sys_version or 1) + 1
self.session.commit()
return True
def bulk_update(self, updates: list[tuple[str, dict]], updated_by: Optional[str] = None, notes: Optional[str] = None) -> int:
count = 0
for id_, data in updates:
if self.update(id_, data, updated_by=updated_by, notes=notes):
count += 1
return count
# ----------------------
# DELETE
# ----------------------
def delete_by_id(self, id_: str) -> bool:
model = self.session.query(self.Modelo).filter_by(id=id_).first()
if model:
self.session.delete(model)
self.session.commit()
return True
return False
def delete_all(self) -> int:
deleted = self.session.query(self.Modelo).delete()
self.session.commit()
return deleted
# ----------------------
# SOFT DELETE
# ----------------------
def soft_delete(self, id_: str, deleted_by: Optional[str] = None, notes: Optional[str] = None) -> bool:
model = self.session.query(self.Modelo).filter_by(id=id_, sys_deleted_at=None).first()
if model:
model.sys_deleted_at = datetime.now()
model.sys_updated_by = deleted_by
model.sys_notes = notes
model.sys_version = (model.sys_version or 1) + 1
self.session.commit()
return True
return False
def soft_restore(self, id_: str, restored_by: Optional[str] = None, notes: Optional[str] = None) -> bool:
model = self.session.query(self.Modelo).filter_by(id=id_).first()
if model and model.sys_deleted_at is not None:
model.sys_deleted_at = None
model.sys_updated_by = restored_by
model.sys_notes = notes
model.sys_version = (model.sys_version or 1) + 1
self.session.commit()
return True
return False
# ----------------------
# OTROS
# ----------------------
def exists(self, id_: str) -> bool:
return self.session.query(self.Modelo).filter_by(id=id_, sys_deleted_at=None).first() is not None
def count(self) -> int:
return self.session.query(self.Modelo).filter_by(sys_deleted_at=None).count()
+49 -36
View File
@@ -2,6 +2,13 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
from sqlalchemy import Column, Integer, String from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy import DateTime, Text, func
import base64
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from src.ConexionSql.Base_conexion import ConexionBase from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base from src.base import Base
@@ -21,7 +28,7 @@ if pssword is None:
# MODELO (SQLAlchemy) # MODELO (SQLAlchemy)
# ---------------------- # ----------------------
class PostgresCredencialModel(Base): class PostgresCredencialModel(Base, Model_base):
__tablename__ = 'postgres_credenciales' __tablename__ = 'postgres_credenciales'
id = Column(String, primary_key=True) id = Column(String, primary_key=True)
@@ -36,9 +43,36 @@ class PostgresCredencialModel(Base):
# MAPPER # MAPPER
# ---------------------- # ----------------------
import base64 class PostgresCredencialMapper(Mapper_base[PostgresCredencial, PostgresCredencialModel]):
@staticmethod
def to_model(obj: PostgresCredencial) -> PostgresCredencialModel:
return PostgresCredencialModel(
id=obj.id,
titulo=obj.titulo,
host=obj.host,
port=obj.port,
dbname=obj.dbname,
user=obj.user,
password=base64.b64encode(
Encriptar_fernet.encriptar(obj.password, pssword)
).decode('utf-8')
)
@staticmethod
def from_model(model: PostgresCredencialModel) -> PostgresCredencial:
return PostgresCredencial(
id=model.id,
titulo=model.titulo,
host=model.host,
port=model.port,
dbname=model.dbname,
user=model.user,
password=Encriptar_fernet.desencriptar(
base64.b64decode(model.password), pssword
)
)
class PostgresCredencialMapper:
@staticmethod @staticmethod
def to_dict(obj: PostgresCredencial) -> dict: def to_dict(obj: PostgresCredencial) -> dict:
return { return {
@@ -67,43 +101,22 @@ class PostgresCredencialMapper:
) )
) )
@staticmethod
def from_model(model: PostgresCredencialModel) -> PostgresCredencial:
return PostgresCredencial(
id=model.id,
titulo=model.titulo,
host=model.host,
port=model.port,
dbname=model.dbname,
user=model.user,
password=Encriptar_fernet.desencriptar(
base64.b64decode(model.password), pssword
)
)
# ---------------------- # ----------------------
# REPO # REPO
# ---------------------- # ----------------------
class PostgresCredencialRepo: class PostgresCredencialRepo(Repo_base[PostgresCredencialModel, PostgresCredencial]):
def __init__(self, conexion: ConexionBase): def __init__(self, conexion: ConexionBase):
self.session = conexion.get_session() super().__init__(
session=conexion.get_session(),
def add(self, credencial: PostgresCredencial) -> str: modelo=PostgresCredencialModel,
data = PostgresCredencialMapper.to_dict(credencial) mapper=PostgresCredencialMapper
model = PostgresCredencialModel(**data) )
self.session.add(model)
self.session.commit()
return model.id
def get_all(self) -> list[PostgresCredencial]:
models = self.session.query(PostgresCredencialModel).all()
return [PostgresCredencialMapper.from_model(m) for m in models]
def get_by_titulo(self, titulo: str) -> PostgresCredencial | None: def get_by_titulo(self, titulo: str) -> PostgresCredencial | None:
model = self.session.query(PostgresCredencialModel).filter_by(titulo=titulo).first() model = (
return PostgresCredencialMapper.from_model(model) if model else None self.session.query(self.Modelo)
.filter_by(titulo=titulo, sys_deleted_at=None)
def get_by_id(self, id_: str) -> PostgresCredencial | None: .first()
model = self.session.get(PostgresCredencialModel, id_) )
return PostgresCredencialMapper.from_model(model) if model else None return self.Mapper.from_model(model) if model else None
+38 -25
View File
@@ -1,6 +1,12 @@
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
from sqlalchemy import Column, String from sqlalchemy import Column, String
from sqlalchemy import Column, String, ForeignKey
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from src.ConexionSql.Base_conexion import ConexionBase from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base from src.base import Base
from src.Security.GenerarIDs import GeneradorIDUnico from src.Security.GenerarIDs import GeneradorIDUnico
@@ -17,18 +23,36 @@ load_dotenv(ENV_PATH)
# MODELO (SQLAlchemy) # MODELO (SQLAlchemy)
# ---------------------- # ----------------------
class OpenAIEmbedderModel(Base): class OpenAIEmbedderModel(Base, Model_base):
__tablename__ = "openai_embedders" __tablename__ = "openai_embedders"
id = Column(String, primary_key=True) id = Column(String, primary_key=True)
api_key_id = Column(String, nullable=False) # ID de la credencial asociada (clave foránea lógica)
api_key_id = Column(String, ForeignKey("openai_credenciales.id"), nullable=False)
model = Column(String, nullable=False) model = Column(String, nullable=False)
# ---------------------- # ----------------------
# MAPPER # MAPPER
# ---------------------- # ----------------------
class OpenAIEmbedderMapper: class OpenAIEmbedderMapper(Mapper_base[OpenAIEmbedder, OpenAIEmbedderModel]):
@staticmethod
def to_model(obj: OpenAIEmbedder) -> OpenAIEmbedderModel:
return OpenAIEmbedderModel(
id=obj.id,
api_key_id=obj.client.credencial.id,
model=obj.model
)
@staticmethod
def from_model(model: OpenAIEmbedderModel, credencial: OpenAICredencial) -> OpenAIEmbedder:
return OpenAIEmbedder(
id=model.id,
credencial=credencial,
model=model.model
)
@staticmethod @staticmethod
def to_dict(obj: OpenAIEmbedder) -> dict: def to_dict(obj: OpenAIEmbedder) -> dict:
return { return {
@@ -45,39 +69,28 @@ class OpenAIEmbedderMapper:
model=data["model"] model=data["model"]
) )
@staticmethod
def from_model(model: OpenAIEmbedderModel, credencial: OpenAICredencial) -> OpenAIEmbedder:
return OpenAIEmbedder(
id=model.id,
credencial=credencial,
model=model.model
)
# ---------------------- # ----------------------
# REPO # REPO
# ---------------------- # ----------------------
class OpenAIEmbedderRepo: class OpenAIEmbedderRepo(Repo_base[OpenAIEmbedderModel, OpenAIEmbedder]):
def __init__(self, conexion: ConexionBase): def __init__(self, conexion: ConexionBase):
self.session = conexion.get_session() super().__init__(
session=conexion.get_session(),
def add(self, embedder: OpenAIEmbedder) -> str: modelo=OpenAIEmbedderModel,
data = OpenAIEmbedderMapper.to_dict(embedder) mapper=OpenAIEmbedderMapper
model = OpenAIEmbedderModel(**data) )
self.session.add(model)
self.session.commit()
return model.id
def get_by_id(self, id_: str, credencial: OpenAICredencial) -> OpenAIEmbedder | None: def get_by_id(self, id_: str, credencial: OpenAICredencial) -> OpenAIEmbedder | None:
model = self.session.get(OpenAIEmbedderModel, id_) model = self.session.get(self.Modelo, id_)
return OpenAIEmbedderMapper.from_model(model, credencial) if model else None return self.Mapper.from_model(model, credencial) if model else None
def get_all(self, credencial_loader: callable) -> list[OpenAIEmbedder]: def get_all(self, credencial_loader: callable) -> list[OpenAIEmbedder]:
""" """
:param credencial_loader: función que recibe un api_key_id y devuelve una instancia de OpenAICredencial :param credencial_loader: función que recibe un api_key_id y devuelve una instancia de OpenAICredencial
""" """
models = self.session.query(OpenAIEmbedderModel).all() models = self.session.query(self.Modelo).all()
return [ return [
OpenAIEmbedderMapper.from_model(m, credencial_loader(m.api_key_id)) self.Mapper.from_model(m, credencial_loader(m.api_key_id))
for m in models for m in models
] ]
+64 -30
View File
@@ -2,6 +2,12 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
from sqlalchemy import Column, Integer, String, Float, Boolean from sqlalchemy import Column, Integer, String, Float, Boolean
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from typing import Optional
from src.ConexionSql.Base_conexion import ConexionBase from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base from src.base import Base
from src.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica from src.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica
@@ -19,23 +25,54 @@ if pssword is None:
# MODELO (SQLAlchemy) # MODELO (SQLAlchemy)
# ---------------------- # ----------------------
class ModeloOpenAIConfigModel(Base): class ModeloOpenAIConfigModel(Base, Model_base):
__tablename__ = 'modelo_openai_configs' __tablename__ = 'modelo_openai_configs'
id = Column(String, primary_key=True) id = Column(String, primary_key=True)
model = Column(String, nullable=False) model = Column(String, nullable=False)
temperature = Column(Float, default=0.7) temperature = Column(Float, default=0.7, nullable=False)
top_p = Column(Float, default=1.0) top_p = Column(Float, default=1.0, nullable=False)
top_k = Column(Integer, nullable=True) top_k = Column(Integer, nullable=True)
frecuencia_penalizacion = Column(Float, default=0.0)
num_tokens_maximos = Column(Integer, default=512) frecuencia_penalizacion = Column(Float, default=0.0, nullable=False)
use_legacy = Column(Boolean, default=False) num_tokens_maximos = Column(Integer, default=512, nullable=False)
use_legacy = Column(Boolean, default=False, nullable=False)
# ---------------------- # ----------------------
# MAPPER # MAPPER
# ---------------------- # ----------------------
class ModeloOpenAIConfigMapper: class ModeloOpenAIConfigMapper(Mapper_base[ModeloOpenAI, ModeloOpenAIConfigModel]):
@staticmethod
def to_model(obj: ModeloOpenAI) -> ModeloOpenAIConfigModel:
return ModeloOpenAIConfigModel(
id=obj.id,
model=obj.model,
temperature=obj.temperature,
top_p=obj.top_p,
top_k=obj.top_k,
frecuencia_penalizacion=obj.frecuencia_penalizacion,
num_tokens_maximos=obj.num_tokens_maximos,
use_legacy=obj.use_legacy
)
@staticmethod
def from_model(model: ModeloOpenAIConfigModel, cliente: Optional[object] = None) -> ModeloOpenAI:
return ModeloOpenAI(
id=model.id,
cliente=cliente,
model=model.model,
temperature=model.temperature,
top_p=model.top_p,
top_k=model.top_k,
frecuencia_penalizacion=model.frecuencia_penalizacion,
num_tokens_maximos=model.num_tokens_maximos,
use_legacy=model.use_legacy
)
@staticmethod @staticmethod
def to_dict(obj: ModeloOpenAI) -> dict: def to_dict(obj: ModeloOpenAI) -> dict:
return { return {
@@ -50,39 +87,36 @@ class ModeloOpenAIConfigMapper:
} }
@staticmethod @staticmethod
def from_model(model: ModeloOpenAIConfigModel, cliente: object) -> ModeloOpenAI: def from_dict(data: dict, cliente: Optional[object] = None) -> ModeloOpenAI:
return ModeloOpenAI( return ModeloOpenAI(
id=model.id, id=data["id"],
cliente=cliente, cliente=cliente,
model=model.model, model=data["model"],
temperature=model.temperature, temperature=data["temperature"],
top_p=model.top_p, top_p=data["top_p"],
top_k=model.top_k, top_k=data["top_k"],
frecuencia_penalizacion=model.frecuencia_penalizacion, frecuencia_penalizacion=data["frecuencia_penalizacion"],
num_tokens_maximos=model.num_tokens_maximos, num_tokens_maximos=data["num_tokens_maximos"],
use_legacy=model.use_legacy use_legacy=data["use_legacy"]
) )
# ---------------------- # ----------------------
# REPO # REPO
# ---------------------- # ----------------------
class ModeloOpenAIConfigRepo: class ModeloOpenAIConfigRepo(Repo_base[ModeloOpenAIConfigModel, ModeloOpenAI]):
def __init__(self, conexion: ConexionBase, cliente: object): def __init__(self, conexion: ConexionBase, cliente: object):
self.session = conexion.get_session() super().__init__(
self.cliente = cliente # Necesario para crear ModeloOpenAI session=conexion.get_session(),
modelo=ModeloOpenAIConfigModel,
def add(self, config: ModeloOpenAI) -> str: mapper=ModeloOpenAIConfigMapper
data = ModeloOpenAIConfigMapper.to_dict(config) )
model = ModeloOpenAIConfigModel(**data) self.cliente = cliente # Necesario para construir el dominio con lógica
self.session.add(model)
self.session.commit()
return model.id
def get_by_id(self, id_: str) -> ModeloOpenAI | None: def get_by_id(self, id_: str) -> ModeloOpenAI | None:
model = self.session.get(ModeloOpenAIConfigModel, id_) model = self.session.get(self.Modelo, id_)
return ModeloOpenAIConfigMapper.from_model(model, self.cliente) if model else None return self.Mapper.from_model(model, self.cliente) if model else None
def get_all(self) -> list[ModeloOpenAI]: def get_all(self) -> list[ModeloOpenAI]:
models = self.session.query(ModeloOpenAIConfigModel).all() models = self.session.query(self.Modelo).all()
return [ModeloOpenAIConfigMapper.from_model(m, self.cliente) for m in models] return [self.Mapper.from_model(m, self.cliente) for m in models]
+1 -1
View File
@@ -3,7 +3,7 @@ from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que es
from typing import List, Optional from typing import List, Optional
from src.ConexionSql.Base_conexion import ConexionBase 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_biblioteca_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
+53 -42
View File
@@ -3,6 +3,10 @@ import base64
from dotenv import load_dotenv from dotenv import load_dotenv
from sqlalchemy import Column, String, Integer from sqlalchemy import Column, String, Integer
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
from src.ConexionSql.Base_conexion import ConexionBase from src.ConexionSql.Base_conexion import ConexionBase
from src.base import Base from src.base import Base
from src.Security.Encriptar import Encriptar_fernet from src.Security.Encriptar import Encriptar_fernet
@@ -23,39 +27,32 @@ if pssword is None:
# MODELO (SQLAlchemy) # MODELO (SQLAlchemy)
# ---------------------- # ----------------------
class BibliotecaModel(Base): class BibliotecaModel(Base, Model_base):
__tablename__ = "bibliotecas" __tablename__ = "bibliotecas"
id = Column(String, primary_key=True, unique=True) id = Column(String, primary_key=True, unique=True)
nombre = Column(String, nullable=False, unique=True) nombre = Column(String, nullable=False, unique=True)
descripcion = Column(String, default="")
descripcion = Column(String, nullable=False, default="")
vector_dim = Column(Integer, nullable=False) vector_dim = Column(Integer, nullable=False)
embedder_info = Column(String, nullable=True) # Se puede guardar nombre de clase o config encriptada
embedder_info = Column(String, nullable=True) # Nombre de clase, ID de configuración, o info encriptada del embedder
# ---------------------- # ----------------------
# MAPPER # MAPPER
# ---------------------- # ----------------------
class BibliotecaMapper: class BibliotecaMapper(Mapper_base[Biblioteca, BibliotecaModel]):
@staticmethod
def to_dict(obj: Biblioteca) -> dict:
embedder_info = type(obj.embedder).__name__ if obj.embedder else None
return {
"id": obj.id,
"nombre": obj.nombre,
"descripcion": obj.descripcion,
"vector_dim": obj.vector_dim,
"embedder_info": embedder_info # sin codificar ni encriptar
}
@staticmethod @staticmethod
def from_dict(data: dict) -> Biblioteca: def to_model(obj: Biblioteca) -> BibliotecaModel:
return Biblioteca( return BibliotecaModel(
id=data["id"], id=obj.id,
nombre=data["nombre"], nombre=obj.nombre,
descripcion=data["descripcion"], descripcion=obj.descripcion,
vector_dim=data["vector_dim"], vector_dim=obj.vector_dim
embedder=None # Mantienes la lógica actual de no restaurarlo automáticamente # embedder no se serializa en el modelo, se maneja por separado
) )
@staticmethod @staticmethod
@@ -65,37 +62,51 @@ class BibliotecaMapper:
nombre=model.nombre, nombre=model.nombre,
descripcion=model.descripcion, descripcion=model.descripcion,
vector_dim=model.vector_dim, vector_dim=model.vector_dim,
embedder=None # Se puede cargar manualmente si es necesario embedder=None # se deja para inyección posterior si es necesario
) )
@staticmethod
def to_dict(obj: Biblioteca) -> dict:
embedder_info = type(obj.embedder).__name__ if obj.embedder else None
return {
"id": obj.id,
"nombre": obj.nombre,
"descripcion": obj.descripcion,
"vector_dim": obj.vector_dim,
"embedder_info": embedder_info
}
@staticmethod
def from_dict(data: dict) -> Biblioteca:
return Biblioteca(
id=data["id"],
nombre=data["nombre"],
descripcion=data["descripcion"],
vector_dim=data["vector_dim"],
embedder=None # inyección manual si se desea
)
# ---------------------- # ----------------------
# REPO # REPO
# ---------------------- # ----------------------
class BibliotecaRepo: class BibliotecaRepo(Repo_base[BibliotecaModel, Biblioteca]):
def __init__(self, conexion: ConexionBase): def __init__(self, conexion: ConexionBase):
self.session = conexion.get_session() super().__init__(
session=conexion.get_session(),
modelo=BibliotecaModel,
mapper=BibliotecaMapper
)
def add(self, biblioteca: Biblioteca) -> str: def add(self, biblioteca: Biblioteca, created_by: str = None, notes: str = None) -> str:
# Verificar si ya existe una biblioteca con el mismo nombre # Lógica personalizada: prevenir duplicados por nombre
existente = self.session.query(BibliotecaModel).filter_by(nombre=biblioteca.nombre).first() existente = self.session.query(self.Modelo).filter_by(nombre=biblioteca.nombre).first()
if existente: if existente:
raise ValueError(f"Ya existe una biblioteca con el nombre '{biblioteca.nombre}'") raise ValueError(f"Ya existe una biblioteca con el nombre '{biblioteca.nombre}'")
data = BibliotecaMapper.to_dict(biblioteca) return super().add(biblioteca, created_by=created_by, notes=notes)
model = BibliotecaModel(**data)
self.session.add(model)
self.session.commit()
return model.id
def get_all(self) -> list[Biblioteca]:
models = self.session.query(BibliotecaModel).all()
return [BibliotecaMapper.from_model(m) for m in models]
def get_by_nombre(self, nombre: str) -> Biblioteca | None: def get_by_nombre(self, nombre: str) -> Biblioteca | None:
model = self.session.query(BibliotecaModel).filter_by(nombre=nombre).first() model = self.session.query(self.Modelo).filter_by(nombre=nombre, sys_deleted_at=None).first()
return BibliotecaMapper.from_model(model) if model else None return self.Mapper.from_model(model) if model else None
def get_by_id(self, id_: str) -> Biblioteca | None:
model = self.session.get(BibliotecaModel, id_)
return BibliotecaMapper.from_model(model) if model else None
-112
View File
@@ -1,112 +0,0 @@
import os
from dotenv import load_dotenv
from sqlalchemy import Column, String, Table, MetaData
from pgvector.sqlalchemy import Vector
from sqlalchemy.orm import registry, Session
from src.TextManager.nota import Nota
from src.ConexionSql.Base_conexion import ConexionBase
# ----------------------
# Cargar .env
# ----------------------
from entrypoint import ENV_PATH
load_dotenv(ENV_PATH)
# ----------------------
# REGISTRO DINÁMICO PARA TABLAS
# ----------------------
mapper_registry = registry()
# ----------------------
# FUNCIONES AUXILIARES
# ----------------------
def generar_tabla_nota_para_biblioteca(biblioteca_nombre: str, vector_dim: int, metadata: MetaData):
nombre_tabla = biblioteca_nombre.lower().replace(" ", "_")
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),
)
class NotaModel:
def __init__(self, nota: 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)
# Evitar mapear dos veces la misma clase
if NotaModel not in mapper_registry._class_registry.values():
mapper_registry.map_imperatively(NotaModel, tabla)
return tabla, NotaModel
# ----------------------
# MAPPER
# ----------------------
class NotaMapper:
@staticmethod
def to_dict(nota: Nota) -> dict:
return {
"id": nota.id,
"titulo": nota.titulo,
"tags": ",".join(nota.tags),
"conexiones": ",".join(nota.conexiones),
"texto": nota.texto,
"resumen": nota.resumen,
"vector": nota.vector,
"vector_resumen": nota.vector_resumen
}
@staticmethod
def from_model(model) -> Nota:
return Nota(
id=model.id,
titulo=model.titulo,
tags=model.tags.split(",") if model.tags else [],
conexiones=model.conexiones.split(",") if model.conexiones else [],
texto=model.texto,
resumen=model.resumen,
vector=list(model.vector) if model.vector is not None else None,
vector_resumen=list(model.vector_resumen) if model.vector_resumen is not None else None
)
# ----------------------
# REPO
# ----------------------
class NotaRepo:
def __init__(self, session: Session, modelo_nota):
if modelo_nota is None:
raise ValueError("No se puede instanciar NotaRepo sin un modelo válido de nota.")
self.session = session
self.ModeloNota = modelo_nota
def add(self, nota: Nota) -> str:
model = self.ModeloNota(nota)
self.session.add(model)
self.session.commit()
return model.id
def get_by_id(self, id_: str) -> Nota | None:
model = self.session.get(self.ModeloNota, id_)
return NotaMapper.from_model(model) if model else None
def get_all(self) -> list[Nota]:
models = self.session.query(self.ModeloNota).all()
return [NotaMapper.from_model(m) for m in models]
+174
View File
@@ -0,0 +1,174 @@
import os
from dotenv import load_dotenv
from sqlalchemy import Column, String, Table, MetaData
from pgvector.sqlalchemy import Vector
from sqlalchemy.orm import registry, Session
from src.TextManager.nota import Nota
from src.ConexionSql.Base_conexion import ConexionBase
from typing import Tuple
import re
from src.ArquitectureLayer.Mapper import Mapper_base
from src.ArquitectureLayer.Model import Model_base
from src.ArquitectureLayer.Repo import Repo_base
# ----------------------
# Cargar .env
# ----------------------
from entrypoint import ENV_PATH
load_dotenv(ENV_PATH)
# ----------------------
# REGISTRO DINÁMICO PARA TABLAS
# ----------------------
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.
"""
# Normalización robusta del nombre de la tabla
nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower())
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
)
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)
# Registrar solo si aún no se ha hecho
if NotaModel.__name__ not in mapper_registry._class_registry:
mapper_registry.map_imperatively(NotaModel, tabla)
return tabla, NotaModel
# ----------------------
# MAPPER
# ----------------------
class NotaMapper(Mapper_base[Nota, object]): # Usa `object` si el modelo es dinámico
@staticmethod
def to_model(nota: Nota):
return {
"id": nota.id,
"titulo": nota.titulo,
"tags": ",".join(nota.tags),
"conexiones": ",".join(nota.conexiones),
"texto": nota.texto,
"resumen": nota.resumen,
"vector": nota.vector,
"vector_resumen": nota.vector_resumen
}
@staticmethod
def from_model(model) -> Nota:
return Nota(
id=model.id,
titulo=model.titulo,
tags=model.tags.split(",") if model.tags else [],
conexiones=model.conexiones.split(",") if model.conexiones else [],
texto=model.texto,
resumen=model.resumen,
vector=list(model.vector) if model.vector is not None else None,
vector_resumen=list(model.vector_resumen) if model.vector_resumen is not None else None
)
@staticmethod
def to_dict(nota: Nota) -> dict:
return {
"id": nota.id,
"titulo": nota.titulo,
"tags": ",".join(nota.tags),
"conexiones": ",".join(nota.conexiones),
"texto": nota.texto,
"resumen": nota.resumen,
"vector": nota.vector,
"vector_resumen": nota.vector_resumen
}
@staticmethod
def from_dict(data: dict) -> Nota:
return Nota(
id=data["id"],
titulo=data["titulo"],
tags=data["tags"].split(",") if data.get("tags") else [],
conexiones=data["conexiones"].split(",") if data.get("conexiones") else [],
texto=data["texto"],
resumen=data["resumen"],
vector=data.get("vector"),
vector_resumen=data.get("vector_resumen")
)
# ----------------------
# REPO
# ----------------------
class NotaRepo(Repo_base):
def __init__(self, session: Session, modelo_nota: type):
if modelo_nota is None:
raise ValueError("No se puede instanciar NotaRepo sin un modelo válido de nota.")
super().__init__(session=session, modelo=modelo_nota, mapper=NotaMapper)
# ------------------------
# Métodos personalizados
# ------------------------
def get_by_tag(self, tag: str) -> list[Nota]:
query = self.session.query(self.Modelo).filter(self.Modelo.tags.contains([tag]))
models = query.all()
return self.Mapper.from_model_list(models)
def search_by_text(self, texto: str) -> list[Nota]:
query = self.session.query(self.Modelo).filter(self.Modelo.texto.ilike(f'%{texto}%'))
models = query.all()
return self.Mapper.from_model_list(models)
def get_paginated(self, offset: int = 0, limit: int = 10) -> list[Nota]:
models = self.session.query(self.Modelo).offset(offset).limit(limit).all()
return self.Mapper.from_model_list(models)
def update(self, id_: str, nota_actualizada: Nota) -> bool:
model = self.session.get(self.Modelo, id_)
if not model:
return False
# Campos de dominio
model.titulo = nota_actualizada.titulo
model.texto = nota_actualizada.texto
model.tags = nota_actualizada.tags
model.conexiones = nota_actualizada.conexiones
model.resumen = nota_actualizada.resumen
# Actualización de campos de sistema
model.sys_version = (model.sys_version or 1) + 1
self.session.commit()
return True