Why: - POST /v1/files was reporting chunks_indexed=0/points_created=0 unconditionally — chunks were parsed and embedded but never written to Qdrant, so nothing was actually searchable after upload. Changes: - upload_source_file() now calls index_chunks() after embedding, inside the same INGESTION_TIMEOUT_SECONDS window, and marks the job failed (error_code=index_failed, 502) if it raises. - Job counters (points_created, points_soft_deleted) and the response's chunks_indexed now reflect the real indexing result instead of a hardcoded zero. - Wired PointStorage through AppResources/lifespan/the files router. Impact: - A successful upload is now searchable in Qdrant by the time 201 returns.
95 lines
3.6 KiB
Python
95 lines
3.6 KiB
Python
"""`POST /v1/files`, `GET /v1/files/{file_id}` (ADR-0008).
|
|
|
|
Routes adapt HTTP to `application/files` calls; they do not parse, hash,
|
|
touch MinIO/Qdrant, or otherwise carry ingestion business logic (ADR-0015).
|
|
"""
|
|
|
|
import uuid
|
|
from collections.abc import Sequence
|
|
from typing import Annotated
|
|
|
|
from anyio import CapacityLimiter, Semaphore
|
|
from fastapi import APIRouter, Depends, Form, HTTPException, Response, UploadFile, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
|
from src.api.dependencies.auth import require_scope
|
|
from src.api.schemas.files import FileStatusResponse, FileUploadResponse
|
|
from src.application.auth.context import AuthContext
|
|
from src.application.files.status import get_file_status
|
|
from src.application.files.upload import upload_source_file
|
|
from src.application.ports.embedding import DenseEmbedder, SparseEmbedder
|
|
from src.application.ports.object_storage import ObjectStorage
|
|
from src.application.ports.point_storage import PointStorage
|
|
from src.bootstrap.dependencies import (
|
|
get_dense_embedders,
|
|
get_ingestion_concurrency_limiter,
|
|
get_ingestion_limiter,
|
|
get_object_storage,
|
|
get_point_storage,
|
|
get_sessionmaker,
|
|
get_settings,
|
|
get_sparse_embedder,
|
|
)
|
|
from src.config import Settings
|
|
|
|
router = APIRouter(prefix="/files", tags=["files"])
|
|
|
|
_RequireFilesWrite = Annotated[AuthContext, Depends(require_scope("files:write"))]
|
|
_SessionmakerDep = Annotated[async_sessionmaker[AsyncSession], Depends(get_sessionmaker)]
|
|
_ObjectStorageDep = Annotated[ObjectStorage, Depends(get_object_storage)]
|
|
_PointStorageDep = Annotated[PointStorage, Depends(get_point_storage)]
|
|
_SettingsDep = Annotated[Settings, Depends(get_settings)]
|
|
_IngestionLimiterDep = Annotated[CapacityLimiter, Depends(get_ingestion_limiter)]
|
|
_ConcurrencyLimiterDep = Annotated[Semaphore, Depends(get_ingestion_concurrency_limiter)]
|
|
_DenseEmbeddersDep = Annotated[Sequence[DenseEmbedder], Depends(get_dense_embedders)]
|
|
_SparseEmbedderDep = Annotated[SparseEmbedder, Depends(get_sparse_embedder)]
|
|
|
|
|
|
@router.post("", status_code=status.HTTP_201_CREATED)
|
|
async def upload_file(
|
|
response: Response,
|
|
file: UploadFile,
|
|
domain: Annotated[str, Form()],
|
|
auth: _RequireFilesWrite,
|
|
sessionmaker: _SessionmakerDep,
|
|
storage: _ObjectStorageDep,
|
|
point_storage: _PointStorageDep,
|
|
settings: _SettingsDep,
|
|
limiter: _IngestionLimiterDep,
|
|
concurrency_limiter: _ConcurrencyLimiterDep,
|
|
dense_embedders: _DenseEmbeddersDep,
|
|
sparse_embedder: _SparseEmbedderDep,
|
|
) -> FileUploadResponse:
|
|
data = await file.read()
|
|
result = await upload_source_file(
|
|
sessionmaker=sessionmaker,
|
|
storage=storage,
|
|
point_storage=point_storage,
|
|
auth=auth,
|
|
domain=domain,
|
|
filename=file.filename or "",
|
|
data=data,
|
|
ingestion_settings=settings.ingestion,
|
|
chunking_settings=settings.chunking,
|
|
qdrant_settings=settings.qdrant,
|
|
thread_limiter=limiter,
|
|
concurrency_limiter=concurrency_limiter,
|
|
dense_embedders=dense_embedders,
|
|
sparse_embedder=sparse_embedder,
|
|
)
|
|
if not result.is_new_attempt:
|
|
response.status_code = status.HTTP_200_OK
|
|
return FileUploadResponse.from_result(result)
|
|
|
|
|
|
@router.get("/{file_id}")
|
|
async def get_file(
|
|
file_id: uuid.UUID,
|
|
auth: _RequireFilesWrite,
|
|
sessionmaker: _SessionmakerDep,
|
|
) -> FileStatusResponse:
|
|
result = await get_file_status(sessionmaker, tenant_id=auth.tenant_id, source_file_id=file_id)
|
|
if result is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="file not found")
|
|
return FileStatusResponse.from_result(result)
|