diff --git a/Apikeys_embedding.ipynb b/Apikeys_embedding.ipynb index b7a974c..f119f46 100644 --- a/Apikeys_embedding.ipynb +++ b/Apikeys_embedding.ipynb @@ -121,7 +121,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "943d0deb", "metadata": {}, "outputs": [ @@ -150,7 +150,7 @@ "from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder\n", "from src.ApiKeys.openai_apikey import OpenAICredencial\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", "conexion = conexion_admin\n", "embedder = OpenAIEmbedder(credencial_openai, model=\"text-embedding-3-large\")\n", diff --git a/Pruebas_notas.ipynb b/Pruebas_notas.ipynb index e3d91bf..0bbb222 100644 --- a/Pruebas_notas.ipynb +++ b/Pruebas_notas.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 8, + "execution_count": 1, "id": "255345d5", "metadata": {}, "outputs": [], @@ -19,7 +19,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 2, "id": "b414a66c", "metadata": {}, "outputs": [], @@ -93,7 +93,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "id": "8e57e511", "metadata": {}, "outputs": [ @@ -104,24 +104,24 @@ "[INFO] Iniciando el proceso de agregar nota a la biblioteca...\n", "[INFO] Buscando biblioteca con ID: BBLI20250511-a91dbb2168172979414\n", "[INFO] Biblioteca encontrada: biblio_Pruebas_1 (vector_dim=3072)\n", - "[INFO] Creando objeto Nota con título: sajdhasjdhasjdh\n", - "[DEBUG] Nota creada: titulo='sajdhasjdhasjdh', texto_len=0, tags=0, conexiones=0, resumen_len=0\n", + "[INFO] Creando objeto Nota con título: fdsfdsfsdfewww\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] Creando tabla en la base de datos si no existe...\n", "[INFO] Tabla 'biblio_pruebas_1' verificada/creada.\n", "[INFO] Guardando nota en la base de datos...\n", - "[INFO] Nota guardada con ID: NOTA20250511-04dbb203a9126228444\n", - "[SUCCESS] Nota 'sajdhasjdhasjdh' agregada con éxito a la biblioteca 'biblio_Pruebas_1'.\n" + "[INFO] Nota guardada con ID: NOTA20250511-0cf0187e58905045667\n", + "[SUCCESS] Nota 'fdsfdsfsdfewww' agregada con éxito a la biblioteca 'biblio_Pruebas_1'.\n" ] }, { "data": { "text/plain": [ - "{'mensaje': \"Nota 'sajdhasjdhasjdh' agregada con éxito a la biblioteca 'biblio_Pruebas_1'.\",\n", - " 'nota_id': 'NOTA20250511-04dbb203a9126228444'}" + "{'mensaje': \"Nota 'fdsfdsfsdfewww' agregada con éxito a la biblioteca 'biblio_Pruebas_1'.\",\n", + " 'nota_id': 'NOTA20250511-0cf0187e58905045667'}" ] }, - "execution_count": 11, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -133,13 +133,13 @@ "agregar_nota_a_biblioteca(\n", " conexion=conexion_admin,\n", " biblioteca_id=\"BBLI20250511-a91dbb2168172979414\",\n", - " titulo=\"sajdhasjdhasjdh\"\n", + " titulo=\"fdsfdsfsdfewww\"\n", ")" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "id": "431f24f1", "metadata": {}, "outputs": [], @@ -171,32 +171,78 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "id": "ae4f2994", "metadata": {}, "outputs": [ { - "name": "stderr", - "output_type": "stream", - "text": [ - "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", - " mapper_registry.map_imperatively(NotaModel, tabla)\n" - ] - }, - { - "ename": "TypeError", - "evalue": "'NoneType' object is not iterable", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", - "\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", - "\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", - "\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 \n", - "\u001b[36mFile \u001b[39m\u001b[32mE:\\Fitz_Studio\\src\\TextManager\\notas_biblioteca_mmr.py:109\u001b[39m, in \u001b[36m\u001b[39m\u001b[34m(.0)\u001b[39m\n\u001b[32m 0\u001b[39m \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", - "\u001b[31mTypeError\u001b[39m: 'NoneType' object is not iterable" - ] + "data": { + "text/plain": [ + "[{'id': 'NOTA20250511-6cf9b177a2249549641',\n", + " 'titulo': 'Hola que tal',\n", + " 'tags': [],\n", + " 'texto': '',\n", + " 'resumen': '',\n", + " 'conexiones': []},\n", + " {'id': 'NOTA20250511-664831e1bd118315114',\n", + " 'titulo': 'Holoooo',\n", + " 'tags': [],\n", + " 'texto': '',\n", + " 'resumen': '',\n", + " 'conexiones': []},\n", + " {'id': 'NOTA20250511-04dbb203a9126228444',\n", + " 'titulo': 'sajdhasjdhasjdh',\n", + " 'tags': [],\n", + " 'texto': '',\n", + " 'resumen': '',\n", + " '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": [ diff --git a/backend/api/v1/endpoints/text_manager_endpoint.py b/backend/api/v1/endpoints/text_manager_endpoint.py index 6b15650..3ca3188 100644 --- a/backend/api/v1/endpoints/text_manager_endpoint.py +++ b/backend/api/v1/endpoints/text_manager_endpoint.py @@ -1,27 +1,14 @@ from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel -from typing import List, Optional from fastapi import Path +from backend.schemas.text_manager_schema import BibliotecaInput, NotaInput from backend.db.conexion import get_conexion -from backend.services.text_manager import ( - crear_biblioteca, - listar_bibliotecas, - agregar_nota_a_biblioteca, - listar_notas_de_biblioteca -) +from backend.services.text_manager_srvc import * from src.ConexionSql.Postgres_conexion import PostgresConexion router = APIRouter() -# --------------------------- -# MODELOS PARA BIBLIOTECAS -# --------------------------- - -class BibliotecaInput(BaseModel): - nombre_biblioteca: str - descripcion: str @router.post("/", summary="Crear una nueva biblioteca") def crear_biblioteca_endpoint( @@ -50,17 +37,6 @@ def listar_todas_bibliotecas( except Exception as e: 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") @@ -97,3 +73,33 @@ def listar_notas( raise HTTPException(status_code=404, detail=str(e)) except Exception as e: 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") \ No newline at end of file diff --git a/backend/schemas/text_manager_schema.py b/backend/schemas/text_manager_schema.py new file mode 100644 index 0000000..ce00536 --- /dev/null +++ b/backend/schemas/text_manager_schema.py @@ -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 diff --git a/backend/services/text_manager.py b/backend/services/text_manager_srvc.py similarity index 66% rename from backend/services/text_manager.py rename to backend/services/text_manager_srvc.py index 2ac1916..fbc0e29 100644 --- a/backend/services/text_manager.py +++ b/backend/services/text_manager_srvc.py @@ -4,8 +4,11 @@ from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo from src.ConexionSql.Postgres_conexion import PostgresConexion 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 backend.schemas.text_manager_schema import NotaInput + + def crear_biblioteca(nombre_biblioteca: str, conexion: PostgresConexion, descripcion: str): cred_repo = OpenAICredencialRepo(conexion) @@ -49,19 +52,14 @@ def agregar_nota_a_biblioteca( conexiones: list[str] = None, resumen: str = "" ): - print("[INFO] Iniciando el proceso de agregar nota a la biblioteca...") # Obtener la biblioteca - print(f"[INFO] Buscando biblioteca con ID: {biblioteca_id}") repo_biblioteca = BibliotecaRepo(conexion) biblioteca = repo_biblioteca.get_by_id(biblioteca_id) 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}") - print(f"[INFO] Biblioteca encontrada: {biblioteca.nombre} (vector_dim={biblioteca.vector_dim})") # Crear objeto Nota - print(f"[INFO] Creando objeto Nota con título: {titulo}") nota = Nota( titulo=titulo, texto=texto, @@ -81,22 +79,17 @@ def agregar_nota_a_biblioteca( ) # Preparar tabla y modelo de nota - print(f"[INFO] Generando tabla y modelo de Nota para la biblioteca: {biblioteca.nombre}") metadata = MetaData() tabla, ModeloNota = generar_tabla_nota_para_biblioteca( biblioteca.nombre, biblioteca.vector_dim, metadata ) - print(f"[INFO] Creando tabla en la base de datos si no existe...") metadata.create_all(conexion.get_engine()) - print(f"[INFO] Tabla '{tabla.name}' verificada/creada.") # Guardar la nota - print(f"[INFO] Guardando nota en la base de datos...") repo_nota = NotaRepo(conexion.get_session(), ModeloNota) nota_id = repo_nota.add(nota) - print(f"[INFO] Nota guardada con ID: {nota_id}") resultado = { "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 } for n in notas - ] \ No newline at end of file + ] + + +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}") diff --git a/entrypoint/init_db.py b/entrypoint/init_db.py index 1d97fc9..2c0bce4 100644 --- a/entrypoint/init_db.py +++ b/entrypoint/init_db.py @@ -7,8 +7,9 @@ from src.Credenciales.postgres_credencial import PostgresCredencial # Asegúrat from src.Credenciales.postgres_credencial_mmr import PostgresCredencialModel from src.ApiKeys.openai_apikey_mmr import OpenAICredencialModel 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.TextManager.biblioteca_mmr import BibliotecaModel + from dotenv import load_dotenv import os diff --git a/frontend/src/pages/Biblioteca.tsx b/frontend/src/pages/Biblioteca.tsx index f098079..515e664 100644 --- a/frontend/src/pages/Biblioteca.tsx +++ b/frontend/src/pages/Biblioteca.tsx @@ -36,6 +36,9 @@ export function Biblioteca() { const [tituloNota, setTituloNota] = useState(''); const [contenidoNota, setContenidoNota] = useState(''); const [loadingNotas, setLoadingNotas] = useState(false); + const [notaEnEdicion, setNotaEnEdicion] = useState(null); + const [modalEditarAbierto, setModalEditarAbierto] = useState(false); + const fetchBibliotecas = async () => { try { @@ -93,12 +96,54 @@ export function Biblioteca() { 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 ( + + {bibliotecas.map((biblio) => ( @@ -130,10 +172,45 @@ export function Biblioteca() { ) : ( bibliotecaSeleccionada.notas.map((nota) => ( - - {nota.titulo} - {nota.texto} - + + // Cards de notas + + + {/* Botones en esquina superior derecha */} + + + + + + {nota.titulo} + {nota.texto} + + + // Fin de notas en cards + )) )} @@ -149,6 +226,7 @@ export function Biblioteca() { + {/* Modal para agregar */} setModalAbierto(false)} title="Agregar nueva nota"> Guardar + + {/* Modal para editar */} + setModalEditarAbierto(false)} title="Editar nota"> + + + setNotaEnEdicion((prev) => (prev ? { ...prev, titulo: e.currentTarget.value } : null)) + } + /> + + setNotaEnEdicion((prev) => (prev ? { ...prev, texto: e.currentTarget.value } : null)) + } + /> + + + ); + } diff --git a/src/ApiKeys/openai_apikey_mmr.py b/src/ApiKeys/openai_apikey_mmr.py index 03fb497..fda0d8b 100644 --- a/src/ApiKeys/openai_apikey_mmr.py +++ b/src/ApiKeys/openai_apikey_mmr.py @@ -8,6 +8,13 @@ from src.base import Base from src.ApiKeys.openai_apikey import OpenAICredencial from src.Security.Encriptar import Encriptar_fernet 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 @@ -21,7 +28,7 @@ if pssword is None: # MODELO (SQLAlchemy) # ---------------------- -class OpenAICredencialModel(Base): +class OpenAICredencialModel(Base, Model_base): __tablename__ = 'openai_credenciales' id = Column(String, primary_key=True) @@ -33,7 +40,30 @@ class OpenAICredencialModel(Base): # 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 def to_dict(obj: OpenAICredencial) -> dict: return { @@ -41,7 +71,7 @@ class OpenAICredencialMapper: "titulo": obj.titulo, "api_key": base64.b64encode( Encriptar_fernet.encriptar(obj.api_key, pssword) - ).decode('utf-8'), + ).decode("utf-8"), "organizacion": obj.organizacion } @@ -56,40 +86,22 @@ class OpenAICredencialMapper: 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 # ---------------------- -class OpenAICredencialRepo: +class OpenAICredencialRepo(Repo_base[OpenAICredencialModel, OpenAICredencial]): def __init__(self, conexion: ConexionBase): - self.session = conexion.get_session() - - def add(self, credencial: OpenAICredencial) -> str: - data = OpenAICredencialMapper.to_dict(credencial) - 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] + super().__init__( + session=conexion.get_session(), + modelo=OpenAICredencialModel, + mapper=OpenAICredencialMapper + ) def get_by_titulo(self, titulo: str) -> OpenAICredencial | None: - model = self.session.query(OpenAICredencialModel).filter_by(titulo=titulo).first() - return OpenAICredencialMapper.from_model(model) if model else None - - def get_by_id(self, id_: str) -> OpenAICredencial | None: - model = self.session.get(OpenAICredencialModel, id_) - return OpenAICredencialMapper.from_model(model) if model else None + model = ( + self.session.query(self.Modelo) + .filter_by(titulo=titulo, sys_deleted_at=None) + .first() + ) + return self.Mapper.from_model(model) if model else None \ No newline at end of file diff --git a/src/ArquitectureLayer/Mapper.py b/src/ArquitectureLayer/Mapper.py new file mode 100644 index 0000000..b1de117 --- /dev/null +++ b/src/ArquitectureLayer/Mapper.py @@ -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)) \ No newline at end of file diff --git a/src/ArquitectureLayer/Model.py b/src/ArquitectureLayer/Model.py new file mode 100644 index 0000000..24d46a5 --- /dev/null +++ b/src/ArquitectureLayer/Model.py @@ -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 diff --git a/src/ArquitectureLayer/Repo.py b/src/ArquitectureLayer/Repo.py new file mode 100644 index 0000000..7434e48 --- /dev/null +++ b/src/ArquitectureLayer/Repo.py @@ -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() diff --git a/src/Credenciales/postgres_credencial_mmr.py b/src/Credenciales/postgres_credencial_mmr.py index 38f8918..9f11584 100644 --- a/src/Credenciales/postgres_credencial_mmr.py +++ b/src/Credenciales/postgres_credencial_mmr.py @@ -2,6 +2,13 @@ import os from dotenv import load_dotenv from sqlalchemy import Column, Integer, String 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.base import Base @@ -21,7 +28,7 @@ if pssword is None: # MODELO (SQLAlchemy) # ---------------------- -class PostgresCredencialModel(Base): +class PostgresCredencialModel(Base, Model_base): __tablename__ = 'postgres_credenciales' id = Column(String, primary_key=True) @@ -36,9 +43,36 @@ class PostgresCredencialModel(Base): # 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 def to_dict(obj: PostgresCredencial) -> dict: 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 # ---------------------- -class PostgresCredencialRepo: +class PostgresCredencialRepo(Repo_base[PostgresCredencialModel, PostgresCredencial]): def __init__(self, conexion: ConexionBase): - self.session = conexion.get_session() - - def add(self, credencial: PostgresCredencial) -> str: - data = PostgresCredencialMapper.to_dict(credencial) - 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] + super().__init__( + session=conexion.get_session(), + modelo=PostgresCredencialModel, + mapper=PostgresCredencialMapper + ) def get_by_titulo(self, titulo: str) -> PostgresCredencial | None: - model = self.session.query(PostgresCredencialModel).filter_by(titulo=titulo).first() - return PostgresCredencialMapper.from_model(model) if model else None - - def get_by_id(self, id_: str) -> PostgresCredencial | None: - model = self.session.get(PostgresCredencialModel, id_) - return PostgresCredencialMapper.from_model(model) if model else None \ No newline at end of file + model = ( + self.session.query(self.Modelo) + .filter_by(titulo=titulo, sys_deleted_at=None) + .first() + ) + return self.Mapper.from_model(model) if model else None \ No newline at end of file diff --git a/src/Llms/Embedders/Openai_embedder_mmr.py b/src/Llms/Embedders/Openai_embedder_mmr.py index 07b0868..d203d1b 100644 --- a/src/Llms/Embedders/Openai_embedder_mmr.py +++ b/src/Llms/Embedders/Openai_embedder_mmr.py @@ -1,6 +1,12 @@ import os from dotenv import load_dotenv 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.base import Base from src.Security.GenerarIDs import GeneradorIDUnico @@ -17,18 +23,36 @@ load_dotenv(ENV_PATH) # MODELO (SQLAlchemy) # ---------------------- -class OpenAIEmbedderModel(Base): +class OpenAIEmbedderModel(Base, Model_base): __tablename__ = "openai_embedders" 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) # ---------------------- # 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 def to_dict(obj: OpenAIEmbedder) -> dict: return { @@ -45,39 +69,28 @@ class OpenAIEmbedderMapper: model=data["model"] ) - @staticmethod - def from_model(model: OpenAIEmbedderModel, credencial: OpenAICredencial) -> OpenAIEmbedder: - return OpenAIEmbedder( - id=model.id, - credencial=credencial, - model=model.model - ) - # ---------------------- # REPO # ---------------------- -class OpenAIEmbedderRepo: +class OpenAIEmbedderRepo(Repo_base[OpenAIEmbedderModel, OpenAIEmbedder]): def __init__(self, conexion: ConexionBase): - self.session = conexion.get_session() - - def add(self, embedder: OpenAIEmbedder) -> str: - data = OpenAIEmbedderMapper.to_dict(embedder) - model = OpenAIEmbedderModel(**data) - self.session.add(model) - self.session.commit() - return model.id + super().__init__( + session=conexion.get_session(), + modelo=OpenAIEmbedderModel, + mapper=OpenAIEmbedderMapper + ) def get_by_id(self, id_: str, credencial: OpenAICredencial) -> OpenAIEmbedder | None: - model = self.session.get(OpenAIEmbedderModel, id_) - return OpenAIEmbedderMapper.from_model(model, credencial) if model else None + model = self.session.get(self.Modelo, id_) + return self.Mapper.from_model(model, credencial) if model else None 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 """ - models = self.session.query(OpenAIEmbedderModel).all() + models = self.session.query(self.Modelo).all() 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 - ] + ] \ No newline at end of file diff --git a/src/Llms/Modelos/Openai_model_mmr.py b/src/Llms/Modelos/Openai_model_mmr.py index f3e6c52..94e4c09 100644 --- a/src/Llms/Modelos/Openai_model_mmr.py +++ b/src/Llms/Modelos/Openai_model_mmr.py @@ -2,6 +2,12 @@ import os from dotenv import load_dotenv 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.base import Base from src.Llms.Modelos.Openai_model import ModeloOpenAI # Clase real de lógica @@ -19,23 +25,54 @@ if pssword is None: # MODELO (SQLAlchemy) # ---------------------- -class ModeloOpenAIConfigModel(Base): +class ModeloOpenAIConfigModel(Base, Model_base): __tablename__ = 'modelo_openai_configs' id = Column(String, primary_key=True) + model = Column(String, nullable=False) - temperature = Column(Float, default=0.7) - top_p = Column(Float, default=1.0) + temperature = Column(Float, default=0.7, nullable=False) + top_p = Column(Float, default=1.0, nullable=False) top_k = Column(Integer, nullable=True) - frecuencia_penalizacion = Column(Float, default=0.0) - num_tokens_maximos = Column(Integer, default=512) - use_legacy = Column(Boolean, default=False) + + frecuencia_penalizacion = Column(Float, default=0.0, nullable=False) + num_tokens_maximos = Column(Integer, default=512, nullable=False) + + use_legacy = Column(Boolean, default=False, nullable=False) # ---------------------- # 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 def to_dict(obj: ModeloOpenAI) -> dict: return { @@ -50,39 +87,36 @@ class ModeloOpenAIConfigMapper: } @staticmethod - def from_model(model: ModeloOpenAIConfigModel, cliente: object) -> ModeloOpenAI: + def from_dict(data: dict, cliente: Optional[object] = None) -> ModeloOpenAI: return ModeloOpenAI( - id=model.id, + id=data["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 + model=data["model"], + temperature=data["temperature"], + top_p=data["top_p"], + top_k=data["top_k"], + frecuencia_penalizacion=data["frecuencia_penalizacion"], + num_tokens_maximos=data["num_tokens_maximos"], + use_legacy=data["use_legacy"] ) # ---------------------- # REPO # ---------------------- -class ModeloOpenAIConfigRepo: +class ModeloOpenAIConfigRepo(Repo_base[ModeloOpenAIConfigModel, ModeloOpenAI]): def __init__(self, conexion: ConexionBase, cliente: object): - self.session = conexion.get_session() - self.cliente = cliente # Necesario para crear ModeloOpenAI - - def add(self, config: ModeloOpenAI) -> str: - data = ModeloOpenAIConfigMapper.to_dict(config) - model = ModeloOpenAIConfigModel(**data) - self.session.add(model) - self.session.commit() - return model.id + super().__init__( + session=conexion.get_session(), + modelo=ModeloOpenAIConfigModel, + mapper=ModeloOpenAIConfigMapper + ) + self.cliente = cliente # Necesario para construir el dominio con lógica def get_by_id(self, id_: str) -> ModeloOpenAI | None: - model = self.session.get(ModeloOpenAIConfigModel, id_) - return ModeloOpenAIConfigMapper.from_model(model, self.cliente) if model else None + model = self.session.get(self.Modelo, id_) + return self.Mapper.from_model(model, self.cliente) if model else None def get_all(self) -> list[ModeloOpenAI]: - models = self.session.query(ModeloOpenAIConfigModel).all() - return [ModeloOpenAIConfigMapper.from_model(m, self.cliente) for m in models] \ No newline at end of file + models = self.session.query(self.Modelo).all() + return [self.Mapper.from_model(m, self.cliente) for m in models] \ No newline at end of file diff --git a/src/TextManager/biblioteca.py b/src/TextManager/biblioteca.py index a43b732..538085b 100644 --- a/src/TextManager/biblioteca.py +++ b/src/TextManager/biblioteca.py @@ -3,7 +3,7 @@ from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que es from typing import List, Optional from src.ConexionSql.Base_conexion import ConexionBase 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 diff --git a/src/TextManager/biblioteca_mmr.py b/src/TextManager/biblioteca_mmr.py index cc0e5bb..163610f 100644 --- a/src/TextManager/biblioteca_mmr.py +++ b/src/TextManager/biblioteca_mmr.py @@ -3,6 +3,10 @@ import base64 from dotenv import load_dotenv 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.base import Base from src.Security.Encriptar import Encriptar_fernet @@ -23,39 +27,32 @@ if pssword is None: # MODELO (SQLAlchemy) # ---------------------- -class BibliotecaModel(Base): +class BibliotecaModel(Base, Model_base): __tablename__ = "bibliotecas" id = Column(String, primary_key=True, 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) - 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 # ---------------------- -class BibliotecaMapper: - @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 - } +class BibliotecaMapper(Mapper_base[Biblioteca, BibliotecaModel]): @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 # Mantienes la lógica actual de no restaurarlo automáticamente + def to_model(obj: Biblioteca) -> BibliotecaModel: + return BibliotecaModel( + id=obj.id, + nombre=obj.nombre, + descripcion=obj.descripcion, + vector_dim=obj.vector_dim + # embedder no se serializa en el modelo, se maneja por separado ) @staticmethod @@ -65,37 +62,51 @@ class BibliotecaMapper: nombre=model.nombre, descripcion=model.descripcion, 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 # ---------------------- -class BibliotecaRepo: +class BibliotecaRepo(Repo_base[BibliotecaModel, Biblioteca]): 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: - # Verificar si ya existe una biblioteca con el mismo nombre - existente = self.session.query(BibliotecaModel).filter_by(nombre=biblioteca.nombre).first() + def add(self, biblioteca: Biblioteca, created_by: str = None, notes: str = None) -> str: + # Lógica personalizada: prevenir duplicados por nombre + existente = self.session.query(self.Modelo).filter_by(nombre=biblioteca.nombre).first() if existente: raise ValueError(f"Ya existe una biblioteca con el nombre '{biblioteca.nombre}'") - data = BibliotecaMapper.to_dict(biblioteca) - 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] + return super().add(biblioteca, created_by=created_by, notes=notes) def get_by_nombre(self, nombre: str) -> Biblioteca | None: - model = self.session.query(BibliotecaModel).filter_by(nombre=nombre).first() - return BibliotecaMapper.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 \ No newline at end of file + model = self.session.query(self.Modelo).filter_by(nombre=nombre, sys_deleted_at=None).first() + return self.Mapper.from_model(model) if model else None \ No newline at end of file diff --git a/src/TextManager/notas_biblioteca_mmr.py b/src/TextManager/notas_biblioteca_mmr.py deleted file mode 100644 index 1e946c5..0000000 --- a/src/TextManager/notas_biblioteca_mmr.py +++ /dev/null @@ -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] diff --git a/src/TextManager/notas_mmr.py b/src/TextManager/notas_mmr.py new file mode 100644 index 0000000..341cc5c --- /dev/null +++ b/src/TextManager/notas_mmr.py @@ -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 \ No newline at end of file