16 Commits

Author SHA1 Message Date
egutierrez 9db2f70009 Actualizacion para mcp 2025-05-16 02:12:33 +02:00
egutierrez 95c1762ca7 env-example añadido 2025-05-15 01:11:24 +02:00
egutierrez 6e716f8f98 Se guarda carpeta Config y pequeños cambios en mcp 2025-05-15 00:51:26 +02:00
egutierrez c13240b481 Notas y bibliotecas funcionando 2025-05-14 02:06:33 +02:00
egutierrez bf1814bb8e Cambios a las 3 bases Model mapper repo para que funcionen a partir de las clases heredando todos los metodos comunes 2025-05-12 01:24:44 +02:00
egutierrez 712bd877b8 Notas en frontend funcionando y pudiendo subir mas por sus endpoints 2025-05-11 02:30:55 +02:00
egutierrez b34d52036e endpoint_biblioteca_funcionando 2025-05-10 20:08:51 +02:00
egutierrez c47b9474f4 feat: Implement text manager API and database connection
- Added `text_manager.py` to handle the creation of text libraries via FastAPI.
- Introduced database connection management in `conexion.py` using PostgreSQL credentials from environment variables.
- Created abstract base class `EmbedderABC` in `Base_Embedder.py` for embedding models.
- Developed `OpenAIEmbedder` class to generate embeddings using OpenAI's API.
- Implemented `OpenAIEmbedderModel` and repository pattern for managing OpenAI embedders in `Openai_embedder_mmr.py`.
- Established `Biblioteca` class for managing text libraries and their associated notes in `biblioteca.py`.
- Created SQLAlchemy models and mappers for `Biblioteca` and `Nota` in `biblioteca_mmr.py` and `notas_biblioteca_mmr.py`.
- Added functionality for dynamic table generation for notes associated with libraries.
- Included comprehensive methods for adding, retrieving, and managing notes and libraries in their respective repositories.
2025-05-10 17:52:43 +02:00
egutierrez c646bc1fef feat: Implement GeneradorIDUnico class for unique ID generation and verification 2025-05-09 23:49:13 +02:00
egutierrez f7879e9557 chore: Allow tracking of config/.env file by updating .gitignore 2025-05-09 19:48:03 +02:00
egutierrez 41b307f4bb chore: Remove specific .env file entry from .gitignore to allow tracking of local configuration 2025-05-09 19:47:48 +02:00
egutierrez 1022e23a0d chore: Remove .env file containing sensitive database and project configuration 2025-05-09 19:47:41 +02:00
egutierrez 0d1ffcd1ff feat: Add ColorSchemeToggle component to HomePage for theme switching functionality 2025-05-09 01:15:19 +02:00
egutierrez b087271255 feat: Update Appshell styling for improved layout; adjust padding and height in title class; modify GridDashboard component for margin in Group 2025-05-09 00:45:48 +02:00
egutierrez 2becc8bc7c feat: Update routing and submenu links for Grid_Dashboard; modify Welcome component text and Appshell styles 2025-05-09 00:22:47 +02:00
egutierrez 8d35da1972 feat: Add GridDashboard component and update routing; replace Prueba_appshell with Grid_Dashboard 2025-05-08 23:31:05 +02:00
68 changed files with 3815 additions and 566 deletions
+1 -1
View File
@@ -4,6 +4,6 @@ from fastapi import APIRouter
router = APIRouter() router = APIRouter()
@router.get("/ping") @router.get("/")
async def ping(): async def ping():
return {"message": "pong"} return {"message": "pong"}
@@ -0,0 +1,109 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import Path
from backend.schemas.text_manager_schema import BibliotecaInput, NotaInput
from fastapi.concurrency import run_in_threadpool
from backend.db.conexion import get_conexion
from backend.services.text_manager_srvc import *
from src.ConexionSql.Postgres_conexion import PostgresConexion
router = APIRouter()
@router.post("/biblioteca", summary="Crear una nueva biblioteca")
async def crear_biblioteca_endpoint(
data: BibliotecaInput,
conexion: PostgresConexion = Depends(get_conexion)
):
try:
return await run_in_threadpool(
crear_biblioteca,
data.nombre_biblioteca,
conexion,
data.descripcion,
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error interno al crear la biblioteca: {str(e)}")
@router.get("/list", summary="Listar todas las bibliotecas")
def listar_todas_bibliotecas(
conexion: PostgresConexion = Depends(get_conexion)
):
try:
return listar_bibliotecas(conexion=conexion)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail="Error interno al listar las bibliotecas")
@router.post("/nota/{biblioteca_id}", summary="Agregar una nota a una biblioteca")
def agregar_nota(
biblioteca_id: str = Path(..., description="ID de la biblioteca a la que se agregará la nota"),
nota: NotaInput = ..., # viene del body
conexion: PostgresConexion = Depends(get_conexion)
):
try:
return agregar_nota_a_biblioteca(
conexion=conexion,
biblioteca_id=biblioteca_id,
titulo=nota.titulo,
texto=nota.texto,
tags=nota.tags,
conexiones=nota.conexiones,
resumen=nota.resumen
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail="Error interno al agregar la nota")
@router.get("/nota/list/{biblioteca_id}", summary="Listar todas las notas de una biblioteca")
def listar_notas(
biblioteca_id: str,
conexion: PostgresConexion = Depends(get_conexion)
):
try:
return listar_notas_de_biblioteca(conexion, biblioteca_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail="Error interno al 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")
+3 -2
View File
@@ -1,7 +1,8 @@
# backend/api/router.py # backend/api/router.py
from fastapi import APIRouter from fastapi import APIRouter
from backend.api.v1.endpoints import ping from backend.api.v1.endpoints import ping, text_manager_endpoint
router = APIRouter() router = APIRouter()
router.include_router(ping.router, prefix="/api/v1") router.include_router(ping.router, prefix="/api/v1/ping")
router.include_router(text_manager_endpoint.router, prefix="/api/v1/text_manager")
+10
View File
@@ -0,0 +1,10 @@
# backend/db/conexion.py
from entrypoint.init_db import db_credencial
from src.ConexionSql.Postgres_conexion import PostgresConexion
def get_conexion():
conexion = PostgresConexion(db_credencial)
try:
yield conexion
finally:
conexion.close()
+1 -1
View File
@@ -13,7 +13,7 @@ app = FastAPI(
# Configuración de CORS # Configuración de CORS
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["http://localhost:5173"], # Solo permite tu frontend local allow_origins=["http://localhost:5173", "http://0.0.0.0:5173"], # Solo permite tu frontend local
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
+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
+191
View File
@@ -0,0 +1,191 @@
from src.TextManager.biblioteca import Biblioteca
from src.TextManager.biblioteca_mmr import BibliotecaRepo
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_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 = None):
print("[INICIO] Creando biblioteca...")
try:
print("[Paso 1] Obteniendo credencial...")
cred_repo = OpenAICredencialRepo(conexion)
credencial = cred_repo.get_by_id("OPAK20250513-61b29978b7604031014")
print("[OK] Credencial obtenida:", credencial.titulo if credencial else "❌ None")
print("[Paso 2] Instanciando embedder...")
embedder = OpenAIEmbedder(credencial, model="text-embedding-3-large")
print("[OK] Embedder instanciado")
print("[Paso 3] Instanciando biblioteca...")
biblioteca = Biblioteca(
nombre=nombre_biblioteca,
embedder=embedder,
descripcion=descripcion
)
print(f"[OK] Biblioteca instanciada con ID: {biblioteca.id}")
print("[Paso 4] Guardando en base de datos...")
repo = BibliotecaRepo(conexion)
repo.add(biblioteca=biblioteca)
print("[OK] Biblioteca guardada")
print("[Paso 5] Generando modelo de notas...")
biblioteca.generar_modelo_notas(conexion)
print("[OK] Modelo de notas generado")
print("[FIN] Biblioteca creada correctamente")
return {
"mensaje": f"Biblioteca '{nombre_biblioteca}' creada con éxito.",
"id": biblioteca.id
}
except Exception as e:
print("[ERROR] Ocurrió una excepción:", str(e))
raise
def listar_bibliotecas(conexion: PostgresConexion) -> list[dict]:
repo = BibliotecaRepo(conexion)
bibliotecas: list[Biblioteca] = repo.get_all()
return [
{
"id": b.id,
"nombre": b.nombre,
"descripcion": b.descripcion,
"vector_dim": b.vector_dim
}
for b in bibliotecas
]
def agregar_nota_a_biblioteca(
conexion: PostgresConexion,
biblioteca_id: str,
titulo: str,
texto: str = "",
tags: list[str] = None,
conexiones: list[str] = None,
resumen: str = ""
):
# Obtener la biblioteca
repo_biblioteca = BibliotecaRepo(conexion)
biblioteca = repo_biblioteca.get_by_id(biblioteca_id)
if biblioteca is None:
raise ValueError(f"No se encontró la biblioteca con ID {biblioteca_id}")
# Crear objeto Nota
nota = Nota(
titulo=titulo,
texto=texto,
tags=tags or [],
conexiones=conexiones or [],
resumen=resumen or "",
# vector=biblioteca.embedder.embed_text(texto),
# vector_resumen=biblioteca.embedder.embed_text(resumen) if resumen else None
)
# Mostrar atributos seguros
print(
f"[DEBUG] Nota creada: titulo='{nota.titulo}', "
f"texto_len={len(nota.texto)}, "
f"tags={len(nota.tags)}, "
f"conexiones={len(nota.conexiones)}, "
f"resumen_len={len(nota.resumen)}"
)
# Preparar tabla y modelo de nota
metadata = MetaData()
tabla, ModeloNota = generar_tabla_nota_para_biblioteca(
biblioteca.nombre,
biblioteca.vector_dim,
metadata
)
metadata.create_all(conexion.get_engine())
# Guardar la nota
repo_nota = NotaRepo(conexion.get_session(), ModeloNota)
nota_id = repo_nota.add(nota)
resultado = {
"mensaje": f"Nota '{titulo}' agregada con éxito a la biblioteca '{biblioteca.nombre}'.",
"nota_id": nota_id
}
print(f"[SUCCESS] {resultado['mensaje']}")
return resultado
def listar_notas_de_biblioteca(conexion: PostgresConexion, biblioteca_id: str) -> list[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()
tabla, 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)
notas = repo_nota.get_all()
return [
{
"id": n.id,
"titulo": n.titulo,
"tags": n.tags,
"texto": n.texto,
"resumen": n.resumen,
"conexiones": n.conexiones
}
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}")
+17
View File
@@ -0,0 +1,17 @@
import os
import shutil
def eliminar_pycache(directorio):
eliminados = 0
for root, dirs, files in os.walk(directorio):
for d in dirs:
if d == "__pycache__":
ruta = os.path.join(root, d)
print(f"🗑️ Eliminando: {ruta}")
shutil.rmtree(ruta)
eliminados += 1
print(f"\n✅ Eliminados {eliminados} directorios '__pycache__'.")
if __name__ == "__main__":
ruta_raiz = os.path.dirname(os.path.abspath(__file__)) # Carpeta actual
eliminar_pycache(ruta_raiz)
+9
View File
@@ -0,0 +1,9 @@
import asyncio
from fastmcp.client import Client
async def main():
async with Client("http://127.0.0.1:4300") as client:
is_alive = await client.ping()
print("Ping exitoso:", is_alive)
asyncio.run(main())
-14
View File
@@ -1,14 +0,0 @@
# Tipo de base de datos
DB_TITLE=Production_Fitz_db
DB_USER=postgres
DB_PASSWORD=7Souw9SFD5P5RYpRWuTVvFkY7zlxATcN
DB_HOST=localhost
DB_PORT=5432
DB_NAME=fitz_db
# Contraseña maestra de la aplicacion
MASTER_PASSWORD=¤òAíÀÇ®8IgÄËïºÅ2a3duÎ4Ô¯5¦ï¤ç··sôÃmL9xWß
#Ruta del proyecto
PROJECT_PATH= E:\Fitz_Studio
+14
View File
@@ -0,0 +1,14 @@
# Tipo de base de datos
DB_TITLE=
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
DB_NAME=
# Contraseña maestra de la aplicacion
MASTER_PASSWORD=
#Ruta del proyecto
PROJECT_PATH=
View File
+42 -1
View File
@@ -41,6 +41,7 @@ E:\Fitz_Studio
│ │ ├── 0e │ │ ├── 0e
│ │ ├── 10 │ │ ├── 10
│ │ ├── 11 │ │ ├── 11
│ │ ├── 13
│ │ ├── 15 │ │ ├── 15
│ │ ├── 17 │ │ ├── 17
│ │ ├── 18 │ │ ├── 18
@@ -55,9 +56,11 @@ E:\Fitz_Studio
│ │ ├── 22 │ │ ├── 22
│ │ ├── 23 │ │ ├── 23
│ │ ├── 24 │ │ ├── 24
│ │ ├── 25
│ │ ├── 26 │ │ ├── 26
│ │ ├── 27 │ │ ├── 27
│ │ ├── 28 │ │ ├── 28
│ │ ├── 2b
│ │ ├── 2c │ │ ├── 2c
│ │ ├── 2d │ │ ├── 2d
│ │ ├── 2f │ │ ├── 2f
@@ -65,11 +68,14 @@ E:\Fitz_Studio
│ │ ├── 32 │ │ ├── 32
│ │ ├── 33 │ │ ├── 33
│ │ ├── 34 │ │ ├── 34
│ │ ├── 36
│ │ ├── 39 │ │ ├── 39
│ │ ├── 3a
│ │ ├── 3c │ │ ├── 3c
│ │ ├── 3d │ │ ├── 3d
│ │ ├── 3e │ │ ├── 3e
│ │ ├── 3f │ │ ├── 3f
│ │ ├── 40
│ │ ├── 41 │ │ ├── 41
│ │ ├── 42 │ │ ├── 42
│ │ ├── 43 │ │ ├── 43
@@ -83,10 +89,14 @@ E:\Fitz_Studio
│ │ ├── 4d │ │ ├── 4d
│ │ ├── 4e │ │ ├── 4e
│ │ ├── 4f │ │ ├── 4f
│ │ ├── 50
│ │ ├── 51 │ │ ├── 51
│ │ ├── 52 │ │ ├── 52
│ │ ├── 55 │ │ ├── 55
│ │ ├── 56
│ │ ├── 57 │ │ ├── 57
│ │ ├── 58
│ │ ├── 59
│ │ ├── 5a │ │ ├── 5a
│ │ ├── 5b │ │ ├── 5b
│ │ ├── 5c │ │ ├── 5c
@@ -100,6 +110,7 @@ E:\Fitz_Studio
│ │ ├── 65 │ │ ├── 65
│ │ ├── 67 │ │ ├── 67
│ │ ├── 69 │ │ ├── 69
│ │ ├── 6b
│ │ ├── 6c │ │ ├── 6c
│ │ ├── 6d │ │ ├── 6d
│ │ ├── 6e │ │ ├── 6e
@@ -109,27 +120,33 @@ E:\Fitz_Studio
│ │ ├── 75 │ │ ├── 75
│ │ ├── 76 │ │ ├── 76
│ │ ├── 77 │ │ ├── 77
│ │ ├── 79
│ │ ├── 7b │ │ ├── 7b
│ │ ├── 7c │ │ ├── 7c
│ │ ├── 7d │ │ ├── 7d
│ │ ├── 7f │ │ ├── 7f
│ │ ├── 80 │ │ ├── 80
│ │ ├── 81 │ │ ├── 81
│ │ ├── 82
│ │ ├── 83 │ │ ├── 83
│ │ ├── 84 │ │ ├── 84
│ │ ├── 85 │ │ ├── 85
│ │ ├── 86
│ │ ├── 87 │ │ ├── 87
│ │ ├── 89 │ │ ├── 89
│ │ ├── 8a │ │ ├── 8a
│ │ ├── 8b │ │ ├── 8b
│ │ ├── 8c │ │ ├── 8c
│ │ ├── 8d
│ │ ├── 90 │ │ ├── 90
│ │ ├── 92
│ │ ├── 94 │ │ ├── 94
│ │ ├── 95 │ │ ├── 95
│ │ ├── 97 │ │ ├── 97
│ │ ├── 98 │ │ ├── 98
│ │ ├── 99 │ │ ├── 99
│ │ ├── 9a │ │ ├── 9a
│ │ ├── 9b
│ │ ├── 9c │ │ ├── 9c
│ │ ├── 9d │ │ ├── 9d
│ │ ├── a0 │ │ ├── a0
@@ -148,7 +165,9 @@ E:\Fitz_Studio
│ │ ├── ad │ │ ├── ad
│ │ ├── ae │ │ ├── ae
│ │ ├── af │ │ ├── af
│ │ ├── b0
│ │ ├── b1 │ │ ├── b1
│ │ ├── b2
│ │ ├── b3 │ │ ├── b3
│ │ ├── b4 │ │ ├── b4
│ │ ├── b5 │ │ ├── b5
@@ -158,6 +177,7 @@ E:\Fitz_Studio
│ │ ├── ba │ │ ├── ba
│ │ ├── bb │ │ ├── bb
│ │ ├── bc │ │ ├── bc
│ │ ├── bd
│ │ ├── bf │ │ ├── bf
│ │ ├── c0 │ │ ├── c0
│ │ ├── c3 │ │ ├── c3
@@ -171,6 +191,7 @@ E:\Fitz_Studio
│ │ ├── ce │ │ ├── ce
│ │ ├── cf │ │ ├── cf
│ │ ├── d1 │ │ ├── d1
│ │ ├── d3
│ │ ├── d4 │ │ ├── d4
│ │ ├── d5 │ │ ├── d5
│ │ ├── d6 │ │ ├── d6
@@ -184,6 +205,8 @@ E:\Fitz_Studio
│ │ ├── de │ │ ├── de
│ │ ├── df │ │ ├── df
│ │ ├── e0 │ │ ├── e0
│ │ ├── e1
│ │ ├── e2
│ │ ├── e3 │ │ ├── e3
│ │ ├── e4 │ │ ├── e4
│ │ ├── e5 │ │ ├── e5
@@ -202,6 +225,7 @@ E:\Fitz_Studio
│ │ ├── f6 │ │ ├── f6
│ │ ├── f7 │ │ ├── f7
│ │ ├── f9 │ │ ├── f9
│ │ ├── fa
│ │ ├── fb │ │ ├── fb
│ │ ├── fc │ │ ├── fc
│ │ ├── fd │ │ ├── fd
@@ -298,6 +322,7 @@ E:\Fitz_Studio
│ ├── jupyter │ ├── jupyter
│ └── man │ └── man
├── Apikeys.ipynb ├── Apikeys.ipynb
├── Apikeys_embedding.ipynb
├── Credenciales.ipynb ├── Credenciales.ipynb
├── Encriptacion.ipynb ├── Encriptacion.ipynb
├── README.md ├── README.md
@@ -310,6 +335,9 @@ E:\Fitz_Studio
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ ├── __pycache__ │ │ ├── __pycache__
│ │ └── v1 │ │ └── v1
│ ├── db
│ │ ├── __init__.py
│ │ └── conexion.py
│ ├── deps │ ├── deps
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ └── auth.py │ │ └── auth.py
@@ -721,16 +749,19 @@ E:\Fitz_Studio
│ │ ├── queue-microtask │ │ ├── queue-microtask
│ │ ├── range-parser │ │ ├── range-parser
│ │ ├── raw-body │ │ ├── raw-body
│ │ ├── re-resizable
│ │ ├── react │ │ ├── react
│ │ ├── react-docgen │ │ ├── react-docgen
│ │ ├── react-docgen-typescript │ │ ├── react-docgen-typescript
│ │ ├── react-dom │ │ ├── react-dom
│ │ ├── react-draggable
│ │ ├── react-is │ │ ├── react-is
│ │ ├── react-number-format │ │ ├── react-number-format
│ │ ├── react-reconciler │ │ ├── react-reconciler
│ │ ├── react-refresh │ │ ├── react-refresh
│ │ ├── react-remove-scroll │ │ ├── react-remove-scroll
│ │ ├── react-remove-scroll-bar │ │ ├── react-remove-scroll-bar
│ │ ├── react-rnd
│ │ ├── react-router │ │ ├── react-router
│ │ ├── react-router-dom │ │ ├── react-router-dom
│ │ ├── react-style-singleton │ │ ├── react-style-singleton
@@ -892,9 +923,9 @@ E:\Fitz_Studio
│ │ ├── Router.tsx │ │ ├── Router.tsx
│ │ ├── assets │ │ ├── assets
│ │ ├── components │ │ ├── components
│ │ ├── data
│ │ ├── main.tsx │ │ ├── main.tsx
│ │ ├── pages │ │ ├── pages
│ │ ├── public
│ │ ├── theme.ts │ │ ├── theme.ts
│ │ ├── types │ │ ├── types
│ │ └── vite-env.d.ts │ │ └── vite-env.d.ts
@@ -905,6 +936,7 @@ E:\Fitz_Studio
│ ├── vite.config.js │ ├── vite.config.js
│ ├── vitest.setup.mjs │ ├── vitest.setup.mjs
│ └── yarn.lock │ └── yarn.lock
├── github_tutorial.ipynb
├── main.py ├── main.py
├── notebooks ├── notebooks
│ └── hacer_script_nombres.ipynb │ └── hacer_script_nombres.ipynb
@@ -958,6 +990,7 @@ E:\Fitz_Studio
│ │ └── postgres_credencial_mmr.py │ │ └── postgres_credencial_mmr.py
│ ├── Llms │ ├── Llms
│ │ ├── Agente.py │ │ ├── Agente.py
│ │ ├── Embedders
│ │ ├── MCPs │ │ ├── MCPs
│ │ ├── Memory │ │ ├── Memory
│ │ ├── Modelos │ │ ├── Modelos
@@ -965,8 +998,16 @@ E:\Fitz_Studio
│ │ └── __pycache__ │ │ └── __pycache__
│ ├── Security │ ├── Security
│ │ ├── Encriptar.py │ │ ├── Encriptar.py
│ │ ├── GenerarIDs.py
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ └── __pycache__ │ │ └── __pycache__
│ ├── TextManager
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ ├── biblioteca.py
│ │ ├── biblioteca_mmr.py
│ │ ├── nota.py
│ │ └── notas_biblioteca_mmr.py
│ ├── __init__.py │ ├── __init__.py
│ ├── __pycache__ │ ├── __pycache__
│ │ ├── __init__.cpython-311.pyc │ │ ├── __init__.cpython-311.pyc
+4 -1
View File
@@ -7,6 +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.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
@@ -41,7 +44,7 @@ def init_db():
# Crear engine desde la clase de conexión PostgreSQL # Crear engine desde la clase de conexión PostgreSQL
conexion = PostgresConexion(db_credencial) conexion = PostgresConexion(db_credencial)
engine = conexion.engine # Recuperamos el engine directamente engine = conexion.get_engine() # Recuperamos el engine directamente
print("Creando tablas...") print("Creando tablas...")
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
+156 -19
View File
@@ -13,8 +13,10 @@
"@react-three/fiber": "^9.1.2", "@react-three/fiber": "^9.1.2",
"@tabler/icons": "^3.31.0", "@tabler/icons": "^3.31.0",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"axios": "^1.9.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-rnd": "^10.5.2",
"react-router-dom": "^7.4.0" "react-router-dom": "^7.4.0"
}, },
"devDependencies": { "devDependencies": {
@@ -3303,6 +3305,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -3329,6 +3337,17 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3551,7 +3570,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -3712,6 +3730,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -4052,6 +4082,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -4126,7 +4165,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
@@ -4278,7 +4316,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -4288,7 +4325,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -4333,7 +4369,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@@ -4346,7 +4381,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -5060,6 +5094,26 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -5093,6 +5147,42 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -5132,7 +5222,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -5183,7 +5272,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@@ -5217,7 +5305,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
@@ -5412,7 +5499,6 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -5491,7 +5577,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -5504,7 +5589,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@@ -5520,7 +5604,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@@ -6268,7 +6351,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
@@ -6513,7 +6595,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0" "js-tokens": "^3.0.0 || ^4.0.0"
@@ -6580,7 +6661,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -6842,7 +6922,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -7531,7 +7610,6 @@
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
@@ -7543,7 +7621,6 @@
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
@@ -7560,6 +7637,12 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -7633,6 +7716,16 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/re-resizable": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz",
"integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "19.1.0", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
@@ -7699,6 +7792,29 @@
"react": "^19.1.0" "react": "^19.1.0"
} }
}, },
"node_modules/react-draggable": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz",
"integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==",
"license": "MIT",
"dependencies": {
"clsx": "^1.1.1",
"prop-types": "^15.8.1"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/react-draggable/node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -7794,6 +7910,27 @@
} }
} }
}, },
"node_modules/react-rnd": {
"version": "10.5.2",
"resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz",
"integrity": "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==",
"license": "MIT",
"dependencies": {
"re-resizable": "6.11.2",
"react-draggable": "4.4.6",
"tslib": "2.6.2"
},
"peerDependencies": {
"react": ">=16.3.0",
"react-dom": ">=16.3.0"
}
},
"node_modules/react-rnd/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"license": "0BSD"
},
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.5.3", "version": "7.5.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz",
+2
View File
@@ -25,8 +25,10 @@
"@react-three/fiber": "^9.1.2", "@react-three/fiber": "^9.1.2",
"@tabler/icons": "^3.31.0", "@tabler/icons": "^3.31.0",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"axios": "^1.9.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-rnd": "^10.5.2",
"react-router-dom": "^7.4.0" "react-router-dom": "^7.4.0"
}, },
"devDependencies": { "devDependencies": {
+10 -3
View File
@@ -2,7 +2,8 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { HomePage } from './pages/Home.page'; import { HomePage } from './pages/Home.page';
import { Consulta_API } from './pages/Consulta_api'; import { Consulta_API } from './pages/Consulta_api';
import { Error_404 } from './pages/404'; // Ajusta si está en otra carpeta import { Error_404 } from './pages/404'; // Ajusta si está en otra carpeta
import { Prueba_appshell } from './pages/Prueba_appshell'; // Ajusta si está en otra carpeta import { Grid_Dashboard } from './pages/Grid_dashboard'; // Ajusta si está en otra carpeta
import { Biblioteca } from './pages/Biblioteca';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -14,9 +15,15 @@ const router = createBrowserRouter([
element: <Consulta_API />, element: <Consulta_API />,
}, },
{ {
path: '/prueba_appshell', path: '/Grid_Dashboard',
element: <Prueba_appshell />, element: <Grid_Dashboard />,
}, },
{
path: '/Biblioteca',
element: <Biblioteca />,
},
{ {
path: '*', path: '*',
element: <Error_404 />, element: <Error_404 />,
@@ -16,9 +16,9 @@
var(--mantine-font-family); var(--mantine-font-family);
margin-bottom: var(--mantine-spacing-sm); margin-bottom: var(--mantine-spacing-sm);
background-color: var(--mantine-color-body); background-color: var(--mantine-color-body);
padding: var(--mantine-spacing-md); padding: var(--mantine-spacing-xs);
padding-top: 18px; padding-top: 15px;
height: px; height: 50px;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7)); border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
} }
@@ -97,7 +97,7 @@
&, &,
&:hover { &:hover {
background-color: var(--mantine-color-brand-7); background-color: var(--mantine-color-brand-7);
color: linear-gradient(90deg, var(--mantine-color-brand-7), var(--mantine-color-brand-4)); color: var(--mantine-color-brand-2);
} }
} }
} }
+177 -137
View File
@@ -2,150 +2,190 @@ import {
AppShell, AppShell,
Burger, Burger,
Group, Group,
Skeleton,
Tooltip, Tooltip,
UnstyledButton, UnstyledButton,
ActionIcon,
Title, Title,
useMantineTheme,
} from '@mantine/core'; } from '@mantine/core';
import { useDisclosure, useMediaQuery } from '@mantine/hooks';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { default as LogoIcon } from '../../assets/icons/favicon'; // ruta relativa ajusta según tu estructura import { default as LogoIcon } from '../../assets/icons/favicon';
import { mainLinksdata } from '../../data/navigationsLinks_1';
import { submenuLinks } from '../../data/submenuLinks_1';
import { useMantineTheme } from '@mantine/core'; import classes from './Appshell.module.css';
import { mainLinksdata } from '../../data/navigationsLinks_1'; // ajusta la ruta type AppShellWithMenuProps = {
import { submenuLinks } from '../../data/submenuLinks_1'; // ajusta la ruta según tu estructura children?: React.ReactNode;
};
// Persistencia en localStorage
import { useDisclosure, useMediaQuery } from '@mantine/hooks'; const STORAGE_KEY = 'lastSubmenuRoutes';
import { useEffect, useMemo, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import classes from './Appshell.module.css';
type AppShellWithMenuProps = { function getLastSubmenuRoute(section: string): string | null {
children?: React.ReactNode; // <- ahora es opcional try {
}; const raw = localStorage.getItem(STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : {};
export function AppShellWithMenu({ children }: AppShellWithMenuProps) { return parsed[section] ?? null;
} catch {
const theme = useMantineTheme(); return null;
const location = useLocation();
const isMobile = useMediaQuery('(max-width: 768px)');
const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] = useDisclosure(false);
const [desktopOpened, { toggle: toggleDesktop, open: openDesktop }] = useDisclosure(true);
const isCollapsed = useMemo(() => (isMobile ? !mobileOpened : !desktopOpened), [isMobile, mobileOpened, desktopOpened]);
const [manualActiveTab, setManualActiveTab] = useState<string | null>(null);
const matchedMain = Object.entries(submenuLinks).find(([mainKey, items]) =>
items.some((item) => location.pathname.startsWith(item.to))
);
const routeBasedActive = matchedMain?.[0] ?? 'Home';
const active = manualActiveTab ?? routeBasedActive;
const activeLink = submenuLinks[active as keyof typeof submenuLinks]?.find((item) => location.pathname === item.to)?.label ?? '';
const mainLinks = mainLinksdata.map((link) => (
<Tooltip
label={link.label}
position="right"
withArrow
transitionProps={{ duration: 0 }}
key={link.label}
>
<UnstyledButton
onClick={() => {
setManualActiveTab(link.label);
}}
className={classes.mainLink}
data-active={link.label === active || undefined}
>
<link.icon />
</UnstyledButton>
</Tooltip>
));
const links = (submenuLinks[active as keyof typeof submenuLinks] || []).map((item) => (
<Link
className={classes.link}
data-active={activeLink === item.label || undefined}
to={item.to}
key={item.label}
style={{ display: isCollapsed ? 'none' : 'block' }}
onClick={() => {
if (isMobile) closeMobile();
}}
>
{item.label}
</Link>
));
useEffect(() => {
setManualActiveTab(null);
}, [location.pathname]);
useEffect(() => {
if (!isMobile) openDesktop();
}, [isMobile, openDesktop]);
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: isCollapsed ? 60 : 300,
breakpoint: 'sm',
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
padding="md"
>
{/* Header */}
<AppShell.Header>
<Group h="100%" px="sm">
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
<LogoIcon
style={{ width: 30, height: 30 }}
circleFill={theme.colors.brand[9]}
pathFill={theme.colors.secondary[2]}
/>
</Group>
</AppShell.Header>
{/* Navbar */}
<AppShell.Navbar>
<div className={classes.wrapper}>
<div className={classes.aside}>
<div className={classes.topSection}>
{mainLinks}
</div>
</div>
<div className={classes.main}>
{!isCollapsed && <Title order={4} className={classes.title}>{active}</Title>}
{links}
</div>
</div>
</AppShell.Navbar>
{/* Main Content */}
<AppShell.Main>
{children}
</AppShell.Main>
</AppShell>
);
} }
}
function setLastSubmenuRoute(section: string, route: string) {
try {
const raw = localStorage.getItem(STORAGE_KEY);
const parsed = raw ? JSON.parse(raw) : {};
parsed[section] = route;
localStorage.setItem(STORAGE_KEY, JSON.stringify(parsed));
} catch {
// fallback silencioso
}
}
export function AppShellWithMenu({ children }: AppShellWithMenuProps) {
const theme = useMantineTheme();
const location = useLocation();
const navigate = useNavigate();
const isMobile = useMediaQuery('(max-width: 768px)');
const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] = useDisclosure(false);
const [desktopOpened, { toggle: toggleDesktop, open: openDesktop }] = useDisclosure(true);
const isCollapsed = useMemo(
() => (isMobile ? !mobileOpened : !desktopOpened),
[isMobile, mobileOpened, desktopOpened]
);
// Estado para el main link activo
const [activeMain, setActiveMain] = useState<string>('Home');
// Ref para saber si el usuario ha hecho clic manualmente en el main link
const userClickedMainRef = useRef(false);
useEffect(() => {
const currentPath = location.pathname.toLowerCase().replace(/\/$/, '');
let matchedMain: string | null = null;
let maxMatchLength = 0;
Object.entries(submenuLinks).forEach(([main, items]) => {
items.forEach((item) => {
const itemPath = item.to.toLowerCase().replace(/\/$/, '');
if (
currentPath === itemPath ||
currentPath.startsWith(itemPath + '/')
) {
if (itemPath.length > maxMatchLength) {
matchedMain = main;
maxMatchLength = itemPath.length;
}
}
});
});
if (matchedMain) {
setActiveMain(matchedMain);
}
}, [location.pathname]);
const activeLink =
submenuLinks[activeMain as keyof typeof submenuLinks]?.find(
(item) => item.to === location.pathname
)?.label ?? '';
const mainLinks = mainLinksdata.map((link) => (
<Tooltip
label={link.label}
position="right"
withArrow
transitionProps={{ duration: 0 }}
key={link.label}
>
<UnstyledButton
onClick={() => {
userClickedMainRef.current = true;
setActiveMain(link.label);
const remembered = getLastSubmenuRoute(link.label);
const fallback = submenuLinks[link.label as keyof typeof submenuLinks]?.[0]?.to;
if (isCollapsed && (remembered || fallback)) {
navigate(remembered ?? fallback);
}
}}
className={classes.mainLink}
data-active={link.label === activeMain || undefined}
>
<link.icon />
</UnstyledButton>
</Tooltip>
));
const links = (submenuLinks[activeMain as keyof typeof submenuLinks] || []).map((item) => (
<Link
className={classes.link}
data-active={activeLink === item.label || undefined}
to={item.to}
key={item.label}
style={{ display: isCollapsed ? 'none' : 'block' }}
onClick={() => {
setLastSubmenuRoute(activeMain, item.to);
if (isMobile) closeMobile();
}}
>
{item.label}
</Link>
));
useEffect(() => {
if (!isMobile) openDesktop();
}, [isMobile, openDesktop]);
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: isCollapsed ? 60 : 300,
breakpoint: 'sm',
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
padding="md"
>
{/* Header */}
<AppShell.Header>
<Group h="100%" px="sm">
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
<LogoIcon
style={{ width: 30, height: 30 }}
circleFill={theme.colors.brand[9]}
pathFill={theme.colors.secondary[2]}
/>
</Group>
</AppShell.Header>
{/* Navbar */}
<AppShell.Navbar>
<div className={classes.wrapper}>
<div className={classes.aside}>
<div className={classes.topSection}>{mainLinks}</div>
</div>
<div className={classes.main}>
{!isCollapsed && (
<Title order={4} className={classes.title}>
{activeMain}
</Title>
)}
{links}
</div>
</div>
</AppShell.Navbar>
{/* Main Content */}
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
);
}
+114
View File
@@ -0,0 +1,114 @@
import { Card, Text, Switch, Group, useMantineTheme, useComputedColorScheme } from '@mantine/core';
import { Rnd } from 'react-rnd';
import { useState } from 'react';
const GRID_SIZE = 30;
function hexToRgba(hex: string, alpha: number): string {
const sanitized = hex.replace('#', '');
const bigint = parseInt(sanitized, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
interface CardData {
id: string;
x: number;
y: number;
width: number;
height: number;
}
const initialCards: CardData[] = [
{ id: '1', x: 0, y: 0, width: GRID_SIZE * 2, height: GRID_SIZE * 2 },
{ id: '2', x: GRID_SIZE * 2, y: 0, width: GRID_SIZE * 3, height: GRID_SIZE * 2 },
{ id: '3', x: GRID_SIZE * 3, y: 0, width: GRID_SIZE * 3, height: GRID_SIZE * 2 },
];
export const GridDashboard = () => {
const theme = useMantineTheme();
const colorScheme = useComputedColorScheme(); // ✅ directamente 'light' o 'dark'
const isDark = colorScheme === 'dark';
// Color de la rejilla adaptado al modo del tema
const gridBaseColor = isDark ? theme.colors.dark[4] : theme.colors.gray[3];
const gridColor = hexToRgba(gridBaseColor, 0.25); // Ajusta la opacidad aquí
const [cards, setCards] = useState<CardData[]>(initialCards);
const [showGrid, setShowGrid] = useState(true);
const updateCard = (id: string, updates: Partial<CardData>) => {
setCards((prev) =>
prev.map((card) => (card.id === id ? { ...card, ...updates } : card))
);
};
return (
<>
<Group mb="xs">
<Switch
checked={showGrid}
onChange={(event) => setShowGrid(event.currentTarget.checked)}
label="Mostrar cuadrícula"
/>
</Group>
<div
style={{
width: '100%',
height: '700px',
backgroundSize: `${GRID_SIZE}px ${GRID_SIZE}px`,
backgroundImage: showGrid
? `linear-gradient(to right, ${gridColor} 1px, transparent 1px),
linear-gradient(to bottom, ${gridColor} 1px, transparent 1px)`
: 'none',
position: 'relative',
}}
>
{cards.map((card) => (
<Rnd
key={card.id}
size={{
width: Math.round(card.width / GRID_SIZE) * GRID_SIZE,
height: Math.round(card.height / GRID_SIZE) * GRID_SIZE,
}}
position={{ x: card.x, y: card.y }}
minWidth={GRID_SIZE * 8}
minHeight={GRID_SIZE * 8}
bounds="parent"
grid={[GRID_SIZE, GRID_SIZE]}
onDragStop={(_, d) =>
updateCard(card.id, {
x: Math.round(d.x / GRID_SIZE) * GRID_SIZE,
y: Math.round(d.y / GRID_SIZE) * GRID_SIZE,
})
}
onResizeStop={(_, __, ref, ___, pos) =>
updateCard(card.id, {
width: Math.round(ref.offsetWidth / GRID_SIZE) * GRID_SIZE,
height: Math.round(ref.offsetHeight / GRID_SIZE) * GRID_SIZE,
x: Math.round(pos.x / GRID_SIZE) * GRID_SIZE,
y: Math.round(pos.y / GRID_SIZE) * GRID_SIZE,
})
}
>
<Card
shadow="sm"
padding="md"
radius="md"
withBorder
style={{ height: '100%', userSelect: 'none' }}
>
<Text fw={500}>Card {card.id}</Text>
<Text size="sm">Mueve o redimensiona</Text>
</Card>
</Rnd>
))}
</div>
</>
);
};
+1 -1
View File
@@ -13,7 +13,7 @@ import { MetodoSelect } from './MetodoSelect';
import { useMantineTheme } from '@mantine/core'; import { useMantineTheme } from '@mantine/core';
export function LlamadorAPI() { export function LlamadorAPI() {
const [direccion, setDireccion] = useState('http://localhost:8000/api/saludo'); const [direccion, setDireccion] = useState('http://localhost:8000/api/v1/ping/');
const [metodo, setMetodo] = useState('GET'); const [metodo, setMetodo] = useState('GET');
const [contenido, setContenido] = useState(''); const [contenido, setContenido] = useState('');
const [respuesta, setRespuesta] = useState(''); const [respuesta, setRespuesta] = useState('');
+1 -1
View File
@@ -11,7 +11,7 @@ export function Welcome() {
<Title className={classes.title} ta="center" mt={100}> <Title className={classes.title} ta="center" mt={100}>
Hola! {' '} Hola! {' '}
<Text inherit variant="gradient" component="span" gradient={{ from: theme.colors.brand[7], to: theme.colors.secondary[4], }} style={{ letterSpacing: '1px' }}> <Text inherit variant="gradient" component="span" gradient={{ from: theme.colors.brand[7], to: theme.colors.secondary[4], }} style={{ letterSpacing: '1px' }}>
Egutierrez Holooooo
</Text> </Text>
</Title> </Title>
<Text c="dimmed" ta="left" size="lg" maw={580} mx="auto" mt="xl"> <Text c="dimmed" ta="left" size="lg" maw={580} mx="auto" mt="xl">
+3 -1
View File
@@ -4,10 +4,12 @@ export const submenuLinks = {
Home: [ Home: [
{ label: 'Inicio', to: '/' }, { label: 'Inicio', to: '/' },
{ label: 'Consulta Api', to: '/Consulta_API' }, { label: 'Consulta Api', to: '/Consulta_API' },
{ label: 'Prueba_appshell', to: '/prueba_appshell' }, { label: 'Biblioteca', to: '/Biblioteca' },
], ],
Dashboard: [ Dashboard: [
{ label: 'Resumen', to: '/dashboard/resumen' }, { label: 'Resumen', to: '/dashboard/resumen' },
{ label: 'Grid_Dashboard', to: '/Grid_Dashboard' },
{ label: 'Estadísticas', to: '/dashboard/estadisticas' }, { label: 'Estadísticas', to: '/dashboard/estadisticas' },
{ label: 'Usuarios', to: '/dashboard/usuarios' }, { label: 'Usuarios', to: '/dashboard/usuarios' },
], ],
+347
View File
@@ -0,0 +1,347 @@
import { useEffect, useState } from 'react';
import {
AppShell,
Stack,
Card,
Text,
Title,
ScrollArea,
Group,
Button,
TextInput,
Modal,
Box,
Loader,
Textarea
} from '@mantine/core';
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import axios from 'axios';
type Nota = {
id: string;
titulo: string;
texto: string;
};
type Biblioteca = {
id: string;
nombre: string;
descripcion: string;
notas: Nota[];
};
export function Biblioteca() {
const [bibliotecas, setBibliotecas] = useState<Biblioteca[]>([]);
const [bibliotecaSeleccionada, setBibliotecaSeleccionada] = useState<Biblioteca | null>(null);
const [modalAbierto, setModalAbierto] = useState(false);
const [tituloNota, setTituloNota] = useState('');
const [contenidoNota, setContenidoNota] = useState('');
const [loadingNotas, setLoadingNotas] = useState(false);
const [notaEnEdicion, setNotaEnEdicion] = useState<Nota | null>(null);
const [modalEditarAbierto, setModalEditarAbierto] = useState(false);
const [modalNuevaBiblio, setModalNuevaBiblio] = useState(false);
const [nombreBiblio, setNombreBiblio] = useState('');
const [descripcionBiblio, setDescripcionBiblio] = useState('');
const [loadingNuevaBiblio, setLoadingNuevaBiblio] = useState(false);
const fetchBibliotecas = async () => {
try {
const res = await axios.get('/api/v1/text_manager/list');
console.log('📦 Respuesta del backend:', res.data);
if (!Array.isArray(res.data)) {
console.error('❌ La respuesta no es un array:', res.data);
return;
}
const bibliotecasConNotas = await Promise.all(
res.data.map(async (biblio: Omit<Biblioteca, 'notas'>) => {
const notas = await axios.get(`/api/v1/text_manager/nota/list/${biblio.id}`);
return { ...biblio, notas: notas.data as Nota[] };
})
);
setBibliotecas(bibliotecasConNotas);
setBibliotecaSeleccionada(bibliotecasConNotas[0] || null);
} catch (error) {
console.error('Error al cargar bibliotecas:', error);
}
};
const crearBiblioteca = async () => {
setLoadingNuevaBiblio(true); // 🔄 Activa el loader en el botón
try {
// Llamada a backend
await axios.post('/api/v1/text_manager/biblioteca', {
nombre_biblioteca: nombreBiblio,
descripcion: descripcionBiblio,
});
// 🧼 Limpia formularios
setNombreBiblio('');
setDescripcionBiblio('');
// 🔒 Cierra el modal
setModalNuevaBiblio(false);
// 🔄 Refresca la lista de bibliotecas
await fetchBibliotecas();
} catch (error) {
console.error('❌ Error al crear biblioteca:', error);
} finally {
setLoadingNuevaBiblio(false); // ✅ Apaga el loader
}
};
const agregarNota = async () => {
if (!bibliotecaSeleccionada) return;
try {
await axios.post(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}`, {
titulo: tituloNota,
texto: contenidoNota,
tags: [],
conexiones: [],
resumen: '',
});
setLoadingNotas(true);
const nuevasNotas = await axios.get(`/api/v1/text_manager/nota/list/${bibliotecaSeleccionada.id}`);
const nuevasBibliotecas = bibliotecas.map((b) =>
b.id === bibliotecaSeleccionada.id ? { ...b, notas: nuevasNotas.data as Nota[] } : b
);
setBibliotecas(nuevasBibliotecas);
setBibliotecaSeleccionada(nuevasBibliotecas.find((b) => b.id === bibliotecaSeleccionada.id) || null);
setTituloNota('');
setContenidoNota('');
setModalAbierto(false);
} catch (error) {
console.error('Error al agregar nota:', error);
} finally {
setLoadingNotas(false);
}
};
useEffect(() => {
fetchBibliotecas();
}, []);
// Eliminar nota
const eliminarNota = async (notaId: string) => {
if (!bibliotecaSeleccionada) return;
try {
await axios.delete(`/api/v1/text_manager/nota/${bibliotecaSeleccionada.id}/${notaId}`);
// Solo actualiza la biblioteca actual
const nuevasNotas = await axios.get(`/api/v1/text_manager/nota/list/${bibliotecaSeleccionada.id}`);
const nuevasBibliotecas = bibliotecas.map((b) =>
b.id === bibliotecaSeleccionada.id ? { ...b, notas: nuevasNotas.data as Nota[] } : b
);
setBibliotecas(nuevasBibliotecas);
setBibliotecaSeleccionada(nuevasBibliotecas.find(b => b.id === bibliotecaSeleccionada.id) || null);
} catch (error) {
console.error("Error al eliminar nota:", error);
}
};
// 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 (
<AppShellWithMenu>
<Box display="flex" h="100%">
<Box w={240} p="md">
<ScrollArea h="100%">
<Stack>
<Button color="teal" onClick={fetchBibliotecas}>🔄 Recuperar bibliotecas</Button>
<Button color="grape" variant="outline" onClick={() => setModalNuevaBiblio(true)}> Nueva biblioteca</Button>
{bibliotecas.map((biblio) => (
<Button
key={biblio.id}
size="xs"
fullWidth
variant={biblio.id === bibliotecaSeleccionada?.id ? 'filled' : 'light'}
color="blue"
onClick={() => setBibliotecaSeleccionada(biblio)}
>
{biblio.nombre}
</Button>
))}
</Stack>
</ScrollArea>
</Box>
<Box p="md" style={{ flex: 1 }}>
{bibliotecaSeleccionada ? (
<Stack>
<Title order={2}>{bibliotecaSeleccionada.nombre}</Title>
<Group>
<Button onClick={() => setModalAbierto(true)}>Agregar nota</Button>
</Group>
<Group>
{loadingNotas ? (
<Loader />
) : (
bibliotecaSeleccionada.notas.map((nota) => (
// Cards de notas
<Card
key={nota.id}
shadow="sm"
padding="lg"
radius="md"
withBorder
style={{
width: 300,
height: 250, // Altura fija para asegurar la separación
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}
>
<div>
<Title order={4} style={{ marginBottom: 10 }}>{nota.titulo}</Title>
<Text>{nota.texto}</Text>
</div>
<Box mt="md" style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
size="xs"
variant="light"
color="blue"
onClick={() => abrirModalEditar(nota)}
>
Editar
</Button>
</Box>
</Card>
// Fin de notas en cards
))
)}
</Group>
</Stack>
) : (
<Stack>
<Text>Selecciona una biblioteca</Text>
</Stack>
)}
</Box>
</Box>
{/* Modal para agregar */}
<Modal opened={modalAbierto} onClose={() => setModalAbierto(false)} title="Agregar nueva nota">
<Stack>
<TextInput
label="Título"
value={tituloNota}
onChange={(event) => setTituloNota(event.currentTarget.value)}
/>
<Textarea
label="Contenido"
minRows={6}
autosize
value={contenidoNota}
onChange={(event) => setContenidoNota(event.currentTarget.value)}
/>
<Button onClick={agregarNota}>Guardar</Button>
</Stack>
</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))
}
/>
<Textarea
label="Contenido"
minRows={6}
autosize
value={notaEnEdicion?.texto || ""}
onChange={(e) =>
setNotaEnEdicion((prev) => (prev ? { ...prev, texto: e.currentTarget.value } : null))
}
/>
<Group grow>
<Button color="blue" onClick={guardarEdicionNota}>
💾 Guardar cambios
</Button>
<Button
color="red"
onClick={async () => {
if (!notaEnEdicion || !bibliotecaSeleccionada) return;
await eliminarNota(notaEnEdicion.id);
setModalEditarAbierto(false);
setNotaEnEdicion(null);
}}
>
🗑 Eliminar nota
</Button>
</Group>
</Stack>
</Modal>
{/* Modal para crear una biblioteca */}
<Modal
opened={modalNuevaBiblio}
onClose={() => setModalNuevaBiblio(false)}
title="Crear nueva biblioteca"
>
<Stack>
<TextInput
label="Nombre"
value={nombreBiblio}
onChange={(e) => setNombreBiblio(e.currentTarget.value)}
disabled={loadingNuevaBiblio}
/>
<TextInput
label="Descripción"
value={descripcionBiblio}
onChange={(e) => setDescripcionBiblio(e.currentTarget.value)}
disabled={loadingNuevaBiblio}
/>
<Button onClick={crearBiblioteca} loading={loadingNuevaBiblio}>
Crear
</Button>
</Stack>
</Modal>
</AppShellWithMenu>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { Grid } from '@mantine/core';
import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { GridDashboard } from '../components/Grid_dashboard';
export function Grid_Dashboard() {
return (
<AppShellWithMenu>
<GridDashboard></GridDashboard>
</AppShellWithMenu>
);
}
+4
View File
@@ -1,5 +1,7 @@
import { AppShellWithMenu } from '../components/Appshell/Appshell'; import { AppShellWithMenu } from '../components/Appshell/Appshell';
import { Welcome } from '@/components/Welcome/Welcome'; import { Welcome } from '@/components/Welcome/Welcome';
import { ColorSchemeToggle } from '@/components/ColorSchemeToggle/ColorSchemeToggle';
export function HomePage() { export function HomePage() {
return ( return (
@@ -12,6 +14,8 @@ export function HomePage() {
<p>This is the home page content.</p> <p>This is the home page content.</p>
</div> </div>
<ColorSchemeToggle></ColorSchemeToggle>
</AppShellWithMenu> </AppShellWithMenu>
); );
-12
View File
@@ -1,12 +0,0 @@
import { AppShellWithMenu } from '../components/Appshell/Appshell';
export function Prueba_appshell() {
return (
<AppShellWithMenu>
</AppShellWithMenu>
);
}
+9
View File
@@ -5,6 +5,15 @@ import svgr from 'vite-plugin-svgr';
export default defineConfig({ export default defineConfig({
plugins: [react(), tsconfigPaths(), svgr()], plugins: [react(), tsconfigPaths(), svgr()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false,
},
},
},
test: { test: {
globals: true, globals: true,
environment: 'jsdom', environment: 'jsdom',
+92 -2
View File
@@ -1224,6 +1224,11 @@ async-function@^1.0.0:
resolved "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz" resolved "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz"
integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
available-typed-arrays@^1.0.7: available-typed-arrays@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz" resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz"
@@ -1236,6 +1241,15 @@ axe-core@^4.10.0:
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz"
integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==
axios@^1.9.0:
version "1.9.0"
resolved "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz"
integrity sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^4.1.0: axobject-query@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz"
@@ -1419,6 +1433,11 @@ check-error@^2.1.1:
resolved "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz" resolved "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz"
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
clsx@^1.1.1:
version "1.2.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
clsx@^2.1.1: clsx@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
@@ -1441,6 +1460,13 @@ colord@^2.9.3:
resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz"
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
@@ -1636,6 +1662,11 @@ define-properties@^1.1.3, define-properties@^1.2.1:
has-property-descriptors "^1.0.0" has-property-descriptors "^1.0.0"
object-keys "^1.1.1" object-keys "^1.1.1"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
depd@^2.0.0, depd@2.0.0: depd@^2.0.0, depd@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
@@ -2268,6 +2299,11 @@ flatted@^3.2.9, flatted@^3.3.3:
resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
for-each@^0.3.3, for-each@^0.3.5: for-each@^0.3.3, for-each@^0.3.5:
version "0.3.5" version "0.3.5"
resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz" resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz"
@@ -2283,6 +2319,16 @@ foreground-child@^3.1.0:
cross-spawn "^7.0.6" cross-spawn "^7.0.6"
signal-exit "^4.0.1" signal-exit "^4.0.1"
form-data@^4.0.0:
version "4.0.2"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz"
integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
mime-types "^2.1.12"
forwarded@0.2.0: forwarded@0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz"
@@ -3157,6 +3203,18 @@ mime-db@^1.54.0:
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz"
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime-types@^3.0.0, mime-types@^3.0.1: mime-types@^3.0.0, mime-types@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz" resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz"
@@ -3581,6 +3639,11 @@ proxy-addr@^2.0.7:
forwarded "0.2.0" forwarded "0.2.0"
ipaddr.js "1.9.1" ipaddr.js "1.9.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
punycode@^2.1.0, punycode@^2.3.1: punycode@^2.1.0, punycode@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
@@ -3613,6 +3676,11 @@ raw-body@^3.0.0:
iconv-lite "0.6.3" iconv-lite "0.6.3"
unpipe "1.0.0" unpipe "1.0.0"
re-resizable@6.11.2:
version "6.11.2"
resolved "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz"
integrity sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==
react-docgen-typescript@^2.2.2: react-docgen-typescript@^2.2.2:
version "2.2.2" version "2.2.2"
resolved "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz" resolved "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz"
@@ -3634,13 +3702,21 @@ react-docgen@^7.0.0:
resolve "^1.22.1" resolve "^1.22.1"
strip-indent "^4.0.0" strip-indent "^4.0.0"
"react-dom@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom@^18.0.0 || ^19.0.0", "react-dom@^18.x || ^19.x", react-dom@^19.0.0, react-dom@^19.1.0, react-dom@>=16.13, react-dom@>=16.8.0, react-dom@>=18: "react-dom@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom@^18.0.0 || ^19.0.0", "react-dom@^18.x || ^19.x", react-dom@^19.0.0, react-dom@^19.1.0, "react-dom@>= 16.3.0", react-dom@>=16.13, react-dom@>=16.3.0, react-dom@>=16.8.0, react-dom@>=18:
version "19.1.0" version "19.1.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz" resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz"
integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g== integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==
dependencies: dependencies:
scheduler "^0.26.0" scheduler "^0.26.0"
react-draggable@4.4.6:
version "4.4.6"
resolved "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz"
integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==
dependencies:
clsx "^1.1.1"
prop-types "^15.8.1"
react-is@^16.13.1: react-is@^16.13.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
@@ -3687,6 +3763,15 @@ react-remove-scroll@^2.6.2:
use-callback-ref "^1.3.3" use-callback-ref "^1.3.3"
use-sidecar "^1.1.3" use-sidecar "^1.1.3"
react-rnd@^10.5.2:
version "10.5.2"
resolved "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz"
integrity sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==
dependencies:
re-resizable "6.11.2"
react-draggable "4.4.6"
tslib "2.6.2"
react-router-dom@^7.4.0: react-router-dom@^7.4.0:
version "7.5.3" version "7.5.3"
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.3.tgz" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.3.tgz"
@@ -3725,7 +3810,7 @@ react-use-measure@^2.1.7:
resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz" resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz"
integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg== integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==
"react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react@^18.0.0 || ^19.0.0", "react@^18.x || ^19.x", react@^19.0.0, react@^19.1.0, "react@>= 16", react@>=16.13, react@>=16.8.0, react@>=17.0, react@>=18, react@>=18.0.0: "react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react@^18.0.0 || ^19.0.0", "react@^18.x || ^19.x", react@^19.0.0, react@^19.1.0, "react@>= 16", "react@>= 16.3.0", react@>=16.13, react@>=16.3.0, react@>=16.8.0, react@>=17.0, react@>=18, react@>=18.0.0:
version "19.1.0" version "19.1.0"
resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz" resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz"
integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
@@ -4535,6 +4620,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0:
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
tslib@2.6.2:
version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
turbo-stream@2.4.0: turbo-stream@2.4.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz" resolved "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz"
+54 -12
View File
@@ -5,15 +5,7 @@
"execution_count": 1, "execution_count": 1,
"id": "26aa8e2b", "id": "26aa8e2b",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [],
{
"name": "stdout",
"output_type": "stream",
"text": [
"✅ Credencial: Credencial_enmanuel\n"
]
}
],
"source": [ "source": [
"from src.ApiKeys.openai_apikey import OpenAICredencial\n", "from src.ApiKeys.openai_apikey import OpenAICredencial\n",
"from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo # Ajusta si está en otro módulo\n", "from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo # Ajusta si está en otro módulo\n",
@@ -24,14 +16,64 @@
"conexion_admin = PostgresConexion(db_credencial)\n", "conexion_admin = PostgresConexion(db_credencial)\n",
"\n", "\n",
"# 3. Guardar la credencial en la base de datos\n", "# 3. Guardar la credencial en la base de datos\n",
"repo = OpenAICredencialRepo(conexion_admin)\n", "repo = OpenAICredencialRepo(conexion_admin)\n"
"credencial_openai = repo.get_by_id(1)\n", ]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4c232ecd",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'OPAK20250513-61b29978b7604031014'"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"apikey_gpt = OpenAICredencial(titulo=\"Credencial_enmanuel_gpt\",\n",
" api_key=\"\")\n",
"\n",
"\n",
"repo.add(apikey_gpt)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "32552452",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✅ Credencial: Credencial_enmanuel_gpt\n"
]
}
],
"source": [
"credencial_openai = repo.get_by_id('OPAK20250513-61b29978b7604031014')\n",
"print(f\"✅ Credencial: {credencial_openai.titulo}\")" "print(f\"✅ Credencial: {credencial_openai.titulo}\")"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 2, "execution_count": null,
"id": "7464fa65",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": 4,
"id": "e5b665a6", "id": "e5b665a6",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
"cells": [ "cells": [
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 1,
"id": "5206b9c6", "id": "5206b9c6",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -14,7 +14,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 3, "execution_count": 2,
"id": "63a0b954", "id": "63a0b954",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -25,7 +25,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 3,
"id": "0575f424", "id": "0575f424",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -38,17 +38,17 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 3, "execution_count": 4,
"id": "a5266309", "id": "a5266309",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"1" "'PGCR20250510-02f3cf9610127084237'"
] ]
}, },
"execution_count": 3, "execution_count": 4,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@@ -72,7 +72,7 @@
} }
], ],
"source": [ "source": [
"modelo_cred = repo_cred.get_by_id(1)\n", "modelo_cred = repo_cred.get_by_id(\"PGCR20250510-02f3cf9610127084237\")\n",
"\n", "\n",
"print(modelo_cred.titulo)" "print(modelo_cred.titulo)"
] ]
+277
View File
@@ -0,0 +1,277 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "255345d5",
"metadata": {},
"outputs": [],
"source": [
"from src.TextManager.biblioteca import Biblioteca\n",
"from src.TextManager.biblioteca_mmr import BibliotecaRepo\n",
"from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder\n",
"from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo\n",
"from src.ConexionSql.Postgres_conexion import PostgresConexion\n",
"from src.TextManager.nota import Nota\n",
"from src.TextManager.notas_biblioteca_mmr import generar_tabla_nota_para_biblioteca, NotaRepo\n",
"from sqlalchemy import MetaData\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "b414a66c",
"metadata": {},
"outputs": [],
"source": [
"def agregar_nota_a_biblioteca(\n",
" conexion: PostgresConexion,\n",
" biblioteca_id: str,\n",
" titulo: str,\n",
" texto: str = \"\",\n",
" tags: list[str] = None,\n",
" conexiones: list[str] = None,\n",
" resumen: str = \"\"\n",
"):\n",
" print(\"[INFO] Iniciando el proceso de agregar nota a la biblioteca...\")\n",
"\n",
" # Obtener la biblioteca\n",
" print(f\"[INFO] Buscando biblioteca con ID: {biblioteca_id}\")\n",
" repo_biblioteca = BibliotecaRepo(conexion)\n",
" biblioteca = repo_biblioteca.get_by_id(biblioteca_id)\n",
" if biblioteca is None:\n",
" print(f\"[ERROR] No se encontró la biblioteca con ID {biblioteca_id}\")\n",
" raise ValueError(f\"No se encontró la biblioteca con ID {biblioteca_id}\")\n",
" print(f\"[INFO] Biblioteca encontrada: {biblioteca.nombre} (vector_dim={biblioteca.vector_dim})\")\n",
"\n",
" # Crear objeto Nota\n",
" print(f\"[INFO] Creando objeto Nota con título: {titulo}\")\n",
" nota = Nota(\n",
" titulo=titulo,\n",
" texto=texto,\n",
" tags=tags or [],\n",
" conexiones=conexiones or [],\n",
" resumen=resumen or \"\",\n",
" # vector=biblioteca.embedder.embed_text(texto),\n",
" # vector_resumen=biblioteca.embedder.embed_text(resumen) if resumen else None\n",
" )\n",
" # Mostrar atributos seguros\n",
" print(\n",
" f\"[DEBUG] Nota creada: titulo='{nota.titulo}', \"\n",
" f\"texto_len={len(nota.texto)}, \"\n",
" f\"tags={len(nota.tags)}, \"\n",
" f\"conexiones={len(nota.conexiones)}, \"\n",
" f\"resumen_len={len(nota.resumen)}\"\n",
" )\n",
"\n",
" # Preparar tabla y modelo de nota\n",
" print(f\"[INFO] Generando tabla y modelo de Nota para la biblioteca: {biblioteca.nombre}\")\n",
" metadata = MetaData()\n",
" tabla, ModeloNota = generar_tabla_nota_para_biblioteca(\n",
" biblioteca.nombre,\n",
" biblioteca.vector_dim,\n",
" metadata\n",
" )\n",
" print(f\"[INFO] Creando tabla en la base de datos si no existe...\")\n",
" metadata.create_all(conexion.get_engine())\n",
" print(f\"[INFO] Tabla '{tabla.name}' verificada/creada.\")\n",
"\n",
" # Guardar la nota\n",
" print(f\"[INFO] Guardando nota en la base de datos...\")\n",
" repo_nota = NotaRepo(conexion.get_session(), ModeloNota)\n",
" nota_id = repo_nota.add(nota)\n",
" print(f\"[INFO] Nota guardada con ID: {nota_id}\")\n",
"\n",
" resultado = {\n",
" \"mensaje\": f\"Nota '{titulo}' agregada con éxito a la biblioteca '{biblioteca.nombre}'.\",\n",
" \"nota_id\": nota_id\n",
" }\n",
"\n",
" print(f\"[SUCCESS] {resultado['mensaje']}\")\n",
" return resultado\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "8e57e511",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[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: 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-0cf0187e58905045667\n",
"[SUCCESS] Nota 'fdsfdsfsdfewww' agregada con éxito a la biblioteca 'biblio_Pruebas_1'.\n"
]
},
{
"data": {
"text/plain": [
"{'mensaje': \"Nota 'fdsfdsfsdfewww' agregada con éxito a la biblioteca 'biblio_Pruebas_1'.\",\n",
" 'nota_id': 'NOTA20250511-0cf0187e58905045667'}"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from entrypoint.init_db import db_credencial\n",
"conexion_admin = PostgresConexion(db_credencial)\n",
"\n",
"agregar_nota_a_biblioteca(\n",
" conexion=conexion_admin,\n",
" biblioteca_id=\"BBLI20250511-a91dbb2168172979414\",\n",
" titulo=\"fdsfdsfsdfewww\"\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "431f24f1",
"metadata": {},
"outputs": [],
"source": [
"def listar_notas_de_biblioteca(conexion: PostgresConexion, biblioteca_id: str) -> list[dict]:\n",
" repo_biblioteca = BibliotecaRepo(conexion)\n",
" biblioteca = repo_biblioteca.get_by_id(biblioteca_id)\n",
" if not biblioteca:\n",
" raise ValueError(f\"No se encontró la biblioteca con ID: {biblioteca_id}\")\n",
"\n",
" metadata = MetaData()\n",
" tabla, ModeloNota = generar_tabla_nota_para_biblioteca(biblioteca.nombre, biblioteca.vector_dim, metadata)\n",
" metadata.create_all(conexion.get_engine())\n",
"\n",
" repo_nota = NotaRepo(conexion.get_session(), ModeloNota)\n",
" notas = repo_nota.get_all()\n",
" return [\n",
" {\n",
" \"id\": n.id,\n",
" \"titulo\": n.titulo,\n",
" \"tags\": n.tags,\n",
" \"texto\": n.texto,\n",
" \"resumen\": n.resumen,\n",
" \"conexiones\": n.conexiones\n",
" }\n",
" for n in notas\n",
" ]"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "ae4f2994",
"metadata": {},
"outputs": [
{
"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": [
"listar_notas_de_biblioteca(\n",
" conexion=conexion_admin,\n",
" biblioteca_id=\"BBLI20250511-a91dbb2168172979414\"\n",
")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+187
View File
@@ -0,0 +1,187 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "b02bfb00",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Your branch is up to date with 'origin/main'.\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"Switched to branch 'main'\n"
]
}
],
"source": [
"!git checkout main"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "92d482e6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Updating 234639a..20173e9\n",
"Fast-forward\n",
" .gitignore | 11 +\n",
" Apikeys.ipynb | 128 +--\n",
" {llms/MCPs => backend}/__init__.py | 0\n",
" {llms/Modelos => backend/api}/__init__.py | 0\n",
" {llms => backend/api/v1}/__init__.py | 0\n",
" backend/api/v1/endpoints/ping.py | 9 +\n",
" backend/api/v1/router.py | 7 +\n",
" {security => backend/deps}/__init__.py | 0\n",
" backend/deps/auth.py | 0\n",
" backend/main.py | 24 +\n",
" data/files/txt/tree.txt | 980 +++++++++++++++++++++\n",
" entrypoint/init_db.py | 2 +-\n",
" frontend/.gitignore | 3 +\n",
" frontend/index.html | 4 +-\n",
" frontend/package-lock.json | 345 ++++++++\n",
" frontend/package.json | 2 +\n",
" frontend/src/assets/icons/favicon.svg | 55 ++\n",
" frontend/src/assets/icons/favicon.tsx | 37 +\n",
" frontend/src/assets/icons/index.ts | 18 +\n",
" .../components/{ => Appshell}/Appshell.module.css | 25 +-\n",
" .../src/components/{ => Appshell}/Appshell.tsx | 97 +-\n",
" frontend/src/components/HoloShader.tsx | 30 +-\n",
" frontend/src/components/Links/MainlLinks.tsx | 0\n",
" frontend/src/components/LlamadorAPI.tsx | 11 +-\n",
" frontend/src/components/MetodoSelect.tsx | 4 +-\n",
" frontend/src/components/Welcome/Welcome.tsx | 6 +-\n",
" frontend/src/components/botoncito.tsx | 16 -\n",
" frontend/src/data/navigationsLinks_1.ts | 20 +\n",
" frontend/src/data/submenuLinks_1.ts | 35 +\n",
" frontend/src/favicon.svg | 1 -\n",
" frontend/src/pages/404.tsx | 55 +-\n",
" frontend/src/pages/Analitica.tsx | 6 +-\n",
" frontend/src/pages/Consulta_api.tsx | 4 +-\n",
" frontend/src/pages/Home.page.tsx | 12 +-\n",
" frontend/src/pages/Plantilla.tsx | 2 +-\n",
" frontend/src/pages/Prueba_appshell.tsx | 7 +-\n",
" frontend/src/theme.ts | 57 +-\n",
" frontend/src/types/svg.d.ts | 7 +\n",
" frontend/src/types/vite-env.d.ts | 1 +\n",
" frontend/tsconfig.json | 2 +-\n",
" frontend/{vite.config.mjs => vite.config.js} | 5 +-\n",
" frontend/yarn.lock | 160 +++-\n",
" llms/Agente.py | 122 ---\n",
" llms/Modelos/Openai_model.py | 64 --\n",
" main.py | 25 +-\n",
" prueba_loop_agente.py | 94 +-\n",
" src/ApiKeys/openai_apikey_mmr.py | 2 +-\n",
" src/ConexionApis/OpenAi_conexion.py | 15 +-\n",
" src/Credenciales/postgres_credencial_mmr.py | 2 +-\n",
" src/Llms/Agente.py | 196 +++++\n",
" {llms => src/Llms}/MCPs/MCPStdioServer.py | 0\n",
" src/Llms/MCPs/__init__.py | 0\n",
" {llms => src/Llms}/Memory/Base_MemoryConv.py | 0\n",
" {llms => src/Llms}/Memory/postgres_MemoryConv.py | 2 +-\n",
" {llms => src/Llms}/Modelos/Base_model.py | 2 +-\n",
" src/Llms/Modelos/Openai_model.py | 82 ++\n",
" {llms => src/Llms}/Modelos/Openai_model_mmr.py | 2 +-\n",
" src/Llms/Modelos/__init__.py | 0\n",
" src/Llms/__init__.py | 0\n",
" {security => src/Security}/Encriptar.py | 0\n",
" src/Security/__init__.py | 0\n",
" 61 files changed, 2343 insertions(+), 453 deletions(-)\n",
" rename {llms/MCPs => backend}/__init__.py (100%)\n",
" rename {llms/Modelos => backend/api}/__init__.py (100%)\n",
" rename {llms => backend/api/v1}/__init__.py (100%)\n",
" create mode 100644 backend/api/v1/endpoints/ping.py\n",
" create mode 100644 backend/api/v1/router.py\n",
" rename {security => backend/deps}/__init__.py (100%)\n",
" create mode 100644 backend/deps/auth.py\n",
" create mode 100644 backend/main.py\n",
" create mode 100644 frontend/src/assets/icons/favicon.svg\n",
" create mode 100644 frontend/src/assets/icons/favicon.tsx\n",
" create mode 100644 frontend/src/assets/icons/index.ts\n",
" rename frontend/src/components/{ => Appshell}/Appshell.module.css (82%)\n",
" rename frontend/src/components/{ => Appshell}/Appshell.tsx (62%)\n",
" create mode 100644 frontend/src/components/Links/MainlLinks.tsx\n",
" delete mode 100644 frontend/src/components/botoncito.tsx\n",
" create mode 100644 frontend/src/data/navigationsLinks_1.ts\n",
" create mode 100644 frontend/src/data/submenuLinks_1.ts\n",
" delete mode 100644 frontend/src/favicon.svg\n",
" create mode 100644 frontend/src/types/svg.d.ts\n",
" create mode 100644 frontend/src/types/vite-env.d.ts\n",
" rename frontend/{vite.config.mjs => vite.config.js} (74%)\n",
" delete mode 100644 llms/Agente.py\n",
" delete mode 100644 llms/Modelos/Openai_model.py\n",
" create mode 100644 src/Llms/Agente.py\n",
" rename {llms => src/Llms}/MCPs/MCPStdioServer.py (100%)\n",
" create mode 100644 src/Llms/MCPs/__init__.py\n",
" rename {llms => src/Llms}/Memory/Base_MemoryConv.py (100%)\n",
" rename {llms => src/Llms}/Memory/postgres_MemoryConv.py (97%)\n",
" rename {llms => src/Llms}/Modelos/Base_model.py (95%)\n",
" create mode 100644 src/Llms/Modelos/Openai_model.py\n",
" rename {llms => src/Llms}/Modelos/Openai_model_mmr.py (97%)\n",
" create mode 100644 src/Llms/Modelos/__init__.py\n",
" create mode 100644 src/Llms/__init__.py\n",
" rename {security => src/Security}/Encriptar.py (100%)\n",
" create mode 100644 src/Security/__init__.py\n"
]
}
],
"source": [
"!git merge cambios_frontend\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "91a704a5",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"remote: . Processing 1 references \n",
"remote: Processed 1 references in total \n",
"To http://10.8.0.6:3123/egutierrez/Fitz_Studio.git\n",
" 234639a..20173e9 main -> main\n"
]
}
],
"source": [
"!git push origin main"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+29
View File
@@ -0,0 +1,29 @@
# client.py
import asyncio
from src.Llms.MCPs.Mcp_client import MCPClient
from src.Llms.MCPs.Http_mcp_server import HttpMCPServer
async def main():
client = MCPClient()
client.register_server(HttpMCPServer(
name="tools",
path="IGNORED_IN_CLIENT", # no importa aquí
host="127.0.0.1",
port=4300,
path_http="/tools"
))
await client.connect_all()
result = await client.call_tool({
"server": "tools",
"tool": "get_hostname",
"input": {}
})
print("RESULT:", result)
await client.disconnect_all()
if __name__ == "__main__":
asyncio.run(main())
+100 -42
View File
@@ -7,53 +7,111 @@ from src.ConexionApis.OpenAi_conexion import OpenAICliente
from src.Llms.Modelos.Openai_model import ModeloOpenAI from src.Llms.Modelos.Openai_model import ModeloOpenAI
from src.Llms.Agente import AgenteAI from src.Llms.Agente import AgenteAI
from src.Llms.Memory.postgres_MemoryConv import MemoryConvPostgres from src.Llms.Memory.postgres_MemoryConv import MemoryConvPostgres
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.client import Client
from src.Llms.MCPs.McpClient import MCPClient # ya tienes esta clase
from src.Llms.MCPs.McpClient_Registry import ClientRegistry # o ajusta según tu estructura
import asyncio
async def main():
# Usar Credencial openai
conexion_admin = PostgresConexion(db_credencial)
repo = OpenAICredencialRepo(conexion_admin)
credencial_openai = repo.get_by_id("OPAK20250513-61b29978b7604031014")
cliente = OpenAICliente(credencial_openai)
conexion_admin = PostgresConexion(db_credencial) # crea el modelo (openai)
repo = OpenAICredencialRepo(conexion_admin) modelo = ModeloOpenAI(
credencial_openai = repo.get_by_id(1) cliente=cliente,
model="gpt-4o",
cliente = OpenAICliente(credencial_openai) temperature=1,
top_p=1.0
modelo = ModeloOpenAI(
cliente=cliente,
model="gpt-4o",
temperature=1,
top_p=1.0
)
memoria = MemoryConvPostgres(
credencial=db_credencial,
nombre_tabla="memoria_conversacion_pruebas",
k=10
)
agente2 = AgenteAI(
modelo=modelo,
nombre="Experto en Astronomía",
descripcion="Un experto en astronomía que responde preguntas sobre el universo.",
system_prompt="Actúa como un experto en astronomía y astrofísica con experiencia académica y práctica en observación astronómica, física estelar, cosmología, mecánica orbital y análisis de datos astronómicos. Cuando respondas, utiliza lenguaje técnico pero accesible para alguien con conocimientos intermedios en física y matemáticas. Siempre que sea posible, incluye explicaciones detalladas, ejemplos numéricos y referencias a teorías o descubrimientos relevantes (por ejemplo, relatividad general, evolución estelar, espectroscopía, etc.). No simplifiques en exceso. Si la pregunta tiene múltiples dimensiones (como observacional y teórica), aborda todas. ¿Estás listo para empezar?",
rol="astronomo",
max_iterations=5,
memoria=memoria,
objetivos=["Responder preguntas sobre astronomía y astrofísica", "Proporcionar explicaciones detalladas y ejemplos numéricos"],
)
async def probar_interaccion_stream():
print("Respuesta en streaming:\n")
# Paso 1: espera la corutina para obtener el generador
respuesta_gen = await agente2.interactuar_en_bucle(
"¿Hacia qué va orbitando cada astro del espacio? responde jerárquicamente",
stream=True
) )
# Paso 2: itera sobre el generador # Le otorga memoria
async for token in respuesta_gen:
print(token, end="", flush=True) memoria = MemoryConvPostgres(
credencial=db_credencial,
nombre_tabla="memoria_conversacion_pruebas",
k=10
)
# Cargamos las herramientas
herramientas = MCPClient.from_http(
name="tools",
url="http://127.0.0.1:4300/tools/"
)
math = MCPClient.from_http(
name="math",
url="http://127.0.0.1:4200/math/"
)
# Las añadimos al registro de herramientas
registry = ClientRegistry()
registry.add("tools", herramientas)
registry.add("math", math)
# --- INICIALIZACIÓN DEL AGENTE ---
agente2 = AgenteAI(
modelo=modelo,
nombre="Asistente Inteligente",
descripcion="Un asistente conversacional versátil, capaz de resolver problemas, acceder a herramientas y proporcionar respuestas útiles.",
system_prompt=(
"Eres un asistente inteligente que ayuda al usuario a resolver tareas, responder preguntas y usar herramientas disponibles si es necesario. "
"Debes razonar paso a paso, y si se detecta que una herramienta MCP es útil, actúa generando el bloque MCP apropiado sin dar más explicaciones. "
"Siempre estructura tus respuestas con claridad, y termina con <END> cuando creas haber completado la tarea."
),
rol="asistente",
objetivos=[
"Resolver tareas del usuario",
"Usar herramientas MCP si es útil",
"Responder de forma clara y útil"
],
# max_iterations=3,
# memoria=memoria,
mcp=registry # ← ✅ Integración del cliente MCP
)
# --- FUNCIÓN DE EJECUCIÓN ---
async def probar_interaccion_stream():
# # 🔌 Conectar a los servidores MCP registrados
# await mcp_client.connect_all()
print("Respuesta en streaming:\n")
respuesta_gen = await agente2.interactuar_en_bucle(
"¿Cuál es mi nombre de usuario en este sistema?",
stream=True
)
async for token in respuesta_gen:
print(token, end="", flush=True)
await probar_interaccion_stream()
# Ejecutar
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(probar_interaccion_stream())
+19 -64
View File
@@ -1,74 +1,29 @@
import asyncio
from llms.MCPs.MCPStdioServer import MCPStdioServer
import os
async def main():
prueba = MCPStdioServer(
name="prueba_server_mcp",
command="C:/Users/lucas/Desktop/mcps/.venv/Scripts/python.exe",
args=["C:/Users/lucas/Desktop/mcps/server_mcp_python/server_mcp.py"],
)
await prueba.start()
print("Herramientas:", prueba.get_tool_names())
await prueba.stop() # <- esto previene el error
if __name__ == "__main__":
# Asegura compatibilidad para subprocess en Windows
if os.name == "nt":
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
asyncio.run(main()) async def test_registry(registry: ClientRegistry):
tools = await registry.listar_tools_por_cliente()
prompts = await registry.listar_prompts_por_cliente()
resources = await registry.listar_resources_por_cliente()
print("\n🔧 Herramientas:", tools)
print("\n📋 Prompts:", prompts)
print("\n📂 Resources:", resources)
asyncio.run(test_registry(registry))
async def test_wrapper():
from src.ApiKeys.openai_apikey import OpenAICredencial # 2. Llamar a una herramienta de prueba
from src.ApiKeys.openai_apikey_mmr import OpenAICredencialRepo # Ajusta si está en otro módulo result = await herramientas.call_tool("generate_uuid")
from src.ConexionSql.Postgres_conexion import PostgresConexion print("\n🆔 UUID generado:", result[0].text) # Accedemos al contenido directamente
# 1. Crear instancia de conexión (asegúrate de configurar bien tu conexión en Base_conexion)
from entrypoint.init_db import db_credencial
conexion_admin = PostgresConexion(db_credencial)
# 3. Guardar la credencial en la base de datos
repo = OpenAICredencialRepo(conexion_admin)
credencial_openai = repo.get_by_id(1)
print(f"✅ Credencial: {credencial_openai.titulo}")
from src.ConexionApis.OpenAi_conexion import OpenAICliente
cliente = OpenAICliente(credencial_openai)
from llms.Modelos.Openai_model import ModeloOpenAI
modelo = ModeloOpenAI(
cliente=cliente,
model="gpt-4o",
temperature=1,
top_p=1.0
)
from llms.Agente import AgenteAI
agente_con_herramientas = AgenteAI( # asyncio.run(test_wrapper())
modelo=modelo,
nombre="Agente con herramientas",
descripcion="Un agente que puede usar herramientas",
system_prompt="Eres un asistente que puede usar herramientas para responder preguntas.",
rol="asistente",
objetivos=["Asistir al usuario en tareas complejas", "usar herramientas para obtener información adicional"]
# tools=
)
respuesta = agente_con_herramientas.interactuar(
prompt="Hola como estas?",
)
print(respuesta)
+1
View File
@@ -28,3 +28,4 @@ def save_tree_to_file(start_path='.', max_depth=2, output_file='tree.txt'):
# Ejemplo de uso: # Ejemplo de uso:
# Puedes cambiar estos valores según lo necesites # Puedes cambiar estos valores según lo necesites
save_tree_to_file(start_path=r'E:\Fitz_Studio', max_depth=3, output_file=r'E:\Fitz_Studio\data\files\txt\tree.txt') save_tree_to_file(start_path=r'E:\Fitz_Studio', max_depth=3, output_file=r'E:\Fitz_Studio\data\files\txt\tree.txt')
+5 -2
View File
@@ -1,11 +1,14 @@
from src.Security.GenerarIDs import GeneradorIDUnico
class OpenAICredencial: class OpenAICredencial:
def __init__(self, titulo: str, api_key: str, organizacion: str = None): def __init__(self, titulo: str, api_key: str, organizacion: str = None, id: str = None):
""" """
:param titulo: Nombre descriptivo para esta credencial. :param titulo: Nombre descriptivo para esta credencial.
:param api_key: Clave secreta de la API de OpenAI. :param api_key: Clave secreta de la API de OpenAI.
:param organizacion: (Opcional) ID de la organización asociada a la cuenta de OpenAI. :param organizacion: (Opcional) ID de la organización asociada a la cuenta de OpenAI.
""" """
self.titulo = titulo self.id = id if id is not None else GeneradorIDUnico("OPAK").generar()
self.titulo = titulo
self.api_key = api_key self.api_key = api_key
self.organizacion = organizacion self.organizacion = organizacion
+53 -38
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,10 +28,10 @@ if pssword is None:
# MODELO (SQLAlchemy) # MODELO (SQLAlchemy)
# ---------------------- # ----------------------
class OpenAICredencialModel(Base): class OpenAICredencialModel(Base, Model_base):
__tablename__ = 'openai_credenciales' __tablename__ = 'openai_credenciales'
id = Column(Integer, primary_key=True) id = Column(String, primary_key=True)
titulo = Column(String, nullable=False) titulo = Column(String, nullable=False)
api_key = Column(String, nullable=False) # Encriptada como base64 string api_key = Column(String, nullable=False) # Encriptada como base64 string
organizacion = Column(String, nullable=True) organizacion = Column(String, nullable=True)
@@ -33,30 +40,23 @@ class OpenAICredencialModel(Base):
# MAPPER # MAPPER
# ---------------------- # ----------------------
class OpenAICredencialMapper: class OpenAICredencialMapper(Mapper_base[OpenAICredencial, OpenAICredencialModel]):
@staticmethod
def to_dict(obj: OpenAICredencial) -> dict:
return {
"titulo": obj.titulo,
"api_key": base64.b64encode(
Encriptar_fernet.encriptar(obj.api_key, pssword)
).decode('utf-8'),
"organizacion": obj.organizacion
}
@staticmethod @staticmethod
def from_dict(data: dict) -> OpenAICredencial: def to_model(obj: OpenAICredencial) -> OpenAICredencialModel:
return OpenAICredencial( return OpenAICredencialModel(
titulo=data["titulo"], id=obj.id,
api_key=Encriptar_fernet.desencriptar( titulo=obj.titulo,
base64.b64decode(data["api_key"]), pssword api_key=base64.b64encode(
), Encriptar_fernet.encriptar(obj.api_key, pssword)
organizacion=data.get("organizacion") ).decode("utf-8"),
organizacion=obj.organizacion
) )
@staticmethod @staticmethod
def from_model(model: OpenAICredencialModel) -> OpenAICredencial: def from_model(model: OpenAICredencialModel) -> OpenAICredencial:
return OpenAICredencial( return OpenAICredencial(
id=model.id,
titulo=model.titulo, titulo=model.titulo,
api_key=Encriptar_fernet.desencriptar( api_key=Encriptar_fernet.desencriptar(
base64.b64decode(model.api_key), pssword base64.b64decode(model.api_key), pssword
@@ -64,29 +64,44 @@ class OpenAICredencialMapper:
organizacion=model.organizacion organizacion=model.organizacion
) )
@staticmethod
def to_dict(obj: OpenAICredencial) -> dict:
return {
"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_dict(data: dict) -> OpenAICredencial:
return OpenAICredencial(
id=data["id"],
titulo=data["titulo"],
api_key=Encriptar_fernet.desencriptar(
base64.b64decode(data["api_key"]), pssword
),
organizacion=data.get("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) -> int: 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_: int) -> 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))
+63
View File
@@ -0,0 +1,63 @@
# 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 = {}
# Prevención de error: solo ejecuta si __table__ existe
if not hasattr(self, "__table__"):
return out
for attr in self.__table__.columns:
val = getattr(self, attr.name, None)
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()
+34 -2
View File
@@ -1,7 +1,39 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from sqlalchemy.orm import Session from datetime import datetime, timezone
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.engine import Engine
from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError
class ConexionBase(ABC): class ConexionBase(ABC):
def __init__(self, uri: str):
self.estado = "pendiente"
self.timestamp = datetime.now(timezone.utc)
self._engine: Engine = create_engine(uri)
self._Session = sessionmaker(bind=self._engine)
self._session_instance: Session | None = None
@abstractmethod @abstractmethod
def get_session(self) -> Session: def get_session(self) -> Session:
pass if self._session_instance is None:
self._session_instance = self._Session()
return self._session_instance
@abstractmethod
def get_engine(self) -> Engine:
return self._engine
def probar_conexion(self) -> bool:
try:
with self._engine.connect() as connection:
connection.execute(text("SELECT 1"))
self.estado = "exito"
return True
except SQLAlchemyError:
self.estado = "fallo"
return False
def close(self):
if self._session_instance is not None:
self._session_instance.close()
self._session_instance = None
+23 -10
View File
@@ -1,12 +1,12 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from sqlalchemy import create_engine, text from sqlalchemy import create_engine, text
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.engine import Engine
from src.ConexionSql.Base_conexion import ConexionBase from src.ConexionSql.Base_conexion import ConexionBase
from src.Credenciales.postgres_credencial import PostgresCredencial from src.Credenciales.postgres_credencial import PostgresCredencial
class PostgresConexion(ConexionBase): class PostgresConexion(ConexionBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.estado = "pendiente" self.estado = "pendiente"
@@ -18,28 +18,41 @@ class PostgresConexion(ConexionBase):
self.port = credencial.port self.port = credencial.port
self.dbname = credencial.dbname self.dbname = credencial.dbname
self.user = credencial.user self.user = credencial.user
self.password = credencial.password # se guarda la contraseña self.password = credencial.password
uri = credencial.get_uri() uri = credencial.get_uri()
else: else:
self.user = kwargs.get("user") self.user = kwargs.get("user")
self.password = kwargs.get("password") # se guarda la contraseña self.password = kwargs.get("password")
self.host = kwargs.get("host") self.host = kwargs.get("host")
self.port = kwargs.get("port", 5432) self.port = kwargs.get("port", 5432)
self.dbname = kwargs.get("db") or kwargs.get("dbname") self.dbname = kwargs.get("db") or kwargs.get("dbname")
uri = f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.dbname}" uri = f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.dbname}"
self.engine = create_engine(uri) self._engine: Engine = create_engine(uri)
self.SessionLocal = sessionmaker(bind=self.engine) self._Session = sessionmaker(bind=self._engine)
def get_session(self): # ✅ INICIALIZAR LA SESIÓN AQUÍ
return self.SessionLocal() self._session_instance: Session | None = None
def get_session(self) -> Session:
if self._session_instance is None:
self._session_instance = self._Session()
return self._session_instance
def get_engine(self) -> Engine:
return self._engine
def probar_conexion(self) -> bool: def probar_conexion(self) -> bool:
try: try:
with self.engine.connect() as connection: with self._engine.connect() as connection:
connection.execute(text("SELECT 1")) connection.execute(text("SELECT 1"))
self.estado = "exito" self.estado = "exito"
return True return True
except SQLAlchemyError: except SQLAlchemyError:
self.estado = "fallo" self.estado = "fallo"
return False return False
def close(self):
if self._session_instance is not None:
self._session_instance.close()
self._session_instance = None
+5 -1
View File
@@ -1,5 +1,8 @@
from src.Security.GenerarIDs import GeneradorIDUnico
class PostgresCredencial: class PostgresCredencial:
def __init__(self, titulo: str, host: str, port: int, dbname: str, user: str, password: str): def __init__(self, titulo: str, host: str, port: int, dbname: str, user: str, password: str, id: str = None):
self.id = id if id is not None else GeneradorIDUnico("PGCR").generar()
self.titulo = titulo self.titulo = titulo
self.host = host self.host = host
self.port = port self.port = port
@@ -7,6 +10,7 @@ class PostgresCredencial:
self.user = user self.user = user
self.password = password self.password = password
def get_uri(self) -> str: def get_uri(self) -> str:
""" """
Retorna una URI de conexión para PostgreSQL. Retorna una URI de conexión para PostgreSQL.
+52 -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,10 +28,10 @@ if pssword is None:
# MODELO (SQLAlchemy) # MODELO (SQLAlchemy)
# ---------------------- # ----------------------
class PostgresCredencialModel(Base): class PostgresCredencialModel(Base, Model_base):
__tablename__ = 'postgres_credenciales' __tablename__ = 'postgres_credenciales'
id = Column(Integer, primary_key=True) id = Column(String, primary_key=True)
titulo = Column(String, nullable=False) titulo = Column(String, nullable=False)
host = Column(String, nullable=False) host = Column(String, nullable=False)
port = Column(Integer, nullable=False) port = Column(Integer, nullable=False)
@@ -36,12 +43,40 @@ 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 {
"id": obj.id,
"titulo": obj.titulo, "titulo": obj.titulo,
"host": obj.host, "host": obj.host,
"port": obj.port, "port": obj.port,
@@ -55,6 +90,7 @@ class PostgresCredencialMapper:
@staticmethod @staticmethod
def from_dict(data: dict) -> PostgresCredencial: def from_dict(data: dict) -> PostgresCredencial:
return PostgresCredencial( return PostgresCredencial(
id=data["id"],
titulo=data["titulo"], titulo=data["titulo"],
host=data["host"], host=data["host"],
port=data["port"], port=data["port"],
@@ -65,42 +101,22 @@ class PostgresCredencialMapper:
) )
) )
@staticmethod
def from_model(model: PostgresCredencialModel) -> PostgresCredencial:
return PostgresCredencial(
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) -> int: 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_: int) -> 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
+128 -49
View File
@@ -1,6 +1,6 @@
from src.Llms.Modelos.Base_model import ModeloABC from src.Llms.Modelos.Base_model import ModeloABC
from src.Llms.Memory.Base_MemoryConv import MemoryConvABC from src.Llms.Memory.Base_MemoryConv import MemoryConvABC
from src.Llms.MCPs.McpClient_Registry import ClientRegistry
from datetime import datetime from datetime import datetime
from typing import Optional, List, Union, AsyncGenerator from typing import Optional, List, Union, AsyncGenerator
@@ -17,12 +17,11 @@ class AgenteAI:
max_iterations: int = 1, max_iterations: int = 1,
memoria: Optional[MemoryConvABC] = None, memoria: Optional[MemoryConvABC] = None,
version: str = "1.0.0", version: str = "1.0.0",
tools: Optional[List] = None, mcp: ClientRegistry = None,
output_schema: Optional[dict] = None, output_schema: Optional[dict] = None,
): ):
self.modelo = modelo self.modelo = modelo
self.memoria = memoria self.memoria = memoria
self.tools = tools or []
self.output_schema = output_schema self.output_schema = output_schema
self.nombre = nombre self.nombre = nombre
@@ -36,6 +35,9 @@ class AgenteAI:
self.created_at = datetime.now() self.created_at = datetime.now()
self.updated_at = self.created_at self.updated_at = self.created_at
self.numero_interacciones = 0 self.numero_interacciones = 0
self.mcp = mcp # <-- Aquí guardamos el registry
def actualizar_configuracion(self, **kwargs): def actualizar_configuracion(self, **kwargs):
for clave, valor in kwargs.items(): for clave, valor in kwargs.items():
@@ -43,56 +45,105 @@ class AgenteAI:
setattr(self, clave, valor) setattr(self, clave, valor)
self.updated_at = datetime.now() self.updated_at = datetime.now()
@property @property
def full_system_prompt(self) -> str: async def full_system_prompt(self) -> str:
partes = [ tools_str = await self._obtener_herramientas_disponibles_str()
f"Tu nombre es: {self.nombre}", return f"""
f"Tu descripción: {self.descripcion}", Eres un agente conversacional con acceso a herramientas MCP (Model Context Protocol).
f"Tu Rol: {self.rol}",
f"Tus Objetivos: {', '.join(self.objetivos)}",
""
]
herramientas = self._obtener_descripcion_tools() Tu comportamiento sigue este flujo:
if herramientas:
partes.append("Estas son tus herramientas disponibles:")
partes.extend(herramientas)
partes.append(
"Cuando consideres necesario, utiliza las herramientas disponibles "
"para responder de manera más precisa o realizar tareas específicas. "
"Indica claramente qué herramienta estás utilizando y por qué."
)
partes.append(self.system_prompt) 1. **Piensa** para razonar tu decisión.
2. **Decide** si:
- puedes responder tú mismo,
- necesitas más información del usuario,
- o necesitas una herramienta MCP.
3. **Actúa**:
- Cuando uses MCP, termina **solo** con un bloque de código MCP y **nada más**.
- Ten en cuenta EXACTAMENTE los parámetros especificados.
- **No expliques, no hables después del bloque. Termina tu turno.**
if self.output_schema: ---
partes.append("SIEMPRE formatea la respuesta final siguiendo estrictamente el siguiente esquema JSON:")
partes.append(f"```json\n{self.output_schema}\n```")
return "\n".join(partes) # Formato MCP
def _obtener_descripcion_tools(self) -> List[str]: ```mcp
descripciones = [] {{
if not hasattr(self, "mcp_servers"): "tool": "<nombre_de_la_herramienta>",
return descripciones "input": {{
"clave": "valor"
}}
}}
Reglas clave:
for server in self.mcp_servers: Razonas antes de actuar.
if hasattr(server, "tools") and server.tools:
for tool in server.tools: Nunca hables después de un bloque MCP.
if isinstance(tool, str):
descripciones.append(f"- {tool}: [sin descripción]") No combines respuestas y herramientas.
elif isinstance(tool, dict):
nombre = tool.get("name", "¿?") Piensa. Decide. Actúa.
descripcion = tool.get("description", "[sin descripción]")
descripciones.append(f"- {nombre}: {descripcion}") Herramientas disponibles para usar con MCP:
elif hasattr(tool, "name"): {tools_str}
descripcion = getattr(tool, "description", "[sin descripción]")
descripciones.append(f"- {tool.name}: {descripcion}") """.strip()
return descripciones
# Conseguir las herramientas disponibles
async def _obtener_herramientas_disponibles_str(self) -> str:
if not self.mcp:
return "No se han definido herramientas disponibles."
herramientas = []
tools_por_cliente = await self.mcp.listar_tools_por_cliente()
for name, tools in tools_por_cliente.items():
if not tools:
continue
herramientas.append(f"\n🔌 Cliente: {name}")
for tool in tools:
props = tool.inputSchema.get("properties", {})
parametros = "\n ".join(f"- {k} ({v.get('type', '?')})" for k, v in props.items())
herramientas.append(f"""Nombre: {tool.name}
Descripción: {tool.description}
Parámetros:
{parametros}
""")
return "\n".join(herramientas) or "No hay herramientas disponibles actualmente."
# Formatear prompt para agentes
def _formatear_prompt(self, mensajes: List[dict]) -> str: def _formatear_prompt(self, mensajes: List[dict]) -> str:
return "\n".join([f"{msg['role']}: {msg['content']}" for msg in mensajes]) return "\n".join([f"{msg['role']}: {msg['content']}" for msg in mensajes])
###----------- Funcion para interactuar
async def interactuar(self, prompt: str, stream: bool = False) -> Union[str, AsyncGenerator[str, None]]: async def interactuar(self, prompt: str, stream: bool = False) -> Union[str, AsyncGenerator[str, None]]:
historial = self.memoria.cargar_historial_chat() if self.memoria else [] historial = self.memoria.cargar_historial_chat() if self.memoria else []
contexto = historial + [{"role": "user", "content": prompt}] contexto = historial + [{"role": "user", "content": prompt}]
@@ -100,12 +151,11 @@ class AgenteAI:
respuesta = await self.modelo.responder( respuesta = await self.modelo.responder(
prompt=prompt_final, prompt=prompt_final,
system_prompt=self.full_system_prompt, system_prompt=await self.full_system_prompt, # ✅ correcto
stream=stream stream=stream
) )
if stream: if stream:
# stream es un generador asincrónico
async def wrapper(): async def wrapper():
buffer_respuesta = "" buffer_respuesta = ""
async for token in respuesta: async for token in respuesta:
@@ -125,31 +175,45 @@ class AgenteAI:
self.updated_at = datetime.now() self.updated_at = datetime.now()
return respuesta return respuesta
###----------- Funcion para interactuar en bucle
async def interactuar_en_bucle(self, prompt: str, stream: bool = False) -> Union[List[str], AsyncGenerator[str, None]]: async def interactuar_en_bucle(self, prompt: str, stream: bool = False) -> Union[List[str], AsyncGenerator[str, None]]:
print("🚀 [interactuar_en_bucle] Iniciando interacción")
historial = self.memoria.cargar_historial_chat() if self.memoria else [] historial = self.memoria.cargar_historial_chat() if self.memoria else []
print(f"📜 [interactuar_en_bucle] Historial cargado: {historial}")
respuestas = [] if not stream else None respuestas = [] if not stream else None
respuesta_anterior = None respuesta_anterior = None
iteration = 0 iteration = 0
prompt_original = prompt.strip() prompt_original = prompt.strip()
print(f"✏️ [interactuar_en_bucle] Prompt original: {prompt_original}")
async def generador(): async def generador():
nonlocal iteration, respuesta_anterior nonlocal iteration, respuesta_anterior
prompt_actual = prompt_original prompt_actual = prompt_original
while self.max_iterations == 0 or iteration < self.max_iterations: while self.max_iterations == 0 or iteration < self.max_iterations:
print(f"\n🔁 [generador] Iteración: {iteration}")
if iteration == 0: if iteration == 0:
prompt_actual += ( prompt_actual += (
"\n\nIMPORTANTE:\n" "\n\nIMPORTANTE:\n"
"Si al revisar tu última respuesta y mi pregunta inicial consideras que has terminado, " "Si al revisar tu última respuesta y mi pregunta inicial consideras que has terminado, "
"di alguna de estas frases: <FIN>" "di alguna de estas frases: <END>"
) )
else: else:
prompt_actual = ( prompt_actual = (
f"Esta es la pregunta original:\n{prompt_original}\n\n" f"Esta es la pregunta original:\n{prompt_original}\n\n"
f"Esto fue lo último que dijiste:\n{respuesta_anterior}\n" f"Esto fue lo último que dijiste:\n{respuesta_anterior}\n"
"\n\nIMPORTANTE:\n" "\n\nIMPORTANTE:\n"
"Si al revisar tu última respuesta y mi pregunta inicial consideras que has terminado, " "Si al revisar tu última respuesta y mi pregunta inicial consideras que has terminado, "
"di alguna de estas frases: <FIN>" "di alguna de estas frases: <END>"
"En caso contrario, responde a la pregunta original " "En caso contrario, responde a la pregunta original "
"y añade información relevante que no hayas mencionado antes.\n\n" "y añade información relevante que no hayas mencionado antes.\n\n"
) )
@@ -157,30 +221,40 @@ class AgenteAI:
contexto = historial + [{"role": "user", "content": prompt_actual}] contexto = historial + [{"role": "user", "content": prompt_actual}]
prompt_final = self._formatear_prompt(contexto) prompt_final = self._formatear_prompt(contexto)
print(f"📨 [generador] Prompt final enviado al modelo:\n{prompt_final}")
print("🤖 [generador] Esperando respuesta del modelo...")
respuesta = await self.modelo.responder( respuesta = await self.modelo.responder(
prompt=prompt_final, prompt=prompt_final,
system_prompt=self.full_system_prompt, system_prompt=await self.full_system_prompt,
stream=stream stream=stream
) )
print("✅ [generador] Respuesta recibida")
if stream: if stream:
buffer_respuesta = "" buffer_respuesta = ""
async for token in respuesta: async for token in respuesta:
buffer_respuesta += token buffer_respuesta += token
# print(f"🔹 [stream] Token: {token}")
yield token yield token
respuesta_anterior = buffer_respuesta respuesta_anterior = buffer_respuesta
# print(f"📦 [stream] Respuesta completa:\n{respuesta_anterior}")
else: else:
respuestas.append(respuesta) respuestas.append(respuesta)
respuesta_anterior = respuesta respuesta_anterior = respuesta
# print(f"📦 [generador] Respuesta completa:\n{respuesta_anterior}")
if self.memoria: if self.memoria:
print("💾 [memoria] Guardando turno en la memoria...")
self.memoria.guardar_turno("user", prompt_actual) self.memoria.guardar_turno("user", prompt_actual)
self.memoria.guardar_turno("assistant", respuesta_anterior) self.memoria.guardar_turno("assistant", respuesta_anterior)
self.numero_interacciones += 1 self.numero_interacciones += 1
self.updated_at = datetime.now() self.updated_at = datetime.now()
print(f"📊 [generador] Interacción #{self.numero_interacciones} registrada")
if "<fin>" in respuesta_anterior.lower(): if "<end>" in respuesta_anterior.lower():
print("🛑 [generador] Detectado <end>. Terminando bucle.")
break break
iteration += 1 iteration += 1
@@ -188,6 +262,11 @@ class AgenteAI:
return generador() if stream else await generador_to_list(generador) return generador() if stream else await generador_to_list(generador)
# Helper para consumir generador asincrónico si no es stream # Helper para consumir generador asincrónico si no es stream
async def generador_to_list(gen: AsyncGenerator[str, None]) -> List[str]: async def generador_to_list(gen: AsyncGenerator[str, None]) -> List[str]:
buffer = "" buffer = ""
+13
View File
@@ -0,0 +1,13 @@
from abc import ABC, abstractmethod
from typing import List
class EmbedderABC(ABC):
@abstractmethod
def encoder(self, text: str) -> List[float]:
"""Genera los embeddings para un texto dado."""
pass
@abstractmethod
def dimension_number(self) -> int:
"""Devuelve la dimensión del modelo de embedding."""
pass
+32
View File
@@ -0,0 +1,32 @@
from typing import List
from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que EmbedderABC esté en este módulo
from src.ApiKeys.openai_apikey import OpenAICredencial
from src.ConexionApis.OpenAi_conexion import OpenAICliente
from src.Security.GenerarIDs import GeneradorIDUnico
class OpenAIEmbedder(EmbedderABC):
def __init__(self, credencial: OpenAICredencial,
model: str,
id: str = None):
self.model = model
self.client = OpenAICliente(credencial)
self._dimension = None # Lazy loading
self.id = id if id is not None else GeneradorIDUnico("OAMB").generar()
def encoder(self, text: str) -> List[float]:
"""
Genera los embeddings para un texto dado utilizando el modelo de OpenAI.
"""
response = self.client.embedding(model=self.model, input=text)
embedding = response.data[0].embedding
if self._dimension is None:
self._dimension = len(embedding)
return embedding
def dimension_number(self) -> int:
"""
Devuelve la dimensión del modelo de embedding, generando un embedding si no se ha calculado aún.
"""
if self._dimension is None:
_ = self.encoder("dimension_check")
return self._dimension
+96
View File
@@ -0,0 +1,96 @@
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
from src.Llms.Embedders.Openai_embedder import OpenAIEmbedder
from src.ApiKeys.openai_apikey import OpenAICredencial
# ----------------------
# Cargar configuración desde .env si se requiere
# ----------------------
from entrypoint import ENV_PATH
load_dotenv(ENV_PATH)
# ----------------------
# MODELO (SQLAlchemy)
# ----------------------
class OpenAIEmbedderModel(Base, Model_base):
__tablename__ = "openai_embedders"
id = Column(String, primary_key=True)
api_key_id = Column(String, ForeignKey("openai_credenciales.id"), nullable=False)
model = Column(String, nullable=False)
# ----------------------
# MAPPER
# ----------------------
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 {
"id": obj.id,
"api_key_id": obj.client.credencial.id,
"model": obj.model
}
@staticmethod
def from_dict(data: dict, credencial: OpenAICredencial) -> OpenAIEmbedder:
return OpenAIEmbedder(
id=data["id"],
credencial=credencial,
model=data["model"]
)
# ----------------------
# REPO
# ----------------------
class OpenAIEmbedderRepo(Repo_base[OpenAIEmbedderModel, OpenAIEmbedder]):
def __init__(self, conexion: ConexionBase):
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(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(self.Modelo).all()
return [
self.Mapper.from_model(m, credencial_loader(m.api_key_id))
for m in models
]
-65
View File
@@ -1,65 +0,0 @@
import asyncio
import os
from typing import Optional, List, Dict
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.types import Tool
class MCPStdioServer:
def __init__(
self,
name: str,
command: str,
args: List[str],
env: Optional[Dict[str, str]] = None
):
self.name = name
self.command = command
self.args = args
self.env = env or os.environ.copy()
self.exit_stack = AsyncExitStack()
self.session: Optional[ClientSession] = None
self.tools: List[Tool] = []
async def start(self):
# Configurar el bucle de eventos Proactor en Windows si es necesario
if os.name == "nt":
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
server_params = StdioServerParameters(
command=self.command,
args=self.args,
env=self.env
)
# Iniciar el transporte y establecer la sesión
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
read, write = stdio_transport
self.session = await self.exit_stack.enter_async_context(ClientSession(read, write))
await self.session.initialize()
response = await self.session.list_tools()
self.tools = response.tools
if self.tools:
print(f"[{self.name}] Servidor iniciado con herramientas:")
for tool in self.tools:
nombre = getattr(tool, "name", "[sin nombre]")
descripcion = getattr(tool, "description", "[sin descripción]")
print(f" - {nombre} - {descripcion}")
else:
print(f"[{self.name}] Servidor iniciado, pero no se detectaron herramientas.")
async def call_tool(self, tool_name: str, arguments: Dict):
if not self.session:
raise RuntimeError("La sesión no está inicializada.")
result = await self.session.call_tool(tool_name, arguments)
return result.content
def get_tool_names(self) -> List[str]:
return [tool.name for tool in self.tools]
async def stop(self):
await self.exit_stack.aclose()
print(f"[{self.name}] Servidor detenido.")
+96
View File
@@ -0,0 +1,96 @@
from pathlib import Path
from typing import Any, Optional, Union
from pydantic import AnyUrl
from fastmcp.client import Client
from fastmcp.client.transports import (
StreamableHttpTransport,
PythonStdioTransport,
ClientTransport,
)
from mcp.types import *
from fastmcp.exceptions import ClientError
class MCPClient:
def __init__(self, name: str, client: Client):
self.name = name
self.client = client
def __repr__(self) -> str:
return f"<ClientWrapper(name={self.name})>"
@classmethod
def from_http(cls, name: str, url: str | AnyUrl) -> "MCPClient":
transport = StreamableHttpTransport(url=str(url))
client = Client(transport=transport)
return cls(name=name, client=client)
@classmethod
def from_stdio(
cls,
name: str,
script_path: Union[str, Path],
args: Optional[list[str]] = None,
cwd: Optional[Union[str, Path]] = None,
env: Optional[dict[str, str]] = None,
) -> "MCPClient":
transport = PythonStdioTransport(
script_path=script_path, args=args, cwd=cwd, env=env
)
client = Client(transport=transport)
return cls(name=name, client=client)
def is_connected(self) -> bool:
return self.client.is_connected()
async def __aenter__(self):
await self.client.__aenter__()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.client.__aexit__(exc_type, exc_val, exc_tb)
# Delegación MCP
async def call_tool(
self, name: str, arguments: dict[str, Any] | None = None
) -> list[TextContent | ImageContent | EmbeddedResource]:
return await self.client.call_tool(name, arguments)
async def get_prompt(
self, name: str, arguments: dict[str, str] | None = None
) -> GetPromptResult:
return await self.client.get_prompt(name, arguments)
async def list_tools(self) -> list[Tool]:
return await self.client.list_tools()
async def list_prompts(self) -> list[Prompt]:
return await self.client.list_prompts()
async def list_resources(self) -> list[Resource]:
return await self.client.list_resources()
async def list_resource_templates(self) -> list[ResourceTemplate]:
return await self.client.list_resource_templates()
async def read_resource(
self, uri: AnyUrl | str
) -> list[TextResourceContents | BlobResourceContents]:
return await self.client.read_resource(uri)
async def complete(
self,
ref: ResourceReference | PromptReference,
argument: dict[str, str],
) -> Completion:
return await self.client.complete(ref, argument)
async def ping(self) -> bool:
return await self.client.ping()
async def set_logging_level(self, level: LoggingLevel) -> None:
return await self.client.set_logging_level(level)
async def send_roots_list_changed(self) -> None:
return await self.client.send_roots_list_changed()
+56
View File
@@ -0,0 +1,56 @@
from src.Llms.MCPs.McpClient import MCPClient
from typing import Any
class ClientRegistry:
def __init__(self):
self._clients: dict[str, MCPClient] = {}
def add(self, name: str, wrapper: MCPClient) -> None:
self._clients[name] = wrapper
def get(self, name: str) -> MCPClient:
if name not in self._clients:
raise KeyError(f"Cliente '{name}' no encontrado en el registro.")
return self._clients[name]
def all(self) -> dict[str, MCPClient]:
return self._clients
def list_names(self) -> list[str]:
return list(self._clients.keys())
def __contains__(self, name: str) -> bool:
return name in self._clients
async def listar_tools_por_cliente(self) -> dict[str, list[Any]]:
resultado = {}
for name, wrapper in self._clients.items():
try:
async with wrapper:
resultado[name] = await wrapper.list_tools()
except Exception as e:
print(f"[TOOLS] ❌ Error en '{name}': {e}")
resultado[name] = []
return resultado
async def listar_prompts_por_cliente(self) -> dict[str, list[Any]]:
resultado = {}
for name, wrapper in self._clients.items():
try:
async with wrapper:
resultado[name] = await wrapper.list_prompts()
except Exception as e:
print(f"[PROMPTS] ❌ Error en '{name}': {e}")
resultado[name] = []
return resultado
async def listar_resources_por_cliente(self) -> dict[str, list[Any]]:
resultado = {}
for name, wrapper in self._clients.items():
try:
async with wrapper:
resultado[name] = await wrapper.list_resources()
except Exception as e:
print(f"[RESOURCES] ❌ Error en '{name}': {e}")
resultado[name] = []
return resultado
+92
View File
@@ -0,0 +1,92 @@
from fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool(description="Suma dos números enteros.")
def add(a: int, b: int) -> int:
return a + b
@mcp.tool(description="Resta dos números enteros.")
def subtract(a: int, b: int) -> int:
return a - b
@mcp.tool(description="Multiplica dos números enteros.")
def multiply(a: int, b: int) -> int:
return a * b
@mcp.tool(description="Divide dos números y devuelve el resultado flotante.")
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("No se puede dividir entre cero.")
return a / b
@mcp.tool(description="Calcula el módulo de dos números enteros.")
def modulo(a: int, b: int) -> int:
return a % b
@mcp.tool(description="Concatena dos cadenas de texto.")
def concat(a: str, b: str) -> str:
return a + b
@mcp.tool(description="Devuelve la longitud de una cadena.")
def string_length(s: str) -> int:
return len(s)
@mcp.tool(description="Convierte una cadena a mayúsculas.")
def to_upper(s: str) -> str:
return s.upper()
@mcp.tool(description="Convierte una cadena a minúsculas.")
def to_lower(s: str) -> str:
return s.lower()
@mcp.tool(description="Devuelve la suma de todos los elementos en una lista de enteros.")
def sum_list(numbers: list[int]) -> int:
return sum(numbers)
@mcp.tool(description="Devuelve el valor máximo en una lista de enteros.")
def max_in_list(numbers: list[int]) -> int:
return max(numbers)
@mcp.tool(description="Verifica si un número es par.")
def is_even(n: int) -> bool:
return n % 2 == 0
@mcp.tool(description="Verifica si una cadena es un palíndromo.")
def is_palindrome(s: str) -> bool:
return s == s[::-1]
@mcp.tool(description="Calcula el factorial de un número entero positivo.")
def factorial(n: int) -> int:
if n < 0:
raise ValueError("El factorial no está definido para negativos.")
if n == 0:
return 1
result = 1
for i in range(1, n + 1):
result *= i
return result
@mcp.tool(description="Devuelve los primeros n números de Fibonacci.")
def fibonacci(n: int) -> list[int]:
if n <= 0:
return []
seq = [0, 1]
while len(seq) < n:
seq.append(seq[-1] + seq[-2])
return seq[:n]
@mcp.tool(description="Devuelve si un número es primo.")
def is_prime(n: int) -> bool:
if n <= 1:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True
if __name__ == "__main__":
# mcp.run(transport="streamable-http", host="127.0.0.1", port=4200, path="/math")
mcp.run(transport="stdio")
+68
View File
@@ -0,0 +1,68 @@
from fastmcp import FastMCP
import uuid
import datetime
import socket
import platform
import os
mcp = FastMCP()
@mcp.tool(description="Genera un UUID versión 4.")
def generate_uuid() -> str:
return str(uuid.uuid4())
@mcp.tool(description="Devuelve la fecha y hora actuales en formato ISO 8601.")
def current_datetime() -> str:
return datetime.datetime.now().isoformat()
@mcp.tool(description="Devuelve solo la fecha actual.")
def current_date() -> str:
return datetime.date.today().isoformat()
@mcp.tool(description="Devuelve el nombre del host actual.")
def get_hostname() -> str:
return socket.gethostname()
@mcp.tool(description="Devuelve el sistema operativo actual.")
def get_os() -> str:
return platform.system()
@mcp.tool(description="Devuelve el nombre del usuario actual del sistema.")
def get_current_user() -> str:
return os.getlogin()
@mcp.tool(description="Invierte un valor booleano.")
def invert_boolean(flag: bool) -> bool:
return not flag
# @mcp.tool(description="Devuelve los archivos y carpetas del directorio actual.")
# def list_current_directory() -> list[str]:
# return os.listdir()
# @mcp.tool(description="Crea un archivo con un nombre dado.")
# def create_file(filename: str) -> str:
# with open(filename, "w") as f:
# f.write("")
# return f"Archivo '{filename}' creado."
# @mcp.tool(description="Lee el contenido de un archivo de texto dado.")
# def read_file(filename: str) -> str:
# with open(filename, "r") as f:
# return f.read()
# @mcp.tool(description="Escribe contenido a un archivo, sobrescribiéndolo.")
# def write_file(filename: str, content: str) -> str:
# with open(filename, "w") as f:
# f.write(content)
# return f"Contenido escrito en '{filename}'."
@mcp.tool(description="Devuelve el número de CPUs disponibles en el sistema.")
def get_cpu_count() -> int:
return os.cpu_count()
@mcp.tool(description="Devuelve el timestamp actual (UNIX).")
def current_timestamp() -> float:
return datetime.datetime.now().timestamp()
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="127.0.0.1", port=4300, path="/tools")
+1 -1
View File
@@ -27,7 +27,7 @@ class MemoryConvPostgres(MemoryConvABC):
) )
# Crea la tabla si no existe # Crea la tabla si no existe
self.metadata.create_all(self.conexion.engine) self.metadata.create_all(self.conexion._engine)
def guardar_turno(self, rol: Literal["user", "assistant"], contenido: str) -> None: def guardar_turno(self, rol: Literal["user", "assistant"], contenido: str) -> None:
stmt = insert(self.tabla).values(rol=rol, contenido=contenido) stmt = insert(self.tabla).values(rol=rol, contenido=contenido)
+10 -2
View File
@@ -1,5 +1,6 @@
from src.Llms.Modelos.Base_model import ModeloABC from src.Llms.Modelos.Base_model import ModeloABC
from src.ConexionApis.OpenAi_conexion import OpenAICliente from src.ConexionApis.OpenAi_conexion import OpenAICliente
from src.Security.GenerarIDs import GeneradorIDUnico
import asyncio import asyncio
from typing import AsyncGenerator, Union from typing import AsyncGenerator, Union
@@ -8,6 +9,7 @@ class ModeloOpenAI(ModeloABC):
self, self,
cliente: OpenAICliente, cliente: OpenAICliente,
model: str = "gpt-4o", model: str = "gpt-4o",
id: str = None,
temperature: float = 0.7, temperature: float = 0.7,
top_p: float = 1.0, top_p: float = 1.0,
top_k: int = None, top_k: int = None,
@@ -15,6 +17,10 @@ class ModeloOpenAI(ModeloABC):
num_tokens_maximos: int = 512, num_tokens_maximos: int = 512,
use_legacy: bool = False use_legacy: bool = False
): ):
# Generar ID con prefijo MOPA si no fue proporcionado
self.id = id if id is not None else GeneradorIDUnico("MOPA").generar()
# Inicializar resto del modelo base
super().__init__( super().__init__(
model=model, model=model,
temperature=temperature, temperature=temperature,
@@ -23,6 +29,8 @@ class ModeloOpenAI(ModeloABC):
frecuencia_penalizacion=frecuencia_penalizacion, frecuencia_penalizacion=frecuencia_penalizacion,
num_tokens_maximos=num_tokens_maximos num_tokens_maximos=num_tokens_maximos
) )
# Asignar cliente e indicadores adicionales
self.cliente = cliente self.cliente = cliente
self.use_legacy = use_legacy self.use_legacy = use_legacy
@@ -75,8 +83,8 @@ class ModeloOpenAI(ModeloABC):
if stream: if stream:
async def generador(): async def generador():
for token in resultado: # ya es un generador del cliente for token in resultado:
yield token yield token
return generador() return generador()
else: else:
return resultado.choices[0].message.content return resultado.choices[0].message.content
+71 -35
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,38 +25,44 @@ 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(Integer, 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_dict(obj: ModeloOpenAI) -> dict:
return {
"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 @staticmethod
def from_model(model: ModeloOpenAIConfigModel, cliente: object) -> ModeloOpenAI: 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( return ModeloOpenAI(
id=model.id,
cliente=cliente, cliente=cliente,
model=model.model, model=model.model,
temperature=model.temperature, temperature=model.temperature,
@@ -61,26 +73,50 @@ class ModeloOpenAIConfigMapper:
use_legacy=model.use_legacy use_legacy=model.use_legacy
) )
@staticmethod
def to_dict(obj: ModeloOpenAI) -> dict:
return {
"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_dict(data: dict, cliente: Optional[object] = None) -> ModeloOpenAI:
return ModeloOpenAI(
id=data["id"],
cliente=cliente,
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 # 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,
mapper=ModeloOpenAIConfigMapper
)
self.cliente = cliente # Necesario para construir el dominio con lógica
def add(self, config: ModeloOpenAI) -> int: def get_by_id(self, id_: str) -> ModeloOpenAI | None:
data = ModeloOpenAIConfigMapper.to_dict(config) model = self.session.get(self.Modelo, id_)
model = ModeloOpenAIConfigModel(**data) return self.Mapper.from_model(model, self.cliente) if model else None
self.session.add(model)
self.session.commit()
return model.id
def get_by_id(self, id_: int) -> ModeloOpenAI | None:
model = self.session.get(ModeloOpenAIConfigModel, id_)
return ModeloOpenAIConfigMapper.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]
+67
View File
@@ -0,0 +1,67 @@
import uuid
import datetime
import re
class GeneradorIDUnico:
def __init__(self, tipo_objeto: str):
if not re.match(r'^[A-Z]{4}$', tipo_objeto):
raise ValueError("El tipo de objeto debe tener 4 letras en mayúscula (ej: ABCD)")
self.tipo_objeto = tipo_objeto
def generar(self):
f = datetime.datetime.now().strftime('%Y%m%d')
u = uuid.uuid4().hex[:9]
n = ''.join(filter(str.isdigit, uuid.uuid4().hex))[:8]
n = n.ljust(8, '0')
t = sum(int(c) for c in f)
t += sum(int(c, 16) for c in u)
t += sum(int(c) for c in n)
c = str(t % 10)
l = t + int(c)
d = hex(l % 16)[-1]
return f"{self.tipo_objeto}{f}-{d}{u}{c}{n}"
@staticmethod
def verificar(id_: str) -> bool:
try:
f = id_[4:12]
cuerpo = id_[13:]
d = cuerpo[0]
u = cuerpo[1:10]
c = cuerpo[10]
n = cuerpo[11:19]
t = sum(int(c) for c in f)
t += sum(int(c, 16) for c in u)
t += sum(int(c) for c in n)
esd = str(t % 10)
l = t + int(esd)
esh = hex(l % 16)[-1]
return (
d.lower() == esh.lower() and
c == esd
)
except Exception:
return False
@staticmethod
def tamaño_bytes_bits(id_: str):
"""Devuelve el tamaño del ID en bytes y bits (UTF-8)"""
bytes_ = len(id_.encode('utf-8'))
return bytes_, bytes_ * 8
if __name__ == "__main__":
# Ejemplo de uso
generador = GeneradorIDUnico("FACT")
nuevo_id = generador.generar()
print(f"Nuevo ID generado: {nuevo_id}")
# Verificación del ID
es_valido = GeneradorIDUnico.verificar(nuevo_id)
print(f"El ID es válido: {es_valido}")
# Tamaño del ID
bytes_, bits_ = GeneradorIDUnico.tamaño_bytes_bits(nuevo_id)
print(f"Tamaño del ID: {bytes_} bytes / {bits_} bits")
View File
+59
View File
@@ -0,0 +1,59 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from src.Llms.Embedders.Base_Embedder import EmbedderABC # Asegúrate de que esta ruta sea correcta
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_mmr import generar_tabla_nota_para_biblioteca # Ajusta si es necesario
from sqlalchemy import inspect
from src.base import Base
class Biblioteca:
def __init__(
self,
nombre: str,
descripcion: str = "",
id: Optional[str] = None,
embedder: Optional[EmbedderABC] = None,
vector_dim: Optional[int] = None
):
"""
Clase que representa una biblioteca de notas de texto.
:param nombre: Nombre de la biblioteca.
:param descripcion: Breve descripción de la biblioteca.
:param id: ID único opcional. Si no se proporciona, se genera automáticamente.
:param embedder: Objeto que implementa EmbedderABC para generar el vector del nombre.
:param vector_dim: Dimensión del vector si no se proporciona un embedder.
"""
self.id = id if id is not None else GeneradorIDUnico("BBLI").generar()
self.nombre = nombre if "biblio" in nombre else f"biblio_{nombre}"
self.descripcion = descripcion
self.embedder = embedder
if self.embedder is not None:
self.vector_dim = self.embedder.dimension_number()
elif vector_dim is not None:
self.vector_dim = vector_dim
else:
raise ValueError("Debes proporcionar un 'embedder' o un 'vector_dim' explícito.")
def generar_modelo_notas(self, conexion: ConexionBase):
nombre_tabla = f"{self.nombre}"
print(f"[Notas] Generando tabla: {nombre_tabla}")
engine = conexion.get_engine()
inspector = inspect(engine)
if inspector.has_table(nombre_tabla):
print(f"[Notas] ❌ Ya existe la tabla {nombre_tabla}")
raise ValueError(f"Ya existe una tabla con el nombre '{nombre_tabla}' en la base de datos.")
print("[Notas] Generando definición SQL...")
tabla, NotaModel = generar_tabla_nota_para_biblioteca(nombre_tabla, self.vector_dim, Base.metadata)
print("[Notas] Creando tabla en base de datos...")
Base.metadata.create_all(engine)
print("[Notas] ✔️ Tabla creada")
return NotaModel
+112
View File
@@ -0,0 +1,112 @@
import os
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
from src.Security.GenerarIDs import GeneradorIDUnico
from src.Llms.Embedders.Base_Embedder import EmbedderABC
from src.TextManager.biblioteca import Biblioteca # Suponiendo que defines la clase lógica Biblioteca aquí
# ----------------------
# Cargar clave maestra
# ----------------------
from entrypoint import ENV_PATH
load_dotenv(ENV_PATH)
pssword = os.getenv('MASTER_PASSWORD')
if pssword is None:
raise ValueError("MASTER_PASSWORD no está definida en el archivo .env")
# ----------------------
# MODELO (SQLAlchemy)
# ----------------------
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, nullable=False, default="")
vector_dim = Column(Integer, nullable=False)
embedder_info = Column(String, nullable=True) # Nombre de clase, ID de configuración, o info encriptada del embedder
# ----------------------
# MAPPER
# ----------------------
class BibliotecaMapper(Mapper_base[Biblioteca, BibliotecaModel]):
@staticmethod
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
def from_model(model: BibliotecaModel) -> Biblioteca:
return Biblioteca(
id=model.id,
nombre=model.nombre,
descripcion=model.descripcion,
vector_dim=model.vector_dim,
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(Repo_base[BibliotecaModel, Biblioteca]):
def __init__(self, conexion: ConexionBase):
super().__init__(
session=conexion.get_session(),
modelo=BibliotecaModel,
mapper=BibliotecaMapper
)
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}'")
return super().add(biblioteca, created_by=created_by, notes=notes)
def get_by_nombre(self, nombre: str) -> Biblioteca | None:
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
+41
View File
@@ -0,0 +1,41 @@
from src.Security.GenerarIDs import GeneradorIDUnico
from typing import List
class Nota:
def __init__(
self,
titulo: str,
tags: List[str] = None,
conexiones: List[str] = None,
texto: str = "",
vector: List[float] = None,
resumen: str = "",
vector_resumen: List[float] = None,
id: str = None
):
"""
Clase que representa una nota de texto con estructura semántica.
:param titulo: Título de la nota.
:param tags: Lista de etiquetas asociadas.
:param conexiones: Lista de identificadores relacionados.
:param vector: Embedding vectorial de la nota.
:param resumen: Texto resumen de la nota.
:param vector_resumen: Embedding del resumen.
:param id: Identificador único (si no se proporciona, se genera automáticamente).
"""
self.id = id if id is not None else GeneradorIDUnico("NOTA").generar()
self.titulo = titulo
self.tags = tags if tags is not None else []
self.conexiones = conexiones if conexiones is not None else []
self.texto = texto
self.vector = vector
self.resumen = resumen
self.vector_resumen = vector_resumen
def __repr__(self):
return (
f"<Nota id={self.id}, titulo='{self.titulo}', tags={len(self.tags)}, "
f"conexiones={len(self.conexiones)}, vector_dim={len(self.vector)}, "
f"resumen_len={len(self.resumen)}, vector_resumen_dim={len(self.vector_resumen)}>"
)
+172
View File
@@ -0,0 +1,172 @@
import os
from dotenv import load_dotenv
from sqlalchemy import Table, Column, String, Text, MetaData
from pgvector.sqlalchemy import Vector
from sqlalchemy.orm import registry, Session
from src.TextManager.nota import Nota
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
from src.base import Base # Este es tu declarative_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 y campos del sistema.
"""
try:
print(f"[INFO] Generando tabla para biblioteca: '{biblioteca_nombre}' con dimensión de vector: {vector_dim}")
# Nombre SQL-safe
nombre_tabla = re.sub(r"[^a-zA-Z0-9_]", "_", biblioteca_nombre.strip().lower())
print(f"[DEBUG] Nombre de tabla SQL-safe: '{nombre_tabla}'")
# Modelo ORM dinámico
class NotaModel(Base, Model_base):
__tablename__ = nombre_tabla
__table_args__ = {"extend_existing": True}
id = Column(String, primary_key=True)
titulo = Column(String, nullable=False)
tags = Column(String)
conexiones = Column(String)
texto = Column(Text)
resumen = Column(Text)
vector = Column(Vector(vector_dim), nullable=True)
vector_resumen = Column(Vector(vector_dim), nullable=True)
print(f"[INFO] Modelo ORM 'NotaModel' creado para la tabla '{nombre_tabla}'")
print(f"[DEBUG] Columnas del modelo: {[c.name for c in NotaModel.__table__.columns]}")
print(f"[DEBUG] Tipos de columnas: {[str(c.type) for c in NotaModel.__table__.columns]}")
print(f"[DEBUG] Claves primarias: {[c.name for c in NotaModel.__table__.primary_key]}")
return NotaModel.__table__, NotaModel
except Exception as e:
print(f"[ERROR] Error al generar la tabla y modelo ORM para '{biblioteca_nombre}': {e}")
raise
# ----------------------
# MAPPER
# ----------------------
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