# Document Classification Workflow

Classify and categorize documents into user-defined categories using AI_CLASSIFY.

## Use When

- User wants to sort/categorize/triage documents by type (e.g., invoice, contract, receipt)
- User wants to route documents to different processing workflows
- User wants to identify document types in a mixed stage

## Constraints

| Constraint | Limit |
|------------|-------|
| Categories | 2-500 (>20 may reduce accuracy) |
| Image formats (direct) | JPG, JPEG, PNG, WEBP, GIF |
| Document formats (parse first) | PDF, DOCX, PPTX, HTML, TXT |
| Output modes | Single-label (default), multi-label |

Requires `SNOWFLAKE.CORTEX_USER` database role.

## Supported Format Strategies

| Format | Strategy |
|--------|----------|
| JPG, PNG, WEBP, GIF | Direct image classification via `AI_CLASSIFY(TO_FILE(...), categories)` |
| PDF, DOCX, PPTX, HTML, TXT | Parse first page (LAYOUT mode) via `AI_PARSE_DOCUMENT`, then classify extracted text |

## Pricing

```
AI_CLASSIFY: Credits based on input tokens (same rate as AI_COMPLETE for the underlying model)

Additional cost when text extraction is needed (PDF, DOCX, etc.):
- AI_PARSE_DOCUMENT (LAYOUT): 3.33 credits / 1,000 pages
- Parsing only first page keeps cost minimal (~0.003 credits per file)

Quick estimates for text-based classification (parse + classify):
- 100 PDF files (first page only): ~0.33 credits (parse) + classify credits
- 1,000 PDF files (first page only): ~3.33 credits (parse) + classify credits
```

**Full pricing details:** See [ai-classify.md](../../references/ai-classify.md)

## Reference
Read `../../references/ai-classify.md` to get the correct AI_CLASSIFY syntax. This prevents errors from incorrect function signatures.

---

## Workflow

### 1. Get File Location [WAIT]

**If files are on Snowflake stage:** Get full stage path, proceed to Step 2.

**If files are local:** You must get the upload destination from the user. Do not create any stages or run any SQL until the user provides this information.

Ask: "Which database, schema, and stage name should I use? (e.g., MY_DB.MY_SCHEMA.MY_STAGE)"

Use the exact stage name the user provides. After user responds, create stage with server-side encryption:
```sql
CREATE STAGE IF NOT EXISTS db.schema.user_provided_stage_name
DIRECTORY = (ENABLE = TRUE)
ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE');
```

Then upload the files.

### 2. Define Categories [WAIT]

Ask the user for classification categories. For each category, get:
- Label name (e.g., "invoice", "contract", "receipt")
- Optional: description to clarify ambiguous labels (up to 25 words each)

Also ask:
- **Single-label or multi-label?** (Can a document belong to multiple categories?)
- **Optional: task_description** — context to help the model (up to 50 words)
- **Optional: few-shot examples** — example classifications for edge cases

Confirm categories back to user before proceeding.

**Example category confirmation:**
```
Categories defined:
1. invoice - "billing document with line items, totals, and payment terms"
2. contract - "legal agreement between two or more parties"
3. receipt - "proof of payment or transaction"
4. tax_form - "government tax document (W2, 1099, etc.)"

Mode: single-label
Task description: "Classify business documents by their document type"

Does this look correct?
```

### 3. List Files & Determine Strategy

Show available files in the stage. Detect file types and determine the classification strategy:

**All images (JPG, PNG, WEBP, GIF):**
- Direct classification — no parsing needed
- Simplest and cheapest approach

**PDFs, DOCX, PPTX, HTML, TXT:**
- Text-based classification: parse first page using AI_PARSE_DOCUMENT (LAYOUT mode), then classify the extracted text
- Default: parse first page only (`page_filter: [{'start': 0, 'end': 1}]`)

**Mixed formats:**
- Handle each file type with its appropriate strategy
- Images → direct classification
- Documents → parse first page, then classify text

