Compare commits

..

25 Commits

Author SHA1 Message Date
Adrian Lyjak b8f95c6a3d Update pyproject.toml
back out of version bump
2026-01-14 13:00:02 -05:00
Adrian Lyjak 878605b347 Update package.json
back out of version bump
2026-01-14 12:59:43 -05:00
Adrian Lyjak 22130a35d7 Update pyproject.toml
back out of version bump
2026-01-14 12:59:27 -05:00
Adrian Lyjak ed4b55305c Update package.json
back out of version bump
2026-01-14 12:59:00 -05:00
Adrian Lyjak 4b9d48f5e6 Update ninety-goats-look.md
Make it a patch version
2026-01-14 12:58:38 -05:00
Pierre-Loic doulcet ae6d6c276f changeset 2026-01-14 18:57:30 +01:00
Pierre-Loic doulcet 25f9efd5bc remove extension filter 2026-01-14 18:41:23 +01:00
github-actions[bot] 812e2f7d72 chore: version packages (#1073)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-12 19:03:13 +01:00
Clelia (Astra) Bertelli d7864afe3f fix: bug fix retry logic in Classify and Extract (#1066)
* fix: bug fix retry logic in Classify and Extract

* chore: apply suggestion

* chore: add PARTIAL_SUCCESS to classify
2026-01-12 18:57:40 +01:00
github-actions[bot] ade8d027a5 chore: version packages (#1071)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-09 20:29:00 -05:00
Adrian Lyjak 997bcc8531 forgot ts changeset (#1070) 2026-01-09 20:23:29 -05:00
github-actions[bot] 8be554c234 chore: version packages (#1068)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-09 18:56:51 -05:00
Adrian Lyjak f777cab0c5 Add bounding box type support to TS too (#1069)
ts too
2026-01-09 18:55:16 -05:00
Adrian Lyjak b9b83c953d Parse bounding boxes from extract jobs results in agent data (#1067) 2026-01-09 18:47:57 -05:00
github-actions[bot] 3ec7024626 chore: version packages (#1058) 2025-12-10 11:53:30 -06:00
Logan d5b18a03fa Remove generate from build path to fix publishing (#1057) 2025-12-10 11:52:43 -06:00
Clelia (Astra) Bertelli 18dd04b6de docs: correct links in readme (#1056) 2025-12-10 17:08:58 +01:00
github-actions[bot] 685a5e6ccc chore: version packages (#1054) 2025-12-09 15:30:13 -06:00
Jim Geurts 576c3d9076 feat: support zod v4 & v3 (#1052) 2025-12-09 15:29:23 -06:00
Logan c8321d2bc5 improve parse ts polling (#1053) 2025-12-09 15:21:19 -06:00
Tuana Çelik 131bbed7aa batch parse sctript with asyncio (#1051)
* batch parse sctript with asyncio

* lint

---------

Co-authored-by: Logan Markewich <logan.markewich@live.com>
2025-12-08 18:50:11 +01:00
Javier Torres 41c8ac2348 docs: Split Example Notebook (#1044)
* split notebook

* Lint
2025-12-08 13:57:20 +01:00
github-actions[bot] 32c53cdf96 chore: version packages (#1046) 2025-12-04 20:43:29 -06:00
Logan 71db318fc2 add tier/version to api (#1045) 2025-12-04 20:42:17 -06:00
George He dac0f79e51 Fix sheets API client (#1032) 2025-12-03 16:39:47 -06:00
39 changed files with 21132 additions and 34921 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"llama_parse": patch
"llama-cloud-services-py": patch
---
Remove extension filter
+183
View File
@@ -0,0 +1,183 @@
"""
Example: Batch Processing a Folder of PDFs with LlamaParse
This script demonstrates how to process multiple PDFs from a folder
using LlamaParse with controlled concurrency using asyncio and semaphores.
Usage:
python batch_parse_folder.py --input-dir ./pdfs --max-concurrent 5
"""
import asyncio
import argparse
from pathlib import Path
from typing import List, Dict, Any
from datetime import datetime
from dotenv import load_dotenv
import os
from llama_cloud_services import LlamaParse
# Load environment variables from .env file
load_dotenv()
async def parse_single_file(
parser: LlamaParse,
file_path: Path,
semaphore: asyncio.Semaphore,
) -> Dict[str, Any]:
"""
Parse a single PDF file with concurrency control.
Args:
parser: LlamaParse instance
file_path: Path to the PDF file
semaphore: Semaphore to control concurrent requests
Returns:
Dictionary with file info and parse result
"""
async with semaphore:
try:
print(f"Starting parse: {file_path.name}")
result = await parser.aparse(str(file_path))
print(f"✓ Completed: {file_path.name} ({len(result.pages)} pages)")
return {
"file": file_path.name,
"status": "success",
"result": result,
"pages": len(result.pages) if result.pages else 0,
}
except Exception as e:
print(f"✗ Error parsing {file_path.name}: {str(e)}")
return {
"file": file_path.name,
"status": "error",
"error": str(e),
}
async def parse_folder(
input_dir: Path,
max_concurrent: int = 5,
api_key: str = None,
) -> List[Dict[str, any]]:
"""
Parse all PDFs in a folder with controlled concurrency.
Args:
input_dir: Directory containing PDF files
max_concurrent: Maximum number of concurrent parse operations
api_key: LlamaCloud API key (loaded from .env file)
Returns:
List of parse results for each file
"""
# Find all PDF files
pdf_files = list(input_dir.glob("*.pdf"))
if not pdf_files:
print(f"No PDF files found in {input_dir}")
return []
print(f"Found {len(pdf_files)} PDF files to parse")
# Initialize parser
parser = LlamaParse(
api_key=api_key,
num_workers=1, # We control concurrency with semaphore
show_progress=False, # We'll show our own progress
)
# Create semaphore to limit concurrent requests
semaphore = asyncio.Semaphore(max_concurrent)
# Create tasks for all files
tasks = [parse_single_file(parser, pdf_file, semaphore) for pdf_file in pdf_files]
# Run all tasks concurrently (but limited by semaphore)
print(
f"Processing {len(tasks)} files with max {max_concurrent} concurrent operations..."
)
start_time = datetime.now()
results = await asyncio.gather(*tasks)
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
# Process results
successful = [
r for r in results if isinstance(r, dict) and r.get("status") == "success"
]
failed = [r for r in results if isinstance(r, dict) and r.get("status") == "error"]
# Print summary
print("PARSE SUMMARY \n")
print(f"Total files: {len(pdf_files)}")
print(f"Successful: {len(successful)}")
print(f"Failed: {len(failed)}")
print(f"Total time: {duration:.2f} seconds")
print(f"Average time per file: {duration / len(pdf_files):.2f} seconds")
if failed:
print("\nFailed files:")
for result in failed:
print(f" - {result['file']}: {result.get('error', 'Unknown error')}")
return results
def main():
"""Main entry point for the script."""
parser = argparse.ArgumentParser(
description="Batch process PDFs in a folder with LlamaParse"
)
parser.add_argument(
"--input-dir",
type=str,
required=True,
help="Directory containing PDF files to parse",
)
parser.add_argument(
"--max-concurrent",
type=int,
default=5,
help="Maximum number of concurrent parse operations (default: 5)",
)
args = parser.parse_args()
input_dir = Path(args.input_dir)
# Validate input directory
if not input_dir.exists():
print(f"Error: Input directory does not exist: {input_dir}")
return
if not input_dir.is_dir():
print(f"Error: Input path is not a directory: {input_dir}")
return
# Get API key from environment (loaded from .env file)
api_key = os.getenv("LLAMA_CLOUD_API_KEY")
if not api_key:
print("Error: LLAMA_CLOUD_API_KEY not found. Please set it in your .env file")
return
# Run async function
asyncio.run(
parse_folder(
input_dir=input_dir,
max_concurrent=args.max_concurrent,
api_key=api_key,
)
)
if __name__ == "__main__":
main()
@@ -0,0 +1,540 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Document Splitting with LlamaCloud\n",
"\n",
"This notebook demonstrates how to use the LlamaCloud **Split** API to automatically segment a concatenated PDF into logical document sections based on content categories.\n",
"\n",
"## Use Case\n",
"\n",
"When dealing with large PDFs that contain multiple distinct documents or sections (e.g., a bundle of research papers, a collection of reports), you often need to split them into individual segments. The Split API uses AI to:\n",
"\n",
"1. Analyze each page's content\n",
"2. Classify pages into user-defined categories\n",
"3. Group consecutive pages of the same category into segments\n",
"\n",
"## Example Document\n",
"\n",
"We'll use a PDF containing three concatenated documents:\n",
"- **Alan Turing's essay** \"Intelligent Machinery, A Heretical Theory\" (an essay)\n",
"- **ImageNet paper** (a research paper)\n",
"- **\"Attention is All You Need\"** paper (a research paper)\n",
"\n",
"We'll split this into segments categorized as either `essay` or `research_paper`.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Requirement already satisfied: llama-cloud in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (0.1.44)\n",
"Requirement already satisfied: python-dotenv in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (1.2.1)\n",
"Requirement already satisfied: requests in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (2.32.5)\n",
"Requirement already satisfied: certifi>=2024.7.4 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from llama-cloud) (2025.11.12)\n",
"Requirement already satisfied: httpx>=0.20.0 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from llama-cloud) (0.28.1)\n",
"Requirement already satisfied: pydantic>=1.10 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from llama-cloud) (2.12.5)\n",
"Requirement already satisfied: charset_normalizer<4,>=2 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from requests) (3.4.4)\n",
"Requirement already satisfied: idna<4,>=2.5 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from requests) (3.11)\n",
"Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from requests) (2.5.0)\n",
"Requirement already satisfied: anyio in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from httpx>=0.20.0->llama-cloud) (4.11.0)\n",
"Requirement already satisfied: httpcore==1.* in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from httpx>=0.20.0->llama-cloud) (1.0.9)\n",
"Requirement already satisfied: h11>=0.16 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from httpcore==1.*->httpx>=0.20.0->llama-cloud) (0.16.0)\n",
"Requirement already satisfied: annotated-types>=0.6.0 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from pydantic>=1.10->llama-cloud) (0.7.0)\n",
"Requirement already satisfied: pydantic-core==2.41.5 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from pydantic>=1.10->llama-cloud) (2.41.5)\n",
"Requirement already satisfied: typing-extensions>=4.14.1 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from pydantic>=1.10->llama-cloud) (4.15.0)\n",
"Requirement already satisfied: typing-inspection>=0.4.2 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from pydantic>=1.10->llama-cloud) (0.4.2)\n",
"Requirement already satisfied: sniffio>=1.1 in /Users/javier/llama_cloud_services/.venv/lib/python3.11/site-packages (from anyio->httpx>=0.20.0->llama-cloud) (1.3.1)\n",
"\n",
"\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m25.0.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m25.3\u001b[0m\n",
"\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n",
"Note: you may need to restart the kernel to use updated packages.\n"
]
}
],
"source": [
"# Install required packages\n",
"%pip install llama-cloud python-dotenv requests"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✅ API configured with base URL: https://api.cloud.llamaindex.ai\n",
"✅ Project ID: using default project\n"
]
}
],
"source": [
"import os\n",
"import time\n",
"import requests\n",
"from dotenv import load_dotenv\n",
"\n",
"# Load environment variables\n",
"load_dotenv()\n",
"\n",
"# Configuration\n",
"LLAMA_CLOUD_API_KEY = os.environ.get(\"LLAMA_CLOUD_API_KEY\", \"llx-...\")\n",
"BASE_URL = os.environ.get(\"LLAMA_CLOUD_BASE_URL\", \"https://api.cloud.llamaindex.ai\")\n",
"PROJECT_ID = os.environ.get(\"LLAMA_CLOUD_PROJECT_ID\", None)\n",
"\n",
"# Headers for API requests\n",
"headers = {\n",
" \"Authorization\": f\"Bearer {LLAMA_CLOUD_API_KEY}\",\n",
" \"Content-Type\": \"application/json\",\n",
"}\n",
"\n",
"print(f\"✅ API configured with base URL: {BASE_URL}\")\n",
"print(f\"✅ Project ID: {PROJECT_ID or 'using default project'}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 1: Upload the PDF File\n",
"\n",
"First, we'll upload our concatenated PDF to LlamaCloud using the Files API. This can be done using the `llama-cloud` SDK.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"📤 Uploading ./data/turing+imagenet+attention.pdf...\n",
"✅ File uploaded successfully!\n",
" File name: turing+imagenet+attention.pdf\n"
]
}
],
"source": [
"from llama_cloud.client import LlamaCloud\n",
"\n",
"# Initialize the client\n",
"client = LlamaCloud(token=LLAMA_CLOUD_API_KEY, base_url=BASE_URL)\n",
"\n",
"# Path to the PDF file\n",
"pdf_path = \"./data/turing+imagenet+attention.pdf\"\n",
"\n",
"# Upload the file\n",
"print(f\"📤 Uploading {pdf_path}...\")\n",
"\n",
"with open(pdf_path, \"rb\") as f:\n",
" uploaded_file = client.files.upload_file(upload_file=f, project_id=PROJECT_ID)\n",
"\n",
"file_id = uploaded_file.id\n",
"print(f\"✅ File uploaded successfully!\")\n",
"print(f\" File name: {uploaded_file.name}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 2: Create a Split Job\n",
"\n",
"Now we'll create a split job using the Split API. Since the Split API is in beta and not yet available in the SDK, we'll use raw HTTP requests.\n",
"\n",
"We define two categories:\n",
"- **essay**: For philosophical or reflective writing\n",
"- **research_paper**: For formal academic documents with methodology and citations\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"🔄 Creating split job...\n",
"✅ Split job created!\n",
" Job ID: spl-zsssb632a742aikliu96pqkb56t5\n",
" Status: pending\n",
" Categories: ['essay', 'research_paper']\n"
]
}
],
"source": [
"# Define the split job request\n",
"split_request = {\n",
" \"document_input\": {\n",
" \"type\": \"file_id\", # only file_id is supported for now\n",
" \"value\": file_id,\n",
" },\n",
" \"categories\": [\n",
" {\n",
" \"name\": \"essay\",\n",
" \"description\": \"A philosophical or reflective piece of writing that presents personal viewpoints, arguments, or thoughts on a topic without strict formal structure\",\n",
" },\n",
" {\n",
" \"name\": \"research_paper\",\n",
" \"description\": \"A formal academic document presenting original research, methodology, experiments, results, and conclusions with citations and references\",\n",
" },\n",
" ],\n",
"}\n",
"\n",
"# Create the split job\n",
"print(\"🔄 Creating split job...\")\n",
"response = requests.post(\n",
" f\"{BASE_URL}/api/v1/beta/split/jobs\",\n",
" params={\"project_id\": PROJECT_ID},\n",
" headers=headers,\n",
" json=split_request,\n",
")\n",
"response.raise_for_status()\n",
"\n",
"split_job = response.json()\n",
"job_id = split_job[\"id\"]\n",
"\n",
"print(f\"✅ Split job created!\")\n",
"print(f\" Job ID: {job_id}\")\n",
"print(f\" Status: {split_job['status']}\")\n",
"print(f\" Categories: {[c['name'] for c in split_job['categories']]}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 3: Poll for Job Completion\n",
"\n",
"The split job runs asynchronously. We'll poll the job status until it completes.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"⏳ Waiting for split job to complete...\n",
" Status: processing (elapsed: 0s)\n",
" Status: processing (elapsed: 5s)\n",
" Status: processing (elapsed: 11s)\n",
" Status: completed (elapsed: 16s)\n",
"\n",
"✅ Split job completed successfully!\n"
]
}
],
"source": [
"def poll_split_job(job_id: str, max_wait_seconds: int = 180, poll_interval: int = 5):\n",
" \"\"\"\n",
" Poll a split job until it reaches a terminal state.\n",
"\n",
" Args:\n",
" job_id: The split job ID\n",
" max_wait_seconds: Maximum time to wait for completion\n",
" poll_interval: Seconds between poll attempts\n",
"\n",
" Returns:\n",
" The completed job response\n",
" \"\"\"\n",
" start_time = time.time()\n",
"\n",
" while (time.time() - start_time) < max_wait_seconds:\n",
" response = requests.get(\n",
" f\"{BASE_URL}/api/v1/beta/split/jobs/{job_id}\",\n",
" params={\"project_id\": PROJECT_ID},\n",
" headers=headers,\n",
" )\n",
" response.raise_for_status()\n",
" job = response.json()\n",
"\n",
" status = job[\"status\"]\n",
" elapsed = int(time.time() - start_time)\n",
" print(f\" Status: {status} (elapsed: {elapsed}s)\")\n",
"\n",
" if status in [\"completed\", \"failed\"]:\n",
" return job\n",
"\n",
" time.sleep(poll_interval)\n",
"\n",
" raise TimeoutError(f\"Job did not complete within {max_wait_seconds} seconds\")\n",
"\n",
"\n",
"print(\"⏳ Waiting for split job to complete...\")\n",
"completed_job = poll_split_job(job_id)\n",
"\n",
"if completed_job[\"status\"] == \"completed\":\n",
" print(\"\\n✅ Split job completed successfully!\")\n",
"else:\n",
" print(\n",
" f\"\\n❌ Split job failed: {completed_job.get('error_message', 'Unknown error')}\"\n",
" )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 4: Analyze the Results\n",
"\n",
"Let's examine the split results to see how the document was segmented.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"📊 Split Results Summary\n",
"==================================================\n",
"Total segments found: 3\n",
"\n",
"Segments by category:\n",
" • essay: 1 segment(s)\n",
" • research_paper: 2 segment(s)\n"
]
}
],
"source": [
"# Get the segments from the result\n",
"segments = completed_job.get(\"result\", {}).get(\"segments\", [])\n",
"\n",
"print(f\"📊 Split Results Summary\")\n",
"print(f\"=\" * 50)\n",
"print(f\"Total segments found: {len(segments)}\")\n",
"print()\n",
"\n",
"# Count by category\n",
"category_counts = {}\n",
"for segment in segments:\n",
" cat = segment[\"category\"]\n",
" category_counts[cat] = category_counts.get(cat, 0) + 1\n",
"\n",
"print(\"Segments by category:\")\n",
"for cat, count in category_counts.items():\n",
" print(f\" • {cat}: {count} segment(s)\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"📄 Segment Details\n",
"==================================================\n",
"\n",
"Segment 1:\n",
" Category: essay\n",
" Pages 1-4 (4 pages)\n",
" Confidence: high\n",
"\n",
"Segment 2:\n",
" Category: research_paper\n",
" Pages 5-13 (9 pages)\n",
" Confidence: high\n",
"\n",
"Segment 3:\n",
" Category: research_paper\n",
" Pages 14-24 (11 pages)\n",
" Confidence: high\n"
]
}
],
"source": [
"# Display detailed segment information\n",
"print(f\"\\n📄 Segment Details\")\n",
"print(f\"=\" * 50)\n",
"\n",
"for i, segment in enumerate(segments, 1):\n",
" category = segment[\"category\"]\n",
" pages = segment[\"pages\"]\n",
" confidence = segment[\"confidence_category\"]\n",
"\n",
" # Format page range\n",
" if len(pages) == 1:\n",
" page_range = f\"Page {pages[0]}\"\n",
" else:\n",
" page_range = f\"Pages {min(pages)}-{max(pages)}\"\n",
"\n",
" print(f\"\\nSegment {i}:\")\n",
" print(f\" Category: {category}\")\n",
" print(f\" {page_range} ({len(pages)} page{'s' if len(pages) > 1 else ''})\")\n",
" print(f\" Confidence: {confidence}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Expected Results\n",
"\n",
"Based on our test document, we expect:\n",
"- **1 essay segment**: Alan Turing's \"Intelligent Machinery, A Heretical Theory\"\n",
"- **2 research paper segments**: ImageNet paper and \"Attention is All You Need\" paper\n",
"\n",
"The pages should be grouped consecutively, with no overlap between segments.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"✅ Validation\n",
"==================================================\n",
"Total pages assigned: 24\n",
"Unique pages: 24\n",
"✅ No page overlap detected - each page belongs to exactly one segment\n"
]
}
],
"source": [
"# Verify no page overlap\n",
"all_pages = []\n",
"for segment in segments:\n",
" all_pages.extend(segment[\"pages\"])\n",
"\n",
"unique_pages = set(all_pages)\n",
"\n",
"print(f\"\\n✅ Validation\")\n",
"print(f\"=\" * 50)\n",
"print(f\"Total pages assigned: {len(all_pages)}\")\n",
"print(f\"Unique pages: {len(unique_pages)}\")\n",
"\n",
"if len(all_pages) == len(unique_pages):\n",
" print(f\"✅ No page overlap detected - each page belongs to exactly one segment\")\n",
"else:\n",
" print(\n",
" f\"⚠️ Page overlap detected - {len(all_pages) - len(unique_pages)} duplicate assignments\"\n",
" )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Using `allow_uncategorized` Strategy\n",
"\n",
"You can also use the `allow_uncategorized` splitting strategy. This is useful when you want to capture pages that don't match any defined category.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"📝 With allow_uncategorized=True and only 'essay' category defined,\n",
" pages that don't match 'essay' will be grouped as 'uncategorized'.\n"
]
}
],
"source": [
"# Example with allow_uncategorized strategy\n",
"split_request_uncategorized = {\n",
" \"document_input\": {\"type\": \"file_id\", \"value\": file_id},\n",
" \"categories\": [\n",
" {\n",
" \"name\": \"essay\",\n",
" \"description\": \"A philosophical or reflective piece of writing that presents personal viewpoints, arguments, or thoughts on a topic\",\n",
" }\n",
" # Note: We only define 'essay' category\n",
" # Research papers will be classified as 'uncategorized'\n",
" ],\n",
" \"splitting_strategy\": {\"allow_uncategorized\": True},\n",
"}\n",
"\n",
"print(\"📝 With allow_uncategorized=True and only 'essay' category defined,\")\n",
"print(\" pages that don't match 'essay' will be grouped as 'uncategorized'.\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Conclusion\n",
"\n",
"The LlamaCloud Split API provides a powerful way to automatically segment concatenated documents based on content categories. This is useful for:\n",
"\n",
"- **Document processing pipelines**: Automatically separate bundled documents before further processing\n",
"- **Content organization**: Categorize and organize mixed document collections\n",
"- **Information extraction**: Identify different document types within a single file\n",
"\n",
"### Key Features\n",
"\n",
"- **AI-powered classification**: Uses LLMs to understand page content and assign categories\n",
"- **Flexible categories**: Define any categories relevant to your use case\n",
"- **Confidence scoring**: Each segment includes a confidence level\n",
"- **Page-level granularity**: Results include exact page numbers for each segment\n",
"\n",
"### API Reference\n",
"\n",
"- **Create Split Job**: `POST /api/v1/beta/split/jobs`\n",
"- **Get Split Job**: `GET /api/v1/beta/split/jobs/{job_id}`\n",
"- **List Split Jobs**: `GET /api/v1/beta/split/jobs`\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+1 -1
View File
@@ -398,6 +398,6 @@ Another option (orthogonal to the above) is to break the document into smaller s
## Additional Resources
- [Extract Documentation](https://docs.cloud.llamaindex.ai/llamaextract/getting_started) - Details on Extract features, API and examples.
- [Example Notebook](docs/examples-py/extract/resume_screening.ipynb) - Detailed walkthrough of resume parsing
- [Example Notebook](examples/extract/resume_screening.ipynb) - Detailed walkthrough of resume parsing
- [Example Application with TypeScript](./examples-ts/extract/) - End-to-end examples using LlamaExtract TypeScript client.
- [Discord Community](https://discord.com/invite/eN6D2HQ4aX) - Get help and share feedback
+5 -5
View File
@@ -97,7 +97,7 @@ for page in result.pages:
print(page.structuredData)
```
See more details about the result object in the [example notebook](./docs/examples-py/parse/demo_json_tour.ipynb).
See more details about the result object in the [example notebook](./examples/parse/demo_json_tour.ipynb).
### Using with file object / bytes
@@ -153,10 +153,10 @@ Full documentation for `SimpleDirectoryReader` can be found on the [LlamaIndex D
Several end-to-end indexing examples can be found in the examples folder
- [Getting Started](docs/examples-py/parse/demo_basic.ipynb)
- [Advanced RAG Example](docs/examples-py/parse/demo_advanced.ipynb)
- [Raw API Usage](docs/examples-py/parse/demo_api.ipynb)
- [Result Object Tour](docs/examples-py/parse/demo_json_tour.ipynb)
- [Getting Started](examples/parse/demo_basic.ipynb)
- [Advanced RAG Example](examples/parse/demo_advanced.ipynb)
- [Raw API Usage](examples/parse/demo_api.ipynb)
- [Result Object Tour](examples/parse/demo_json_tour.ipynb)
## Documentation
+48 -3260
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -1,5 +1,17 @@
# llama-cloud-services-py
## 0.6.89
### Patch Changes
- b9b83c9: Parse bounding boxes from extract jobs results in agent data
## 0.6.88
### Patch Changes
- 71db318: Add tier and version
## 0.6.87
### Patch Changes
@@ -11,6 +11,9 @@ from .schema import (
InvalidExtractionData,
ExtractedFieldMetadata,
ExtractedFieldMetaDataDict,
FieldCitation,
BoundingBox,
PageDimensions,
)
from .client import AsyncAgentDataClient
@@ -28,4 +31,7 @@ __all__ = [
"InvalidExtractionData",
"ExtractedFieldMetadata",
"ExtractedFieldMetaDataDict",
"FieldCitation",
"BoundingBox",
"PageDimensions",
]
@@ -174,6 +174,22 @@ class TypedAgentDataItems(BaseModel, Generic[AgentDataT]):
)
class BoundingBox(BaseModel):
"""Bounding box coordinates for a citation location on a page."""
x: float = Field(description="X coordinate of the bounding box origin")
y: float = Field(description="Y coordinate of the bounding box origin")
w: float = Field(description="Width of the bounding box")
h: float = Field(description="Height of the bounding box")
class PageDimensions(BaseModel):
"""Dimensions of a page in the source document."""
width: float = Field(description="Width of the page")
height: float = Field(description="Height of the page")
class FieldCitation(BaseModel):
page: Optional[int] = Field(
None, description="The page number that the field occurred on"
@@ -182,6 +198,14 @@ class FieldCitation(BaseModel):
None,
description="The original text this field's value was derived from",
)
bounding_boxes: Optional[List[BoundingBox]] = Field(
None,
description="Bounding boxes indicating where the citation appears on the page",
)
page_dimensions: Optional[PageDimensions] = Field(
None,
description="Dimensions of the page containing the citation",
)
class ExtractedFieldMetadata(BaseModel):
@@ -201,6 +225,10 @@ class ExtractedFieldMetadata(BaseModel):
None,
description="The confidence score for the field based on the extracted text only",
)
parsing_confidence: Optional[float] = Field(
None,
description="The confidence score for the field based on the parsing/OCR quality",
)
citation: Optional[List[FieldCitation]] = Field(
None,
description="The citation for the field, including page number and matching text",
+35 -3
View File
@@ -2,7 +2,7 @@ import asyncio
import io
import os
import time
from typing import TYPE_CHECKING
from typing import Any, Dict, TYPE_CHECKING
import httpx
from llama_cloud.client import AsyncLlamaCloud
@@ -68,6 +68,8 @@ class LlamaSheets:
max_timeout: int = 300,
poll_interval: int = 5,
max_retries: int = 3,
project_id: str | None = None,
organization_id: str | None = None,
async_httpx_client: httpx.AsyncClient | None = None,
) -> None:
"""Initialize the LlamaSheets client.
@@ -78,6 +80,8 @@ class LlamaSheets:
max_timeout: Maximum time to wait for job completion in seconds
poll_interval: Interval between status checks in seconds
max_retries: Maximum number of retries for failed requests
project_id: Project ID for file operations. If not provided, will use LLAMA_CLOUD_PROJECT_ID env var
organization_id: Organization ID for file operations. If not provided, will use LLAMA_CLOUD_ORGANIZATION_ID env var
async_httpx_client: Optional custom async httpx client
"""
self.api_key = api_key or os.environ.get("LLAMA_CLOUD_API_KEY")
@@ -93,15 +97,32 @@ class LlamaSheets:
self.poll_interval = poll_interval
self.max_retries = max_retries
self.project_id = project_id or os.environ.get("LLAMA_CLOUD_PROJECT_ID")
self.organization_id = organization_id or os.environ.get(
"LLAMA_CLOUD_ORGANIZATION_ID"
)
self._async_client: httpx.AsyncClient | None = async_httpx_client
self._files_client = FileClient(
AsyncLlamaCloud(
token=self.api_key,
base_url=self.base_url,
httpx_client=async_httpx_client,
)
),
project_id=self.project_id,
organization_id=self.organization_id,
)
def _get_default_params(self) -> dict[str, str]:
"""Get default query parameters for API requests"""
params = {}
if self.project_id is not None:
params["project_id"] = self.project_id
if self.organization_id is not None:
params["organization_id"] = self.organization_id
return params
def _get_async_client(self) -> httpx.AsyncClient:
"""Get or create the async httpx client"""
if self._async_client is None:
@@ -306,6 +327,8 @@ class LlamaSheets:
"config": config.model_dump(mode="json", exclude_none=True),
}
params = self._get_default_params()
try:
async for attempt in AsyncRetrying(
stop=stop_after_attempt(self.max_retries),
@@ -318,6 +341,7 @@ class LlamaSheets:
response = await client.post(
f"{self.base_url}/api/v1/beta/sheets/jobs",
headers=self._get_headers(),
params=params,
json=payload,
)
response.raise_for_status()
@@ -347,12 +371,17 @@ class LlamaSheets:
):
with attempt:
client = self._get_async_client()
params: Dict[str, Any] = {
"include_results": include_results_metadata,
**self._get_default_params(),
}
response = await client.get(
f"{self.base_url}/api/v1/beta/sheets/jobs/{job_id}",
headers=self._get_headers(),
params={"include_results": include_results_metadata},
params=params,
)
response.raise_for_status()
return SpreadsheetJobResult.model_validate(response.json())
except Exception as e:
raise SpreadsheetAPIError(f"Failed to get job status: {e}") from e
@@ -415,6 +444,8 @@ class LlamaSheets:
# Get presigned URL
presigned_response = None
result_type_str = str(result_type)
params = self._get_default_params()
try:
async for attempt in AsyncRetrying(
stop=stop_after_attempt(self.max_retries),
@@ -427,6 +458,7 @@ class LlamaSheets:
response = await client.get(
f"{self.base_url}/api/v1/beta/sheets/jobs/{job_id}/regions/{region_id}/result/{result_type_str}",
headers=self._get_headers(),
params=params,
)
response.raise_for_status()
presigned_response = PresignedUrlResponse.model_validate(
+22
View File
@@ -654,6 +654,9 @@ class LlamaCloudIndex(BaseManagedIndex):
],
)
# Trigger a sync
client.pipelines.sync_pipeline(pipeline_id=index.pipeline.id)
doc_ids = [doc.id for doc in upserted_documents]
index.wait_for_completion(
doc_ids=doc_ids, verbose=verbose, raise_on_error=raise_on_error
@@ -738,6 +741,10 @@ class LlamaCloudIndex(BaseManagedIndex):
)
],
)
# Trigger a sync
self._client.pipelines.sync_pipeline(pipeline_id=self.pipeline.id)
upserted_document = upserted_documents[0]
self.wait_for_completion(
doc_ids=[upserted_document.id], verbose=verbose, raise_on_error=True
@@ -760,6 +767,9 @@ class LlamaCloudIndex(BaseManagedIndex):
)
],
)
# Trigger a sync
await self._aclient.pipelines.sync_pipeline(pipeline_id=self.pipeline.id)
upserted_document = upserted_documents[0]
await self.await_for_completion(
doc_ids=[upserted_document.id], verbose=verbose, raise_on_error=True
@@ -782,6 +792,9 @@ class LlamaCloudIndex(BaseManagedIndex):
)
],
)
# Trigger a sync
self._client.pipelines.sync_pipeline(pipeline_id=self.pipeline.id)
upserted_document = upserted_documents[0]
self.wait_for_completion(
doc_ids=[upserted_document.id], verbose=verbose, raise_on_error=True
@@ -804,6 +817,9 @@ class LlamaCloudIndex(BaseManagedIndex):
)
],
)
# Trigger a sync
await self._aclient.pipelines.sync_pipeline(pipeline_id=self.pipeline.id)
upserted_document = upserted_documents[0]
await self.await_for_completion(
doc_ids=[upserted_document.id], verbose=verbose, raise_on_error=True
@@ -827,6 +843,9 @@ class LlamaCloudIndex(BaseManagedIndex):
for doc in documents
],
)
# Trigger a sync
self._client.pipelines.sync_pipeline(pipeline_id=self.pipeline.id)
doc_ids = [doc.id for doc in upserted_documents]
self.wait_for_completion(doc_ids=doc_ids, verbose=True, raise_on_error=True)
return [True] * len(doc_ids)
@@ -849,6 +868,9 @@ class LlamaCloudIndex(BaseManagedIndex):
for doc in documents
],
)
# Trigger a sync
await self._aclient.pipelines.sync_pipeline(pipeline_id=self.pipeline.id)
doc_ids = [doc.id for doc in upserted_documents]
await self.await_for_completion(
doc_ids=doc_ids, verbose=True, raise_on_error=True
+16 -5
View File
@@ -568,6 +568,13 @@ class LlamaParse(BasePydanticReader):
default=False,
description="If set to true, the parser will include line-level bounding boxes in the result.",
)
tier: Optional[str] = Field(
default=None, description="The tier to use for the parsing job."
)
version: Optional[str] = Field(
default=None,
description="The version of the parser to use at the specified tier.",
)
# Deprecated
bounding_box: Optional[str] = Field(
@@ -744,11 +751,9 @@ class LlamaParse(BasePydanticReader):
file_path = str(file_input)
file_ext = os.path.splitext(file_path)[1].lower()
if file_ext not in SUPPORTED_FILE_TYPES:
raise Exception(
f"Currently, only the following file types are supported: {SUPPORTED_FILE_TYPES}\n"
f"Current file type: {file_ext}"
)
mime_type = mimetypes.guess_type(file_path)[0]
mime_type = "application/octet-stream"
else:
mime_type = mimetypes.guess_type(file_path)[0]
# Open the file here for the duration of the async context
# load data, set the mime type
fs = fs or get_default_fs()
@@ -1125,6 +1130,12 @@ class LlamaParse(BasePydanticReader):
if self.line_level_bounding_box is not None:
data["line_level_bounding_box"] = self.line_level_bounding_box
if self.tier is not None:
data["tier"] = self.tier
if self.version is not None:
data["version"] = self.version
# Deprecated
if self.bounding_box is not None:
data["bounding_box"] = self.bounding_box
+14
View File
@@ -1,5 +1,19 @@
# llama_parse
## 0.6.89
### Patch Changes
- Updated dependencies [b9b83c9]
- llama-cloud-services-py@0.6.89
## 0.6.88
### Patch Changes
- Updated dependencies [71db318]
- llama-cloud-services-py@0.6.88
## 0.6.87
### Patch Changes
+3 -3
View File
@@ -146,9 +146,9 @@ Full documentation for `SimpleDirectoryReader` can be found on the [LlamaIndex D
Several end-to-end indexing examples can be found in the examples folder
- [Getting Started](/docs/examples-py/parse/demo_basic.ipynb)
- [Advanced RAG Example](/docs/examples-py/parse/demo_advanced.ipynb)
- [Raw API Usage](/docs/examples-py/parse/demo_api.ipynb)
- [Getting Started](../../examples/parse/demo_basic.ipynb)
- [Advanced RAG Example](../../examples/parse/demo_advanced.ipynb)
- [Raw API Usage](../../examples/parse/demo_api.ipynb)
## Documentation
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama_parse",
"version": "0.6.87",
"version": "0.6.89",
"description": "",
"main": "index.js",
"private": false,
+2 -2
View File
@@ -11,13 +11,13 @@ dev = [
[project]
name = "llama-parse"
version = "0.6.87"
version = "0.6.89"
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.87"]
dependencies = ["llama-cloud-services>=0.6.89"]
[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.87",
"version": "0.6.89",
"private": false,
"license": "MIT",
"scripts": {},
+1 -1
View File
@@ -23,7 +23,7 @@ dev = [
[project]
name = "llama-cloud-services"
version = "0.6.87"
version = "0.6.89"
description = "Tailored SDK clients for LlamaCloud services."
authors = [{name = "Logan Markewich", email = "logan@runllama.ai"}]
requires-python = ">=3.9,<4.0"
+6 -12
View File
@@ -44,16 +44,16 @@ class TestSpreadsheetParsingConfig:
@pytest.fixture
def sheets_client():
"""Create a LlamaSheets client for testing."""
api_key = os.getenv(
"LLAMA_CLOUD_API_KEY", "llx-3AEorIw5v0lnJPzEOI9xSl0N8yFx3fguw0Zn8QJHzGWmwg5r"
)
base_url = os.getenv("LLAMA_CLOUD_BASE_URL", "https://api.staging.llamaindex.ai")
api_key = os.getenv("LLAMA_CLOUD_API_KEY")
base_url = os.getenv("LLAMA_CLOUD_BASE_URL", "https://api.cloud.llamaindex.ai")
project_id = os.getenv("LLAMA_CLOUD_PROJECT_ID")
client = LlamaSheets(
api_key=api_key,
base_url=base_url,
max_timeout=300,
poll_interval=2,
project_id=project_id,
)
return client
@@ -85,10 +85,7 @@ def sample_excel_file():
@pytest.mark.skipif(
os.environ.get(
"LLAMA_CLOUD_API_KEY", "llx-3AEorIw5v0lnJPzEOI9xSl0N8yFx3fguw0Zn8QJHzGWmwg5r"
)
== "",
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
reason="LLAMA_CLOUD_API_KEY not set",
)
@pytest.mark.asyncio
@@ -168,10 +165,7 @@ async def test_spreadsheet_extraction_e2e(
@pytest.mark.skipif(
os.environ.get(
"LLAMA_CLOUD_API_KEY", "llx-3AEorIw5v0lnJPzEOI9xSl0N8yFx3fguw0Zn8QJHzGWmwg5r"
)
== "",
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
reason="LLAMA_CLOUD_API_KEY not set",
)
@pytest.mark.asyncio
+2 -2
View File
@@ -78,7 +78,7 @@ async def test_upload_bytes(
uploaded_file = await file_client.upload_bytes(file_bytes, external_file_id)
assert isinstance(uploaded_file, File)
expected_name = external_file_id if use_presigned_url else "upload"
expected_name = external_file_id
assert uploaded_file.name == expected_name
assert uploaded_file.external_file_id == external_file_id
@@ -100,7 +100,7 @@ async def test_upload_buffer(
uploaded_file = await file_client.upload_buffer(buffer, external_file_id, file_size)
assert isinstance(uploaded_file, File)
expected_name = external_file_id if use_presigned_url else "upload"
expected_name = external_file_id
assert uploaded_file.name == expected_name
assert uploaded_file.external_file_id == external_file_id
@@ -11,10 +11,12 @@ from llama_cloud.types.aggregate_group import AggregateGroup
from pydantic import BaseModel, Field, ValidationError
from llama_cloud_services.beta.agent_data.schema import (
BoundingBox,
ExtractedData,
ExtractedFieldMetadata,
FieldCitation,
InvalidExtractionData,
PageDimensions,
TypedAgentData,
TypedAggregateGroup,
calculate_overall_confidence,
@@ -663,3 +665,69 @@ def test_field_conflict_in_schema():
assert isinstance(
extracted["majority_opinion"]["reasoning"], ExtractedFieldMetadata
)
def test_parse_extracted_field_metadata_with_bounding_boxes():
"""Test parse_extracted_field_metadata with bounding boxes and page dimensions."""
raw_metadata = {
"document_type": {
"citation": [
{
"page": 1,
"matching_text": "FACTURE ORIGINALE",
"bounding_boxes": [{"x": 77.28, "y": 615.12, "w": 70.6, "h": 7.2}],
"page_dimensions": {"width": 222.24, "height": 736.56},
}
],
"parsing_confidence": 1.0,
"extraction_confidence": 0.7252506422636493,
"confidence": 0.7252506422636493,
},
"summary": {
"citation": [
{
"page": 1,
"matching_text": "FACTURE ORIGINALE",
"bounding_boxes": [{"x": 77.28, "y": 615.12, "w": 70.6, "h": 7.2}],
"page_dimensions": {"width": 222.24, "height": 736.56},
},
{
"page": 1,
"matching_text": "Café filtre assiette — $1.90",
"bounding_boxes": [
{"x": 10.56, "y": 172.83, "w": 171.85, "h": 497.01}
],
"page_dimensions": {"width": 222.24, "height": 736.56},
},
],
"parsing_confidence": 1.0,
"extraction_confidence": 0.5700013128334419,
"confidence": 0.5700013128334419,
},
}
result = parse_extracted_field_metadata(raw_metadata)
# Verify document_type citation with bounding boxes
assert isinstance(result["document_type"], ExtractedFieldMetadata)
assert result["document_type"].parsing_confidence == 1.0
assert result["document_type"].extraction_confidence == 0.7252506422636493
assert result["document_type"].confidence == 0.7252506422636493
assert len(result["document_type"].citation) == 1
citation = result["document_type"].citation[0]
assert citation.page == 1
assert citation.matching_text == "FACTURE ORIGINALE"
assert len(citation.bounding_boxes) == 1
assert citation.bounding_boxes[0] == BoundingBox(x=77.28, y=615.12, w=70.6, h=7.2)
assert citation.page_dimensions == PageDimensions(width=222.24, height=736.56)
# Verify summary citation with multiple bounding boxes
assert isinstance(result["summary"], ExtractedFieldMetadata)
assert len(result["summary"].citation) == 2
assert result["summary"].citation[0].bounding_boxes[0].x == 77.28
assert result["summary"].citation[1].bounding_boxes[0].x == 10.56
# Verify round-trip serialization
result2 = parse_extracted_field_metadata(result)
assert result2 == result
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'",
@@ -1609,7 +1609,7 @@ wheels = [
[[package]]
name = "llama-cloud-services"
version = "0.6.85"
version = "0.6.88"
source = { editable = "." }
dependencies = [
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+39
View File
@@ -1,5 +1,44 @@
# llama-cloud-services
## 0.5.3
### Patch Changes
- d7864af: bugfixes in retry logic for LlamaExtract and LlamaClassify
## 0.5.2
### Patch Changes
- 997bcc8: Add types for bounding boxes
## 0.5.1
### Patch Changes
- d5b18a0: Fix publishing
## 0.5.0
### Minor Changes
- 576c3d9: feat: support zod v4 & v3
Adds support for zod v4 while maintaining backward compatibility with v3.
- Updated zod peer dependency to accept both v3 and v4: `^3.25.76 || ^4.0.0`
- Migrated all import statements to use `zod/v4` import path for compatibility
### Patch Changes
- c8321d2: Improve parse results polling
- 576c3d9: Support zod v3 an v4
## 0.4.3
### Patch Changes
- 71db318: Add tier and version
## 0.4.2
### Patch Changes
File diff suppressed because it is too large Load Diff
+9 -8
View File
@@ -1,12 +1,12 @@
{
"name": "llama-cloud-services",
"version": "0.4.2",
"version": "0.5.3",
"type": "module",
"license": "MIT",
"scripts": {
"get-openapi": "node ./scripts/get-openapi.js",
"generate": "./node_modules/.bin/openapi-ts",
"build": "pnpm run generate && bunchee",
"build": "bunchee",
"dev": "bunchee --watch",
"lint": "eslint src/ --ignore-pattern client/*.ts --no-warn-ignored",
"format": "prettier --write ./src/ tests/",
@@ -116,9 +116,9 @@
"@eslint/js": "^9.32.0",
"@hey-api/client-fetch": "^0.10.1",
"@hey-api/openapi-ts": "^0.67.5",
"@llamaindex/core": "^0.6.19",
"@llamaindex/core": "^0.6.22",
"@llamaindex/env": "^0.1.30",
"@llamaindex/workflow-core": "^0.4.1",
"@llamaindex/workflow-core": "^1.3.3",
"@types/node": "^20.19.9",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
@@ -131,18 +131,19 @@
"turbo": "^2.5.5",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vitest": "^2.0.0"
"vitest": "^2.0.0",
"zod": "^4.1.13"
},
"peerDependencies": {
"@llamaindex/core": "^0.6.19",
"@llamaindex/env": "^0.1.30",
"@llamaindex/workflow-core": "^0.4.1"
"@llamaindex/workflow-core": "^1.3.3",
"zod": "^3.25.0 || ^4.0.0"
},
"dependencies": {
"ajv": "^8.17.1",
"file-type": "^21.0.0",
"p-retry": "^6.2.1",
"zod": "^3.25.76"
"p-retry": "^6.2.1"
},
"packageManager": "pnpm@10.8.1"
}
@@ -1,7 +1,7 @@
import {
addFilesToPipelineApiApiV1PipelinesPipelineIdFilesPut,
getPipelineFileStatusApiV1PipelinesPipelineIdFilesFileIdStatusGet,
listPipelineFilesApiV1PipelinesPipelineIdFilesGet,
listPipelineFiles2ApiV1PipelinesPipelineIdFiles2Get,
listProjectsApiV1ProjectsGet,
readFileContentApiV1FilesIdContentGet,
searchPipelinesApiV1PipelinesGet,
@@ -97,21 +97,20 @@ export class LLamaCloudFileService {
*/
public static async getFileUrl(pipelineId: string, filename: string) {
initService();
const { data: allPipelineFiles } =
await listPipelineFilesApiV1PipelinesPipelineIdFilesGet({
path: {
pipeline_id: pipelineId,
},
throwOnError: true,
});
const file = allPipelineFiles.find((file) => file.name === filename);
const response = await listPipelineFiles2ApiV1PipelinesPipelineIdFiles2Get({
path: {
pipeline_id: pipelineId,
},
throwOnError: true,
});
const file = response.data.files.find((file) => file.name === filename);
if (!file?.file_id) return null;
const { data: fileContent } = await readFileContentApiV1FilesIdContentGet({
path: {
id: file.file_id,
},
query: {
project_id: file.project_id,
project_id: file.project_id || null,
},
throwOnError: true,
});
@@ -2,11 +2,14 @@ export { AgentClient, createAgentDataClient } from "./client";
export type {
AggregateAgentDataOptions,
BoundingBox,
ComparisonOperator,
ExtractedData,
ExtractedFieldMetadata,
ExtractedFieldMetadataDict,
FieldCitation,
FilterOperation,
PageDimensions,
SearchAgentDataOptions,
StatusType,
TypedAgentData,
@@ -28,6 +28,44 @@ export type ComparisonOperator =
*/
export type FilterOperation = RawFilterOperation;
/**
* Bounding box coordinates for a citation location on a page
*/
export interface BoundingBox {
/** X coordinate of the bounding box origin */
x: number;
/** Y coordinate of the bounding box origin */
y: number;
/** Width of the bounding box */
w: number;
/** Height of the bounding box */
h: number;
}
/**
* Dimensions of a page in the source document
*/
export interface PageDimensions {
/** Width of the page */
width: number;
/** Height of the page */
height: number;
}
/**
* Citation information for an extracted field
*/
export interface FieldCitation {
/** The page number that the field occurred on */
page?: number;
/** The original text this field's value was derived from */
matching_text?: string;
/** Bounding boxes indicating where the citation appears on the page */
bounding_boxes?: BoundingBox[];
/** Dimensions of the page containing the citation */
page_dimensions?: PageDimensions;
}
/**
* Metadata for an extracted field, including confidence and citation information
*/
@@ -38,16 +76,11 @@ export interface ExtractedFieldMetadata {
confidence?: number;
/** The confidence score for the field based on the extracted text only */
extraction_confidence?: number;
/** The confidence score for the field based on the parsing/OCR quality */
parsing_confidence?: number;
citation?: FieldCitation[];
}
export interface FieldCitation {
/** The page number that the field occurred on */
page?: number;
/** The original text this field's value was derived from */
matching_text?: string;
}
/**
* Dictionary mapping field names to their metadata
* Values can be ExtractedFieldMetadata objects, nested dictionaries, or arrays
+8 -9
View File
@@ -108,20 +108,19 @@ async function pollForJobCompletion({
}
const response =
await getClassifyJobApiV1ClassifierJobsClassifyJobIdGet(jobOptions);
if (!response.response.ok) {
numIterations++;
}
if (typeof response.data != "undefined") {
status = response.data.status as StatusEnum;
if (status == StatusEnum.CANCELLED || status == StatusEnum.ERROR) {
throw new Error("There was an error during the classification job.");
} else if (status == StatusEnum.SUCCESS) {
throw new Error("There was an error extracting data from your file.");
} else if (
status == StatusEnum.SUCCESS ||
status == StatusEnum.PARTIAL_SUCCESS
) {
return true;
} else {
numIterations++;
await sleep(interval * 1000);
}
}
numIterations++;
await sleep(interval * 1000);
}
}
@@ -169,7 +168,7 @@ async function getJobResult({
retries++;
await sleep(retryInterval * 1000);
}
if (typeof response.data != "undefined") {
if (response.response.ok && typeof response.data != "undefined") {
return response.data as ClassifyJobResults;
} else {
throw new Error(
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
import { workflowEvent } from "@llamaindex/workflow-core";
import { zodEvent } from "@llamaindex/workflow-core/util/zod";
import { z } from "zod";
import { z } from "zod/v4";
import { parseFormSchema } from "./schema";
export const uploadEvent = zodEvent(
+3 -7
View File
@@ -296,20 +296,16 @@ async function pollForJobCompletion(
return false;
}
const response = await getJobApiV1ExtractionJobsJobIdGet(jobOptions);
if (!response.response.ok) {
numIterations++;
}
if (typeof response.data != "undefined") {
status = response.data.status as StatusEnum;
if (status == StatusEnum.CANCELLED || status == StatusEnum.ERROR) {
throw new Error("There was an error extracting data from your file.");
} else if (status == StatusEnum.SUCCESS) {
return true;
} else {
numIterations++;
await sleep(interval * 1000);
}
}
numIterations++;
await sleep(interval * 1000);
}
}
@@ -350,7 +346,7 @@ async function getJobResult(
retries++;
await sleep(retryInterval * 1000);
}
if (typeof response.data != "undefined") {
if (response.response.ok && typeof response.data != "undefined") {
return {
data: response.data.data,
extractionMetadata: response.data.extraction_metadata,
+2 -4
View File
@@ -2,7 +2,7 @@ import type { JSONValue } from "@llamaindex/core/global";
import type { ToolMetadata } from "@llamaindex/core/llms";
import type { BaseQueryEngine } from "@llamaindex/core/query-engine";
import { tool } from "@llamaindex/core/tools";
import { z } from "zod";
import { z } from "zod/v4";
const DEFAULT_NAME = "llama_cloud_index_tool";
const DEFAULT_DESCRIPTION =
@@ -21,9 +21,7 @@ export function createQueryEngineTool(
name: metadata?.name ?? DEFAULT_NAME,
description: metadata?.description ?? DEFAULT_DESCRIPTION,
parameters: z.object({
query: z.string({
description: "The query to search for",
}),
query: z.string().describe("The query to search for"),
}),
execute: async ({ query }) => {
const response = await queryEngine.query({ query });
+117 -59
View File
@@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type Client, createClient, createConfig } from "@hey-api/client-fetch";
import { type FailedAttemptError } from "p-retry";
import { Document, FileReader } from "@llamaindex/core/schema";
import { fs, getEnv, path } from "@llamaindex/env";
import {
type BodyUploadFileApiParsingUploadPost,
type BodyUploadFileApiV1ParsingUploadPost,
type FailPageMode,
type ParserLanguages,
type ParsingMode,
@@ -32,6 +33,33 @@ type WriteStream = {
// eslint-disable-next-line no-var
var process: any;
function handleFailedAttempt(
error: FailedAttemptError,
jobId: string,
verbose: boolean,
) {
// Retry only on 5XX or socket errors.
const status = (error.cause as any)?.response?.status;
if (
!(
(status && status >= 500 && status < 600) ||
((error.cause as any)?.code &&
((error.cause as any).code === "ECONNRESET" ||
(error.cause as any).code === "ETIMEDOUT" ||
(error.cause as any).code === "ECONNREFUSED")) ||
(status && status === 404)
)
) {
throw error;
}
if (verbose) {
console.warn(
`Attempting to get job ${jobId} result (attempt ${error.attemptNumber}) failed. Retrying...`,
);
}
}
/**
* Represents a reader for parsing files using the LlamaParse API.
* See https://github.com/run-llama/llama_parse
@@ -186,6 +214,18 @@ export class LlamaParseReader extends FileReader {
page_footer_suffix?: string | undefined;
merge_tables_across_pages_in_markdown?: boolean | undefined;
extract_printed_page_number?: boolean | undefined;
tier?: string | undefined;
version?: string | undefined;
layout_aware?: boolean | undefined;
line_level_bounding_box?: boolean | undefined;
specialized_image_parsing?: boolean | undefined;
aggressive_table_extraction?: boolean | undefined;
preserve_very_small_text?: boolean | undefined;
spreadsheet_force_formula_computation?: boolean | undefined;
inline_images_in_markdown?: boolean | undefined;
keep_page_separator_when_merging_tables?: boolean | undefined;
remove_hidden_text?: boolean | undefined;
presentation_out_of_bounds_content?: boolean | undefined;
constructor(
params: Partial<Omit<LlamaParseReader, "language" | "apiKey">> & {
@@ -383,11 +423,27 @@ export class LlamaParseReader extends FileReader {
merge_tables_across_pages_in_markdown:
this.merge_tables_across_pages_in_markdown,
extract_printed_page_number: this.extract_printed_page_number,
tier: this.tier,
version: this.version,
layout_aware: this.layout_aware,
line_level_bounding_box: this.line_level_bounding_box,
specialized_image_parsing: this.specialized_image_parsing,
aggressive_table_extraction: this.aggressive_table_extraction,
preserve_very_small_text: this.preserve_very_small_text,
spreadsheet_force_formula_computation:
this.spreadsheet_force_formula_computation,
inline_images_in_markdown: this.inline_images_in_markdown,
webhook_configurations: undefined,
keep_page_separator_when_merging_tables:
this.keep_page_separator_when_merging_tables,
remove_hidden_text: this.remove_hidden_text,
presentation_out_of_bounds_content:
this.presentation_out_of_bounds_content,
} satisfies {
[Key in keyof BodyUploadFileApiParsingUploadPost]-?:
| BodyUploadFileApiParsingUploadPost[Key]
[Key in keyof BodyUploadFileApiV1ParsingUploadPost]-?:
| BodyUploadFileApiV1ParsingUploadPost[Key]
| undefined;
} as unknown as BodyUploadFileApiParsingUploadPost;
} as unknown as BodyUploadFileApiV1ParsingUploadPost;
const response = await uploadFileApiV1ParsingUploadPost({
client: this.#client,
@@ -439,26 +495,8 @@ export class LlamaParseReader extends FileReader {
}),
{
retries: this.maxErrorCount,
onFailedAttempt: (error) => {
// Retry only on 5XX or socket errors.
const status = (error.cause as any)?.response?.status;
if (
!(
(status && status >= 500 && status < 600) ||
((error.cause as any)?.code &&
((error.cause as any).code === "ECONNRESET" ||
(error.cause as any).code === "ETIMEDOUT" ||
(error.cause as any).code === "ECONNREFUSED"))
)
) {
throw error;
}
if (this.verbose) {
console.warn(
`Attempting to get job ${jobId} result (attempt ${error.attemptNumber}) failed. Retrying...`,
);
}
},
onFailedAttempt: (error) =>
handleFailedAttempt(error, jobId, this.verbose),
},
);
} catch (e: any) {
@@ -471,49 +509,69 @@ export class LlamaParseReader extends FileReader {
const status = (data as Record<string, unknown>)["status"];
if (status === "SUCCESS") {
let resultData;
switch (resultType) {
case "json": {
resultData =
await getJobJsonResultApiV1ParsingJobJobIdResultJsonGet({
client: this.#client,
throwOnError: true,
path: { job_id: jobId },
query: {
organization_id: this.organization_id ?? null,
},
signal: AbortSignal.timeout(this.maxTimeout * 1000),
});
break;
const resultData = await pRetry(
() =>
getJobJsonResultApiV1ParsingJobJobIdResultJsonGet({
client: this.#client,
throwOnError: true,
path: { job_id: jobId },
query: {
organization_id: this.organization_id ?? null,
},
signal: AbortSignal.timeout(this.maxTimeout * 1000),
}),
{
retries: this.maxErrorCount,
onFailedAttempt: (error) =>
handleFailedAttempt(error, jobId, this.verbose),
},
);
return resultData.data;
}
case "markdown": {
resultData =
await getJobResultApiV1ParsingJobJobIdResultMarkdownGet({
client: this.#client,
throwOnError: true,
path: { job_id: jobId },
query: {
organization_id: this.organization_id ?? null,
},
signal: AbortSignal.timeout(this.maxTimeout * 1000),
});
break;
const resultData = await pRetry(
() =>
getJobResultApiV1ParsingJobJobIdResultMarkdownGet({
client: this.#client,
throwOnError: true,
path: { job_id: jobId },
query: {
organization_id: this.organization_id ?? null,
},
signal: AbortSignal.timeout(this.maxTimeout * 1000),
}),
{
retries: this.maxErrorCount,
onFailedAttempt: (error) =>
handleFailedAttempt(error, jobId, this.verbose),
},
);
return resultData.data;
}
case "text": {
resultData =
await getJobTextResultApiV1ParsingJobJobIdResultTextGet({
client: this.#client,
throwOnError: true,
path: { job_id: jobId },
query: {
organization_id: this.organization_id ?? null,
},
signal: AbortSignal.timeout(this.maxTimeout * 1000),
});
break;
const resultData = await pRetry(
() =>
getJobTextResultApiV1ParsingJobJobIdResultTextGet({
client: this.#client,
throwOnError: true,
path: { job_id: jobId },
query: {
organization_id: this.organization_id ?? null,
},
signal: AbortSignal.timeout(this.maxTimeout * 1000),
}),
{
retries: this.maxErrorCount,
onFailedAttempt: (error) =>
handleFailedAttempt(error, jobId, this.verbose),
},
);
return resultData.data;
}
}
return resultData.data;
} else if (status === "PENDING") {
if (this.verbose && tries % 10 === 0) {
this.stdout?.write(".");
+8 -7
View File
@@ -1,6 +1,6 @@
import { FailPageMode, ParserLanguages, ParsingMode } from "./client";
import { z } from "zod";
import { z } from "zod/v4";
type Language = ParserLanguages;
const VALUES: [Language, ...Language[]] = [
@@ -52,9 +52,10 @@ export const parseFormSchema = z.object({
html_remove_navigation_elements: z.boolean().optional(),
http_proxy: z
.string()
.url(
'Set a valid URL for the HTTP proxy, e.g., "http://proxy.example.com:8080"',
)
.url({
error:
'Set a valid URL for the HTTP proxy, e.g., "http://proxy.example.com:8080"',
})
.refine(
(url) => {
try {
@@ -67,7 +68,7 @@ export const parseFormSchema = z.object({
}
},
{
message: "Invalid HTTP proxy URL",
error: "Invalid HTTP proxy URL",
},
)
.optional(),
@@ -100,7 +101,7 @@ export const parseFormSchema = z.object({
vendor_multimodal_model_name: z.string().optional(),
model: z.string().optional(),
webhook_url: z.string().url().optional(),
parse_mode: z.nativeEnum(ParsingMode).nullable().optional(),
parse_mode: z.enum(ParsingMode).nullable().optional(),
system_prompt: z.string().optional(),
system_prompt_append: z.string().optional(),
user_prompt: z.string().optional(),
@@ -129,7 +130,7 @@ export const parseFormSchema = z.object({
compact_markdown_table: z.boolean().optional(),
markdown_table_multiline_header_separator: z.string().optional(),
page_error_tolerance: z.number().min(0).max(1).optional(),
replace_failed_page_mode: z.nativeEnum(FailPageMode).nullable().optional(),
replace_failed_page_mode: z.enum(FailPageMode).nullable().optional(),
replace_failed_page_with_error_message_prefix: z.string().optional(),
replace_failed_page_with_error_message_suffix: z.string().optional(),
});