Back to Blog
6 min read

Building Document Intelligence Solutions with Azure Form Recognizer

Introduction

Azure Form Recognizer (now part of Azure AI Document Intelligence) enables automated extraction of text, key-value pairs, tables, and structures from documents. This is essential for automating document-heavy business processes.

Form Recognizer Capabilities

Pre-built Models

  • Invoice: Extract vendor, dates, line items, totals
  • Receipt: Extract merchant, transaction details
  • ID Document: Extract identity information
  • Business Card: Extract contact information
  • W-2 Tax Forms: Extract tax document data

Custom Models

  • Train on your specific document types
  • Layout-based or neural training options

Getting Started

Installation

pip install azure-ai-formrecognizer

Basic Setup

import os
from azure.ai.formrecognizer import DocumentAnalysisClient
from azure.core.credentials import AzureKeyCredential

# Initialize client
endpoint = os.getenv("AZURE_FORM_RECOGNIZER_ENDPOINT")
key = os.getenv("AZURE_FORM_RECOGNIZER_KEY")

client = DocumentAnalysisClient(
    endpoint=endpoint,
    credential=AzureKeyCredential(key)
)

Analyzing Documents

General Document Analysis

def analyze_document(document_url: str) -> dict:
    """Analyze a document and extract content."""
    poller = client.begin_analyze_document_from_url(
        "prebuilt-document",
        document_url
    )
    result = poller.result()

    analysis = {
        "content": result.content,
        "pages": [],
        "tables": [],
        "key_value_pairs": []
    }

    # Extract page information
    for page in result.pages:
        page_info = {
            "page_number": page.page_number,
            "width": page.width,
            "height": page.height,
            "lines": [line.content for line in page.lines] if page.lines else []
        }
        analysis["pages"].append(page_info)

    # Extract tables
    for table in result.tables:
        table_data = {
            "row_count": table.row_count,
            "column_count": table.column_count,
            "cells": []
        }
        for cell in table.cells:
            table_data["cells"].append({
                "row": cell.row_index,
                "column": cell.column_index,
                "content": cell.content,
                "is_header": cell.kind == "columnHeader"
            })
        analysis["tables"].append(table_data)

    # Extract key-value pairs
    for kv_pair in result.key_value_pairs:
        if kv_pair.key and kv_pair.value:
            analysis["key_value_pairs"].append({
                "key": kv_pair.key.content,
                "value": kv_pair.value.content,
                "confidence": kv_pair.confidence
            })

    return analysis

# Usage
result = analyze_document("https://example.com/document.pdf")
print(f"Found {len(result['key_value_pairs'])} key-value pairs")
print(f"Found {len(result['tables'])} tables")

Invoice Processing

from dataclasses import dataclass
from typing import List, Optional
from datetime import date

@dataclass
class LineItem:
    description: str
    quantity: Optional[float]
    unit_price: Optional[float]
    amount: Optional[float]

@dataclass
class Invoice:
    vendor_name: Optional[str]
    vendor_address: Optional[str]
    invoice_id: Optional[str]
    invoice_date: Optional[date]
    due_date: Optional[date]
    subtotal: Optional[float]
    tax: Optional[float]
    total: Optional[float]
    line_items: List[LineItem]

