If you've ever spent hours reading through a pile of documents just to summarize the key points, you already know exactly what kind of problem Gemini 2.5 Pro was built to solve. PDFs, scanned images, meeting notes — feed them all in at once and get a structured report back in seconds.
What follows is a Python script that accepts a folder of mixed document types and outputs a clean Markdown report, built on Gemini 2.5 Pro's File API. Every code block here is complete and runnable.
Why Gemini 2.5 Pro for Document Analysis
Choosing the right model for document analysis comes down to three factors, and Gemini 2.5 Pro wins on all three.
Context window size. At up to 1 million tokens, you can pass dozens of PDF pages and multiple images in a single request. GPT-4o tops out at 128K; Claude Sonnet at 200K. For real-world document sets, this difference is decisive.
Native multimodal input. PDF layouts, charts, diagrams, and handwritten text can all be passed directly without format conversion. No more running pdfplumber to strip text and losing the visual context that makes a chart meaningful.
Controllable reasoning depth. For fast, low-cost summarization, leave Thinking mode off. For documents requiring complex cross-referencing or inference, set a thinking_budget. This flexibility matters when you're paying per token in production.
Architecture Overview
Before writing a line of code, let's fix the data flow:
Input files (PDF / JPG / PNG / TXT / MD)
↓
Parallel upload to File API
↓
Gemini 2.5 Pro multimodal analysis
↓
Markdown report saved to disk
One design decision worth explaining: we're routing everything through the File API rather than base64-encoding files inline. Files over 1MB must go through the File API regardless, and for multiple files the parallelism savings are significant. Uploaded files remain valid for 48 hours, so you can re-analyze the same document set with different prompts without re-uploading.
Environment Setup
pip install google-genai
export GOOGLE_API_KEY="YOUR_GEMINI_API_KEY"google-genai is the current official SDK (replacing the older google-generativeai). Get your API key from Google AI Studio at aistudio.google.com.
Step 1: Parallel File Upload
import os
import time
import concurrent.futures
from pathlib import Path
from google import genai
from google.genai import types
client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
SUPPORTED_MIME = {
".pdf": "application/pdf",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".webp": "image/webp",
".txt": "text/plain",
".md": "text/markdown",
}
def upload_file(file_path: Path) -> types.File | None:
"""Upload a single file to the File API.
Returns the File object on success, None on failure.
"""
suffix = file_path.suffix.lower()
mime = SUPPORTED_MIME.get(suffix)
if mime is None:
print(f" ⚠ Skipping unsupported format: {file_path.name}")
return None
try:
uploaded = client.files.upload(
file=file_path,
config=types.UploadFileConfig(mime_type=mime),
)
# Wait for processing to complete
while uploaded.state == "PROCESSING":
time.sleep(2)
uploaded = client.files.get(name=uploaded.name)
if uploaded.state == "FAILED":
print(f" ✗ Upload failed: {file_path.name}")
return None
print(f" ✓ Uploaded: {file_path.name} ({mime})")
return uploaded
except Exception as e:
print(f" ✗ Error uploading {file_path.name}: {e}")
return None
def upload_files_parallel(file_paths: list[Path], max_workers: int = 4) -> list[types.File]:
"""Upload multiple files concurrently."""
print(f"\n📤 Uploading {len(file_paths)} files...")
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(upload_file, p): p for p in file_paths}
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result is not None:
results.append(result)
print(f"✅ {len(results)} files uploaded successfully\n")
return resultsmax_workers=4 is intentionally conservative. The File API has per-minute upload limits, and hammering it with 20 concurrent requests will trigger RESOURCE_EXHAUSTED errors. For large batches, you'll want to add a time.sleep(1) between requests or implement exponential backoff.
Step 2: Batch Analysis with Gemini 2.5 Pro
ANALYSIS_PROMPT = """
Analyze the following documents and produce a structured Markdown report with these sections:
1. **Executive Summary** (~100 words) — the single most important takeaway across all documents
2. **Per-Document Summaries** — key points from each file as bullet lists
3. **Key Numbers & Data** — tables, charts, and metrics extracted and consolidated
4. **Cross-Document Insights** — patterns, contradictions, or themes that span multiple files
5. **Recommended Actions** — concrete next steps, if applicable
Guidelines:
- Preserve visual context from charts and diagrams in your analysis
- Keep each language's content in its original language
- Distinguish clearly between stated facts and inferences
"""
def analyze_documents(uploaded_files: list[types.File]) -> str:
"""Analyze uploaded files with Gemini 2.5 Pro.
Returns the generated Markdown report as a string.
"""
# Build the content list: prompt first, then file references
contents: list = [ANALYSIS_PROMPT]
for f in uploaded_files:
contents.append(
types.Part.from_uri(file_uri=f.uri, mime_type=f.mime_type)
)
print("🤖 Analyzing with Gemini 2.5 Pro...")
response = client.models.generate_content(
model="gemini-2.5-pro-preview-05-06",
contents=contents,
config=types.GenerateContentConfig(
temperature=0.2, # Lower temp for factual accuracy
max_output_tokens=8192,
# thinking_budget=2000, # Enable for complex inference tasks
),
)
if not response.text:
raise ValueError("Empty response from model. Check your file contents and API key permissions.")
return response.text
def save_report(markdown_text: str, output_path: Path) -> None:
"""Save the report to a Markdown file."""
output_path.write_text(markdown_text, encoding="utf-8")
print(f"📄 Report saved: {output_path}")The temperature=0.2 setting deserves an explanation. For document summarization and fact extraction, you want the model to stay close to the source material rather than embellishing. Setting temperature to exactly 0 can produce slightly robotic prose; 0.1–0.3 gives you accuracy without sounding like a machine.
Step 3: Main Entrypoint with Guaranteed Cleanup
def cleanup_uploaded_files(uploaded_files: list[types.File]) -> None:
"""Delete uploaded files from the File API immediately after use.
Files auto-delete after 48 hours, but for sensitive documents
(contracts, financials, PII), immediate deletion is strongly recommended.
"""
for f in uploaded_files:
try:
client.files.delete(name=f.name)
except Exception:
pass # Silently ignore — auto-delete will handle it
print(f"🗑 Deleted {len(uploaded_files)} uploaded files")
def main(input_dir: str, output_file: str = "report.md") -> None:
"""Main entry point.
Args:
input_dir: Directory containing the files to analyze
output_file: Output Markdown report filename
"""
input_path = Path(input_dir)
if not input_path.is_dir():
raise FileNotFoundError(f"Directory not found: {input_dir}")
# Collect supported files
file_paths = [
p for p in input_path.iterdir()
if p.is_file() and p.suffix.lower() in SUPPORTED_MIME
]
if not file_paths:
print("No supported files found in the directory.")
return
print(f"📁 Found {len(file_paths)} files to analyze:")
for p in file_paths:
print(f" - {p.name}")
uploaded_files = []
try:
# Step 1: Parallel upload
uploaded_files = upload_files_parallel(file_paths)
if not uploaded_files:
print("No files were uploaded successfully.")
return
# Step 2: Analyze
markdown_report = analyze_documents(uploaded_files)
# Step 3: Save
output_path = Path(output_file)
save_report(markdown_report, output_path)
print("\n✅ Done\!")
finally:
# Always clean up — even if analysis fails
if uploaded_files:
cleanup_uploaded_files(uploaded_files)
if __name__ == "__main__":
import sys
input_dir = sys.argv[1] if len(sys.argv) > 1 else "./documents"
output_file = sys.argv[2] if len(sys.argv) > 2 else "report.md"
main(input_dir, output_file)The try/finally pattern here is non-negotiable. If analysis raises an exception midway, you don't want sensitive document fragments sitting in the File API for 48 hours. This is especially true in multi-tenant environments where the API key is shared.
Running It
Given this directory structure:
documents/
├── Q1_report.pdf # English financial report
├── architecture.png # System diagram
└── meeting_notes.md # Meeting notes
Run with:
python document_assistant.py ./documents report_20260415.mdExpected console output:
📁 Found 3 files to analyze:
- Q1_report.pdf
- architecture.png
- meeting_notes.md
📤 Uploading 3 files...
✓ Uploaded: Q1_report.pdf (application/pdf)
✓ Uploaded: architecture.png (image/png)
✓ Uploaded: meeting_notes.md (text/markdown)
✅ 3 files uploaded successfully
🤖 Analyzing with Gemini 2.5 Pro...
📄 Report saved: report_20260415.md
✅ Done\!
🗑 Deleted 3 uploaded files
The generated Markdown report will contain consolidated financial metrics from the PDF, a description of the architecture diagram, and a summary of meeting decisions — all cross-referenced where relevant.
Common Errors and Fixes
RESOURCE_EXHAUSTED: Quota exceeded
The free tier has daily upload limits. For production workloads, upgrade to a paid tier or add time.sleep(60) between batches to stay within rate limits.
INVALID_ARGUMENT: The File API does not support this MIME type
.docx and .xlsx files cannot be uploaded directly. Convert them to PDF first using a library like python-docx or openpyxl, or extract their text content and send it as plain text.
File state is FAILED
This usually means the file is password-protected, corrupted, or an unsupported PDF variant. Use PyMuPDF or pikepdf to validate and repair files before uploading.
response.text is None
The safety filter likely blocked the request. Check response.prompt_feedback to see the reason. Adjust your prompt or, where appropriate, configure safety_settings to allow the content type.
Adapting the Prompt for Different Use Cases
The ANALYSIS_PROMPT constant is the only thing you need to change for completely different output styles.
For legal document review:
LEGAL_PROMPT = """
Review the following contracts and legal documents. Extract:
1. Parties involved, effective dates, and term length
2. Key obligations for each party
3. Risk clauses (penalties, indemnification, confidentiality)
4. Ambiguous language requiring legal review
Note: This is an information summary, not legal advice.
"""For competitive intelligence:
COMPETITIVE_PROMPT = """
Analyze the attached materials (press releases, product specs, social posts).
Produce:
1. Recent announcements and product changes per company
2. Feature and pricing comparison matrix
3. Market positioning shifts
4. Implications and risks for our business
"""Next Steps
The script above handles the core pipeline in around 100 lines. Once it's working for your document set, a natural next step is adding streaming output so users see the report being generated in real time rather than waiting for the full response. For teams needing consistent report formatting, system instructions let you lock in the tone and structure across all analyses.