From ddf2d2fd0e985a20d68a007669a6d3ca015a0043 Mon Sep 17 00:00:00 2001 From: phernandez Date: Sat, 21 Dec 2024 19:56:54 -0600 Subject: [PATCH] add metadata and errors to /documents api --- src/basic_memory/api/routers/documents.py | 42 ++++++------- src/basic_memory/models/knowledge.py | 72 +++++++---------------- 2 files changed, 44 insertions(+), 70 deletions(-) diff --git a/src/basic_memory/api/routers/documents.py b/src/basic_memory/api/routers/documents.py index b0ec191c..e2f1493c 100644 --- a/src/basic_memory/api/routers/documents.py +++ b/src/basic_memory/api/routers/documents.py @@ -16,19 +16,26 @@ from basic_memory.services.document_service import ( router = APIRouter(prefix="/documents", tags=["documents"]) -@router.post("/", response_model=DocumentResponse) +@router.post("/", response_model=DocumentResponse, status_code=201) async def create_document( doc: DocumentCreate, service: DocumentServiceDep, ) -> DocumentResponse: - """Create a new document.""" + """Create a new document. + + The document will be created with appropriate frontmatter including: + - Generated ID + - Creation timestamp + - Last modified timestamp + - Any provided metadata + """ try: document = await service.create_document( path=doc.path, content=doc.content, metadata=doc.metadata, ) - return document + return DocumentResponse.from_orm(document) except DocumentWriteError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -38,7 +45,8 @@ async def list_documents( service: DocumentServiceDep, ) -> List[DocumentResponse]: """List all documents.""" - return await service.list_documents() + documents = await service.list_documents() + return [DocumentResponse.from_orm(doc) for doc in documents] @router.get("/{path:path}", response_model=DocumentResponse) @@ -49,12 +57,11 @@ async def get_document( """Get a document by path.""" try: document, content = await service.read_document(path) - # Attach content to response response = DocumentResponse.from_orm(document) - response.content = content # type: ignore + response.content = content return response except DocumentNotFoundError: - raise HTTPException(status_code=404, detail="Document not found") + raise HTTPException(status_code=404, detail=f"Document not found: {path}") except DocumentWriteError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -72,9 +79,9 @@ async def update_document( content=doc.content, metadata=doc.metadata, ) - return document + return DocumentResponse.model_validate(document) except DocumentNotFoundError: - raise HTTPException(status_code=404, detail="Document not found") + raise HTTPException(status_code=404, detail=f"Document not found: {path}") except DocumentWriteError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -85,17 +92,12 @@ async def patch_document( patch: DocumentPatch, service: DocumentServiceDep, ) -> DocumentResponse: - """ - Partially update a document. - - TODO: Implement partial content updates to minimize data transfer. - For now, this is stubbed to require full content on update. - """ - # For now, require full content updates + """Partially update a document.""" + # Require full content updates for now if patch.content is None: raise HTTPException( status_code=400, - detail="Partial content updates not yet implemented. Please provide full content.", + detail=("Partial content updates not yet implemented. " "Please provide full content."), ) try: @@ -104,9 +106,9 @@ async def patch_document( content=patch.content, metadata=patch.metadata, ) - return document + return DocumentResponse.from_orm(document) except DocumentNotFoundError: - raise HTTPException(status_code=404, detail="Document not found") + raise HTTPException(status_code=404, detail=f"Document not found: {path}") except DocumentWriteError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -120,6 +122,6 @@ async def delete_document( try: await service.delete_document(path) except DocumentNotFoundError: - raise HTTPException(status_code=404, detail="Document not found") + raise HTTPException(status_code=404, detail=f"Document not found: {path}") except DocumentWriteError as e: raise HTTPException(status_code=400, detail=str(e)) diff --git a/src/basic_memory/models/knowledge.py b/src/basic_memory/models/knowledge.py index 4452ddb7..6466c885 100644 --- a/src/basic_memory/models/knowledge.py +++ b/src/basic_memory/models/knowledge.py @@ -3,10 +3,7 @@ from datetime import datetime from typing import List, Optional -from sqlalchemy import ( - String, DateTime, ForeignKey, Text, Integer, - text, UniqueConstraint -) +from sqlalchemy import String, DateTime, ForeignKey, Text, Integer, text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from basic_memory.models.base import Base @@ -24,53 +21,42 @@ class Entity(Base): - A description (optional) - A list of observations """ + __tablename__ = "entity" - __table_args__ = ( - UniqueConstraint("entity_type", "name", name="uix_entity_type_name"), - ) + __table_args__ = (UniqueConstraint("entity_type", "name", name="uix_entity_type_name"),) id: Mapped[str] = mapped_column(String, primary_key=True) name: Mapped[str] = mapped_column(String) entity_type: Mapped[str] = mapped_column(String) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) - created_at: Mapped[datetime] = mapped_column( - DateTime, - server_default=text("CURRENT_TIMESTAMP") - ) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP")) updated_at: Mapped[datetime] = mapped_column( - DateTime, - server_default=text("CURRENT_TIMESTAMP"), - onupdate=text("CURRENT_TIMESTAMP") + DateTime, server_default=text("CURRENT_TIMESTAMP"), onupdate=text("CURRENT_TIMESTAMP") ) # Link to source document doc_id: Mapped[Optional[int]] = mapped_column( - Integer, - ForeignKey("documents.id", ondelete="SET NULL"), - nullable=True + Integer, ForeignKey("documents.id", ondelete="SET NULL"), nullable=True ) # Relationships - document: Mapped["Document"] = relationship( - "Document", - back_populates="entities" + document: Mapped["Document"] = relationship( # pyright: ignore [reportUndefinedVariable] # noqa: F821 + "Document", back_populates="entities" ) observations: Mapped[List["Observation"]] = relationship( - "Observation", - back_populates="entity", - cascade="all, delete-orphan" + "Observation", back_populates="entity", cascade="all, delete-orphan" ) outgoing_relations: Mapped[List["Relation"]] = relationship( "Relation", foreign_keys="[Relation.from_id]", back_populates="from_entity", - cascade="all, delete-orphan" + cascade="all, delete-orphan", ) incoming_relations: Mapped[List["Relation"]] = relationship( "Relation", foreign_keys="[Relation.to_id]", back_populates="to_entity", - cascade="all, delete-orphan" + cascade="all, delete-orphan", ) @property @@ -100,19 +86,15 @@ class Observation(Base): - Can be added or removed independently - Should be atomic (one fact per observation) """ + __tablename__ = "observation" id: Mapped[int] = mapped_column(Integer, primary_key=True) entity_id: Mapped[str] = mapped_column( - String, - ForeignKey("entity.id", ondelete="CASCADE"), - index=True + String, ForeignKey("entity.id", ondelete="CASCADE"), index=True ) content: Mapped[str] = mapped_column(String) - created_at: Mapped[datetime] = mapped_column( - DateTime, - server_default=text("CURRENT_TIMESTAMP") - ) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP")) context: Mapped[Optional[str]] = mapped_column(String, nullable=True) # Relationships @@ -126,40 +108,30 @@ class Observation(Base): class Relation(Base): """ Relations define directed connections between entities. - They are always stored in active voice and describe how entities + They are always stored in active voice and describe how entities interact or relate to each other. """ + __tablename__ = "relation" id: Mapped[int] = mapped_column(Integer, primary_key=True) from_id: Mapped[str] = mapped_column( - String, - ForeignKey("entity.id", ondelete="CASCADE"), - index=True + String, ForeignKey("entity.id", ondelete="CASCADE"), index=True ) to_id: Mapped[str] = mapped_column( - String, - ForeignKey("entity.id", ondelete="CASCADE"), - index=True + String, ForeignKey("entity.id", ondelete="CASCADE"), index=True ) relation_type: Mapped[str] = mapped_column(String) - created_at: Mapped[datetime] = mapped_column( - DateTime, - server_default=text("CURRENT_TIMESTAMP") - ) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP")) context: Mapped[Optional[str]] = mapped_column(String, nullable=True) # Relationships from_entity: Mapped[Entity] = relationship( - "Entity", - foreign_keys=[from_id], - back_populates="outgoing_relations" + "Entity", foreign_keys=[from_id], back_populates="outgoing_relations" ) to_entity: Mapped[Entity] = relationship( - "Entity", - foreign_keys=[to_id], - back_populates="incoming_relations" + "Entity", foreign_keys=[to_id], back_populates="incoming_relations" ) def __repr__(self) -> str: - return f"Relation(id={self.id}, from='{self.from_id}', type='{self.relation_type}', to='{self.to_id}')" \ No newline at end of file + return f"Relation(id={self.id}, from='{self.from_id}', type='{self.relation_type}', to='{self.to_id}')"