Inform user you'll test on one file first.

### 4. Cost Estimate

Display estimated cost for the test file, then proceed to test.

**For image files:**
```
Cost estimate (1 test file):
- AI_CLASSIFY: ~X credits
```

**For document files (parse + classify):**
```
Cost estimate (1 test file):
- AI_PARSE_DOCUMENT (1 page, LAYOUT): ~0.003 credits
- AI_CLASSIFY: ~X credits
- Total: ~X credits
```

### 5. Single File Test [WAIT]

Classify ONE file only. Display results clearly.
## ⚠️ CRITICAL: always use the AI_* function names WITHOUT the `SNOWFLAKE.CORTEX` namespace prefix

**For image files:**
```sql
SELECT AI_CLASSIFY(
    TO_FILE('@stage', 'photo.jpg'),
    ['invoice', 'contract', 'receipt', 'tax_form'],
    {'task_description': 'Classify business documents by their document type'}
):labels AS classification;
```

**For document files (parse first page, then classify):**
```sql
SELECT AI_CLASSIFY(
    AI_PARSE_DOCUMENT(
        TO_FILE('@stage', 'doc.pdf'),
        {'mode': 'LAYOUT', 'page_filter': [{'start': 0, 'end': 1}]}
    ):pages[0]:content::VARCHAR,
    ['invoice', 'contract', 'receipt', 'tax_form'],
    {'task_description': 'Classify business documents by their document type'}
):labels AS classification;
```

Ask if satisfied:
- **Yes, classification is correct** → proceed to Step 6 (batch)
- **No, wrong category assigned** → return to Step 2 to refine labels, descriptions, task_description, or add few-shot examples
- **No, classification seems uncertain or off** (may need more document context) → proceed to Step 5a (Page Context Tuning)

**How to detect category issues vs context issues:**
- Category issues: the assigned label doesn't make sense for this document type, or the categories themselves are poorly defined
- Context issues: the document's first page doesn't contain enough information to determine the type (e.g., cover pages, generic headers)

### 5a. Page Context Tuning [WAIT]

When classification may be wrong due to insufficient context from first-page-only parsing, iteratively increase the number of pages parsed. This only applies to document files (PDF, DOCX, PPTX), not images.

**Procedure:**

1. **Try first 3 pages.** Display the cost impact before running:
   ```
   Retrying with first 3 pages:
   - AI_PARSE_DOCUMENT cost: ~0.01 credits per file (3x first-page cost)
   - More document context for classification
   ```
   Re-run with `{'mode': 'LAYOUT', 'page_filter': [{'start': 0, 'end': 3}]}`. Note: when using `page_filter`, access content via `:pages[0]:content` (not `:content`).

2. **Show results and ask user:**
   - **Good enough** → lock in 3 pages, proceed to Step 6
   - **Better but not there yet** → try first 5 pages
   - **No improvement** → the issue is likely category definitions, go back to Step 2

3. **If first 5 pages not enough, try all pages:**
   ```
   Retrying with all pages:
   - AI_PARSE_DOCUMENT cost: ~0.003 × num_pages credits per file
   - Full document context for classification
   ```
   Re-run without `page_filter` parameter (omit it entirely). When no `page_filter` is used, access content via `:content` directly.

4. **If all pages still not satisfactory:** Suggest:
   - Refining category labels and descriptions (Step 2)
   - Adding few-shot examples with edge cases
   - Adding a more specific task_description

**Cost impact reference (display at each iteration):**

| Pages parsed | AI_PARSE_DOCUMENT cost per file | Notes |
|---|---|---|
| First page only | ~0.003 credits | Default, cheapest |
| First 3 pages | ~0.01 credits | 3x cost |
| First 5 pages | ~0.017 credits | 5x cost |
| All pages | ~0.003 × num_pages credits | Highest cost, most context |

**Important:** Always display the cost impact before each retry. Never silently increase page count.

### 6. Batch Process

