mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
add documents api
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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))
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user