By Sagar Shankaran, Founder of CallSphere
Handle file uploads in FastAPI for AI agent document processing and image analysis. Learn type validation, size limits, chunked uploads for large files, and async processing pipelines for uploaded content.
Key takeaways
AI agents frequently need to process user-uploaded files: PDFs for research agents, images for vision analysis, CSV files for data agents, or code files for coding assistants. FastAPI handles file uploads through Starlette's UploadFile class, which provides async file reading, automatic temp file management, and streaming for large files.
The key challenge is not just receiving the file but validating it, storing it safely, and feeding it into your AI processing pipeline efficiently.
Start with a simple upload endpoint that accepts a file alongside agent parameters:
flowchart LR
CLIENT(["Client SDK"])
GW["API Gateway<br/>auth plus rate limit"]
APP["FastAPI app<br/>handlers and DI"]
VAL["Pydantic validation"]
SVC["Service layer<br/>business logic"]
DB[(Database)]
QUEUE[(Background queue)]
OBS[(Tracing)]
CLIENT --> GW --> APP --> VAL --> SVC
SVC --> DB
SVC --> QUEUE
SVC --> OBS
SVC --> CLIENT
style GW fill:#4f46e5,stroke:#4338ca,color:#fff
style APP fill:#f59e0b,stroke:#d97706,color:#1f2937
style DB fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
from fastapi import UploadFile, File, Form, HTTPException
@router.post("/agent/upload")
async def upload_and_process(
file: UploadFile = File(...),
agent_type: str = Form(default="document"),
instructions: str = Form(default="Summarize this document"),
):
content = await file.read()
if not content:
raise HTTPException(400, "Empty file")
result = await document_agent.process(
content=content,
filename=file.filename,
instructions=instructions,
)
return {
"filename": file.filename,
"size_bytes": len(content),
"result": result,
}
Never trust client-provided file types. Validate both the extension and the actual file content:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
import magic # python-magic library
ALLOWED_TYPES = {
"application/pdf": [".pdf"],
"text/plain": [".txt", ".md", ".csv"],
"text/csv": [".csv"],
"image/png": [".png"],
"image/jpeg": [".jpg", ".jpeg"],
}
MAX_FILE_SIZE = 20 * 1024 * 1024 # 20 MB
async def validate_upload(file: UploadFile) -> bytes:
# Read content
content = await file.read()
# Check size
if len(content) > MAX_FILE_SIZE:
raise HTTPException(
413,
f"File too large. Maximum size: "
f"{MAX_FILE_SIZE // (1024*1024)}MB",
)
# Check actual MIME type using file content
detected_type = magic.from_buffer(content, mime=True)
if detected_type not in ALLOWED_TYPES:
raise HTTPException(
415,
f"Unsupported file type: {detected_type}. "
f"Allowed: {', '.join(ALLOWED_TYPES.keys())}",
)
# Verify extension matches content
ext = Path(file.filename).suffix.lower()
allowed_exts = ALLOWED_TYPES[detected_type]
if ext not in allowed_exts:
raise HTTPException(
400,
f"Extension {ext} does not match "
f"detected type {detected_type}",
)
# Reset file position for downstream processing
await file.seek(0)
return content
The python-magic library reads file headers to determine the actual type, preventing renamed malicious files from bypassing extension checks.
AI agents that compare documents or process batches need multi-file upload:
from typing import List
@router.post("/agent/batch-upload")
async def batch_upload(
files: List[UploadFile] = File(...),
instructions: str = Form(default="Compare these documents"),
):
if len(files) > 10:
raise HTTPException(400, "Maximum 10 files per batch")
processed_files = []
total_size = 0
for file in files:
content = await validate_upload(file)
total_size += len(content)
if total_size > 50 * 1024 * 1024: # 50MB total limit
raise HTTPException(
413, "Total upload size exceeds 50MB limit"
)
processed_files.append({
"filename": file.filename,
"content": content,
"size": len(content),
})
result = await document_agent.process_batch(
files=processed_files,
instructions=instructions,
)
return result
For files that need to persist beyond the request, save them to disk or object storage:
import aiofiles
from pathlib import Path
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
async def save_upload(
file: UploadFile, subdirectory: str = ""
) -> Path:
# Generate safe filename
safe_name = f"{uuid.uuid4()}{Path(file.filename).suffix}"
save_dir = UPLOAD_DIR / subdirectory
save_dir.mkdir(parents=True, exist_ok=True)
file_path = save_dir / safe_name
async with aiofiles.open(file_path, "wb") as f:
while chunk := await file.read(8192):
await f.write(chunk)
return file_path
@router.post("/agent/upload-and-store")
async def upload_store_process(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
):
await validate_upload(file)
await file.seek(0)
file_path = await save_upload(file, subdirectory="documents")
# Record in database
doc = Document(
filename=file.filename,
stored_path=str(file_path),
size_bytes=file_path.stat().st_size,
uploaded_at=datetime.utcnow(),
)
db.add(doc)
await db.flush()
return {"document_id": str(doc.id), "filename": file.filename}
Reading the file in 8KB chunks with aiofiles prevents loading the entire file into memory at once, which matters for large uploads.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
Combine file upload with background processing for a complete document agent workflow:
@router.post("/agent/analyze-document", status_code=202)
async def analyze_document(
file: UploadFile = File(...),
analysis_type: str = Form(default="summary"),
background_tasks: BackgroundTasks = None,
db: AsyncSession = Depends(get_db),
):
content = await validate_upload(file)
await file.seek(0)
# Save file
file_path = await save_upload(file, "analysis")
# Create task record
task = AnalysisTask(
filename=file.filename,
stored_path=str(file_path),
analysis_type=analysis_type,
status="pending",
)
db.add(task)
await db.flush()
task_id = str(task.id)
# Process in background
background_tasks.add_task(
run_document_analysis,
task_id=task_id,
file_path=str(file_path),
analysis_type=analysis_type,
)
return {"task_id": task_id, "status": "pending"}
Use chunked reading with await file.read(chunk_size) in a loop instead of await file.read() which loads the entire file into memory. For files over 100MB, consider a chunked upload protocol where the client uploads in parts, or use presigned URLs to upload directly to object storage like S3, then pass the object key to your API for processing.
FastAPI does not allow combining UploadFile with a JSON request body in the same endpoint because multipart form data and JSON bodies use different content types. Use Form() parameters alongside File(), or accept the JSON as a string Form field and parse it with Pydantic manually. Another approach is a two-step flow: upload the file first and get back a file ID, then send a JSON request referencing that file ID.
Use libraries like PyMuPDF (fitz) or pdfplumber for text extraction. Read the uploaded bytes, open the PDF, iterate through pages, and extract text. For scanned PDFs without embedded text, you need OCR with a library like pytesseract. Process PDF extraction in a background task because it can be CPU-intensive for large documents with many pages.
#FastAPI #FileUpload #DocumentProcessing #AIAgents #Python #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
A founder's guide to the personal AI assistant market: best AI assistant apps, business-grade options, and how CallSphere's voice agent fits in.
A founder's guide to free AI agents, low-code AI agent builders, and how to know when you should pay for a real platform like CallSphere.
Graphiti is the open-source temporal knowledge graph for AI agents in 2026. Learn how bi-temporal memory beats vector RAG for voice agents and long-running LLMs.
Chatbot app vs ChatGPT in 2026: a founder's clear take on the difference, when to use which, and how a real AI chatbot app development works.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
Head-to-head: OpenAI Frontier and Anthropic's managed agent stack — strengths, fit, and what each means for enterprise AI voice and chat deployment.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.