frontend añadido y backend de creacion de notas

This commit is contained in:
2025-09-18 00:31:23 +02:00
parent 1deabb8a26
commit a76b13ec33
75 changed files with 20364 additions and 317 deletions
View File
View File
View File
+45
View File
@@ -0,0 +1,45 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from backend.services.notaService import NotaService
from backend.schemas.notaSchema import NotaCreate, NotaUpdate, NotaOut, SearchQuery, HybridSearchQuery
from backend.db.dependencies import get_db # tu función para obtener la sesión
router = APIRouter(prefix="/notas", tags=["Notas"])
@router.post("/", response_model=str)
def create_nota(data: NotaCreate, db: Session = Depends(get_db)):
return NotaService(db).create(**data.dict())
@router.get("/{id_}", response_model=NotaOut)
def get_nota(id_: str, db: Session = Depends(get_db)):
return NotaService(db).get(id_)
@router.get("/", response_model=list[NotaOut])
def list_notas(db: Session = Depends(get_db)):
return NotaService(db).list_all()
@router.put("/{id_}", response_model=bool)
def update_nota(id_: str, data: NotaUpdate, db: Session = Depends(get_db)):
return NotaService(db).update(id_, **data.dict(exclude_none=True))
@router.delete("/{id_}", response_model=bool)
def delete_nota(id_: str, db: Session = Depends(get_db)):
return NotaService(db).delete(id_)
# ---------------------
# RAG Endpoints
# ---------------------
@router.post("/search", response_model=list[NotaOut])
def search_nota(query: SearchQuery, db: Session = Depends(get_db)):
return NotaService(db).search_by_text(query.texto, query.top_k)
@router.post("/hybrid-search", response_model=list[NotaOut])
def hybrid_search(query: HybridSearchQuery, db: Session = Depends(get_db)):
return NotaService(db).hybrid_search(query.texto, query.tags, query.top_k)
+7
View File
@@ -0,0 +1,7 @@
from fastapi import APIRouter
from backend.api.endpoints.notaEndpoint import router as nota_router
api_router = APIRouter()
api_router.include_router(nota_router)
View File
+5
View File
@@ -0,0 +1,5 @@
# backend/db/base.py
from sqlalchemy.orm import declarative_base
# Declarative Base común para todos los modelos ORM
Base = declarative_base()
+8
View File
@@ -0,0 +1,8 @@
from backend.db.session import SessionLocal
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
+36
View File
@@ -0,0 +1,36 @@
# backend/db/session.py
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from urllib.parse import quote_plus
from dotenv import load_dotenv
# Cargar variables de entorno (.env) desde backend/.env
load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), "..", ".env"))
DB_USER = os.getenv("DB_USER")
DB_PASSWORD = quote_plus(os.getenv("DB_PASSWORD"))
DB_HOST = os.getenv("DB_HOST")
DB_PORT = os.getenv("DB_PORT", "55455")
DB_NAME = os.getenv("DB_NAME")
DATABASE_URL = (
f"postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
)
# Crear engine
engine = create_engine(
DATABASE_URL,
pool_size=5,
max_overflow=10,
future=True,
echo=False, # pon True para ver las queries en consola
)
# Fábrica de sesiones
SessionLocal = sessionmaker(
bind=engine,
autoflush=False,
expire_on_commit=False,
future=True,
)
+7
View File
@@ -0,0 +1,7 @@
from fastapi import FastAPI
from backend.api.router import api_router
app = FastAPI(title="Notes")
# attach router
app.include_router(api_router)
+52
View File
@@ -0,0 +1,52 @@
from pydantic import BaseModel, Field
from typing import Optional, List, Dict
# ---------------------
# Base
# ---------------------
class NotaBase(BaseModel):
titulo: str = Field(..., description="Título de la nota")
contenido: str = Field(..., description="Contenido de la nota")
tags: Optional[Dict] = Field(None, description="Tags de la nota")
relaciones: Optional[Dict] = Field(None, description="Relaciones")
propiedades: Optional[Dict] = Field(None, description="Propiedades adicionales")
# ---------------------
# Create & Update
# ---------------------
class NotaCreate(NotaBase):
pass
class NotaUpdate(BaseModel):
contenido: Optional[str]
tags: Optional[Dict]
relaciones: Optional[Dict]
propiedades: Optional[Dict]
# ---------------------
# Response
# ---------------------
class NotaOut(NotaBase):
id: str
embedder: List[float] = Field(..., description="Vector embedding de la nota")
class Config:
from_attributes = True
# ---------------------
# Search Inputs
# ---------------------
class SearchQuery(BaseModel):
texto: str
top_k: int = 5
class HybridSearchQuery(BaseModel):
texto: str
tags: Optional[Dict] = None
top_k: int = 5
+75
View File
@@ -0,0 +1,75 @@
from sqlalchemy.orm import Session
from typing import List, Optional
from domains.nota import notaDom, notaRepo
from domains.embedder import NomicEmbedder
class NotaService:
def __init__(self, session: Session):
self.repo = notaRepo(session)
self.emb = NomicEmbedder.get_instance()
# -------------------
# CRUD
# -------------------
def create(self, titulo: str, contenido: str, tags: Optional[dict] = None,
relaciones: Optional[dict] = None, propiedades: Optional[dict] = None) -> str:
embedding = self.emb.embed(contenido)
nota = notaDom(titulo, contenido, embedding, tags, relaciones, propiedades)
return self.repo.add(nota)
def get(self, id_: str) -> Optional[notaDom]:
return self.repo.get_by_id(id_)
def list_all(self) -> List[notaDom]:
return self.repo.get_all()
def update(self, id_: str, nuevo_contenido: Optional[str] = None,
tags: Optional[dict] = None, relaciones: Optional[dict] = None,
propiedades: Optional[dict] = None) -> bool:
new_data = {}
if nuevo_contenido:
new_data["contenido"] = nuevo_contenido
new_data["embedder"] = self.emb.embed(nuevo_contenido)
if tags: new_data["tags"] = tags
if relaciones: new_data["relaciones"] = relaciones
if propiedades: new_data["propiedades"] = propiedades
return self.repo.update(id_, new_data)
def delete(self, id_: str) -> bool:
return self.repo.soft_delete(id_)
# -------------------
# RAG: búsquedas
# -------------------
def search_by_text(self, texto: str, top_k: int = 5) -> List[notaDom]:
emb = self.emb.embed(texto)
sql = """
SELECT * FROM public.nota
WHERE sys_deleted_at IS NULL
ORDER BY embedder <-> :query_emb
LIMIT :top_k
"""
rows = self.repo.session.execute(sql, {"query_emb": emb, "top_k": top_k}).fetchall()
return [self.repo.Mapper.from_model(r) for r in rows]
def search_by_tags(self, tags: dict, offset: int = 0, limit: int = 10) -> List[notaDom]:
return self.repo.get_paginated_by_tags(tags, offset, limit)
def hybrid_search(self, texto: str, tags: Optional[dict] = None, top_k: int = 5) -> List[notaDom]:
emb = self.emb.embed(texto)
sql = """
SELECT * FROM public.nota
WHERE sys_deleted_at IS NULL
{tags_filter}
ORDER BY embedder <-> :query_emb
LIMIT :top_k
"""
tags_filter = "AND tags @> :tags" if tags else ""
query = sql.format(tags_filter=tags_filter)
params = {"query_emb": emb, "top_k": top_k}
if tags:
params["tags"] = tags
rows = self.repo.session.execute(query, params).fetchall()
return [self.repo.Mapper.from_model(r) for r in rows]