# `.to_llm()` Specification

## What this is

A method on `PDF`, `PageCollection`, `Page`, `Region`, `ElementCollection`, and `Element` objects that produces a text representation optimized for LLM agent consumption. It describes the *structure* of the document — what an agent would learn from `.show()` if it could see — so agents can select the right extraction approach without visual inspection.

## Design principles

1. **Describe primitives, not recommendations (by default).** Say "42 horizontal + 8 vertical ruling lines forming a grid of ~76 enclosed cells." Let the agent infer the tool. Capability hints are available via feature flag, not mixed into observations.

2. **Text serves structure.** Include text samples to illustrate style tiers, demonstrate text layer quality, and surface form labels — not as a replacement for `extract_text()`.

3. **O(n) compute ceiling.** Default invocation only aggregates pre-parsed elements (styles, lines, rects, images). Never trigger ML inference, image rendering, or OpenCV pipelines. Include results from expensive operations (layout analysis, form cells, checkboxes) only if already computed.

4. **Token-bounded.** Every variable-length list has a hard cap. Output size is predictable regardless of document complexity.

5. **Stable across document variants.** Deterministic ordering (top-to-bottom, left-to-right). Consistent section headers. Style tiers based on numeric bucketing, not content.

6. **Feature-flagged for benchmarking.** Multiple approaches to hints, text inclusion, and detail level can be toggled independently. Configurations can be compared by running agents with different flags.

---

## API

### Method signature

```python
def to_llm(
    self,
    # Detail level
    detail: str = "standard",     # "brief" | "standard" | "full"
    
    # Feature flags (for benchmarking different approaches)
    include_text: bool = True,    # Include text samples in style tiers
    include_hints: str = "none",  # "none" | "descriptive" | "api"
    
    # Caps (override defaults)
    max_style_tiers: int = 8,
    max_samples_per_tier: int = 5,
    max_duplicates: int = 10,
    max_regions: int = 15,
) -> str:
```

### Available on

| Object | Purpose | Token budget |
|--------|---------|-------------|
| `PDF` | Routing overview — where to look, what each page contains | ~100-150 tokens/page |
| `PageCollection` | Same as PDF, for a subset of pages | ~100-150 tokens/page |
| `Page` | Structural map — replace the need to "see" the page | ~800-2000 tokens |
| `Region` | Microscope — focused view of a bounded area | ~200-800 tokens |
| `ElementCollection` | Collection summary — what's in this set of elements | ~200-600 tokens |
| `Element` | Single element context | ~50-100 tokens |

### Detail levels

| Level | What it includes | Use case |
|-------|-----------------|----------|
| `brief` | Summary stats, text layer status, top 3 style tiers, line/rect counts. No text samples, no hazards. | Quick routing, page classification |
| `standard` | Full structural map with bounded text samples, hazards, spatial stats, grid analysis. | Writing extraction code (default) |
| `full` | Expanded samples (more per tier), more duplicate labels, includes text content blocks per detected region. | Debugging failed extractions |

### Feature flags for hints

| Flag value | Behavior | Example |
|------------|----------|---------|
| `"none"` | Pure observation. No method names appear. | (default) |
| `"descriptive"` | Observations include natural-language suggestions tied to structural patterns. No API names. | "Grid structure suggests cell-based extraction rather than spatial navigation." |
| `"api"` | Explicit method names appear in a separated SUGGESTED METHODS section at the end. | "Grid detected → .detect_form_cells(), .guides()" |

The flag approach lets us benchmark: run agents with each setting, compare recipe quality, and pick the winner.

---

## Output format

Plain text with ALL-CAPS section headers. No Markdown, no YAML, no JSON. Parseable by any LLM without format knowledge. Consistent delimiters. Max 2 levels of indentation.

Section headers are stable strings that won't change across versions. Content under headers may evolve.

---

## Sections (Page level — the primary level)

### 1. TEXT LAYER

**Always included.** Cost: O(n), trivial.

**Source data:** Element `.source` attribute ("pdf" vs "ocr"), page `.images`, page `.words`.

**Content:**
- Native text word count
- OCR text word count (if any OCR has been applied)
- Image coverage: percentage of page area covered by embedded images
- Source breakdown: "all native" / "all OCR" / "mixed" / "no text"