def process_invoice(invoice_url: str) -> Invoice:
    """Process an invoice and extract structured data."""
    poller = client.begin_analyze_document_from_url(
        "prebuilt-invoice",
        invoice_url
    )
    result = poller.result()

    if not result.documents:
        raise ValueError("No invoice found in document")

    invoice_doc = result.documents[0]
    fields = invoice_doc.fields

    def get_value(field_name: str, default=None):
        field = fields.get(field_name)
        if field and field.value:
            return field.value
        return default

    def get_currency_value(field_name: str):
        field = fields.get(field_name)
        if field and field.value:
            return field.value.amount
        return None

    # Extract line items
    line_items = []
    items_field = fields.get("Items")
    if items_field and items_field.value:
        for item in items_field.value:
            item_fields = item.value
            line_items.append(LineItem(
                description=get_value("Description", item_fields) if "Description" in item_fields else None,
                quantity=get_value("Quantity", item_fields) if "Quantity" in item_fields else None,
                unit_price=get_currency_value("UnitPrice") if "UnitPrice" in item_fields else None,
                amount=get_currency_value("Amount") if "Amount" in item_fields else None
            ))

    return Invoice(
        vendor_name=get_value("VendorName"),
        vendor_address=str(get_value("VendorAddress")) if get_value("VendorAddress") else None,
        invoice_id=get_value("InvoiceId"),
        invoice_date=get_value("InvoiceDate"),
        due_date=get_value("DueDate"),
        subtotal=get_currency_value("SubTotal"),
        tax=get_currency_value("TotalTax"),
        total=get_currency_value("InvoiceTotal"),
        line_items=line_items
    )

# Usage
invoice = process_invoice("https://example.com/invoice.pdf")
print(f"Invoice from: {invoice.vendor_name}")
print(f"Total: ${invoice.total}")
print(f"Line items: {len(invoice.line_items)}")

Receipt Processing

@dataclass
class Receipt:
    merchant_name: Optional[str]
    merchant_address: Optional[str]
    transaction_date: Optional[date]
    transaction_time: Optional[str]
    items: List[dict]
    subtotal: Optional[float]
    tax: Optional[float]
    total: Optional[float]

def process_receipt(receipt_url: str) -> Receipt:
    """Process a receipt and extract transaction data."""
    poller = client.begin_analyze_document_from_url(
        "prebuilt-receipt",
        receipt_url
    )
    result = poller.result()

    if not result.documents:
        raise ValueError("No receipt found")

    receipt_doc = result.documents[0]
    fields = receipt_doc.fields

    def get_field_value(name: str):
        field = fields.get(name)
        if field:
            return field.value
        return None

    # Extract items
    items = []
    items_field = fields.get("Items")
    if items_field and items_field.value:
        for item in items_field.value:
            item_dict = {}
            if item.value:
                for key, val in item.value.items():
                    if val and val.value:
                        item_dict[key] = val.value
            items.append(item_dict)

    return Receipt(
        merchant_name=get_field_value("MerchantName"),
        merchant_address=str(get_field_value("MerchantAddress")) if get_field_value("MerchantAddress") else None,
        transaction_date=get_field_value("TransactionDate"),
        transaction_time=get_field_value("TransactionTime"),
        items=items,
        subtotal=get_field_value("Subtotal"),
        tax=get_field_value("TotalTax"),
        total=get_field_value("Total")
    )

# Usage
receipt = process_receipt("https://example.com/receipt.jpg")
print(f"Store: {receipt.merchant_name}")
print(f"Total: ${receipt.total}")

Building a Document Processing Pipeline

from enum import Enum
from typing import Union
import asyncio

class DocumentType(Enum):
    INVOICE = "invoice"
    RECEIPT = "receipt"
    GENERAL = "general"
    UNKNOWN = "unknown"

class DocumentProcessor:
    def __init__(self, client: DocumentAnalysisClient):
        self.client = client

    def classify_document(self, document_url: str) -> DocumentType:
        """Classify document type based on content."""
        # Analyze with general model first
        poller = self.client.begin_analyze_document_from_url(
            "prebuilt-document",
            document_url
        )
        result = poller.result()

        content_lower = result.content.lower()

        # Simple classification based on keywords
        if any(word in content_lower for word in ["invoice", "bill to", "invoice number"]):
            return DocumentType.INVOICE
        elif any(word in content_lower for word in ["receipt", "total", "thank you for"]):
            return DocumentType.RECEIPT
        else:
            return DocumentType.GENERAL

    def process(self, document_url: str) -> Union[Invoice, Receipt, dict]:
        """Process document based on its type."""
        doc_type = self.classify_document(document_url)

        if doc_type == DocumentType.INVOICE:
            return process_invoice(document_url)
        elif doc_type == DocumentType.RECEIPT:
            return process_receipt(document_url)
        else:
            return analyze_document(document_url)

    async def process_batch(self, document_urls: List[str]) -> List[dict]:
        """Process multiple documents concurrently."""
        results = []

        for url in document_urls:
            try:
                result = self.process(url)
                results.append({
                    "url": url,
                    "status": "success",
                    "data": result
                })
            except Exception as e:
                results.append({
                    "url": url,
                    "status": "error",
                    "error": str(e)
                })

        return results

