Compare commits

..

12 Commits

Author SHA1 Message Date
github-actions[bot] d705b16923 chore: version packages (#1035) 2025-12-02 09:43:20 -06:00
Logan ca781132c8 No more presigned URLs by default (#1034) 2025-12-02 09:41:49 -06:00
Roman Isecke 7a68b0fb68 docs: add batch parse directory example notebook (#1009)
* create notebook to parse a batch of documents

* remove local dev code

* tidy

* don't git track the sample pdfs

* update notebook to use client

* add logic to fetch parse results using job id from batch item

* generate example for fetching results via parse job id

* fix linting

* convert notebook to use httpx rather than client for now

* fix linting
2025-12-01 13:57:18 -05:00
George He 87dec5433d Add timeouts to E2E GHA (#1031)
* Add timeouts

* Session timeouts too
2025-11-27 14:57:59 -08:00
Pierre-Loic Doulcet 99f4eba8d0 Pierre/more parse parameters (#1027)
* up python sdk

* bupmVErsion

* Update py/llama_cloud_services/parse/base.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update py/llama_cloud_services/parse/base.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-25 14:43:27 +01:00
github-actions[bot] 54561e2dd2 chore: version packages (#1025) 2025-11-24 16:41:22 -06:00
Logan Markewich bfaec79a8f changeset 2025-11-24 16:37:58 -06:00
Logan Markewich 3e0e522a6b update ts 2025-11-24 16:36:31 -06:00
Logan Markewich f70b6d87ec update py 2025-11-24 16:31:15 -06:00
Logan Markewich 693b5b83b1 improve llama-sheets example 2025-11-24 09:44:11 -06:00
Neeraj Pradhan ad38ef5cd7 Add notebook for tabular extraction (#1017) 2025-11-18 09:47:07 -08:00
Logan Markewich 4c4c6e6575 fix sheets test 2025-11-17 16:14:29 -06:00
25 changed files with 1500 additions and 74 deletions
+1
View File
@@ -12,6 +12,7 @@ env:
jobs:
test_e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
# You can use PyPy versions in python-version.
# For example, pypy-2.7 and pypy-3.8
+1
View File
@@ -0,0 +1 @@
sample_files/
@@ -0,0 +1,807 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "cell-0",
"metadata": {},
"source": [
"# Batch Parse with LlamaCloud Directories\n",
"\n",
"This notebook demonstrates how to use LlamaCloud's batch processing API to parse multiple files in a directory. The workflow includes:\n",
"\n",
"1. **Creating a Directory** - Set up a directory to organize your files\n",
"2. **Uploading Files** - Upload multiple files to the directory\n",
"3. **Starting a Batch Parse Job** - Kick off batch processing on all files\n",
"4. **Monitoring Progress** - Check the status and view results\n",
"\n",
"This is useful when you need to parse many documents at once, as the batch API handles the orchestration and provides progress tracking."
]
},
{
"cell_type": "markdown",
"id": "cell-1",
"metadata": {},
"source": [
"## Setup and Installation"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-2",
"metadata": {},
"outputs": [],
"source": [
"%pip install llama-cloud python-dotenv"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-3",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"from dotenv import load_dotenv\n",
"import httpx\n",
"\n",
"# Load environment variables\n",
"load_dotenv()\n",
"\n",
"# Set your API key\n",
"LLAMA_CLOUD_API_KEY = os.environ.get(\"LLAMA_CLOUD_API_KEY\", \"llx-...\")\n",
"\n",
"# Optional: Set base URL (defaults to https://api.cloud.llamaindex.ai if not set)\n",
"LLAMA_CLOUD_BASE_URL = os.environ.get(\n",
" \"LLAMA_CLOUD_BASE_URL\", \"https://api.cloud.llamaindex.ai\"\n",
")\n",
"\n",
"# Optional: Set project_id if you have one, otherwise it will use your default project\n",
"PROJECT_ID = os.environ.get(\"LLAMA_CLOUD_PROJECT_ID\", None)\n",
"\n",
"print(\"✅ API key configured\")\n",
"print(f\" Base URL: {LLAMA_CLOUD_BASE_URL}\")"
]
},
{
"cell_type": "markdown",
"id": "cell-4",
"metadata": {},
"source": [
"## Setup HTTP Client\n",
"\n",
"Since the current version of the llama-cloud SDK has some issues with the beta endpoints, we'll use direct HTTP requests with httpx for reliability."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-5",
"metadata": {},
"outputs": [],
"source": [
"# Create HTTP client with authentication\n",
"headers = {\n",
" \"Authorization\": f\"Bearer {LLAMA_CLOUD_API_KEY}\",\n",
"}\n",
"\n",
"print(\"✅ HTTP client configured\")\n",
"print(f\" Using base URL: {LLAMA_CLOUD_BASE_URL}\")"
]
},
{
"cell_type": "markdown",
"id": "cell-6",
"metadata": {},
"source": [
"## Step 1: Create a Directory\n",
"\n",
"First, we'll create a directory to organize our files. Directories help you group related files together for batch processing."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-7",
"metadata": {},
"outputs": [],
"source": [
"from datetime import datetime\n",
"\n",
"# Create a directory with a timestamp in the name\n",
"timestamp = datetime.now().strftime(\"%Y%m%d-%H%M%S\")\n",
"directory_name = f\"batch-parse-demo-{timestamp}\"\n",
"\n",
"# Create directory using HTTP request\n",
"response = httpx.post(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/beta/directories\",\n",
" headers=headers,\n",
" params={\"project_id\": PROJECT_ID},\n",
" json={\n",
" \"name\": directory_name,\n",
" \"description\": \"Demo directory for batch parse example\",\n",
" },\n",
" timeout=60.0,\n",
")\n",
"\n",
"if response.status_code in [200, 201]:\n",
" directory = response.json()\n",
" directory_id = directory[\"id\"]\n",
" project_id = directory[\"project_id\"]\n",
"\n",
" print(f\"✅ Created directory: {directory['name']}\")\n",
" print(f\" Directory ID: {directory_id}\")\n",
" print(f\" Project ID: {project_id}\")\n",
"else:\n",
" raise Exception(\n",
" f\"Failed to create directory: {response.status_code} - {response.text}\"\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "cell-8",
"metadata": {},
"source": [
"## Step 2: Upload Files to the Directory\n",
"\n",
"Now we'll upload some files to our directory. For this demo, we'll download some sample PDFs and upload them.\n",
"\n",
"You can replace these with your own files."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-9",
"metadata": {},
"outputs": [],
"source": [
"# Create a directory for sample files\n",
"import requests\n",
"\n",
"os.makedirs(\"sample_files\", exist_ok=True)\n",
"\n",
"# Sample documents to download\n",
"sample_docs = {\n",
" \"attention.pdf\": \"https://arxiv.org/pdf/1706.03762.pdf\",\n",
" \"bert.pdf\": \"https://arxiv.org/pdf/1810.04805.pdf\",\n",
"}\n",
"\n",
"# Download sample documents\n",
"for filename, url in sample_docs.items():\n",
" filepath = f\"sample_files/{filename}\"\n",
" if not os.path.exists(filepath):\n",
" print(f\"📥 Downloading {filename}...\")\n",
" response = requests.get(url)\n",
" if response.status_code == 200:\n",
" with open(filepath, \"wb\") as f:\n",
" f.write(response.content)\n",
" print(f\" ✅ Downloaded {filename}\")\n",
" else:\n",
" print(f\" ❌ Failed to download {filename}\")\n",
" else:\n",
" print(f\"📁 {filename} already exists\")\n",
"\n",
"print(\"\\n✅ Sample files ready!\")"
]
},
{
"cell_type": "markdown",
"id": "cell-10",
"metadata": {},
"source": [
"### Upload Files to Directory\n",
"\n",
"Now let's upload the files to our directory using the `upload_file_to_directory` endpoint."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-11",
"metadata": {},
"outputs": [],
"source": [
"uploaded_files = []\n",
"\n",
"# Workaround: Use direct HTTP requests instead of SDK due to SDK bug\n",
"import httpx\n",
"\n",
"for filename in os.listdir(\"sample_files\"):\n",
" if filename.endswith(\".pdf\"):\n",
" filepath = f\"sample_files/{filename}\"\n",
"\n",
" print(f\"📤 Uploading {filename}...\")\n",
"\n",
" # Upload file using direct HTTP request (SDK has a bug with file uploads)\n",
" with open(filepath, \"rb\") as f:\n",
" # Prepare the multipart form data correctly\n",
" files = {\"upload_file\": (filename, f, \"application/pdf\")}\n",
"\n",
" # Make the request directly\n",
" response = httpx.post(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/beta/directories/{directory_id}/files/upload\",\n",
" params={\"project_id\": project_id},\n",
" files=files,\n",
" headers={\"Authorization\": f\"Bearer {LLAMA_CLOUD_API_KEY}\"},\n",
" timeout=60.0,\n",
" )\n",
"\n",
" if response.status_code in [200, 201]:\n",
" directory_file = response.json()\n",
" uploaded_files.append(directory_file)\n",
" print(f\" ✅ Uploaded: {directory_file.get('display_name')}\")\n",
" print(f\" File ID: {directory_file.get('id')}\")\n",
" else:\n",
" print(f\" ❌ Upload failed: {response.status_code}\")\n",
" print(f\" Error: {response.text[:200]}\")\n",
"\n",
"print(f\"\\n✅ Uploaded {len(uploaded_files)} files to directory\")"
]
},
{
"cell_type": "markdown",
"id": "cell-12",
"metadata": {},
"source": [
"## Step 3: Create a Batch Parse Job\n",
"\n",
"Now that we have files in our directory, let's create a batch parse job to process them all at once.\n",
"\n",
"The batch processing API uses the same configuration as LlamaParse."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-13",
"metadata": {},
"outputs": [],
"source": [
"# Configure the parse job\n",
"# This configuration will apply to all files in the directory\n",
"job_config = {\n",
" \"job_name\": \"parse_raw_file_job\", # Must match the JobNames enum value\n",
" \"partitions\": {},\n",
" \"parameters\": {\n",
" \"type\": \"parse\",\n",
" \"lang\": \"en\",\n",
" \"fast_mode\": True,\n",
" },\n",
"}\n",
"\n",
"print(\"✅ Job configuration created\")\n",
"print(f\" Language: {job_config['parameters']['lang']}\")\n",
"print(f\" Fast mode: {job_config['parameters']['fast_mode']}\")"
]
},
{
"cell_type": "markdown",
"id": "cell-14",
"metadata": {},
"source": [
"### Submit the Batch Job\n",
"\n",
"Now let's submit the batch job to process all files in the directory."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-15",
"metadata": {},
"outputs": [],
"source": [
"print(f\"🚀 Submitting batch parse job for directory: {directory_id}\")\n",
"print(f\" Processing {len(uploaded_files)} files...\\n\")\n",
"\n",
"# Submit batch job using HTTP request\n",
"response = httpx.post(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/beta/batch-processing\",\n",
" headers=headers,\n",
" params={\"project_id\": project_id},\n",
" json={\n",
" \"directory_id\": directory_id,\n",
" \"job_config\": job_config,\n",
" \"page_size\": 100, # Number of files to fetch per batch\n",
" \"continue_as_new_threshold\": 10, # Workflow continuation threshold\n",
" },\n",
" timeout=60.0,\n",
")\n",
"\n",
"if response.status_code in [200, 201]:\n",
" batch_job = response.json()\n",
" batch_job_id = batch_job[\"id\"]\n",
"\n",
" print(\"✅ Batch job submitted successfully!\")\n",
" print(f\" Batch Job ID: {batch_job_id}\")\n",
" print(f\" Workflow ID: {batch_job.get('workflow_id')}\")\n",
" print(f\" Status: {batch_job.get('status')}\")\n",
" print(f\" Total Items: {batch_job.get('total_items')}\")\n",
"else:\n",
" raise Exception(\n",
" f\"Failed to create batch job: {response.status_code} - {response.text}\"\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "cell-16",
"metadata": {},
"source": [
"## Step 4: Monitor Job Progress\n",
"\n",
"Now let's monitor the batch job progress. We'll poll the status endpoint to see how the job is progressing."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-17",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"\n",
"\n",
"def print_job_status(status_data):\n",
" \"\"\"Helper function to print job status in a readable format.\"\"\"\n",
" job = status_data[\"job\"]\n",
" progress_pct = status_data[\"progress_percentage\"]\n",
"\n",
" print(f\"\\n{'='*60}\")\n",
" print(f\"Job Status: {job['status']}\")\n",
" print(f\"{'='*60}\")\n",
" print(f\"Total Items: {job['total_items']}\")\n",
" print(f\"Completed: {job['processed_items']}\")\n",
" print(f\"Failed: {job['failed_items']}\")\n",
" print(f\"Skipped: {job['skipped_items']}\")\n",
" print(f\"Progress: {progress_pct:.1f}%\")\n",
"\n",
" if job.get(\"completed_at\"):\n",
" print(f\"Completed At: {job['completed_at']}\")\n",
" elif job.get(\"started_at\"):\n",
" print(f\"Started At: {job['started_at']}\")\n",
"\n",
" print(f\"{'='*60}\")\n",
"\n",
"\n",
"# Poll for status updates\n",
"print(\"🔄 Monitoring batch job progress...\")\n",
"print(\n",
" \"Note: It may take a few seconds for the workflow to initialize and count files.\\n\"\n",
")\n",
"\n",
"max_polls = 60 # Maximum number of status checks (increased for longer jobs)\n",
"poll_interval = 10 # Seconds between checks\n",
"\n",
"for i in range(max_polls):\n",
" response = httpx.get(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/beta/batch-processing/{batch_job_id}\",\n",
" headers=headers,\n",
" params={\"project_id\": project_id},\n",
" timeout=60.0,\n",
" )\n",
"\n",
" if response.status_code == 200:\n",
" status_data = response.json()\n",
" print_job_status(status_data)\n",
"\n",
" # Check if job is complete\n",
" job_status = status_data[\"job\"][\"status\"]\n",
" if job_status in [\"completed\", \"failed\", \"cancelled\"]:\n",
" print(f\"\\n✅ Job finished with status: {job_status}\")\n",
" break\n",
"\n",
" if i < max_polls - 1:\n",
" print(f\"\\n⏳ Waiting {poll_interval} seconds before next check...\")\n",
" time.sleep(poll_interval)\n",
" else:\n",
" print(f\"Error getting status: {response.status_code} - {response.text}\")\n",
" break\n",
"else:\n",
" print(f\"\\n⚠️ Reached maximum polling attempts. Job may still be running.\")"
]
},
{
"cell_type": "markdown",
"id": "cell-18",
"metadata": {},
"source": [
"## Step 5: View Job Items\n",
"\n",
"Let's look at the individual items in the batch job to see which files were processed successfully."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-19",
"metadata": {},
"outputs": [],
"source": [
"# Get all items in the batch job\n",
"response = httpx.get(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/beta/batch-processing/{batch_job_id}/items\",\n",
" headers=headers,\n",
" params={\"project_id\": project_id, \"limit\": 100},\n",
" timeout=60.0,\n",
")\n",
"\n",
"if response.status_code == 200:\n",
" items_response = response.json()\n",
"\n",
" print(f\"\\n📋 Batch Job Items ({items_response['total_size']} total)\")\n",
" print(f\"{'='*80}\\n\")\n",
"\n",
" for item in items_response[\"items\"]:\n",
" status_emoji = (\n",
" \"✅\"\n",
" if item[\"status\"] == \"completed\"\n",
" else \"❌\"\n",
" if item[\"status\"] == \"failed\"\n",
" else \"⏳\"\n",
" )\n",
" print(f\"{status_emoji} {item['item_name']}\")\n",
" print(f\" Status: {item['status']}\")\n",
" print(f\" Item ID: {item['item_id']}\")\n",
"\n",
" if item.get(\"error_message\"):\n",
" print(f\" Error: {item['error_message']}\")\n",
"\n",
" print()\n",
"else:\n",
" print(f\"Error listing items: {response.status_code} - {response.text}\")"
]
},
{
"cell_type": "markdown",
"id": "cell-20",
"metadata": {},
"source": [
"## Step 6: Retrieve Processing Results\n",
"\n",
"For each completed file, we can retrieve the processing results to see where the parsed output is stored."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-21",
"metadata": {},
"outputs": [],
"source": [
"# Get processing results for a specific item\n",
"if items_response[\"items\"]:\n",
" first_item = items_response[\"items\"][0]\n",
"\n",
" print(f\"\\n🔍 Processing results for: {first_item['item_name']}\")\n",
" print(f\"{'='*80}\\n\")\n",
"\n",
" response = httpx.get(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/beta/batch-processing/items/{first_item['item_id']}/processing-results\",\n",
" headers=headers,\n",
" params={\"project_id\": project_id},\n",
" timeout=60.0,\n",
" )\n",
"\n",
" if response.status_code == 200:\n",
" results = response.json()\n",
"\n",
" print(f\"Item: {results['item_name']}\")\n",
" print(f\"Total processing runs: {len(results['processing_results'])}\\n\")\n",
"\n",
" for i, result in enumerate(results[\"processing_results\"], 1):\n",
" print(f\"Run {i}:\")\n",
" print(f\" Job Type: {result['job_type']}\")\n",
" print(f\" Processed At: {result['processed_at']}\")\n",
" print(f\" Parameters Hash: {result['parameters_hash']}\")\n",
"\n",
" if result.get(\"output_s3_path\"):\n",
" print(f\" Output S3 Path: {result['output_s3_path']}\")\n",
"\n",
" if result.get(\"output_metadata\"):\n",
" print(f\" Output Metadata: {result['output_metadata']}\")\n",
"\n",
" print()\n",
" else:\n",
" print(f\"Error getting results: {response.status_code} - {response.text}\")"
]
},
{
"cell_type": "markdown",
"id": "cell-22",
"metadata": {},
"source": [
"## Optional: List All Batch Jobs\n",
"\n",
"You can also list all batch jobs in your project to see the history of batch processing operations."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell-23",
"metadata": {},
"outputs": [],
"source": [
"# List all parse jobs in the project\n",
"response = httpx.get(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/beta/batch-processing\",\n",
" headers=headers,\n",
" params={\"project_id\": project_id, \"job_type\": \"parse\", \"limit\": 10},\n",
" timeout=60.0,\n",
")\n",
"\n",
"if response.status_code == 200:\n",
" jobs_response = response.json()\n",
"\n",
" print(f\"\\n📊 Recent Batch Parse Jobs ({jobs_response['total_size']} total)\")\n",
" print(f\"{'='*80}\\n\")\n",
"\n",
" for job in jobs_response[\"items\"]:\n",
" status_emoji = (\n",
" \"✅\"\n",
" if job[\"status\"] == \"completed\"\n",
" else \"❌\"\n",
" if job[\"status\"] == \"failed\"\n",
" else \"⏳\"\n",
" )\n",
" print(f\"{status_emoji} Job ID: {job['id']}\")\n",
" print(f\" Status: {job['status']}\")\n",
" print(f\" Directory: {job['directory_id']}\")\n",
" print(f\" Total Items: {job['total_items']}\")\n",
" print(f\" Completed: {job['processed_items']}\")\n",
" print(f\" Created: {job['created_at']}\")\n",
" print()\n",
"else:\n",
" print(f\"Error listing jobs: {response.status_code} - {response.text}\")"
]
},
{
"cell_type": "markdown",
"id": "uug7591rkq",
"metadata": {},
"source": [
"## Step 7: Retrieve Parsed Text Results\n",
"\n",
"Once the batch job is complete, each BatchJobItem will have a `job_id` field that maps to a parse job ID. We can use this ID with the standard parse client methods to fetch the actual parsed text results."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "vpp0vxtc0y",
"metadata": {},
"outputs": [],
"source": [
"# Get all completed items and their job IDs\n",
"completed_items = [\n",
" item for item in items_response[\"items\"] if item[\"status\"] == \"completed\"\n",
"]\n",
"\n",
"print(f\"📄 Found {len(completed_items)} completed items\\n\")\n",
"print(f\"{'='*80}\\n\")\n",
"\n",
"# Display the job_id for each completed item\n",
"for item in completed_items:\n",
" print(f\"📝 {item['item_name']}\")\n",
" print(f\" Item ID: {item['item_id']}\")\n",
" print(f\" Parse Job ID: {item['job_id']}\")\n",
" print()"
]
},
{
"cell_type": "markdown",
"id": "4gck6hwpnl6",
"metadata": {},
"source": [
"### Fetch Parsed Text for a Specific Document\n",
"\n",
"Now let's use the `job_id` to retrieve the actual parsed text content using the parse client methods."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g191kvgxxvk",
"metadata": {},
"outputs": [],
"source": [
"# Get the parsed text for the first completed item\n",
"if completed_items:\n",
" first_completed = completed_items[0]\n",
"\n",
" print(f\"📖 Retrieving parsed text for: {first_completed['item_name']}\")\n",
" print(f\" Using Parse Job ID: {first_completed['job_id']}\\n\")\n",
" print(f\"{'='*80}\\n\")\n",
"\n",
" # Use the job_id to fetch the parse result\n",
" response = httpx.get(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/parsing/job/{first_completed['job_id']}/result/text\",\n",
" headers=headers,\n",
" params={\"project_id\": project_id},\n",
" timeout=60.0,\n",
" )\n",
"\n",
" if response.status_code == 200:\n",
" parse_result = response.text\n",
"\n",
" print(f\"✅ Retrieved parsed text ({len(parse_result)} characters)\\n\")\n",
"\n",
" # Display first 1000 characters as a preview\n",
" print(\"Preview (first 1000 characters):\")\n",
" print(\"-\" * 80)\n",
" print(parse_result[:1000])\n",
" print(\"-\" * 80)\n",
"\n",
" if len(parse_result) > 1000:\n",
" print(f\"\\n... and {len(parse_result) - 1000} more characters\")\n",
" else:\n",
" print(\n",
" f\"Error retrieving parse result: {response.status_code} - {response.text}\"\n",
" )\n",
"else:\n",
" print(\"⚠️ No completed items found to retrieve results from\")"
]
},
{
"cell_type": "markdown",
"id": "2olccb4l8fj",
"metadata": {},
"source": [
"### Retrieve Parsed Results in Other Formats\n",
"\n",
"You can also retrieve the parsed results in JSON or Markdown format using different client methods."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "lcqsfxiw0sr",
"metadata": {},
"outputs": [],
"source": [
"if completed_items:\n",
" first_completed = completed_items[0]\n",
"\n",
" print(\n",
" f\"📋 Retrieving parse results in different formats for: {first_completed['item_name']}\\n\"\n",
" )\n",
"\n",
" # Get as JSON (includes structured data with pages, images, etc.)\n",
" print(\"1️⃣ Retrieving as JSON...\")\n",
" response = httpx.get(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/parsing/job/{first_completed['job_id']}/result/json\",\n",
" headers=headers,\n",
" params={\"project_id\": project_id},\n",
" timeout=60.0,\n",
" )\n",
"\n",
" if response.status_code == 200:\n",
" json_result = response.json()\n",
" print(f\" ✅ JSON result with {len(json_result['pages'])} pages\")\n",
" print(f\" Keys: {list(json_result.keys())}\\n\")\n",
" else:\n",
" print(f\" Error: {response.status_code}\\n\")\n",
"\n",
" # Get as Markdown\n",
" print(\"2️⃣ Retrieving as Markdown...\")\n",
" response = httpx.get(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/parsing/job/{first_completed['job_id']}/result/markdown\",\n",
" headers=headers,\n",
" params={\"project_id\": project_id},\n",
" timeout=60.0,\n",
" )\n",
"\n",
" if response.status_code == 200:\n",
" markdown_result = response.text\n",
" print(f\" ✅ Markdown result ({len(markdown_result)} characters)\\n\")\n",
"\n",
" # Display markdown preview\n",
" print(\"Markdown Preview (first 500 characters):\")\n",
" print(\"-\" * 80)\n",
" print(markdown_result[:500])\n",
" print(\"-\" * 80)\n",
"\n",
" if len(markdown_result) > 500:\n",
" print(f\"\\n... and {len(markdown_result) - 500} more characters\")\n",
" else:\n",
" print(f\" Error: {response.status_code}\")\n",
"else:\n",
" print(\"⚠️ No completed items found to retrieve results from\")"
]
},
{
"cell_type": "markdown",
"id": "lr61wqkfq3",
"metadata": {},
"source": [
"### Batch Process All Parsed Results\n",
"\n",
"You can also loop through all completed items to retrieve and process all the parsed results."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "kltydf9xzkl",
"metadata": {},
"outputs": [],
"source": [
"# Process all completed items\n",
"print(f\"🔄 Processing all {len(completed_items)} completed items...\\n\")\n",
"print(f\"{'='*80}\\n\")\n",
"\n",
"all_results = {}\n",
"\n",
"for item in completed_items:\n",
" print(f\"📄 Processing: {item['item_name']}\")\n",
" print(f\" Parse Job ID: {item['job_id']}\")\n",
"\n",
" try:\n",
" # Retrieve the parsed text for this item\n",
" response = httpx.get(\n",
" f\"{LLAMA_CLOUD_BASE_URL}/api/v1/parsing/job/{item['job_id']}/result/text\",\n",
" headers=headers,\n",
" params={\"project_id\": project_id},\n",
" timeout=60.0,\n",
" )\n",
"\n",
" if response.status_code == 200:\n",
" parsed_text = response.text\n",
"\n",
" all_results[item[\"item_name\"]] = {\n",
" \"job_id\": item[\"job_id\"],\n",
" \"text\": parsed_text,\n",
" \"length\": len(parsed_text),\n",
" }\n",
"\n",
" print(f\" ✅ Retrieved {len(parsed_text)} characters\")\n",
" else:\n",
" all_results[item[\"item_name\"]] = {\n",
" \"job_id\": item[\"job_id\"],\n",
" \"error\": f\"HTTP {response.status_code}\",\n",
" }\n",
" print(f\" ❌ Error: HTTP {response.status_code}\")\n",
"\n",
" except Exception as e:\n",
" print(f\" ❌ Error: {str(e)}\")\n",
" all_results[item[\"item_name\"]] = {\"job_id\": item[\"job_id\"], \"error\": str(e)}\n",
"\n",
" print()\n",
"\n",
"print(f\"{'='*80}\")\n",
"print(f\"\\n✅ Processed {len(all_results)} items\")\n",
"print(f\"\\nSummary:\")\n",
"for name, result in all_results.items():\n",
" if \"error\" in result:\n",
" print(f\" ❌ {name}: Error - {result['error']}\")\n",
" else:\n",
" print(f\" ✅ {name}: {result['length']:,} characters\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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": 5
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

+508
View File
@@ -0,0 +1,508 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "a7oq3cfnync",
"metadata": {},
"source": [
"# Extracting Repeating Entities from Documents\n",
"\n",
"This notebook demonstrates how to use the `PER_TABLE_ROW` extraction target to extract structured data from documents containing repeating entities like tables, lists, or catalogs.\n",
"\n",
"## Why Use the Tabular Extraction Target?\n",
"\n",
"`PER_DOC` (refer to the table below for a quick overview of the different extraction targets) is the default extraction target in LlamaExtract, which looks at the entire document's context when doing an extraction. When extracting lists of entities, LLM-based extraction has a critical failure mode — it often **only extracts the first few tens of entries** from a long list. This happens because LLMs have limited attention spans for repetitive data. Document-level extraction doesn't guarantee exhaustive coverage, and long lists lead to incomplete extractions.\n",
"\n",
"**The Solution**: `PER_TABLE_ROW` solves this by processing each entity individually or in smaller batches, ensuring **exhaustive extraction** of all entries regardless of list length.\n",
"\n",
"### Entity-Level Extraction\n",
"\n",
"When using `extraction_target=ExtractTarget.PER_TABLE_ROW`, you define a schema for a **single entity** (e.g., one hospital, one product, one invoice line item), not the full document. LlamaExtract automatically:\n",
"- Detects the formatting patterns that distinguish individual entities (table rows, list items, section headers, etc.)\n",
"- Applies your schema to each identified entity\n",
"- Returns a `list[YourSchema]` with one object per entity\n",
"\n",
"This approach is ideal when each entity locally contains all the information needed for your schema.\n",
"\n",
"### Choosing the Right Extraction Target\n",
"\n",
"| Extraction Target | Best For | Returns |\n",
"|-------------------|----------|---------|\n",
"| `PER_DOC` | Single-entity documents, summaries, or short lists | One JSON object for entire document |\n",
"| `PER_PAGE` | Multi-page documents where each page is independent | One JSON object per page |\n",
"| `PER_TABLE_ROW` | **Long lists, tables, catalogs with repeating entities** | List of JSON objects (one per entity) |\n",
"\n",
"📖 For more details, see the [Extraction Target documentation](https://developers.llamaindex.ai/python/cloud/llamaextract/features/concepts/#extraction-target)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9427d1de",
"metadata": {},
"outputs": [],
"source": [
"from dotenv import load_dotenv\n",
"from llama_cloud_services import LlamaExtract\n",
"\n",
"\n",
"# Load environment variables (put LLAMA_CLOUD_API_KEY in your .env file)\n",
"load_dotenv(override=True)\n",
"\n",
"# Optionally, add your project id/organization id\n",
"llama_extract = LlamaExtract()"
]
},
{
"cell_type": "markdown",
"id": "4426b360",
"metadata": {},
"source": [
"## Table of Hospitals by County and Insurance Plans\n",
"\n",
"We have a PDF document with a list of hospitals by county and different insurance plans offered by Blue Shield of California. \n",
"\n",
"\n",
"![First few entries from the PDF](./data/tables/bsc_page1.png)"
]
},
{
"cell_type": "markdown",
"id": "c86sjymhn1r",
"metadata": {},
"source": [
"We want to extract each hospital from this table along with a list of applicable insurance plans. \n",
"\n",
"### Example 1: Structured Table\n",
"\n",
"This is an ideal use case for `PER_TABLE_ROW` extraction:\n",
"- **Clear structure**: The document has explicit table formatting with rows and columns\n",
"- **Repeating entities**: Each row represents one hospital with consistent attributes\n",
"- **Local information**: All data for each hospital (county, name, plans) is contained within its row\n",
"\n",
"Notice that our `Hospital` schema describes a **single hospital**, not the full document. LlamaExtract will return a `list[Hospital]` with one entry per table row."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7c61a802",
"metadata": {},
"outputs": [],
"source": [
"from pydantic import BaseModel, Field\n",
"\n",
"\n",
"class Hospital(BaseModel):\n",
" \"\"\"List of hospitals by county available for different BSC plans\"\"\"\n",
"\n",
" county: str = Field(description=\"County name\")\n",
" hospital_name: str = Field(description=\"Name of the hospital\")\n",
" plan_names: list[str] = Field(\n",
" description=\"List of plans available at the hospital. One of: Trio HMO, SaveNet, Access+ HMO, BlueHPN PPO, Tandem PPO, PPO\"\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b8a69b7a",
"metadata": {},
"outputs": [],
"source": [
"from llama_cloud_services.extract import ExtractConfig, ExtractMode, ExtractTarget\n",
"\n",
"\n",
"result = await llama_extract.aextract(\n",
" data_schema=Hospital,\n",
" files=\"./data/tables/BSC-Hospital-List-by-County.pdf\",\n",
" config=ExtractConfig(\n",
" extraction_mode=ExtractMode.PREMIUM,\n",
" extraction_target=ExtractTarget.PER_TABLE_ROW,\n",
" parse_model=\"anthropic-sonnet-4.5\",\n",
" ),\n",
")"
]
},
{
"cell_type": "markdown",
"id": "43722cda",
"metadata": {},
"source": [
"### Results"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "95b5aca6",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"380"
]
},
"execution_count": null,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(result.data)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1e355770",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'county': 'Alameda',\n",
" 'hospital_name': 'Alameda Hospital',\n",
" 'plan_names': ['Trio HMO',\n",
" 'SaveNet',\n",
" 'Access+ HMO',\n",
" 'BlueHPN PPO',\n",
" 'Tandem PPO',\n",
" 'PPO']},\n",
" {'county': 'Alameda',\n",
" 'hospital_name': 'Alta Bates Med Ctr Herrick Campus',\n",
" 'plan_names': ['Trio HMO',\n",
" 'Access+ HMO',\n",
" 'BlueHPN PPO',\n",
" 'Tandem PPO',\n",
" 'PPO']},\n",
" {'county': 'Alameda',\n",
" 'hospital_name': 'Alta Bates Summit Med Ctr Alta Bates Campus',\n",
" 'plan_names': ['Trio HMO',\n",
" 'Access+ HMO',\n",
" 'BlueHPN PPO',\n",
" 'Tandem PPO',\n",
" 'PPO']},\n",
" {'county': 'Alameda',\n",
" 'hospital_name': 'Alta Bates Summit Med Ctr Summit Campus',\n",
" 'plan_names': ['Trio HMO',\n",
" 'Access+ HMO',\n",
" 'BlueHPN PPO',\n",
" 'Tandem PPO',\n",
" 'PPO']},\n",
" {'county': 'Alameda',\n",
" 'hospital_name': 'Alta Bates Summit Medical Center',\n",
" 'plan_names': ['Trio HMO',\n",
" 'Access+ HMO',\n",
" 'BlueHPN PPO',\n",
" 'Tandem PPO',\n",
" 'PPO']},\n",
" {'county': 'Alameda',\n",
" 'hospital_name': 'BHC Fremont Hospital',\n",
" 'plan_names': ['Trio HMO',\n",
" 'SaveNet',\n",
" 'Access+ HMO',\n",
" 'BlueHPN PPO',\n",
" 'Tandem PPO',\n",
" 'PPO']},\n",
" {'county': 'Alameda',\n",
" 'hospital_name': 'Centre For Neuro Skills San Francisco',\n",
" 'plan_names': ['Trio HMO',\n",
" 'SaveNet',\n",
" 'Access+ HMO',\n",
" 'BlueHPN PPO',\n",
" 'Tandem PPO',\n",
" 'PPO']},\n",
" {'county': 'Alameda',\n",
" 'hospital_name': 'Eden Medical Center',\n",
" 'plan_names': ['Trio HMO', 'Access+ HMO', 'PPO']},\n",
" {'county': 'Alameda',\n",
" 'hospital_name': 'Fairmont Hospital',\n",
" 'plan_names': ['Trio HMO',\n",
" 'SaveNet',\n",
" 'Access+ HMO',\n",
" 'BlueHPN PPO',\n",
" 'Tandem PPO',\n",
" 'PPO']},\n",
" {'county': 'Alameda',\n",
" 'hospital_name': 'Highland Hospital',\n",
" 'plan_names': ['Trio HMO',\n",
" 'SaveNet',\n",
" 'Access+ HMO',\n",
" 'BlueHPN PPO',\n",
" 'Tandem PPO',\n",
" 'PPO']}]"
]
},
"execution_count": null,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"result.data[:10]"
]
},
{
"cell_type": "markdown",
"id": "e28f0de8",
"metadata": {},
"source": [
"![](./data/tables/bsc_results.png)"
]
},
{
"cell_type": "markdown",
"id": "di156pb7s6j",
"metadata": {},
"source": [
"**Success!** We extracted all **380 hospitals** from the multi-page PDF. Each entity was correctly parsed with its county, hospital name, and applicable insurance plans. With `PER_DOC`, we would likely have only gotten the first 20-30 entries."
]
},
{
"cell_type": "markdown",
"id": "gelvl6db268",
"metadata": {},
"source": [
"## Extracting from a Toy Catalog\n",
"\n",
"### Example 2: Semi-Structured List\n",
"\n",
"The `PER_TABLE_ROW` extraction target also works well for documents that aren't explicit tables but have similar properties:\n",
"- **Ordered listing**: The toys are listed sequentially with visual separation (section headers, spacing)\n",
"- **Repeating pattern**: Each toy entry has a consistent structure (code, name, specs, description)\n",
"- **Local information**: All attributes for each toy are grouped together in its entry\n",
"\n",
"Even though this isn't a traditional table format, each toy entity locally contains all the information needed for our schema. LlamaExtract detects the formatting patterns that distinguish each toy and extracts them as separate entities.\n",
"\n",
"![](./data/tables/toy_catalog_page.png)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8cf0b2db",
"metadata": {},
"outputs": [],
"source": [
"from pydantic import BaseModel, Field\n",
"\n",
"\n",
"class ToyCatalog(BaseModel):\n",
" \"\"\"Product information from a toy catalog.\"\"\"\n",
"\n",
" section_name: str = Field(\n",
" description=\"The name of the toy section (e.g. Table Toys, Active Toys).\"\n",
" )\n",
" product_code: str = Field(\n",
" description=\"The unique product code for the toy (e.g., GA457).\"\n",
" )\n",
" toy_name: str = Field(description=\"The name of the toy.\")\n",
" age_range: str = Field(\n",
" description=\"The recommended age range for the toy (e.g., 6 +, 4 +).\",\n",
" )\n",
" player_range: str = Field(\n",
" description=\"The number of players the toy is designed for (e.g., 2, 2-4, 1-6).\",\n",
" )\n",
" material: str = Field(\n",
" description=\"The primary material(s) the toy is made of (e.g., wood, cardboard).\",\n",
" )\n",
" description: str = Field(\n",
" description=\"A brief description of the toy and its components and dimensions.\",\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "mysu1i2qo9e",
"metadata": {},
"source": [
"### Results\n",
"\n",
"Again, our schema represents a **single toy product**, not the entire catalog. The system will return a `list[ToyCatalog]` with one entry per toy."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5b38b806",
"metadata": {},
"outputs": [],
"source": [
"result = await llama_extract.aextract(\n",
" data_schema=ToyCatalog,\n",
" files=\"./data/tables/Click-BS-Toys-Catalogue-2024.pdf\",\n",
" config=ExtractConfig(\n",
" extraction_mode=ExtractMode.PREMIUM,\n",
" extraction_target=ExtractTarget.PER_TABLE_ROW,\n",
" parse_model=\"anthropic-sonnet-4.5\",\n",
" ),\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "91aface0",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"153"
]
},
"execution_count": null,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(result.data)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "51278736",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'section_name': 'Table Toys',\n",
" 'product_code': 'GA457',\n",
" 'toy_name': 'Dots and Boxes',\n",
" 'age_range': '6+',\n",
" 'player_range': '2',\n",
" 'material': 'wood',\n",
" 'description': 'base 17x17 cm\\n50 border pieces 4x1,2x0,3 cm\\n34 trees 2,6x1,4 cm'},\n",
" {'section_name': 'Table Toys',\n",
" 'product_code': 'GA456',\n",
" 'toy_name': '3 In a Row',\n",
" 'age_range': '8+',\n",
" 'player_range': '2',\n",
" 'material': 'wood, pine, cardboard',\n",
" 'description': 'base 24x22,5x2,5 cm\\n30 cards 5,5x5 cm\\n6 chips'},\n",
" {'section_name': 'Table Toys',\n",
" 'product_code': 'GA467',\n",
" 'toy_name': 'Which Cow am i?',\n",
" 'age_range': '6+',\n",
" 'player_range': '2',\n",
" 'material': 'wood, beech',\n",
" 'description': '2 cow bases 56x4x4,5 cm\\n16 cards 4x5 cm'},\n",
" {'section_name': 'Table Toys',\n",
" 'product_code': 'GA460',\n",
" 'toy_name': 'Balance Bunnies',\n",
" 'age_range': '4+',\n",
" 'player_range': '2',\n",
" 'material': 'wood',\n",
" 'description': '1 base 35x12x25 cm\\n7 bunnies 7 foxes\\n1 dice 3 cm'},\n",
" {'section_name': 'Table Toys',\n",
" 'product_code': 'GA462',\n",
" 'toy_name': 'Color Combination Race',\n",
" 'age_range': '4+',\n",
" 'player_range': '2-4',\n",
" 'material': 'wood, cardboard',\n",
" 'description': 'base 6,5x6,5x15 cm, rings 5,5x5,5x0,5 mm\\ncardholder 6x6x2 cm, cards 5,5x5,5 cm\\ncolor cards Ø 15,5 cm - Ø 7 cm'},\n",
" {'section_name': 'Table Toys',\n",
" 'product_code': 'GA465',\n",
" 'toy_name': 'Plop It',\n",
" 'age_range': '6+',\n",
" 'player_range': '2-4',\n",
" 'material': 'wood, elastic, cardboard',\n",
" 'description': 'Catch the right balls and plop them in the net!\\n* 2 ploppers 8x5 cm\\n* 2 net holders Ø 5cm, length 55 cm\\n* 6 cards 1,5x2,5 cm, 30 balls Ø 2,5 cm\\n* 1 rope 120 cm'},\n",
" {'section_name': 'Table Toys',\n",
" 'product_code': 'GA466',\n",
" 'toy_name': 'Whack a Shape',\n",
" 'age_range': '4+',\n",
" 'player_range': '2-4',\n",
" 'material': 'wood',\n",
" 'description': '* base 38,5x15,5 cm\\n* 2 stands 36 half balls, 4 hammers\\n* 1 dice 2,5 cm\\n* 4 cards'},\n",
" {'section_name': 'Table Toys',\n",
" 'product_code': 'GA458',\n",
" 'toy_name': 'Sling Puck | Table Hockey',\n",
" 'age_range': '6+',\n",
" 'player_range': '2',\n",
" 'material': 'wood',\n",
" 'description': '* double sides base 39x21x3 cm\\n* 10 chips Ø 2,5 cm\\n* 2 pushers 4x4x3 cm'},\n",
" {'section_name': 'Table Toys',\n",
" 'product_code': 'GA039',\n",
" 'toy_name': 'DIY Birdhouse',\n",
" 'age_range': '3+',\n",
" 'player_range': '1',\n",
" 'material': 'wood',\n",
" 'description': '* house 9x9x13 cm'},\n",
" {'section_name': 'Table Toys',\n",
" 'product_code': 'GA319',\n",
" 'toy_name': 'Triangle Domino',\n",
" 'age_range': '6+',\n",
" 'player_range': '2-4',\n",
" 'material': 'wood',\n",
" 'description': '* 35 triangles 10x10 x10 cm'}]"
]
},
"execution_count": null,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"result.data[:10]"
]
},
{
"cell_type": "markdown",
"id": "d1810c0a",
"metadata": {},
"source": [
"![](./data/tables/toy_catalog_results.png)"
]
},
{
"cell_type": "markdown",
"id": "ezur9gnhmsb",
"metadata": {},
"source": [
"**Success!** Despite the semi-structured format, we extracted all **152 toy products** from the catalog (there's an extra repeated extracted toy from the Appendix section). LlamaExtract automatically detected the visual patterns separating each toy entry and applied our schema to each one."
]
},
{
"cell_type": "markdown",
"id": "aeyr3io29u",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"The `PER_TABLE_ROW` extraction target is powerful for extracting repeating structured entities from documents. Key takeaways:\n",
"\n",
"1. **Schema design**: Define your schema for a single entity, not the full document. The system returns `list[YourSchema]`.\n",
"\n",
"2. **Works with various formats**: Not just traditional tables—any document with distinguishable repeating entities (bullets, numbering, headers, visual separation, etc.). The common requirement is that each entity should contain all the necessary data for your schema within its local context.\n",
"\n",
"3. **Automatic pattern detection**: LlamaExtract identifies the formatting patterns that distinguish entities and applies your schema to each one."
]
}
],
"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": 5
}
@@ -24,8 +24,8 @@ from workflows import Context
dotenv.load_dotenv()
# Global context for loaded dataframes
_dataframe_context: Dict[str, Any] = {}
# Global context for executed code
_code_context: Dict[str, Any] = {}
# Helper function for initial agent context
@@ -79,36 +79,30 @@ def list_extracted_data(data_dir: str = "data") -> str:
# Agent tool for code execution against dataframes
def execute_dataframe_code(
code: str, load_files: Optional[Dict[str, str]] = None
) -> str:
def execute_code(code: str) -> str:
"""
Execute Python pandas code against LlamaSheets extracted data.
Execute Python pandas code against LlamaSheets extracted data.
This tool allows flexible data analysis by executing arbitrary pandas code.
You can load parquet files, manipulate dataframes, and return results.
This tool allows flexible data analysis by executing arbitrary pandas code.
You can load parquet files, manipulate dataframes, and return results.
The code executes in a context where:
- pandas is available as 'pd'
- json is available for formatting output
- Previously loaded dataframes are accessible by their variable names
The code executes in a context where:
- pandas is available as 'pd'
- json is available for formatting output
Args:
code: Python code to execute. Any print() statements or stdout/stderr
will be captured and returned. Optionally set a 'result' variable
for structured output.
load_files: Optional dict mapping variable names to file paths to load
Example: {"df": "data/sales_region_1.parquet",
"meta": "data/sales_metadata_1.parquet"}
Args:
code: Python code to execute. Any print() statements or stdout/stderr
will be captured and returned. Optionally set a 'result' variable
for structured output.
Returns:
String containing:
- Any stdout/stderr output from the code execution
- The 'result' variable if it was set (formatted appropriately)
- Error message if execution failed
Returns:
String containing:
- Any stdout/stderr output from the code execution
- The 'result' variable if it was set (formatted appropriately)
- Error message if execution failed
Example usage:
code = '''
Example usage:
code = '''
# Load and inspect data
df = pd.read_parquet("data/sales_region_1.parquet")
print(f"Loaded {len(df)} rows")
@@ -118,9 +112,9 @@ def execute_dataframe_code(
"columns": list(df.columns),
"sample": df.head(3).to_dict(orient="records")
}
'''
'''
"""
global _dataframe_context
global _code_context
# Capture stdout and stderr
stdout_capture = io.StringIO()
@@ -138,24 +132,17 @@ def execute_dataframe_code(
"pd": pd,
"json": json,
"Path": Path,
**_dataframe_context, # Include previously loaded dataframes
**_code_context, # Include previously loaded dataframes
}
# Load any requested files into context
if load_files:
for var_name, file_path in load_files.items():
if file_path.endswith(".parquet"):
exec_context[var_name] = pd.read_parquet(file_path)
# Also save to global context for future calls
_dataframe_context[var_name] = exec_context[var_name]
elif file_path.endswith(".json"):
with open(file_path, "r") as f:
exec_context[var_name] = json.load(f)
_dataframe_context[var_name] = exec_context[var_name]
# Execute the code
exec(code, exec_context)
# Update global context with any new variables (excluding built-ins and modules)
for key, value in exec_context.items():
if not key.startswith("_") and key not in ["pd", "json", "Path"]:
_code_context[key] = value
# Restore stdout/stderr
sys.stdout = old_stdout
sys.stderr = old_stderr
@@ -223,8 +210,8 @@ def create_llamasheets_agent(
# Initialize LLM
llm = OpenAI(model=llm_model, api_key=api_key)
# Create tools - just 4 simple but powerful tools
tools = [execute_dataframe_code]
# Create tools list
tools = [execute_code]
# System prompt to guide the agent
available_regions = list_extracted_data()
@@ -238,11 +225,8 @@ LlamaSheets extracts messy spreadsheets into clean parquet files with two types
- Type detection: data_type, is_date_like, is_percentage, is_currency
- Layout: is_in_first_row, is_merged_cell, horizontal_alignment
Your approach:
1. Use list_extracted_data() to discover available files
2. Use execute_dataframe_code() to load and analyze data with pandas
3. Use metadata to understand structure (bold = headers, colors = groups)
4. Use save_dataframe() to export results
You have access to tools that allow you to execute Python pandas code against these files.
Use these tools to load the parquet files, analyze the data, and return results.
Key tips:
- Bold cells in metadata often indicate headers
@@ -299,7 +283,7 @@ async def main():
print(ev.delta, end="", flush=True)
_ = await handler
print("=== End Query ===\n")
print("\n=== End Query ===\n")
if __name__ == "__main__":
+12
View File
@@ -1,5 +1,17 @@
# llama-cloud-services-py
## 0.6.83
### Patch Changes
- ca78113: Do not use presigned URLs by default in files client
## 0.6.82
### Patch Changes
- bfaec79: Update for new page number params
## 0.6.81
### Patch Changes
+1 -1
View File
@@ -15,4 +15,4 @@ test: ## Run unit tests via pytest
.PHONY: e2e
e2e: ## Run all tests. Run with high parallelism using xdist since tests are bottlenecked bound by the slow backend parsing
uv run pytest -v -n 32 tests/
uv run pytest -v -n 32 --timeout=300 --session-timeout=1740 tests/
+2 -2
View File
@@ -11,7 +11,7 @@ from llama_cloud_services.utils import SourceText, FileInput
class FileClient:
"""
Higher-level client for interacting with the LlamaCloud Files API.
Uses presigned URLs for uploads by default.
Optionally uses presigned URLs for uploads.
Args:
client: The LlamaCloud client to use.
@@ -25,7 +25,7 @@ class FileClient:
client: AsyncLlamaCloud,
project_id: Optional[str] = None,
organization_id: Optional[str] = None,
use_presigned_url: bool = True,
use_presigned_url: bool = False,
):
self.client = client
self.project_id = project_id
+72 -3
View File
@@ -285,7 +285,7 @@ class LlamaParse(BasePydanticReader):
description="Note: Non compatible with gpt-4o. If set to true, the parser will use a faster mode to extract text from documents. This mode will skip OCR of images, and table/heading reconstruction.",
)
guess_xlsx_sheet_names: Optional[bool] = Field(
guess_xlsx_sheet_name: Optional[bool] = Field(
default=False,
description="Whether to guess the sheet names of the xlsx file.",
)
@@ -313,6 +313,10 @@ class LlamaParse(BasePydanticReader):
default=False,
description="If set to true, the parser will ignore document elements for layout detection and only rely on a vision model.",
)
inline_images_in_markdown: Optional[bool] = Field(
default=False,
description="If set to true, the parser will inline images in the markdown output.",
)
input_s3_region: Optional[str] = Field(
default=None,
description="The region of the input S3 bucket if input_s3_path is specified.",
@@ -329,6 +333,10 @@ class LlamaParse(BasePydanticReader):
default=None,
description="The maximum timeout in seconds to wait for the parsing to finish. Override default timeout of 30 minutes. Minimum is 120 seconds.",
)
keep_page_separator_when_merging_tables: Optional[bool] = Field(
default=False,
description="If set to true, the parser will keep the page separator when merging tables across pages.",
)
language: Optional[str] = Field(
default="en", description="The language of the text to parse."
)
@@ -400,6 +408,10 @@ class LlamaParse(BasePydanticReader):
default=False,
description="If set, the parser will try to preserve very small text lines. This can be useful for documents containing vector graphics with very small text lines that may not be recognized by OCR or a vision model (such as in CAD drawings).",
)
presentation_out_of_bounds_content: Optional[bool] = Field(
default=False,
description="If set to true, the parser will include out-of-bounds content in presentation files.",
)
precise_bounding_box: Optional[bool] = Field(
default=False,
description="If set to true, the parser will use a more precise bounding box to extract text from documents. This will increase the accuracy of the parsing job, but reduce the speed.",
@@ -416,6 +428,14 @@ class LlamaParse(BasePydanticReader):
default=None,
description="A suffix to add after error message in failed pages. If not set, no suffix will be used.",
)
remove_hidden_text: Optional[bool] = Field(
default=False,
description="If set to true, the parser will remove hidden text from the document.",
)
save_images: Optional[bool] = Field(
default=True,
description="If set to true, the parser will save images extracted from the document.",
)
skip_diagonal_text: Optional[bool] = Field(
default=False,
description="If set to true, the parser will ignore diagonal text (when the text rotation in degrees modulo 90 is not 0).",
@@ -440,6 +460,10 @@ class LlamaParse(BasePydanticReader):
default=False,
description="If set to true, the parser will use a specialized one-shot chart parsing model to extract data from charts. This model is able to understand the chart type and extract the data accordingly. It is more accurate than the efficient model, but also more expensive.",
)
specialized_image_parsing: Optional[bool] = Field(
default=False,
description="If set to true, the parser will use a specialized image parsing model to extract data from images.",
)
strict_mode_buggy_font: Optional[bool] = Field(
default=False,
description="If set to true, the parser will fail if it can't extract text from a document because of a buggy font.",
@@ -536,6 +560,10 @@ class LlamaParse(BasePydanticReader):
default=None,
description="A prefix to add to the page footer in the output markdown.",
)
extract_printed_page_number: Optional[bool] = Field(
default=None,
description="Whether to extract the printed page numbers from pages in the document.",
)
# Deprecated
bounding_box: Optional[str] = Field(
@@ -580,6 +608,23 @@ class LlamaParse(BasePydanticReader):
description="Automatically check for Python SDK updates.",
)
@model_validator(mode="before")
@classmethod
def handle_deprecated_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
# Handle deprecated guess_xlsx_sheet_names -> guess_xlsx_sheet_name
if "guess_xlsx_sheet_names" in data:
warnings.warn(
"The parameter 'guess_xlsx_sheet_names' is deprecated and will be removed in a future release. "
"Use 'guess_xlsx_sheet_name' instead.",
DeprecationWarning,
stacklevel=2,
)
# Only set the new parameter if it's not already explicitly set
if "guess_xlsx_sheet_name" not in data:
data["guess_xlsx_sheet_name"] = data["guess_xlsx_sheet_names"]
del data["guess_xlsx_sheet_names"]
return data
@model_validator(mode="before")
@classmethod
def warn_extra_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
@@ -820,8 +865,8 @@ class LlamaParse(BasePydanticReader):
)
data["formatting_instruction"] = self.formatting_instruction
if self.guess_xlsx_sheet_names:
data["guess_xlsx_sheet_names"] = self.guess_xlsx_sheet_names
if self.guess_xlsx_sheet_name:
data["guess_xlsx_sheet_name"] = self.guess_xlsx_sheet_name
if self.html_make_all_elements_visible:
data["html_make_all_elements_visible"] = self.html_make_all_elements_visible
@@ -845,6 +890,9 @@ class LlamaParse(BasePydanticReader):
"ignore_document_elements_for_layout_detection"
] = self.ignore_document_elements_for_layout_detection
if self.inline_images_in_markdown:
data["inline_images_in_markdown"] = self.inline_images_in_markdown
if input_url is not None:
files = None
data["input_url"] = str(input_url)
@@ -873,6 +921,11 @@ class LlamaParse(BasePydanticReader):
if self.job_timeout_in_seconds is not None:
data["job_timeout_in_seconds"] = self.job_timeout_in_seconds
if self.keep_page_separator_when_merging_tables:
data[
"keep_page_separator_when_merging_tables"
] = self.keep_page_separator_when_merging_tables
if self.language:
data["language"] = self.language
@@ -951,6 +1004,11 @@ class LlamaParse(BasePydanticReader):
if self.preserve_very_small_text:
data["preserve_very_small_text"] = self.preserve_very_small_text
if self.presentation_out_of_bounds_content:
data[
"presentation_out_of_bounds_content"
] = self.presentation_out_of_bounds_content
if self.preset is not None:
data["preset"] = self.preset
@@ -970,6 +1028,11 @@ class LlamaParse(BasePydanticReader):
"replace_failed_page_with_error_message_suffix"
] = self.replace_failed_page_with_error_message_suffix
if self.remove_hidden_text:
data["remove_hidden_text"] = self.remove_hidden_text
data["save_images"] = self.save_images
if self.skip_diagonal_text:
data["skip_diagonal_text"] = self.skip_diagonal_text
@@ -994,6 +1057,9 @@ class LlamaParse(BasePydanticReader):
if self.specialized_chart_parsing_plus:
data["specialized_chart_parsing_plus"] = self.specialized_chart_parsing_plus
if self.specialized_image_parsing:
data["specialized_image_parsing"] = self.specialized_image_parsing
if self.strict_mode_buggy_font:
data["strict_mode_buggy_font"] = self.strict_mode_buggy_font
@@ -1049,6 +1115,9 @@ class LlamaParse(BasePydanticReader):
"markdown_table_multiline_header_separator"
] = self.markdown_table_multiline_header_separator
if self.extract_printed_page_number is not None:
data["extract_printed_page_number"] = self.extract_printed_page_number
# Deprecated
if self.bounding_box is not None:
data["bounding_box"] = self.bounding_box
+13
View File
@@ -250,6 +250,19 @@ class Page(SafeBaseModel):
slideSpeakerNotes: Optional[str] = Field(
default=None, description="The speaker notes for the slide."
)
confidence: Optional[float] = Field(
default=None, description="The confidence of the page parsing."
)
printedPageNumber: Optional[str] = Field(
default=None,
description="The printed page number on the page, if found and extractPrintedPageNumber is set to true.",
)
pageHeaderMarkdown: Optional[str] = Field(
default=None, description="The page header in markdown format."
)
pageFooterMarkdown: Optional[str] = Field(
default=None, description="The page footer in markdown format."
)
class JobResult(SafeBaseModel):
+14
View File
@@ -1,5 +1,19 @@
# llama_parse
## 0.6.83
### Patch Changes
- Updated dependencies [ca78113]
- llama-cloud-services-py@0.6.83
## 0.6.82
### Patch Changes
- Updated dependencies [bfaec79]
- llama-cloud-services-py@0.6.82
## 0.6.81
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama_parse",
"version": "0.6.81",
"version": "0.6.83",
"description": "",
"main": "index.js",
"private": false,
+2 -2
View File
@@ -11,13 +11,13 @@ dev = [
[project]
name = "llama-parse"
version = "0.6.81"
version = "0.6.83"
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.81"]
dependencies = ["llama-cloud-services>=0.6.83"]
[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.81",
"version": "0.6.83",
"private": false,
"license": "MIT",
"scripts": {},
+2 -1
View File
@@ -7,6 +7,7 @@ dev = [
"pytest>=8.0.0,<9",
"pytest-xdist>=3.6.1,<4",
"pytest-asyncio",
"pytest-timeout>=2.3.1",
"ipykernel>=6.29.0,<7",
"pre-commit==3.2.0",
"autoevals>=0.0.114,<0.0.115",
@@ -22,7 +23,7 @@ dev = [
[project]
name = "llama-cloud-services"
version = "0.6.81"
version = "0.6.83"
description = "Tailored SDK clients for LlamaCloud services."
authors = [{name = "Logan Markewich", email = "logan@runllama.ai"}]
requires-python = ">=3.9,<4.0"
+21 -13
View File
@@ -10,8 +10,10 @@ from llama_cloud_services.beta.sheets.types import SpreadsheetParsingConfig
@pytest.fixture
def sheets_client():
"""Create a LlamaSheets client for testing."""
api_key = os.getenv("LLAMA_CLOUD_API_KEY")
base_url = os.getenv("LLAMA_CLOUD_BASE_URL", "https://api.cloud.llamaindex.ai")
api_key = os.getenv(
"LLAMA_CLOUD_API_KEY", "llx-3AEorIw5v0lnJPzEOI9xSl0N8yFx3fguw0Zn8QJHzGWmwg5r"
)
base_url = os.getenv("LLAMA_CLOUD_BASE_URL", "https://api.staging.llamaindex.ai")
client = LlamaSheets(
api_key=api_key,
@@ -49,7 +51,10 @@ def sample_excel_file():
@pytest.mark.skipif(
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
os.environ.get(
"LLAMA_CLOUD_API_KEY", "llx-3AEorIw5v0lnJPzEOI9xSl0N8yFx3fguw0Zn8QJHzGWmwg5r"
)
== "",
reason="LLAMA_CLOUD_API_KEY not set",
)
@pytest.mark.asyncio
@@ -65,30 +70,30 @@ async def test_spreadsheet_extraction_e2e(
4. Verifies the extracted data matches the original data
"""
# Extract tables from the spreadsheet
result = await sheets_client.aextract_tables(sample_excel_file)
result = await sheets_client.aextract_regions(sample_excel_file)
# Verify job completed successfully
assert result.status in ("SUCCESS", "PARTIAL_SUCCESS")
assert result.success is True
# Verify we extracted at least one table
assert len(result.tables) > 0, "Expected at least one table to be extracted"
assert len(result.regions) > 0, "Expected at least one table to be extracted"
# Get the first table
first_table = result.tables[0]
first_table = result.regions[0]
assert first_table.sheet_name == "TestSheet"
# Download the table as a DataFrame
extracted_df = await sheets_client.adownload_table_as_dataframe(
extracted_df = await sheets_client.adownload_region_as_dataframe(
job_id=result.id,
table_id=first_table.table_id,
region_id=first_table.region_id,
result_type=first_table.region_type,
)
# Load the original dataframe for comparison
original_df = pd.read_excel(sample_excel_file)
# Verify the extracted DataFrame has the expected shape
breakpoint()
assert extracted_df.shape[0] == original_df.shape[0], (
f"Row count mismatch: extracted {extracted_df.shape[0]}, "
f"original {original_df.shape[0]}"
@@ -129,7 +134,10 @@ async def test_spreadsheet_extraction_e2e(
@pytest.mark.skipif(
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
os.environ.get(
"LLAMA_CLOUD_API_KEY", "llx-3AEorIw5v0lnJPzEOI9xSl0N8yFx3fguw0Zn8QJHzGWmwg5r"
)
== "",
reason="LLAMA_CLOUD_API_KEY not set",
)
@pytest.mark.asyncio
@@ -145,7 +153,7 @@ async def test_spreadsheet_extraction_with_config(
)
# Extract tables with the config
result = await sheets_client.aextract_tables(sample_excel_file, config=config)
result = await sheets_client.aextract_regions(sample_excel_file, config=config)
# Verify job completed successfully
assert result.status in ("SUCCESS", "PARTIAL_SUCCESS")
@@ -157,7 +165,7 @@ async def test_spreadsheet_extraction_with_config(
assert result.worksheet_metadata[0].description is not None
# Verify we extracted at least one table
assert len(result.tables) > 0
assert len(result.regions) > 0
# Verify the sheet name matches
assert result.tables[0].sheet_name == "TestSheet"
assert result.regions[0].sheet_name == "TestSheet"
+6
View File
@@ -1,5 +1,11 @@
# llama-cloud-services
## 0.4.2
### Patch Changes
- bfaec79: Update for new page number params
## 0.4.1
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama-cloud-services",
"version": "0.4.1",
"version": "0.4.2",
"type": "module",
"license": "MIT",
"scripts": {
+2
View File
@@ -185,6 +185,7 @@ export class LlamaParseReader extends FileReader {
page_footer_prefix?: string | undefined;
page_footer_suffix?: string | undefined;
merge_tables_across_pages_in_markdown?: boolean | undefined;
extract_printed_page_number?: boolean | undefined;
constructor(
params: Partial<Omit<LlamaParseReader, "language" | "apiKey">> & {
@@ -381,6 +382,7 @@ export class LlamaParseReader extends FileReader {
page_footer_suffix: this.page_footer_suffix,
merge_tables_across_pages_in_markdown:
this.merge_tables_across_pages_in_markdown,
extract_printed_page_number: this.extract_printed_page_number,
} satisfies {
[Key in keyof BodyUploadFileApiParsingUploadPost]-?:
| BodyUploadFileApiParsingUploadPost[Key]