**Example:**
```
TEXT LAYER
  Status: native text (842 words)
  Image coverage: 2% (1 logo, 32x45 pts at top-left)
```

```
TEXT LAYER
  Status: no native text
  Image coverage: 98% (1 full-page image, 608x788 pts)
  Note: OCR has not been applied. Call page.apply_ocr() first.
```

```
TEXT LAYER
  Status: mixed — 842 native words + 36 OCR words (rapidocr, median confidence 0.92)
```

**Caps:** None needed (fixed size).

---

### 2. STYLES

**Always included.** Cost: O(n), groups elements by (size, bold, italic).

**Source data:** Element `.size`, `.bold`, `.italic`, `.fontname`, `.text`. Uses `analyze_text_styles()` Jenks bucketing if not already applied; falls back to simple rounding if Jenks is unavailable.

**Content:**
- Style tiers sorted by font size descending
- Each tier: size, bold/italic flags, occurrence count, role hint (by count heuristic: 1-3 = "likely headings", 10-50 = "labels/anchors", 100+ = "body text")
- Text samples per tier (if `include_text=True`)
- Matching selector for each tier (teaches agents the syntax by example)

**Example:**
```
STYLES
  16pt bold (1 occurrence, likely heading)
    selector: text:bold[size>=15][size<=17]
    samples: "ARREST REPORT"
  8pt bold (38 occurrences, likely labels)
    selector: text:bold[size>=7][size<=9]
    samples: "Name", "DOB", "Address", "Sex", "Race" (+33 more)
  10pt (247 occurrences, body text)
    selector: text[size>=9][size<=11]:not(:bold)
    samples: "JOHN DOE", "01/15/1985", "123 Main Street" (+244 more)
  7pt (12 occurrences, likely footnotes)
    selector: text[size>=6][size<=8]:not(:bold)
    samples: "Page 1 of 3", "Form Rev. 2019" (+10 more)
```

**Caps:** max 8 tiers, max 5 samples per tier (standard), max 10 samples per tier (full).

**Why this matters:** The single highest-value section. Shifts agents from brittle `:contains("SECTION NAME")` to robust `text:bold[size>=14]`. The selector examples teach the syntax without prescribing usage.

---

### 3. RULING LINES & GRID

**Always included (vector lines are free from PDF parsing).** Cost: O(h * v) for intersection computation where h/v = horizontal/vertical line count. Trivial for typical documents.

**Source data:** `page.lines` (LineElement objects from pdfplumber vector paths).

**Content:**
- Horizontal line count, vertical line count
- If lines form a grid (intersections detected): estimated cell count, cell size range
- Grid regularity: "uniform" (data table) vs "variable" (form layout)
- If `detect_form_cells()` was already run: report detected cell count and source

**Example (form with grid):**
```
RULING LINES & GRID
  Lines: 42 horizontal, 8 vertical
  Grid: ~76 enclosed cells (variable sizes, 40-200pt wide, 15-45pt tall)
  Grid type: irregular (form layout, not data table)
```

**Example (table with borders):**
```
RULING LINES & GRID
  Lines: 12 horizontal, 6 vertical
  Grid: ~55 enclosed cells (uniform, ~95pt wide, ~18pt tall)
  Grid type: regular (data table, 11 rows x 5 columns)
```

**Example (no lines):**
```
RULING LINES & GRID
  Lines: 0 horizontal, 0 vertical
  No ruling lines detected (tables may be borderless/whitespace-separated)
```

**Caps:** Fixed size output.

---

### 4. LAYOUT REGIONS

**Included only if `analyze_layout()` was already called.** Cost: O(1) — just reads existing region data.

**Source data:** Regions with `source="detected"` stored in page element manager.

**Content:**
- Detected regions with type, position, confidence
- Sorted top-to-bottom, left-to-right

**Example:**
```
LAYOUT REGIONS (yolo, computed)
  [1] title at y=30-55, full width, confidence=0.96
  [2] table at y=60-520, full width, confidence=0.94
  [3] text at y=525-560, right-aligned, confidence=0.88
```

