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
+75
View File
@@ -0,0 +1,75 @@
# 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))
+58
View File
@@ -0,0 +1,58 @@
# Model.py
from sqlalchemy import Column, DateTime, String, Integer, Text, BigInteger, func
from sqlalchemy.ext.declarative import declared_attr, as_declarative
from datetime import datetime
from backend.db.base import Base # tu Base declarativa
class Model_base(Base):
__abstract__ = True
# ID autoincremental por defecto en todos los modelos
id = Column(BigInteger, primary_key=True, autoincrement=True)
@declared_attr
def sys_created_at(cls):
# timestamptz + default en BD
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):
# onupdate lo pone la BD al actualizar
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:
out = {}
if not hasattr(self, "__table__"): return out
for c in self.__table__.columns:
v = getattr(self, c.name, None)
out[c.name] = v.isoformat() if isinstance(v, datetime) else v
return out
+149
View File
@@ -0,0 +1,149 @@
# 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 datetime import datetime, timezone
from .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(timezone.utc) # TZ-aware
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()
+6
View File
@@ -0,0 +1,6 @@
# domains/arquitecture_layer/__init__.py
from .Repo import *
from .Mapper import *
from .Model import *
__all__ = ["Repo", "Mapper", "Model"]
+26
View File
@@ -0,0 +1,26 @@
# embedder_nomic.py
from transformers import AutoTokenizer, AutoModel
import torch
class NomicEmbedder:
_instance = None
def __init__(self, model_path: str = ".model/nomic-embed-text-v1.5"):
# Load model only once
self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
self.model = AutoModel.from_pretrained(model_path, trust_remote_code=True)
@classmethod
def get_instance(cls) -> "NomicEmbedder":
if cls._instance is None:
cls._instance = NomicEmbedder()
return cls._instance
def embed(self, text: str) -> list[float]:
"""Generate embedding from text"""
inputs = self.tokenizer(
[text], return_tensors="pt", padding=True, truncation=True, max_length=8192
)
with torch.no_grad():
embedding = self.model(**inputs).last_hidden_state.mean(dim=1).squeeze()
return embedding.tolist()
+137
View File
@@ -0,0 +1,137 @@
from __future__ import annotations
from typing import Optional
from sqlalchemy import Column, String, Text, JSON
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session
from pgvector.sqlalchemy import Vector # ✅ importación correcta para pgvector
from domains.arquitecture_layer.Model import Model_base
from domains.arquitecture_layer.Mapper import Mapper_base
from domains.arquitecture_layer.Repo import Repo_base
from .embedder import NomicEmbedder
Base = declarative_base()
class notaDom:
def __init__(
self,
titulo: str,
contenido: str,
embedder: list[float], # ✅ sigue siendo list[float] en Python
tags: Optional[dict] = None,
relaciones: Optional[dict] = None,
propiedades: Optional[dict] = None, # ✅ nueva propiedad
):
self.titulo = titulo
self.contenido = contenido
self.embedder = embedder
self.tags = tags
self.relaciones = relaciones
self.propiedades = propiedades
# ------------------------
class notaModel(Model_base):
__tablename__ = "nota"
__table_args__ = ({"schema": "public"})
titulo = Column(String(255), nullable=False, comment="Título de la nota")
contenido = Column(Text, nullable=True, comment="Contenido de la nota")
embedder = Column(Vector(768), nullable=True, comment="Vectores de embedder de tamaño 768") # ✅ ahora pgvector
tags = Column(JSON, nullable=True, default=None, comment="Tags en formato JSON")
relaciones = Column(JSON, nullable=True, default=None, comment="Relaciones en formato JSON")
propiedades = Column(JSON, nullable=True, default=None, comment="Propiedades adicionales en formato JSON")
# ------------------
class notaMapper(Mapper_base[notaDom, notaModel]):
emb = NomicEmbedder.get_instance()
@staticmethod
def to_model(obj: notaDom) -> notaModel:
# Auto-generate embedding if not provided
if obj.embedder is None:
obj.embedder = notaMapper.emb.embed(obj.contenido)
return notaModel(
titulo=obj.titulo,
contenido=obj.contenido,
embedder=obj.embedder,
tags=obj.tags,
relaciones=obj.relaciones,
propiedades=obj.propiedades,
)
@staticmethod
def from_model(model: notaModel) -> notaDom:
return notaDom(
titulo=model.titulo,
contenido=model.contenido,
embedder=model.embedder,
tags=model.tags,
relaciones=model.relaciones,
propiedades=model.propiedades,
)
@staticmethod
def from_dict(d: dict) -> notaDom:
# Auto-generate embedding if missing
emb = d.get("embedder")
if emb is None and "contenido" in d:
emb = notaMapper.emb.embed(d["contenido"])
return notaDom(
titulo=d["titulo"],
contenido=d["contenido"],
embedder=emb,
tags=d.get("tags"),
relaciones=d.get("relaciones"),
propiedades=d.get("propiedades"),
)
# -----------------
class notaRepo(Repo_base[notaModel, notaDom]):
def __init__(self, session: Session):
super().__init__(session=session, modelo=notaModel, mapper=notaMapper)
def update_contenido(self, nota: notaModel, nuevo_contenido: str) -> notaDom:
emb = notaMapper.emb.embed(nuevo_contenido)
nota.contenido = nuevo_contenido
nota.embedder = emb
self.session.commit()
self.session.refresh(nota)
return self.Mapper.from_model(nota)
def get_by_titulo(self, titulo: str) -> Optional[notaDom]:
model = (
self.session.query(self.Modelo)
.filter_by(titulo=titulo, sys_deleted_at=None)
.first()
)
return self.Mapper.from_model(model) if model else None
def buscar_por_contenido(self, contenido: str) -> list[notaDom]:
models = (
self.session.query(self.Modelo)
.filter(
self.Modelo.contenido.ilike(f"%{contenido}%"),
self.Modelo.sys_deleted_at.is_(None),
)
.all()
)
return self.Mapper.from_model_list(models)
def get_paginated_by_tags(self, tags: dict, offset: int = 0, limit: int = 10) -> list[notaDom]:
models = (
self.session.query(self.Modelo)
.filter(
self.Modelo.tags.contains(tags),
self.Modelo.sys_deleted_at.is_(None),
)
.offset(offset)
.limit(limit)
.all()
)
return self.Mapper.from_model_list(models)