Compare commits

..

15 Commits

Author SHA1 Message Date
Jerry Liu 68ea59b623 cr 2025-10-18 11:27:39 -07:00
Jerry Liu 41050ae084 cr 2025-10-17 23:54:35 -07:00
Jerry Liu 02009cb249 cr 2025-10-17 23:40:30 -07:00
Jerry Liu 117af53323 cr 2025-10-17 23:12:36 -07:00
github-actions[bot] d0649ece6e chore: version packages (#982) 2025-10-16 16:58:29 -06:00
MartijnLeplae 5d4cabd843 Add ImageNode support in TypeScript (#969) 2025-10-16 16:56:28 -06:00
github-actions[bot] 9070a6ac16 chore: version packages (#981) 2025-10-15 12:01:34 -06:00
Bogdan Gheorghe 4f24f537f6 Add agressive table extraction argument (#980) 2025-10-15 11:57:34 -06:00
github-actions[bot] 8859a203e2 chore: version packages (#977) 2025-10-14 19:03:36 -06:00
dependabot[bot] b091364054 build(deps): bump astral-sh/setup-uv from 6 to 7 (#974) 2025-10-14 19:02:32 -06:00
dependabot[bot] 43b1a013ca build(deps): bump github/codeql-action from 3 to 4 (#973) 2025-10-14 19:02:20 -06:00
Logan f81532e7f2 safest types possible for parse (#976) 2025-10-14 19:02:07 -06:00
github-actions[bot] 986d3987d3 chore: version packages (#965)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-14 08:14:49 -06:00
Logan 1bf522311f fix default bbox values (#975) 2025-10-14 07:44:35 -06:00
Preston Carlson 24166dcfc8 Only escape single dollar sign in notebook md (#964)
* Limit escaping to lone dollar signs - preserve double dollar for latex equations

* Updated uv.lock via make lint

* Patch bump

* Unit test for _format_markdown_for_notebook

Test doesn't depend on getting real results/is just testing a string manipulation function, so inserting before other tests. Should move to its own file if we add additional formatting configurations
2025-10-07 08:06:03 -07:00
21 changed files with 934 additions and 233 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
+2 -2
View File
@@ -30,12 +30,12 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: python
dependency-caching: true
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:python"
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
@@ -31,7 +31,7 @@ jobs:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: pnpm install
@@ -4,31 +4,19 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# Complete Parse → Classify → Extract Workflow with LlamaCloud Services\n",
"# Document Classification + Extraction Workflow with LlamaCloud + LlamaIndex Workflows\n",
"\n",
"This notebook demonstrates the complete workflow for processing documents using LlamaCloud services:\n",
"1. **Parse** - Extract and convert documents to markdown\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/misc/parse_classify_extract_workflow.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"\n",
"This notebook shows a multi-step agentic document workflow that uses the **parsing**, **classification** and **extraction** modules in LlamaCloud, orchestrated through **LlamaIndex Workflows**. The workflow can take in a complex input document, parse it into clean markdown, classify it according to its subtype, and extract data according to a specified schema for that subtype. This allows you to automate document extraction of various types within the same workflow instead of having to manually separate the data beforehand. \n",
"\n",
"This notebook uses the following modules:\n",
"1. **Parse (LlamaParse)** - Extract and convert documents to markdown\n",
"2. **Classify** - Categorize documents based on their content\n",
"3. **Extract** - Extract structured data using the markdown as input via SourceText\n",
"3. **Extract (LlamaExtract)** - Extract structured data using the markdown as input via SourceText\n",
"4. **LlamaIndex Workflows** - Event-driven orchestration of the parse, classify and extract steps\n",
"\n",
"## Overview of the Workflow\n",
"\n",
"### 1. Parse Phase\n",
"- Use `LlamaParse` to convert documents (PDFs, Word docs, etc.) into structured formats\n",
"- Extract markdown content that preserves document structure\n",
"- Get both raw text and markdown representations\n",
"\n",
"### 2. Classify Phase\n",
"- Use `ClassifyClient` to categorize documents based on content\n",
"- Apply classification rules to route documents appropriately\n",
"- Handle different document types with specific processing logic\n",
"\n",
"### 3. Extract Phase\n",
"- Use `LlamaExtract` with `SourceText` to extract structured data\n",
"- Pass the markdown content as input for more accurate extraction\n",
"- Define custom schemas for structured data extraction\n",
"\n",
"Let's walk through each step with practical examples."
"The workflow is implemented as a proper LlamaIndex Workflow with separate steps for parsing, classification, and extraction, connected by typed events. This provides modularity, observability, and type safety."
]
},
{
@@ -45,8 +33,8 @@
"outputs": [],
"source": [
"# Install required packages\n",
"!pip install llama-cloud-services\n",
"!pip install python-dotenv"
"%pip install llama-cloud-services\n",
"%pip install python-dotenv"
]
},
{
@@ -73,7 +61,7 @@
"nest_asyncio.apply()\n",
"\n",
"# Set up API key\n",
"os.environ[\"LLAMA_CLOUD_API_KEY\"] = \"\" # edit it\n",
"# os.environ[\"LLAMA_CLOUD_API_KEY\"] = \"\" # edit it\n",
"\n",
"# Setup Base URL\n",
"# os.envrion[\"LLAMA_CLOUD_BASE_URL\"] = \"https://api.cloud.eu.llamaindex.ai/\" # update if necessay\n",
@@ -99,7 +87,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
"📁 financial_report.pdf already exists\n",
"Downloading financial_report.pdf...\n",
"✅ Downloaded financial_report.pdf\n",
"📁 technical_spec.pdf already exists\n",
"\n",
"📂 Sample documents ready!\n"
@@ -115,7 +104,7 @@
"\n",
"# Download sample documents\n",
"docs_to_download = {\n",
" \"financial_report.pdf\": \"https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/10k/uber_2021.pdf\",\n",
" \"financial_report.pdf\": \"https://raw.githubusercontent.com/run-llama/llama_index/main/docs/examples/data/10k/uber_2021.pdf\",\n",
" \"technical_spec.pdf\": \"https://www.ti.com/lit/ds/symlink/lm317.pdf\",\n",
"}\n",
"\n",
@@ -155,10 +144,10 @@
"output_type": "stream",
"text": [
"🔄 Parsing documents...\n",
"Started parsing the file under job_id 8a8c76f9-354d-4275-91d8-312ff1adc762\n",
"...✅ Parsed financial report (Job ID: 8a8c76f9-354d-4275-91d8-312ff1adc762)\n",
"Started parsing the file under job_id 7e603448-ed80-4d18-948b-6801ed51c41b\n",
"✅ Parsed technical spec (Job ID: 7e603448-ed80-4d18-948b-6801ed51c41b)\n",
"Started parsing the file under job_id 530c187a-bd2d-4eea-b38d-9e5738eab465\n",
".✅ Parsed financial report (Job ID: 530c187a-bd2d-4eea-b38d-9e5738eab465)\n",
"Started parsing the file under job_id a6e27710-776b-4445-8b94-8d75959ff5db\n",
"✅ Parsed technical spec (Job ID: a6e27710-776b-4445-8b94-8d75959ff5db)\n",
"\n",
"📄 Parsing complete!\n"
]
@@ -246,23 +235,23 @@
"\n",
"## 1 Features\n",
"\n",
" Output voltage range:\n",
"- Output voltage range:\n",
" Adjustable: 1.25V to 37V\n",
" Output current: 1.5A\n",
" Line regulation: 0.01%/V (typ)\n",
" Load regulation: 0.1% (typ)\n",
" Internal short-circuit current limiting\n",
" Thermal overload protection\n",
" Output safe-area compensation (new chip)\n",
" PSRR: 80dB at 120Hz for CADJ = 10μF (new chip)\n",
" Packages:\n",
"- Output current: 1.5A\n",
"- Line regulation: 0.01%/V (typ)\n",
"- Load regulation: 0.1% (typ)\n",
"- Internal short-circuit current limiting\n",
"- Thermal overload protection\n",
"- Output safe-area compensation (new chip)\n",
"- PSRR: 80dB at 120Hz for CADJ = 10μF (new chip)\n",
"- Packages:\n",
" 4-pin, SOT-223 (DCY)\n",
" 3-pin, TO-263 (KTT)\n",
" 3-pin, TO-220 (KCS, KCT),\n",
"...\n",
"\n",
"📏 Financial report markdown length: 1348671 characters\n",
"📏 Technical spec markdown length: 90971 characters\n"
"📏 Financial report markdown length: 1338499 characters\n",
"📏 Technical spec markdown length: 92483 characters\n"
]
}
],
@@ -339,6 +328,72 @@
"print(f\"📝 Created {len(classification_rules)} classification rules\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Try Classification Independently\n",
"\n",
"Let's test the classification on one of our parsed documents to see how it works:\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"🔍 Classifying financial document...\n",
" Document length: 1,338,499 characters\n",
"\n",
"✅ Classification Result:\n",
" Type: financial_document\n",
" Confidence: 100.00%\n",
" Reasoning: This document is a Form 10-K, which is an annual report required by the U.S. Securities and Exchange Commission (SEC) for publicly traded companies. It contains financial data, information about the c...\n",
"\n",
"======================================================================\n"
]
}
],
"source": [
"# Let's classify the financial document\n",
"print(\"🔍 Classifying financial document...\")\n",
"print(f\" Document length: {len(financial_markdown):,} characters\\n\")\n",
"\n",
"# Write to temp file for classification\n",
"import tempfile\n",
"from pathlib import Path\n",
"\n",
"with tempfile.NamedTemporaryFile(\n",
" mode=\"w\", suffix=\".md\", delete=False, encoding=\"utf-8\"\n",
") as tmp:\n",
" tmp.write(financial_markdown)\n",
" temp_financial_path = Path(tmp.name)\n",
"\n",
"# Classify the document\n",
"financial_classification = await classify_client.aclassify_file_path(\n",
" rules=classification_rules, file_input_path=str(temp_financial_path)\n",
")\n",
"\n",
"doc_type = financial_classification.items[0].result.type\n",
"confidence = financial_classification.items[0].result.confidence\n",
"reasoning = financial_classification.items[0].result.reasoning\n",
"\n",
"print(f\"✅ Classification Result:\")\n",
"print(f\" Type: {doc_type}\")\n",
"print(f\" Confidence: {confidence:.2%}\")\n",
"print(\n",
" f\" Reasoning: {reasoning[:200]}...\"\n",
" if reasoning and len(reasoning) > 200\n",
" else f\" Reasoning: {reasoning}\"\n",
")\n",
"\n",
"print(\"\\n\" + \"=\" * 70)"
]
},
{
"cell_type": "markdown",
"metadata": {},
@@ -444,9 +499,31 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## Complete Workflow Summary\n",
"## Building the Complete Workflow\n",
"\n",
"Let's create a function that demonstrates the complete workflow:"
"Now that we've seen how parsing works, let's build a complete 3-step workflow (Parse → Classify → Extract) using LlamaIndex Workflows. We'll define the workflow structure here, and you can see it in action below where we also demonstrate the classification and extraction modules independently.\n",
"\n",
"### Install Workflows Package\n",
"\n",
"First, let's install the LlamaIndex workflows package:\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%pip install llama-index-workflows llama-index-utils-workflow"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Define the Workflow\n",
"\n",
"Let's restructure the document processing into a proper LlamaIndex Workflow with separate classification and extraction steps:\n"
]
},
{
@@ -458,7 +535,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"🔧 Workflow function defined!\n"
"🔧 Workflow defined!\n"
]
}
],
@@ -466,81 +543,286 @@
"import tempfile\n",
"from pathlib import Path\n",
"from llama_cloud import ExtractConfig\n",
"from workflows import Workflow, step, Context\n",
"from workflows.events import Event, StartEvent, StopEvent\n",
"\n",
"\n",
"async def complete_document_workflow(markdown_content: str):\n",
"# Define workflow events\n",
"class ParseEvent(Event):\n",
" \"\"\"Event emitted after parsing\"\"\"\n",
"\n",
" file_path: str\n",
" markdown_content: str\n",
" job_id: str\n",
"\n",
"\n",
"class ClassifyEvent(Event):\n",
" \"\"\"Event emitted after classification\"\"\"\n",
"\n",
" markdown_content: str\n",
" temp_path: str\n",
" doc_type: str\n",
" confidence: float\n",
"\n",
"\n",
"class ExtractEvent(Event):\n",
" \"\"\"Event emitted after extraction\"\"\"\n",
"\n",
" doc_type: str\n",
" confidence: float\n",
" extracted_data: dict\n",
" markdown_length: int\n",
" temp_path: str\n",
" markdown_sample: str\n",
"\n",
"\n",
"class DocumentWorkflow(Workflow):\n",
" \"\"\"\n",
" Complete workflow: Parse → Classify → Extract\n",
" Complete document processing workflow: Parse → Classify → Extract\n",
" \"\"\"\n",
" print(f\"🚀 Starting complete workflow\")\n",
" print(\"=\" * 60)\n",
"\n",
" # Step 1: Classify\n",
" print(\"🏷️ Step 2: Classifying document...\")\n",
" def __init__(\n",
" self,\n",
" parser,\n",
" classify_client,\n",
" classification_rules,\n",
" llama_extract,\n",
" financial_schema,\n",
" technical_schema,\n",
" **kwargs,\n",
" ):\n",
" super().__init__(**kwargs)\n",
" self.parser = parser\n",
" self.classify_client = classify_client\n",
" self.classification_rules = classification_rules\n",
" self.llama_extract = llama_extract\n",
" self.financial_schema = financial_schema\n",
" self.technical_schema = technical_schema\n",
"\n",
" with tempfile.NamedTemporaryFile(\n",
" mode=\"w\", suffix=\".md\", delete=False, encoding=\"utf-8\"\n",
" ) as tmp:\n",
" tmp.write(markdown_content)\n",
" temp_path = Path(tmp.name)\n",
" @step\n",
" async def parse_document(self, ctx: Context, ev: StartEvent) -> ParseEvent:\n",
" \"\"\"\n",
" Step 1: Parse the document to extract markdown\n",
" \"\"\"\n",
" file_path = ev.file_path\n",
" print(f\"📄 Step 1: Parsing document: {file_path}...\")\n",
"\n",
" print(temp_path)\n",
" # Parse the document\n",
" parse_result = await self.parser.aparse(file_path)\n",
" markdown_content = await parse_result.aget_markdown()\n",
" job_id = parse_result.job_id\n",
"\n",
" classification = await classify_client.aclassify_file_path(\n",
" rules=classification_rules, file_input_path=str(temp_path)\n",
" )\n",
" doc_type = classification.items[0].result.type\n",
" confidence = classification.items[0].result.confidence\n",
" print(f\" ✅ Classified as: {doc_type} (confidence: {confidence:.2f})\")\n",
" print(f\" ✅ Parsed successfully (Job ID: {job_id})\")\n",
" print(f\" 📝 Extracted {len(markdown_content):,} characters\")\n",
"\n",
" # Step 2: Extract based on classification\n",
" print(\"🔍 Step 3: Extracting structured data using SourceText...\")\n",
" source_text = SourceText(\n",
" text_content=markdown_content,\n",
" filename=f\"{os.path.basename(temp_path)}_markdown.md\",\n",
" )\n",
" # Write event to stream for monitoring\n",
" parse_event = ParseEvent(\n",
" file_path=file_path,\n",
" markdown_content=markdown_content,\n",
" job_id=job_id,\n",
" )\n",
" ctx.write_event_to_stream(parse_event)\n",
"\n",
" # Choose schema based on classification\n",
" if \"financial\" in doc_type.lower():\n",
" schema = FinancialMetrics\n",
" print(\" 📊 Using FinancialMetrics schema\")\n",
" elif \"technical\" in doc_type.lower():\n",
" schema = TechnicalSpec\n",
" print(\" 🔧 Using TechnicalSpec schema\")\n",
" else:\n",
" schema = FinancialMetrics # Default fallback\n",
" print(\" 📊 Using default FinancialMetrics schema\")\n",
" return parse_event\n",
"\n",
" extract_config = ExtractConfig(\n",
" extraction_mode=\"BALANCED\",\n",
" )\n",
" @step\n",
" async def classify_document(self, ctx: Context, ev: ParseEvent) -> ClassifyEvent:\n",
" \"\"\"\n",
" Step 2: Classify the document based on its content\n",
" \"\"\"\n",
" markdown_content = ev.markdown_content\n",
" print(\"🏷️ Step 2: Classifying document...\")\n",
"\n",
" extraction_result = llama_extract.extract(\n",
" data_schema=schema, config=extract_config, files=source_text\n",
" )\n",
" # Write markdown to temp file for classification\n",
" with tempfile.NamedTemporaryFile(\n",
" mode=\"w\", suffix=\".md\", delete=False, encoding=\"utf-8\"\n",
" ) as tmp:\n",
" tmp.write(markdown_content)\n",
" temp_path = Path(tmp.name)\n",
"\n",
" print(\" ✅ Extraction complete!\")\n",
" # Classify the document\n",
" classification = await self.classify_client.aclassify_file_path(\n",
" rules=self.classification_rules, file_input_path=str(temp_path)\n",
" )\n",
" doc_type = classification.items[0].result.type\n",
" confidence = classification.items[0].result.confidence\n",
"\n",
" return {\n",
" \"file_path\": temp_path,\n",
" \"markdown_length\": len(markdown_content),\n",
" \"classification\": doc_type,\n",
" \"confidence\": confidence,\n",
" \"extracted_data\": extraction_result.data,\n",
" \"markdown_sample\": markdown_content[:200] + \"...\"\n",
" if len(markdown_content) > 200\n",
" else markdown_content,\n",
" }\n",
" print(f\" ✅ Classified as: {doc_type} (confidence: {confidence:.2f})\")\n",
"\n",
" # Write event to stream for monitoring\n",
" classify_event = ClassifyEvent(\n",
" markdown_content=markdown_content,\n",
" temp_path=str(temp_path),\n",
" doc_type=doc_type,\n",
" confidence=confidence,\n",
" )\n",
" ctx.write_event_to_stream(classify_event)\n",
"\n",
" return classify_event\n",
"\n",
" @step\n",
" async def extract_data(self, ctx: Context, ev: ClassifyEvent) -> ExtractEvent:\n",
" \"\"\"\n",
" Step 3: Extract structured data based on classification\n",
" \"\"\"\n",
" print(\"🔍 Step 3: Extracting structured data using SourceText...\")\n",
"\n",
" # Choose schema based on classification\n",
" if \"financial\" in ev.doc_type.lower():\n",
" schema = self.financial_schema\n",
" print(\" 📊 Using FinancialMetrics schema\")\n",
" elif \"technical\" in ev.doc_type.lower():\n",
" schema = self.technical_schema\n",
" print(\" 🔧 Using TechnicalSpec schema\")\n",
" else:\n",
" schema = self.financial_schema # Default fallback\n",
" print(\" 📊 Using default FinancialMetrics schema\")\n",
"\n",
" # Create SourceText from markdown content\n",
" source_text = SourceText(\n",
" text_content=ev.markdown_content,\n",
" filename=f\"{os.path.basename(ev.temp_path)}_markdown.md\",\n",
" )\n",
"\n",
" # Configure extraction\n",
" extract_config = ExtractConfig(\n",
" extraction_mode=\"BALANCED\",\n",
" )\n",
"\n",
" # Perform extraction\n",
" extraction_result = self.llama_extract.extract(\n",
" data_schema=schema, config=extract_config, files=source_text\n",
" )\n",
"\n",
" print(\" ✅ Extraction complete!\")\n",
"\n",
" # Create markdown sample\n",
" markdown_sample = (\n",
" ev.markdown_content[:200] + \"...\"\n",
" if len(ev.markdown_content) > 200\n",
" else ev.markdown_content\n",
" )\n",
"\n",
" extract_event = ExtractEvent(\n",
" doc_type=ev.doc_type,\n",
" confidence=ev.confidence,\n",
" extracted_data=extraction_result.data,\n",
" markdown_length=len(ev.markdown_content),\n",
" temp_path=ev.temp_path,\n",
" markdown_sample=markdown_sample,\n",
" )\n",
" ctx.write_event_to_stream(extract_event)\n",
"\n",
" return extract_event\n",
"\n",
" @step\n",
" async def finalize_results(self, ctx: Context, ev: ExtractEvent) -> StopEvent:\n",
" \"\"\"\n",
" Step 4: Finalize and return results\n",
" \"\"\"\n",
" result = {\n",
" \"file_path\": ev.temp_path,\n",
" \"markdown_length\": ev.markdown_length,\n",
" \"classification\": ev.doc_type,\n",
" \"confidence\": ev.confidence,\n",
" \"extracted_data\": ev.extracted_data,\n",
" \"markdown_sample\": ev.markdown_sample,\n",
" }\n",
"\n",
" return StopEvent(result=result)\n",
"\n",
"\n",
"print(\"🔧 Workflow function defined!\")"
"print(\"🔧 Workflow defined!\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Run Complete Workflow on Both Documents"
"### Workflow Structure\n",
"\n",
"The workflow consists of four steps connected by typed events:\n",
"\n",
"```\n",
"┌─────────────┐\n",
"│ StartEvent │ (file_path)\n",
"└──────┬──────┘\n",
" │\n",
" ▼\n",
"┌──────────────────┐\n",
"│ parse_document │ Step 1: Parse PDF to markdown\n",
"└──────┬───────────┘\n",
" │\n",
" ▼\n",
"┌─────────────┐\n",
"│ ParseEvent │ (markdown_content, job_id)\n",
"└──────┬──────┘\n",
" │\n",
" ▼\n",
"┌─────────────────────┐\n",
"│ classify_document │ Step 2: Classification\n",
"└──────┬──────────────┘\n",
" │\n",
" ▼\n",
"┌──────────────┐\n",
"│ ClassifyEvent│ (doc_type, confidence, markdown_content)\n",
"└──────┬───────┘\n",
" │\n",
" ▼\n",
"┌──────────────┐\n",
"│ extract_data │ Step 3: Extraction with schema selection\n",
"└──────┬───────┘\n",
" │\n",
" ▼\n",
"┌──────────────┐\n",
"│ ExtractEvent │ (extracted_data, doc_type, confidence)\n",
"└──────┬───────┘\n",
" │\n",
" ▼\n",
"┌──────────────────┐\n",
"│ finalize_results │ Step 4: Format and return results\n",
"└──────┬───────────┘\n",
" │\n",
" ▼\n",
"┌─────────────┐\n",
"│ StopEvent │ (final result dictionary)\n",
"└─────────────┘\n",
"```\n",
"\n",
"**Key Features:**\n",
"- **Step 1 (parse_document)**: Takes a file path and parses the document into clean markdown\n",
"- **Step 2 (classify_document)**: Takes markdown content and classifies it into document types\n",
"- **Step 3 (extract_data)**: Selects appropriate schema based on classification and extracts structured data\n",
"- **Step 4 (finalize_results)**: Packages all results into final output format\n",
"- Events are written to the stream for real-time monitoring\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Visualize the Workflow\n",
"\n",
"Let's visualize the workflow structure to see the flow of events:\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Initialize the workflow\n",
"workflow = DocumentWorkflow(\n",
" parser=parser,\n",
" classify_client=classify_client,\n",
" classification_rules=classification_rules,\n",
" llama_extract=llama_extract,\n",
" financial_schema=FinancialMetrics,\n",
" technical_schema=TechnicalSpec,\n",
" timeout=300,\n",
" verbose=True,\n",
")"
]
},
{
@@ -552,53 +834,173 @@
"name": "stdout",
"output_type": "stream",
"text": [
"🚀 Starting complete workflow\n",
"============================================================\n",
"🏷️ Step 2: Classifying document...\n",
"/var/folders/g6/4b5lpp5974gcpr890ybhbw4r0000gn/T/tmpos3b62tm.md\n",
" ✅ Classified as: financial_document (confidence: 1.00)\n",
"🔍 Step 3: Extracting structured data using SourceText...\n",
" 📊 Using FinancialMetrics schema\n",
".. ✅ Extraction complete!\n",
"\n",
"============================================================\n",
"\n",
"🚀 Starting complete workflow\n",
"============================================================\n",
"🏷️ Step 2: Classifying document...\n",
"/var/folders/g6/4b5lpp5974gcpr890ybhbw4r0000gn/T/tmpppz9ub_m.md\n",
" ✅ Classified as: technical_specification (confidence: 1.00)\n",
"🔍 Step 3: Extracting structured data using SourceText...\n",
" 🔧 Using TechnicalSpec schema\n",
" ✅ Extraction complete!\n",
"\n",
"============================================================\n",
"\n",
"📋 Processed 2 documents successfully!\n"
"document_workflow.html\n"
]
}
],
"source": [
"# Process both documents through the complete workflow\n",
"results = []\n",
"# Draw the workflow visualization\n",
"from llama_index.utils.workflow import draw_all_possible_flows\n",
"\n",
"for doc_text in document_texts:\n",
" try:\n",
" result = await complete_document_workflow(doc_text)\n",
" results.append(result)\n",
" print(\"\\n\" + \"=\" * 60 + \"\\n\")\n",
" except Exception as e:\n",
" print(f\"❌ Error processing {doc_path}: {str(e)}\")\n",
" print(\"\\n\" + \"=\" * 60 + \"\\n\")\n",
"\n",
"print(f\"📋 Processed {len(results)} documents successfully!\")"
"draw_all_possible_flows(\n",
" workflow,\n",
" filename=\"document_workflow.html\",\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Final Results Summary"
"The workflow has been visualized and saved to `document_workflow.html`. You can open this file in a browser to see the interactive workflow diagram.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The workflow visualization shows:\n",
"1. **StartEvent** → **parse_document** step\n",
"2. **ParseEvent** → **classify_document** step\n",
"3. **ClassifyEvent** → **extract_data** step \n",
"4. **ExtractEvent** → **finalize_results** step\n",
"5. **StopEvent** (final output)\n",
"\n",
"Each step is connected by typed events, allowing for clean separation of concerns and easy monitoring of the workflow execution.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Run the Workflow on Both Documents\n",
"\n",
"Now let's run the workflow on both documents and monitor the events:\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"======================================================================\n",
"🚀 Processing Document 1: sample_docs/financial_report.pdf\n",
"======================================================================\n",
"\n",
"Running step parse_document\n",
"📄 Step 1: Parsing document: sample_docs/financial_report.pdf...\n",
"Started parsing the file under job_id bb53c6bf-79cc-4f63-9c97-16983d59f29d\n",
". ✅ Parsed successfully (Job ID: bb53c6bf-79cc-4f63-9c97-16983d59f29d)\n",
" 📝 Extracted 1,338,499 characters\n",
"Step parse_document produced event ParseEvent\n",
"📄 Parse Event: Extracted 1,338,499 characters\n",
"Running step classify_document\n",
"🏷️ Step 2: Classifying document...\n",
" ✅ Classified as: financial_document (confidence: 1.00)\n",
"Step classify_document produced event ClassifyEvent\n",
"📊 Classification Event: financial_document (1.00)\n",
"Running step extract_data\n",
"🔍 Step 3: Extracting structured data using SourceText...\n",
" 📊 Using FinancialMetrics schema\n",
".. ✅ Extraction complete!\n",
"Step extract_data produced event ExtractEvent\n",
"Running step finalize_results\n",
"Step finalize_results produced event StopEvent\n",
"✅ Extraction Event: 7 fields extracted\n",
"\n",
"✅ Document 1 processed successfully!\n",
"\n",
"======================================================================\n",
"🚀 Processing Document 2: sample_docs/technical_spec.pdf\n",
"======================================================================\n",
"\n",
"Running step parse_document\n",
"📄 Step 1: Parsing document: sample_docs/technical_spec.pdf...\n",
"Started parsing the file under job_id 944905c1-3c49-431a-ad86-4436d16f3d1c\n",
" ✅ Parsed successfully (Job ID: 944905c1-3c49-431a-ad86-4436d16f3d1c)\n",
" 📝 Extracted 92,483 characters\n",
"Step parse_document produced event ParseEvent\n",
"📄 Parse Event: Extracted 92,483 characters\n",
"Running step classify_document\n",
"🏷️ Step 2: Classifying document...\n",
" ✅ Classified as: technical_specification (confidence: 1.00)\n",
"Step classify_document produced event ClassifyEvent\n",
"📊 Classification Event: technical_specification (1.00)\n",
"Running step extract_data\n",
"🔍 Step 3: Extracting structured data using SourceText...\n",
" 🔧 Using TechnicalSpec schema\n",
" ✅ Extraction complete!\n",
"Step extract_data produced event ExtractEvent\n",
"Running step finalize_results\n",
"Step finalize_results produced event StopEvent\n",
"✅ Extraction Event: 8 fields extracted\n",
"\n",
"✅ Document 2 processed successfully!\n",
"\n",
"\n",
"📋 Processed 2 documents successfully!\n"
]
}
],
"source": [
"# Process both documents through the workflow\n",
"results = []\n",
"\n",
"# Define the document files to process\n",
"document_files = [\n",
" \"sample_docs/financial_report.pdf\",\n",
" \"sample_docs/technical_spec.pdf\",\n",
"]\n",
"\n",
"for i, file_path in enumerate(document_files, 1):\n",
" print(f\"\\n{'='*70}\")\n",
" print(f\"🚀 Processing Document {i}: {file_path}\")\n",
" print(f\"{'='*70}\\n\")\n",
"\n",
" try:\n",
" # Run the workflow\n",
" handler = workflow.run(file_path=file_path)\n",
"\n",
" # Monitor events as they are emitted\n",
" async for event in handler.stream_events():\n",
" if isinstance(event, ParseEvent):\n",
" print(\n",
" f\"📄 Parse Event: Extracted {len(event.markdown_content):,} characters\"\n",
" )\n",
" elif isinstance(event, ClassifyEvent):\n",
" print(\n",
" f\"📊 Classification Event: {event.doc_type} ({event.confidence:.2f})\"\n",
" )\n",
" elif isinstance(event, ExtractEvent):\n",
" print(\n",
" f\"✅ Extraction Event: {len(event.extracted_data)} fields extracted\"\n",
" )\n",
"\n",
" # Get final result\n",
" result = await handler\n",
" results.append(result)\n",
"\n",
" print(f\"\\n✅ Document {i} processed successfully!\")\n",
"\n",
" except Exception as e:\n",
" print(f\"❌ Error processing document {i}: {str(e)}\")\n",
" import traceback\n",
"\n",
" traceback.print_exc()\n",
"\n",
"print(f\"\\n\\n📋 Processed {len(results)} documents successfully!\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Final Results Summary\n"
]
},
{
@@ -613,9 +1015,9 @@
"📈 COMPLETE WORKFLOW RESULTS SUMMARY\n",
"======================================================================\n",
"\n",
"📄 Document 1: tmpos3b62tm.md\n",
"📄 Document 1: tmpuyxzpd3x.md\n",
" 📊 Classification: financial_document (confidence: 1.00)\n",
" 📝 Markdown length: 1,348,671 characters\n",
" 📝 Markdown length: 1,338,499 characters\n",
" 📋 Markdown sample: \n",
"\n",
"# UNITED STATES\n",
@@ -629,14 +1031,14 @@
" • company_name: Uber Technologies, Inc.\n",
" • document_type: Annual Report on Form 10-K\n",
" • fiscal_year: 2021\n",
" • revenue_2021: $21,764\n",
" • net_income_2021: $(496)\n",
" • key_business_segments: ['Mobility', 'Delivery', 'Freight', 'All Other (including former New Mobility, e-bikes, e-scooters, Advanced Technologies Group and other technology programs)']\n",
" • risk_factors: [\"The company faces numerous risk factors across its business operations and environment. The COVID-19 pandemic and related mitigation measures have adversely affected parts of the business, including reduced demand for Mobility offerings and creating ongoing uncertainties. The company's operational and financial performance is influenced by competitive pressure in the mobility, delivery, and logistics industries, characterized by well-established alternatives, low barriers to entry, and low switching costs. Driver classification risks exist if Drivers are deemed employees, workers, or quasi-employees rather than independent contractors, exposing the company to legal actions and financial liabilities globally. Competition challenges require the company to sometimes lower fares, offer incentives, and promotions, which impacts profitability. There are significant operating losses historically with substantial future operating expense increases anticipated, and the ability to achieve or maintain profitability is uncertain. Network value depends on maintaining critical mass among Drivers, consumers, merchants, shippers, and carriers, and failures to do so diminish platform attractiveness. Brand and reputation maintenance is critical, with exposure to negative publicity, media coverage, and risks from associated companies' brands or licensed brands in joint ventures.\\n\\nOperational risks include historical workplace culture and compliance challenges, management complexity due to rapid growth, technological infrastructure issues potentially causing disruptions or poor user experience, and security or data privacy breaches that could impact revenue and reputation. Platform users may engage in or be subjected to criminal, violent, or dangerous activity leading to safety incidents and legal actions. New offerings and technologies investments are inherently risky without guaranteed benefits. Economic conditions, inflation, and increased costs (fuel, food, labor, energy) may negatively impact results. Regulatory risks are extensive and global, involving payment and financial services compliance, licensing, anti-money laundering laws, data privacy (GDPR, CCPA, LGPD), and labor laws. Legal and regulatory investigations and inquiries, including antitrust, FCPA, labor classification, data protection, and intellectual property matters, pose risks of fines, penalties, operational changes, and increased costs.\\n\\nGeopolitical and jurisdictional risks include operating limitations or bans in some locations, currency exchange risk, and complex evolving regulations with the potential for fines and loss of licenses or permits. Insurance risks include potential inadequacy of reserves, liability exposure from accidents or impersonation, and insurer insolvency. Driver qualification requirements and background checks may increase costs or fail to expose all relevant information, with associated insurance cost risks and potential for courtroom or regulatory challenges to pricing models.\\n\\nFinancial risks comprise significant accumulated deficits, requirement for additional capital with uncertain availability, debt obligations, tax exposure including uncertain positions and observed changes in tax laws, and volatility in common stock price with no expected cash dividends. Accounting judgments and estimates involve critical assumptions affecting reported financial metrics related to goodwill, revenue recognition, incentive accruals, and stock-based compensation. Cybersecurity risks include exposures to malware, ransomware, phishing, and other cyberattacks. Climate change presents physical and transitional risks that may impact operations and costs, and failure to meet climate commitments may have operational and reputational consequences.\\n\\nOther risks include potential liability under anti-corruption and anti-terrorism laws, adverse effects from defaults under debt agreements, limitations in takeover actions due to corporate governance provisions, and the impact of non-GAAP financial measure limitations. Overall, these diverse and interconnected risk factors contribute to significant uncertainty regarding the company's future business prospects, operating results, and financial condition.\"]\n",
" • revenue_2021: $17,455 and $21,764\n",
" • net_income_2021: $(496) to (700)\n",
" • key_business_segments: ['Borrower and the Restricted Subsidiaries', 'Holdings', 'Guarantors', 'Material Domestic Subsidiaries', 'Material Foreign Subsidiaries']\n",
" • risk_factors: ['Indemnification obligations of the borrower for losses, claims, damages, liabilities, and out-of-pocket expenses incurred by agents, lenders, arrangers, and related parties in connection with the agreement or loans, except in certain cases such as gross negligence, bad faith, willful misconduct, or material breach by the indemnitee.', \"Borrower not required to indemnify any indemnitee for settlements entered into without the borrower's consent.\", 'Limitation of liability for special, indirect, consequential, or punitive damages, and for damages from unauthorized use of information, except for direct damages resulting from gross negligence, bad faith, or willful misconduct.', 'Obligation of the borrower to indemnify the administrative agent for liabilities arising from performance of duties, except in cases of gross negligence, bad faith, or willful misconduct.', 'Limitations and conditions on assignments and participations of lender rights, including restrictions on assignments to disqualified institutions, loan parties, affiliates of loan parties, defaulting lenders, and natural persons.', 'Setoff rights for lenders and issuing banks after an event of default, allowing them to apply borrower deposits toward obligations under the agreement.', 'Potential for increased obligations under the agreement as a result of changes in law affecting payment terms.', 'Requirement for the borrower and guarantors to provide information to comply with anti-money laundering rules and the USA PATRIOT Act.']\n",
"\n",
"📄 Document 2: tmpppz9ub_m.md\n",
"📄 Document 2: tmp7ower2xm.md\n",
" 📊 Classification: technical_specification (confidence: 1.00)\n",
" 📝 Markdown length: 90,971 characters\n",
" 📝 Markdown length: 92,483 characters\n",
" 📋 Markdown sample: \n",
"\n",
"LM317\n",
@@ -648,20 +1050,14 @@
" 🎯 Extracted fields: 8 fields\n",
" • component_name: LM317\n",
" • manufacturer: Texas Instruments\n",
" • part_number: LM317\n",
" • description: The LM317 is an adjustable three-pin, positive-voltage regulator capable of supplying up to 1.5A over an output voltage range of 1.25V to 37V. It features line and load regulation, internal current limiting, thermal overload protection, and safe operating area compensation.\n",
" • part_number: LM317, SLVS044Z\n",
" • description: The LM317 is an adjustable three-pin, positive-voltage regulator capable of supplying more than 1.5A (typically up to 1.5A) over an output voltage range of 1.25V to 37V. The device requires only two external resistors to set the output voltage. It features a typical line regulation of 0.01% and typical load regulation of 0.1%. The LM317 includes current limiting, thermal overload protection, and safe operating area protection. Overload protection remains functional even if the ADJUST pin is disconnected. The regulator is used in applications such as constant-current battery-charger circuits, slow turn-on 15V regulator circuits, AC voltage-regulator circuits, current-limited charger circuits, and high-current and adjustable regulator circuits. It is available in packages including SOT-223 (DCY), TO-220 (KCS), and TO-263 (KTT).\n",
" • operating_voltage: {'min_voltage': 1.25, 'max_voltage': 37.0, 'unit': 'V'}\n",
" • maximum_current: 1.5\n",
" • key_features: ['Adjustable output voltage: 1.25V to 37V', 'Output current up to 1.5A', 'Line regulation: 0.01%/V (typical)', 'Load regulation: 0.1% (typical)', 'Internal short-circuit current limiting', 'Thermal overload protection', 'Output safe-area compensation', 'High power supply rejection ratio (PSRR): 80dB at 120Hz (new chip)', 'Available in SOT-223, TO-263, and TO-220 packages']\n",
" • applications: ['Multifunction printers', 'AC drive power stage modules', 'Electricity meters', 'Servo drive control modules', 'Merchant network and server power supply units']\n",
" • maximum_current: 4.0\n",
" • key_features: ['Adjustable output voltage range: 1.25V to 37V', 'Output current up to 1.5A (up to 4A with external pass elements)', 'Line regulation: typically 0.01%/V', 'Load regulation: typically 0.1%', 'Internal short-circuit current limiting / Current limiting', 'Thermal overload protection / Thermal shutdown', 'Output safe-area compensation / Safe operating area protection', 'PSRR: 80dB at 120Hz for CADJ = 10μF (new chip)', 'NPN Darlington output drive', 'Programmable feedback', 'Multiple package options (SOT-223, TO-220, TO-263)', 'Can be used in constant-current, battery-charging, and regulator applications']\n",
" • applications: ['Multifunction printers, AC drive power stage modules, Electricity meters, Servo drive control modules, Merchant network and server PSU, Adjustable voltage regulator, 0V to 30V regulator circuit, Regulator circuit with improved ripple rejection, Precision current-limiter, Tracking preregulator, 1.25V to 20V regulator, Battery charger circuit, Constant-current battery charger circuits, Slow turn-on regulator, AC voltage-regulator, Current-limited charger circuits, High-current adjustable regulator circuits, General-purpose adjustable power supply']\n",
"\n",
"✨ Workflow completed successfully!\n",
"\n",
"📚 Key Learnings:\n",
" • Parse: Converted documents to clean markdown format\n",
" • Classify: Automatically categorized document types\n",
" • Extract: Used SourceText with markdown for structured data extraction\n",
" • The markdown content provides much better context for extraction than raw PDFs\n"
"✨ Workflow completed successfully!\n"
]
}
],
@@ -683,14 +1079,7 @@
" for key, value in extracted.items():\n",
" print(f\" • {key}: {value}\")\n",
"\n",
"print(\"\\n✨ Workflow completed successfully!\")\n",
"print(\"\\n📚 Key Learnings:\")\n",
"print(\" • Parse: Converted documents to clean markdown format\")\n",
"print(\" • Classify: Automatically categorized document types\")\n",
"print(\" • Extract: Used SourceText with markdown for structured data extraction\")\n",
"print(\n",
" \" • The markdown content provides much better context for extraction than raw PDFs\"\n",
")"
"print(\"\\n✨ Workflow completed successfully!\")"
]
},
{
@@ -699,9 +1088,9 @@
"source": [
"## Conclusion\n",
"\n",
"This notebook demonstrated the complete **Parse → Classify → Extract** workflow using LlamaCloud services:\n",
"The notebook shows you how to build an e2e document **Classify → Extract** workflow using LlamaCloud. This uses some of our core building blocks around **classification** interleaved with **document extraction**.\n",
"\n",
"### Key Components:\n",
"### Main Components:\n",
"\n",
"1. **LlamaParse** (`llama_cloud_services.parse.base.LlamaParse`):\n",
" - Converts documents to clean, structured markdown\n",
@@ -715,38 +1104,17 @@
"\n",
"3. **LlamaExtract with SourceText** (`llama_cloud_services.extract.extract.LlamaExtract`, `SourceText`):\n",
" - Extracts structured data using custom Pydantic schemas\n",
" - **SourceText** allows using markdown content as input instead of raw files\n",
" - Provides much better extraction accuracy when using processed markdown\n",
" - You can either feed in the file directly (in which case parsing will happen under the hood), or the parsed text through the **SourceText** object (which is the case in this example) \n",
"\n",
"### Workflow Benefits:\n",
"\n",
"- **Better Accuracy**: Using markdown from parsing provides cleaner, more structured input for extraction\n",
"- **Automatic Routing**: Classification allows different processing logic for different document types\n",
"- **Structured Output**: Custom schemas ensure consistent, structured data extraction\n",
"- **Flexible Input**: SourceText supports text content, file paths, and bytes\n",
"\n",
"### Key Insights:\n",
"\n",
"1. **SourceText is the bridge**: It allows you to pass the clean markdown content from parsing directly to extraction\n",
"2. **Markdown improves extraction**: Pre-processed markdown provides much better context than raw PDFs\n",
"3. **Classification enables smart routing**: Different document types can use different extraction schemas\n",
"4. **End-to-end automation**: The entire workflow can be automated for production use\n",
"\n",
"This approach is ideal for production document processing pipelines where you need to:\n",
"- Process various document types automatically\n",
"- Extract structured data consistently\n",
"- Maintain high accuracy and reliability\n",
"- Handle documents at scale\n",
"\n",
"The combination of these three services provides a powerful, flexible document processing pipeline that can handle complex, real-world document processing requirements."
"**Benefits of an e2e workflow**: The main benefit of doing Classify -> Extract, instead of only Extract, is the fact that you can handle documents of different types/different expected schemas within the same workflow, without having to separate out the data before and running separate extractions on each data subset. "
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "llama_parse",
"language": "python",
"name": "python3"
"name": "llama_parse"
},
"language_info": {
"codemirror_mode": {
+19
View File
@@ -1,5 +1,24 @@
# llama-cloud-services-py
## 0.6.76
### Patch Changes
- 4f24f53: Add aggressive_table_extraction flag in python sdk
## 0.6.75
### Patch Changes
- f81532e: Safest types possible for parse
## 0.6.74
### Patch Changes
- 1bf5223: Fix default bbox values
- 24166dc: Now only escape single dollar signs - preserve double for latex equations
## 0.6.73
### Patch Changes
+7
View File
@@ -188,6 +188,10 @@ class LlamaParse(BasePydanticReader):
default=False,
description="If set to true, LlamaParse will try to detect long table and adapt the output.",
)
aggressive_table_extraction: Optional[bool] = Field(
default=False,
description="If set to true, LlamaParse will try to extract tables aggressively, may lead to false positives.",
)
annotate_links: Optional[bool] = Field(
default=False,
description="Annotate links found in the document to extract their URL.",
@@ -713,6 +717,9 @@ class LlamaParse(BasePydanticReader):
if self.adaptive_long_table:
data["adaptive_long_table"] = self.adaptive_long_table
if self.aggressive_table_extraction:
data["aggressive_table_extraction"] = self.aggressive_table_extraction
if self.annotate_links:
data["annotate_links"] = self.annotate_links
+116 -26
View File
@@ -1,8 +1,8 @@
import httpx
import os
import re
from pydantic import BaseModel, Field, SerializeAsAny
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny, model_validator
from typing import Dict, Any, List, Optional, get_origin, get_args
from llama_cloud_services.parse.utils import (
make_api_request,
@@ -13,8 +13,75 @@ from llama_index.core.schema import Document, ImageDocument, ImageNode, TextNode
PAGE_REGEX = r"page[-_](\d+)\.jpg$"
SAFE_MODEL_CONFIGS = ConfigDict(
extra="allow",
validate_assignment=False,
arbitrary_types_allowed=True,
validate_default=False,
)
class JobMetadata(BaseModel):
class SafeBaseModel(BaseModel):
"""Base model that gracefully handles None values from unstable backend responses."""
model_config = SAFE_MODEL_CONFIGS
@model_validator(mode="before")
@classmethod
def coerce_none_to_defaults(cls, data: Any) -> Any:
"""
Replace None values with appropriate defaults based on field type annotations.
This prevents validation errors when the backend returns None for non-optional fields.
"""
if not isinstance(data, dict):
return data
# Process each field that has a None value
result = {}
for key, value in data.items():
if value is not None or key not in cls.model_fields:
result[key] = value
continue
# Value is None and field exists in model
field_info = cls.model_fields[key]
# If field has a default or default_factory, let Pydantic handle it
from pydantic_core import PydanticUndefined
if (
field_info.default is not PydanticUndefined
or field_info.default_factory is not None
):
continue
# Otherwise, provide a sensible default based on the type annotation
annotation = field_info.annotation
origin = get_origin(annotation)
# Handle List types
if origin is list:
result[key] = []
# Handle Dict types
elif origin is dict:
result[key] = {}
# Handle basic types
elif annotation == str or (origin and str in get_args(annotation)):
result[key] = ""
elif annotation == int or (origin and int in get_args(annotation)):
result[key] = 0
elif annotation == float or (origin and float in get_args(annotation)):
result[key] = 0.0
elif annotation == bool or (origin and bool in get_args(annotation)):
result[key] = False
# If we can't determine a safe default, skip (let Pydantic try)
else:
result[key] = value
return result
class JobMetadata(SafeBaseModel):
"""Metadata about the job."""
job_pages: int = Field(default=0, description="The number of pages in the job.")
@@ -27,19 +94,31 @@ class JobMetadata(BaseModel):
)
class BBox(BaseModel):
class BBox(SafeBaseModel):
"""A bounding box."""
x: float = Field(description="The x-coordinate of the bounding box.")
y: float = Field(description="The y-coordinate of the bounding box.")
w: float = Field(description="The width of the bounding box.")
h: float = Field(description="The height of the bounding box.")
x: Optional[float] = Field(
default=None,
description="The x-coordinate of the bounding box.",
)
y: Optional[float] = Field(
default=None,
description="The y-coordinate of the bounding box.",
)
w: Optional[float] = Field(
default=None,
description="The width of the bounding box.",
)
h: Optional[float] = Field(
default=None,
description="The height of the bounding box.",
)
class PageItem(BaseModel):
class PageItem(SafeBaseModel):
"""An item in a page."""
type: str = Field(description="The type of the item.")
type: str = Field(default="", description="The type of the item.")
lvl: Optional[int] = Field(
default=None, description="The level of indentation of the item."
)
@@ -61,10 +140,10 @@ class PageItem(BaseModel):
)
class ImageItem(BaseModel):
class ImageItem(SafeBaseModel):
"""An image in a page."""
name: str = Field(description="The name of the image.")
name: str = Field(default="", description="The name of the image.")
height: Optional[float] = Field(
default=None, description="The height of the image."
)
@@ -84,22 +163,28 @@ class ImageItem(BaseModel):
type: Optional[str] = Field(default=None, description="The type of the image.")
class LayoutItem(BaseModel):
class LayoutItem(SafeBaseModel):
"""The layout of a page."""
image: str = Field(description="The name of the image containing the layout item")
confidence: float = Field(description="The confidence of the layout item.")
label: str = Field(description="The label of the layout item.")
image: str = Field(
default="", description="The name of the image containing the layout item"
)
confidence: float = Field(
default=0.0, description="The confidence of the layout item."
)
label: str = Field(default="", description="The label of the layout item.")
bbox: Optional[BBox] = Field(
default=None, description="The bounding box of the layout item."
)
isLikelyNoise: bool = Field(description="Whether the layout item is likely noise.")
isLikelyNoise: bool = Field(
default=False, description="Whether the layout item is likely noise."
)
class ChartItem(BaseModel):
class ChartItem(SafeBaseModel):
"""A chart in a page."""
name: str = Field(description="The name of the chart.")
name: str = Field(default="", description="The name of the chart.")
x: Optional[float] = Field(
default=None, description="The x-coordinate of the chart."
)
@@ -112,7 +197,7 @@ class ChartItem(BaseModel):
)
class Page(BaseModel):
class Page(SafeBaseModel):
"""A page of the document."""
page: int = Field(default=0, description="The page number.")
@@ -167,7 +252,7 @@ class Page(BaseModel):
)
class JobResult(BaseModel):
class JobResult(SafeBaseModel):
"""The raw JSON result from the LlamaParse API."""
pages: List[Page] = Field(
@@ -266,18 +351,23 @@ class JobResult(BaseModel):
if text is None:
return None
def escape_dollar_signs(text: str) -> str:
"""Escape dollar signs in text to prevent Jupyter from interpreting them as LaTeX.
def escape_single_dollar_signs(text: str) -> str:
"""Escape single dollar signs in text to prevent Jupyter from interpreting them as LaTeX.
Preserves all strings of dollar signs greater than length 1,
especially preserving double dollar signs ($$) which denote LaTeX equations.
Args:
text: The text to escape
Returns:
Text with dollar signs escaped
Text with single dollar signs escaped
"""
return text.replace("$", r"\$")
# Replace single $ with \$, but preserve $$
# Use negative lookahead and lookbehind to match $ not preceded or followed by $
return re.sub(r"(?<!\$)\$(?!\$)", r"\$", text)
return escape_dollar_signs(text)
return escape_single_dollar_signs(text)
def get_markdown_documents(self, split_by_page: bool = False) -> List[Document]:
"""
+22
View File
@@ -1,5 +1,27 @@
# llama_parse
## 0.6.76
### Patch Changes
- Updated dependencies [4f24f53]
- llama-cloud-services-py@0.6.76
## 0.6.75
### Patch Changes
- Updated dependencies [f81532e]
- llama-cloud-services-py@0.6.75
## 0.6.74
### Patch Changes
- Updated dependencies [1bf5223]
- Updated dependencies [24166dc]
- llama-cloud-services-py@0.6.74
## 0.6.73
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama_parse",
"version": "0.6.73",
"version": "0.6.76",
"description": "",
"main": "index.js",
"private": false,
+2 -2
View File
@@ -11,13 +11,13 @@ dev = [
[project]
name = "llama-parse"
version = "0.6.73"
version = "0.6.76"
description = "Parse files into RAG-Optimized formats."
authors = [{name = "Logan Markewich", email = "logan@llamaindex.ai"}]
requires-python = ">=3.9,<4.0"
readme = "README.md"
license = "MIT"
dependencies = ["llama-cloud-services>=0.6.73"]
dependencies = ["llama-cloud-services>=0.6.76"]
[project.scripts]
llama-parse = "llama_parse.cli.main:parse"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama-cloud-services-py",
"version": "0.6.73",
"version": "0.6.76",
"private": false,
"license": "MIT",
"scripts": {},
+1 -1
View File
@@ -19,7 +19,7 @@ dev = [
[project]
name = "llama-cloud-services"
version = "0.6.73"
version = "0.6.76"
description = "Tailored SDK clients for LlamaCloud services."
authors = [{name = "Logan Markewich", email = "logan@runllama.ai"}]
requires-python = ">=3.9,<4.0"
+34
View File
@@ -6,6 +6,40 @@ from llama_cloud_services import LlamaParse
from llama_cloud_services.parse.types import JobResult
def test_format_parse_result_markdown_for_notebook():
"""Test the _format_markdown_for_notebook function.
Right now, the only work it does is escape single dollar signs."""
result = JobResult(job_id="test", file_name="test.pdf", job_result={})
# Test None input
assert result._format_markdown_for_notebook(None) is None
# Test single dollar sign gets escaped
assert result._format_markdown_for_notebook("This costs $5") == "This costs \\$5"
# Test double dollar signs are preserved (LaTeX equations)
assert (
result._format_markdown_for_notebook("$$x^2 + y^2 = z^2$$")
== "$$x^2 + y^2 = z^2$$"
)
# Test mixed single and double dollar signs
text = "This costs $5, but $$E = mc^2$$ is priceless"
expected = "This costs \\$5, but $$E = mc^2$$ is priceless"
assert result._format_markdown_for_notebook(text) == expected
# Test multiple single dollar signs
assert result._format_markdown_for_notebook("$10 and $20") == "\\$10 and \\$20"
# Test three or more consecutive dollar signs (preserve them)
assert result._format_markdown_for_notebook("$$$") == "$$$"
# Test adjacent dollar signs with text in between
text = "$$inline$$ and $separate"
expected = "$$inline$$ and \\$separate"
assert result._format_markdown_for_notebook(text) == expected
@pytest.fixture
def file_path() -> str:
return "tests/test_files/attention_is_all_you_need.pdf"
Generated
+2 -2
View File
@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.9, <4.0"
resolution-markers = [
"python_full_version >= '3.14'",
@@ -1596,7 +1596,7 @@ wheels = [
[[package]]
name = "llama-cloud-services"
version = "0.6.72"
version = "0.6.73"
source = { editable = "." }
dependencies = [
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+3
View File
@@ -9,10 +9,12 @@ test("LlamaIndex module resolution test", async (t) => {
const index = new LlamaCloudIndex({
name: "test-index",
projectName: "Default",
apiKey: process.env.LLAMA_CLOUD_API_KEY || "test-key",
});
const reader = new LlamaParseReader({
resultType: "markdown",
verbose: false,
apiKey: process.env.LLAMA_CLOUD_API_KEY || "test-key",
});
ok(index !== undefined);
ok(reader !== undefined);
@@ -24,6 +26,7 @@ test("LlamaIndex module resolution test", async (t) => {
const index = new mod.LlamaCloudIndex({
name: "test-index",
projectName: "Default",
apiKey: process.env.LLAMA_CLOUD_API_KEY || "test-key",
});
ok(index !== undefined);
});
+6
View File
@@ -1,5 +1,11 @@
# llama-cloud-services
## 0.3.9
### Patch Changes
- 5d4cabd: Add ImageNode support in TypeScript
## 0.3.8
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama-cloud-services",
"version": "0.3.8",
"version": "0.3.9",
"type": "module",
"license": "MIT",
"scripts": {
@@ -9,10 +9,16 @@ import { DEFAULT_PROJECT_NAME } from "@llamaindex/core/global";
import type { QueryBundle } from "@llamaindex/core/query-engine";
import { BaseRetriever } from "@llamaindex/core/retriever";
import type { NodeWithScore } from "@llamaindex/core/schema";
import { jsonToNode, ObjectType } from "@llamaindex/core/schema";
import { jsonToNode, ObjectType, ImageNode } from "@llamaindex/core/schema";
import { extractText } from "@llamaindex/core/utils";
import type { ClientParams, CloudConstructorParams } from "./type.js";
import { getPipelineId, initService } from "./utils.js";
import { getPipelineId, getProjectId, initService } from "./utils.js";
import {
type PageScreenshotNodeWithScore,
type PageFigureNodeWithScore,
generateFilePageScreenshotPresignedUrlApiV1FilesIdPageScreenshotsPageIndexPresignedUrlPost,
generateFilePageFigurePresignedUrlApiV1FilesIdPageFiguresPageIndexFigureNamePresignedUrlPost,
} from "./api";
export type CloudRetrieveParams = Omit<
RetrievalParams,
@@ -43,6 +49,95 @@ export class LlamaCloudRetriever extends BaseRetriever {
});
}
private async fetchBase64FromPresignedUrl(url: string): Promise<string> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to fetch media from presigned URL: ${response.status} ${response.statusText}`,
);
}
const buffer = Buffer.from(await response.arrayBuffer());
return buffer.toString("base64");
}
private async pageScreenshotNodesToNodeWithScore(
nodes: PageScreenshotNodeWithScore[] | undefined,
projectId: string,
): Promise<NodeWithScore[]> {
if (!nodes || nodes.length === 0) return [];
const results = await Promise.all(
nodes.map(async (n) => {
const { data: presigned } =
await generateFilePageScreenshotPresignedUrlApiV1FilesIdPageScreenshotsPageIndexPresignedUrlPost(
{
throwOnError: true,
path: {
id: n.node.file_id,
page_index: n.node.page_index,
},
query: {
project_id: projectId,
organization_id: this.organizationId ?? null,
},
},
);
const base64 = await this.fetchBase64FromPresignedUrl(presigned.url);
const imageNode = new ImageNode({
image: base64,
metadata: {
...(n.node.metadata ?? {}),
file_id: n.node.file_id,
page_index: n.node.page_index,
},
});
return { node: imageNode, score: n.score } satisfies NodeWithScore;
}),
);
return results;
}
private async pageFigureNodesToNodeWithScore(
nodes: PageFigureNodeWithScore[] | undefined,
projectId: string,
): Promise<NodeWithScore[]> {
if (!nodes || nodes.length === 0) return [];
const results = await Promise.all(
nodes.map(async (n) => {
const { data: presigned } =
await generateFilePageFigurePresignedUrlApiV1FilesIdPageFiguresPageIndexFigureNamePresignedUrlPost(
{
throwOnError: true,
path: {
id: n.node.file_id,
page_index: n.node.page_index,
figure_name: n.node.figure_name,
},
query: {
project_id: projectId,
organization_id: this.organizationId ?? null,
},
},
);
const base64 = await this.fetchBase64FromPresignedUrl(presigned.url);
const imageNode = new ImageNode({
image: base64,
metadata: {
...(n.node.metadata ?? {}),
file_id: n.node.file_id,
page_index: n.node.page_index,
figure_name: n.node.figure_name,
},
});
return { node: imageNode, score: n.score } satisfies NodeWithScore;
}),
);
return results;
}
// LlamaCloud expects null values for filters, but LlamaIndexTS uses undefined for empty values
// This function converts the undefined values to null
private convertFilter(filters?: MetadataFilters): MetadataFilters | null {
@@ -76,6 +171,35 @@ export class LlamaCloudRetriever extends BaseRetriever {
}
async _retrieve(query: QueryBundle): Promise<NodeWithScore[]> {
// Handle deprecated image retrieval flag
const retrieveImageNodes = (this.retrieveParams as RetrievalParams)
.retrieve_image_nodes;
if (typeof retrieveImageNodes !== "undefined") {
console.warn(
"The `retrieve_image_nodes` parameter is deprecated. Use `retrieve_page_screenshot_nodes` and `retrieve_page_figure_nodes` instead.",
);
}
const retrievePageScreenshotNodes = (this.retrieveParams as RetrievalParams)
.retrieve_page_screenshot_nodes;
const retrievePageFigureNodes = (this.retrieveParams as RetrievalParams)
.retrieve_page_figure_nodes;
if (retrieveImageNodes) {
if (
retrievePageScreenshotNodes === false ||
retrievePageFigureNodes === false
) {
throw new Error(
"If `retrieve_image_nodes` is set to true, both `retrieve_page_screenshot_nodes` and `retrieve_page_figure_nodes` must also be set to true or omitted.",
);
}
(this.retrieveParams as RetrievalParams).retrieve_page_screenshot_nodes =
true;
(this.retrieveParams as RetrievalParams).retrieve_page_figure_nodes =
true;
}
const pipelineId = await getPipelineId(
this.pipelineName,
this.projectName,
@@ -98,6 +222,34 @@ export class LlamaCloudRetriever extends BaseRetriever {
},
});
return this.resultNodesToNodeWithScore(results.retrieval_nodes);
const textNodes = this.resultNodesToNodeWithScore(results.retrieval_nodes);
const needScreenshots = (this.retrieveParams as RetrievalParams)
.retrieve_page_screenshot_nodes;
const needFigures = (this.retrieveParams as RetrievalParams)
.retrieve_page_figure_nodes;
if (!needScreenshots && !needFigures) {
return textNodes;
}
const projectId = await getProjectId(this.projectName, this.organizationId);
const [screenshotNodes, figureNodes] = await Promise.all([
needScreenshots
? this.pageScreenshotNodesToNodeWithScore(
results.image_nodes,
projectId,
)
: Promise.resolve([] as NodeWithScore[]),
needFigures
? this.pageFigureNodesToNodeWithScore(
results.page_figure_nodes,
projectId,
)
: Promise.resolve([] as NodeWithScore[]),
]);
return [...textNodes, ...screenshotNodes, ...figureNodes];
}
}