**Example (not computed):**
```
LAYOUT REGIONS
  Not analyzed. Call page.analyze_layout() for content block detection.
```

**Caps:** Max 15 regions.

---

### 5. VISUAL ELEMENTS

**Always included.** Cost: O(n), iterates images and rects.

**Source data:** `page.images` (ImageElement), `page.find_all('rect')` (RectangleElement). If `detect_checkboxes()` was run, includes those regions.

**Content:**
- Image inventory: count, total coverage, largest image dimensions
- Rectangle summary: count, size distribution (small/medium/large buckets)
- Small square rects (potential checkboxes): count and positions if not yet classified
- Detected checkboxes (if already computed): count, checked/unchecked breakdown

**Example:**
```
VISUAL ELEMENTS
  Images: 1 (logo, 32x45 pts at top-left, 0.3% coverage)
  Rectangles: 84 total (76 small <50pt, 6 medium, 2 large)
  Small squares (potential checkboxes): 8 squares ~11x11pt near y=400-440
  Checkboxes: not yet detected. Call page.detect_checkboxes() to classify.
```

**Example (checkboxes already detected):**
```
VISUAL ELEMENTS
  Images: none
  Rectangles: 12 total
  Checkboxes (detected, vector): 6 total — 2 checked, 4 unchecked
```

**Caps:** Fixed size output.

---

### 6. SPATIAL STATS

**Included at standard and full detail.** Cost: O(n), computes from element positions.

**Source data:** Element bounding boxes, font sizes.

**Content:**
- Page dimensions (width x height in pts)
- Font size range: min / median / max
- Line spacing: median vertical gap between consecutive text elements
- Horizontal density: median gap between horizontally adjacent elements

**Example:**
```
SPATIAL STATS
  Page: 612 x 792 pts
  Font sizes: 7pt (min), 10pt (median), 16pt (max)
  Vertical line gap: 14pt (median), 22pt (90th percentile)
  Horizontal element gap: 42pt (median)
```

**Caps:** Fixed size output.

---

### 7. HAZARDS

**Included at standard and full detail.** Cost: O(n), groups text by content.

**Source data:** Element text content and positions.

**Content:**
- Duplicate labels: text strings appearing 2+ times, with y-positions
- Very small text positions (< body text size, potential footnotes/fine print)
- Repeated headers/footers (at PDF level): text appearing at same position across 3+ pages

**Example:**
```
HAZARDS
  Duplicate labels:
    "Date" appears 3x at y=68, y=200, y=480
    "Total" appears 4x at y=100, y=250, y=400, y=550
    "Name" appears 2x at y=68, y=340
  Small text (7pt) in footer region y=530-560
```

**Caps:** Max 10 duplicate label groups, max 3 positions per group.

---

### 8. SUGGESTED METHODS (feature-flagged)

**Only included when `include_hints="api"`.** Cost: O(1), conditional on observations.

**Content:** A bounded list of method suggestions triggered by observed structural patterns. Max 5 entries.

**Trigger logic (code-driven):**

| Observation | Suggestion |
|------------|------------|
| Grid detected (>4 intersecting lines) | `.detect_form_cells()`, `.guides()`, `.extract_table()` |
| No text + high image coverage | `.apply_ocr(engine="rapidocr")` |
| Multiple style tiers with clear hierarchy | Use style selectors: `text:bold[size>=N]` |
| Borderless but aligned text columns | `.guides().from_whitespace()`, `.extract_table(method="text")` |
| Checkboxes not yet detected but small squares found | `.detect_checkboxes()` |
| Layout not yet analyzed | `.analyze_layout()` for content block detection |
| Multiple pages with similar structure | `pages.groupby('text:bold[size>=14]')` |

**Example:**
```
SUGGESTED METHODS
  Grid of ~76 cells detected → .detect_form_cells() or .extract_table()
  8 small squares likely checkboxes → .detect_checkboxes()
  Layout not analyzed → .analyze_layout() for block-level structure
```

---

## PDF / PageCollection level

**Purpose:** Routing overview. Help agents decide which pages to examine and what approach to use per page.

**Content:**
- Document summary: page count, page size consistency
- Per-page digest as a compact list:
  - Page index, text status, word count, key structural signals
  - Top 2-3 unique anchor labels (from largest/boldest text)
