duyduc
Fix HF retrieval source dependency closure
14cd803
Raw
History Blame Contribute Delete
9.09 kB
from typing import Annotated
from fastapi import APIRouter, Depends, File, Form, Path, Query, Response, UploadFile
from fastapi.responses import JSONResponse
from src.api.dependencies import DbSessionDep, GuestSessionHeaderDep
from src.api.errors import error_response
from src.config import Settings, get_settings
from src.models.auth import AuthorityLevel, SourceDocumentStatus, SourceScope
from src.models.schemas import (
DocumentListResponse,
DocumentType,
DocumentUploadResult,
EffectiveStatus,
IngestionStatus,
Purpose,
RetentionPolicy,
ScopedDocumentItem,
ScopedDocumentListResponse,
ScopedDocumentUploadResult,
)
from src.services.documents import (
DocumentLibraryService,
DocumentListFilters,
DocumentServiceError,
DocumentUploadInput,
InMemoryDocumentIngestionService,
ScopedUploadContext,
build_document_metadata,
document_ingestion_service,
parse_upload_metadata,
)
router = APIRouter(prefix="/documents", tags=["documents"])
SettingsDep = Annotated[Settings, Depends(get_settings)]
def get_document_service() -> InMemoryDocumentIngestionService:
return document_ingestion_service
@router.post("", response_model=DocumentUploadResult)
async def upload_document(
file: Annotated[UploadFile, File()],
settings: SettingsDep,
service: Annotated[InMemoryDocumentIngestionService, Depends(get_document_service)],
document_type: Annotated[DocumentType, Form()],
purpose: Annotated[Purpose, Form()],
retention_policy: Annotated[RetentionPolicy, Form()],
title: Annotated[str | None, Form(max_length=500)] = None,
metadata: Annotated[str | None, Form()] = None,
) -> DocumentUploadResult | JSONResponse:
"""Upload a document and queue it for ingestion."""
try:
raw_metadata = parse_upload_metadata(metadata)
document_metadata = build_document_metadata(raw_metadata, title)
record = service.upload_document(
DocumentUploadInput(
filename=file.filename or "",
content=await file.read(),
document_type=document_type,
purpose=purpose,
retention_policy=retention_policy,
title=title,
metadata=document_metadata,
),
max_upload_bytes=settings.max_upload_bytes,
)
except DocumentServiceError as exc:
return error_response(
status_code=exc.status_code,
code=exc.code,
message=exc.message,
details=exc.details,
)
return DocumentUploadResult(
document_id=record.document_id,
filename=record.filename,
document_type=record.document_type,
purpose=record.purpose,
retention_policy=record.retention_policy,
ingestion_status=record.ingestion_status,
metadata=record.metadata,
created_at=record.created_at,
)
@router.get("", response_model=DocumentListResponse)
def list_documents(
service: Annotated[InMemoryDocumentIngestionService, Depends(get_document_service)],
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=100)] = 20,
document_type: Annotated[DocumentType | None, Query()] = None,
purpose: Annotated[Purpose | None, Query()] = None,
ingestion_status: Annotated[IngestionStatus | None, Query()] = None,
effective_status: Annotated[EffectiveStatus | None, Query()] = None,
q: Annotated[str | None, Query(max_length=200)] = None,
) -> DocumentListResponse | JSONResponse:
"""List uploaded or indexed documents."""
try:
items, total = service.list_documents(
DocumentListFilters(
page=page,
page_size=page_size,
document_type=document_type,
purpose=purpose,
ingestion_status=ingestion_status,
effective_status=effective_status,
q=q,
)
)
except DocumentServiceError as exc:
return error_response(
status_code=exc.status_code,
code=exc.code,
message=exc.message,
details=exc.details,
)
return DocumentListResponse(
items=items,
page=page,
page_size=page_size,
total=total,
)
@router.post("/guest/upload", response_model=ScopedDocumentUploadResult)
async def upload_guest_document(
file: Annotated[UploadFile, File()],
response: Response,
settings: SettingsDep,
db: DbSessionDep,
guest_session_id: GuestSessionHeaderDep = None,
document_type: Annotated[DocumentType, Form()] = DocumentType.OTHER,
purpose: Annotated[Purpose, Form()] = Purpose.DRAFT_CONTEXT,
title: Annotated[str | None, Form(max_length=500)] = None,
metadata: Annotated[str | None, Form()] = None,
) -> ScopedDocumentUploadResult | JSONResponse:
service = DocumentLibraryService(db, settings)
session_id = service.ensure_guest_session_id(guest_session_id)
response.headers["X-Guest-Session-Id"] = session_id
response.set_cookie(
settings.guest_session_cookie_name,
session_id,
max_age=settings.guest_upload_ttl_hours * 3600,
httponly=True,
secure=settings.auth_cookie_secure,
samesite="lax",
)
return await _upload_scoped(
file=file,
service=service,
document_type=document_type,
purpose=purpose,
retention_policy=RetentionPolicy.SESSION,
title=title,
metadata=metadata,
context=ScopedUploadContext(
source_scope=SourceScope.GUEST_SESSION,
session_id=session_id,
authority_level=AuthorityLevel.UPLOADED_CONTEXT,
document_status=_document_status_from_metadata(metadata),
),
)
@router.get("/guest", response_model=ScopedDocumentListResponse)
def list_guest_documents(
db: DbSessionDep,
settings: SettingsDep,
guest_session_id: GuestSessionHeaderDep = None,
) -> ScopedDocumentListResponse:
service = DocumentLibraryService(db, settings)
session_id = service.ensure_guest_session_id(guest_session_id)
items = service.list_documents(
ScopedUploadContext(source_scope=SourceScope.GUEST_SESSION, session_id=session_id)
)
return ScopedDocumentListResponse(
items=[service.to_item(item) for item in items],
total=len(items),
)
@router.delete("/guest/{document_id}", response_model=ScopedDocumentItem)
def delete_guest_document(
document_id: Annotated[str, Path()],
db: DbSessionDep,
settings: SettingsDep,
guest_session_id: GuestSessionHeaderDep = None,
) -> ScopedDocumentItem | JSONResponse:
try:
service = DocumentLibraryService(db, settings)
record = service.delete_document(
document_id,
ScopedUploadContext(
source_scope=SourceScope.GUEST_SESSION,
session_id=service.ensure_guest_session_id(guest_session_id),
),
)
except DocumentServiceError as exc:
return _document_error(exc)
return service.to_item(record)
async def _upload_scoped(
*,
file: UploadFile,
service: DocumentLibraryService,
document_type: DocumentType,
purpose: Purpose,
retention_policy: RetentionPolicy,
title: str | None,
metadata: str | None,
context: ScopedUploadContext,
) -> ScopedDocumentUploadResult | JSONResponse:
try:
raw_metadata = parse_upload_metadata(metadata)
document_metadata = build_document_metadata(raw_metadata, title)
record, chunk_count, warnings = service.upload_scoped_document(
DocumentUploadInput(
filename=file.filename or "",
content=await file.read(),
document_type=document_type,
purpose=purpose,
retention_policy=retention_policy,
title=title,
metadata=document_metadata,
),
context=context,
mime_type=file.content_type or "",
)
except DocumentServiceError as exc:
return _document_error(exc)
return ScopedDocumentUploadResult(
document=service.to_item(record),
chunk_count=chunk_count,
warnings=warnings,
)
def _document_status_from_metadata(metadata: str | None) -> SourceDocumentStatus:
try:
raw = parse_upload_metadata(metadata)
except DocumentServiceError:
return SourceDocumentStatus.UNKNOWN
value = str(raw.get("document_status") or raw.get("effective_status") or "active")
if value in {"repealed", "expired", "amended"}:
return SourceDocumentStatus.OUTDATED
try:
return SourceDocumentStatus(value)
except ValueError:
return SourceDocumentStatus.UNKNOWN
def _document_error(exc: DocumentServiceError) -> JSONResponse:
return error_response(
status_code=exc.status_code,
code=exc.code,
message=exc.message,
details=exc.details,
)