fix document api

This commit is contained in:
phernandez
2024-12-21 22:53:55 -06:00
parent 1db97b59a4
commit f4bc0cabd9
4 changed files with 229 additions and 86 deletions
+32 -41
View File
@@ -22,7 +22,7 @@ async def create_document(
service: DocumentServiceDep,
) -> DocumentCreateResponse:
"""Create a new document.
The document will be created with appropriate frontmatter including:
- Generated ID
- Creation timestamp
@@ -35,18 +35,18 @@ async def create_document(
content=doc.content,
metadata=doc.doc_metadata,
)
return DocumentCreateResponse.from_orm(document)
return DocumentCreateResponse.model_validate(document.__dict__)
except DocumentWriteError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/", response_model=List[DocumentResponse])
@router.get("/", response_model=List[DocumentCreateResponse])
async def list_documents(
service: DocumentServiceDep,
) -> List[DocumentResponse]:
"""List all documents."""
) -> List[DocumentCreateResponse]:
"""List all documents (without content)."""
documents = await service.list_documents()
return [DocumentResponse.from_orm(doc) for doc in documents]
return [DocumentCreateResponse.model_validate(doc.__dict__) for doc in documents]
@router.get("/{path:path}", response_model=DocumentResponse)
@@ -57,57 +57,45 @@ async def get_document(
"""Get a document by path."""
try:
document, content = await service.read_document(path)
response = DocumentResponse.model_validate(document.__dict__ | {"content": content})
doc_dict = document.__dict__ | {"content": content}
response = DocumentResponse.model_validate(doc_dict)
return response
except DocumentNotFoundError:
raise HTTPException(status_code=404, detail=f"Document not found: {path}")
raise HTTPException(
status_code=404,
detail=f"Document not found: {path}"
)
except DocumentWriteError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/{path:path}", response_model=DocumentResponse)
@router.put("/{id:int}", response_model=DocumentResponse)
async def update_document(
path: str,
id: int,
doc: DocumentUpdate,
service: DocumentServiceDep,
) -> DocumentResponse:
"""Update a document's content and/or metadata."""
"""Update a document by ID."""
# Verify IDs match
if doc.id != id:
raise HTTPException(
status_code=400,
detail="Document ID in URL must match ID in request body"
)
try:
document = await service.update_document(
path=path,
document = await service.update_document_by_id(
id=id,
content=doc.content,
metadata=doc.doc_metadata,
)
return DocumentResponse.from_orm(document)
doc_dict = document.__dict__ | {"content": doc.content}
return DocumentResponse.model_validate(doc_dict)
except DocumentNotFoundError:
raise HTTPException(status_code=404, detail=f"Document not found: {path}")
except DocumentWriteError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.patch("/{path:path}", response_model=DocumentResponse)
async def patch_document(
path: str,
patch: DocumentPatch,
service: DocumentServiceDep,
) -> DocumentResponse:
"""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."),
status_code=404,
detail=f"Document not found: {id}"
)
try:
document = await service.update_document(
path=path,
content=patch.content,
metadata=patch.doc_metadata,
)
return DocumentResponse.from_orm(document)
except DocumentNotFoundError:
raise HTTPException(status_code=404, detail=f"Document not found: {path}")
except DocumentWriteError as e:
raise HTTPException(status_code=400, detail=str(e))
@@ -121,6 +109,9 @@ async def delete_document(
try:
await service.delete_document(path)
except DocumentNotFoundError:
raise HTTPException(status_code=404, detail=f"Document not found: {path}")
raise HTTPException(
status_code=404,
detail=f"Document not found: {path}"
)
except DocumentWriteError as e:
raise HTTPException(status_code=400, detail=str(e))
+4 -16
View File
@@ -1,16 +1,4 @@
"""Request schemas for interacting with the knowledge graph.
This module defines all the request schemas for creating, retrieving, and managing
knowledge graph entities. Each schema includes validation rules and clear examples
of proper usage.
Request Types:
1. Entity Creation - Create new nodes in the graph
2. Observation Addition - Add facts to existing entities
3. Relation Creation - Connect entities with typed edges
4. Node Search - Find entities across the graph
5. Node Retrieval - Load specific entities by ID
"""
"""Request schemas for interacting with the knowledge graph."""
from typing import List, Optional, Annotated, Dict, Any
@@ -220,14 +208,14 @@ class DocumentCreate(BaseModel):
class DocumentUpdate(BaseModel):
id: int
checksum: str
"""Update an existing document by ID."""
id: int # Document ID is required for updates
content: str
doc_metadata: Optional[Dict[str, Any]] = None
class DocumentPatch(BaseModel):
id: int
checksum: str
content: Optional[str] = None
doc_metadata: Optional[Dict[str, Any]] = None
+55 -27
View File
@@ -3,11 +3,11 @@
import hashlib
from datetime import datetime, UTC
from pathlib import Path
from typing import Optional, Dict, Any, Sequence
from typing import Optional, Dict, Any, List
import yaml
from icecream import ic
from loguru import logger
from sqlalchemy import select
from basic_memory.models import Document
from basic_memory.repository.document_repository import DocumentRepository
@@ -16,19 +16,16 @@ from basic_memory.services.service import BaseService
class DocumentError(Exception):
"""Base exception for document operations."""
pass
class DocumentNotFoundError(DocumentError):
"""Raised when a document doesn't exist."""
pass
class DocumentWriteError(DocumentError):
"""Raised when document file operations fail."""
pass
@@ -49,41 +46,37 @@ class DocumentService(BaseService[DocumentRepository]):
async def ensure_parent_directory(self, path: Path) -> None:
"""
Ensure parent directory exists and is writable.
Ensure parent directory exists.
Args:
path: Path to check
Raises:
DocumentWriteError: If directory cannot be created or is not writable
DocumentWriteError: If directory cannot be created
"""
parent = path.parent
try:
if not parent.exists():
parent.mkdir(parents=True)
# Verify we can write to it
test_file = parent / ".write_test"
test_file.touch()
test_file.unlink()
parent.mkdir(parents=True, exist_ok=True)
except Exception as e:
raise DocumentWriteError(f"Directory not writable: {parent}: {e}")
raise DocumentWriteError(f"Failed to create directory: {parent}: {e}")
async def add_frontmatter(
self, content: str, doc_id: int, metadata: Optional[Dict[str, Any]] = None
) -> str:
async def add_frontmatter(self, content: str, doc_id: int, metadata: Optional[Dict[str, Any]] = None) -> str:
"""Add frontmatter to document content."""
# Generate frontmatter with timestamps
now = datetime.now(UTC).isoformat()
frontmatter = {"id": doc_id, "created": now, "modified": now}
frontmatter = {
"id": doc_id,
"created": now,
"modified": now
}
if metadata:
frontmatter.update(metadata)
yaml_fm = yaml.dump(frontmatter, sort_keys=False)
return f"---\n{yaml_fm}---\n\n{content}"
async def list_documents(self) -> Sequence[Document]:
"""List all documents in the database."""
async def list_documents(self) -> List[Document]:
"""List all documents."""
return await self.repository.find_all()
async def create_document(
@@ -105,13 +98,16 @@ class DocumentService(BaseService[DocumentRepository]):
"""
logger.debug(f"Creating document at {path}")
# Ensure parent directories exist and are writable
# Ensure parent directories exist
file_path = Path(path)
await self.ensure_parent_directory(file_path)
try:
# 1. Create initial DB record to get ID
doc = await self.repository.create({"path": str(path), "doc_metadata": metadata})
doc = await self.repository.create({
"path": str(path),
"doc_metadata": metadata
})
# 2. Add frontmatter with DB-generated ID
content_with_frontmatter = await self.add_frontmatter(content, doc.id, metadata)
@@ -122,12 +118,11 @@ class DocumentService(BaseService[DocumentRepository]):
# 4. Update DB with checksum to mark completion
checksum = await self.compute_checksum(content_with_frontmatter)
doc = await self.repository.update(doc.id, {"checksum": checksum})
ic(doc)
return doc
except Exception as e:
# Clean up on any failure
if "doc" in locals(): # DB record was created
if 'doc' in locals(): # DB record was created
await self.repository.delete(doc.id)
file_path.unlink(missing_ok=True)
raise DocumentWriteError(f"Failed to create document: {e}")
@@ -167,6 +162,39 @@ class DocumentService(BaseService[DocumentRepository]):
return doc, content
async def update_document_by_id(
self, id: int, content: str, metadata: Optional[Dict[str, Any]] = None
) -> Document:
"""
Update a document using its ID.
Args:
id: Document ID
content: New content
metadata: Optional new metadata
Returns:
Updated document record
Raises:
DocumentNotFoundError: If document doesn't exist
DocumentWriteError: If update fails
"""
logger.debug(f"Updating document with ID: {id}")
# Find document first
query = select(Document).where(Document.id == id)
document = await self.repository.find_one(query)
if not document:
raise DocumentNotFoundError(f"Document not found: {id}")
# Use existing path to update file
return await self.update_document(
path=document.path,
content=content,
metadata=metadata
)
async def update_document(
self, path: str, content: str, metadata: Optional[Dict[str, Any]] = None
) -> Document:
@@ -244,4 +272,4 @@ class DocumentService(BaseService[DocumentRepository]):
# Delete database record if it exists
doc = await self.repository.find_by_path(str(path))
if doc:
await self.repository.delete(doc.id)
await self.repository.delete(doc.id)