frontend añadido y backend de creacion de notas
This commit is contained in:
@@ -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))
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1,6 @@
|
||||
# domains/arquitecture_layer/__init__.py
|
||||
from .Repo import *
|
||||
from .Mapper import *
|
||||
from .Model import *
|
||||
|
||||
__all__ = ["Repo", "Mapper", "Model"]
|
||||
@@ -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
@@ -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)
|
||||
Reference in New Issue
Block a user