- Cross-page patterns:
  - Repeated text at consistent positions (headers/footers)
  - Pages sharing similar structure (same style distribution)
  - Table/grid continuity across pages (same column count at similar positions)

**Example:**
```
=== PDF: invoice_packet.pdf (5 pages) ===

PAGE SUMMARY
  All pages: 612 x 792 pts, native text

  Page 1: 342 words, 3 style tiers, no grid
    anchors: "INVOICE", "Bill To", "Ship To"
  Page 2: 891 words, 2 style tiers, 12x6 grid (data table)
    anchors: "Line Items", "Description", "Amount"
  Page 3: 887 words, 2 style tiers, 12x6 grid (data table, continued)
    anchors: "Description", "Amount" (same headers as page 2)
  Page 4: 156 words, 2 style tiers, no grid
    anchors: "Terms and Conditions"
  Page 5: 89 words, 1 style tier, no grid
    anchors: "Remittance Advice"

CROSS-PAGE PATTERNS
  Repeated footer: "Acme Corp — Confidential" at y=750 on all pages
  Pages 2-3: same table structure (continuing table)
```

**Token budget:** ~100-150 tokens per page.

**Caps:** Max 3 anchors per page. Max 5 cross-page patterns.

---

## Region level

**Purpose:** Focused view for tight extraction logic in a bounded area.

Since regions are small, include more text and local detail than page level.

**Content:**
- Region bbox and size relative to page
- Full text content (or first 500 chars if large), preserving layout
- Local style breakdown (usually 1-3 styles in a region)
- Local grid structure if ruling lines are present within the region
- What's adjacent: brief description of content above/below/left/right of the region

**Example:**
```
=== Region: y=60-180, x=0-612 (full width, 120pt tall) ===

TEXT CONTENT
  Name: JOHN DOE                    DOB: 01/15/1985
  Address: 123 Main Street          Sex: M    Race: W
           Apt 4B                   Height: 5'11"
           Springfield, IL 62701    Weight: 185

STYLES
  8pt bold (8 occurrences): "Name", "DOB", "Address", "Sex", "Race", "Height", "Weight"
  10pt (8 occurrences): "JOHN DOE", "01/15/1985", "123 Main Street", "M", "W"

RULING LINES & GRID
  Local grid: 6 horizontal, 4 vertical lines → ~15 cells
  Cell sizes: 140-280pt wide, 18-36pt tall

ADJACENT CONTENT
  Above: "ARREST REPORT" (16pt bold, centered)
  Below: grid continues with "CHARGES" section header at y=185
```

**Token budget:** ~200-800 tokens depending on region size.

**Caps:** Text content max 500 chars. Adjacent content max 1 line per direction.

---

## ElementCollection level

**Purpose:** Summarize a set of elements an agent has selected (e.g., `page.find_all('text:bold')`).

**Content:**
- Count and element type breakdown
- Bounding box of all elements (spatial extent)
- Style distribution (if text elements): size range, bold/italic counts
- Text samples (bounded)
- Spatial pattern: clustered, scattered, column-aligned, row-aligned

**Example:**
```
=== ElementCollection: 38 text elements ===

EXTENT
  Bounding box: x=52-580, y=60-520 (covers 87% of page)

STYLES
  All 8pt bold
  
TEXT SAMPLES
  "Name", "DOB", "Address", "Sex", "Race", "Height", "Weight",
  "Hair", "Eyes", "SSN" (+28 more)

SPATIAL PATTERN
  Left-column aligned (34/38 elements at x=52 or x=320)
  Vertical spacing: 18-22pt between elements (regular)
```

**Token budget:** ~200-600 tokens.

**Caps:** Max 10 text samples at standard, max 20 at full.

---

## Element level

**Purpose:** Describe a single element with enough context for an agent to write code targeting it.

**Content:**
- Text, style, position
- Source (native/ocr) and confidence (if OCR)
- What would select this element (a working selector)

**Example:**
```
"ARREST REPORT" — 16pt bold, page 1 at (306, 42)
  selector: text:bold[size>=15]:contains("ARREST REPORT")
  source: native
```