Display batch cost for all files. If page count was adjusted in Step 5a, use the chosen page count.

**Show cost breakdown:**
```
Batch classification (X files):
- AI_PARSE_DOCUMENT (Y pages each): ~Z credits
- AI_CLASSIFY: ~W credits
- Total: ~T credits
```

Execute batch classification:

**For image files:**
```sql
SELECT
    SPLIT_PART(relative_path, '/', -1) AS filename,
    AI_CLASSIFY(
        TO_FILE('@stage', SPLIT_PART(relative_path, '/', -1)),
        ['invoice', 'contract', 'receipt', 'tax_form'],
        {'task_description': 'Classify business documents by their document type'}
    ):labels[0]::VARCHAR AS category
FROM DIRECTORY(@stage)
WHERE relative_path ILIKE '%.jpg'
   OR relative_path ILIKE '%.png';
```

**For document files:**
```sql
SELECT
    SPLIT_PART(relative_path, '/', -1) AS filename,
    AI_CLASSIFY(
        AI_PARSE_DOCUMENT(
            TO_FILE('@stage', SPLIT_PART(relative_path, '/', -1)),
            {'mode': 'LAYOUT', 'page_filter': [{'start': 0, 'end': 1}]}
        ):pages[0]:content::VARCHAR,
        ['invoice', 'contract', 'receipt', 'tax_form'],
        {'task_description': 'Classify business documents by their document type'}
    ):labels[0]::VARCHAR AS category
FROM DIRECTORY(@stage)
WHERE relative_path ILIKE '%.pdf';
```

**After batch completes, show a summary:**
```
Classification complete! Results:
- invoice: 45 files
- contract: 12 files
- receipt: 8 files
- tax_form: 3 files
```

### 7. Post-Processing [WAIT]

Present options in this order:

```
What would you like to do next?

1. Process a specific category - Extract fields, parse text, or analyze
   visuals from documents of one type
   (e.g., "Extract invoice fields from all 45 invoices")
2. Store results in a Snowflake table
3. Set up a pipeline for continuous classification
4. Done - I have what I need
```

**Option 1 — Chain to another workflow:**

Ask which category to process and what to do with it. Route to the appropriate sub-skill with a WHERE clause filtering by category:

| User wants | Route to |
|---|---|
| Extract fields from a category (e.g., all invoices) | `references/extraction.md` |
| Parse full text from a category | `references/parsing.md` |
| Analyze charts/visuals from a category | `references/visual-analysis.md` |

The chained workflow handles its own post-processing from there.

**Option 2 — Store results:**

```sql
CREATE TABLE IF NOT EXISTS db.schema.classification_results (
    result_id INT AUTOINCREMENT,
    file_path STRING,
    file_name STRING,
    category STRING,
    labels_raw VARIANT,
    classified_at TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()
);

INSERT INTO db.schema.classification_results (file_path, file_name, category, labels_raw)
SELECT
    relative_path,
    SPLIT_PART(relative_path, '/', -1),
    AI_CLASSIFY(...):labels
FROM DIRECTORY(@stage)
WHERE relative_path ILIKE '%.pdf';
```

After storing, always suggest pipeline setup.

**Option 3 — Pipeline:**

- If the user **only classified** (did not chain to another workflow via Option 1): Load `references/pipeline.md` **Template D** (classification-only pipeline).
- If the user **chained to another workflow** via Option 1 (e.g., classify → extract, or classify → extract → summarize): Load `references/pipeline.md` **Template E** (multi-step pipeline). This creates an N-step pipeline with a root classification task and child tasks for each subsequent operation, linked via `AFTER`.

**Option 4 — Done:** End workflow.

---

## Stopping Points

| After Step | Wait For |
|------------|----------|
| 1 | File location (and upload destination if local) |
| 2 | Category definitions confirmed |
| 5 | Single file classification result confirmation |
| 5a | Page context tuning result (at each iteration) |
| 7 | Post-processing choice |