# Usage
processor = DocumentProcessor(client)

# Single document
result = processor.process("https://example.com/document.pdf")

# Batch processing
urls = [
    "https://example.com/invoice1.pdf",
    "https://example.com/receipt1.jpg",
    "https://example.com/contract.pdf"
]
results = asyncio.run(processor.process_batch(urls))

Combining with Azure OpenAI

Enhance extracted data with GPT-4 for additional insights:

from langchain_openai import AzureChatOpenAI

def analyze_invoice_with_ai(invoice: Invoice) -> dict:
    """Use GPT-4 to provide insights on invoice data."""

    llm = AzureChatOpenAI(
        azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
        api_key=os.getenv("AZURE_OPENAI_KEY"),
        deployment_name="gpt-4"
    )

    invoice_summary = f"""
    Invoice Analysis Request:
    - Vendor: {invoice.vendor_name}
    - Invoice ID: {invoice.invoice_id}
    - Date: {invoice.invoice_date}
    - Due Date: {invoice.due_date}
    - Subtotal: ${invoice.subtotal}
    - Tax: ${invoice.tax}
    - Total: ${invoice.total}
    - Line Items: {len(invoice.line_items)}
    """

    prompt = f"""Analyze this invoice and provide:
1. Payment urgency (based on due date)
2. Any potential issues or anomalies
3. Category suggestions for expense tracking
4. Budget allocation recommendations

{invoice_summary}"""

    response = llm.invoke(prompt)

    return {
        "invoice": invoice,
        "ai_analysis": response.content
    }

# Usage
invoice = process_invoice("https://example.com/invoice.pdf")
analysis = analyze_invoice_with_ai(invoice)
print(analysis["ai_analysis"])

Best Practices

Error Handling

from azure.core.exceptions import HttpResponseError

def safe_process_document(document_url: str) -> dict:
    """Process document with comprehensive error handling."""
    try:
        poller = client.begin_analyze_document_from_url(
            "prebuilt-document",
            document_url
        )
        result = poller.result()
        return {"status": "success", "data": result}

    except HttpResponseError as e:
        if e.status_code == 400:
            return {"status": "error", "error": "Invalid document format"}
        elif e.status_code == 404:
            return {"status": "error", "error": "Document not found"}
        else:
            return {"status": "error", "error": str(e)}

    except Exception as e:
        return {"status": "error", "error": f"Unexpected error: {str(e)}"}

Cost Optimization

class CostOptimizedProcessor:
    def __init__(self, client):
        self.client = client
        self.page_count = 0
        self.cost_per_page = 0.01  # Approximate cost

    def process_with_tracking(self, document_url: str) -> dict:
        """Process document while tracking costs."""
        poller = self.client.begin_analyze_document_from_url(
            "prebuilt-document",
            document_url
        )
        result = poller.result()

        pages = len(result.pages)
        self.page_count += pages
        estimated_cost = pages * self.cost_per_page

        return {
            "result": result,
            "pages_processed": pages,
            "estimated_cost": estimated_cost,
            "total_pages_session": self.page_count
        }

    def get_session_cost(self) -> float:
        return self.page_count * self.cost_per_page

Conclusion

Azure Form Recognizer provides powerful document intelligence capabilities for automating document processing workflows. Combined with GPT-4 for additional analysis, you can build sophisticated document understanding solutions that transform how organizations handle paperwork.

Resources

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.