add documents api

This commit is contained in:
phernandez
2024-12-21 19:06:16 -06:00
parent 27242d1a52
commit 0e654c750e
5 changed files with 197 additions and 6 deletions
+2
View File
@@ -3,6 +3,7 @@
from fastapi import FastAPI
from loguru import logger
from .routers import documents
from .routers import knowledge
# Initialize FastAPI app
@@ -12,6 +13,7 @@ app = FastAPI(
# Include routers
app.include_router(knowledge.router)
app.include_router(documents.router)
# Add startup event
+125
View File
@@ -0,0 +1,125 @@
"""Router for document management endpoints."""
from typing import List
from fastapi import APIRouter, HTTPException
from basic_memory.deps import DocumentServiceDep
from basic_memory.schemas.request import DocumentCreate, DocumentUpdate, DocumentPatch
from basic_memory.schemas.response import DocumentResponse
from basic_memory.services.document_service import (
DocumentNotFoundError,
DocumentWriteError,
)
# Router
router = APIRouter(prefix="/documents", tags=["documents"])
@router.post("/", response_model=DocumentResponse)
async def create_document(
doc: DocumentCreate,
service: DocumentServiceDep,
) -> DocumentResponse:
"""Create a new document."""
try:
document = await service.create_document(
path=doc.path,
content=doc.content,
metadata=doc.metadata,
)
return document
except DocumentWriteError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/", response_model=List[DocumentResponse])
async def list_documents(
service: DocumentServiceDep,
) -> List[DocumentResponse]:
"""List all documents."""
return await service.list_documents()
@router.get("/{path:path}", response_model=DocumentResponse)
async def get_document(
path: str,
service: DocumentServiceDep,
) -> DocumentResponse:
"""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
return response
except DocumentNotFoundError:
raise HTTPException(status_code=404, detail="Document not found")
except DocumentWriteError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/{path:path}", response_model=DocumentResponse)
async def update_document(
path: str,
doc: DocumentUpdate,
service: DocumentServiceDep,
) -> DocumentResponse:
"""Update a document's content and/or metadata."""
try:
document = await service.update_document(
path=path,
content=doc.content,
metadata=doc.metadata,
)
return document
except DocumentNotFoundError:
raise HTTPException(status_code=404, detail="Document not found")
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.
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
if patch.content is None:
raise HTTPException(
status_code=400,
detail="Partial content updates not yet implemented. Please provide full content.",
)
try:
document = await service.update_document(
path=path,
content=patch.content,
metadata=patch.metadata,
)
return document
except DocumentNotFoundError:
raise HTTPException(status_code=404, detail="Document not found")
except DocumentWriteError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/{path:path}", status_code=204)
async def delete_document(
path: str,
service: DocumentServiceDep,
) -> None:
"""Delete a document."""
try:
await service.delete_document(path)
except DocumentNotFoundError:
raise HTTPException(status_code=404, detail="Document not found")
except DocumentWriteError as e:
raise HTTPException(status_code=400, detail=str(e))
+34 -1
View File
@@ -13,10 +13,19 @@ from sqlalchemy.ext.asyncio import (
from basic_memory import db
from basic_memory.config import ProjectConfig, config
from basic_memory.db import DatabaseType
from basic_memory.repository.document_repository import DocumentRepository
from basic_memory.repository.entity_repository import EntityRepository
from basic_memory.repository.observation_repository import ObservationRepository
from basic_memory.repository.relation_repository import RelationRepository
from basic_memory.services import EntityService, ObservationService, RelationService
from basic_memory.services import (
EntityService,
ObservationService,
RelationService,
DocumentService,
)
## project
def get_project_config() -> ProjectConfig:
@@ -32,6 +41,8 @@ def get_project_path(project_config: ProjectConfigDep) -> Path:
ProjectPathDep = Annotated[Path, Depends(get_project_path)]
## sqlalchemy
async def get_engine_factory(
project_path: ProjectPathDep, db_type=DatabaseType.FILESYSTEM
@@ -56,6 +67,8 @@ async def get_session_maker(engine_factory: EngineFactoryDep) -> async_sessionma
SessionMakerDep = Annotated[async_sessionmaker, Depends(get_session_maker)]
## repositories
async def get_entity_repository(
session_maker: SessionMakerDep,
@@ -87,6 +100,18 @@ async def get_relation_repository(
RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repository)]
async def get_document_repository(
session_maker: SessionMakerDep,
) -> DocumentRepository:
"""Create a DocumentRepository instance."""
return DocumentRepository(session_maker)
DocumentRepositoryDep = Annotated[DocumentRepository, Depends(get_document_repository)]
## services
async def get_entity_service(entity_repository: EntityRepositoryDep) -> EntityService:
"""Create EntityService with repository."""
return EntityService(entity_repository)
@@ -111,3 +136,11 @@ async def get_relation_service(relation_repository: RelationRepositoryDep) -> Re
RelationServiceDep = Annotated[RelationService, Depends(get_relation_service)]
async def get_document_service(document_repository: DocumentRepositoryDep) -> DocumentService:
"""Create RelationService with repository."""
return DocumentService(document_repository)
DocumentServiceDep = Annotated[DocumentService, Depends(get_relation_service)]
+23 -4
View File
@@ -12,7 +12,7 @@ Request Types:
5. Node Retrieval - Load specific entities by ID
"""
from typing import List, Optional, Annotated
from typing import List, Optional, Annotated, Dict, Any
from annotated_types import MinLen, MaxLen
from pydantic import BaseModel
@@ -112,7 +112,7 @@ class SearchNodesRequest(BaseModel):
The search looks across multiple fields:
- Entity names
- Entity types
- Descriptions
- Descriptions
- Observations
Features:
@@ -159,7 +159,7 @@ class OpenNodesRequest(BaseModel):
2. Non-existent IDs are silently skipped
3. Returns complete entity objects
4. Relations are included in response
Best Practice: Use this to explore the graph by following
relations between entities that interest you.
"""
@@ -207,4 +207,23 @@ class CreateRelationsRequest(BaseModel):
6. Use relations to build a rich, navigable knowledge graph
"""
relations: List[Relation]
relations: List[Relation]
## document
class DocumentCreate(BaseModel):
path: str
content: str
metadata: Optional[Dict[str, Any]] = None
class DocumentUpdate(BaseModel):
content: str
metadata: Optional[Dict[str, Any]] = None
class DocumentPatch(BaseModel):
content: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
+13 -1
View File
@@ -11,7 +11,7 @@ Key Features:
4. Bulk operations return all affected items
"""
from typing import List, Optional
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, ConfigDict
@@ -324,3 +324,15 @@ class DeleteObservationsResponse(SQLAlchemyModel):
"""
deleted: bool
class DocumentResponse(BaseModel):
id: int
path: str
checksum: str
doc_metadata: Optional[Dict[str, Any]] = None
created_at: str
updated_at: str
class Config:
from_attributes = True