**Token budget:** ~50-100 tokens. Single line at brief detail, 3 lines at standard.

---

## Architecture

### Service pattern

Follow existing service delegation: create `ToLLMService` registered under capability `"to_llm"`.

```
natural_pdf/
  services/
    to_llm_service.py          # Service class with @register_delegate
  describe/
    to_llm.py                  # Builder functions (page, region, collection, element)
    to_llm_sections.py         # Individual section renderers
```

### Builder functions

Each level has a builder that assembles sections:

```python
def page_to_llm(page, detail, include_text, include_hints, caps) -> str
def region_to_llm(region, detail, include_text, caps) -> str
def collection_to_llm(collection, detail, include_text, caps) -> str
def element_to_llm(element, detail) -> str
def pdf_to_llm(pdf, detail, include_text, include_hints, caps) -> str
```

### Section renderers

Each section is a pure function that takes a page/region + caps and returns a string:

```python
def render_text_layer(page) -> str
def render_styles(page, include_text, max_tiers, max_samples) -> str
def render_ruling_lines(page) -> str
def render_layout_regions(page, max_regions) -> str
def render_visual_elements(page) -> str
def render_spatial_stats(page) -> str
def render_hazards(page, max_duplicates) -> str
def render_suggested_methods(page, hint_style) -> str
```

Sections are composed by the builder. Detail level controls which sections are included. Feature flags control section behavior.

### Reuse existing infrastructure

- **Style bucketing:** Reuse `analyze_text_styles()` Jenks breaks logic (don't duplicate)
- **Element iteration:** Reuse `describe()` typography analysis patterns
- **Grid detection:** New code, but operates on existing `page.lines` data (free from pdfplumber)
- **Checkbox/layout inclusion:** Just check for existing regions with `source="detected"` or `source="checkbox"`

---

## Compute cost summary

| Section | Default cost | Triggers expensive ops? |
|---------|-------------|------------------------|
| TEXT LAYER | O(n) count | No |
| STYLES | O(n) group-by, O(n log n) if Jenks needed | No |
| RULING LINES & GRID | O(h*v) intersections, typically <100 | No |
| LAYOUT REGIONS | O(1) read existing | No, skip if not computed |
| VISUAL ELEMENTS | O(n) iterate images/rects | No |
| SPATIAL STATS | O(n) sort + percentiles | No |
| HAZARDS | O(n) group-by | No |
| SUGGESTED METHODS | O(1) conditional checks | No |

**Guarantee:** `page.to_llm()` never triggers image rendering, ML model loading, or network calls. It only aggregates data that pdfplumber already parsed when the PDF was opened.

---

## What's deferred from v1

| Feature | Why deferred | Where it belongs |
|---------|-------------|-----------------|
| Key-value field topology | Fragile heuristics, false positives on narrative docs | Separate `page.analyze_form()` method |
| Handwriting detection | Not implemented, requires image ML | Future OCR diagnostic |
| Signature/stamp detection | Not implemented, requires ML | Future visual element detector |
| Skew angle | Requires image rendering | Include if already computed via deskew |
| GlyphLessFont detection | Not implemented for input PDFs | Future text layer diagnostic |
| Content block segmentation (non-ML) | Unstable across variants | Use analyze_layout() explicitly |
| Per-word coordinates | Token-expensive, poor LLM spatial reasoning | Available via `region.inspect()` |

---

## Benchmarking plan

The feature flags enable systematic comparison:

```python
configs = [
    {"include_text": False, "include_hints": "none"},      # structure only
    {"include_text": True,  "include_hints": "none"},      # + text samples
    {"include_text": True,  "include_hints": "descriptive"},# + natural hints
    {"include_text": True,  "include_hints": "api"},       # + explicit API hints
]

# For each config, run N agents on M document types
# Measure: feature usage diversity, magic number frequency,
# regex count, extraction accuracy, tokens consumed
```

Metrics to track per configuration:
- **Feature diversity:** how many distinct library methods appear in recipes
- **Magic number count:** hardcoded pixel values in spatial navigation calls
- **Regex density:** re.search + re.sub calls per recipe
- **Extraction accuracy:** correct values extracted on test corpus
- **Token cost:** total tokens consumed per extraction recipe
