GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-04-15Intermediate

Building an AI Document Assistant with Gemini 2.5 Pro — Analyze PDFs, Images & Text to Auto-Generate Markdown Reports

Learn how to use Gemini 2.5 Pro's File API and multimodal capabilities to batch-analyze PDFs, images, and text files, automatically generating structured Markdown reports. Includes complete, runnable Python code.

gemini102gemini-2.5-pro13file-api6multimodal44python104document-analysis2automation51

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 results

max_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.md

Expected 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.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-06-17
Moving My Automation Off the Gemini CLI Before the June 18 Shutdown
On June 18, the Gemini CLI stops responding for hosted plans. Here is how I moved unattended scripts that called gemini from the shell over to the google-genai SDK, with structured output, retries, and cost measurement built in.
API / SDK2026-05-18
Building Automatic Wallpaper Category Classification with Gemini Vision
An indie developer shares how they implemented automatic wallpaper image classification with the Gemini Vision API — including accuracy results, real pitfalls, structured-output tips, and a cost comparison with GPT-4o Vision.
API / SDK2026-05-16
Testing Gemini Vision for Wallpaper Auto-Classification — Real Accuracy Numbers and Pitfalls
An indie developer behind a 50M+ download wallpaper app shares a hands-on Gemini Vision classification experiment — including a first attempt at 67% accuracy and the improvements that brought it to 87%.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →