amazon-transcreation/backend/app/api/v1/files.py
DJP 98fa16bfc3 feat: complete Phase 1-2 scaffold — backend, frontend, pipeline skeleton
Full-stack Amazon AI Transcreation Platform with:
- FastAPI backend (async, PostgreSQL, Redis, Celery) with 11 DB tables
- JWT auth (SSO-ready abstract provider pattern)
- 6-agent pipeline orchestrator with deterministic modules
- Next.js 14 frontend with Amazon branding (Ember fonts, orange/dark theme)
- Job wizard, monitoring HUD, output review, admin screens
- 154 TM/reference files imported, 12 locales configured
- Docker Compose for all services

Agents 2-5 (TM retrieval, ranker, transcreator, compliance) are stubs
pending Phase 3 LLM integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 12:31:43 -04:00

179 lines
5.9 KiB
Python

from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File, status
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_current_user, get_db
from app.models.files import ReferenceFileType
from app.schemas.files import (
FileUploadResponse,
ReferenceFileResponse,
TMFileResponse,
)
from app.services.file_service import FileService
router = APIRouter(prefix="/files", tags=["files"])
file_service = FileService()
# ---- TM Files ----
@router.post("/tm", response_model=FileUploadResponse, status_code=status.HTTP_201_CREATED)
async def upload_tm_file(
client_id: UUID = Query(...),
locale_code: str = Query(...),
channel: str = Query(...),
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> FileUploadResponse:
"""Upload a Translation Memory (JSONL) file."""
if not file.filename:
raise HTTPException(status_code=400, detail="File must have a filename")
if not file_service.validate_file_extension(file.filename, [".jsonl", ".json"]):
raise HTTPException(status_code=400, detail="Only .jsonl/.json files accepted")
tm = await file_service.upload_tm_file(
db, client_id, locale_code, channel, file.file, file.filename,
uploaded_by=current_user["user_id"],
)
return FileUploadResponse(
id=tm.id,
filename=tm.filename,
file_path=tm.file_path,
message=f"Uploaded TM file with {tm.segment_count} segments",
)
@router.get("/tm", response_model=list[TMFileResponse])
async def list_tm_files(
client_id: UUID = Query(...),
locale_code: str | None = Query(None),
channel: str | None = Query(None),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> list[TMFileResponse]:
"""List TM files for a client."""
files = await file_service.list_tm_files(db, client_id, locale_code, channel)
return [TMFileResponse.model_validate(f) for f in files]
@router.get("/tm/{file_id}/download")
async def download_tm_file(
file_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> FileResponse:
"""Download a TM file."""
from sqlalchemy import select
from app.models.files import TMFileRegistry
result = await db.execute(
select(TMFileRegistry).where(TMFileRegistry.id == file_id)
)
tm = result.scalar_one_or_none()
if tm is None:
raise HTTPException(status_code=404, detail="TM file not found")
path = file_service.get_file_path(tm.file_path)
if path is None:
raise HTTPException(status_code=404, detail="File not found on disk")
return FileResponse(path=str(path), filename=tm.filename)
@router.delete("/tm/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_tm_file(
file_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> None:
"""Delete a TM file."""
deleted = await file_service.delete_tm_file(db, file_id)
if not deleted:
raise HTTPException(status_code=404, detail="TM file not found")
# ---- Reference Files ----
@router.post(
"/reference",
response_model=FileUploadResponse,
status_code=status.HTTP_201_CREATED,
)
async def upload_reference_file(
client_id: UUID = Query(...),
file_type: ReferenceFileType = Query(...),
locale_scope: str = Query(...),
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> FileUploadResponse:
"""Upload a reference file (glossary, blacklist, TOV, etc.)."""
if not file.filename:
raise HTTPException(status_code=400, detail="File must have a filename")
ref = await file_service.upload_reference_file(
db, client_id, file_type, locale_scope, file.file, file.filename,
uploaded_by=current_user["user_id"],
)
return FileUploadResponse(
id=ref.id,
filename=ref.filename,
file_path=ref.file_path,
message=f"Uploaded {file_type.value} reference file",
)
@router.get("/reference", response_model=list[ReferenceFileResponse])
async def list_reference_files(
client_id: UUID = Query(...),
file_type: ReferenceFileType | None = Query(None),
locale_scope: str | None = Query(None),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> list[ReferenceFileResponse]:
"""List reference files for a client."""
files = await file_service.list_reference_files(
db, client_id, file_type, locale_scope
)
return [ReferenceFileResponse.model_validate(f) for f in files]
@router.get("/reference/{file_id}/download")
async def download_reference_file(
file_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> FileResponse:
"""Download a reference file."""
from sqlalchemy import select
from app.models.files import ReferenceFile
result = await db.execute(
select(ReferenceFile).where(ReferenceFile.id == file_id)
)
ref = result.scalar_one_or_none()
if ref is None:
raise HTTPException(status_code=404, detail="Reference file not found")
path = file_service.get_file_path(ref.file_path)
if path is None:
raise HTTPException(status_code=404, detail="File not found on disk")
return FileResponse(path=str(path), filename=ref.filename)
@router.delete("/reference/{file_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_reference_file(
file_id: UUID,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
) -> None:
"""Delete a reference file."""
deleted = await file_service.delete_reference_file(db, file_id)
if not deleted:
raise HTTPException(status_code=404, detail="Reference file not found")