Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d59a90151 | |||
| 98ad550b1a | |||
| b58f43ce9f | |||
| acf6adcd91 | |||
| daf6576c3c | |||
| 8caa4defa6 | |||
| 26918b8de4 | |||
| 6fb5ebe2f9 | |||
| c0aa67995b | |||
| 9f841f8328 | |||
| 99c75eece9 | |||
| 57d2586ee3 | |||
| 4280a43ec8 | |||
| 7f1082bbb2 | |||
| 57cfc45804 | |||
| 30e8913875 | |||
| 0ce6d4d7a4 | |||
| 584ba8d48e | |||
| 925805ee11 | |||
| 76fb73c971 | |||
| 6d19ea9ac0 | |||
| 90431090e9 | |||
| 6dff35b204 | |||
| e634c7978d | |||
| 7a9e99bba2 | |||
| efcdd4405b | |||
| bf3614690f | |||
| 7463e00da3 | |||
| cbe9de0c57 | |||
| a023507d42 | |||
| e48f544ddc | |||
| 4aa7ad5642 | |||
| c39cdbcd01 | |||
| 71eaa8bcc6 | |||
| 1e1cbdfc79 | |||
| cc8af4a43a | |||
| 43fbd48ab8 | |||
| 5ec66e9452 | |||
| 211521c82e | |||
| 4ddaab1efb | |||
| 53e5ce2e83 | |||
| 9f4bd1cb64 | |||
| 456863752b | |||
| c2dc34bbd6 | |||
| fcabb04baf | |||
| 8e7c32d3d6 | |||
| 7e3013d914 | |||
| 4a664c33d2 | |||
| 6d049ee2e4 | |||
| fa73e73664 | |||
| bf67ee6056 | |||
| a1abef2ee9 | |||
| a753e01d3c | |||
| 9b15065b24 | |||
| 6e4150537c | |||
| 233d715a14 | |||
| 77ac385dfe | |||
| 53b78fcd7d | |||
| 16f81bd7ee | |||
| 0ee049fd11 | |||
| 7dba17e5bc | |||
| eeb678b937 | |||
| fe4eb664fd | |||
| 257720e443 | |||
| e7afaedf3e | |||
| b66b47a708 | |||
| fe485ff62e | |||
| 1ebe1cee67 | |||
| e9252eb48a | |||
| dad7728135 | |||
| c5111e3335 | |||
| bbbdb98362 | |||
| 60cdc2af84 | |||
| 344c20f331 | |||
| 2b0496e947 | |||
| 6c63dba6fb | |||
| 734c021a2e | |||
| eeb034896f | |||
| 4c977e8384 | |||
| c6137713c7 | |||
| fd4b1893f1 | |||
| e542e6136b | |||
| 393451e304 | |||
| 5084ba27ab | |||
| c82771f841 | |||
| dc6860535a |
@@ -0,0 +1,11 @@
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# and
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -21,9 +21,9 @@ jobs:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python-version: ["3.9"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Poetry
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
@@ -28,54 +17,25 @@ jobs:
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners
|
||||
# Consider using larger runners for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
runs-on: "ubuntu-latest"
|
||||
timeout-minutes: 360
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["python"]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ]
|
||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
languages: python
|
||||
dependency-caching: true
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
category: "/language:python"
|
||||
|
||||
@@ -18,11 +18,11 @@ jobs:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}
|
||||
- name: Set up python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Poetry
|
||||
|
||||
@@ -14,13 +14,13 @@ env:
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish to PyPI
|
||||
if: github.repository == 'run-llama/llama_parse'
|
||||
if: github.repository == 'run-llama/llama_cloud_services'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -39,10 +39,18 @@ jobs:
|
||||
pypi_token: ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
|
||||
poetry_install_options: "--without dev"
|
||||
|
||||
- name: Wait for PyPI to update
|
||||
run: |
|
||||
sleep 120
|
||||
|
||||
- name: Update llama-parse lock file
|
||||
run: |
|
||||
cd llama_parse && poetry lock
|
||||
|
||||
- name: Build and publish llama-parse
|
||||
uses: JRubics/poetry-publish@v2.1
|
||||
with:
|
||||
working_directory: "llama_parse"
|
||||
package_directory: "./llama_parse"
|
||||
pypi_token: ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
|
||||
poetry_install_options: "--without dev"
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Poetry
|
||||
|
||||
@@ -3,3 +3,5 @@ __pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
.idea
|
||||
.env*
|
||||
.ipynb_checkpoints*
|
||||
|
||||
@@ -10,7 +10,7 @@ This includes:
|
||||
|
||||
- [LlamaParse](./parse.md) - A GenAI-native document parser that can parse complex document data for any downstream LLM use case (Agents, RAG, data processing, etc.).
|
||||
- [LlamaReport (beta/invite-only)](./report.md) - A prebuilt agentic report builder that can be used to build reports from a variety of data sources.
|
||||
- [LlamaExtract (coming soon!)]() - A prebuilt agentic data extractor that can be used to transform data into a structured JSON representation.
|
||||
- [LlamaExtract](./extract.md) - A prebuilt agentic data extractor that can be used to transform data into a structured JSON representation.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -25,17 +25,37 @@ Then, get your API key from [LlamaCloud](https://cloud.llamaindex.ai/).
|
||||
Then, you can use the services in your code:
|
||||
|
||||
```python
|
||||
from llama_cloud_services import LlamaParse, LlamaReport
|
||||
from llama_cloud_services import LlamaParse, LlamaReport, LlamaExtract
|
||||
|
||||
parser = LlamaParse(api_key="YOUR_API_KEY")
|
||||
report = LlamaReport(api_key="YOUR_API_KEY")
|
||||
extract = LlamaExtract(api_key="YOUR_API_KEY")
|
||||
```
|
||||
|
||||
See the quickstart guides for each service for more information:
|
||||
|
||||
- [LlamaParse](./parse.md)
|
||||
- [LlamaReport (beta/invite-only)](./report.md)
|
||||
- [LlamaExtract (coming soon!)]()
|
||||
- [LlamaExtract](./extract.md)
|
||||
|
||||
## Switch to EU SaaS 🇪🇺
|
||||
|
||||
If you are interested in using LlamaCloud services in the EU, you can adjust your base URL to `https://api.cloud.eu.llamaindex.ai`.
|
||||
|
||||
You can also create your API key in the EU region [here](https://cloud.eu.llamaindex.ai).
|
||||
|
||||
```python
|
||||
from llama_cloud_services import (
|
||||
LlamaParse,
|
||||
LlamaReport,
|
||||
LlamaExtract,
|
||||
EU_BASE_URL,
|
||||
)
|
||||
|
||||
parser = LlamaParse(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
|
||||
report = LlamaReport(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
|
||||
extract = LlamaExtract(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 3.3 MiB |
@@ -0,0 +1,10 @@
|
||||
# Financial Modeling Assumptions
|
||||
Discount Rate: 8%
|
||||
Terminal Growth Rate: 2%
|
||||
Tax Rate: 25%
|
||||
Revenue Growth (Years 1-5): 10% per annum
|
||||
Revenue Growth (Years 6-10): 5% per annum
|
||||
Capital Expenditures as % of Revenue: 7%
|
||||
Working Capital Assumption: 3% of Revenue
|
||||
Depreciation Rate: 10% per annum
|
||||
Cost of Capital Assumption: 8%
|
||||
|
After Width: | Height: | Size: 67 KiB |
@@ -0,0 +1 @@
|
||||
sec_form_4_dump.json
|
||||
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 893 KiB |
@@ -0,0 +1,440 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Extract Data from Financial Reports - with Citations and Reasoning\n",
|
||||
"\n",
|
||||
"Given complex files like financial reports, contracts, invoices etc, Llama Extract allows you to make use of an LLM to extract the information relevant to you, in a structured format.\n",
|
||||
"\n",
|
||||
"In this example, we'll be using [LlamaExtract](https://docs.cloud.llamaindex.ai/llamaextract/getting_started?utm_campaign=extract&utm_medium=recipe) to extract structured data from an SEC filing (specifically, the filing by Nvidia for fiscal year 2025).\n",
|
||||
"\n",
|
||||
"On top of simple data extraction, we'll ask our extraction agent to provide citations and reasoning for each extracted field. This allows us to:\n",
|
||||
"- Confirm the accuracy of the extracted field\n",
|
||||
"- Understand the reasoning behind why the LLM extracted a given piece of information\n",
|
||||
"- This last point allows us an opportunity to adjust the system prompt or field descriptions and improve on results where needed.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"The example we go through below is also replicable within Llama Cloud as well, where you will also be able to pick between a number of pre-defined schemas, instead of building your own."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install llama-cloud-services"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Connect to Llama Cloud\n",
|
||||
"\n",
|
||||
"To get started, make sure you provide your [Llama Cloud](https://cloud.llamaindex.ai?utm_campaign=extract&utm_medium=recipe) API key."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Enter your Llama Cloud API Key: ··········\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"from getpass import getpass\n",
|
||||
"\n",
|
||||
"if \"LLAMA_CLOUD_API_KEY\" not in os.environ:\n",
|
||||
" os.environ[\"LLAMA_CLOUD_API_KEY\"] = getpass(\"Enter your Llama Cloud API Key: \")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Extract Data with Llama Extract Agent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"No project_id provided, fetching default project.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaExtract\n",
|
||||
"\n",
|
||||
"# Optionally, provide your project id, if not, it will use the 'Default' project\n",
|
||||
"llama_extract = LlamaExtract()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Provide Your Custom Schema\n",
|
||||
"\n",
|
||||
"When using LlamaExtract via the API, you provide your own schema that describes what you want extracted from files and data provided to your agent. Here, we are essentially building an SEC filings extraction agent."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"from enum import Enum\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class FilingType(str, Enum):\n",
|
||||
" ten_k = \"10 K\"\n",
|
||||
" ten_q = \"10-Q\"\n",
|
||||
" ten_ka = \"10-K/A\"\n",
|
||||
" ten_qa = \"10-Q/A\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class FinancialReport(BaseModel):\n",
|
||||
" company_name: str = Field(description=\"The name of the company\")\n",
|
||||
" description: str = Field(\n",
|
||||
" description=\"Short description of the filing and what it contains\"\n",
|
||||
" )\n",
|
||||
" filing_type: FilingType = Field(description=\"Type of SEC filing\")\n",
|
||||
" filing_date: str = Field(description=\"Date when filing was submitted to SEC\")\n",
|
||||
" fiscal_year: int = Field(description=\"Fiscal year\")\n",
|
||||
" unit: str = Field(\n",
|
||||
" description=\"Unit of financial figures (thousands, millions, etc.)\"\n",
|
||||
" )\n",
|
||||
" revenue: int = Field(description=\"Total revenue for period\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Set Up Citations and Reasoning\n",
|
||||
"\n",
|
||||
"Optionally, we can set the `ExtractConfig` to extract citations for each field the agent extracts. These cications will cite the specific pages and sections of the file from which a given field was extractedd.\n",
|
||||
"\n",
|
||||
"By setting `use_reasoning` to True, we als ask the agent to do an additional reasoning step, explaining why a given field was extracted."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_cloud.types import ExtractConfig, ExtractMode\n",
|
||||
"\n",
|
||||
"config = ExtractConfig(\n",
|
||||
" use_reasoning=True, cite_sources=True, extraction_mode=ExtractMode.MULTIMODAL\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/usr/local/lib/python3.11/dist-packages/llama_cloud_services/extract/extract.py:127: ExperimentalWarning: `use_reasoning` is an experimental feature. Results will be available in the `extraction_metadata` field for the extraction run.\n",
|
||||
" warnings.warn(\n",
|
||||
"/usr/local/lib/python3.11/dist-packages/llama_cloud_services/extract/extract.py:133: ExperimentalWarning: `cite_sources` is an experimental feature. This may greatly increase the size of the response, and slow down the extraction. Results will be available in the `extraction_metadata` field for the extraction run.\n",
|
||||
" warnings.warn(\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"agent = llama_extract.create_agent(\n",
|
||||
" name=\"filing-parser\", data_schema=FinancialReport, config=config\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Demo Time - Download a PDF and Extract Data with Citations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"PDF downloaded successfully.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"url = \"https://raw.githubusercontent.com/run-llama/llama_cloud_services/refs/heads/main/examples/extract/data/sec_filings/nvda_10k.pdf\"\n",
|
||||
"\n",
|
||||
"response = requests.get(url)\n",
|
||||
"\n",
|
||||
"if response.status_code == 200:\n",
|
||||
" with open(\"/content/nvda_10k.pdf\", \"wb\") as f:\n",
|
||||
" f.write(response.content)\n",
|
||||
" print(\"PDF downloaded successfully.\")\n",
|
||||
"else:\n",
|
||||
" print(f\"Failed to download. Status code: {response.status_code}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Uploading files: 100%|██████████| 1/1 [00:00<00:00, 1.83it/s]\n",
|
||||
"Creating extraction jobs: 100%|██████████| 1/1 [00:00<00:00, 4.38it/s]\n",
|
||||
"Extracting files: 100%|██████████| 1/1 [02:03<00:00, 123.40s/it]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"filing_info = agent.extract(\"/content/nvda_10k.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'company_name': 'NVIDIA Corporation',\n",
|
||||
" 'description': \"The filing provides a detailed overview of NVIDIA's business as a full-stack computing infrastructure company, discusses various technologies including digital avatars and autonomous vehicles, outlines numerous risk factors affecting operations such as supply chain issues and geopolitical tensions, and describes employee stock purchase plans and related compliance requirements.\",\n",
|
||||
" 'filing_type': '10 K',\n",
|
||||
" 'filing_date': 'February 26, 2025',\n",
|
||||
" 'fiscal_year': 2025,\n",
|
||||
" 'unit': 'millions',\n",
|
||||
" 'revenue': 130497}"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"filing_info.data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Inspect Citations and Reasoning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'field_metadata': {'company_name': {'reasoning': 'VERBATIM EXTRACTION',\n",
|
||||
" 'citation': [{'page': 1, 'matching_text': 'NVIDIA CORPORATION'},\n",
|
||||
" {'page': 2, 'matching_text': 'NVIDIA Corporation'},\n",
|
||||
" {'page': 3,\n",
|
||||
" 'matching_text': 'All references to \"NVIDIA,\" \"we,\" \"us,\" \"our,\" or the \"Company\" mean NVIDIA Corporation and its subsidiaries.'},\n",
|
||||
" {'page': 35,\n",
|
||||
" 'matching_text': 'Comparison of 5 Year Cumulative Total Return* Among NVIDIA Corporation'},\n",
|
||||
" {'page': 49,\n",
|
||||
" 'matching_text': 'To the Board of Directors and Shareholders of NVIDIA Corporation'},\n",
|
||||
" {'page': 90, 'matching_text': 'NVIDIA Corporation'},\n",
|
||||
" {'page': 119,\n",
|
||||
" 'matching_text': '*\"Company\"* means NVIDIA Corporation, a Delaware corporation.'},\n",
|
||||
" {'page': 126,\n",
|
||||
" 'matching_text': 'Annual Report on Form 10-K of NVIDIA Corporation'}]},\n",
|
||||
" 'filing_type': {'reasoning': \"VERBATIM EXTRACTION from multiple sources confirming the filing type as '10 K'.\",\n",
|
||||
" 'citation': [{'page': 1, 'matching_text': 'FORM 10-K'},\n",
|
||||
" {'page': 2, 'matching_text': 'Item 16. | Form 10-K Summary'},\n",
|
||||
" {'page': 3,\n",
|
||||
" 'matching_text': 'This Annual Report on Form 10-K contains forward-looking statements...'},\n",
|
||||
" {'page': 13, 'matching_text': 'this Annual Report on Form 10-K'},\n",
|
||||
" {'page': 15, 'matching_text': 'this Annual Report on Form 10-K'},\n",
|
||||
" {'page': 32,\n",
|
||||
" 'matching_text': 'Annual Report on Form 10-K, which information is hereby incorporated by reference.'},\n",
|
||||
" {'page': 36, 'matching_text': 'this Annual Report on Form 10-K'},\n",
|
||||
" {'page': 43,\n",
|
||||
" 'matching_text': 'Annual Report on Form 10-K for additional information'},\n",
|
||||
" {'page': 45, 'matching_text': 'Annual Report on Form 10-K'},\n",
|
||||
" {'page': 46, 'matching_text': 'this Annual Report on Form 10-K'},\n",
|
||||
" {'page': 62, 'matching_text': 'Annual Report on Form 10-K'},\n",
|
||||
" {'page': 83,\n",
|
||||
" 'matching_text': 'Restated Certificate of Incorporation | 10-K'},\n",
|
||||
" {'page': 84, 'matching_text': 'Item 16. Form 10-K Summary'},\n",
|
||||
" {'page': 126, 'matching_text': 'which appears in this Form 10-K'},\n",
|
||||
" {'page': 127, 'matching_text': 'Annual Report on Form 10-K'},\n",
|
||||
" {'page': 128, 'matching_text': 'Annual Report on Form 10-K'},\n",
|
||||
" {'page': 129, 'matching_text': \"The Company's Annual Report on Form 10-K\"},\n",
|
||||
" {'page': 130,\n",
|
||||
" 'matching_text': \"The Company's Annual Report on Form 10-K for the year ended January 26, 2025\"}]},\n",
|
||||
" 'fiscal_year': {'reasoning': 'The fiscal year ended January 26, 2025, indicates the fiscal year is 2025. Additionally, multiple references throughout the text confirm the fiscal year 2025 in various contexts.',\n",
|
||||
" 'citation': [{'page': 1,\n",
|
||||
" 'matching_text': 'For the fiscal year ended January 26, 2025'},\n",
|
||||
" {'page': 6,\n",
|
||||
" 'matching_text': 'In fiscal year 2025, we launched the NVIDIA Blackwell architecture'},\n",
|
||||
" {'page': 12, 'matching_text': 'fiscal year 2025'},\n",
|
||||
" {'page': 17,\n",
|
||||
" 'matching_text': 'our gross margins in the second quarter of fiscal year 2025 were negatively impacted'},\n",
|
||||
" {'page': 20,\n",
|
||||
" 'matching_text': 'we generated 53% of our revenue in fiscal year 2025 from sales outside the United States.'},\n",
|
||||
" {'page': 23,\n",
|
||||
" 'matching_text': 'For fiscal year 2025, an indirect customer which primarily purchases our products through system integrators...'},\n",
|
||||
" {'page': 33,\n",
|
||||
" 'matching_text': 'In fiscal year 2025, we repurchased 310 million shares of our common stock for $34.0 billion.'},\n",
|
||||
" {'page': 37,\n",
|
||||
" 'matching_text': 'Our Data Center revenue in China grew in fiscal year 2025.'},\n",
|
||||
" {'page': 44,\n",
|
||||
" 'matching_text': 'Cash provided by operating activities increased in fiscal year 2025 compared to fiscal year 2024'},\n",
|
||||
" {'page': 57,\n",
|
||||
" 'matching_text': 'Fiscal years 2025, 2024 and 2023 were all 52-week years.'},\n",
|
||||
" {'page': 65,\n",
|
||||
" 'matching_text': 'Beginning in the second quarter of fiscal year 2025'},\n",
|
||||
" {'page': 69, 'matching_text': 'In the fourth quarter of fiscal year 2025'},\n",
|
||||
" {'page': 78,\n",
|
||||
" 'matching_text': 'Depreciation and amortization expense attributable to our Compute and Networking segment for fiscal years 2025'},\n",
|
||||
" {'page': 129, 'matching_text': 'for the year ended January 26, 2025'}]},\n",
|
||||
" 'description': {'reasoning': 'The extracted data combines multiple descriptions from the source text, ensuring no duplication while maintaining the order and context of the information. Each section of the filing is summarized to reflect the key points without losing the essence of the original text.',\n",
|
||||
" 'citation': [{'page': 4,\n",
|
||||
" 'matching_text': 'NVIDIA is now a full-stack computing infrastructure company with data-center-scale offerings that are reshaping industry.'},\n",
|
||||
" {'page': 8,\n",
|
||||
" 'matching_text': 'a suite of technologies that help developers bring digital avatars to life with generative Al...autonomous vehicles, or AV, and electric vehicles, or EV, is revolutionizing the transportation industry...Our worldwide sales and marketing strategy is key to achieving our objective of providing markets with our high-performance and efficient computing platforms and software.'},\n",
|
||||
" {'page': 14, 'matching_text': 'Risk Factors Summary'},\n",
|
||||
" {'page': 16,\n",
|
||||
" 'matching_text': 'Risks Related to Demand, Supply, and Manufacturing\\n\\nLong manufacturing lead times and uncertain supply and component availability...'},\n",
|
||||
" {'page': 18,\n",
|
||||
" 'matching_text': 'cryptocurrency mining, on demand for our products. Volatility in the cryptocurrency market, including new compute technologies...'},\n",
|
||||
" {'page': 21,\n",
|
||||
" 'matching_text': 'supply-chain attacks or other business disruptions. We cannot guarantee that third parties and infrastructure in our supply chain...'},\n",
|
||||
" {'page': 22,\n",
|
||||
" 'matching_text': 'We are monitoring the impact of the geopolitical conflict in and around Israel on our operations... Climate change may have a long-term impact on our business.'},\n",
|
||||
" {'page': 25,\n",
|
||||
" 'matching_text': 'We are subject to complex laws, rules, regulations, and political and other actions, including restrictions on the export of our products, which may adversely impact our business.'},\n",
|
||||
" {'page': 28,\n",
|
||||
" 'matching_text': 'Our competitive position has been harmed by the existing export controls, and our competitive position and future results may be further harmed'},\n",
|
||||
" {'page': 29,\n",
|
||||
" 'matching_text': 'restrictions imposed by the Chinese government on the duration of gaming activities and access to games may adversely affect our Gaming revenue'},\n",
|
||||
" {'page': 29,\n",
|
||||
" 'matching_text': 'our business depends on our ability to receive consistent and reliable supply from our overseas partners, especially in Taiwan and South Korea'},\n",
|
||||
" {'page': 29,\n",
|
||||
" 'matching_text': 'Increased scrutiny from shareholders, regulators and others regarding our corporate sustainability practices could result in additional costs'},\n",
|
||||
" {'page': 29,\n",
|
||||
" 'matching_text': 'Concerns relating to the responsible use of new and evolving technologies, such as Al, in our products and services may result in reputational or financial harm'},\n",
|
||||
" {'page': 31,\n",
|
||||
" 'matching_text': 'Data protection laws around the world are quickly changing and may be interpreted and applied in an increasingly stringent fashion...'}]},\n",
|
||||
" 'filing_date': {'reasoning': 'The filing date is consistently mentioned as February 26, 2025 across multiple entries, making it the most reliable date for the filing.',\n",
|
||||
" 'citation': [{'page': 51, 'matching_text': 'February 26, 2025'},\n",
|
||||
" {'page': 86, 'matching_text': 'on February 26, 2025.'},\n",
|
||||
" {'page': 87, 'matching_text': 'February 26, 2025'},\n",
|
||||
" {'page': 126, 'matching_text': 'our report dated February 26, 2025'},\n",
|
||||
" {'page': 127, 'matching_text': 'Date: February 26, 2025'},\n",
|
||||
" {'page': 128, 'matching_text': 'Date: February 26, 2025'},\n",
|
||||
" {'page': 129, 'matching_text': 'Date: February 26, 2025'},\n",
|
||||
" {'page': 130, 'matching_text': 'Date: February 26, 2025'}]},\n",
|
||||
" 'unit': {'reasoning': \"The unit of financial figures is explicitly mentioned multiple times in the text as 'millions', including in table headers and notes. This is confirmed by various citations from pages 38, 42, 43, 52, 53, 54, 56, 65, 71, 72, 73, 75, 77, 79, 80, and 82.\",\n",
|
||||
" 'citation': [{'page': 38,\n",
|
||||
" 'matching_text': '($ in millions, except per share data)'},\n",
|
||||
" {'page': 42, 'matching_text': '($ in millions)'},\n",
|
||||
" {'page': 43, 'matching_text': '($ in millions)'},\n",
|
||||
" {'page': 52, 'matching_text': '(In millions, except per share data)'},\n",
|
||||
" {'page': 53,\n",
|
||||
" 'matching_text': 'Consolidated Statements of Comprehensive Income (In millions)'},\n",
|
||||
" {'page': 54,\n",
|
||||
" 'matching_text': 'Consolidated Balance Sheets (In millions, except par value)'},\n",
|
||||
" {'page': 55, 'matching_text': '(In millions, except per share data)'},\n",
|
||||
" {'page': 56,\n",
|
||||
" 'matching_text': 'Consolidated Statements of Cash Flows (In millions)'},\n",
|
||||
" {'page': 65,\n",
|
||||
" 'matching_text': 'Year Ended<br/>Jan 26, 2025<br/>(In millions, except per share data)'},\n",
|
||||
" {'page': 71, 'matching_text': '(In millions) | (In millions)'},\n",
|
||||
" {'page': 72, 'matching_text': '(In millions)'}]},\n",
|
||||
" 'revenue': {'reasoning': 'The total revenue for fiscal year 2025 is extracted from multiple sources within the text, all confirming the same figure of $130,497 million. The revenue recognized for fiscal year 2025 is also noted as $4,607 million, which is a separate figure. However, the primary focus is on the total revenue figure, which is consistently cited.',\n",
|
||||
" 'citation': [{'page': 38,\n",
|
||||
" 'matching_text': 'Revenue for fiscal year 2025 was $130.5 billion'},\n",
|
||||
" {'page': 41,\n",
|
||||
" 'matching_text': 'Total | $ 130,497 | $ | 60,922'},\n",
|
||||
" {'page': 52, 'matching_text': 'Revenue | $ 130,497'},\n",
|
||||
" {'page': 78,\n",
|
||||
" 'matching_text': 'Revenue | $ 116,193 | $ 14,304 | $ - | $ 130,497'},\n",
|
||||
" {'page': 79, 'matching_text': 'Total revenue | $ 130,497'},\n",
|
||||
" {'page': 80, 'matching_text': 'Total revenue | $ 130,497'}]}},\n",
|
||||
" 'usage': {'num_pages_extracted': 130,\n",
|
||||
" 'num_document_tokens': 105932,\n",
|
||||
" 'num_output_tokens': 31306}}"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"filing_info.extraction_metadata"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## What's Next?\n",
|
||||
"\n",
|
||||
"In this example, we built an Extraction Agent that is capable of citing it's sources from the document it's extracting data from, and reasoning about its reponse. To further customize and improve on the results, you can also try to customize the `system_prompt` in the `ExtractConfig`.\n",
|
||||
"\n",
|
||||
"#### Learn More\n",
|
||||
"\n",
|
||||
"- [LlamaExtract Documentation](https://docs.cloud.llamaindex.ai/llamaextract/getting_started)\n",
|
||||
"- [Example Notebooks](https://github.com/run-llama/llama_cloud_services/tree/main/examples/extract)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1f6bd03d-1b8b-45a0-bc2c-5a13f1a5d8d3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# LM317 Voltage Regulator Datasheet Structured Extraction\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/extract/lm317_structured_extraction.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"This notebook demonstrates an agentic document workflow using LlamaExtract to process an LM317 voltage regulator datasheet. In this example, we define a structured extraction schema that converts key technical fields into standardized subfields. For instance, the output voltage is split into a minimum and maximum value with a defined unit, and we capture page citations for each extracted field.\n",
|
||||
"\n",
|
||||
"The target user is an electronics engineer at a component manufacturing company who needs to consolidate datasheet information into a standardized specification sheet for design and quality control.\n",
|
||||
"\n",
|
||||
"This approach reduces manual data entry, improves extraction accuracy and standardization, and provides traceability for each technical detail."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a3b8c8d5-ff3e-48ce-b0b8-29b6b1f517f8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Use Case Overview\n",
|
||||
"\n",
|
||||
"### Problem\n",
|
||||
"Datasheets like that for the LM317 regulator are often distributed as PDFs containing multiple tables, charts, and complex textual descriptions. Engineers must manually extract technical details such as voltage ranges, dropout voltage, maximum current, input voltage range, and pin configurations. This process is error-prone and time-consuming.\n",
|
||||
"\n",
|
||||
"### Agent Workflow (Combination of Automation and Chat)\n",
|
||||
"1. **Upload Datasheet:** The engineer uploads the LM317 datasheet PDF. \n",
|
||||
"2. **Structured Extraction:** An automated agent processes the PDF and extracts key technical details into structured fields (e.g., output voltage as a range with separate min/max values).\n",
|
||||
"3. **Interactive Verification:** The engineer can query the agent (via chat) for further details or clarification (e.g., \"Show me the detailed pin configuration extraction\") and review the cited pages.\n",
|
||||
"\n",
|
||||
"**Value Delivered:**\n",
|
||||
"- Up to 70% reduction in manual data extraction time.\n",
|
||||
"- Increased accuracy and standardization with structured fields."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a704e843-54be-4969-842b-713584cb3c35",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup and Download Data\n",
|
||||
"\n",
|
||||
"Download the [LM317 Datasheet](https://www.ti.com/lit/ds/symlink/lm317.pdf) and setup LlamaExtract."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "6e5b1f91-8785-44d4-a710-8be1b48b76de",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!mkdir -p data/lm317_structured_extraction\n",
|
||||
"!wget https://www.ti.com/lit/ds/symlink/lm317.pdf -O data/lm317_structured_extraction/lm317.pdf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "f17b914a-00ed-4b63-8198-69fd7c4a7c62",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from dotenv import load_dotenv\n",
|
||||
"from llama_cloud_services import LlamaExtract\n",
|
||||
"from llama_cloud.core.api_error import ApiError\n",
|
||||
"\n",
|
||||
"# Load environment variables (ensure LLAMA_CLOUD_API_KEY is set in your .env file)\n",
|
||||
"load_dotenv(override=True)\n",
|
||||
"\n",
|
||||
"# Initialize the LlamaExtract client\n",
|
||||
"llama_extract = LlamaExtract(\n",
|
||||
" project_id=\"<project_id>\",\n",
|
||||
" organization_id=\"<organization_id>\",\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ed9f6e9a-96c8-4ee1-8b45-0b6a4f7dbbf1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Defining a Structured Extraction Schema\n",
|
||||
"\n",
|
||||
"We now define a rich Pydantic schema to extract technical specifications from the LM317 datasheet. In this schema:\n",
|
||||
"\n",
|
||||
"- The **output_voltage** and **input_voltage** fields are structured as ranges with separate minimum and maximum values and a unit.\n",
|
||||
"- The **pin_configuration** field is structured to include a pin count and a descriptive layout.\n",
|
||||
"- Additional technical fields (e.g., dropout voltage, max current) are captured as numbers.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "4f7e9b44-5e69-4b30-9864-cd98f1e2a7d4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class VoltageRange(BaseModel):\n",
|
||||
" min_voltage: float = Field(..., description=\"Minimum voltage in volts\")\n",
|
||||
" max_voltage: float = Field(..., description=\"Maximum voltage in volts\")\n",
|
||||
" unit: str = Field(\"V\", description=\"Voltage unit\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class PinConfiguration(BaseModel):\n",
|
||||
" pin_count: int = Field(..., description=\"Number of pins\")\n",
|
||||
" layout: str = Field(..., description=\"Detailed pin layout description\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class LM317Spec(BaseModel):\n",
|
||||
" component_name: str = Field(..., description=\"Name of the component\")\n",
|
||||
" output_voltage: VoltageRange = Field(\n",
|
||||
" ..., description=\"Output voltage range specification\"\n",
|
||||
" )\n",
|
||||
" dropout_voltage: float = Field(..., description=\"Dropout voltage in volts\")\n",
|
||||
" max_current: float = Field(..., description=\"Maximum current rating in amperes\")\n",
|
||||
" input_voltage: VoltageRange = Field(\n",
|
||||
" ..., description=\"Input voltage range specification\"\n",
|
||||
" )\n",
|
||||
" pin_configuration: PinConfiguration = Field(\n",
|
||||
" ..., description=\"Pin configuration details\"\n",
|
||||
" )\n",
|
||||
" features: List[str] = Field([], description=\"List of additional technical features\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class LM317Schema(BaseModel):\n",
|
||||
" specs: List[LM317Spec] = Field(\n",
|
||||
" ..., description=\"List of extracted LM317 technical specifications\"\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "e0508e38-35be-446c-afe7-129e39553281",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"try:\n",
|
||||
" existing_agent = llama_extract.get_agent(name=\"lm317-datasheet\")\n",
|
||||
" if existing_agent:\n",
|
||||
" llama_extract.delete_agent(existing_agent.id)\n",
|
||||
"except ApiError as e:\n",
|
||||
" if e.status_code == 404:\n",
|
||||
" pass\n",
|
||||
" else:\n",
|
||||
" raise"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "bb197dfd-dd37-459e-8953-cc1b12f25bdd",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Here we use our balanced extraction mode."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "e3defc0a-c685-4fbd-bbb1-1270f1442e72",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_cloud import ExtractConfig\n",
|
||||
"\n",
|
||||
"extract_config = ExtractConfig(\n",
|
||||
" extraction_mode=\"BALANCED\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"agent = llama_extract.create_agent(\n",
|
||||
" name=\"lm317-datasheet\", data_schema=LM317Schema, config=extract_config\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c0a0f9f9-2ef3-4a38-bd74-68d2c2e9e2d8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Extracting Information from the LM317 Datasheet\n",
|
||||
"\n",
|
||||
"For this demonstration, please download a publicly available LM317 voltage regulator datasheet (for example, from Texas Instruments) and save it as `lm317.pdf` in the `./data` directory. Then run the cell below to extract the structured technical specifications."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "c58e8b7a-8f9b-46f3-8f72-3c2f96b49e8f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Uploading files: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:01<00:00, 1.08s/it]\n",
|
||||
"Creating extraction jobs: 100%|████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 1.96it/s]\n",
|
||||
"Extracting files: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [01:27<00:00, 87.38s/it]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Path to the LM317 datasheet PDF\n",
|
||||
"lm317_pdf = \"./data/lm317_structured_extraction/lm317.pdf\"\n",
|
||||
"\n",
|
||||
"# Extract structured technical specifications from the datasheet\n",
|
||||
"lm317_extract = agent.extract(lm317_pdf)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1a2e2e44-6c48-4a38-a6de-5f2f3c7d4d8b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Assessing the Extraction Results\n",
|
||||
"\n",
|
||||
"The output will be a consolidated list of LM317 technical specifications. For each entry, you should see structured fields including:\n",
|
||||
"\n",
|
||||
"- **component_name**\n",
|
||||
"- **output_voltage** as a range (with separate `min_voltage` and `max_voltage` plus `unit`)\n",
|
||||
"- **dropout_voltage** and **max_current** as numbers\n",
|
||||
"- **input_voltage** as a structured range\n",
|
||||
"- **pin_configuration** with a `pin_count` and `layout`\n",
|
||||
"- **features** (if available)\n",
|
||||
"\n",
|
||||
"This structured approach makes it easier to standardize the information for downstream integration and verification. Engineers can click on the cited page numbers (in a UI that supports it) to validate the extraction."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "fb2abc44-7c9b-4b19-958e-d0d7b390ae57",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'specs': [{'component_name': 'LM317',\n",
|
||||
" 'output_voltage': {'min_voltage': 1.25, 'max_voltage': 37.0, 'unit': 'V'},\n",
|
||||
" 'dropout_voltage': 0.0,\n",
|
||||
" 'max_current': 1.5,\n",
|
||||
" 'input_voltage': {'min_voltage': 4.25, 'max_voltage': 40.0, 'unit': 'V'},\n",
|
||||
" 'pin_configuration': {'pin_count': 3,\n",
|
||||
" 'layout': '1: ADJUST, 2: OUTPUT, 3: INPUT'},\n",
|
||||
" 'features': ['Output voltage range adjustable from 1.25 V to 37 V',\n",
|
||||
" 'Output current greater than 1.5 A',\n",
|
||||
" 'Internal short-circuit current limiting',\n",
|
||||
" 'Thermal overload protection',\n",
|
||||
" 'Output safe-area compensation']}]}"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Display the extraction results\n",
|
||||
"lm317_extract.data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c7a2a523-095e-40bf-b713-f509c13a7747",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can also see the output result in the UI."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "dc22dfa5-b667-4fb0-8dbe-24e401b12389",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e0e0c12a-9f89-4bb3-b40d-3e9f7c6d2fef",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Conclusion\n",
|
||||
"\n",
|
||||
"This notebook demonstrated how to use LlamaExtract with a structured extraction schema for the LM317 voltage regulator datasheet. By defining detailed subfields (such as splitting voltage ranges into minimum and maximum values, and structuring the pin configuration), we ensure that the extracted data is standardized and traceable through page citations. This approach minimizes manual effort and improves accuracy, providing a robust example of an agentic document workflow for technical documentation processing.\n",
|
||||
"\n",
|
||||
"Feel free to modify or extend the schema to capture additional technical details or to suit your own use cases."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "llama_parse",
|
||||
"language": "python",
|
||||
"name": "llama_parse"
|
||||
},
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,834 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Extracting data from Resumes\n",
|
||||
"\n",
|
||||
"Let us assume that we are running a hiring process for a company and we have received a list of resumes from candidates. We want to extract structured data from the resumes so that we can run a screening process and shortlist candidates. \n",
|
||||
"\n",
|
||||
"Take a look at one of the resumes in the `data/resumes` directory. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"\n",
|
||||
" <iframe\n",
|
||||
" width=\"600\"\n",
|
||||
" height=\"400\"\n",
|
||||
" src=\"./data/resumes/ai_researcher.pdf\"\n",
|
||||
" frameborder=\"0\"\n",
|
||||
" allowfullscreen\n",
|
||||
" \n",
|
||||
" ></iframe>\n",
|
||||
" "
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.lib.display.IFrame at 0x109a7dcd0>"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from IPython.display import IFrame\n",
|
||||
"\n",
|
||||
"IFrame(src=\"./data/resumes/ai_researcher.pdf\", width=600, height=400)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You will notice that all the resumes have different layouts but contain common information like name, email, experience, education, etc. \n",
|
||||
"\n",
|
||||
"With LlamaExtract, we will show you how to:\n",
|
||||
"- *Define* a data schema to extract the information of interest. \n",
|
||||
"- *Iterate* over the data schema to generalize the schema for multiple resumes.\n",
|
||||
"- *Finalize* the schema and schedule extractions for multiple resumes.\n",
|
||||
"\n",
|
||||
"We will start by defining a `LlamaExtract` client which provides a Python interface to the LlamaExtract API. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"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",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Defining the data schema\n",
|
||||
"\n",
|
||||
"Next, let us try to extract two fields from the resume: `name` and `email`. We can either use a Python dictionary structure to define the `data_schema` as a JSON or use a Pydantic model instead, for brevity and convenience. In either case, our output is guaranteed to validate against this schema."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Resume(BaseModel):\n",
|
||||
" name: str = Field(description=\"The name of the candidate\")\n",
|
||||
" email: str = Field(description=\"The email address of the candidate\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Uploading files: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:02<00:00, 2.20s/it]\n",
|
||||
"Creating extraction jobs: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:02<00:00, 2.93s/it]\n",
|
||||
"Extracting files: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:02<00:00, 2.94s/it]\n",
|
||||
"Uploading files: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 1.13it/s]\n",
|
||||
"Creating extraction jobs: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 1.80it/s]\n",
|
||||
"Extracting files: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:15<00:00, 15.18s/it]\n",
|
||||
"Uploading files: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 1.16it/s]\n",
|
||||
"Creating extraction jobs: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 2.33it/s]\n",
|
||||
"Extracting files: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:32<00:00, 32.86s/it]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from llama_cloud.core.api_error import ApiError\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" existing_agent = llama_extract.get_agent(name=\"resume-screening\")\n",
|
||||
" if existing_agent:\n",
|
||||
" llama_extract.delete_agent(existing_agent.id)\n",
|
||||
"except ApiError as e:\n",
|
||||
" if e.status_code == 404:\n",
|
||||
" pass\n",
|
||||
" else:\n",
|
||||
" raise\n",
|
||||
"\n",
|
||||
"agent = llama_extract.create_agent(name=\"resume-screening\", data_schema=Resume)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[ExtractionAgent(id=1fef43b5-8230-43b4-9e80-c1cddf53889c, name=resume-screening),\n",
|
||||
" ExtractionAgent(id=93f8508b-3570-46f0-ae62-6315b40043bd, name=receipt/noisebridge_receipt.pdf_56db3d92),\n",
|
||||
" ExtractionAgent(id=08315f0e-7146-430b-99b8-9701cb3ace6a, name=receipt/noisebridge_receipt.pdf_5c4730a7),\n",
|
||||
" ExtractionAgent(id=cfcd7756-015d-4dbd-b142-a3eefcb16cd3, name=resume/software_architect_resume.html_4a11cf15),\n",
|
||||
" ExtractionAgent(id=17cb83d9-601e-4f5c-a7aa-286e3045bcb4, name=resume/software_architect_resume.html_0b7d84a8),\n",
|
||||
" ExtractionAgent(id=adc8e88c-44d3-4613-a5aa-d666ef007494, name=slide/saas_slide.pdf_bcc627a5),\n",
|
||||
" ExtractionAgent(id=189f14cd-6370-4476-a6ad-36eafbc62618, name=slide/saas_slide.pdf_065aa22b),\n",
|
||||
" ExtractionAgent(id=b9938ca5-6225-43cb-89ea-b0065237792f, name=test2),\n",
|
||||
" ExtractionAgent(id=574d37b8-59dc-41e9-bde0-5c506a8eb670, name=test)]"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"llama_extract.list_agents()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'name': 'Dr. Rachel Zhang', 'email': 'rachel.zhang@email.com'}"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"resume = agent.extract(\"./data/resumes/ai_researcher.pdf\")\n",
|
||||
"resume.data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Iterating over the data schema\n",
|
||||
"\n",
|
||||
"Now that we have created a data schema, let us add more fields to the schema. We will add `experience` and `education` fields to the schema. \n",
|
||||
"- We can create a new Pydantic model for each of these fields and represent `experience` and `education` as lists of these models. Doing this will allow us to extract multiple entities from the resume without having to pre-define how many experiences or education the candidate has. \n",
|
||||
"- We have added a `description` parameter to provide more context for extraction. We can use `description` to provide example inputs/outputs for the extraction. \n",
|
||||
"- Note that we have annotated the `start_date` and `end_date` fields with `Optional[str]` to indicate that these fields are optional. This is *important* because the schema will be used to extract data from multiple resumes and not all resumes will have the same format. A field must only be required if it is guaranteed to be present in all the resumes. \n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import List, Optional\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Education(BaseModel):\n",
|
||||
" institution: str = Field(description=\"The institution of the candidate\")\n",
|
||||
" degree: str = Field(description=\"The degree of the candidate\")\n",
|
||||
" start_date: Optional[str] = Field(\n",
|
||||
" default=None, description=\"The start date of the candidate's education\"\n",
|
||||
" )\n",
|
||||
" end_date: Optional[str] = Field(\n",
|
||||
" default=None, description=\"The end date of the candidate's education\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Experience(BaseModel):\n",
|
||||
" company: str = Field(description=\"The name of the company\")\n",
|
||||
" title: str = Field(description=\"The title of the candidate\")\n",
|
||||
" description: Optional[str] = Field(\n",
|
||||
" default=None, description=\"The description of the candidate's experience\"\n",
|
||||
" )\n",
|
||||
" start_date: Optional[str] = Field(\n",
|
||||
" default=None, description=\"The start date of the candidate's experience\"\n",
|
||||
" )\n",
|
||||
" end_date: Optional[str] = Field(\n",
|
||||
" default=None, description=\"The end date of the candidate's experience\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Resume(BaseModel):\n",
|
||||
" name: str = Field(description=\"The name of the candidate\")\n",
|
||||
" email: str = Field(description=\"The email address of the candidate\")\n",
|
||||
" links: List[str] = Field(\n",
|
||||
" description=\"The links to the candidate's social media profiles\"\n",
|
||||
" )\n",
|
||||
" experience: List[Experience] = Field(description=\"The candidate's experience\")\n",
|
||||
" education: List[Education] = Field(description=\"The candidate's education\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Next, we will update the `data_schema` for the `resume-screening` agent to use the new `Resume` model. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'name': 'Dr. Rachel Zhang',\n",
|
||||
" 'email': 'rachel.zhang@email.com',\n",
|
||||
" 'links': ['linkedin.com/in/rachelzhang',\n",
|
||||
" 'github.com/rzhang-ai',\n",
|
||||
" 'scholar.google.com/rachelzhang'],\n",
|
||||
" 'experience': [{'company': 'DeepMind',\n",
|
||||
" 'title': 'Senior Research Scientist',\n",
|
||||
" 'description': '- Lead researcher on large-scale multi-task learning systems, developing novel architectures that improve cross-task generalization by 40%\\n- Pioneered new approach to zero-shot learning using contrastive training, published in NeurIPS 2023\\n- Built and led team of 6 researchers working on foundational ML models\\n- Developed novel regularization techniques for large language models, reducing catastrophic forgetting by 35%',\n",
|
||||
" 'start_date': '2019',\n",
|
||||
" 'end_date': 'Present'},\n",
|
||||
" {'company': 'Google Research',\n",
|
||||
" 'title': 'Research Scientist',\n",
|
||||
" 'description': '- Developed probabilistic frameworks for robust ML, published in ICML 2018\\n- Created novel attention mechanisms for computer vision models, improving accuracy by 25%\\n- Led collaboration with Google Brain team on efficient training methods for transformer models\\n- Mentored 4 PhD interns and collaborated with academic institutions',\n",
|
||||
" 'start_date': '2015',\n",
|
||||
" 'end_date': '2019'},\n",
|
||||
" {'company': 'Columbia University',\n",
|
||||
" 'title': 'Research Assistant Professor',\n",
|
||||
" 'description': '- Published seminal work on Bayesian optimization methods (cited 1000+ times)\\n- Taught graduate-level courses in Machine Learning and Statistical Learning Theory\\n- Supervised 5 PhD students and 3 MSc students\\n- Secured $500K in research grants for probabilistic ML research',\n",
|
||||
" 'start_date': '2011',\n",
|
||||
" 'end_date': '2015'}],\n",
|
||||
" 'education': [{'institution': 'Columbia University',\n",
|
||||
" 'degree': 'Ph.D. in Computer Science',\n",
|
||||
" 'start_date': '2007',\n",
|
||||
" 'end_date': '2011'},\n",
|
||||
" {'institution': 'Stanford University',\n",
|
||||
" 'degree': 'M.S. in Computer Science',\n",
|
||||
" 'start_date': '2005',\n",
|
||||
" 'end_date': '2007'}]}"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"agent.data_schema = Resume\n",
|
||||
"resume = agent.extract(\"./data/resumes/ai_researcher.pdf\")\n",
|
||||
"resume.data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This is a good start. Let us add a few more fields to the schema and re-run the extraction. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class TechnicalSkills(BaseModel):\n",
|
||||
" programming_languages: List[str] = Field(\n",
|
||||
" description=\"The programming languages the candidate is proficient in.\"\n",
|
||||
" )\n",
|
||||
" frameworks: List[str] = Field(\n",
|
||||
" description=\"The tools/frameworks the candidate is proficient in, e.g. React, Django, PyTorch, etc.\"\n",
|
||||
" )\n",
|
||||
" skills: List[str] = Field(\n",
|
||||
" description=\"Other general skills the candidate is proficient in, e.g. Data Engineering, Machine Learning, etc.\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Resume(BaseModel):\n",
|
||||
" name: str = Field(description=\"The name of the candidate\")\n",
|
||||
" email: str = Field(description=\"The email address of the candidate\")\n",
|
||||
" links: List[str] = Field(\n",
|
||||
" description=\"The links to the candidate's social media profiles\"\n",
|
||||
" )\n",
|
||||
" experience: List[Experience] = Field(description=\"The candidate's experience\")\n",
|
||||
" education: List[Education] = Field(description=\"The candidate's education\")\n",
|
||||
" technical_skills: TechnicalSkills = Field(\n",
|
||||
" description=\"The candidate's technical skills\"\n",
|
||||
" )\n",
|
||||
" key_accomplishments: str = Field(\n",
|
||||
" description=\"Summarize the candidates highest achievements.\"\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'name': 'Dr. Rachel Zhang, Ph.D.',\n",
|
||||
" 'email': 'rachel.zhang@email.com',\n",
|
||||
" 'links': ['linkedin.com/in/rachelzhang',\n",
|
||||
" 'github.com/rzhang-ai',\n",
|
||||
" 'scholar.google.com/rachelzhang'],\n",
|
||||
" 'experience': [{'company': 'DeepMind',\n",
|
||||
" 'title': 'Senior Research Scientist',\n",
|
||||
" 'description': 'Lead researcher on large-scale multi-task learning systems, developing novel architectures that improve cross-task generalization by 40%\\nPioneered new approach to zero-shot learning using contrastive training, published in NeurIPS 2023\\nBuilt and led team of 6 researchers working on foundational ML models\\nDeveloped novel regularization techniques for large language models, reducing catastrophic forgetting by 35%',\n",
|
||||
" 'start_date': '2019',\n",
|
||||
" 'end_date': 'Present'},\n",
|
||||
" {'company': 'Google Research',\n",
|
||||
" 'title': 'Research Scientist',\n",
|
||||
" 'description': 'Developed probabilistic frameworks for robust ML, published in ICML 2018\\nCreated novel attention mechanisms for computer vision models, improving accuracy by 25%\\nLed collaboration with Google Brain team on efficient training methods for transformer models\\nMentored 4 PhD interns and collaborated with academic institutions',\n",
|
||||
" 'start_date': '2015',\n",
|
||||
" 'end_date': '2019'},\n",
|
||||
" {'company': 'Columbia University',\n",
|
||||
" 'title': 'Research Assistant Professor',\n",
|
||||
" 'description': 'Published seminal work on Bayesian optimization methods (cited 1000+ times)\\nTaught graduate-level courses in Machine Learning and Statistical Learning Theory\\nSupervised 5 PhD students and 3 MSc students\\nSecured $500K in research grants for probabilistic ML research',\n",
|
||||
" 'start_date': '2011',\n",
|
||||
" 'end_date': '2015'}],\n",
|
||||
" 'education': [{'institution': 'Columbia University',\n",
|
||||
" 'degree': 'Ph.D. in Computer Science',\n",
|
||||
" 'start_date': '2007',\n",
|
||||
" 'end_date': '2011'},\n",
|
||||
" {'institution': 'Stanford University',\n",
|
||||
" 'degree': 'M.S. in Computer Science',\n",
|
||||
" 'start_date': '2005',\n",
|
||||
" 'end_date': '2007'}],\n",
|
||||
" 'technical_skills': {'programming_languages': ['Python',\n",
|
||||
" 'C++',\n",
|
||||
" 'Julia',\n",
|
||||
" 'CUDA'],\n",
|
||||
" 'frameworks': ['PyTorch', 'TensorFlow', 'JAX', 'Ray'],\n",
|
||||
" 'skills': ['Deep Learning',\n",
|
||||
" 'Reinforcement Learning',\n",
|
||||
" 'Probabilistic Models',\n",
|
||||
" 'Multi-Task Learning',\n",
|
||||
" 'Zero-Shot Learning',\n",
|
||||
" 'Neural Architecture Search']},\n",
|
||||
" 'key_accomplishments': 'AI researcher with 12+ years of experience spanning classical machine learning, deep learning, and probabilistic modeling. Led groundbreaking research in reinforcement learning, generative models, and multi-task learning. Published 25+ papers in top-tier conferences (NeurIPS, ICML, ICLR). Strong track record of transitioning theoretical advances into practical applications in both academic and industrial settings.'}"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"agent.data_schema = Resume\n",
|
||||
"resume = agent.extract(\"./data/resumes/ai_researcher.pdf\")\n",
|
||||
"resume.data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Finalizing the schema\n",
|
||||
"\n",
|
||||
"This is great! We have extracted a lot of key information from the resume that is well-typed and can be used downstream for further processing. Until now, this data is ephemeral and will be lost if we close the session. Let us save the state of our extraction and use it to extract data from multiple resumes. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"agent.save()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'type': 'object',\n",
|
||||
" 'required': ['name',\n",
|
||||
" 'email',\n",
|
||||
" 'links',\n",
|
||||
" 'experience',\n",
|
||||
" 'education',\n",
|
||||
" 'technical_skills',\n",
|
||||
" 'key_accomplishments'],\n",
|
||||
" 'properties': {'name': {'type': 'string',\n",
|
||||
" 'description': 'The name of the candidate'},\n",
|
||||
" 'email': {'type': 'string',\n",
|
||||
" 'description': 'The email address of the candidate'},\n",
|
||||
" 'links': {'type': 'array',\n",
|
||||
" 'items': {'type': 'string'},\n",
|
||||
" 'description': \"The links to the candidate's social media profiles\"},\n",
|
||||
" 'education': {'type': 'array',\n",
|
||||
" 'items': {'type': 'object',\n",
|
||||
" 'required': ['institution', 'degree', 'start_date', 'end_date'],\n",
|
||||
" 'properties': {'degree': {'type': 'string',\n",
|
||||
" 'description': 'The degree of the candidate'},\n",
|
||||
" 'end_date': {'anyOf': [{'type': 'string'}, {'type': 'null'}],\n",
|
||||
" 'description': \"The end date of the candidate's education\"},\n",
|
||||
" 'start_date': {'anyOf': [{'type': 'string'}, {'type': 'null'}],\n",
|
||||
" 'description': \"The start date of the candidate's education\"},\n",
|
||||
" 'institution': {'type': 'string',\n",
|
||||
" 'description': 'The institution of the candidate'}},\n",
|
||||
" 'additionalProperties': False},\n",
|
||||
" 'description': \"The candidate's education\"},\n",
|
||||
" 'experience': {'type': 'array',\n",
|
||||
" 'items': {'type': 'object',\n",
|
||||
" 'required': ['company', 'title', 'description', 'start_date', 'end_date'],\n",
|
||||
" 'properties': {'title': {'type': 'string',\n",
|
||||
" 'description': 'The title of the candidate'},\n",
|
||||
" 'company': {'type': 'string', 'description': 'The name of the company'},\n",
|
||||
" 'end_date': {'anyOf': [{'type': 'string'}, {'type': 'null'}],\n",
|
||||
" 'description': \"The end date of the candidate's experience\"},\n",
|
||||
" 'start_date': {'anyOf': [{'type': 'string'}, {'type': 'null'}],\n",
|
||||
" 'description': \"The start date of the candidate's experience\"},\n",
|
||||
" 'description': {'anyOf': [{'type': 'string'}, {'type': 'null'}],\n",
|
||||
" 'description': \"The description of the candidate's experience\"}},\n",
|
||||
" 'additionalProperties': False},\n",
|
||||
" 'description': \"The candidate's experience\"},\n",
|
||||
" 'technical_skills': {'type': 'object',\n",
|
||||
" 'required': ['programming_languages', 'frameworks', 'skills'],\n",
|
||||
" 'properties': {'skills': {'type': 'array',\n",
|
||||
" 'items': {'type': 'string'},\n",
|
||||
" 'description': 'Other general skills the candidate is proficient in, e.g. Data Engineering, Machine Learning, etc.'},\n",
|
||||
" 'frameworks': {'type': 'array',\n",
|
||||
" 'items': {'type': 'string'},\n",
|
||||
" 'description': 'The tools/frameworks the candidate is proficient in, e.g. React, Django, PyTorch, etc.'},\n",
|
||||
" 'programming_languages': {'type': 'array',\n",
|
||||
" 'items': {'type': 'string'},\n",
|
||||
" 'description': 'The programming languages the candidate is proficient in.'}},\n",
|
||||
" 'description': \"The candidate's technical skills\",\n",
|
||||
" 'additionalProperties': False},\n",
|
||||
" 'key_accomplishments': {'type': 'string',\n",
|
||||
" 'description': 'Summarize the candidates highest achievements.'}},\n",
|
||||
" 'additionalProperties': False}"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"agent = llama_extract.get_agent(\"resume-screening\")\n",
|
||||
"agent.data_schema # Latest schema should be returned"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Queueing extractions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"For multiple resumes, we can use the `queue_extraction` method to run extractions asynchronously. This is ideal for processing batch extraction jobs."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Uploading files: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 3/3 [00:01<00:00, 2.13it/s]\n",
|
||||
"Creating extraction jobs: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 5.83it/s]\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# All resumes in the data/resumes directory\n",
|
||||
"resumes = []\n",
|
||||
"\n",
|
||||
"with os.scandir(\"./data/resumes\") as entries:\n",
|
||||
" for entry in entries:\n",
|
||||
" if entry.is_file():\n",
|
||||
" resumes.append(entry.path)\n",
|
||||
"\n",
|
||||
"jobs = await agent.queue_extraction(resumes)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"To get the latest status of the extractions for any `job_id`, we can use the `get_extraction_job` method. \n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[<StatusEnum.PENDING: 'PENDING'>,\n",
|
||||
" <StatusEnum.PENDING: 'PENDING'>,\n",
|
||||
" <StatusEnum.PENDING: 'PENDING'>]"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"[agent.get_extraction_job(job_id=job.id).status for job in jobs]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We notice that all extraction runs are in a PENDING state. We can check back again to see if the extractions have completed. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[<StatusEnum.SUCCESS: 'SUCCESS'>,\n",
|
||||
" <StatusEnum.SUCCESS: 'SUCCESS'>,\n",
|
||||
" <StatusEnum.SUCCESS: 'SUCCESS'>]"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"[agent.get_extraction_job(job_id=job.id).status for job in jobs]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Retrieving results\n",
|
||||
"\n",
|
||||
"Let us now retrieve the results of the extractions. If the status of the extraction is `SUCCESS`, we can retrieve the data from the `data` field. In case there are errors (status = `ERROR`), we can retrieve the error message from the `error` field. \n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"results = []\n",
|
||||
"for job in jobs:\n",
|
||||
" extract_run = agent.get_extraction_run_for_job(job.id)\n",
|
||||
" if extract_run.status == \"SUCCESS\":\n",
|
||||
" results.append(extract_run.data)\n",
|
||||
" else:\n",
|
||||
" print(f\"Extraction status for job {job.id}: {extract_run.status}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'name': 'Dr. Rachel Zhang, Ph.D.',\n",
|
||||
" 'email': 'rachel.zhang@email.com',\n",
|
||||
" 'links': ['linkedin.com/in/rachelzhang',\n",
|
||||
" 'github.com/rzhang-ai',\n",
|
||||
" 'scholar.google.com/rachelzhang'],\n",
|
||||
" 'education': [{'degree': 'Ph.D. in Computer Science',\n",
|
||||
" 'end_date': '2011',\n",
|
||||
" 'start_date': '2007',\n",
|
||||
" 'institution': 'Columbia University'},\n",
|
||||
" {'degree': 'M.S. in Computer Science',\n",
|
||||
" 'end_date': '2007',\n",
|
||||
" 'start_date': '2005',\n",
|
||||
" 'institution': 'Stanford University'}],\n",
|
||||
" 'experience': [{'title': 'Senior Research Scientist',\n",
|
||||
" 'company': 'DeepMind',\n",
|
||||
" 'end_date': None,\n",
|
||||
" 'start_date': '2019',\n",
|
||||
" 'description': '- Lead researcher on large-scale multi-task learning systems, developing novel architectures that improve cross-task generalization by 40%\\n- Pioneered new approach to zero-shot learning using contrastive training, published in NeurIPS 2023\\n- Built and led team of 6 researchers working on foundational ML models\\n- Developed novel regularization techniques for large language models, reducing catastrophic forgetting by 35%'},\n",
|
||||
" {'title': 'Research Scientist',\n",
|
||||
" 'company': 'Google Research',\n",
|
||||
" 'end_date': '2019',\n",
|
||||
" 'start_date': '2015',\n",
|
||||
" 'description': '- Developed probabilistic frameworks for robust ML, published in ICML 2018\\n- Created novel attention mechanisms for computer vision models, improving accuracy by 25%\\n- Led collaboration with Google Brain team on efficient training methods for transformer models\\n- Mentored 4 PhD interns and collaborated with academic institutions'},\n",
|
||||
" {'title': 'Research Assistant Professor',\n",
|
||||
" 'company': 'Columbia University',\n",
|
||||
" 'end_date': '2015',\n",
|
||||
" 'start_date': '2011',\n",
|
||||
" 'description': '- Published seminal work on Bayesian optimization methods (cited 1000+ times)\\n- Taught graduate-level courses in Machine Learning and Statistical Learning Theory\\n- Supervised 5 PhD students and 3 MSc students\\n- Secured $500K in research grants for probabilistic ML research'}],\n",
|
||||
" 'technical_skills': {'skills': ['Deep Learning',\n",
|
||||
" 'Reinforcement Learning',\n",
|
||||
" 'Probabilistic Models',\n",
|
||||
" 'Multi-Task Learning',\n",
|
||||
" 'Zero-Shot Learning',\n",
|
||||
" 'Neural Architecture Search'],\n",
|
||||
" 'frameworks': ['PyTorch', 'TensorFlow', 'JAX', 'Ray'],\n",
|
||||
" 'programming_languages': ['Python', 'C++', 'Julia', 'CUDA']},\n",
|
||||
" 'key_accomplishments': 'AI researcher with 12+ years of experience spanning classical machine learning, deep learning, and probabilistic modeling. Led groundbreaking research in reinforcement learning, generative models, and multi-task learning. Published 25+ papers in top-tier conferences (NeurIPS, ICML, ICLR). Strong track record of transitioning theoretical advances into practical applications in both academic and industrial settings.'}"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"results[0]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'name': 'Alex Park',\n",
|
||||
" 'email': 'alex park@email.com',\n",
|
||||
" 'links': ['linkedin.com/in/alexpark'],\n",
|
||||
" 'education': [{'degree': 'M.S. Computer Science',\n",
|
||||
" 'end_date': None,\n",
|
||||
" 'start_date': None,\n",
|
||||
" 'institution': 'University of California, Berkeley'},\n",
|
||||
" {'degree': 'B.S. Computer Science',\n",
|
||||
" 'end_date': None,\n",
|
||||
" 'start_date': None,\n",
|
||||
" 'institution': 'University of California, Berkeley'}],\n",
|
||||
" 'experience': [{'title': 'Senior Machine Learning Engineer',\n",
|
||||
" 'company': 'SearchTech AI',\n",
|
||||
" 'end_date': None,\n",
|
||||
" 'start_date': None,\n",
|
||||
" 'description': 'Led development of next-generation learning-to-rank system using BER\\nArchitected and deployed real-time personalization system processing 10\\nIncreasing CTR by 15%\\nImproving search relevance by 24% (NDCG@10)'},\n",
|
||||
" {'title': '',\n",
|
||||
" 'company': 'Commerce Corp',\n",
|
||||
" 'end_date': None,\n",
|
||||
" 'start_date': None,\n",
|
||||
" 'description': 'Developed semantic search system using transformer models and approximate nearest neighbors, reducing null search results by 35%'},\n",
|
||||
" {'title': 'Machine Learning Engineer',\n",
|
||||
" 'company': 'Tech Solutions Inc',\n",
|
||||
" 'end_date': None,\n",
|
||||
" 'start_date': None,\n",
|
||||
" 'description': 'Implemented query understanding pipeline'},\n",
|
||||
" {'title': 'Software Engineer',\n",
|
||||
" 'company': '',\n",
|
||||
" 'end_date': None,\n",
|
||||
" 'start_date': None,\n",
|
||||
" 'description': 'Built data pipelines and Flasticsearch'}],\n",
|
||||
" 'technical_skills': {'skills': ['Elasticsearch',\n",
|
||||
" 'Solr',\n",
|
||||
" 'Lucene',\n",
|
||||
" 'Python',\n",
|
||||
" 'SQL',\n",
|
||||
" 'Java',\n",
|
||||
" 'Scala',\n",
|
||||
" 'Shell Scripting'],\n",
|
||||
" 'frameworks': ['PyTorch',\n",
|
||||
" 'TensorFlow',\n",
|
||||
" 'Scikit-learn',\n",
|
||||
" 'BERT',\n",
|
||||
" 'Word2Vec',\n",
|
||||
" 'FastAI',\n",
|
||||
" 'BM25',\n",
|
||||
" 'FAISS',\n",
|
||||
" 'Docker',\n",
|
||||
" 'Kubernetes'],\n",
|
||||
" 'programming_languages': []},\n",
|
||||
" 'key_accomplishments': 'Machine Learning Engineer with 5 years of experience building and deploying large-scale search and relevance systems: Specialized in developing personalized search algorithms, learning-to-rank models; and recommendation systems. Strong track record of improving search relevance metrics and user engagement through ML-driven solutions:'}"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"results[1]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'name': 'Sarah Chen',\n",
|
||||
" 'email': 'sarah.chen@email.com',\n",
|
||||
" 'links': [],\n",
|
||||
" 'education': [{'degree': 'Master of Science in Computer Science',\n",
|
||||
" 'end_date': '2013',\n",
|
||||
" 'start_date': None,\n",
|
||||
" 'institution': 'Stanford University'},\n",
|
||||
" {'degree': 'Bachelor of Science in Computer Engineering',\n",
|
||||
" 'end_date': '2011',\n",
|
||||
" 'start_date': None,\n",
|
||||
" 'institution': 'University of California, Berkeley'}],\n",
|
||||
" 'experience': [{'title': 'Senior Software Architect',\n",
|
||||
" 'company': 'TechCorp Solutions',\n",
|
||||
" 'end_date': None,\n",
|
||||
" 'start_date': '2020',\n",
|
||||
" 'description': '- Led architectural design and implementation of a cloud-native platform serving 2M+ users\\n- Established architectural guidelines and best practices adopted across 12 development teams\\n- Reduced system latency by 40% through implementation of event-driven architecture\\n- Mentored 15+ senior developers in cloud-native development practices'},\n",
|
||||
" {'title': 'Lead Software Engineer',\n",
|
||||
" 'company': 'DataFlow Systems',\n",
|
||||
" 'end_date': '2020',\n",
|
||||
" 'start_date': '2016',\n",
|
||||
" 'description': '- Architected and led development of distributed data processing platform handling 5TB daily\\n- Designed microservices architecture reducing deployment time by 65%\\n- Led migration of legacy monolith to cloud-native architecture\\n- Managed team of 8 engineers across 3 international locations'},\n",
|
||||
" {'title': 'Senior Software Engineer',\n",
|
||||
" 'company': 'InnovateTech',\n",
|
||||
" 'end_date': '2016',\n",
|
||||
" 'start_date': '2013',\n",
|
||||
" 'description': '- Developed high-performance trading platform processing 100K transactions per second\\n- Implemented real-time analytics engine reducing processing latency by 75%\\n- Led adoption of container orchestration reducing deployment costs by 35%'}],\n",
|
||||
" 'technical_skills': {'skills': ['Architecture & Design',\n",
|
||||
" 'Microservices',\n",
|
||||
" 'Event-Driven Architecture',\n",
|
||||
" 'Domain-Driven Design',\n",
|
||||
" 'REST APIs',\n",
|
||||
" 'Cloud Platforms'],\n",
|
||||
" 'frameworks': ['AWS (Advanced)', 'Azure', 'Google Cloud Platform'],\n",
|
||||
" 'programming_languages': ['Java', 'Python', 'Go', 'JavaScript/TypeScript']},\n",
|
||||
" 'key_accomplishments': '- Co-inventor on three patents for distributed systems architecture\\n- Published paper on \"Scalable Microservices Architecture\" at IEEE Cloud Computing Conference 2022\\n- Keynote Speaker, CloudCon 2023: \"Future of Cloud-Native Architecture\"\\n- Regular presenter at local tech meetups and conferences'}"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"results[2]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Congratulations! You now have an agent that can extract structured data from resumes. \n",
|
||||
"- You can now use this agent to extract data from more resumes and use the extracted data for further processing. \n",
|
||||
"- To update the schema, you can simply update the `data_schema` attribute of the agent and re-run the extraction. \n",
|
||||
"- You can also use the `save` method to save the state of the agent and persist changes to the schema for future use. \n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "00f6713b-2a32-4f8f-80e5-9a7d9b6e3b90",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Solar Panel Datasheet Comparison Workflow\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/extract/solar_panel_e2e_comparison.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"This notebook demonstrates an end‑to‑end agentic workflow using LlamaExtract and the LlamaIndex event‑driven workflow framework. In this workflow, we:\n",
|
||||
"\n",
|
||||
"1. **Extract** structured technical specifications from a solar panel datasheet (e.g. a PDF downloaded from a vendor).\n",
|
||||
"2. **Load** design requirements (provided as a text blob) for a lab‑grade solar panel.\n",
|
||||
"3. **Generate** a detailed comparison report by triggering an event that injects both the extracted data and the requirements into an LLM prompt.\n",
|
||||
"\n",
|
||||
"The workflow is designed for renewable energy engineers who need to quickly validate that a solar panel meets specific design criteria.\n",
|
||||
"\n",
|
||||
"The following notebook uses the event‑driven syntax (with custom events, steps, and a workflow class) adapted from the technical datasheet and contract review examples."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "36d8e34e-ed98-46ac-b744-1642f6e253d5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup and Load Data\n",
|
||||
"\n",
|
||||
"We download the [Honey M TSM-DE08M.08(II) datasheet](https://static.trinasolar.com/sites/default/files/EU_Datasheet_HoneyM_DE08M.08%28II%29_2021_A.pdf) as a PDF.\n",
|
||||
"\n",
|
||||
"**NOTE**: The design requirements are already stored in `data/solar_panel_e2e_comparison/design_reqs.txt`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "1de7b1b3-c285-492c-8b2e-b37974b4fc63",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"--2025-04-01 14:47:56-- https://static.trinasolar.com/sites/default/files/EU_Datasheet_HoneyM_DE08M.08%28II%29_2021_A.pdf\n",
|
||||
"Resolving static.trinasolar.com (static.trinasolar.com)... 47.246.23.232, 47.246.23.234, 47.246.23.227, ...\n",
|
||||
"Connecting to static.trinasolar.com (static.trinasolar.com)|47.246.23.232|:443... connected.\n",
|
||||
"WARNING: cannot verify static.trinasolar.com's certificate, issued by ‘CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1,O=DigiCert Inc,C=US’:\n",
|
||||
" Unable to locally verify the issuer's authority.\n",
|
||||
"HTTP request sent, awaiting response... 200 OK\n",
|
||||
"Length: 1888183 (1.8M) [application/pdf]\n",
|
||||
"Saving to: ‘data/solar_panel_e2e_comparison/datasheet.pdf’\n",
|
||||
"\n",
|
||||
"data/solar_panel_e2 100%[===================>] 1.80M 7.47MB/s in 0.2s \n",
|
||||
"\n",
|
||||
"2025-04-01 14:47:56 (7.47 MB/s) - ‘data/solar_panel_e2e_comparison/datasheet.pdf’ saved [1888183/1888183]\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"!wget https://static.trinasolar.com/sites/default/files/EU_Datasheet_HoneyM_DE08M.08%28II%29_2021_A.pdf -O data/solar_panel_e2e_comparison/datasheet.pdf --no-check-certificate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "89d2f4c9-f785-424d-a409-3381796c457c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define the Structured Extraction Schema\n",
|
||||
"\n",
|
||||
"We define a new, rich schema called `SolarPanelSchema` to capture key technical details from the datasheet. This schema includes:\n",
|
||||
"\n",
|
||||
"- **PowerRange:** Structured as minimum and maximum power output (in Watts).\n",
|
||||
"- **SolarPanelSpec:** Includes module name, power output range, maximum efficiency, certifications, and a mapping of page citations.\n",
|
||||
"\n",
|
||||
"This schema replaces the earlier LM317 schema and will be used when creating our extraction agent."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "bfb40d48-36e0-4b1c-97a1-32a1704c582b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class PowerRange(BaseModel):\n",
|
||||
" min_power: float = Field(..., description=\"Minimum power output in Watts\")\n",
|
||||
" max_power: float = Field(..., description=\"Maximum power output in Watts\")\n",
|
||||
" unit: str = Field(\"W\", description=\"Power unit\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class SolarPanelSpec(BaseModel):\n",
|
||||
" module_name: str = Field(..., description=\"Name or model of the solar panel module\")\n",
|
||||
" power_output: PowerRange = Field(..., description=\"Power output range\")\n",
|
||||
" maximum_efficiency: float = Field(\n",
|
||||
" ..., description=\"Maximum module efficiency in percentage\"\n",
|
||||
" )\n",
|
||||
" temperature_coefficient: float = Field(\n",
|
||||
" ..., description=\"Temperature coefficient in %/°C\"\n",
|
||||
" )\n",
|
||||
" certifications: List[str] = Field([], description=\"List of certifications\")\n",
|
||||
" page_citations: dict = Field(\n",
|
||||
" ..., description=\"Mapping of each extracted field to its page numbers\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class SolarPanelSchema(BaseModel):\n",
|
||||
" specs: List[SolarPanelSpec] = Field(\n",
|
||||
" ..., description=\"List of extracted solar panel specifications\"\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "19dc309e-7cec-43c1-8f6c-72e14df58f8f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Initialize Extraction Agent\n",
|
||||
"\n",
|
||||
"Here we initialize our extraction agent that will be responsible for extracting the schema from the solar panel datasheet."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "c9d9f4a2-2e14-493d-8a7e-d01159d38b8f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from dotenv import load_dotenv\n",
|
||||
"from llama_cloud_services import LlamaExtract\n",
|
||||
"from llama_cloud.core.api_error import ApiError\n",
|
||||
"from llama_cloud import ExtractConfig\n",
|
||||
"\n",
|
||||
"# Initialize the LlamaExtract client\n",
|
||||
"llama_extract = LlamaExtract(\n",
|
||||
" project_id=\"2fef999e-1073-40e6-aeb3-1f3c0e64d99b\",\n",
|
||||
" organization_id=\"43b88c8f-e488-46f6-9013-698e3d2e374a\",\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "ec0eb2a7-6e02-45da-a6af-227e2f7c81f2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"try:\n",
|
||||
" existing_agent = llama_extract.get_agent(name=\"solar-panel-datasheet\")\n",
|
||||
" if existing_agent:\n",
|
||||
" llama_extract.delete_agent(existing_agent.id)\n",
|
||||
"except ApiError as e:\n",
|
||||
" if e.status_code == 404:\n",
|
||||
" pass\n",
|
||||
" else:\n",
|
||||
" raise\n",
|
||||
"\n",
|
||||
"extract_config = ExtractConfig(\n",
|
||||
" extraction_mode=\"BALANCED\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"agent = llama_extract.create_agent(\n",
|
||||
" name=\"solar-panel-datasheet\", data_schema=SolarPanelSchema, config=extract_config\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "b4d7bb60-0456-4a2d-8d48-14f9bb3e71d2",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Workflow Overview\n",
|
||||
"\n",
|
||||
"The workflow consists of four main steps:\n",
|
||||
"\n",
|
||||
"1. **parse_datasheet:** Reads the solar panel datasheet (PDF) and converts its content into text (with page citations).\n",
|
||||
"2. **load_requirements:** Loads the design requirements (as a text blob) that will be injected into the prompt.\n",
|
||||
"3. **generate_comparison_report:** Constructs a prompt using the extracted datasheet content and design requirements and triggers the LLM to generate a comparison report.\n",
|
||||
"4. **output_result:** Logs and returns the final report as the workflow’s result.\n",
|
||||
"\n",
|
||||
"Each step is implemented as an asynchronous function decorated with `@step`, and the workflow is built by subclassing `Workflow`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "7c482e3a-66b4-4e1b-8d2d-9a9c6b3967f3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_index.core.workflow import (\n",
|
||||
" Event,\n",
|
||||
" StartEvent,\n",
|
||||
" StopEvent,\n",
|
||||
" Context,\n",
|
||||
" Workflow,\n",
|
||||
" step,\n",
|
||||
")\n",
|
||||
"from llama_index.llms.openai import OpenAI\n",
|
||||
"from llama_index.core.prompts import ChatPromptTemplate\n",
|
||||
"from llama_cloud_services import LlamaExtract\n",
|
||||
"from llama_cloud.core.api_error import ApiError\n",
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define output schema for the comparison report (for reference)\n",
|
||||
"class ComparisonReportOutput(BaseModel):\n",
|
||||
" component_name: str = Field(\n",
|
||||
" ..., description=\"The name of the component being evaluated.\"\n",
|
||||
" )\n",
|
||||
" meets_requirements: bool = Field(\n",
|
||||
" ...,\n",
|
||||
" description=\"Overall indicator of whether the component meets the design criteria.\",\n",
|
||||
" )\n",
|
||||
" summary: str = Field(..., description=\"A brief summary of the evaluation results.\")\n",
|
||||
" details: dict = Field(\n",
|
||||
" ..., description=\"Detailed comparisons for each key parameter.\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define custom events\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class DatasheetParseEvent(Event):\n",
|
||||
" datasheet_content: dict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class RequirementsLoadEvent(Event):\n",
|
||||
" requirements_text: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class ComparisonReportEvent(Event):\n",
|
||||
" report: ComparisonReportOutput\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class LogEvent(Event):\n",
|
||||
" msg: str\n",
|
||||
" delta: bool = False\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# For our demonstration, we assume that LlamaExtract is used to parse the datasheet into text.\n",
|
||||
"# We'll also use OpenAI (via LlamaIndex) as our LLM for generating the report.\n",
|
||||
"\n",
|
||||
"llm = OpenAI(model=\"gpt-4o\") # or your preferred model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "67a0c391-c7f5-4b93-8d6b-9e31b2d7a817",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class SolarPanelComparisonWorkflow(Workflow):\n",
|
||||
" \"\"\"\n",
|
||||
" Workflow to extract data from a solar panel datasheet and generate a comparison report\n",
|
||||
" against provided design requirements.\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" def __init__(self, agent: LlamaExtract, requirements_path: str, **kwargs):\n",
|
||||
" super().__init__(**kwargs)\n",
|
||||
" self.agent = agent\n",
|
||||
" # Load design requirements from file as a text blob\n",
|
||||
" with open(requirements_path, \"r\") as f:\n",
|
||||
" self.requirements_text = f.read()\n",
|
||||
"\n",
|
||||
" @step\n",
|
||||
" async def parse_datasheet(\n",
|
||||
" self, ctx: Context, ev: StartEvent\n",
|
||||
" ) -> DatasheetParseEvent:\n",
|
||||
" # datasheet_path is provided in the StartEvent\n",
|
||||
" datasheet_path = (\n",
|
||||
" ev.datasheet_path\n",
|
||||
" ) # e.g., \"./data/solar_panel_comparison/datasheet.pdf\"\n",
|
||||
" extraction_result = await self.agent.aextract(datasheet_path)\n",
|
||||
" datasheet_dict = (\n",
|
||||
" extraction_result.data\n",
|
||||
" ) # assumed to be a string with page citations\n",
|
||||
" await ctx.set(\"datasheet_content\", datasheet_dict)\n",
|
||||
" ctx.write_event_to_stream(LogEvent(msg=\"Datasheet parsed successfully.\"))\n",
|
||||
" return DatasheetParseEvent(datasheet_content=datasheet_dict)\n",
|
||||
"\n",
|
||||
" @step\n",
|
||||
" async def load_requirements(\n",
|
||||
" self, ctx: Context, ev: DatasheetParseEvent\n",
|
||||
" ) -> RequirementsLoadEvent:\n",
|
||||
" # Use the pre-loaded requirements text from __init__\n",
|
||||
" req_text = self.requirements_text\n",
|
||||
" ctx.write_event_to_stream(LogEvent(msg=\"Design requirements loaded.\"))\n",
|
||||
" return RequirementsLoadEvent(requirements_text=req_text)\n",
|
||||
"\n",
|
||||
" @step\n",
|
||||
" async def generate_comparison_report(\n",
|
||||
" self, ctx: Context, ev: RequirementsLoadEvent\n",
|
||||
" ) -> StopEvent:\n",
|
||||
" # Build a prompt that injects both the extracted datasheet content and the design requirements\n",
|
||||
" datasheet_content = await ctx.get(\"datasheet_content\")\n",
|
||||
" prompt_str = \"\"\"\n",
|
||||
"You are an expert renewable energy engineer.\n",
|
||||
"\n",
|
||||
"Compare the following solar panel datasheet information with the design requirements.\n",
|
||||
"\n",
|
||||
"Design Requirements:\n",
|
||||
"{requirements_text}\n",
|
||||
"\n",
|
||||
"Extracted Datasheet Information:\n",
|
||||
"{datasheet_content}\n",
|
||||
"\n",
|
||||
"Generate a detailed comparison report in JSON format with the following schema:\n",
|
||||
" - component_name: string\n",
|
||||
" - meets_requirements: boolean\n",
|
||||
" - summary: string\n",
|
||||
" - details: dictionary of comparisons for each parameter\n",
|
||||
"\n",
|
||||
"For each parameter (Maximum Power, Open-Circuit Voltage, Short-Circuit Current, Efficiency, Temperature Coefficient),\n",
|
||||
"indicate PASS or FAIL and provide brief explanations and recommendations.\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
" # extract from contract\n",
|
||||
" prompt = ChatPromptTemplate.from_messages([(\"user\", prompt_str)])\n",
|
||||
"\n",
|
||||
" # Call the LLM to generate the report using the prompt\n",
|
||||
" report_output = await llm.astructured_predict(\n",
|
||||
" ComparisonReportOutput,\n",
|
||||
" prompt,\n",
|
||||
" requirements_text=ev.requirements_text,\n",
|
||||
" datasheet_content=str(datasheet_content),\n",
|
||||
" )\n",
|
||||
" ctx.write_event_to_stream(LogEvent(msg=\"Comparison report generated.\"))\n",
|
||||
" return StopEvent(\n",
|
||||
" result={\"report\": report_output, \"datasheet_content\": datasheet_content}\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d205f532-1a11-4a48-b5a8-87a7f85e9ce7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Running the Workflow\n",
|
||||
"\n",
|
||||
"Below, we instantiate and run the workflow. We inject the design requirements as a text blob (no custom code to load) and pass the path to the solar panel datasheet (the HoneyM datasheet from Trina).\n",
|
||||
"\n",
|
||||
"The design requirements are:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"Solar Panel Design Requirements:\n",
|
||||
"- Power Output Range: ≥ 350 W\n",
|
||||
"- Maximum Efficiency: ≥ 18%\n",
|
||||
"- Certifications: Must include IEC61215 and UL1703\n",
|
||||
"```\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "6b24fa61-a2f5-4ebb-84eb-1c9b48683b1b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import nest_asyncio\n",
|
||||
"\n",
|
||||
"nest_asyncio.apply()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "be3ebad5-1f70-4671-a2ec-17bf9e4d788f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Path to design requirements file (e.g., a text file with design criteria for solar panels)\n",
|
||||
"requirements_path = \"./data/solar_panel_e2e_comparison/design_reqs.txt\"\n",
|
||||
"\n",
|
||||
"# Instantiate the workflow\n",
|
||||
"workflow = SolarPanelComparisonWorkflow(\n",
|
||||
" agent=agent, requirements_path=requirements_path, verbose=True, timeout=120\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Run the workflow; pass the datasheet path in the StartEvent\n",
|
||||
"result = await workflow.run(\n",
|
||||
" datasheet_path=\"./data/solar_panel_e2e_comparison/datasheet.pdf\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "e1e61f1e-8701-4acc-8f99-cc89d8aae535",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"********Final Comparison Report:********\n",
|
||||
"\n",
|
||||
"{\n",
|
||||
" \"component_name\": \"TSM-DE08M.08(II)\",\n",
|
||||
" \"meets_requirements\": true,\n",
|
||||
" \"summary\": \"The solar panel TSM-DE08M.08(II) meets all the design requirements, making it a suitable choice for the intended application.\",\n",
|
||||
" \"details\": {\n",
|
||||
" \"Maximum Power Output\": \"PASS - The panel's power output ranges from 360 W to 385 W, exceeding the minimum requirement of 350 W.\",\n",
|
||||
" \"Open-Circuit Voltage\": \"PASS - The datasheet does not specify Voc, but the panel meets other critical requirements. Verification of Voc is recommended.\",\n",
|
||||
" \"Short-Circuit Current\": \"PASS - The datasheet does not specify Isc, but the panel meets other critical requirements. Verification of Isc is recommended.\",\n",
|
||||
" \"Efficiency\": \"PASS - The panel's efficiency is 21.0%, which is above the required 18%.\",\n",
|
||||
" \"Temperature Coefficient\": \"PASS - The temperature coefficient is -0.34%/°C, which is better than the maximum allowable -0.5%/°C.\"\n",
|
||||
" }\n",
|
||||
"}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(\"\\n********Final Comparison Report:********\\n\")\n",
|
||||
"print(result[\"report\"].model_dump_json(indent=4))\n",
|
||||
"# print(\"\\n********Datasheet Content:********\\n\", result[\"datasheet_content\"])"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "llama_parse",
|
||||
"language": "python",
|
||||
"name": "llama_parse"
|
||||
},
|
||||
"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
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Using llama-parse with AstraDB"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"In this notebook, we show a basic RAG-style example that uses `llama-parse` to parse a PDF document, store the corresponding document into a vector store (`AstraDB`) and finally, perform some basic queries against that store. The notebook is modeled after the quick start notebooks and hence is meant as a way of getting started with `llama-parse`, backed by a vector database."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Requirements"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# First, install the required dependencies\n",
|
||||
"%pip install --quiet llama-index llama-parse llama-index-vector-stores-astra-db llama-index-llms-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Configuration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"import openai\n",
|
||||
"\n",
|
||||
"from getpass import getpass\n",
|
||||
"\n",
|
||||
"# Get all required API keys and parameters\n",
|
||||
"llama_cloud_api_key = getpass(\"Enter your Llama Index Cloud API Key: \")\n",
|
||||
"api_endpoint = input(\"Enter your Astra DB API Endpoint: \")\n",
|
||||
"token = getpass(\"Enter your Astra DB Token: \")\n",
|
||||
"namespace = (\n",
|
||||
" input(\"Enter your Astra DB namespace (optional, must exist on Astra): \") or None\n",
|
||||
")\n",
|
||||
"openai_api_key = getpass(\"Enter your OpenAI API Key: \")\n",
|
||||
"\n",
|
||||
"os.environ[\"LLAMA_CLOUD_API_KEY\"] = llama_cloud_api_key\n",
|
||||
"openai.api_key = openai_api_key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# llama-parse is async-first, running the sync code in a notebook requires the use of nest_asyncio\n",
|
||||
"import nest_asyncio\n",
|
||||
"\n",
|
||||
"nest_asyncio.apply()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Using llama-parse to parse a PDF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Download complete.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Grab a PDF from Arxiv for indexing\n",
|
||||
"import requests\n",
|
||||
"\n",
|
||||
"# The URL of the file you want to download\n",
|
||||
"url = \"https://arxiv.org/pdf/1706.03762.pdf\"\n",
|
||||
"# The local path where you want to save the file\n",
|
||||
"file_path = \"./attention.pdf\"\n",
|
||||
"\n",
|
||||
"# Perform the HTTP request\n",
|
||||
"response = requests.get(url)\n",
|
||||
"\n",
|
||||
"# Check if the request was successful\n",
|
||||
"if response.status_code == 200:\n",
|
||||
" # Open the file in binary write mode and save the content\n",
|
||||
" with open(file_path, \"wb\") as file:\n",
|
||||
" file.write(response.content)\n",
|
||||
" print(\"Download complete.\")\n",
|
||||
"else:\n",
|
||||
" print(\"Error downloading the file.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Started parsing the file under job_id ce3909a7-54cf-438b-849a-fe9a903b0c71\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"documents = LlamaParse(result_type=\"text\").load_data(file_path)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'rmer - model architecture.\\nThe Transformer follows this overall architecture using stacked self-attention and point-wise, fully\\nconnected layers for both the encoder and decoder, shown in the left and right halves of Figure 1,\\nrespectively.\\n3.1 Encoder and Decoder Stacks\\nEncoder: The encoder is composed of a stack of N = 6 identical layers. Each layer has two\\nsub-layers. The first is a multi-head self-attention mechanism, and the second is a simple, position-\\nwise fully connected feed-forward network. We employ a residual connection [11] around each of\\nthe two sub-layers, followed by layer normalization [1]. That is, the output of each sub-layer is\\nLayerNorm(x + Sublayer(x)), where Sublayer(x) is the function implemented by the sub-layer\\nitself. To facilitate these residual connections, all sub-layers in the model, as well as the embedding\\nlayers, produce outputs of dimension dmodel = 512.\\nDecoder: The decoder is also composed of a stack of N = 6 identical layers. In addition '"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Take a quick look at some of the parsed text from the document:\n",
|
||||
"documents[0].get_content()[10000:11000]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Storing into Astra DB"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_index.vector_stores.astra_db import AstraDBVectorStore\n",
|
||||
"\n",
|
||||
"astra_db_store = AstraDBVectorStore(\n",
|
||||
" token=token,\n",
|
||||
" api_endpoint=api_endpoint,\n",
|
||||
" namespace=namespace,\n",
|
||||
" collection_name=\"astra_v_table_llamaparse\",\n",
|
||||
" embedding_dimension=1536,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_index.core.node_parser import SimpleNodeParser\n",
|
||||
"\n",
|
||||
"node_parser = SimpleNodeParser()\n",
|
||||
"\n",
|
||||
"nodes = node_parser.get_nodes_from_documents(documents)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_index.embeddings.openai import OpenAIEmbedding\n",
|
||||
"from llama_index.core import VectorStoreIndex, StorageContext\n",
|
||||
"\n",
|
||||
"storage_context = StorageContext.from_defaults(vector_store=astra_db_store)\n",
|
||||
"\n",
|
||||
"index = VectorStoreIndex(\n",
|
||||
" nodes=nodes,\n",
|
||||
" storage_context=storage_context,\n",
|
||||
" embed_model=OpenAIEmbedding(api_key=openai_api_key),\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Simple RAG Example"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"query_engine = index.as_query_engine(similarity_top_k=15)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"***********New LlamaParse+ Basic Query Engine***********\n",
|
||||
"Multi-Head Attention is also known as multi-headed self-attention.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"query = \"What is Multi-Head Attention also known as?\"\n",
|
||||
"\n",
|
||||
"response_1 = query_engine.query(query)\n",
|
||||
"print(\"\\n***********New LlamaParse+ Basic Query Engine***********\")\n",
|
||||
"print(response_1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'We used beam search as described in the previous section, but no\\ncheckpoint averaging. We present these results in Table 3.\\nIn Table 3 rows (A), we vary the number of attention heads and the attention key and value dimensions,\\nkeeping the amount of computation constant, as described in Section 3.2.2. While single-head\\nattention is 0.9 BLEU worse than the best setting, quality also drops off with too many heads.\\nIn Table 3 rows (B), we observe that reducing the attention key size dk hurts model quality. This\\nsuggests that determining compatibility is not easy and that a more sophisticated compatibility\\nfunction than dot product may be beneficial. We further observe in rows (C) and (D) that, as expected,\\nbigger models are better, and dropout is very helpful in avoiding over-fitting. In row (E) we replace our\\nsinusoidal positional encoding with learned positional embeddings [9], and observe nearly identical\\nresults to the base model.\\n6.3 English Constituency Parsing\\nTo evaluate if the Transformer can generalize to other tasks we performed experiments on English\\nconstituency parsing. This task presents specific challenges: the output is subject to strong structural\\nconstraints and is significantly longer than the input. Furthermore, RNN sequence-to-sequence\\nmodels have not been able to attain state-of-the-art results in small-data regimes [37].\\nWe trained a 4-layer transformer with dmodel = 1024 on the Wall Street Journal (WSJ) portion of the\\nPenn Treebank [25], about 40K training sentences. We also trained it in a semi-supervised setting,\\nusing the larger high-confidence and BerkleyParser corpora from with approximately 17M sentences\\n[37]. We used a vocabulary of 16K tokens for the WSJ only setting and a vocabulary of 32K tokens\\nfor the semi-supervised setting.\\nWe performed only a small number of experiments to select the dropout, both attention and residual\\n(section 5.4), learning rates and beam size on the Section 22 development set, all other parameters\\nremained unchanged from the English-to-German base translation model. During inference, we\\n 9\\n---\\nTable 4: The Transformer generalizes well to English constituency parsing (Results are on Section 23\\nof WSJ)\\n Parser Training WSJ 23 F1\\n Vinyals & Kaiser el al. (2014) [37] WSJ only, discriminative 88.3\\n Petrov et al. (2006) [29] WSJ only, discriminative 90.4\\n Zhu et al. (2013) [40] WSJ only, discriminative 90.4\\n Dyer et al. (2016) [8] WSJ only, discriminative 91.7\\n Transformer (4 layers) WSJ only, discriminative 91.3\\n Zhu et al. (2013) [40] semi-supervised 91.3\\n Huang & Harper (2009) [14] semi-supervised 91.3\\n McClosky et al. (2006) [26] semi-supervised 92.1\\n Vinyals & Kaiser el al. (2014) [37] semi-supervised 92.1\\n Transformer (4 layers) semi-supervised 92.7\\n Luong et al. (2015) [23] multi-task 93.0\\n Dyer et al. (2016) [8] generative 93.3\\nincreased the maximum output length to input length + 300. We used a beam size of 21 and α = 0.3\\nfor both WSJ only and the semi-supervised setting.\\nOur results in Table 4 show that despite the lack of task-specific tuning our model performs sur-\\nprisingly well, yielding better results than all previously reported models with the exception of the\\nRecurrent Neural Network Grammar [8].\\nIn contrast to RNN sequence-to-sequence models [37], the Transformer outperforms the Berkeley-\\nParser [29] even when training only on the WSJ training set of 40K sentences.\\n7 Conclusion\\nIn this work, we presented the Transformer, the first sequence transduction model based entirely on\\nattention, replacing the recurrent layers most commonly used in encoder-decoder architectures with\\nmulti-headed self-attention.\\nFor translation tasks, the Transformer can be trained significantly faster than architectures based\\non recurrent or convolutional layers.'"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Take a look at one of the source nodes from the response\n",
|
||||
"response_1.source_nodes[0].get_content()"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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": 4
|
||||
}
|
||||
@@ -13,32 +13,14 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install llama-index llama-parse"
|
||||
"%pip install llama-index llama-cloud-services"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"--2024-02-02 11:10:10-- https://arxiv.org/pdf/1706.03762.pdf\n",
|
||||
"Resolving arxiv.org (arxiv.org)... 151.101.131.42, 151.101.3.42, 151.101.67.42, ...\n",
|
||||
"Connecting to arxiv.org (arxiv.org)|151.101.131.42|:443... connected.\n",
|
||||
"HTTP request sent, awaiting response... 200 OK\n",
|
||||
"Length: 2215244 (2.1M) [application/pdf]\n",
|
||||
"Saving to: ‘./attention.pdf’\n",
|
||||
"\n",
|
||||
"./attention.pdf 100%[===================>] 2.11M --.-KB/s in 0.08s \n",
|
||||
"\n",
|
||||
"2024-02-02 11:10:10 (25.9 MB/s) - ‘./attention.pdf’ saved [2215244/2215244]\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!wget \"https://arxiv.org/pdf/1706.03762.pdf\" -O \"./attention.pdf\""
|
||||
]
|
||||
@@ -49,11 +31,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# llama-parse is async-first, running the sync code in a notebook requires the use of nest_asyncio\n",
|
||||
"import nest_asyncio\n",
|
||||
"\n",
|
||||
"nest_asyncio.apply()\n",
|
||||
"\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"os.environ[\"LLAMA_CLOUD_API_KEY\"] = \"llx-...\""
|
||||
@@ -68,14 +45,14 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Started parsing the file under job_id dd0b8e31-0c09-4497-b78a-cc1c92f1d6cf\n"
|
||||
"Started parsing the file under job_id 79ae653c-4598-4bd0-ba6e-b3dab7eab57e\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"documents = LlamaParse(result_type=\"text\").load_data(\"./attention.pdf\")"
|
||||
"result = await LlamaParse().aparse(\"./attention.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -87,23 +64,62 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"ad\n",
|
||||
"1 Introduction\n",
|
||||
"Recurrent neural networks, long short-term memory [13] and gated recurrent [7] neural networks\n",
|
||||
"in particular, have been firmly established as state of the art approaches in sequence modeling and\n",
|
||||
"transduction problems such as language modeling and machine translation [35, 2, 5]. Numerous\n",
|
||||
"efforts have since continued to push the boundaries of recurrent language models and encoder-decoder\n",
|
||||
"architectures [38, 24, 15].\n",
|
||||
"Recurrent models typically factor computation along the symbol positions of the input and output\n",
|
||||
"sequences. Aligning the positions to steps in computation time, they generate a sequence of hidden\n",
|
||||
"states ht, as a function of the previous hidden state ht−1 and the input for position t. This inherently\n",
|
||||
"sequential nature precludes parallelization within training examples, which becomes critical at longer\n",
|
||||
"sequence lengths, as memory constraints limit batching across examples. Recent work has achieved\n",
|
||||
"significant improvements in computational efficiency through factorization tricks [21] and conditional\n",
|
||||
"computation [32], while also improving model performance in case of the latter. The fundamental\n",
|
||||
"constraint of sequential computation, however, remains.\n",
|
||||
"Attention mechanisms have become an integral part of compelling sequence modeling and transduc-\n",
|
||||
"tion models in various tasks, allowing modeling of dependencies without regard to their distance in\n",
|
||||
"the input or output sequences [2, 19]. In all but a few cases [27], however, such attention mechanisms\n",
|
||||
"are used in conjunction with a recurrent network.\n",
|
||||
"In this work we propose the Transformer, a model architecture eschewing recurrence and instead\n",
|
||||
"relying entirely on an attention mechanism to draw global dependencies between input and output.\n",
|
||||
"The Transformer allows for significantly more parallelization and can reach a new state of the art in\n",
|
||||
"translation quality after being trained for as little as twelve hours on eight P100 GPUs.\n",
|
||||
"2 Background\n",
|
||||
"2 Background\n",
|
||||
"The goal of reducing sequential computation also forms the foundation of the Extended Neural GPU\n",
|
||||
"[16], ByteNet [18] and ConvS2S [9], all of which use convolutional neural networks as basic building\n",
|
||||
"block, computing hidden representations in parallel for all input and output positions. In these models,\n",
|
||||
"the number of operations required to relate signals from two arbitrary input or output positions grows\n",
|
||||
"in the distance between positions, linearly for ConvS2S and logarithmically for ByteNet. This makes\n",
|
||||
"it more difficult to learn dependencies between distant positions [12]. In the Transformer this is\n",
|
||||
"reduced to a constant number of operations, albeit at the cost of reduced effective res\n"
|
||||
"reduced to a constant number of operations, albeit at the cost of reduced effective resolution due\n",
|
||||
"to averaging attention-weighted positions, an effect we counteract with Multi-Head Attention as\n",
|
||||
"described in section 3.2.\n",
|
||||
"Self-attention, sometimes called intra-attention is an attention mechanism relating different positions\n",
|
||||
"of a single sequence in order to compute a representation of the sequence. Self-attention has been\n",
|
||||
"used successfully in a variety of tasks including reading comprehension, abstractive summarization,\n",
|
||||
"textual entailment and learning task-independent sentence representations [4, 27, 28, 22].\n",
|
||||
"End-to-end memory networks are based on a recurrent attention mechanism instead of sequence-\n",
|
||||
"aligned recurrence and have been shown to perform well on simple-language question answering and\n",
|
||||
"language modeling tasks [34].\n",
|
||||
"To the best of our knowledge, however, the Transformer is the first transduction model relying\n",
|
||||
"entirely on self-attention to compute representations of its input and output without using sequence-\n",
|
||||
"aligned RNNs or convolution. In the following sections, we will describe the Transformer, motivate\n",
|
||||
"self-attention and discuss its advantages over models such as [17, 18] and [9].\n",
|
||||
"3 Model Architecture\n",
|
||||
"Most competitive neural sequence transduction models have an encoder-decoder structure [5, 2, 35].\n",
|
||||
"Here, the encoder maps an input sequence of symbol representations (x1, ..., xn) to a sequence\n",
|
||||
"of continuous representations z = (z1, ..., zn). Given z, the decoder then generates an output\n",
|
||||
"sequence (y1, ..., ym) of symbols one element at a time. At each step the model is auto-regressive\n",
|
||||
"[10], consuming the previously generated symbols as additional input when generating the next.\n",
|
||||
" 2\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(documents[0].text[6000:7000])"
|
||||
"documents = result.get_text_documents(split_by_page=True)\n",
|
||||
"print(documents[1].text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -115,48 +131,45 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Started parsing the file under job_id d4531453-1bbb-48c4-8324-ae9fea9f2fa2\n"
|
||||
"arXiv:1706.03762v7 [cs.CL] 2 Aug 2023\n",
|
||||
"\n",
|
||||
"Provided proper attribution is provided, Google hereby grants permission to reproduce the tables and figures in this paper solely for use in journalistic or scholarly works.\n",
|
||||
"\n",
|
||||
"# Attention Is All You Need\n",
|
||||
"\n",
|
||||
"Ashish Vaswani∗ Noam Shazeer∗ Niki Parmar∗ Jakob Uszkoreit∗\n",
|
||||
"\n",
|
||||
"Google Brain Google Brain Google Research Google Research\n",
|
||||
"\n",
|
||||
"avaswani@google.com noam@google.com nikip@google.com usz@google.com\n",
|
||||
"\n",
|
||||
"Llion Jones∗ Aidan N. Gomez∗ † Łukasz Kaiser∗\n",
|
||||
"\n",
|
||||
"Google Research University of Toronto Google Brain\n",
|
||||
"\n",
|
||||
"llion@google.com aidan@cs.toronto.edu lukaszkaiser@google.com\n",
|
||||
"\n",
|
||||
"Illia Polosukhin∗ ‡\n",
|
||||
"\n",
|
||||
"illia.polosukhin@gmail.com\n",
|
||||
"\n",
|
||||
"# Abstract\n",
|
||||
"\n",
|
||||
"The dominant sequence transduction models are based on complex recurrent or convolutional neural networks that include an encoder and a decoder. The best performing models also connect the encoder and decoder through an attention mechanism. We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely. Experiments on two machine translation tasks show these models to be superior in quality while being more parallelizable and requiring significantly less time to train. Our model achieves 28.4 BLEU on the WMT 2014 English-to-German translation task, improving over the existing best results, including ensembles, by over 2 BLEU. On the WMT 2014 English-to-French translation task, our model establishes a new single-model state-of-the-art BLEU score of 41.8 after training for 3.5 days on eight GPUs, a small fraction of the training costs of the best models from the literature. We show that the Transformer generalizes well to other tasks by applying it successfully to English constituency parsing both with large and limited training data.\n",
|
||||
"\n",
|
||||
"∗Equal contribution. Listing order is random. Jakob proposed replacing RNNs with self-attention and started the effort to evaluate this idea. Ashish, with Illia, designed and implemented the first Transformer models and has been crucially involved in every aspect of this work. Noam proposed scaled dot-product attention, multi-head attention and the parameter-free position representation and became the other person involved in nearly every detail. Niki designed, implemented, tuned and evaluated countless model variants in our original codebase and tensor2tensor. Llion also experimented with novel model variants, was responsible for our initial codebase, and efficient inference and visualizations. Lukasz and Aidan spent countless long days designing various parts of and implementing tensor2tensor, replacing our earlier codebase, greatly improving results and massively accelerating our research.\n",
|
||||
"\n",
|
||||
"†Work performed while at Google Brain.\n",
|
||||
"\n",
|
||||
"‡Work performed while at Google Research.\n",
|
||||
"\n",
|
||||
"31st Conference on Neural Information Processing Systems (NIPS 2017), Long Beach, CA, USA.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"documents = LlamaParse(result_type=\"markdown\").load_data(\"./attention.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"ction describes the training regime for our models.\n",
|
||||
"\n",
|
||||
"##### Training Data and Batching\n",
|
||||
"\n",
|
||||
"We trained on the standard WMT 2014 English-German dataset consisting of about 4.5 million\n",
|
||||
"sentence pairs. Sentences were encoded using byte-pair encoding [3], which has a shared source-\n",
|
||||
"target vocabulary of about 37000 tokens. For English-French, we used the significantly larger WMT\n",
|
||||
"2014 English-French dataset consisting of 36M sentences and split tokens into a 32000 word-piece\n",
|
||||
"vocabulary [38]. Sentence pairs were batched together by approximate sequence length. Each training\n",
|
||||
"batch contained a set of sentence pairs containing approximately 25000 source tokens and 25000\n",
|
||||
"target tokens.\n",
|
||||
"\n",
|
||||
"##### Hardware and Schedule\n",
|
||||
"\n",
|
||||
"We trained our models on one machine with 8 NVIDIA P100 GPUs. For our base models using\n",
|
||||
"the hyperparameters described throughout the paper, each training step took about 0.4 seconds. We\n",
|
||||
"trained the base models for a total of 100,000 steps or 12 hours. For our big models,(described on the\n",
|
||||
"bo...\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(documents[0].text[20000:21000] + \"...\")"
|
||||
"documents = result.get_markdown_documents(split_by_page=True)\n",
|
||||
"print(documents[0].text)"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install llama-index\n",
|
||||
"!pip install llama-index-core\n",
|
||||
"!pip install llama-index-llms-anthropic llama-index-multi-modal-llms-anthropic\n",
|
||||
"!pip install llama-index-embeddings-huggingface\n",
|
||||
"!pip install llama-cloud-services"
|
||||
"%pip install llama-index\n",
|
||||
"%pip install llama-index-core\n",
|
||||
"%pip install llama-index-llms-anthropic\n",
|
||||
"%pip install llama-index-embeddings-huggingface\n",
|
||||
"%pip install llama-cloud-services"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -45,11 +45,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# llama-parse is async-first, running the async code in a notebook requires the use of nest_asyncio\n",
|
||||
"import nest_asyncio\n",
|
||||
"\n",
|
||||
"nest_asyncio.apply()\n",
|
||||
"\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# API access to llama-cloud\n",
|
||||
@@ -68,7 +63,7 @@
|
||||
"source": [
|
||||
"from llama_index.llms.anthropic import Anthropic\n",
|
||||
"\n",
|
||||
"llm = Anthropic(model=\"claude-3-opus-20240229\", temperature=0.0)"
|
||||
"llm = Anthropic(model=\"claude-3-5-sonnet-20241022\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -131,28 +126,8 @@
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"parser = LlamaParse(verbose=True)\n",
|
||||
"json_objs = parser.get_json_result(\"./uber_10q_march_2022.pdf\")\n",
|
||||
"json_list = json_objs[0][\"pages\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "b26d21d1-05b5-4f49-b937-c13106a84015",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_index.core.schema import TextNode\n",
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def get_text_nodes(json_list: List[dict]):\n",
|
||||
" text_nodes = []\n",
|
||||
" for idx, page in enumerate(json_list):\n",
|
||||
" text_node = TextNode(text=page[\"text\"], metadata={\"page\": page[\"page\"]})\n",
|
||||
" text_nodes.append(text_node)\n",
|
||||
" return text_nodes"
|
||||
"parser = LlamaParse(take_screenshot=True)\n",
|
||||
"result = await parser.aparse(\"./uber_10q_march_2022.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -162,7 +137,12 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"text_nodes = get_text_nodes(json_list)"
|
||||
"text_nodes = await result.aget_text_nodes(split_by_page=True)\n",
|
||||
"image_nodes = await result.aget_image_nodes(\n",
|
||||
" include_screenshot_images=True,\n",
|
||||
" include_object_images=True,\n",
|
||||
" image_download_dir=\"./uber_10q_images\",\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -172,7 +152,7 @@
|
||||
"source": [
|
||||
"## Extract/Index images from image dicts\n",
|
||||
"\n",
|
||||
"Here we use a multimodal model to extract and index images from image dictionaries."
|
||||
"Here we use a multimodal model to caption images and create text nodes for indexing."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -190,27 +170,32 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# call get_images on parser, convert to ImageDocuments\n",
|
||||
"!mkdir llama2_images\n",
|
||||
"!mkdir -p llama2_images\n",
|
||||
"\n",
|
||||
"from llama_index.core.schema import ImageDocument\n",
|
||||
"from llama_index.multi_modal_llms.anthropic import AnthropicMultiModal\n",
|
||||
"from llama_index.core.llms import ChatMessage, ImageBlock, TextBlock\n",
|
||||
"from llama_index.core.schema import ImageNode, TextNode\n",
|
||||
"from llama_index.llms.anthropic import Anthropic\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def get_image_text_nodes(json_objs: List[dict]):\n",
|
||||
"def get_image_text_nodes(image_nodes: list[ImageNode]):\n",
|
||||
" \"\"\"Extract out text from images using a multimodal model.\"\"\"\n",
|
||||
" anthropic_mm_llm = AnthropicMultiModal(max_tokens=300)\n",
|
||||
" image_dicts = parser.get_images(json_objs, download_path=\"llama2_images\")\n",
|
||||
" image_documents = []\n",
|
||||
" llm = Anthropic(model=\"claude-3-5-haiku-20241022\", max_tokens=300)\n",
|
||||
" img_text_nodes = []\n",
|
||||
" for image_dict in image_dicts:\n",
|
||||
" image_doc = ImageDocument(image_path=image_dict[\"path\"])\n",
|
||||
" response = anthropic_mm_llm.complete(\n",
|
||||
" prompt=\"Describe the images as alt text\",\n",
|
||||
" image_documents=[image_doc],\n",
|
||||
" for image_node in image_nodes:\n",
|
||||
" image_path = image_node.image_path\n",
|
||||
" message = ChatMessage(\n",
|
||||
" role=\"user\",\n",
|
||||
" blocks=[\n",
|
||||
" TextBlock(text=\"Describe the images as alt text\"),\n",
|
||||
" ImageBlock(path=image_path),\n",
|
||||
" ],\n",
|
||||
" )\n",
|
||||
" response = llm.chat([message])\n",
|
||||
" text_node = TextNode(\n",
|
||||
" text=str(response.message.content), metadata={\"path\": image_path}\n",
|
||||
" )\n",
|
||||
" text_node = TextNode(text=str(response), metadata={\"path\": image_dict[\"path\"]})\n",
|
||||
" img_text_nodes.append(text_node)\n",
|
||||
"\n",
|
||||
" return img_text_nodes"
|
||||
]
|
||||
},
|
||||
@@ -221,7 +206,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"image_text_nodes = get_image_text_nodes(json_objs)"
|
||||
"image_text_nodes = get_image_text_nodes(image_nodes)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,14 +31,9 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# llama-parse is async-first, running the sync code in a notebook requires the use of nest_asyncio\n",
|
||||
"import nest_asyncio\n",
|
||||
"\n",
|
||||
"nest_asyncio.apply()\n",
|
||||
"\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# os.environ[\"LLAMA_CLOUD_API_KEY\"] = \"llx-...\""
|
||||
"os.environ[\"LLAMA_CLOUD_API_KEY\"] = \"llx-...\""
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -79,8 +74,9 @@
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"parser = LlamaParse(result_type=\"text\", language=\"fr\")\n",
|
||||
"documents = parser.load_data(\"./treasury_report.pdf\")"
|
||||
"parser = LlamaParse(language=\"fr\")\n",
|
||||
"result = await parser.aparse(\"./treasury_report.pdf\")\n",
|
||||
"documents = result.get_text_documents(split_by_page=False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -252,8 +248,9 @@
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"parser = LlamaParse(result_type=\"text\", language=\"ch_sim\")\n",
|
||||
"documents = parser.load_data(\"./chinese_pdf.pdf\")"
|
||||
"parser = LlamaParse(language=\"ch_sim\")\n",
|
||||
"result = await parser.aparse(\"./chinese_pdf.pdf\")\n",
|
||||
"documents = result.get_text_documents(split_by_page=False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -406,8 +403,9 @@
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"base_parser = LlamaParse(result_type=\"text\", language=\"en\")\n",
|
||||
"base_documents = parser.load_data(\"./chinese_pdf2.pdf\")"
|
||||
"base_parser = LlamaParse(language=\"en\")\n",
|
||||
"result = await base_parser.aparse(\"./chinese_pdf2.pdf\")\n",
|
||||
"base_documents = result.get_text_documents(split_by_page=False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -60,11 +60,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# llama-parse is async-first, running the sync code in a notebook requires the use of nest_asyncio\n",
|
||||
"import nest_asyncio\n",
|
||||
"\n",
|
||||
"nest_asyncio.apply()\n",
|
||||
"\n",
|
||||
"import requests\n",
|
||||
"import pymongo\n",
|
||||
"\n",
|
||||
@@ -72,7 +67,7 @@
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"from llama_index.embeddings.openai import OpenAIEmbedding\n",
|
||||
"from llama_index.core import VectorStoreIndex, StorageContext\n",
|
||||
"from llama_index.core.node_parser import SimpleNodeParser"
|
||||
"from llama_index.core.node_parser import SentenceSplitter"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -137,7 +132,8 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"documents = LlamaParse(result_type=\"text\").load_data(file_path)"
|
||||
"result = await LlamaParse().aparse(file_path)\n",
|
||||
"documents = result.get_text_documents(split_by_page=False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -203,7 +199,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"node_parser = SimpleNodeParser()\n",
|
||||
"node_parser = SentenceSplitter()\n",
|
||||
"\n",
|
||||
"nodes = node_parser.get_nodes_from_documents(documents)"
|
||||
]
|
||||
|
||||
@@ -1,544 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# LlamaParse - Parsing comic books with parsing intructions\n",
|
||||
"Parsing intructions allow you to instruct our parsing model the same way you would instruct an LLM!\n",
|
||||
"\n",
|
||||
"They can be useful to help the parser get better results on complex document layouts, to extract data in a specific format, or to transform the document in other ways.\n",
|
||||
"\n",
|
||||
"Using Parsing Instruction you will get better results out of LlamaParse on complicated documents, and also be able to simplify your application code."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Installation\n",
|
||||
"\n",
|
||||
"Parsing instructions are part of the llamaParse API. They can be accessed by directly specifying the parsing_instruction parameter in the API or by using the LlamaParse python module (which we will use for this tutorial).\n",
|
||||
"\n",
|
||||
"To install llama-parse, just get it from PIP:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Collecting llama-parse\n",
|
||||
" Downloading llama_parse-0.3.8-py3-none-any.whl (6.7 kB)\n",
|
||||
"Collecting llama-index-core>=0.10.7 (from llama-parse)\n",
|
||||
" Downloading llama_index_core-0.10.19-py3-none-any.whl (15.3 MB)\n",
|
||||
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m15.3/15.3 MB\u001b[0m \u001b[31m31.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
|
||||
"\u001b[?25hRequirement already satisfied: PyYAML>=6.0.1 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (6.0.1)\n",
|
||||
"Requirement already satisfied: SQLAlchemy[asyncio]>=1.4.49 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (2.0.28)\n",
|
||||
"Requirement already satisfied: aiohttp<4.0.0,>=3.8.6 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (3.9.3)\n",
|
||||
"Collecting dataclasses-json (from llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading dataclasses_json-0.6.4-py3-none-any.whl (28 kB)\n",
|
||||
"Collecting deprecated>=1.2.9.3 (from llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading Deprecated-1.2.14-py2.py3-none-any.whl (9.6 kB)\n",
|
||||
"Collecting dirtyjson<2.0.0,>=1.0.8 (from llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading dirtyjson-1.0.8-py3-none-any.whl (25 kB)\n",
|
||||
"Requirement already satisfied: fsspec>=2023.5.0 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (2023.6.0)\n",
|
||||
"Collecting httpx (from llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading httpx-0.27.0-py3-none-any.whl (75 kB)\n",
|
||||
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m75.6/75.6 kB\u001b[0m \u001b[31m6.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
|
||||
"\u001b[?25hCollecting llamaindex-py-client<0.2.0,>=0.1.13 (from llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading llamaindex_py_client-0.1.13-py3-none-any.whl (107 kB)\n",
|
||||
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m108.0/108.0 kB\u001b[0m \u001b[31m10.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
|
||||
"\u001b[?25hRequirement already satisfied: nest-asyncio<2.0.0,>=1.5.8 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (1.6.0)\n",
|
||||
"Requirement already satisfied: networkx>=3.0 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (3.2.1)\n",
|
||||
"Requirement already satisfied: nltk<4.0.0,>=3.8.1 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (3.8.1)\n",
|
||||
"Requirement already satisfied: numpy in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (1.25.2)\n",
|
||||
"Collecting openai>=1.1.0 (from llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading openai-1.13.3-py3-none-any.whl (227 kB)\n",
|
||||
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m227.4/227.4 kB\u001b[0m \u001b[31m16.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
|
||||
"\u001b[?25hRequirement already satisfied: pandas in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (1.5.3)\n",
|
||||
"Requirement already satisfied: pillow>=9.0.0 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (9.4.0)\n",
|
||||
"Requirement already satisfied: requests>=2.31.0 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (2.31.0)\n",
|
||||
"Requirement already satisfied: tenacity<9.0.0,>=8.2.0 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (8.2.3)\n",
|
||||
"Collecting tiktoken>=0.3.3 (from llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading tiktoken-0.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.8 MB)\n",
|
||||
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.8/1.8 MB\u001b[0m \u001b[31m43.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
|
||||
"\u001b[?25hRequirement already satisfied: tqdm<5.0.0,>=4.66.1 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (4.66.2)\n",
|
||||
"Requirement already satisfied: typing-extensions>=4.5.0 in /usr/local/lib/python3.10/dist-packages (from llama-index-core>=0.10.7->llama-parse) (4.10.0)\n",
|
||||
"Collecting typing-inspect>=0.8.0 (from llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading typing_inspect-0.9.0-py3-none-any.whl (8.8 kB)\n",
|
||||
"Requirement already satisfied: aiosignal>=1.1.2 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.6->llama-index-core>=0.10.7->llama-parse) (1.3.1)\n",
|
||||
"Requirement already satisfied: attrs>=17.3.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.6->llama-index-core>=0.10.7->llama-parse) (23.2.0)\n",
|
||||
"Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.6->llama-index-core>=0.10.7->llama-parse) (1.4.1)\n",
|
||||
"Requirement already satisfied: multidict<7.0,>=4.5 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.6->llama-index-core>=0.10.7->llama-parse) (6.0.5)\n",
|
||||
"Requirement already satisfied: yarl<2.0,>=1.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.6->llama-index-core>=0.10.7->llama-parse) (1.9.4)\n",
|
||||
"Requirement already satisfied: async-timeout<5.0,>=4.0 in /usr/local/lib/python3.10/dist-packages (from aiohttp<4.0.0,>=3.8.6->llama-index-core>=0.10.7->llama-parse) (4.0.3)\n",
|
||||
"Requirement already satisfied: wrapt<2,>=1.10 in /usr/local/lib/python3.10/dist-packages (from deprecated>=1.2.9.3->llama-index-core>=0.10.7->llama-parse) (1.14.1)\n",
|
||||
"Requirement already satisfied: pydantic>=1.10 in /usr/local/lib/python3.10/dist-packages (from llamaindex-py-client<0.2.0,>=0.1.13->llama-index-core>=0.10.7->llama-parse) (2.6.3)\n",
|
||||
"Requirement already satisfied: anyio in /usr/local/lib/python3.10/dist-packages (from httpx->llama-index-core>=0.10.7->llama-parse) (3.7.1)\n",
|
||||
"Requirement already satisfied: certifi in /usr/local/lib/python3.10/dist-packages (from httpx->llama-index-core>=0.10.7->llama-parse) (2024.2.2)\n",
|
||||
"Collecting httpcore==1.* (from httpx->llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading httpcore-1.0.4-py3-none-any.whl (77 kB)\n",
|
||||
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m77.8/77.8 kB\u001b[0m \u001b[31m8.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
|
||||
"\u001b[?25hRequirement already satisfied: idna in /usr/local/lib/python3.10/dist-packages (from httpx->llama-index-core>=0.10.7->llama-parse) (3.6)\n",
|
||||
"Requirement already satisfied: sniffio in /usr/local/lib/python3.10/dist-packages (from httpx->llama-index-core>=0.10.7->llama-parse) (1.3.1)\n",
|
||||
"Collecting h11<0.15,>=0.13 (from httpcore==1.*->httpx->llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading h11-0.14.0-py3-none-any.whl (58 kB)\n",
|
||||
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m58.3/58.3 kB\u001b[0m \u001b[31m5.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
|
||||
"\u001b[?25hRequirement already satisfied: click in /usr/local/lib/python3.10/dist-packages (from nltk<4.0.0,>=3.8.1->llama-index-core>=0.10.7->llama-parse) (8.1.7)\n",
|
||||
"Requirement already satisfied: joblib in /usr/local/lib/python3.10/dist-packages (from nltk<4.0.0,>=3.8.1->llama-index-core>=0.10.7->llama-parse) (1.3.2)\n",
|
||||
"Requirement already satisfied: regex>=2021.8.3 in /usr/local/lib/python3.10/dist-packages (from nltk<4.0.0,>=3.8.1->llama-index-core>=0.10.7->llama-parse) (2023.12.25)\n",
|
||||
"Requirement already satisfied: distro<2,>=1.7.0 in /usr/lib/python3/dist-packages (from openai>=1.1.0->llama-index-core>=0.10.7->llama-parse) (1.7.0)\n",
|
||||
"Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests>=2.31.0->llama-index-core>=0.10.7->llama-parse) (3.3.2)\n",
|
||||
"Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests>=2.31.0->llama-index-core>=0.10.7->llama-parse) (2.0.7)\n",
|
||||
"Requirement already satisfied: greenlet!=0.4.17 in /usr/local/lib/python3.10/dist-packages (from SQLAlchemy[asyncio]>=1.4.49->llama-index-core>=0.10.7->llama-parse) (3.0.3)\n",
|
||||
"Collecting mypy-extensions>=0.3.0 (from typing-inspect>=0.8.0->llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading mypy_extensions-1.0.0-py3-none-any.whl (4.7 kB)\n",
|
||||
"Collecting marshmallow<4.0.0,>=3.18.0 (from dataclasses-json->llama-index-core>=0.10.7->llama-parse)\n",
|
||||
" Downloading marshmallow-3.21.1-py3-none-any.whl (49 kB)\n",
|
||||
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m49.4/49.4 kB\u001b[0m \u001b[31m4.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
|
||||
"\u001b[?25hRequirement already satisfied: python-dateutil>=2.8.1 in /usr/local/lib/python3.10/dist-packages (from pandas->llama-index-core>=0.10.7->llama-parse) (2.8.2)\n",
|
||||
"Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/dist-packages (from pandas->llama-index-core>=0.10.7->llama-parse) (2023.4)\n",
|
||||
"Requirement already satisfied: exceptiongroup in /usr/local/lib/python3.10/dist-packages (from anyio->httpx->llama-index-core>=0.10.7->llama-parse) (1.2.0)\n",
|
||||
"Requirement already satisfied: packaging>=17.0 in /usr/local/lib/python3.10/dist-packages (from marshmallow<4.0.0,>=3.18.0->dataclasses-json->llama-index-core>=0.10.7->llama-parse) (23.2)\n",
|
||||
"Requirement already satisfied: annotated-types>=0.4.0 in /usr/local/lib/python3.10/dist-packages (from pydantic>=1.10->llamaindex-py-client<0.2.0,>=0.1.13->llama-index-core>=0.10.7->llama-parse) (0.6.0)\n",
|
||||
"Requirement already satisfied: pydantic-core==2.16.3 in /usr/local/lib/python3.10/dist-packages (from pydantic>=1.10->llamaindex-py-client<0.2.0,>=0.1.13->llama-index-core>=0.10.7->llama-parse) (2.16.3)\n",
|
||||
"Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil>=2.8.1->pandas->llama-index-core>=0.10.7->llama-parse) (1.16.0)\n",
|
||||
"Installing collected packages: dirtyjson, mypy-extensions, marshmallow, h11, deprecated, typing-inspect, tiktoken, httpcore, httpx, dataclasses-json, openai, llamaindex-py-client, llama-index-core, llama-parse\n",
|
||||
"Successfully installed dataclasses-json-0.6.4 deprecated-1.2.14 dirtyjson-1.0.8 h11-0.14.0 httpcore-1.0.4 httpx-0.27.0 llama-index-core-0.10.19 llama-parse-0.3.8 llamaindex-py-client-0.1.13 marshmallow-3.21.1 mypy-extensions-1.0.0 openai-1.13.3 tiktoken-0.6.0 typing-inspect-0.9.0\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"%pip install llama-cloud-services"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## API key\n",
|
||||
"\n",
|
||||
"The use of LlamaParse requires an API key which you can get here: https://cloud.llamaindex.ai/parse"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"\n",
|
||||
"os.environ[\"LLAMA_CLOUD_API_KEY\"] = \"llx-...\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Async (Notebook only)\n",
|
||||
"llama-parse is async-first, so running the code in a notebook requires the use of nest_asyncio\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import nest_asyncio\n",
|
||||
"\n",
|
||||
"nest_asyncio.apply()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Import the package"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaParse"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Using llamaparse for getting better results (on Manga!)\n",
|
||||
"\n",
|
||||
"Sometimes the layout of a page is unusual and you will get sub-optimal reading order results with LlamaParse. For example, when parsing manga you expect the reading order to be right to left even if the content is in English!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's download an extract of a great manga \"The manga guide to calculus\", by Hiroyuki Kojima (https://www.amazon.com/Manga-Guide-Calculus-Hiroyuki-Kojima/dp/1593271948)\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"--2024-03-13 13:57:19-- https://drive.usercontent.google.com/uc?id=1tZJhcpepLRdQFJFCFX50QIqLyLgqzZsY&export=download\n",
|
||||
"Resolving drive.usercontent.google.com (drive.usercontent.google.com)... 173.194.211.132, 2607:f8b0:400c:c10::84\n",
|
||||
"Connecting to drive.usercontent.google.com (drive.usercontent.google.com)|173.194.211.132|:443... connected.\n",
|
||||
"HTTP request sent, awaiting response... 303 See Other\n",
|
||||
"Location: https://drive.usercontent.google.com/download?id=1tZJhcpepLRdQFJFCFX50QIqLyLgqzZsY&export=download [following]\n",
|
||||
"--2024-03-13 13:57:19-- https://drive.usercontent.google.com/download?id=1tZJhcpepLRdQFJFCFX50QIqLyLgqzZsY&export=download\n",
|
||||
"Reusing existing connection to drive.usercontent.google.com:443.\n",
|
||||
"HTTP request sent, awaiting response... 200 OK\n",
|
||||
"Length: 3041634 (2.9M) [application/octet-stream]\n",
|
||||
"Saving to: ‘./manga.pdf’\n",
|
||||
"\n",
|
||||
"./manga.pdf 100%[===================>] 2.90M --.-KB/s in 0.04s \n",
|
||||
"\n",
|
||||
"2024-03-13 13:57:20 (78.6 MB/s) - ‘./manga.pdf’ saved [3041634/3041634]\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"! wget \"https://drive.usercontent.google.com/uc?id=1tZJhcpepLRdQFJFCFX50QIqLyLgqzZsY&export=download\" -O ./manga.pdf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Without parsing instructions\n",
|
||||
"For the sake of comparison, let's first parse without any instructions."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Started parsing the file under job_id 25bf4202-78d8-4705-88cf-c616ae7c82af\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"vanilaParsing = LlamaParse(result_type=\"markdown\").load_data(\"./manga.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"As you can see below, LlamaParse is not doing a great job here. It is interpreting the grid of comic panels as a table, and trying to fit the dialogue into a table. It's very hard to follow."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"The Asagake Times Sanda-Cho Distributor\n",
|
||||
"\n",
|
||||
"A newspaper distributor? do I have the wrong map?\n",
|
||||
"\n",
|
||||
"You’re looking It’s next for the Sanda-cho door. branch office? Everybody mistakes us for the office because we are larger. What Is a Function? 3\n",
|
||||
"---\n",
|
||||
"## Calculating the Derivative of a Constant, Linear, or Quadratic Function\n",
|
||||
"\n",
|
||||
"|1.|Let’s find the derivative of constant function f(x) = α. The differential coefficient of f(x) at x = a is|\n",
|
||||
"|---|---|\n",
|
||||
"| |lim ε→0 (f(a + ε) - f(a)) / ε = lim ε→0 (α - α) = lim ε→0 0 = 0|\n",
|
||||
"| |Thus, the derivative of f(x) is f′(x) = 0. This makes sense, since our function is constant—the rate of change is 0.|\n",
|
||||
"\n",
|
||||
"Note: The differential coefficient of f(x) at x = a is often simply called the derivative of f(x) at x = a, or just f′(a).\n",
|
||||
"\n",
|
||||
"|2.|Let’s calculate the derivative of linear function f(x) = αx + β. The derivative of f(x) at x = α is|\n",
|
||||
"|---|---|\n",
|
||||
"| |lim ε→0 (f(α + ε) - f(a)) = \n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(vanilaParsing[0].text[100:1000])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Using parsing instructions\n",
|
||||
"Let's try to parse the manga with custom instructions:\n",
|
||||
"\n",
|
||||
"\"The provided document is a manga comic book. Most pages do NOT have a title. It does not contain tables. Try to reconstruct the dialogue spoken in a cohesive way.\"\n",
|
||||
"\n",
|
||||
"To do so just pass the parsing instruction as a parameter to LlamaParse:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Started parsing the file under job_id 88ab273e-b2a7-4f84-8e72-e9367cf6b114\n",
|
||||
"."
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"parsingInstructionManga = \"\"\"The provided document is a manga comic book. Most pages do NOT have a title.\n",
|
||||
"It does not contain tables.\n",
|
||||
"Try to reconstruct the dialogue spoken in a cohesive way.\"\"\"\n",
|
||||
"withInstructionParsing = LlamaParse(\n",
|
||||
" result_type=\"markdown\", parsing_instruction=parsingInstructionManga\n",
|
||||
").load_data(\"./manga.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's see how it compare with page 3! We encourage you to play with the target page and explore other pages. As you will see, the parsing instruction allowed LlamaParse to make sense of the document!\n",
|
||||
"\n",
|
||||
"<img src=\"https://drive.usercontent.google.com/download?id=1M87rXTIZE8d5v7aHmVZVW6gW3eDGq6ks&authuser=0\" />\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"The Asagake Times Sanda-Cho Distributor\n",
|
||||
"\n",
|
||||
"A newspaper distributor? do I have the wrong map?\n",
|
||||
"\n",
|
||||
"You’re looking It’s next for the Sanda-cho door. branch office? Everybody mistakes us for the office because we are larger. What Is a Function? 3\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"------------------------------------------------------------\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# The Asagake Times\n",
|
||||
"\n",
|
||||
"Sanda-Cho Distributor\n",
|
||||
"\n",
|
||||
"A newspaper distributor?\n",
|
||||
"\n",
|
||||
"Do I have the wrong map?\n",
|
||||
"\n",
|
||||
"You're looking for the Sanda-cho branch office?\n",
|
||||
"\n",
|
||||
"It's next door.\n",
|
||||
"\n",
|
||||
"Everybody mistakes us for the office because we are larger.\n",
|
||||
"\n",
|
||||
"What Is a Function? 3\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"target_page = 1\n",
|
||||
"print(vanilaParsing[0].text.split(\"\\n---\\n\")[target_page])\n",
|
||||
"print(\"\\n\\n------------------------------------------------------------\\n\\n\")\n",
|
||||
"print(withInstructionParsing[0].text.split(\"\\n---\\n\")[target_page])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Math - doing more with parsing instuction!\n",
|
||||
"\n",
|
||||
"But this manga is about math and full of equations, why not ask the parser to output them in **LaTeX**?\n",
|
||||
"\n",
|
||||
"<img src=\"https://drive.usercontent.google.com/download?id=1tze3xcQ7axVA-vC_iZeAj_GvYcyNuYDa&authuser=0\" />"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Started parsing the file under job_id 3a055e64-d91e-484e-b9b0-99a2e637c08d\n",
|
||||
"."
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"parsingInstructionMangaLatex = \"\"\"The provided document is a manga comic book. Most pages do NOT have a title.\n",
|
||||
"It does not contain tables.\n",
|
||||
"Try to reconstruct the dialogue spoken in a cohesive way.\n",
|
||||
"Output any math equation in LATEX markdown (between $$)\"\"\"\n",
|
||||
"withLatex = LlamaParse(\n",
|
||||
" result_type=\"markdown\", parsing_instruction=parsingInstructionMangaLatex\n",
|
||||
").load_data(\"./manga.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"\n",
|
||||
"[Without instruction]------------------------------------------------------------\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"## Calculating the Derivative of a Constant, Linear, or Quadratic Function\n",
|
||||
"\n",
|
||||
"|1.|Let’s find the derivative of constant function f(x) = α. The differential coefficient of f(x) at x = a is|\n",
|
||||
"|---|---|\n",
|
||||
"| |lim ε→0 (f(a + ε) - f(a)) / ε = lim ε→0 (α - α) = lim ε→0 0 = 0|\n",
|
||||
"| |Thus, the derivative of f(x) is f′(x) = 0. This makes sense, since our function is constant—the rate of change is 0.|\n",
|
||||
"\n",
|
||||
"Note: The differential coefficient of f(x) at x = a is often simply called the derivative of f(x) at x = a, or just f′(a).\n",
|
||||
"\n",
|
||||
"|2.|Let’s calculate the derivative of linear function f(x) = αx + β. The derivative of f(x) at x = α is|\n",
|
||||
"|---|---|\n",
|
||||
"| |lim ε→0 (f(α + ε) - f(a)) = lim ε→0 (α(a + ε) + β - (αa + β)) = lim ε→0 α = α|\n",
|
||||
"| |Thus, the derivative of f(x) is f′(x) = α, a constant value. This result should also be intuitive—linear functions have a constant rate of change by definition.|\n",
|
||||
"\n",
|
||||
"|3.|Let’s find the derivative of f(x) = x^2, which appeared in the story. The differential coefficient of f(x) at x = a is|\n",
|
||||
"|---|---|\n",
|
||||
"| |lim ε→0 ((a + ε)^2 - a^2) / ε = lim (a^2 + 2aε + ε^2 - a^2) / ε = lim (2aε + ε^2) = lim (2a + ε) = 2a|\n",
|
||||
"| |Thus, the differential coefficient of f(x) at x = a is 2a, or f′(a) = 2a. Therefore, the derivative of f(x) is f′(x) = 2x.|\n",
|
||||
"\n",
|
||||
"## Summary\n",
|
||||
"\n",
|
||||
"- The calculation of a limit that appears in calculus is simply a formula calculating an error.\n",
|
||||
"- A limit is used to obtain a derivative.\n",
|
||||
"- The derivative is the slope of the tangent line at a given point.\n",
|
||||
"- The derivative is nothing but the rate of change.\n",
|
||||
"\n",
|
||||
"## Chapter 1 Let’s Differentiate a Function!\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"[With instruction to output math in LATEX!]------------------------------------------------------------\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Derivative of Constant, Linear, or Quadratic Function\n",
|
||||
"\n",
|
||||
"## Calculating the Derivative of a Constant, Linear, or Quadratic Function\n",
|
||||
"\n",
|
||||
"1. Let’s find the derivative of constant function f(x) = α. The differential coefficient of f(x) at x = a is\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"\\begin{align*}\n",
|
||||
"&\\lim_{{\\varepsilon \\to 0}} \\left( \\frac{f(a + \\varepsilon) - f(a)}{\\varepsilon} \\right) = \\lim_{{\\varepsilon \\to 0}} \\frac{\\alpha - \\alpha}{\\varepsilon} = \\lim_{{\\varepsilon \\to 0}} 0 = 0 \\\\\n",
|
||||
"\\end{align*}\n",
|
||||
"$$\n",
|
||||
"Thus, the derivative of f(x) is f′(x) = 0. This makes sense, since our function is constant—the rate of change is 0.\n",
|
||||
"\n",
|
||||
"Note: The differential coefficient of f(x) at x = a is often simply called the derivative of f(x) at x = a, or just f′(a).\n",
|
||||
"\n",
|
||||
"2. Let’s calculate the derivative of linear function f(x) = αx + β. The derivative of f(x) at x = α is\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"\\begin{align*}\n",
|
||||
"&\\lim_{{\\varepsilon \\to 0}} \\left( \\frac{f(\\alpha + \\varepsilon) - f(a)}{\\varepsilon} \\right) = \\lim_{{\\varepsilon \\to 0}} \\frac{\\alpha(a + \\varepsilon) + \\beta - (\\alpha a + \\beta)}{\\varepsilon} = \\lim_{{\\varepsilon \\to 0}} \\alpha = \\alpha \\\\\n",
|
||||
"\\end{align*}\n",
|
||||
"$$\n",
|
||||
"Thus, the derivative of f(x) is f′(x) = α, a constant value. This result should also be intuitive—linear functions have a constant rate of change by definition.\n",
|
||||
"\n",
|
||||
"3. Let’s find the derivative of f(x) = x2. The differential coefficient of f(x) at x = a is\n",
|
||||
"\n",
|
||||
"$$\n",
|
||||
"\\begin{align*}\n",
|
||||
"&\\lim_{{\\varepsilon \\to 0}} \\left( \\frac{f(a + \\varepsilon) - f(a)}{\\varepsilon} \\right) = \\lim_{{\\varepsilon \\to 0}} \\left( (a + \\varepsilon)^2 - a^2 \\right) = \\lim_{{\\varepsilon \\to 0}} 2a\\varepsilon + \\varepsilon = \\lim_{{\\varepsilon \\to 0}} (2a + \\varepsilon) = 2a \\\\\n",
|
||||
"\\end{align*}\n",
|
||||
"$$\n",
|
||||
"Thus, the differential coefficient of f(x) at x = a is 2a, or f′(a) = 2a. Therefore, the derivative of f(x) is f′(x) = 2x.\n",
|
||||
"\n",
|
||||
"### Summary\n",
|
||||
"\n",
|
||||
"- The calculation of a limit that appears in calculus is simply a formula calculating an error.\n",
|
||||
"- A limit is used to obtain a derivative.\n",
|
||||
"- The derivative is the slope of the tangent line at a given point.\n",
|
||||
"- The derivative is nothing but the rate of change.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"target_page = 2\n",
|
||||
"print(\n",
|
||||
" \"\\n\\n[Without instruction]------------------------------------------------------------\\n\\n\"\n",
|
||||
")\n",
|
||||
"print(vanilaParsing[0].text.split(\"\\n---\\n\")[target_page])\n",
|
||||
"print(\n",
|
||||
" \"\\n\\n[With instruction to output math in LATEX!]------------------------------------------------------------\\n\\n\"\n",
|
||||
")\n",
|
||||
"print(withLatex[0].text.split(\"\\n---\\n\")[target_page])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"And here is the result as rendered by https://upmath.me/ .\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"<img src=\"https://drive.usercontent.google.com/download?id=1qGo5bMGYOiIC9MnprcgEByaYjU9YII2Q&authuser=0\" />\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Over this short notebook we saw how to use parsing instructions to increase the quality and accuracy of parsing with LLamaParse!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"colab": {
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
@@ -55,10 +55,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import nest_asyncio\n",
|
||||
"\n",
|
||||
"nest_asyncio.apply()\n",
|
||||
"\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# API access to llama-cloud\n",
|
||||
@@ -80,25 +76,7 @@
|
||||
"execution_count": null,
|
||||
"id": "IjtKDQRLrylI",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"--2024-12-05 18:54:24-- https://arxiv.org/pdf/2409.18486\n",
|
||||
"Resolving arxiv.org (arxiv.org)... 151.101.67.42, 151.101.131.42, 151.101.3.42, ...\n",
|
||||
"Connecting to arxiv.org (arxiv.org)|151.101.67.42|:443... connected.\n",
|
||||
"HTTP request sent, awaiting response... 200 OK\n",
|
||||
"Length: 13986265 (13M) [application/pdf]\n",
|
||||
"Saving to: ‘o1.pdf’\n",
|
||||
"\n",
|
||||
"o1.pdf 100%[===================>] 13.34M 11.8MB/s in 1.1s \n",
|
||||
"\n",
|
||||
"2024-12-05 18:54:26 (11.8 MB/s) - ‘o1.pdf’ saved [13986265/13986265]\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!wget \"https://arxiv.org/pdf/2409.18486\" -O \"o1.pdf\""
|
||||
]
|
||||
@@ -118,25 +96,6 @@
|
||||
"Using your own API key may incur additional costs from your model provider and could result in failed pages or documents if you do not have sufficient usage limits."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "dc921729-3446-42ca-8e1b-a6fd26195ed9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_index.core.schema import TextNode\n",
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def get_text_nodes(json_list: List[dict]):\n",
|
||||
" text_nodes = []\n",
|
||||
" for idx, page in enumerate(json_list):\n",
|
||||
" text_node = TextNode(text=page[\"md\"], metadata={\"page\": page[\"page\"]})\n",
|
||||
" text_nodes.append(text_node)\n",
|
||||
" return text_nodes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1b5d6da6",
|
||||
@@ -163,15 +122,13 @@
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"parser = LlamaParse(\n",
|
||||
" result_type=\"markdown\",\n",
|
||||
" use_vendor_multimodal_model=True,\n",
|
||||
" vendor_multimodal_model_name=\"anthropic-sonnet-3.5\",\n",
|
||||
" target_pages=\"24\"\n",
|
||||
" # invalidate_cache=True\n",
|
||||
")\n",
|
||||
"json_objs = parser.get_json_result(\"o1.pdf\")\n",
|
||||
"json_list = json_objs[0][\"pages\"]\n",
|
||||
"docs = get_text_nodes(json_list)"
|
||||
"result = await parser.aparse(\"o1.pdf\")\n",
|
||||
"nodes = result.get_text_nodes(split_by_page=False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -202,15 +159,13 @@
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"parser_gpt4o = LlamaParse(\n",
|
||||
" result_type=\"markdown\",\n",
|
||||
" use_vendor_multimodal_model=True,\n",
|
||||
" vendor_multimodal_model=\"openai-gpt4o\",\n",
|
||||
" target_pages=\"24\",\n",
|
||||
" # invalidate_cache=True\n",
|
||||
")\n",
|
||||
"json_objs_gpt4o = parser_gpt4o.get_json_result(\"o1.pdf\")\n",
|
||||
"json_list_gpt4o = json_objs_gpt4o[0][\"pages\"]\n",
|
||||
"docs_gpt4o = get_text_nodes(json_list_gpt4o)"
|
||||
"result = await parser_gpt4o.aparse(\"o1.pdf\")\n",
|
||||
"nodes = result.get_markdown_nodes(split_by_page=False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -268,7 +223,7 @@
|
||||
],
|
||||
"source": [
|
||||
"# using Sonnet-3.5\n",
|
||||
"print(docs[0].get_content(metadata_mode=\"all\"))"
|
||||
"print(nodes[0].get_content(metadata_mode=\"all\"))"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -327,7 +282,7 @@
|
||||
],
|
||||
"source": [
|
||||
"# using GPT-4o\n",
|
||||
"print(docs_gpt4o[0].get_content(metadata_mode=\"all\"))"
|
||||
"print(nodes[0].get_content(metadata_mode=\"all\"))"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -47,11 +47,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# llama-parse is async-first, running the async code in a notebook requires the use of nest_asyncio\n",
|
||||
"import nest_asyncio\n",
|
||||
"\n",
|
||||
"nest_asyncio.apply()\n",
|
||||
"\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# API access to llama-cloud\n",
|
||||
@@ -71,25 +66,7 @@
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"--2024-12-05 11:40:59-- https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/10k/uber_2021.pdf\n",
|
||||
"Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 2606:50c0:8000::154, 2606:50c0:8002::154, 2606:50c0:8003::154, ...\n",
|
||||
"Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|2606:50c0:8000::154|:443... connected.\n",
|
||||
"HTTP request sent, awaiting response... 200 OK\n",
|
||||
"Length: 1880483 (1.8M) [application/octet-stream]\n",
|
||||
"Saving to: ‘./uber_2021.pdf’\n",
|
||||
"\n",
|
||||
"./uber_2021.pdf 100%[===================>] 1.79M --.-KB/s in 0.1s \n",
|
||||
"\n",
|
||||
"2024-12-05 11:40:59 (14.2 MB/s) - ‘./uber_2021.pdf’ saved [1880483/1880483]\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!wget 'https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/10k/uber_2021.pdf' -O './uber_2021.pdf'"
|
||||
]
|
||||
@@ -119,9 +96,10 @@
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"parser = LlamaParse(target_pages=\"0,1,2\", result_type=\"markdown\")\n",
|
||||
"parser = LlamaParse(target_pages=\"0,1,2\")\n",
|
||||
"\n",
|
||||
"documents = parser.load_data(\"./uber_2021.pdf\")"
|
||||
"results = await parser.aparse(\"./uber_2021.pdf\")\n",
|
||||
"documents = results.get_text_documents(split_by_page=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# RAG for Table Comparisons with LlamaParse + LlamaIndex\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/demo_table_comparisons.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"This notebook shows you how to do comparisons across both tabular and text data across multiple PDF documents.\n",
|
||||
"\n",
|
||||
"We load in multiple PDFs with embedded tables (2021 and 2020 10K filings for Apple) using LlamaParse, parse each into a hierarchy of tables/text objects, define a recursive retriever over each, and then compose both with a SubQuestionQueryEngine."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"Install core packages, download files, parse documents."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install llama-index\n",
|
||||
"%pip install llama-index-core\n",
|
||||
"%pip install llama-index-embeddings-openai\n",
|
||||
"%pip install llama-index-question-gen-openai\n",
|
||||
"%pip install llama-index-postprocessor-flag-embedding-reranker\n",
|
||||
"%pip install git+https://github.com/FlagOpen/FlagEmbedding.git\n",
|
||||
"%pip install llama-cloud-services"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!wget \"https://s2.q4cdn.com/470004039/files/doc_financials/2020/ar/_10-K-2020-(As-Filed).pdf\" -O apple_2020_10k.pdf\n",
|
||||
"!wget \"https://s2.q4cdn.com/470004039/files/doc_financials/2021/q4/_10-K-2021-(As-Filed).pdf\" -O apple_2021_10k.pdf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Some OpenAI and LlamaParse details"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# llama-parse is async-first, running the async code in a notebook requires the use of nest_asyncio\n",
|
||||
"import nest_asyncio\n",
|
||||
"\n",
|
||||
"nest_asyncio.apply()\n",
|
||||
"\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"# API access to llama-cloud\n",
|
||||
"os.environ[\"LLAMA_CLOUD_API_KEY\"] = \"llx-\"\n",
|
||||
"\n",
|
||||
"# Using OpenAI API for embeddings/llms\n",
|
||||
"os.environ[\"OPENAI_API_KEY\"] = \"sk-\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_index.llms.openai import OpenAI\n",
|
||||
"from llama_index.embeddings.openai import OpenAIEmbedding\n",
|
||||
"from llama_index.core import VectorStoreIndex\n",
|
||||
"from llama_index.core import Settings\n",
|
||||
"\n",
|
||||
"embed_model = OpenAIEmbedding(model=\"text-embedding-3-small\")\n",
|
||||
"llm = OpenAI(model=\"gpt-3.5-turbo-0125\")\n",
|
||||
"\n",
|
||||
"Settings.llm = llm\n",
|
||||
"Settings.embed_model = embed_model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Using brand new `LlamaParse` PDF reader for PDF Parsing\n",
|
||||
"\n",
|
||||
"we also compare two different retrieval/query engine strategies:\n",
|
||||
"1. Using raw Markdown text as nodes for building index and apply simple query engine for generating the results;\n",
|
||||
"2. Using `MarkdownElementNodeParser` for parsing the `LlamaParse` output Markdown results and building recursive retriever query engine for generation."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaParse\n",
|
||||
"\n",
|
||||
"docs_2021 = LlamaParse(result_type=\"markdown\").load_data(\"./apple_2021_10k.pdf\")\n",
|
||||
"docs_2020 = LlamaParse(result_type=\"markdown\").load_data(\"./apple_2020_10k.pdf\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Create Recursive Retriever over each Document\n",
|
||||
"\n",
|
||||
"We define a function to get a recursive retriever from each document. The steps are the following:\n",
|
||||
"- Hierarchically parse the document using our `MarkdownElementNodeParser`, which will embed/summarize embedded tables.\n",
|
||||
"- Load into a vector store. Under the hood we will automatically store links between nodes (e.g. table summary to table text).\n",
|
||||
"- Get a query engine over the vector store, which performs retrieval/synthesis. Under the hood we will automatically perform recursive retrieval if there are links."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_index.core.node_parser import MarkdownElementNodeParser\n",
|
||||
"\n",
|
||||
"node_parser = MarkdownElementNodeParser(\n",
|
||||
" llm=OpenAI(model=\"gpt-3.5-turbo-0125\"), num_workers=8\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import pickle\n",
|
||||
"from llama_index.postprocessor.flag_embedding_reranker import (\n",
|
||||
" FlagEmbeddingReranker,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"reranker = FlagEmbeddingReranker(\n",
|
||||
" top_n=5,\n",
|
||||
" model=\"BAAI/bge-reranker-large\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def create_query_engine_over_doc(docs, nodes_save_path=None):\n",
|
||||
" \"\"\"Big function to go from document path -> recursive retriever.\"\"\"\n",
|
||||
" if nodes_save_path is not None and os.path.exists(nodes_save_path):\n",
|
||||
" raw_nodes = pickle.load(open(nodes_save_path, \"rb\"))\n",
|
||||
" else:\n",
|
||||
" raw_nodes = node_parser.get_nodes_from_documents(docs)\n",
|
||||
" if nodes_save_path is not None:\n",
|
||||
" pickle.dump(raw_nodes, open(nodes_save_path, \"wb\"))\n",
|
||||
"\n",
|
||||
" base_nodes, objects = node_parser.get_nodes_and_objects(raw_nodes)\n",
|
||||
"\n",
|
||||
" ### Construct Retrievers\n",
|
||||
" # construct top-level vector index + query engine\n",
|
||||
" vector_index = VectorStoreIndex(nodes=base_nodes + objects)\n",
|
||||
" query_engine = vector_index.as_query_engine(\n",
|
||||
" similarity_top_k=15, node_postprocessors=[reranker]\n",
|
||||
" )\n",
|
||||
" return query_engine, base_nodes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"query_engine_2021, nodes_2021 = create_query_engine_over_doc(\n",
|
||||
" docs_2021, nodes_save_path=\"2021_nodes.pkl\"\n",
|
||||
")\n",
|
||||
"query_engine_2020, nodes_2020 = create_query_engine_over_doc(\n",
|
||||
" docs_2020, nodes_save_path=\"2020_nodes.pkl\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_index.core.tools import QueryEngineTool, ToolMetadata\n",
|
||||
"from llama_index.core.query_engine import SubQuestionQueryEngine\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# setup base query engine as tool\n",
|
||||
"query_engine_tools = [\n",
|
||||
" QueryEngineTool(\n",
|
||||
" query_engine=query_engine_2021,\n",
|
||||
" metadata=ToolMetadata(\n",
|
||||
" name=\"apple_2021_10k\",\n",
|
||||
" description=(\"Provides information about Apple financials for year 2021\"),\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
" QueryEngineTool(\n",
|
||||
" query_engine=query_engine_2020,\n",
|
||||
" metadata=ToolMetadata(\n",
|
||||
" name=\"apple_2020_10k\",\n",
|
||||
" description=(\"Provides information about Apple financials for year 2020\"),\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"sub_query_engine = SubQuestionQueryEngine.from_defaults(\n",
|
||||
" query_engine_tools=query_engine_tools,\n",
|
||||
" llm=llm,\n",
|
||||
" use_async=True,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Try out Some Comparisons"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Generated 4 sub questions.\n",
|
||||
"\u001b[1;3;38;2;237;90;200m[apple_2021_10k] Q: What are the deferred assets in 2021?\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;90;149;237m[apple_2021_10k] Q: What are the deferred liabilities in 2021?\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;11;159;203m[apple_2020_10k] Q: What are the deferred assets in 2020?\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;155;135;227m[apple_2020_10k] Q: What are the deferred liabilities in 2020?\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;90;149;237m[apple_2021_10k] A: $7,200\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;155;135;227m[apple_2020_10k] A: $10,138\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;237;90;200m[apple_2021_10k] A: $25,176 million\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;11;159;203m[apple_2020_10k] A: $19,336\n",
|
||||
"\u001b[0m"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response = sub_query_engine.query(\n",
|
||||
" \"Can you compare and contrast the deferred assets and liabilities in 2021 with 2020?\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"In 2021, the deferred assets increased by $5,840 million compared to 2020, while the deferred liabilities decreased by $2,938 million in the same period.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(str(response))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Generated 2 sub questions.\n",
|
||||
"\u001b[1;3;38;2;237;90;200m[apple_2021_10k] Q: What is the total number of RSUs in Apple's 2021 financials?\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;90;149;237m[apple_2020_10k] Q: What is the total number of RSUs in Apple's 2020 financials?\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;237;90;200m[apple_2021_10k] A: The total number of RSUs in Apple's 2021 financials is 240,427.\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;90;149;237m[apple_2020_10k] A: The total number of RSUs in Apple's 2020 financials is 310,778.\n",
|
||||
"\u001b[0m"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response = sub_query_engine.query(\n",
|
||||
" \"Can you compare and contrast the total number of RSUs in 2021 and 2020?\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Generated 2 sub questions.\n",
|
||||
"\u001b[1;3;38;2;237;90;200m[apple_2021_10k] Q: What are the risk factors mentioned in the 2021 financial report of Apple?\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;90;149;237m[apple_2020_10k] Q: What are the risk factors mentioned in the 2020 financial report of Apple?\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;237;90;200m[apple_2021_10k] A: The risk factors mentioned in the 2021 financial report of Apple include risks related to COVID-19, macroeconomic and industry risks, political events, trade and international disputes, natural disasters, public health issues, industrial accidents, credit risk, fluctuations in foreign currency exchange rates, changes in tax rates and legislation, volatility in the price of the company's stock, and exposure to legal proceedings and claims.\n",
|
||||
"\u001b[0m\u001b[1;3;38;2;90;149;237m[apple_2020_10k] A: The risk factors mentioned in the 2020 financial report of Apple include the impact of the COVID-19 pandemic on the company's business operations, financial condition, and stock price; global and regional economic conditions affecting demand for products and services; competition in global markets with rapid technological changes; potential disruptions in the supply chain due to industrial accidents or public health issues; information technology system failures or network disruptions affecting business operations; risks associated with confidential information security and potential unauthorized access; fluctuations in quarterly net sales and operating results due to various factors; stock price volatility impacting investor confidence and employee retention; financial performance risks related to changes in foreign currency exchange rates affecting sales and earnings.\n",
|
||||
"\u001b[0m"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response = sub_query_engine.query(\n",
|
||||
" \"Can you compare and contrast the risk factors in 2021 vs. 2020?\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"The risk factors mentioned in the 2021 financial report of Apple include risks related to COVID-19, macroeconomic and industry risks, political events, trade and international disputes, natural disasters, public health issues, industrial accidents, credit risk, fluctuations in foreign currency exchange rates, changes in tax rates and legislation, volatility in the price of the company's stock, and exposure to legal proceedings and claims. In contrast, the risk factors mentioned in the 2020 financial report of Apple focused more on the impact of the COVID-19 pandemic on the company's business operations, financial condition, and stock price; global and regional economic conditions affecting demand for products and services; competition in global markets with rapid technological changes; potential disruptions in the supply chain due to industrial accidents or public health issues; information technology system failures or network disruptions affecting business operations; risks associated with confidential information security and potential unauthorized access; fluctuations in quarterly net sales and operating results due to various factors; stock price volatility impacting investor confidence and employee retention; financial performance risks related to changes in foreign currency exchange rates affecting sales and earnings.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(str(response))"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "llama_parse",
|
||||
"language": "python",
|
||||
"name": "llama_parse"
|
||||
},
|
||||
"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": 4
|
||||
}
|
||||
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 828 KiB |
|
After Width: | Height: | Size: 626 KiB |
@@ -0,0 +1,212 @@
|
||||
# LlamaExtract
|
||||
|
||||
LlamaExtract provides a simple API for extracting structured data from unstructured documents like PDFs, text files and images.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from llama_cloud_services import LlamaExtract
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Initialize client
|
||||
extractor = LlamaExtract()
|
||||
|
||||
|
||||
# Define schema using Pydantic
|
||||
class Resume(BaseModel):
|
||||
name: str = Field(description="Full name of candidate")
|
||||
email: str = Field(description="Email address")
|
||||
skills: list[str] = Field(description="Technical skills and technologies")
|
||||
|
||||
|
||||
# Create extraction agent
|
||||
agent = extractor.create_agent(name="resume-parser", data_schema=Resume)
|
||||
|
||||
# Extract data from document
|
||||
result = agent.extract("resume.pdf")
|
||||
print(result.data)
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **Extraction Agents**: Reusable extractors configured with a specific schema and extraction settings.
|
||||
- **Data Schema**: Structure definition for the data you want to extract in the form of a JSON schema or a Pydantic model.
|
||||
- **Extraction Jobs**: Asynchronous extraction tasks that can be monitored.
|
||||
|
||||
## Defining Schemas
|
||||
|
||||
Schemas can be defined using either Pydantic models or JSON Schema:
|
||||
|
||||
### Using Pydantic (Recommended)
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class Experience(BaseModel):
|
||||
company: str = Field(description="Company name")
|
||||
title: str = Field(description="Job title")
|
||||
start_date: Optional[str] = Field(description="Start date of employment")
|
||||
end_date: Optional[str] = Field(description="End date of employment")
|
||||
|
||||
|
||||
class Resume(BaseModel):
|
||||
name: str = Field(description="Candidate name")
|
||||
experience: List[Experience] = Field(description="Work history")
|
||||
```
|
||||
|
||||
### Using JSON Schema
|
||||
|
||||
```python
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Candidate name"},
|
||||
"experience": {
|
||||
"type": "array",
|
||||
"description": "Work history",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"company": {
|
||||
"type": "string",
|
||||
"description": "Company name",
|
||||
},
|
||||
"title": {"type": "string", "description": "Job title"},
|
||||
"start_date": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Start date of employment",
|
||||
},
|
||||
"end_date": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "End date of employment",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
agent = extractor.create_agent(name="resume-parser", data_schema=schema)
|
||||
```
|
||||
|
||||
### Important restrictions on JSON/Pydantic Schema
|
||||
|
||||
_LlamaExtract only supports a subset of the JSON Schema specification._ While limited, it should
|
||||
be sufficient for a wide variety of use-cases.
|
||||
|
||||
- All fields are required by default. Nullable fields must be explicitly marked as such,
|
||||
using `anyOf` with a `null` type. See `"start_date"` field above.
|
||||
- Root node must be of type `object`.
|
||||
- Schema nesting must be limited to within 5 levels.
|
||||
- The important fields are key names/titles, type and description. Fields for
|
||||
formatting, default values, etc. are **not supported**. If you need these, you can add the
|
||||
restrictions to your field description and/or use a post-processing step. e.g. default values can be supported by making a field optional and then setting `"null"` values from the extraction result to the default value.
|
||||
- There are other restrictions on number of keys, size of the schema, etc. that you may
|
||||
hit for complex extraction use cases. In such cases, it is worth thinking how to restructure
|
||||
your extraction workflow to fit within these constraints, e.g. by extracting subset of fields
|
||||
and later merging them together.
|
||||
|
||||
## Other Extraction APIs
|
||||
|
||||
### Extraction over bytes or text
|
||||
|
||||
You can use the `SourceText` class to extract from bytes or text directly without using a file. If passing the file bytes,
|
||||
you will need to pass the filename to the `SourceText` class.
|
||||
|
||||
```python
|
||||
with open("resume.pdf", "rb") as f:
|
||||
file_bytes = f.read()
|
||||
result = test_agent.extract(SourceText(file=file_bytes, filename="resume.pdf"))
|
||||
```
|
||||
|
||||
```python
|
||||
result = test_agent.extract(
|
||||
SourceText(text_content="Candidate Name: Jane Doe")
|
||||
)
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
|
||||
Process multiple files asynchronously:
|
||||
|
||||
```python
|
||||
# Queue multiple files for extraction
|
||||
jobs = await agent.queue_extraction(["resume1.pdf", "resume2.pdf"])
|
||||
|
||||
# Check job status
|
||||
for job in jobs:
|
||||
status = agent.get_extraction_job(job.id).status
|
||||
print(f"Job {job.id}: {status}")
|
||||
|
||||
# Get results when complete
|
||||
results = [agent.get_extraction_run_for_job(job.id) for job in jobs]
|
||||
```
|
||||
|
||||
### Updating Schemas
|
||||
|
||||
Schemas can be modified and updated after creation:
|
||||
|
||||
```python
|
||||
# Update schema
|
||||
agent.data_schema = new_schema
|
||||
|
||||
# Save changes
|
||||
agent.save()
|
||||
```
|
||||
|
||||
### Managing Agents
|
||||
|
||||
```python
|
||||
# List all agents
|
||||
agents = extractor.list_agents()
|
||||
|
||||
# Get specific agent
|
||||
agent = extractor.get_agent(name="resume-parser")
|
||||
|
||||
# Delete agent
|
||||
extractor.delete_agent(agent.id)
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install llama-extract==0.1.0
|
||||
```
|
||||
|
||||
## Tips & Best Practices
|
||||
|
||||
At the core of LlamaExtract is the schema, which defines the structure of the data you want to extract from your documents.
|
||||
|
||||
1. **Schema Design**:
|
||||
|
||||
- Try to limit schema nesting to 3-4 levels.
|
||||
- Make fields optional when data might not always be present. Having required fields may force the model
|
||||
to hallucinate when these fields are not present in the documents.
|
||||
- When you want to extract a variable number of entities, use an `array` type. However, note that you cannot use
|
||||
an `array` type for the root node.
|
||||
- Use descriptive field names and detailed descriptions. Use descriptions to pass formatting
|
||||
instructions or few-shot examples.
|
||||
- Above all, start simple and iteratively build your schema to incorporate requirements.
|
||||
|
||||
2. **Running Extractions**:
|
||||
- Note that resetting `agent.schema` will not save the schema to the database,
|
||||
until you call `agent.save`, but it will be used for running extractions.
|
||||
- Check job status prior to accessing results. Any extraction error should be available as
|
||||
part of `job.error` or `extraction_run.error` fields for debugging.
|
||||
- Consider async operations (`queue_extraction`) for large-scale extraction once you have finalized your schema.
|
||||
|
||||
### Hitting "The response was too long to be processed" Error
|
||||
|
||||
This implies that the extraction response is hitting output token limits of the LLM. In such cases, it is worth rethinking the design of your schema to enable a more efficient/scalable extraction. e.g.
|
||||
|
||||
- Instead of one field that extracts a complex object, you can use multiple fields to distribute the extraction logic.
|
||||
- You can also use multiple schemas to extract different subsets of fields from the same document and merge them later.
|
||||
|
||||
Another option (orthogonal to the above) is to break the document into smaller sections and extract from each section individually, when possible. LlamaExtract will in most cases be able to handle both document and schema chunking automatically, but there are cases where you may need to do this manually.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Example Notebook](examples/resume_screening.ipynb) - Detailed walkthrough of resume parsing
|
||||
- [Discord Community](https://discord.com/invite/eN6D2HQ4aX) - Get help and share feedback
|
||||
@@ -1,8 +1,13 @@
|
||||
from llama_cloud_services.parse import LlamaParse
|
||||
from llama_cloud_services.report import ReportClient, LlamaReport
|
||||
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent
|
||||
from llama_cloud_services.constants import EU_BASE_URL
|
||||
|
||||
__all__ = [
|
||||
"LlamaParse",
|
||||
"ReportClient",
|
||||
"LlamaReport",
|
||||
"LlamaExtract",
|
||||
"ExtractionAgent",
|
||||
"EU_BASE_URL",
|
||||
]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
EU_BASE_URL = "https://api.cloud.eu.llamaindex.ai"
|
||||
@@ -0,0 +1,17 @@
|
||||
from llama_cloud_services.extract.extract import (
|
||||
LlamaExtract,
|
||||
ExtractConfig,
|
||||
ExtractionAgent,
|
||||
SourceText,
|
||||
ExtractTarget,
|
||||
ExtractMode,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LlamaExtract",
|
||||
"ExtractionAgent",
|
||||
"SourceText",
|
||||
"ExtractConfig",
|
||||
"ExtractTarget",
|
||||
"ExtractMode",
|
||||
]
|
||||
@@ -0,0 +1,862 @@
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
from io import BufferedIOBase, BufferedReader, BytesIO, TextIOWrapper
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Type, Union, Coroutine, Any, TypeVar
|
||||
import secrets
|
||||
import warnings
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
from tenacity import (
|
||||
retry_if_exception,
|
||||
stop_after_attempt,
|
||||
wait_exponential_jitter,
|
||||
AsyncRetrying,
|
||||
)
|
||||
from llama_cloud import (
|
||||
ExtractAgent as CloudExtractAgent,
|
||||
ExtractConfig,
|
||||
ExtractJob,
|
||||
ExtractJobCreate,
|
||||
ExtractRun,
|
||||
File,
|
||||
ExtractMode,
|
||||
StatusEnum,
|
||||
ExtractTarget,
|
||||
LlamaExtractSettings,
|
||||
PaginatedExtractRunsResponse,
|
||||
)
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from llama_cloud.core.api_error import ApiError
|
||||
from llama_cloud_services.extract.utils import (
|
||||
JSONObjectType,
|
||||
augment_async_errors,
|
||||
ExperimentalWarning,
|
||||
)
|
||||
from llama_index.core.schema import BaseComponent
|
||||
from llama_index.core.async_utils import run_jobs
|
||||
from llama_index.core.bridge.pydantic import Field, PrivateAttr
|
||||
from llama_index.core.constants import DEFAULT_BASE_URL
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
SchemaInput = Union[JSONObjectType, Type[BaseModel]]
|
||||
|
||||
DEFAULT_EXTRACT_CONFIG = ExtractConfig(
|
||||
extraction_target=ExtractTarget.PER_DOC,
|
||||
extraction_mode=ExtractMode.BALANCED,
|
||||
)
|
||||
|
||||
|
||||
def _is_retryable_error(exception: BaseException) -> bool:
|
||||
"""Check if an exception is retryable."""
|
||||
if isinstance(exception, ApiError):
|
||||
return exception.status_code in (502, 503, 504, 425, 408)
|
||||
elif isinstance(
|
||||
exception, (httpx.HTTPStatusError, httpx.RequestError, httpx.TimeoutException)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class SourceText:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
file: Union[bytes, BufferedIOBase, TextIOWrapper, str, Path, None] = None,
|
||||
text_content: Optional[str] = None,
|
||||
filename: Optional[str] = None,
|
||||
):
|
||||
self.file = file
|
||||
self.filename = filename
|
||||
self.text_content = text_content
|
||||
self._validate()
|
||||
|
||||
def _validate(self) -> None:
|
||||
"""Ensure filename is provided when needed."""
|
||||
if not ((self.file is None) ^ (self.text_content is None)):
|
||||
raise ValueError("Either file or text_content must be provided.")
|
||||
if self.text_content is not None:
|
||||
if not self.filename:
|
||||
random_hex = secrets.token_hex(4)
|
||||
self.filename = f"text_input_{random_hex}.txt"
|
||||
return
|
||||
|
||||
if isinstance(self.file, (bytes, BufferedIOBase, TextIOWrapper)):
|
||||
if not self.filename and hasattr(self.file, "name"):
|
||||
self.filename = os.path.basename(str(self.file.name))
|
||||
elif not hasattr(self.file, "name") and self.filename is None:
|
||||
raise ValueError(
|
||||
"filename must be provided when file is bytes or a file-like object without a name"
|
||||
)
|
||||
elif isinstance(self.file, (str, Path)):
|
||||
if not self.filename:
|
||||
self.filename = os.path.basename(str(self.file))
|
||||
else:
|
||||
raise ValueError(f"Unsupported file type: {type(self.file)}")
|
||||
|
||||
|
||||
FileInput = Union[str, Path, BufferedIOBase, SourceText]
|
||||
|
||||
|
||||
def run_in_thread(
|
||||
coro: Coroutine[Any, Any, T],
|
||||
thread_pool: ThreadPoolExecutor,
|
||||
verify: bool,
|
||||
httpx_timeout: float,
|
||||
client_wrapper: Any,
|
||||
) -> T:
|
||||
"""Run coroutine in a thread with proper client management."""
|
||||
|
||||
async def wrapped_coro() -> T:
|
||||
client = httpx.AsyncClient(
|
||||
verify=verify,
|
||||
timeout=httpx_timeout,
|
||||
limits=httpx.Limits(max_keepalive_connections=100, max_connections=100),
|
||||
)
|
||||
original_client = client_wrapper.httpx_client
|
||||
try:
|
||||
client_wrapper.httpx_client = client
|
||||
return await coro
|
||||
finally:
|
||||
client_wrapper.httpx_client = original_client
|
||||
await client.aclose()
|
||||
|
||||
def run_coro() -> T:
|
||||
try:
|
||||
return asyncio.run(wrapped_coro())
|
||||
except httpx.TimeoutException as e:
|
||||
raise TimeoutError(f"Request timed out: {str(e)}") from e
|
||||
except httpx.NetworkError as e:
|
||||
raise ConnectionError(f"Network error: {str(e)}") from e
|
||||
|
||||
return thread_pool.submit(run_coro).result()
|
||||
|
||||
|
||||
def _extraction_config_warning(config: ExtractConfig) -> None:
|
||||
if config.extraction_mode == ExtractMode.ACCURATE:
|
||||
warnings.warn("ACCURATE extraction mode is deprecated. Using BALANCED instead.")
|
||||
config.extraction_mode = ExtractMode.BALANCED
|
||||
if config.use_reasoning:
|
||||
warnings.warn(
|
||||
"`use_reasoning` is an experimental feature. Results will be available in "
|
||||
"the `extraction_metadata` field for the extraction run.",
|
||||
ExperimentalWarning,
|
||||
)
|
||||
if config.cite_sources:
|
||||
warnings.warn(
|
||||
"`cite_sources` is an experimental feature. This may greatly increase the "
|
||||
"size of the response, and slow down the extraction. Results will be "
|
||||
"available in the `extraction_metadata` field for the extraction run.",
|
||||
ExperimentalWarning,
|
||||
)
|
||||
|
||||
|
||||
class ExtractionAgent:
|
||||
"""Class representing a single extraction agent with methods for extraction operations."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: AsyncLlamaCloud,
|
||||
agent: CloudExtractAgent,
|
||||
project_id: Optional[str] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
check_interval: int = 1,
|
||||
max_timeout: int = 2000,
|
||||
num_workers: int = 4,
|
||||
show_progress: bool = True,
|
||||
verbose: bool = False,
|
||||
verify: Optional[bool] = True,
|
||||
httpx_timeout: Optional[float] = 60,
|
||||
):
|
||||
self._client = client
|
||||
self._agent = agent
|
||||
self._project_id = project_id
|
||||
self._organization_id = organization_id
|
||||
self.check_interval = check_interval
|
||||
self.max_timeout = max_timeout
|
||||
self.num_workers = num_workers
|
||||
self.show_progress = show_progress
|
||||
self.verify = verify
|
||||
self.httpx_timeout = httpx_timeout
|
||||
self._verbose = verbose
|
||||
self._data_schema: Union[JSONObjectType, None] = None
|
||||
self._config: Union[ExtractConfig, None] = None
|
||||
self._thread_pool = ThreadPoolExecutor(
|
||||
max_workers=min(10, (os.cpu_count() or 1) + 4)
|
||||
)
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return self._agent.id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._agent.name
|
||||
|
||||
@property
|
||||
def data_schema(self) -> dict:
|
||||
return self._agent.data_schema if not self._data_schema else self._data_schema
|
||||
|
||||
@data_schema.setter
|
||||
def data_schema(self, data_schema: SchemaInput) -> None:
|
||||
processed_schema: JSONObjectType
|
||||
if isinstance(data_schema, dict):
|
||||
# TODO: if we expose a get_validated JSON schema method, we can use it here
|
||||
processed_schema = data_schema # type: ignore
|
||||
elif isinstance(data_schema, type) and issubclass(data_schema, BaseModel):
|
||||
processed_schema = data_schema.model_json_schema()
|
||||
else:
|
||||
raise ValueError(
|
||||
"data_schema must be either a dictionary or a Pydantic model"
|
||||
)
|
||||
validated_schema = self._run_in_thread(
|
||||
self._client.llama_extract.validate_extraction_schema(
|
||||
data_schema=processed_schema
|
||||
)
|
||||
)
|
||||
self._data_schema = validated_schema.data_schema
|
||||
|
||||
@property
|
||||
def config(self) -> ExtractConfig:
|
||||
return self._agent.config if not self._config else self._config
|
||||
|
||||
@config.setter
|
||||
def config(self, config: ExtractConfig) -> None:
|
||||
_extraction_config_warning(config)
|
||||
self._config = config
|
||||
|
||||
def _run_in_thread(self, coro: Coroutine[Any, Any, T]) -> T:
|
||||
"""Run coroutine in a separate thread to avoid event loop issues"""
|
||||
return run_in_thread(
|
||||
coro,
|
||||
self._thread_pool,
|
||||
self.verify, # type: ignore
|
||||
self.httpx_timeout, # type: ignore
|
||||
self._client._client_wrapper,
|
||||
)
|
||||
|
||||
async def upload_file(self, file_input: SourceText) -> File:
|
||||
"""Upload a file for extraction.
|
||||
|
||||
Args:
|
||||
file_input: The file to upload (path, bytes, or file-like object)
|
||||
|
||||
Raises:
|
||||
ValueError: If filename is not provided for bytes input or for file-like objects
|
||||
without a name attribute.
|
||||
"""
|
||||
file_contents: Optional[Union[BufferedIOBase, BytesIO]] = None
|
||||
try:
|
||||
if file_input.text_content is not None:
|
||||
# Handle direct text content
|
||||
file_contents = BytesIO(file_input.text_content.encode("utf-8"))
|
||||
elif isinstance(file_input.file, TextIOWrapper):
|
||||
# Handle text-based IO objects
|
||||
file_contents = BytesIO(file_input.file.read().encode("utf-8"))
|
||||
elif isinstance(file_input.file, (str, Path)):
|
||||
# Handle file paths
|
||||
file_contents = open(file_input.file, "rb")
|
||||
elif isinstance(file_input.file, bytes):
|
||||
# Handle bytes
|
||||
file_contents = BytesIO(file_input.file)
|
||||
elif isinstance(file_input.file, BufferedIOBase):
|
||||
# Handle binary IO objects
|
||||
file_contents = file_input.file
|
||||
else:
|
||||
raise ValueError(f"Unsupported file type: {type(file_input.file)}")
|
||||
|
||||
# Add name attribute to file object if needed
|
||||
if not hasattr(file_contents, "name"):
|
||||
file_contents.name = file_input.filename # type: ignore
|
||||
|
||||
return await self._client.files.upload_file(
|
||||
project_id=self._project_id, upload_file=file_contents
|
||||
)
|
||||
finally:
|
||||
if file_contents is not None and isinstance(file_contents, BufferedReader):
|
||||
file_contents.close()
|
||||
|
||||
async def _upload_file(self, file_input: FileInput) -> File:
|
||||
source_text = None
|
||||
if isinstance(file_input, SourceText):
|
||||
source_text = file_input
|
||||
elif isinstance(file_input, (str, Path)):
|
||||
path = Path(file_input)
|
||||
source_text = SourceText(file=path, filename=path.name)
|
||||
else:
|
||||
# Try to get filename from the file object if not provided
|
||||
filename = None
|
||||
if hasattr(file_input, "name"):
|
||||
filename = os.path.basename(str(file_input.name))
|
||||
if filename is None:
|
||||
raise ValueError(
|
||||
"Use SourceText to provide filename when uploading bytes or file-like objects."
|
||||
)
|
||||
|
||||
warnings.warn(
|
||||
"Use SourceText instead of bytes or file-like objects",
|
||||
DeprecationWarning,
|
||||
)
|
||||
source_text = SourceText(file=file_input, filename=filename)
|
||||
|
||||
return await self.upload_file(source_text)
|
||||
|
||||
async def _get_job_with_retry(self, job_id: str) -> ExtractJob:
|
||||
"""Get job with retry logic for transient errors."""
|
||||
async for attempt in AsyncRetrying(
|
||||
retry=retry_if_exception(_is_retryable_error),
|
||||
stop=stop_after_attempt(5),
|
||||
wait=wait_exponential_jitter(initial=1, max=60, jitter=5),
|
||||
reraise=True,
|
||||
):
|
||||
with attempt:
|
||||
return await self._client.llama_extract.get_job(job_id=job_id)
|
||||
|
||||
async def _get_run_with_retry(self, job_id: str) -> ExtractRun:
|
||||
"""Get extraction run with retry logic for transient errors."""
|
||||
async for attempt in AsyncRetrying(
|
||||
retry=retry_if_exception(_is_retryable_error),
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential_jitter(initial=1, max=20, jitter=3),
|
||||
reraise=True,
|
||||
):
|
||||
with attempt:
|
||||
return await self._client.llama_extract.get_run_by_job_id(job_id=job_id)
|
||||
|
||||
async def _wait_for_job_result(self, job_id: str) -> Optional[ExtractRun]:
|
||||
"""Wait for and return the results of an extraction job."""
|
||||
start = time.perf_counter()
|
||||
tries = 0
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(self.check_interval)
|
||||
tries += 1
|
||||
|
||||
try:
|
||||
job = await self._get_job_with_retry(job_id)
|
||||
|
||||
if job.status == StatusEnum.SUCCESS:
|
||||
return await self._get_run_with_retry(job_id)
|
||||
elif job.status == StatusEnum.PENDING:
|
||||
end = time.perf_counter()
|
||||
if end - start > self.max_timeout:
|
||||
raise Exception(f"Timeout while extracting the file: {job_id}")
|
||||
if self._verbose and tries % 10 == 0:
|
||||
print(".", end="", flush=True)
|
||||
continue
|
||||
else:
|
||||
warnings.warn(
|
||||
f"Failure in job: {job_id}, status: {job.status}, error: {job.error}"
|
||||
)
|
||||
return await self._get_run_with_retry(job_id)
|
||||
|
||||
except Exception as e:
|
||||
# If we get a non-retryable error or all retries are exhausted, re-raise
|
||||
if self._verbose:
|
||||
print(f"\nError in job polling for {job_id}: {e}")
|
||||
raise e
|
||||
|
||||
def save(self) -> None:
|
||||
"""Persist the extraction agent's schema and config to the database.
|
||||
|
||||
Returns:
|
||||
ExtractionAgent: The updated extraction agent
|
||||
"""
|
||||
self._agent = self._run_in_thread(
|
||||
self._client.llama_extract.update_extraction_agent(
|
||||
extraction_agent_id=self.id,
|
||||
data_schema=self.data_schema,
|
||||
config=self.config,
|
||||
)
|
||||
)
|
||||
|
||||
async def _run_extraction_test(
|
||||
self,
|
||||
files: Union[FileInput, List[FileInput]],
|
||||
extract_settings: LlamaExtractSettings,
|
||||
) -> Union[ExtractJob, List[ExtractJob]]:
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
single_file = True
|
||||
else:
|
||||
single_file = False
|
||||
|
||||
upload_tasks = [self._upload_file(file) for file in files]
|
||||
with augment_async_errors():
|
||||
uploaded_files = await run_jobs(
|
||||
upload_tasks,
|
||||
workers=self.num_workers,
|
||||
desc="Uploading files",
|
||||
show_progress=self.show_progress,
|
||||
)
|
||||
|
||||
async def run_job(file: File) -> ExtractRun:
|
||||
job_queued = await self._client.llama_extract.run_job_test_user(
|
||||
job_create=ExtractJobCreate(
|
||||
extraction_agent_id=self.id,
|
||||
file_id=file.id,
|
||||
data_schema_override=self.data_schema,
|
||||
config_override=self.config,
|
||||
),
|
||||
extract_settings=extract_settings,
|
||||
)
|
||||
return await self._wait_for_job_result(job_queued.id)
|
||||
|
||||
job_tasks = [run_job(file) for file in uploaded_files]
|
||||
with augment_async_errors():
|
||||
extract_results = await run_jobs(
|
||||
job_tasks,
|
||||
workers=self.num_workers,
|
||||
desc="Running extraction jobs",
|
||||
show_progress=self.show_progress,
|
||||
)
|
||||
|
||||
if self._verbose:
|
||||
for file, job in zip(files, extract_results):
|
||||
file_repr = (
|
||||
str(file) if isinstance(file, (str, Path)) else "<bytes/buffer>"
|
||||
)
|
||||
print(f"Running extraction for file {file_repr} under job_id {job.id}")
|
||||
|
||||
return extract_results[0] if single_file else extract_results
|
||||
|
||||
async def queue_extraction(
|
||||
self,
|
||||
files: Union[FileInput, List[FileInput]],
|
||||
) -> Union[ExtractJob, List[ExtractJob]]:
|
||||
"""
|
||||
Queue multiple files for extraction.
|
||||
|
||||
Args:
|
||||
files (Union[FileInput, List[FileInput]]): The files to extract
|
||||
|
||||
Returns:
|
||||
Union[ExtractJob, List[ExtractJob]]: The queued extraction jobs
|
||||
"""
|
||||
"""Queue one or more files for extraction concurrently."""
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
single_file = True
|
||||
else:
|
||||
single_file = False
|
||||
|
||||
upload_tasks = [self._upload_file(file) for file in files]
|
||||
with augment_async_errors():
|
||||
uploaded_files = await run_jobs(
|
||||
upload_tasks,
|
||||
workers=self.num_workers,
|
||||
desc="Uploading files",
|
||||
show_progress=self.show_progress,
|
||||
)
|
||||
|
||||
job_tasks = [
|
||||
self._client.llama_extract.run_job(
|
||||
request=ExtractJobCreate(
|
||||
extraction_agent_id=self.id,
|
||||
file_id=file.id,
|
||||
data_schema_override=self.data_schema,
|
||||
config_override=self.config,
|
||||
),
|
||||
)
|
||||
for file in uploaded_files
|
||||
]
|
||||
with augment_async_errors():
|
||||
extract_jobs = await run_jobs(
|
||||
job_tasks,
|
||||
workers=self.num_workers,
|
||||
desc="Creating extraction jobs",
|
||||
show_progress=self.show_progress,
|
||||
)
|
||||
|
||||
if self._verbose:
|
||||
for file, job in zip(files, extract_jobs):
|
||||
file_repr = (
|
||||
str(file) if isinstance(file, (str, Path)) else "<bytes/buffer>"
|
||||
)
|
||||
print(
|
||||
f"Queued file extraction for file {file_repr} under job_id {job.id}"
|
||||
)
|
||||
|
||||
return extract_jobs[0] if single_file else extract_jobs
|
||||
|
||||
async def aextract(
|
||||
self, files: Union[FileInput, List[FileInput]]
|
||||
) -> Union[ExtractRun, List[ExtractRun]]:
|
||||
"""Asynchronously extract data from one or more files using this agent.
|
||||
|
||||
Args:
|
||||
files (Union[FileInput, List[FileInput]]): The files to extract
|
||||
|
||||
Returns:
|
||||
Union[ExtractRun, List[ExtractRun]]: The extraction results
|
||||
"""
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
single_file = True
|
||||
else:
|
||||
single_file = False
|
||||
|
||||
# Queue all files for extraction
|
||||
jobs = await self.queue_extraction(files)
|
||||
# Wait for all results concurrently
|
||||
result_tasks = [self._wait_for_job_result(job.id) for job in jobs]
|
||||
with augment_async_errors():
|
||||
results = await run_jobs(
|
||||
result_tasks,
|
||||
workers=self.num_workers,
|
||||
desc="Extracting files",
|
||||
show_progress=self.show_progress,
|
||||
)
|
||||
|
||||
return results[0] if single_file else results
|
||||
|
||||
def extract(
|
||||
self, files: Union[FileInput, List[FileInput]]
|
||||
) -> Union[ExtractRun, List[ExtractRun]]:
|
||||
"""Synchronously extract data from one or more files using this agent.
|
||||
|
||||
Args:
|
||||
files (Union[FileInput, List[FileInput]]): The files to extract
|
||||
|
||||
Returns:
|
||||
Union[ExtractRun, List[ExtractRun]]: The extraction results
|
||||
"""
|
||||
return self._run_in_thread(self.aextract(files))
|
||||
|
||||
def get_extraction_job(self, job_id: str) -> ExtractJob:
|
||||
"""
|
||||
Get the extraction job for a given job_id.
|
||||
|
||||
Args:
|
||||
job_id (str): The job_id to get the extraction job for
|
||||
|
||||
Returns:
|
||||
ExtractJob: The extraction job
|
||||
"""
|
||||
return self._run_in_thread(self._client.llama_extract.get_job(job_id=job_id))
|
||||
|
||||
def get_extraction_run_for_job(self, job_id: str) -> ExtractRun:
|
||||
"""
|
||||
Get the extraction run for a given job_id.
|
||||
|
||||
Args:
|
||||
job_id (str): The job_id to get the extraction run for
|
||||
|
||||
Returns:
|
||||
ExtractRun: The extraction run
|
||||
"""
|
||||
return self._run_in_thread(
|
||||
self._client.llama_extract.get_run_by_job_id(
|
||||
job_id=job_id,
|
||||
)
|
||||
)
|
||||
|
||||
def delete_extraction_run(self, run_id: str) -> None:
|
||||
"""Delete an extraction run by ID.
|
||||
|
||||
Args:
|
||||
run_id (str): The ID of the extraction run to delete
|
||||
"""
|
||||
self._run_in_thread(
|
||||
self._client.llama_extract.delete_extraction_run(run_id=run_id)
|
||||
)
|
||||
|
||||
def list_extraction_runs(
|
||||
self, page: int = 0, limit: int = 100
|
||||
) -> PaginatedExtractRunsResponse:
|
||||
"""List extraction runs for the extraction agent.
|
||||
|
||||
Returns:
|
||||
PaginatedExtractRunsResponse: Paginated list of extraction runs
|
||||
"""
|
||||
return self._run_in_thread(
|
||||
self._client.llama_extract.list_extract_runs(
|
||||
extraction_agent_id=self.id,
|
||||
skip=page * limit,
|
||||
limit=limit,
|
||||
)
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ExtractionAgent(id={self.id}, name={self.name})"
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""Cleanup resources properly."""
|
||||
try:
|
||||
if hasattr(self, "_thread_pool"):
|
||||
self._thread_pool.shutdown(wait=True)
|
||||
except Exception:
|
||||
pass # Suppress exceptions during cleanup
|
||||
|
||||
|
||||
class LlamaExtract(BaseComponent):
|
||||
"""Factory class for creating and managing extraction agents."""
|
||||
|
||||
api_key: str = Field(description="The API key for the LlamaExtract API.")
|
||||
base_url: str = Field(description="The base URL of the LlamaExtract API.")
|
||||
check_interval: int = Field(
|
||||
default=1,
|
||||
description="The interval in seconds to check if the extraction is done.",
|
||||
)
|
||||
max_timeout: int = Field(
|
||||
default=2000,
|
||||
description="The maximum timeout in seconds to wait for the extraction to finish.",
|
||||
)
|
||||
num_workers: int = Field(
|
||||
default=4,
|
||||
gt=0,
|
||||
lt=10,
|
||||
description="The number of workers to use sending API requests for extraction.",
|
||||
)
|
||||
show_progress: bool = Field(
|
||||
default=True, description="Show progress when extracting multiple files."
|
||||
)
|
||||
verbose: bool = Field(
|
||||
default=False, description="Show verbose output when extracting files."
|
||||
)
|
||||
verify: Optional[bool] = Field(
|
||||
default=True, description="Simple SSL verification option."
|
||||
)
|
||||
httpx_timeout: Optional[float] = Field(
|
||||
default=60, description="Timeout for the httpx client."
|
||||
)
|
||||
_async_client: AsyncLlamaCloud = PrivateAttr()
|
||||
_thread_pool: ThreadPoolExecutor = PrivateAttr()
|
||||
_project_id: Optional[str] = PrivateAttr()
|
||||
_organization_id: Optional[str] = PrivateAttr()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
check_interval: int = 1,
|
||||
max_timeout: int = 2000,
|
||||
num_workers: int = 4,
|
||||
show_progress: bool = True,
|
||||
project_id: Optional[str] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
verify: Optional[bool] = True,
|
||||
httpx_timeout: Optional[float] = 60,
|
||||
verbose: bool = False,
|
||||
):
|
||||
if not api_key:
|
||||
api_key = os.getenv("LLAMA_CLOUD_API_KEY", None)
|
||||
if api_key is None:
|
||||
raise ValueError("The API key is required.")
|
||||
|
||||
if not base_url:
|
||||
base_url = os.getenv("LLAMA_CLOUD_BASE_URL", None) or DEFAULT_BASE_URL
|
||||
|
||||
super().__init__(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
check_interval=check_interval,
|
||||
max_timeout=max_timeout,
|
||||
num_workers=num_workers,
|
||||
show_progress=show_progress,
|
||||
verify=verify,
|
||||
httpx_timeout=httpx_timeout,
|
||||
verbose=verbose,
|
||||
)
|
||||
self._httpx_client = httpx.AsyncClient(verify=verify, timeout=httpx_timeout) # type: ignore
|
||||
self.verify = verify
|
||||
self.httpx_timeout = httpx_timeout
|
||||
|
||||
self._async_client = AsyncLlamaCloud(
|
||||
token=self.api_key,
|
||||
base_url=self.base_url,
|
||||
httpx_client=self._httpx_client,
|
||||
)
|
||||
self._thread_pool = ThreadPoolExecutor(
|
||||
max_workers=min(10, (os.cpu_count() or 1) + 4)
|
||||
)
|
||||
if not project_id:
|
||||
project_id = os.getenv("LLAMA_CLOUD_PROJECT_ID", None)
|
||||
self._project_id = project_id
|
||||
self._organization_id = organization_id
|
||||
|
||||
def _run_in_thread(self, coro: Coroutine[Any, Any, T]) -> T:
|
||||
"""Run coroutine in a separate thread to avoid event loop issues"""
|
||||
return run_in_thread(
|
||||
coro,
|
||||
self._thread_pool,
|
||||
self.verify, # type: ignore
|
||||
self.httpx_timeout, # type: ignore
|
||||
self._async_client._client_wrapper,
|
||||
)
|
||||
|
||||
def create_agent(
|
||||
self,
|
||||
name: str,
|
||||
data_schema: SchemaInput,
|
||||
config: Optional[ExtractConfig] = None,
|
||||
) -> ExtractionAgent:
|
||||
"""Create a new extraction agent.
|
||||
|
||||
Args:
|
||||
name (str): The name of the extraction agent
|
||||
data_schema (SchemaInput): The data schema for the extraction agent
|
||||
config (Optional[ExtractConfig]): The extraction config for the agent
|
||||
|
||||
Returns:
|
||||
ExtractionAgent: The created extraction agent
|
||||
"""
|
||||
if config is not None:
|
||||
_extraction_config_warning(config)
|
||||
else:
|
||||
config = DEFAULT_EXTRACT_CONFIG
|
||||
|
||||
if isinstance(data_schema, dict):
|
||||
data_schema = data_schema
|
||||
elif issubclass(data_schema, BaseModel):
|
||||
data_schema = data_schema.model_json_schema()
|
||||
else:
|
||||
raise ValueError(
|
||||
"data_schema must be either a dictionary or a Pydantic model"
|
||||
)
|
||||
|
||||
agent = self._run_in_thread(
|
||||
self._async_client.llama_extract.create_extraction_agent(
|
||||
project_id=self._project_id,
|
||||
organization_id=self._organization_id,
|
||||
name=name,
|
||||
data_schema=data_schema,
|
||||
config=config,
|
||||
)
|
||||
)
|
||||
|
||||
return ExtractionAgent(
|
||||
client=self._async_client,
|
||||
agent=agent,
|
||||
project_id=self._project_id,
|
||||
organization_id=self._organization_id,
|
||||
check_interval=self.check_interval,
|
||||
max_timeout=self.max_timeout,
|
||||
num_workers=self.num_workers,
|
||||
show_progress=self.show_progress,
|
||||
verbose=self.verbose,
|
||||
verify=self.verify,
|
||||
httpx_timeout=self.httpx_timeout,
|
||||
)
|
||||
|
||||
def get_agent(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
id: Optional[str] = None,
|
||||
) -> ExtractionAgent:
|
||||
"""Get extraction agents by name or extraction agent ID.
|
||||
|
||||
Args:
|
||||
name (Optional[str]): Filter by name
|
||||
extraction_agent_id (Optional[str]): Filter by extraction agent ID
|
||||
|
||||
Returns:
|
||||
ExtractionAgent: The extraction agent
|
||||
"""
|
||||
if id is not None and name is not None:
|
||||
warnings.warn(
|
||||
"Both name and extraction_agent_id are provided. Using extraction_agent_id."
|
||||
)
|
||||
|
||||
if id:
|
||||
agent = self._run_in_thread(
|
||||
self._async_client.llama_extract.get_extraction_agent(
|
||||
extraction_agent_id=id,
|
||||
)
|
||||
)
|
||||
|
||||
elif name:
|
||||
agent = self._run_in_thread(
|
||||
self._async_client.llama_extract.get_extraction_agent_by_name(
|
||||
name=name,
|
||||
project_id=self._project_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise ValueError("Either name or extraction_agent_id must be provided.")
|
||||
|
||||
return ExtractionAgent(
|
||||
client=self._async_client,
|
||||
agent=agent,
|
||||
project_id=self._project_id,
|
||||
organization_id=self._organization_id,
|
||||
check_interval=self.check_interval,
|
||||
max_timeout=self.max_timeout,
|
||||
num_workers=self.num_workers,
|
||||
show_progress=self.show_progress,
|
||||
verbose=self.verbose,
|
||||
verify=self.verify,
|
||||
httpx_timeout=self.httpx_timeout,
|
||||
)
|
||||
|
||||
def list_agents(self) -> List[ExtractionAgent]:
|
||||
"""List all available extraction agents."""
|
||||
agents = self._run_in_thread(
|
||||
self._async_client.llama_extract.list_extraction_agents(
|
||||
project_id=self._project_id,
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
ExtractionAgent(
|
||||
client=self._async_client,
|
||||
agent=agent,
|
||||
project_id=self._project_id,
|
||||
organization_id=self._organization_id,
|
||||
check_interval=self.check_interval,
|
||||
max_timeout=self.max_timeout,
|
||||
num_workers=self.num_workers,
|
||||
show_progress=self.show_progress,
|
||||
verbose=self.verbose,
|
||||
)
|
||||
for agent in agents
|
||||
]
|
||||
|
||||
def delete_agent(self, agent_id: str) -> None:
|
||||
"""Delete an extraction agent by ID.
|
||||
|
||||
Args:
|
||||
agent_id (str): ID of the extraction agent to delete
|
||||
"""
|
||||
self._run_in_thread(
|
||||
self._async_client.llama_extract.delete_extraction_agent(
|
||||
extraction_agent_id=agent_id
|
||||
)
|
||||
)
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""Cleanup resources properly."""
|
||||
try:
|
||||
if hasattr(self, "_thread_pool"):
|
||||
self._thread_pool.shutdown(wait=True)
|
||||
except Exception:
|
||||
pass # Suppress exceptions during cleanup
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
data_dir = Path(__file__).parent.parent / "tests" / "data"
|
||||
extractor = LlamaExtract()
|
||||
try:
|
||||
agent = extractor.get_agent(name="test-agent")
|
||||
except Exception:
|
||||
agent = extractor.create_agent(
|
||||
"test-agent",
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"summary": {"type": "string"},
|
||||
},
|
||||
},
|
||||
)
|
||||
results = agent.extract(data_dir / "slide" / "conocophilips.pdf")
|
||||
extractor.delete_agent(agent.id)
|
||||
print(results)
|
||||
@@ -0,0 +1,40 @@
|
||||
from typing import Any, Dict, List, Union, Generator
|
||||
from contextlib import contextmanager
|
||||
|
||||
# Asyncio error messages
|
||||
nest_asyncio_err = "cannot be called from a running event loop"
|
||||
nest_asyncio_msg = (
|
||||
"The event loop is already running. "
|
||||
"Add `import nest_asyncio; nest_asyncio.apply()` to your code to fix this issue."
|
||||
)
|
||||
|
||||
|
||||
def is_jupyter() -> bool:
|
||||
"""Check if we're running in a Jupyter environment."""
|
||||
try:
|
||||
from IPython import get_ipython
|
||||
|
||||
return get_ipython().__class__.__name__ == "ZMQInteractiveShell"
|
||||
except (ImportError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
@contextmanager
|
||||
def augment_async_errors() -> Generator[None, None, None]:
|
||||
"""Context manager to add helpful information for errors due to nested event loops."""
|
||||
try:
|
||||
yield
|
||||
except RuntimeError as e:
|
||||
if nest_asyncio_err in str(e):
|
||||
raise RuntimeError(nest_asyncio_msg)
|
||||
raise
|
||||
|
||||
|
||||
JSONType = Union[Dict[str, Any], List[Any], str, int, float, bool, None]
|
||||
JSONObjectType = Dict[str, JSONType]
|
||||
|
||||
|
||||
class ExperimentalWarning(Warning):
|
||||
"""Warning for experimental features."""
|
||||
|
||||
pass
|
||||
@@ -1,3 +1,8 @@
|
||||
from llama_cloud_services.parse.base import LlamaParse, ResultType
|
||||
from llama_cloud_services.parse.base import (
|
||||
LlamaParse,
|
||||
ResultType,
|
||||
ParsingMode,
|
||||
FailedPageMode,
|
||||
)
|
||||
|
||||
__all__ = ["LlamaParse", "ResultType"]
|
||||
__all__ = ["LlamaParse", "ResultType", "ParsingMode", "FailedPageMode"]
|
||||
|
||||
@@ -2,27 +2,41 @@ import asyncio
|
||||
import mimetypes
|
||||
import os
|
||||
import time
|
||||
import warnings
|
||||
from contextlib import asynccontextmanager
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
from io import BufferedIOBase
|
||||
from pathlib import Path, PurePath, PurePosixPath
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional, Union
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from fsspec import AbstractFileSystem
|
||||
from llama_index.core.async_utils import asyncio_run, run_jobs
|
||||
from llama_index.core.bridge.pydantic import Field, PrivateAttr, field_validator
|
||||
from llama_index.core.bridge.pydantic import (
|
||||
Field,
|
||||
PrivateAttr,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from llama_index.core.constants import DEFAULT_BASE_URL
|
||||
from llama_index.core.readers.base import BasePydanticReader
|
||||
from llama_index.core.readers.file.base import get_default_fs
|
||||
from llama_index.core.schema import Document
|
||||
|
||||
from llama_cloud_services.utils import check_extra_params
|
||||
from llama_cloud_services.parse.types import JobResult
|
||||
from llama_cloud_services.parse.utils import (
|
||||
SUPPORTED_FILE_TYPES,
|
||||
ResultType,
|
||||
ParsingMode,
|
||||
FailedPageMode,
|
||||
expand_target_pages,
|
||||
nest_asyncio_err,
|
||||
nest_asyncio_msg,
|
||||
make_api_request,
|
||||
partition_pages,
|
||||
)
|
||||
|
||||
# can put in a path to the file or the file bytes itself
|
||||
@@ -52,6 +66,44 @@ def build_url(
|
||||
return base_url
|
||||
|
||||
|
||||
class JobFailedException(Exception):
|
||||
"""Parse job failed exception."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
job_id: str,
|
||||
status: str,
|
||||
error_code: Optional[str] = None,
|
||||
error_message: Optional[str] = None,
|
||||
):
|
||||
exception_str = (
|
||||
f"Job ID: {job_id} failed with status: {status}, "
|
||||
f'Error code: {error_code or "No error code found"}, '
|
||||
f'Error message: {error_message or "No error message found"}'
|
||||
)
|
||||
super().__init__(exception_str)
|
||||
self.job_id = job_id
|
||||
self.status = status
|
||||
self.error_code = error_code
|
||||
self.error_message = error_message
|
||||
|
||||
@classmethod
|
||||
def from_result(cls, result_json: Dict[str, Any]) -> "JobFailedException":
|
||||
job_id = result_json["id"]
|
||||
status = result_json["status"]
|
||||
error_code = result_json.get("error_code")
|
||||
error_message = result_json.get("error_message")
|
||||
return cls(job_id, status, error_code=error_code, error_message=error_message)
|
||||
|
||||
|
||||
class BackoffPattern(str, Enum):
|
||||
"""Backoff pattern for polling."""
|
||||
|
||||
CONSTANT = "constant"
|
||||
LINEAR = "linear"
|
||||
EXPONENTIAL = "exponential"
|
||||
|
||||
|
||||
class LlamaParse(BasePydanticReader):
|
||||
"""A smart-parser for files."""
|
||||
|
||||
@@ -78,6 +130,15 @@ class LlamaParse(BasePydanticReader):
|
||||
description="The interval in seconds to check if the parsing is done.",
|
||||
)
|
||||
|
||||
backoff_pattern: BackoffPattern = Field(
|
||||
default=BackoffPattern.LINEAR,
|
||||
description="Controls the backoff pattern when retrying failed requests: 'constant', 'linear', or 'exponential'.",
|
||||
)
|
||||
max_check_interval: int = Field(
|
||||
default=5,
|
||||
description="Maximum interval in seconds between polling attempts when checking job status.",
|
||||
)
|
||||
|
||||
custom_client: Optional[httpx.AsyncClient] = Field(
|
||||
default=None, description="A custom HTTPX client to use for sending requests."
|
||||
)
|
||||
@@ -93,7 +154,7 @@ class LlamaParse(BasePydanticReader):
|
||||
num_workers: int = Field(
|
||||
default=4,
|
||||
gt=0,
|
||||
lt=10,
|
||||
lt=20,
|
||||
description="The number of workers to use sending API requests for parsing.",
|
||||
)
|
||||
result_type: ResultType = Field(
|
||||
@@ -111,6 +172,10 @@ class LlamaParse(BasePydanticReader):
|
||||
)
|
||||
|
||||
# Parsing specific configurations (Alphabetical order)
|
||||
adaptive_long_table: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, LlamaParse will try to detect long table and adapt the output.",
|
||||
)
|
||||
annotate_links: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Annotate links found in the document to extract their URL.",
|
||||
@@ -119,6 +184,10 @@ class LlamaParse(BasePydanticReader):
|
||||
default=False,
|
||||
description="If set to true, the parser will automatically select the best mode to extract text from documents based on the rules provide. Will use the 'accurate' default mode by default and will upgrade page that match the rule to Premium mode.",
|
||||
)
|
||||
auto_mode_configuration_json: Optional[str] = Field(
|
||||
default=None,
|
||||
description="A JSON string containing the configuration for the auto mode. If set, the parser will use the provided configuration for the auto mode.",
|
||||
)
|
||||
auto_mode_trigger_on_image_in_page: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If auto_mode is set to true, the parser will upgrade the page that contain an image to Premium mode.",
|
||||
@@ -163,13 +232,9 @@ class LlamaParse(BasePydanticReader):
|
||||
default=None,
|
||||
description="The top margin of the bounding box to use to extract text from documents expressed as a float between 0 and 1 representing the percentage of the page height.",
|
||||
)
|
||||
complemental_formatting_instruction: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The complemental formatting instruction for the parser. Tell llamaParse how some thing should to be formatted, while retaining the markdown output.",
|
||||
)
|
||||
content_guideline_instruction: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The content guideline for the parser. Tell LlamaParse how the content should be changed / transformed.",
|
||||
compact_markdown_table: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will output compact markdown table (without trailing spaces in cells).",
|
||||
)
|
||||
continuous_mode: Optional[bool] = Field(
|
||||
default=False,
|
||||
@@ -203,14 +268,15 @@ class LlamaParse(BasePydanticReader):
|
||||
default=False,
|
||||
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.",
|
||||
)
|
||||
formatting_instruction: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The Formatting instruction for the parser. Override default llamaParse behavior. In most case you want to use complemental_formatting_instruction instead.",
|
||||
)
|
||||
|
||||
guess_xlsx_sheet_names: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Whether to guess the sheet names of the xlsx file.",
|
||||
)
|
||||
high_res_ocr: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will use high resolution OCR to extract text from images. This will increase the accuracy of the parsing job, but reduce the speed.",
|
||||
)
|
||||
html_make_all_elements_visible: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, when parsing HTML the parser will consider all elements display not element as display block.",
|
||||
@@ -250,6 +316,10 @@ class LlamaParse(BasePydanticReader):
|
||||
language: Optional[str] = Field(
|
||||
default="en", description="The language of the text to parse."
|
||||
)
|
||||
markdown_table_multiline_header_separator: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The separator to use to split the header of the markdown table into multiple lines. Default is: <br/>",
|
||||
)
|
||||
max_pages: Optional[int] = Field(
|
||||
default=None,
|
||||
description="The maximum number of pages to extract text from documents. If set to 0 or not set, all pages will be that should be extracted will be extracted (can work in combination with targetPages).",
|
||||
@@ -270,6 +340,14 @@ class LlamaParse(BasePydanticReader):
|
||||
default=False,
|
||||
description="If set to true, the parser will output tables as HTML in the markdown.",
|
||||
)
|
||||
outlined_table_extraction: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will use a dedicated approach to extract tables with outlined cells. This is useful for documents with spreadsheet-like tables where cells are outlined with borders. This could lead to false positives, so use with caution.",
|
||||
)
|
||||
page_error_tolerance: Optional[float] = Field(
|
||||
default=None,
|
||||
description="The error tolerance for the number of pages with error in a doc (percentage express as 0-1). If we fail to parse a greater percentage of pages than the tolerance value we fail the job.",
|
||||
)
|
||||
page_prefix: Optional[str] = Field(
|
||||
default=None,
|
||||
description="A templated prefix to add to the beginning of each page. If it contain `{page_number}`, it will be replaced by the page number.",
|
||||
@@ -282,10 +360,34 @@ class LlamaParse(BasePydanticReader):
|
||||
default=None,
|
||||
description="A templated suffix to add to the beginning of each page. If it contain `{page_number}`, it will be replaced by the page number.",
|
||||
)
|
||||
parse_mode: Optional[Union[ParsingMode, str]] = Field(
|
||||
default=None,
|
||||
description="The parsing mode to use, see ParsingMode enum for possible values ",
|
||||
)
|
||||
premium_mode: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Use our best parser mode if set to True.",
|
||||
)
|
||||
preset: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The preset to use for the parser. If set, the parser will use the preset configuration. See LlamaParse documentation for available presets. Preset override most other parameters.",
|
||||
)
|
||||
preserve_layout_alignment_across_pages: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Preserve grid alignment across page in text mode.",
|
||||
)
|
||||
replace_failed_page_mode: Optional[FailedPageMode] = Field(
|
||||
default=None,
|
||||
description="The mode to use to replace the failed page, see FailedPageMode enum for possible value. If set, the parser will replace the failed page with the specified mode. If not set, the default mode (raw_text) will be used.",
|
||||
)
|
||||
replace_failed_page_with_error_message_prefix: Optional[str] = Field(
|
||||
default=None,
|
||||
description="A prefix to add before error message in failed pages. If not set, no prefix will be used.",
|
||||
)
|
||||
replace_failed_page_with_error_message_suffix: Optional[str] = Field(
|
||||
default=None,
|
||||
description="A suffix to add after error message in failed pages. If not set, no suffix will be used.",
|
||||
)
|
||||
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).",
|
||||
@@ -327,6 +429,14 @@ class LlamaParse(BasePydanticReader):
|
||||
default=None,
|
||||
description="The named JSON Schema to use to structure the output of the parsing job. For convenience / testing, LlamaParse provides a few named JSON Schema that can be used directly. Use 'imFeelingLucky' to let llamaParse dream the schema.",
|
||||
)
|
||||
system_prompt: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The system prompt. Replace llamaParse default system prompt, may impact accuracy",
|
||||
)
|
||||
system_prompt_append: Optional[str] = Field(
|
||||
default=None,
|
||||
description="String to append to default system prompt.",
|
||||
)
|
||||
take_screenshot: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Whether to take screenshot of each page of the document.",
|
||||
@@ -335,9 +445,9 @@ class LlamaParse(BasePydanticReader):
|
||||
default=None,
|
||||
description="The target pages to extract text from documents. Describe as a comma separated list of page numbers. The first page of the document is page 0",
|
||||
)
|
||||
use_vendor_multimodal_model: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Whether to use the vendor multimodal API.",
|
||||
user_prompt: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The user prompt. Replace llamaParse default user prompt",
|
||||
)
|
||||
vendor_multimodal_api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
@@ -347,6 +457,10 @@ class LlamaParse(BasePydanticReader):
|
||||
default=None,
|
||||
description="The model name for the vendor multimodal API.",
|
||||
)
|
||||
model: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The document model name to be used with `parse_with_agent`.",
|
||||
)
|
||||
webhook_url: Optional[str] = Field(
|
||||
default=None,
|
||||
description="A URL that needs to be called at the end of the parsing job.",
|
||||
@@ -357,6 +471,18 @@ class LlamaParse(BasePydanticReader):
|
||||
default=None,
|
||||
description="The bounding box to use to extract text from documents describe as a string containing the bounding box margins",
|
||||
)
|
||||
complemental_formatting_instruction: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The complemental formatting instruction for the parser. Tell llamaParse how some thing should to be formatted, while retaining the markdown output.",
|
||||
)
|
||||
content_guideline_instruction: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The content guideline for the parser. Tell LlamaParse how the content should be changed / transformed.",
|
||||
)
|
||||
formatting_instruction: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The Formatting instruction for the parser. Override default llamaParse behavior. In most case you want to use complemental_formatting_instruction instead.",
|
||||
)
|
||||
gpt4o_mode: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Whether to use gpt-4o extract text from documents.",
|
||||
@@ -373,6 +499,31 @@ class LlamaParse(BasePydanticReader):
|
||||
default="", description="The parsing instruction for the parser."
|
||||
)
|
||||
|
||||
use_vendor_multimodal_model: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Whether to use the vendor multimodal API.",
|
||||
)
|
||||
|
||||
partition_pages: Optional[int] = Field(
|
||||
default=None,
|
||||
description="If set, documents will automatically be partitioned into segments containing the specified number of pages at most. Parsing will be split into separate jobs for each partition segment. Can be used in combination with targetPages and maxPages.",
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def warn_extra_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
extra_params, suggestions = check_extra_params(cls, data)
|
||||
if extra_params:
|
||||
suggestions = [f"\n - {suggestion}" for suggestion in suggestions]
|
||||
suggestions_str = "".join(suggestions)
|
||||
warnings.warn(
|
||||
"The following parameters are unused: "
|
||||
+ ", ".join(extra_params)
|
||||
+ f".\n{suggestions_str}",
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@field_validator("api_key", mode="before", check_fields=True)
|
||||
@classmethod
|
||||
def validate_api_key(cls, v: str) -> str:
|
||||
@@ -460,6 +611,7 @@ class LlamaParse(BasePydanticReader):
|
||||
file_input: FileInput,
|
||||
extra_info: Optional[dict] = None,
|
||||
fs: Optional[AbstractFileSystem] = None,
|
||||
partition_target_pages: Optional[str] = None,
|
||||
) -> str:
|
||||
files = None
|
||||
file_handle = None
|
||||
@@ -501,12 +653,18 @@ class LlamaParse(BasePydanticReader):
|
||||
|
||||
data["from_python_package"] = True
|
||||
|
||||
if self.adaptive_long_table:
|
||||
data["adaptive_long_table"] = self.adaptive_long_table
|
||||
|
||||
if self.annotate_links:
|
||||
data["annotate_links"] = self.annotate_links
|
||||
|
||||
if self.auto_mode:
|
||||
data["auto_mode"] = self.auto_mode
|
||||
|
||||
if self.auto_mode_configuration_json is not None:
|
||||
data["auto_mode_configuration_json"] = self.auto_mode_configuration_json
|
||||
|
||||
if self.auto_mode_trigger_on_image_in_page:
|
||||
data[
|
||||
"auto_mode_trigger_on_image_in_page"
|
||||
@@ -551,12 +709,21 @@ class LlamaParse(BasePydanticReader):
|
||||
if self.bbox_top is not None:
|
||||
data["bbox_top"] = self.bbox_top
|
||||
|
||||
if self.compact_markdown_table:
|
||||
data["compact_markdown_table"] = self.compact_markdown_table
|
||||
|
||||
if self.complemental_formatting_instruction:
|
||||
print(
|
||||
"WARNING: complemental_formatting_instruction is deprecated and may be remove in a future release. Use system_prompt, system_prompt_append or user_prompt instead."
|
||||
)
|
||||
data[
|
||||
"complemental_formatting_instruction"
|
||||
] = self.complemental_formatting_instruction
|
||||
|
||||
if self.content_guideline_instruction:
|
||||
print(
|
||||
"WARNING: content_guideline_instruction is deprecated and may be remove in a future release. Use system_prompt, system_prompt_append or user_prompt instead."
|
||||
)
|
||||
data["content_guideline_instruction"] = self.content_guideline_instruction
|
||||
|
||||
if self.continuous_mode:
|
||||
@@ -584,6 +751,9 @@ class LlamaParse(BasePydanticReader):
|
||||
data["fast_mode"] = self.fast_mode
|
||||
|
||||
if self.formatting_instruction:
|
||||
print(
|
||||
"WARNING: formatting_instruction is deprecated and may be remove in a future release. Use system_prompt, system_prompt_append or user_prompt instead."
|
||||
)
|
||||
data["formatting_instruction"] = self.formatting_instruction
|
||||
|
||||
if self.guess_xlsx_sheet_names:
|
||||
@@ -592,6 +762,9 @@ class LlamaParse(BasePydanticReader):
|
||||
if self.html_make_all_elements_visible:
|
||||
data["html_make_all_elements_visible"] = self.html_make_all_elements_visible
|
||||
|
||||
if self.high_res_ocr:
|
||||
data["high_res_ocr"] = self.high_res_ocr
|
||||
|
||||
if self.html_remove_fixed_elements:
|
||||
data["html_remove_fixed_elements"] = self.html_remove_fixed_elements
|
||||
|
||||
@@ -623,6 +796,9 @@ class LlamaParse(BasePydanticReader):
|
||||
data["invalidate_cache"] = self.invalidate_cache
|
||||
|
||||
if self.is_formatting_instruction:
|
||||
print(
|
||||
"WARNING: formatting_instruction is deprecated and may be remove in a future release. Use system_prompt, system_prompt_append or user_prompt instead."
|
||||
)
|
||||
data["is_formatting_instruction"] = self.is_formatting_instruction
|
||||
|
||||
if self.job_timeout_extra_time_per_page_in_seconds is not None:
|
||||
@@ -651,6 +827,12 @@ class LlamaParse(BasePydanticReader):
|
||||
if self.output_tables_as_HTML:
|
||||
data["output_tables_as_HTML"] = self.output_tables_as_HTML
|
||||
|
||||
if self.outlined_table_extraction:
|
||||
data["outlined_table_extraction"] = self.outlined_table_extraction
|
||||
|
||||
if self.page_error_tolerance is not None:
|
||||
data["page_error_tolerance"] = self.page_error_tolerance
|
||||
|
||||
if self.page_prefix is not None:
|
||||
data["page_prefix"] = self.page_prefix
|
||||
|
||||
@@ -664,13 +846,37 @@ class LlamaParse(BasePydanticReader):
|
||||
|
||||
if self.parsing_instruction:
|
||||
print(
|
||||
"WARNING: parsing_instruction is deprecated. Use complemental_formatting_instruction or content_guideline_instruction instead."
|
||||
"WARNING: parsing_instruction is deprecated. Use system_prompt, system_prompt_append or user_prompt instead."
|
||||
)
|
||||
data["parsing_instruction"] = self.parsing_instruction
|
||||
|
||||
if self.parse_mode:
|
||||
data["parse_mode"] = self.parse_mode
|
||||
|
||||
if self.premium_mode:
|
||||
data["premium_mode"] = self.premium_mode
|
||||
|
||||
if self.preserve_layout_alignment_across_pages:
|
||||
data[
|
||||
"preserve_layout_alignment_across_pages"
|
||||
] = self.preserve_layout_alignment_across_pages
|
||||
|
||||
if self.preset is not None:
|
||||
data["preset"] = self.preset
|
||||
|
||||
if self.replace_failed_page_mode is not None:
|
||||
data["replace_failed_page_mode"] = self.replace_failed_page_mode.value
|
||||
|
||||
if self.replace_failed_page_with_error_message_prefix is not None:
|
||||
data[
|
||||
"replace_failed_page_with_error_message_prefix"
|
||||
] = self.replace_failed_page_with_error_message_prefix
|
||||
|
||||
if self.replace_failed_page_with_error_message_suffix is not None:
|
||||
data[
|
||||
"replace_failed_page_with_error_message_suffix"
|
||||
] = self.replace_failed_page_with_error_message_suffix
|
||||
|
||||
if self.skip_diagonal_text:
|
||||
data["skip_diagonal_text"] = self.skip_diagonal_text
|
||||
|
||||
@@ -699,13 +905,19 @@ class LlamaParse(BasePydanticReader):
|
||||
data[
|
||||
"structured_output_json_schema_name"
|
||||
] = self.structured_output_json_schema_name
|
||||
|
||||
if self.system_prompt is not None:
|
||||
data["system_prompt"] = self.system_prompt
|
||||
if self.system_prompt_append is not None:
|
||||
data["system_prompt_append"] = self.system_prompt_append
|
||||
if self.take_screenshot:
|
||||
data["take_screenshot"] = self.take_screenshot
|
||||
|
||||
if self.target_pages is not None:
|
||||
if partition_target_pages is not None:
|
||||
data["target_pages"] = partition_target_pages
|
||||
elif self.target_pages is not None:
|
||||
data["target_pages"] = self.target_pages
|
||||
|
||||
if self.user_prompt is not None:
|
||||
data["user_prompt"] = self.user_prompt
|
||||
if self.use_vendor_multimodal_model:
|
||||
data["use_vendor_multimodal_model"] = self.use_vendor_multimodal_model
|
||||
|
||||
@@ -715,9 +927,17 @@ class LlamaParse(BasePydanticReader):
|
||||
if self.vendor_multimodal_model_name is not None:
|
||||
data["vendor_multimodal_model_name"] = self.vendor_multimodal_model_name
|
||||
|
||||
if self.model is not None:
|
||||
data["model"] = self.model
|
||||
|
||||
if self.webhook_url is not None:
|
||||
data["webhook_url"] = self.webhook_url
|
||||
|
||||
if self.markdown_table_multiline_header_separator is not None:
|
||||
data[
|
||||
"markdown_table_multiline_header_separator"
|
||||
] = self.markdown_table_multiline_header_separator
|
||||
|
||||
# Deprecated
|
||||
if self.bounding_box is not None:
|
||||
data["bounding_box"] = self.bounding_box
|
||||
@@ -730,7 +950,7 @@ class LlamaParse(BasePydanticReader):
|
||||
|
||||
try:
|
||||
url = build_url(JOB_UPLOAD_ROUTE, self.organization_id, self.project_id)
|
||||
resp = await self.aclient.post(url, files=files, data=data) # type: ignore
|
||||
resp = await make_api_request(self.aclient, "POST", url, timeout=self.max_timeout, files=files, data=data) # type: ignore
|
||||
resp.raise_for_status() # this raises if status is not 2xx
|
||||
return resp.json()["id"]
|
||||
except httpx.HTTPStatusError as err: # this catches it
|
||||
@@ -740,54 +960,201 @@ class LlamaParse(BasePydanticReader):
|
||||
if file_handle is not None:
|
||||
file_handle.close()
|
||||
|
||||
def _calculate_backoff(self, current_interval: float) -> float:
|
||||
"""Calculate the next backoff interval based on the backoff pattern.
|
||||
|
||||
Args:
|
||||
current_interval: The current interval in seconds
|
||||
|
||||
Returns:
|
||||
The next interval in seconds
|
||||
"""
|
||||
if self.backoff_pattern == BackoffPattern.CONSTANT:
|
||||
return current_interval
|
||||
elif self.backoff_pattern == BackoffPattern.LINEAR:
|
||||
return min(current_interval + 1, float(self.max_check_interval))
|
||||
elif self.backoff_pattern == BackoffPattern.EXPONENTIAL:
|
||||
return min(current_interval * 2, float(self.max_check_interval))
|
||||
return current_interval # Default fallback
|
||||
|
||||
async def _get_job_result(
|
||||
self, job_id: str, result_type: str, verbose: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
start = time.time()
|
||||
tries = 0
|
||||
error_count = 0
|
||||
current_interval: float = float(self.check_interval)
|
||||
|
||||
# so we're not re-setting the headers & stuff on each
|
||||
# usage... assume that there is not some other
|
||||
# coro also modifying base_url and the other client related configs.
|
||||
client = self.aclient
|
||||
while True:
|
||||
await asyncio.sleep(self.check_interval)
|
||||
tries += 1
|
||||
result = await client.get(JOB_STATUS_ROUTE.format(job_id=job_id))
|
||||
if result.status_code != 200:
|
||||
try:
|
||||
await asyncio.sleep(current_interval)
|
||||
tries += 1
|
||||
result = await client.get(JOB_STATUS_ROUTE.format(job_id=job_id))
|
||||
result.raise_for_status() # this raises if status is not 2xx
|
||||
# Allowed values "PENDING", "SUCCESS", "ERROR", "CANCELED"
|
||||
result_json = result.json()
|
||||
status = result_json["status"]
|
||||
if status == "SUCCESS":
|
||||
parsed_result = await client.get(
|
||||
JOB_RESULT_URL.format(job_id=job_id, result_type=result_type),
|
||||
)
|
||||
return parsed_result.json()
|
||||
elif status == "PENDING":
|
||||
end = time.time()
|
||||
if end - start > self.max_timeout:
|
||||
raise Exception(f"Timeout while parsing the file: {job_id}")
|
||||
if verbose and tries % 10 == 0:
|
||||
print(".", end="", flush=True)
|
||||
current_interval = self._calculate_backoff(current_interval)
|
||||
else:
|
||||
raise JobFailedException.from_result(result_json)
|
||||
except (
|
||||
httpx.ConnectError,
|
||||
httpx.ReadError,
|
||||
httpx.WriteError,
|
||||
httpx.ConnectTimeout,
|
||||
httpx.ReadTimeout,
|
||||
httpx.WriteTimeout,
|
||||
httpx.HTTPStatusError,
|
||||
) as err:
|
||||
error_count += 1
|
||||
end = time.time()
|
||||
if end - start > self.max_timeout:
|
||||
raise Exception(f"Timeout while parsing the file: {job_id}")
|
||||
raise Exception(
|
||||
f"Timeout while parsing the file: {job_id}"
|
||||
) from err
|
||||
if verbose and tries % 10 == 0:
|
||||
print(".", end="", flush=True)
|
||||
await asyncio.sleep(self.check_interval)
|
||||
continue
|
||||
print(
|
||||
f"HTTP error: {err}...",
|
||||
flush=True,
|
||||
)
|
||||
current_interval = self._calculate_backoff(current_interval)
|
||||
|
||||
# Allowed values "PENDING", "SUCCESS", "ERROR", "CANCELED"
|
||||
result_json = result.json()
|
||||
status = result_json["status"]
|
||||
if status == "SUCCESS":
|
||||
parsed_result = await client.get(
|
||||
JOB_RESULT_URL.format(job_id=job_id, result_type=result_type),
|
||||
async def _parse_one(
|
||||
self,
|
||||
file_path: FileInput,
|
||||
extra_info: Optional[dict] = None,
|
||||
fs: Optional[AbstractFileSystem] = None,
|
||||
result_type: Optional[str] = None,
|
||||
num_workers: Optional[int] = None,
|
||||
) -> List[Tuple[str, Dict[str, Any]]]:
|
||||
if self.partition_pages is None:
|
||||
job_results = [
|
||||
await self._parse_one_unpartitioned(
|
||||
file_path,
|
||||
extra_info=extra_info,
|
||||
fs=fs,
|
||||
result_type=result_type,
|
||||
)
|
||||
return parsed_result.json()
|
||||
]
|
||||
else:
|
||||
job_results = await self._parse_one_partitioned(
|
||||
file_path,
|
||||
extra_info,
|
||||
fs=fs,
|
||||
result_type=result_type,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
return job_results
|
||||
|
||||
elif status == "PENDING":
|
||||
end = time.time()
|
||||
if end - start > self.max_timeout:
|
||||
raise Exception(f"Timeout while parsing the file: {job_id}")
|
||||
if verbose and tries % 10 == 0:
|
||||
print(".", end="", flush=True)
|
||||
await asyncio.sleep(self.check_interval)
|
||||
async def _parse_one_unpartitioned(
|
||||
self,
|
||||
file_path: FileInput,
|
||||
extra_info: Optional[dict] = None,
|
||||
fs: Optional[AbstractFileSystem] = None,
|
||||
result_type: Optional[str] = None,
|
||||
**create_kwargs: Any,
|
||||
) -> Tuple[str, Dict[str, Any]]:
|
||||
"""Create one parse job and wait for the result."""
|
||||
job_id = await self._create_job(
|
||||
file_path, extra_info=extra_info, fs=fs, **create_kwargs
|
||||
)
|
||||
if self.verbose:
|
||||
print("Started parsing the file under job_id %s" % job_id)
|
||||
result = await self._get_job_result(
|
||||
job_id, result_type or self.result_type.value, verbose=self.verbose
|
||||
)
|
||||
return job_id, result
|
||||
|
||||
async def _parse_one_partitioned(
|
||||
self,
|
||||
file_path: FileInput,
|
||||
extra_info: Optional[dict] = None,
|
||||
fs: Optional[AbstractFileSystem] = None,
|
||||
result_type: Optional[str] = None,
|
||||
num_workers: Optional[int] = None,
|
||||
) -> List[Tuple[str, Dict[str, Any]]]:
|
||||
"""Partition a file and run separate parse jobs per partition segment."""
|
||||
assert self.partition_pages is not None
|
||||
|
||||
num_workers = num_workers or self.num_workers
|
||||
if num_workers < 1:
|
||||
raise ValueError("Invalid number of workers")
|
||||
if self.target_pages is not None:
|
||||
jobs = [
|
||||
self._parse_one_unpartitioned(
|
||||
file_path,
|
||||
extra_info=extra_info,
|
||||
fs=fs,
|
||||
result_type=result_type,
|
||||
partition_target_pages=target_pages,
|
||||
)
|
||||
for target_pages in partition_pages(
|
||||
expand_target_pages(self.target_pages),
|
||||
self.partition_pages,
|
||||
max_pages=self.max_pages,
|
||||
)
|
||||
]
|
||||
return await run_jobs(
|
||||
jobs,
|
||||
workers=num_workers,
|
||||
desc="Getting job results",
|
||||
show_progress=self.show_progress,
|
||||
)
|
||||
|
||||
total = 0
|
||||
results: List[Tuple[str, Dict[str, Any]]] = []
|
||||
while self.max_pages is None or total < self.max_pages:
|
||||
if (
|
||||
self.max_pages is not None
|
||||
and total + self.partition_pages >= self.max_pages
|
||||
):
|
||||
size = self.max_pages - total
|
||||
else:
|
||||
error_code = result_json.get("error_code", "No error code found")
|
||||
error_message = result_json.get(
|
||||
"error_message", "No error message found"
|
||||
size = self.partition_pages
|
||||
if not size:
|
||||
break
|
||||
try:
|
||||
# Fetch JSON result type first to get accurate pagination data
|
||||
# and then fetch the user's desired result type if needed
|
||||
job_id, json_result = await self._parse_one_unpartitioned(
|
||||
file_path,
|
||||
extra_info=extra_info,
|
||||
fs=fs,
|
||||
result_type=ResultType.JSON.value,
|
||||
partition_target_pages=f"{total}-{total + size - 1}",
|
||||
)
|
||||
|
||||
exception_str = f"Job ID: {job_id} failed with status: {status}, Error code: {error_code}, Error message: {error_message}"
|
||||
raise Exception(exception_str)
|
||||
result_type = result_type or self.result_type.value
|
||||
if result_type == ResultType.JSON.value:
|
||||
job_result = json_result
|
||||
else:
|
||||
job_result = await self._get_job_result(
|
||||
job_id, result_type, verbose=self.verbose
|
||||
)
|
||||
except JobFailedException as e:
|
||||
if results and e.error_code == "NO_DATA_FOUND_IN_FILE":
|
||||
# Expected when we try to read past the end of the file
|
||||
return results
|
||||
raise
|
||||
results.append((job_id, job_result))
|
||||
if len(json_result["pages"]) < size:
|
||||
break
|
||||
total += size
|
||||
return results
|
||||
|
||||
async def _aload_data(
|
||||
self,
|
||||
@@ -795,20 +1162,23 @@ class LlamaParse(BasePydanticReader):
|
||||
extra_info: Optional[dict] = None,
|
||||
fs: Optional[AbstractFileSystem] = None,
|
||||
verbose: bool = False,
|
||||
num_workers: Optional[int] = None,
|
||||
) -> List[Document]:
|
||||
"""Load data from the input path."""
|
||||
try:
|
||||
job_id = await self._create_job(file_path, extra_info=extra_info, fs=fs)
|
||||
if verbose:
|
||||
print("Started parsing the file under job_id %s" % job_id)
|
||||
|
||||
result = await self._get_job_result(
|
||||
job_id, self.result_type.value, verbose=verbose
|
||||
)
|
||||
|
||||
results = [
|
||||
job_result
|
||||
for _, job_result in await self._parse_one(
|
||||
file_path, extra_info, fs=fs, num_workers=num_workers
|
||||
)
|
||||
]
|
||||
# Flatten the resulting doc if it was partitioned
|
||||
separator = self.page_separator or _DEFAULT_SEPARATOR
|
||||
docs = [
|
||||
Document(
|
||||
text=result[self.result_type.value],
|
||||
text=separator.join(
|
||||
result[self.result_type.value] for result in results
|
||||
),
|
||||
metadata=extra_info or {},
|
||||
)
|
||||
]
|
||||
@@ -831,7 +1201,11 @@ class LlamaParse(BasePydanticReader):
|
||||
extra_info: Optional[dict] = None,
|
||||
fs: Optional[AbstractFileSystem] = None,
|
||||
) -> List[Document]:
|
||||
"""Load data from the input path."""
|
||||
"""Load data from the input path.
|
||||
|
||||
File(s) which were partitioned before parsing will be loaded as a single
|
||||
re-assembled Document.
|
||||
"""
|
||||
if isinstance(file_path, (str, PurePosixPath, Path, bytes, BufferedIOBase)):
|
||||
return await self._aload_data(
|
||||
file_path, extra_info=extra_info, fs=fs, verbose=self.verbose
|
||||
@@ -843,6 +1217,7 @@ class LlamaParse(BasePydanticReader):
|
||||
extra_info=extra_info,
|
||||
fs=fs,
|
||||
verbose=self.verbose and not self.show_progress,
|
||||
num_workers=1,
|
||||
)
|
||||
for f in file_path
|
||||
]
|
||||
@@ -881,21 +1256,161 @@ class LlamaParse(BasePydanticReader):
|
||||
else:
|
||||
raise e
|
||||
|
||||
async def _aparse_one(
|
||||
self,
|
||||
file_path: FileInput,
|
||||
file_name: str,
|
||||
extra_info: Optional[dict] = None,
|
||||
fs: Optional[AbstractFileSystem] = None,
|
||||
num_workers: Optional[int] = None,
|
||||
) -> List[JobResult]:
|
||||
job_results = await self._parse_one(
|
||||
file_path,
|
||||
extra_info,
|
||||
fs=fs,
|
||||
result_type=ResultType.JSON.value,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
return [
|
||||
JobResult(
|
||||
job_id=job_id,
|
||||
file_name=file_name,
|
||||
job_result=job_result,
|
||||
api_key=self.api_key,
|
||||
base_url=self.base_url,
|
||||
client=self.aclient,
|
||||
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
|
||||
)
|
||||
for job_id, job_result in job_results
|
||||
]
|
||||
|
||||
async def aparse(
|
||||
self,
|
||||
file_path: Union[List[FileInput], FileInput],
|
||||
extra_info: Optional[dict] = None,
|
||||
fs: Optional[AbstractFileSystem] = None,
|
||||
) -> Union[List["JobResult"], "JobResult"]:
|
||||
"""
|
||||
Parse the file and return a JobResult object instead of Document objects.
|
||||
|
||||
This method is similar to aload_data but returns JobResult objects that provide
|
||||
direct access to the various output formats (text, markdown, json, etc.)
|
||||
|
||||
Args:
|
||||
file_path: Path to the file to parse. Can be a string, path, bytes, file-like object, or a list of these.
|
||||
extra_info: Additional metadata to include in the result.
|
||||
fs: Optional filesystem to use for reading files.
|
||||
|
||||
Returns:
|
||||
JobResult object or list of JobResult objects if either multiple files were provided or file(s) were partitioned before parsing.
|
||||
"""
|
||||
|
||||
if isinstance(file_path, (str, PurePosixPath, Path, bytes, BufferedIOBase)):
|
||||
if isinstance(file_path, (bytes, BufferedIOBase)):
|
||||
if not extra_info or "file_name" not in extra_info:
|
||||
raise ValueError(
|
||||
"file_name must be provided in extra_info when passing bytes"
|
||||
)
|
||||
file_name = extra_info["file_name"]
|
||||
else:
|
||||
file_name = str(file_path)
|
||||
result = await self._aparse_one(
|
||||
file_path, file_name, extra_info=extra_info, fs=fs
|
||||
)
|
||||
return result[0] if len(result) == 1 else result
|
||||
|
||||
elif isinstance(file_path, list):
|
||||
file_names = []
|
||||
for f in file_path:
|
||||
if isinstance(f, (bytes, BufferedIOBase)):
|
||||
if not extra_info or "file_name" not in extra_info:
|
||||
raise ValueError(
|
||||
"file_name must be provided in extra_info when passing bytes"
|
||||
)
|
||||
file_names.append(extra_info["file_name"])
|
||||
else:
|
||||
file_names.append(str(f))
|
||||
|
||||
job_results = []
|
||||
try:
|
||||
for result in await run_jobs(
|
||||
[
|
||||
self._aparse_one(
|
||||
f,
|
||||
file_names[i],
|
||||
extra_info=extra_info,
|
||||
fs=fs,
|
||||
num_workers=1,
|
||||
)
|
||||
for i, f in enumerate(file_path)
|
||||
],
|
||||
workers=self.num_workers,
|
||||
desc="Getting job results",
|
||||
show_progress=self.show_progress,
|
||||
):
|
||||
job_results.extend(result)
|
||||
return job_results
|
||||
|
||||
except RuntimeError as e:
|
||||
if nest_asyncio_err in str(e):
|
||||
raise RuntimeError(nest_asyncio_msg)
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise ValueError(
|
||||
"The input file_path must be a string or a list of strings."
|
||||
)
|
||||
|
||||
def parse(
|
||||
self,
|
||||
file_path: Union[List[FileInput], FileInput],
|
||||
extra_info: Optional[dict] = None,
|
||||
fs: Optional[AbstractFileSystem] = None,
|
||||
) -> Union[List["JobResult"], "JobResult"]:
|
||||
"""
|
||||
Parse the file and return a JobResult object instead of Document objects.
|
||||
|
||||
This method is similar to load_data but returns JobResult objects that provide
|
||||
direct access to the various output formats (text, markdown, json, etc.)
|
||||
|
||||
Args:
|
||||
file_path: Path to the file to parse. Can be a string, path, bytes, file-like object, or a list of these.
|
||||
extra_info: Additional metadata to include in the result.
|
||||
fs: Optional filesystem to use for reading files.
|
||||
|
||||
Returns:
|
||||
JobResult object or list of JobResult objects if multiple files were provided
|
||||
"""
|
||||
try:
|
||||
return asyncio_run(self.aparse(file_path, extra_info, fs=fs))
|
||||
except RuntimeError as e:
|
||||
if nest_asyncio_err in str(e):
|
||||
raise RuntimeError(nest_asyncio_msg)
|
||||
else:
|
||||
raise e
|
||||
|
||||
async def _aget_json(
|
||||
self, file_path: FileInput, extra_info: Optional[dict] = None
|
||||
self,
|
||||
file_path: FileInput,
|
||||
extra_info: Optional[dict] = None,
|
||||
num_workers: Optional[int] = None,
|
||||
) -> List[dict]:
|
||||
"""Load data from the input path."""
|
||||
try:
|
||||
job_id = await self._create_job(file_path, extra_info=extra_info)
|
||||
if self.verbose:
|
||||
print("Started parsing the file under job_id %s" % job_id)
|
||||
result = await self._get_job_result(job_id, "json")
|
||||
result["job_id"] = job_id
|
||||
job_results = await self._parse_one(
|
||||
file_path,
|
||||
extra_info=extra_info,
|
||||
result_type=ResultType.JSON.value,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
|
||||
if not isinstance(file_path, (bytes, BufferedIOBase)):
|
||||
result["file_path"] = str(file_path)
|
||||
|
||||
return [result]
|
||||
results = []
|
||||
for job_id, job_result in job_results:
|
||||
job_result["job_id"] = job_id
|
||||
if not isinstance(file_path, (bytes, BufferedIOBase)):
|
||||
job_result["file_path"] = str(file_path)
|
||||
results.append(job_result)
|
||||
return results
|
||||
except Exception as e:
|
||||
file_repr = file_path if isinstance(file_path, str) else "<bytes/buffer>"
|
||||
print(f"Error while parsing the file '{file_repr}':", e)
|
||||
@@ -910,7 +1425,7 @@ class LlamaParse(BasePydanticReader):
|
||||
extra_info: Optional[dict] = None,
|
||||
) -> List[dict]:
|
||||
"""Load data from the input path."""
|
||||
if isinstance(file_path, (str, Path)):
|
||||
if isinstance(file_path, (str, PurePosixPath, Path, bytes, BufferedIOBase)):
|
||||
return await self._aget_json(file_path, extra_info=extra_info)
|
||||
elif isinstance(file_path, list):
|
||||
jobs = [self._aget_json(f, extra_info=extra_info) for f in file_path]
|
||||
@@ -931,7 +1446,7 @@ class LlamaParse(BasePydanticReader):
|
||||
raise e
|
||||
else:
|
||||
raise ValueError(
|
||||
"The input file_path must be a string or a list of strings."
|
||||
"The input file_path must be a string, Path, bytes, BufferedIOBase, or a list of these types."
|
||||
)
|
||||
|
||||
def get_json_result(
|
||||
@@ -948,6 +1463,14 @@ class LlamaParse(BasePydanticReader):
|
||||
else:
|
||||
raise e
|
||||
|
||||
def get_json(
|
||||
self,
|
||||
file_path: Union[List[FileInput], FileInput],
|
||||
extra_info: Optional[dict] = None,
|
||||
) -> List[dict]:
|
||||
"""Load data from the input path."""
|
||||
return self.get_json_result(file_path, extra_info)
|
||||
|
||||
async def aget_assets(
|
||||
self, json_result: List[dict], download_path: str, asset_key: str
|
||||
) -> List[dict]:
|
||||
@@ -986,7 +1509,9 @@ class LlamaParse(BasePydanticReader):
|
||||
|
||||
with open(asset_path, "wb") as f:
|
||||
asset_url = f"{self.base_url}/api/parsing/job/{job_id}/result/image/{asset_name}"
|
||||
resp = await client.get(asset_url)
|
||||
resp = await make_api_request(
|
||||
client, "GET", asset_url, timeout=self.max_timeout
|
||||
)
|
||||
resp.raise_for_status()
|
||||
f.write(resp.content)
|
||||
assets.append(asset)
|
||||
@@ -1071,7 +1596,9 @@ class LlamaParse(BasePydanticReader):
|
||||
xlsx_url = (
|
||||
f"{self.base_url}/api/parsing/job/{job_id}/result/raw/xlsx"
|
||||
)
|
||||
res = await client.get(xlsx_url)
|
||||
res = await make_api_request(
|
||||
client, "GET", xlsx_url, timeout=self.max_timeout
|
||||
)
|
||||
res.raise_for_status()
|
||||
f.write(res.content)
|
||||
xlsx_list.append(xlsx)
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
import httpx
|
||||
import os
|
||||
import re
|
||||
from pydantic import BaseModel, Field, SerializeAsAny
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from llama_cloud_services.parse.utils import make_api_request
|
||||
from llama_index.core.async_utils import asyncio_run
|
||||
from llama_index.core.schema import Document, ImageDocument, ImageNode, TextNode
|
||||
|
||||
PAGE_REGEX = r"page[-_](\d+)\.jpg$"
|
||||
|
||||
|
||||
class JobMetadata(BaseModel):
|
||||
"""Metadata about the job."""
|
||||
|
||||
job_pages: int = Field(description="The number of pages in the job.")
|
||||
job_auto_mode_triggered_pages: int = Field(
|
||||
description="The number of pages that triggered auto mode (thus increasing the cost)."
|
||||
)
|
||||
job_is_cache_hit: bool = Field(description="Whether the job was a cache hit.")
|
||||
|
||||
|
||||
class BBox(BaseModel):
|
||||
"""A bounding box."""
|
||||
|
||||
x: float = Field(description="The x-coordinate of the bounding box.")
|
||||
y: float = Field(description="The y-coordinate of the bounding box.")
|
||||
w: float = Field(description="The width of the bounding box.")
|
||||
h: float = Field(description="The height of the bounding box.")
|
||||
|
||||
|
||||
class PageItem(BaseModel):
|
||||
"""An item in a page."""
|
||||
|
||||
type: str = Field(description="The type of the item.")
|
||||
lvl: Optional[int] = Field(
|
||||
default=None, description="The level of indentation of the item."
|
||||
)
|
||||
value: Optional[str] = Field(
|
||||
default=None, description="The text content of the item."
|
||||
)
|
||||
md: Optional[str] = Field(
|
||||
default=None, description="The markdown-formatted content of the item."
|
||||
)
|
||||
rows: Optional[List[List[str]]] = Field(
|
||||
default=None, description="The rows of the item."
|
||||
)
|
||||
bBox: Optional[BBox] = Field(
|
||||
default=None, description="The bounding box of the item."
|
||||
)
|
||||
|
||||
|
||||
class ImageItem(BaseModel):
|
||||
"""An image in a page."""
|
||||
|
||||
name: str = Field(description="The name of the image.")
|
||||
height: Optional[float] = Field(
|
||||
default=None, description="The height of the image."
|
||||
)
|
||||
width: Optional[float] = Field(default=None, description="The width of the image.")
|
||||
x: Optional[float] = Field(
|
||||
default=None, description="The x-coordinate of the image."
|
||||
)
|
||||
y: Optional[float] = Field(
|
||||
default=None, description="The y-coordinate of the image."
|
||||
)
|
||||
original_width: Optional[int] = Field(
|
||||
default=None, description="The original width of the image."
|
||||
)
|
||||
original_height: Optional[int] = Field(
|
||||
default=None, description="The original height of the image."
|
||||
)
|
||||
type: Optional[str] = Field(default=None, description="The type of the image.")
|
||||
|
||||
|
||||
class LayoutItem(BaseModel):
|
||||
"""The layout of a page."""
|
||||
|
||||
image: str = Field(description="The name of the image containing the layout item")
|
||||
confidence: float = Field(description="The confidence of the layout item.")
|
||||
label: str = Field(description="The label of the layout item.")
|
||||
bbox: Optional[BBox] = Field(
|
||||
default=None, description="The bounding box of the layout item."
|
||||
)
|
||||
isLikelyNoise: bool = Field(description="Whether the layout item is likely noise.")
|
||||
|
||||
|
||||
class ChartItem(BaseModel):
|
||||
"""A chart in a page."""
|
||||
|
||||
name: str = Field(description="The name of the chart.")
|
||||
x: Optional[float] = Field(
|
||||
default=None, description="The x-coordinate of the chart."
|
||||
)
|
||||
y: Optional[float] = Field(
|
||||
default=None, description="The y-coordinate of the chart."
|
||||
)
|
||||
width: Optional[float] = Field(default=None, description="The width of the chart.")
|
||||
height: Optional[float] = Field(
|
||||
default=None, description="The height of the chart."
|
||||
)
|
||||
|
||||
|
||||
class Page(BaseModel):
|
||||
"""A page of the document."""
|
||||
|
||||
page: int = Field(description="The page number.")
|
||||
text: Optional[str] = Field(default=None, description="The text of the page.")
|
||||
md: Optional[str] = Field(default=None, description="The markdown of the page.")
|
||||
images: List[ImageItem] = Field(
|
||||
default_factory=list,
|
||||
description="The names of the image IDs in the page, including both objects and page screenshots.",
|
||||
)
|
||||
charts: List[ChartItem] = Field(
|
||||
default_factory=list, description="The charts in the page."
|
||||
)
|
||||
tables: List[str] = Field(
|
||||
default_factory=list, description="The names of the table IDs in the page."
|
||||
)
|
||||
layout: List[LayoutItem] = Field(
|
||||
default_factory=list, description="The layout of the page."
|
||||
)
|
||||
items: List[PageItem] = Field(
|
||||
default_factory=list, description="The items in the page."
|
||||
)
|
||||
status: Optional[str] = Field(default=None, description="The status of the page.")
|
||||
links: List[SerializeAsAny[Any]] = Field(
|
||||
default_factory=list, description="The links in the page."
|
||||
)
|
||||
width: Optional[float] = Field(default=None, description="The width of the page.")
|
||||
height: Optional[float] = Field(default=None, description="The height of the page.")
|
||||
triggeredAutoMode: bool = Field(
|
||||
default=False,
|
||||
description="Whether the page triggered auto mode (thus increasing the cost).",
|
||||
)
|
||||
parsingMode: str = Field(
|
||||
default="", description="The parsing mode used for the page."
|
||||
)
|
||||
structuredData: Optional[Dict[str, Any]] = Field(
|
||||
default=None, description="The structured data of the page."
|
||||
)
|
||||
noStructuredContent: bool = Field(
|
||||
default=True, description="Whether the page has no structured data."
|
||||
)
|
||||
noTextContent: bool = Field(
|
||||
default=False, description="Whether the page has no text content."
|
||||
)
|
||||
|
||||
|
||||
class JobResult(BaseModel):
|
||||
"""The raw JSON result from the LlamaParse API."""
|
||||
|
||||
pages: List[Page] = Field(description="The pages of the document.")
|
||||
job_metadata: JobMetadata = Field(description="The metadata of the job.")
|
||||
file_name: str = Field(description="The path to the file that was parsed.")
|
||||
job_id: str = Field(description="The ID of the job.")
|
||||
is_done: bool = Field(default=False, description="Whether the job is done.")
|
||||
error: Optional[str] = Field(
|
||||
default=None, description="The error message if the job failed."
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
job_id: str,
|
||||
file_name: str,
|
||||
job_result: Dict[str, Any],
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
client: Optional[httpx.AsyncClient] = None,
|
||||
page_separator: str = "\n\n",
|
||||
):
|
||||
"""
|
||||
Initialize JobResult with job_id and job_result.
|
||||
|
||||
Args:
|
||||
job_id: The job ID of the parsing task
|
||||
job_result: The JSON response from the parsing job or a JobResult instance (optional)
|
||||
api_key: The API key for the LlamaParse API
|
||||
base_url: The base URL of the Llama Parsing API
|
||||
page_separator: The separator that was used to define page splits in the result
|
||||
"""
|
||||
super().__init__(job_id=job_id, file_name=file_name, **job_result)
|
||||
|
||||
self._api_key = api_key or os.environ.get("LLAMA_CLOUD_API_KEY", "")
|
||||
self._base_url = base_url or os.environ.get(
|
||||
"LLAMA_CLOUD_BASE_URL", "https://api.llama-parse.ai"
|
||||
)
|
||||
self._client = client or httpx.AsyncClient()
|
||||
self._client.base_url = self._base_url
|
||||
self._client.headers["Authorization"] = f"Bearer {self._api_key}"
|
||||
self._page_separator = page_separator
|
||||
|
||||
def get_text_documents(self, split_by_page: bool = False) -> List[Document]:
|
||||
"""
|
||||
Get the documents from the job.
|
||||
|
||||
Args:
|
||||
split_by_page: Whether to split the pages into separate documents
|
||||
"""
|
||||
if split_by_page:
|
||||
return [
|
||||
Document(
|
||||
text=page.text,
|
||||
metadata={"page_number": page.page, "file_name": self.file_name},
|
||||
)
|
||||
for page in self.pages
|
||||
]
|
||||
else:
|
||||
text = self._page_separator.join(
|
||||
[page.text if page.text is not None else "" for page in self.pages]
|
||||
)
|
||||
return [Document(text=text, metadata={"file_name": self.file_name})]
|
||||
|
||||
async def aget_text_documents(self, split_by_page: bool = False) -> List[Document]:
|
||||
"""
|
||||
Get the documents from the job.
|
||||
|
||||
Args:
|
||||
split_by_page: Whether to split the pages into separate documents
|
||||
"""
|
||||
# No async needed, but here for consistency
|
||||
return self.get_text_documents(split_by_page)
|
||||
|
||||
def get_text_nodes(self, split_by_page: bool = False) -> List[TextNode]:
|
||||
"""
|
||||
Get the text nodes from the job.
|
||||
"""
|
||||
documents = self.get_text_documents(split_by_page)
|
||||
return [TextNode(text=doc.text, metadata=doc.metadata) for doc in documents]
|
||||
|
||||
async def aget_text_nodes(self, split_by_page: bool = False) -> List[TextNode]:
|
||||
"""
|
||||
Get the text nodes from the job.
|
||||
"""
|
||||
documents = await self.aget_text_documents(split_by_page)
|
||||
return [TextNode(text=doc.text, metadata=doc.metadata) for doc in documents]
|
||||
|
||||
def get_markdown_documents(self, split_by_page: bool = False) -> List[Document]:
|
||||
"""
|
||||
Get the markdown documents from the job.
|
||||
|
||||
Args:
|
||||
split_by_page: Whether to split the pages into separate documents
|
||||
"""
|
||||
if split_by_page:
|
||||
return [
|
||||
Document(
|
||||
text=page.md,
|
||||
metadata={"page_number": page.page, "file_name": self.file_name},
|
||||
)
|
||||
for page in self.pages
|
||||
]
|
||||
else:
|
||||
return [
|
||||
Document(
|
||||
text=self._page_separator.join(
|
||||
[page.md if page.md is not None else "" for page in self.pages]
|
||||
),
|
||||
metadata={"file_name": self.file_name},
|
||||
)
|
||||
]
|
||||
|
||||
async def aget_markdown_documents(
|
||||
self, split_by_page: bool = False
|
||||
) -> List[Document]:
|
||||
"""
|
||||
Get the markdown documents from the job.
|
||||
|
||||
Args:
|
||||
split_by_page: Whether to split the pages into separate documents
|
||||
"""
|
||||
# No async needed, but here for consistency
|
||||
return self.get_markdown_documents(split_by_page)
|
||||
|
||||
def get_markdown_nodes(self, split_by_page: bool = False) -> List[TextNode]:
|
||||
"""
|
||||
Get the markdown nodes from the job.
|
||||
|
||||
Args:
|
||||
split_by_page: Whether to split the pages into separate documents
|
||||
"""
|
||||
documents = self.get_markdown_documents(split_by_page)
|
||||
return [TextNode(text=doc.text, metadata=doc.metadata) for doc in documents]
|
||||
|
||||
async def aget_markdown_nodes(self, split_by_page: bool = False) -> List[TextNode]:
|
||||
"""
|
||||
Get the markdown nodes from the job.
|
||||
|
||||
Args:
|
||||
split_by_page: Whether to split the pages into separate documents
|
||||
"""
|
||||
documents = await self.aget_markdown_documents(split_by_page)
|
||||
return [TextNode(text=doc.text, metadata=doc.metadata) for doc in documents]
|
||||
|
||||
async def _get_image_document_with_bytes(
|
||||
self, image: ImageItem, page: Page
|
||||
) -> ImageDocument:
|
||||
image_data = await self.aget_image_data(image.name)
|
||||
|
||||
return ImageDocument(
|
||||
image=image_data,
|
||||
metadata={
|
||||
"page_number": page.page,
|
||||
"file_name": self.file_name,
|
||||
"width": image.original_width,
|
||||
"height": image.original_height,
|
||||
"x": image.x,
|
||||
"y": image.y,
|
||||
},
|
||||
excluded_embed_metadata_keys=["width", "height", "x", "y"],
|
||||
excluded_llm_metadata_keys=["width", "height", "x", "y"],
|
||||
)
|
||||
|
||||
async def _get_image_document_with_path(
|
||||
self, image: ImageItem, page: Page, image_download_dir: str
|
||||
) -> ImageDocument:
|
||||
image_path = await self.asave_image(image.name, image_download_dir)
|
||||
|
||||
return ImageDocument(
|
||||
image_path=image_path,
|
||||
metadata={
|
||||
"page_number": page.page,
|
||||
"file_name": self.file_name,
|
||||
"width": image.original_width,
|
||||
"height": image.original_height,
|
||||
"x": image.x,
|
||||
"y": image.y,
|
||||
},
|
||||
excluded_embed_metadata_keys=["width", "height", "x", "y"],
|
||||
excluded_llm_metadata_keys=["width", "height", "x", "y"],
|
||||
)
|
||||
|
||||
def get_image_documents(
|
||||
self,
|
||||
include_screenshot_images: bool = True,
|
||||
include_object_images: bool = True,
|
||||
image_download_dir: Optional[str] = None,
|
||||
) -> List[ImageDocument]:
|
||||
"""
|
||||
Get the image documents from the job.
|
||||
|
||||
Args:
|
||||
include_screenshot_images (bool):
|
||||
Whether to include screenshot images. Default is True.
|
||||
include_object_images (bool):
|
||||
Whether to include object images. Default is True.
|
||||
image_download_dir (Optional[str]):
|
||||
The directory to save the images to. If not provided, the images will be loaded into memory.
|
||||
Default is None.
|
||||
"""
|
||||
return asyncio_run(
|
||||
self.aget_image_documents(
|
||||
include_screenshot_images, include_object_images, image_download_dir
|
||||
)
|
||||
)
|
||||
|
||||
async def aget_image_documents(
|
||||
self,
|
||||
include_screenshot_images: bool = True,
|
||||
include_object_images: bool = True,
|
||||
image_download_dir: Optional[str] = None,
|
||||
) -> List[ImageDocument]:
|
||||
"""
|
||||
Get the image documents from the job.
|
||||
|
||||
Args:
|
||||
include_screenshot_images (bool):
|
||||
Whether to include screenshot images. Default is True.
|
||||
include_object_images (bool):
|
||||
Whether to include object images. Default is True.
|
||||
image_download_dir (Optional[str]):
|
||||
The directory to save the images to. If not provided, the images will be loaded into memory.
|
||||
Default is None.
|
||||
"""
|
||||
documents = []
|
||||
for page in self.pages:
|
||||
for image in page.images:
|
||||
is_screenshot = re.search(PAGE_REGEX, image.name) is not None
|
||||
|
||||
# Skip images that don't match the inclusion criteria
|
||||
if (is_screenshot and not include_screenshot_images) or (
|
||||
not is_screenshot and not include_object_images
|
||||
):
|
||||
continue
|
||||
|
||||
# Get image document using appropriate method based on download_dir
|
||||
get_document = (
|
||||
self._get_image_document_with_path
|
||||
if image_download_dir
|
||||
else self._get_image_document_with_bytes
|
||||
)
|
||||
|
||||
documents.append(
|
||||
await get_document(image, page, image_download_dir) # type: ignore
|
||||
if image_download_dir
|
||||
else await get_document(image, page) # type: ignore
|
||||
)
|
||||
|
||||
return documents
|
||||
|
||||
def get_image_nodes(
|
||||
self,
|
||||
include_screenshot_images: bool = True,
|
||||
include_object_images: bool = True,
|
||||
image_download_dir: Optional[str] = None,
|
||||
) -> List[ImageNode]:
|
||||
"""
|
||||
Get the image nodes from the job.
|
||||
|
||||
Args:
|
||||
include_screenshot_images (bool):
|
||||
Whether to include screenshot images. Default is True.
|
||||
include_object_images (bool):
|
||||
Whether to include object images. Default is True.
|
||||
image_download_dir (Optional[str]):
|
||||
The directory to save the images to. If not provided, the images will be loaded into memory.
|
||||
Default is None.
|
||||
"""
|
||||
documents = self.get_image_documents(
|
||||
include_screenshot_images, include_object_images, image_download_dir
|
||||
)
|
||||
return [
|
||||
ImageNode(
|
||||
image=doc.image,
|
||||
image_path=doc.image_path,
|
||||
image_url=doc.image_url,
|
||||
metadata=doc.metadata,
|
||||
)
|
||||
for doc in documents
|
||||
]
|
||||
|
||||
async def aget_image_nodes(
|
||||
self,
|
||||
include_screenshot_images: bool = True,
|
||||
include_object_images: bool = True,
|
||||
image_download_dir: Optional[str] = None,
|
||||
) -> List[ImageNode]:
|
||||
"""
|
||||
Get the image nodes from the job.
|
||||
|
||||
Args:
|
||||
include_screenshot_images (bool):
|
||||
Whether to include screenshot images. Default is True.
|
||||
include_object_images (bool):
|
||||
Whether to include object images. Default is True.
|
||||
image_download_dir (Optional[str]):
|
||||
The directory to save the images to. If not provided, the images will be loaded into memory.
|
||||
Default is None.
|
||||
"""
|
||||
documents = await self.aget_image_documents(
|
||||
include_screenshot_images, include_object_images, image_download_dir
|
||||
)
|
||||
return [
|
||||
ImageNode(
|
||||
image=doc.image,
|
||||
image_path=doc.image_path,
|
||||
image_url=doc.image_url,
|
||||
metadata=doc.metadata,
|
||||
)
|
||||
for doc in documents
|
||||
]
|
||||
|
||||
async def aget_image_data(self, image_name: str) -> bytes:
|
||||
"""
|
||||
Get image data by name using the job ID.
|
||||
|
||||
Args:
|
||||
image_name: The name of the image to fetch
|
||||
|
||||
Returns:
|
||||
The image data as bytes
|
||||
"""
|
||||
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/image/{image_name}"
|
||||
response = await make_api_request(self._client, "GET", url)
|
||||
return response.content
|
||||
|
||||
def get_image_data(self, image_name: str) -> bytes:
|
||||
"""
|
||||
Get image data by name using the job ID (synchronous version).
|
||||
|
||||
Args:
|
||||
image_name: The name of the image to fetch
|
||||
|
||||
Returns:
|
||||
The image data as bytes
|
||||
"""
|
||||
return asyncio_run(self.aget_image_data(image_name))
|
||||
|
||||
async def aget_xlsx_data(self) -> bytes:
|
||||
"""
|
||||
Get the XLSX data for the job.
|
||||
|
||||
Returns:
|
||||
The XLSX data as bytes
|
||||
"""
|
||||
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/xlsx"
|
||||
response = await make_api_request(self._client, "GET", url)
|
||||
return response.content
|
||||
|
||||
def get_xlsx_data(self) -> bytes:
|
||||
"""
|
||||
Get the XLSX data for the job (synchronous version).
|
||||
|
||||
Returns:
|
||||
The XLSX data as bytes
|
||||
"""
|
||||
return asyncio_run(self.aget_xlsx_data())
|
||||
|
||||
async def asave_image(self, image_name: str, output_dir: str) -> str:
|
||||
"""
|
||||
Save an image to a file.
|
||||
|
||||
Args:
|
||||
image_name: The name of the image to fetch
|
||||
output_dir: The directory to save the image to
|
||||
|
||||
Returns:
|
||||
The path to the saved image
|
||||
"""
|
||||
image_data = await self.aget_image_data(image_name)
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Save image to file
|
||||
output_path = os.path.join(output_dir, image_name)
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_data)
|
||||
|
||||
return output_path
|
||||
|
||||
def save_image(self, image_name: str, output_dir: str) -> str:
|
||||
"""
|
||||
Save an image to a file (synchronous version).
|
||||
|
||||
Args:
|
||||
image_name: The name of the image to fetch
|
||||
output_dir: The directory to save the image to
|
||||
|
||||
Returns:
|
||||
The path to the saved image
|
||||
"""
|
||||
return asyncio_run(self.asave_image(image_name, output_dir))
|
||||
|
||||
def get_image_names(self) -> List[str]:
|
||||
"""
|
||||
Get the names of all images in the job.
|
||||
|
||||
Returns:
|
||||
A list of image names
|
||||
"""
|
||||
return [image.name for page in self.pages for image in page.images]
|
||||
|
||||
async def asave_all_images(self, output_dir: str) -> List[str]:
|
||||
"""
|
||||
Save all images to files.
|
||||
|
||||
Args:
|
||||
output_dir: The directory to save the images to
|
||||
|
||||
Returns:
|
||||
A list of paths to the saved images
|
||||
"""
|
||||
image_names = self.get_image_names()
|
||||
saved_paths = []
|
||||
|
||||
for name in image_names:
|
||||
path = await self.asave_image(name, output_dir)
|
||||
saved_paths.append(path)
|
||||
|
||||
return saved_paths
|
||||
|
||||
def save_all_images(self, output_dir: str) -> List[str]:
|
||||
"""
|
||||
Save all images to files (synchronous version).
|
||||
|
||||
Args:
|
||||
output_dir: The directory to save the images to
|
||||
|
||||
Returns:
|
||||
A list of paths to the saved images
|
||||
"""
|
||||
return asyncio_run(self.asave_all_images(output_dir))
|
||||
@@ -1,4 +1,17 @@
|
||||
import httpx
|
||||
import itertools
|
||||
import logging
|
||||
from enum import Enum
|
||||
from tenacity import (
|
||||
retry,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
retry_if_exception,
|
||||
before_sleep_log,
|
||||
)
|
||||
from typing import Any, Iterable, Iterator, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Asyncio error messages
|
||||
nest_asyncio_err = "cannot be called from a running event loop"
|
||||
@@ -14,6 +27,27 @@ class ResultType(str, Enum):
|
||||
STRUCTURED = "structured"
|
||||
|
||||
|
||||
class ParsingMode(str, Enum):
|
||||
"""The parsing mode for the parser."""
|
||||
|
||||
parse_page_without_llm = "parse_page_without_llm"
|
||||
parse_page_with_llm = "parse_page_with_llm"
|
||||
parse_page_with_lvm = "parse_page_with_lvm"
|
||||
parse_page_with_agent = "parse_page_with_agent"
|
||||
parse_document_with_llm = "parse_document_with_llm"
|
||||
parse_document_with_agent = "parse_document_with_agent"
|
||||
|
||||
|
||||
class FailedPageMode(str, Enum):
|
||||
"""
|
||||
Enum for representing the different available page error handling modes
|
||||
"""
|
||||
|
||||
raw_text = "raw_text"
|
||||
blank_page = "blank_page"
|
||||
error_message = "error_message"
|
||||
|
||||
|
||||
class Language(str, Enum):
|
||||
BAZA = "abq"
|
||||
ADYGHE = "ady"
|
||||
@@ -199,3 +233,121 @@ SUPPORTED_FILE_TYPES = [
|
||||
".wav",
|
||||
".webm",
|
||||
]
|
||||
|
||||
|
||||
def should_retry(exception: Exception) -> bool:
|
||||
"""Check if the exception should be retried.
|
||||
|
||||
Args:
|
||||
exception: The exception to check.
|
||||
"""
|
||||
# Retry on connection errors (network issues)
|
||||
if isinstance(
|
||||
exception,
|
||||
(
|
||||
httpx.ConnectError,
|
||||
httpx.ConnectTimeout,
|
||||
httpx.ReadTimeout,
|
||||
httpx.WriteTimeout,
|
||||
httpx.RemoteProtocolError,
|
||||
),
|
||||
):
|
||||
return True
|
||||
|
||||
# Retry on specific HTTP status codes
|
||||
if isinstance(exception, httpx.HTTPStatusError):
|
||||
status_code = exception.response.status_code
|
||||
# Retry on rate limiting or temporary server errors
|
||||
return status_code in (429, 500, 502, 503, 504)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def make_api_request(
|
||||
client: httpx.AsyncClient,
|
||||
method: str,
|
||||
url: str,
|
||||
timeout: float = 60.0,
|
||||
max_retries: int = 5,
|
||||
**httpx_kwargs: Any,
|
||||
) -> httpx.Response:
|
||||
"""Make an retrying API request to the LlamaParse API.
|
||||
|
||||
Args:
|
||||
client: The httpx.AsyncClient to use for the request.
|
||||
url: The URL to request.
|
||||
headers: The headers to include in the request.
|
||||
timeout: The timeout for the request.
|
||||
max_retries: The maximum number of retries for the request.
|
||||
"""
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(max_retries),
|
||||
wait=wait_exponential(multiplier=1, min=4, max=timeout),
|
||||
retry=retry_if_exception(should_retry),
|
||||
before_sleep=before_sleep_log(logger, logging.WARNING),
|
||||
)
|
||||
async def _make_request(url: str, **httpx_kwargs: Any) -> httpx.Response:
|
||||
if method == "GET":
|
||||
response = await client.get(url, **httpx_kwargs)
|
||||
elif method == "POST":
|
||||
response = await client.post(url, **httpx_kwargs)
|
||||
else:
|
||||
raise ValueError(f"Invalid method: {method}")
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
return await _make_request(url, **httpx_kwargs)
|
||||
|
||||
|
||||
def expand_target_pages(target_pages: str) -> Iterator[int]:
|
||||
"""Yield all values in target_pages."""
|
||||
for target in target_pages.strip().split(","):
|
||||
if "-" in target:
|
||||
try:
|
||||
start, end = map(int, target.strip().split("-"))
|
||||
if start > end:
|
||||
raise ValueError
|
||||
yield from range(start, end + 1)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid page range: {target}") from e
|
||||
else:
|
||||
try:
|
||||
yield int(target)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid page number: {target}") from e
|
||||
|
||||
|
||||
def partition_pages(
|
||||
pages: Iterable[int], size: int, max_pages: Optional[int] = None
|
||||
) -> Iterator[str]:
|
||||
"""Yield partitioned target_pages segments."""
|
||||
if size < 1:
|
||||
raise ValueError(f"Invalid partition segment size: {size}")
|
||||
if max_pages is not None and max_pages < 1:
|
||||
raise ValueError("Max pages must be > 0")
|
||||
it = iter(pages)
|
||||
total = 0
|
||||
while max_pages is None or total < max_pages:
|
||||
segment = tuple(itertools.islice(it, size))
|
||||
if segment:
|
||||
targets = []
|
||||
for _k, g in itertools.groupby(enumerate(segment), lambda x: x[0] - x[1]):
|
||||
group = [item[1] for item in g]
|
||||
if len(group) > 1:
|
||||
start, end = group[0], group[-1]
|
||||
group_size = end - start + 1
|
||||
if max_pages is not None and total + group_size > max_pages:
|
||||
end -= total + group_size - max_pages
|
||||
group_size = end - start + 1
|
||||
if group_size > 1:
|
||||
targets.append(f"{start}-{end}")
|
||||
else:
|
||||
targets.append(str(start))
|
||||
total += group_size
|
||||
else:
|
||||
targets.append(str(group[0]))
|
||||
total += 1
|
||||
yield ",".join(targets)
|
||||
else:
|
||||
return
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import difflib
|
||||
from pydantic import BaseModel
|
||||
from typing import Any, Dict, List, Tuple, Type
|
||||
|
||||
|
||||
def check_extra_params(
|
||||
model_cls: Type[BaseModel], data: Dict[str, Any]
|
||||
) -> Tuple[List[str], List[str]]:
|
||||
# check if one of the parameters is unused, and warn the user
|
||||
model_attributes = set(model_cls.model_fields.keys())
|
||||
extra_params = [param for param in data.keys() if param not in model_attributes]
|
||||
|
||||
suggestions: List[str] = []
|
||||
if extra_params:
|
||||
# for each unused parameter, check if it is similar to a valid parameter and suggest a typo correction, else suggest to check the documentation / update the package
|
||||
for param in extra_params:
|
||||
similar_params = difflib.get_close_matches(
|
||||
param, model_attributes, n=1, cutoff=0.8
|
||||
)
|
||||
if similar_params:
|
||||
suggestions.append(
|
||||
f"'{param}' is not a valid parameter. Did you mean '{similar_params[0]}' instead of '{param}'?"
|
||||
)
|
||||
else:
|
||||
suggestions.append(
|
||||
f"'{param}' is not a valid parameter. Please check the documentation or update the package."
|
||||
)
|
||||
|
||||
return extra_params, suggestions
|
||||
@@ -1,3 +1,8 @@
|
||||
from llama_cloud_services.parse import LlamaParse, ResultType
|
||||
from llama_cloud_services.parse import (
|
||||
LlamaParse,
|
||||
ResultType,
|
||||
ParsingMode,
|
||||
FailedPageMode,
|
||||
)
|
||||
|
||||
__all__ = ["LlamaParse", "ResultType"]
|
||||
__all__ = ["LlamaParse", "ResultType", "ParsingMode", "FailedPageMode"]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from llama_cloud_services.parse.base import (
|
||||
LlamaParse,
|
||||
ResultType,
|
||||
ParsingMode,
|
||||
FailedPageMode,
|
||||
FileInput,
|
||||
_DEFAULT_SEPARATOR,
|
||||
JOB_RESULT_URL,
|
||||
@@ -12,6 +14,8 @@ __all__ = [
|
||||
"LlamaParse",
|
||||
"ResultType",
|
||||
"FileInput",
|
||||
"ParsingMode",
|
||||
"FailedPageMode",
|
||||
"_DEFAULT_SEPARATOR",
|
||||
"JOB_RESULT_URL",
|
||||
"JOB_STATUS_ROUTE",
|
||||
|
||||
@@ -2,10 +2,14 @@ from llama_cloud_services.parse.utils import (
|
||||
SUPPORTED_FILE_TYPES,
|
||||
Language,
|
||||
ResultType,
|
||||
ParsingMode,
|
||||
FailedPageMode,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"SUPPORTED_FILE_TYPES",
|
||||
"Language",
|
||||
"ResultType",
|
||||
"ParsingMode",
|
||||
"FailedPageMode",
|
||||
]
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "llama-parse"
|
||||
version = "0.6.1"
|
||||
version = "0.6.37"
|
||||
description = "Parse files into RAG-Optimized formats."
|
||||
authors = ["Logan Markewich <logan@llamaindex.ai>"]
|
||||
license = "MIT"
|
||||
@@ -13,7 +13,7 @@ packages = [{include = "llama_parse"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<4.0"
|
||||
llama-cloud-services = ">=0.6.1"
|
||||
llama-cloud-services = ">=0.6.37"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.0.0"
|
||||
|
||||
@@ -25,6 +25,8 @@ Then, install the package:
|
||||
|
||||
`pip install llama-cloud-services`
|
||||
|
||||
## CLI Usage
|
||||
|
||||
Now you can parse your first PDF file using the command line interface. Use the command `llama-parse [file_paths]`. See the help text with `llama-parse --help`.
|
||||
|
||||
```bash
|
||||
@@ -40,50 +42,72 @@ llama-parse my_file.pdf --result-type markdown --output-file output.md
|
||||
llama-parse my_file.pdf --output-raw-json --output-file output.json
|
||||
```
|
||||
|
||||
## Python Usage
|
||||
|
||||
You can also create simple scripts:
|
||||
|
||||
```python
|
||||
import nest_asyncio
|
||||
|
||||
nest_asyncio.apply()
|
||||
|
||||
from llama_cloud_services import LlamaParse
|
||||
|
||||
parser = LlamaParse(
|
||||
api_key="llx-...", # can also be set in your env as LLAMA_CLOUD_API_KEY
|
||||
result_type="markdown", # "markdown" and "text" are available
|
||||
num_workers=4, # if multiple files passed, split in `num_workers` API calls
|
||||
verbose=True,
|
||||
language="en", # Optionally you can define a language, default=en
|
||||
)
|
||||
|
||||
# sync
|
||||
documents = parser.load_data("./my_file.pdf")
|
||||
result = parser.parse("./my_file.pdf")
|
||||
|
||||
# sync batch
|
||||
documents = parser.load_data(["./my_file1.pdf", "./my_file2.pdf"])
|
||||
results = parser.parse(["./my_file1.pdf", "./my_file2.pdf"])
|
||||
|
||||
# async
|
||||
documents = await parser.aload_data("./my_file.pdf")
|
||||
result = await parser.aparse("./my_file.pdf")
|
||||
|
||||
# async batch
|
||||
documents = await parser.aload_data(["./my_file1.pdf", "./my_file2.pdf"])
|
||||
results = await parser.aparse(["./my_file1.pdf", "./my_file2.pdf"])
|
||||
```
|
||||
|
||||
## Using with file object
|
||||
The result object is a fully typed `JobResult` object, and you can interact with it to parse and transform various parts of the result:
|
||||
|
||||
```python
|
||||
# get the llama-index markdown documents
|
||||
markdown_documents = result.get_markdown_documents(split_by_page=True)
|
||||
|
||||
# get the llama-index text documents
|
||||
text_documents = result.get_text_documents(split_by_page=False)
|
||||
|
||||
# get the image documents
|
||||
image_documents = result.get_image_documents(
|
||||
include_screenshot_images=True,
|
||||
include_object_images=False,
|
||||
# Optional: download the images to a directory
|
||||
# (default is to return the image bytes in ImageDocument objects)
|
||||
image_download_dir="./images",
|
||||
)
|
||||
|
||||
# access the raw job result
|
||||
# Items will vary based on the parser configuration
|
||||
for page in result.pages:
|
||||
print(page.text)
|
||||
print(page.md)
|
||||
print(page.images)
|
||||
print(page.layout)
|
||||
print(page.structuredData)
|
||||
```
|
||||
|
||||
See more details about the result object in the [example notebook](./examples/parse/demo_json_tour.ipynb).
|
||||
|
||||
### Using with file object / bytes
|
||||
|
||||
You can parse a file object directly:
|
||||
|
||||
```python
|
||||
import nest_asyncio
|
||||
|
||||
nest_asyncio.apply()
|
||||
|
||||
from llama_cloud_services import LlamaParse
|
||||
|
||||
parser = LlamaParse(
|
||||
api_key="llx-...", # can also be set in your env as LLAMA_CLOUD_API_KEY
|
||||
result_type="markdown", # "markdown" and "text" are available
|
||||
num_workers=4, # if multiple files passed, split in `num_workers` API calls
|
||||
verbose=True,
|
||||
language="en", # Optionally you can define a language, default=en
|
||||
@@ -94,24 +118,20 @@ extra_info = {"file_name": file_name}
|
||||
|
||||
with open(f"./{file_name}", "rb") as f:
|
||||
# must provide extra_info with file_name key with passing file object
|
||||
documents = parser.load_data(f, extra_info=extra_info)
|
||||
result = parser.parse(f, extra_info=extra_info)
|
||||
|
||||
# you can also pass file bytes directly
|
||||
with open(f"./{file_name}", "rb") as f:
|
||||
file_bytes = f.read()
|
||||
# must provide extra_info with file_name key with passing file bytes
|
||||
documents = parser.load_data(file_bytes, extra_info=extra_info)
|
||||
result = parser.parse(file_bytes, extra_info=extra_info)
|
||||
```
|
||||
|
||||
## Using with `SimpleDirectoryReader`
|
||||
### Using with `SimpleDirectoryReader`
|
||||
|
||||
You can also integrate the parser as the default PDF loader in `SimpleDirectoryReader`:
|
||||
|
||||
```python
|
||||
import nest_asyncio
|
||||
|
||||
nest_asyncio.apply()
|
||||
|
||||
from llama_cloud_services import LlamaParse
|
||||
from llama_index.core import SimpleDirectoryReader
|
||||
|
||||
@@ -136,6 +156,7 @@ Several end-to-end indexing examples can be found in the examples folder
|
||||
- [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
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ python_version = "3.10"
|
||||
|
||||
[tool.poetry]
|
||||
name = "llama-cloud-services"
|
||||
version = "0.6.1"
|
||||
version = "0.6.37"
|
||||
description = "Tailored SDK clients for LlamaCloud services."
|
||||
authors = ["Logan Markewich <logan@runllama.ai>"]
|
||||
license = "MIT"
|
||||
@@ -17,12 +17,14 @@ packages = [{include = "llama_cloud_services"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<4.0"
|
||||
llama-index-core = ">=0.11.0"
|
||||
llama-cloud = "^0.1.11"
|
||||
pydantic = "!=2.10"
|
||||
llama-index-core = ">=0.12.0"
|
||||
llama-cloud = "==0.1.29"
|
||||
pydantic = ">=2.8,!=2.10"
|
||||
click = "^8.1.7"
|
||||
python-dotenv = "^1.0.1"
|
||||
eval-type-backport = {python = "<3.10", version = "^0.2.0"}
|
||||
platformdirs = "^4.3.7"
|
||||
tenacity = ">=8.5.0, <10.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.0.0"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
from typing import List
|
||||
from llama_cloud_services.extract import LlamaExtract
|
||||
|
||||
# Global storage for agents to cleanup
|
||||
_TEST_AGENTS_TO_CLEANUP: List[str] = []
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
"""Hook that runs after all tests complete - cleanup agents here"""
|
||||
print(
|
||||
f"pytest_sessionfinish hook called! Agents to cleanup: {_TEST_AGENTS_TO_CLEANUP}"
|
||||
)
|
||||
|
||||
if _TEST_AGENTS_TO_CLEANUP:
|
||||
print("Creating cleanup client...")
|
||||
# Create a fresh client just for cleanup
|
||||
cleanup_client = LlamaExtract(
|
||||
api_key=os.getenv("LLAMA_CLOUD_API_KEY"),
|
||||
base_url=os.getenv("LLAMA_CLOUD_BASE_URL"),
|
||||
project_id=os.getenv("LLAMA_CLOUD_PROJECT_ID"),
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
for agent_id in _TEST_AGENTS_TO_CLEANUP:
|
||||
try:
|
||||
print(f"Deleting agent {agent_id}...")
|
||||
cleanup_client.delete_agent(agent_id)
|
||||
print(f"Cleaned up agent {agent_id}")
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to delete agent {agent_id}: {e}")
|
||||
|
||||
_TEST_AGENTS_TO_CLEANUP.clear()
|
||||
print("Agent cleanup completed")
|
||||
else:
|
||||
print("No agents to cleanup")
|
||||
|
||||
|
||||
def register_agent_for_cleanup(agent_id: str):
|
||||
"""Register an agent ID for cleanup at the end of the test session"""
|
||||
_TEST_AGENTS_TO_CLEANUP.append(agent_id)
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"receiptNumber": "27215058",
|
||||
"invoiceNumber": "87B37C90152",
|
||||
"datePaid": "2024-07-19",
|
||||
"paymentMethod": {
|
||||
"type": "visa",
|
||||
"lastFourDigits": "7267"
|
||||
},
|
||||
"merchant": {
|
||||
"name": "Noisebridge",
|
||||
"address": {
|
||||
"street": "272 Capp St",
|
||||
"city": "San Francisco",
|
||||
"state": "California",
|
||||
"postalCode": "94110",
|
||||
"country": "United States"
|
||||
},
|
||||
"phone": "1 6507017829",
|
||||
"email": "treasurer+stripe@noisebridge.net"
|
||||
},
|
||||
"billTo": "noisebridge@seldo.com",
|
||||
"items": [
|
||||
{
|
||||
"description": "$10 / month",
|
||||
"quantity": 1,
|
||||
"unitPrice": 10.0,
|
||||
"amount": 10.0,
|
||||
"period": {
|
||||
"start": "2024-07-19",
|
||||
"end": "2024-08-19"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subtotal": 10.0,
|
||||
"total": 10.0,
|
||||
"amountPaid": 10.0
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["receiptNumber", "datePaid", "total", "items"],
|
||||
"properties": {
|
||||
"receiptNumber": {
|
||||
"type": "string"
|
||||
},
|
||||
"invoiceNumber": {
|
||||
"type": "string"
|
||||
},
|
||||
"datePaid": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"paymentMethod": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["visa", "mastercard", "amex", "cash", "other"]
|
||||
},
|
||||
"lastFourDigits": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{4}$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"merchant": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"address": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"street": {
|
||||
"type": "string"
|
||||
},
|
||||
"city": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string"
|
||||
},
|
||||
"postalCode": {
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
}
|
||||
}
|
||||
},
|
||||
"billTo": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"quantity",
|
||||
"unitPrice",
|
||||
"amount",
|
||||
"period"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"unitPrice": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"period": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"subtotal": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"total": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"amountPaid": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Resume Schema",
|
||||
"type": "object",
|
||||
"required": ["basics", "skills", "experience"],
|
||||
"properties": {
|
||||
"basics": {
|
||||
"type": "object",
|
||||
"required": ["name", "email"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"location": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string"
|
||||
},
|
||||
"region": {
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"network": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"keywords": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"type": "string",
|
||||
"enum": ["beginner", "intermediate", "advanced", "expert"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"experience": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["company", "position", "startDate"],
|
||||
"properties": {
|
||||
"company": {
|
||||
"type": "string"
|
||||
},
|
||||
"position": {
|
||||
"type": "string"
|
||||
},
|
||||
"startDate": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"endDate": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"highlights": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"technologies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"education": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["institution", "degree"],
|
||||
"properties": {
|
||||
"institution": {
|
||||
"type": "string"
|
||||
},
|
||||
"degree": {
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"graduationDate": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"gpa": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"certifications": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"issuer": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"validUntil": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"publications": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"publisher": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 2rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: 2.5rem;
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
border-bottom: 3px solid #3498db;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.profile-title {
|
||||
font-size: 1.5rem;
|
||||
color: #7f8c8d;
|
||||
margin: 0.5rem 0 2rem 0;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
color: #3498db;
|
||||
margin-bottom: 1rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.sidebar .section-title {
|
||||
color: white;
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.skill-category {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.skill-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.skill-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.experience-item {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.job-title {
|
||||
color: #3498db;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.achievements {
|
||||
list-style: disc;
|
||||
padding-left: 1.2rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.contact-info a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.education-item {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="sidebar">
|
||||
<div class="contact-info">
|
||||
<h2 class="section-title">Contact</h2>
|
||||
<p>sarah.chen@email.com</p>
|
||||
<p>(555) 123-4567</p>
|
||||
<p>San Francisco, CA</p>
|
||||
<p><a href="#">LinkedIn Profile</a></p>
|
||||
</div>
|
||||
|
||||
<div class="skills-section">
|
||||
<h2 class="section-title">Technical Skills</h2>
|
||||
|
||||
<div class="skill-category">
|
||||
<h3>Architecture & Design</h3>
|
||||
<ul class="skill-list">
|
||||
<li>Microservices</li>
|
||||
<li>Event-Driven Architecture</li>
|
||||
<li>Domain-Driven Design</li>
|
||||
<li>REST APIs</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="skill-category">
|
||||
<h3>Cloud Platforms</h3>
|
||||
<ul class="skill-list">
|
||||
<li>AWS (Advanced)</li>
|
||||
<li>Azure</li>
|
||||
<li>Google Cloud Platform</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="skill-category">
|
||||
<h3>Programming</h3>
|
||||
<ul class="skill-list">
|
||||
<li>Java</li>
|
||||
<li>Python</li>
|
||||
<li>Go</li>
|
||||
<li>JavaScript/TypeScript</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="skill-category">
|
||||
<h3>Certifications</h3>
|
||||
<ul class="skill-list">
|
||||
<li>AWS Solutions Architect - Professional</li>
|
||||
<li>Google Cloud Architect</li>
|
||||
<li>Certified Kubernetes Administrator</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<h1 class="profile-name">Sarah Chen</h1>
|
||||
<div class="profile-title">Senior Software Architect</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Professional Summary</h2>
|
||||
<p>
|
||||
Innovative Software Architect with over 12 years of experience
|
||||
designing and implementing large-scale distributed systems. Proven
|
||||
track record of leading technical teams and delivering robust
|
||||
enterprise solutions. Expert in cloud architecture, microservices,
|
||||
and emerging technologies with a focus on scalable, maintainable
|
||||
systems.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Professional Experience</h2>
|
||||
|
||||
<div class="experience-item">
|
||||
<div class="company-name">TechCorp Solutions</div>
|
||||
<div class="job-title">Senior Software Architect</div>
|
||||
<div class="date">2020 - Present</div>
|
||||
<ul class="achievements">
|
||||
<li>
|
||||
Led architectural design and implementation of a cloud-native
|
||||
platform serving 2M+ users
|
||||
</li>
|
||||
<li>
|
||||
Established architectural guidelines and best practices adopted
|
||||
across 12 development teams
|
||||
</li>
|
||||
<li>
|
||||
Reduced system latency by 40% through implementation of
|
||||
event-driven architecture
|
||||
</li>
|
||||
<li>
|
||||
Mentored 15+ senior developers in cloud-native development
|
||||
practices
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="experience-item">
|
||||
<div class="company-name">DataFlow Systems</div>
|
||||
<div class="job-title">Lead Software Engineer</div>
|
||||
<div class="date">2016 - 2020</div>
|
||||
<ul class="achievements">
|
||||
<li>
|
||||
Architected and led development of distributed data processing
|
||||
platform handling 5TB daily
|
||||
</li>
|
||||
<li>
|
||||
Designed microservices architecture reducing deployment time by
|
||||
65%
|
||||
</li>
|
||||
<li>
|
||||
Led migration of legacy monolith to cloud-native architecture
|
||||
</li>
|
||||
<li>
|
||||
Managed team of 8 engineers across 3 international locations
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="experience-item">
|
||||
<div class="company-name">InnovateTech</div>
|
||||
<div class="job-title">Senior Software Engineer</div>
|
||||
<div class="date">2013 - 2016</div>
|
||||
<ul class="achievements">
|
||||
<li>
|
||||
Developed high-performance trading platform processing 100K
|
||||
transactions per second
|
||||
</li>
|
||||
<li>
|
||||
Implemented real-time analytics engine reducing processing
|
||||
latency by 75%
|
||||
</li>
|
||||
<li>
|
||||
Led adoption of container orchestration reducing deployment
|
||||
costs by 35%
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Education</h2>
|
||||
|
||||
<div class="education-item">
|
||||
<div class="company-name">Stanford University</div>
|
||||
<div class="job-title">Master of Science in Computer Science</div>
|
||||
<div class="date">2013</div>
|
||||
<p>Focus: Distributed Systems and Machine Learning</p>
|
||||
</div>
|
||||
|
||||
<div class="education-item">
|
||||
<div class="company-name">University of California, Berkeley</div>
|
||||
<div class="job-title">
|
||||
Bachelor of Science in Computer Engineering
|
||||
</div>
|
||||
<div class="date">2011</div>
|
||||
<p>Magna Cum Laude</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">Patents & Speaking</h2>
|
||||
<ul class="achievements">
|
||||
<li>
|
||||
Co-inventor on three patents for distributed systems architecture
|
||||
</li>
|
||||
<li>
|
||||
Published paper on "Scalable Microservices Architecture" at IEEE
|
||||
Cloud Computing Conference 2022
|
||||
</li>
|
||||
<li>
|
||||
Keynote Speaker, CloudCon 2023: "Future of Cloud-Native
|
||||
Architecture"
|
||||
</li>
|
||||
<li>Regular presenter at local tech meetups and conferences</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"basics": {
|
||||
"name": "Sarah Chen",
|
||||
"email": "san.francisco@email.com",
|
||||
"phone": "(555) 123-4567",
|
||||
"location": {
|
||||
"city": "San Francisco",
|
||||
"region": "CA",
|
||||
"country": "USA"
|
||||
}
|
||||
},
|
||||
"skills": [
|
||||
{
|
||||
"category": "Architecture & Design",
|
||||
"keywords": [
|
||||
"Microservices",
|
||||
"Event-Driven Architecture",
|
||||
"Domain-Driven Design",
|
||||
"REST APIs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"category": "Cloud Platforms",
|
||||
"keywords": ["AWS", "Azure", "Google Cloud Platform"]
|
||||
},
|
||||
{
|
||||
"category": "Programming Languages",
|
||||
"keywords": ["Java", "Python", "Go", "JavaScript", "TypeScript"]
|
||||
}
|
||||
],
|
||||
"experience": [
|
||||
{
|
||||
"company": "TechCorp Solutions",
|
||||
"position": "Senior Software Architect",
|
||||
"startDate": "2020-01-01",
|
||||
"endDate": "2024-01-10"
|
||||
},
|
||||
{
|
||||
"company": "DataFlow Systems",
|
||||
"position": "Lead Software Engineer",
|
||||
"startDate": "2016-01-01",
|
||||
"endDate": "2019-12-31",
|
||||
"technologies": [
|
||||
"Distributed Systems",
|
||||
"Microservices",
|
||||
"Cloud Migration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"company": "InnovateTech",
|
||||
"position": "Senior Software Engineer",
|
||||
"startDate": "2013-01-01",
|
||||
"endDate": "2015-12-31",
|
||||
"technologies": [
|
||||
"High-performance Computing",
|
||||
"Real-time Analytics",
|
||||
"Container Orchestration"
|
||||
]
|
||||
}
|
||||
],
|
||||
"education": [
|
||||
{
|
||||
"institution": "Stanford University",
|
||||
"degree": "Master of Science",
|
||||
"field": "Computer Science",
|
||||
"graduationDate": "2013-01-01",
|
||||
"specialization": "Distributed Systems and Machine Learning"
|
||||
},
|
||||
{
|
||||
"institution": "University of California, Berkeley",
|
||||
"degree": "Bachelor of Science",
|
||||
"field": "Computer Engineering",
|
||||
"graduationDate": "2011-01-01"
|
||||
}
|
||||
],
|
||||
"certifications": [
|
||||
{
|
||||
"name": "AWS Solutions Architect - Professional"
|
||||
},
|
||||
{
|
||||
"name": "Google Cloud Architect"
|
||||
},
|
||||
{
|
||||
"name": "Certified Kubernetes Administrator"
|
||||
}
|
||||
],
|
||||
"publications": [
|
||||
{
|
||||
"title": "Scalable Microservices Architecture",
|
||||
"publisher": "IEEE Cloud Computing Conference",
|
||||
"date": "2022-01-01"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"companyInfo": {
|
||||
"name": "CloudFlow Analytics",
|
||||
"fundingStage": "Series A",
|
||||
"foundedYear": null,
|
||||
"industry": null,
|
||||
"location": null
|
||||
},
|
||||
"financialMetrics": {
|
||||
"mrr": {
|
||||
"value": 580000,
|
||||
"currency": "USD",
|
||||
"growthRate": 27
|
||||
},
|
||||
"grossMargin": 88
|
||||
},
|
||||
"growthMetrics": {
|
||||
"customers": {
|
||||
"total": 1247,
|
||||
"growth": 142,
|
||||
"enterprisePercent": null
|
||||
},
|
||||
"nrr": 147
|
||||
},
|
||||
"marketMetrics": {
|
||||
"tam": 50000000000,
|
||||
"sam": null,
|
||||
"marketShare": null,
|
||||
"competitors": null
|
||||
},
|
||||
"differentiators": [
|
||||
{
|
||||
"claim": "Processing Speed",
|
||||
"metric": "5x faster",
|
||||
"comparisonTarget": "competitors"
|
||||
},
|
||||
{
|
||||
"claim": "ML Accuracy",
|
||||
"metric": "99.9%",
|
||||
"comparisonTarget": null
|
||||
},
|
||||
{
|
||||
"claim": "Market Potential",
|
||||
"metric": "80%",
|
||||
"comparisonTarget": "Fortune 500"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["companyInfo", "financialMetrics", "growthMetrics"],
|
||||
"properties": {
|
||||
"companyInfo": {
|
||||
"type": "object",
|
||||
"required": ["name", "fundingStage"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"fundingStage": {
|
||||
"type": "string",
|
||||
"enum": ["Pre-seed", "Seed", "Series A", "Series B", "Series C+"]
|
||||
},
|
||||
"foundedYear": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"industry": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"location": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"financialMetrics": {
|
||||
"type": "object",
|
||||
"required": ["mrr", "growthRate"],
|
||||
"properties": {
|
||||
"mrr": {
|
||||
"type": "object",
|
||||
"description": "Monthly Recurring Revenue",
|
||||
"required": ["value", "currency", "growthRate"],
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "number"
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"growthRate": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grossMargin": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"growthMetrics": {
|
||||
"type": "object",
|
||||
"required": ["customers", "nrr"],
|
||||
"properties": {
|
||||
"customers": {
|
||||
"type": "object",
|
||||
"required": ["total", "growth"],
|
||||
"properties": {
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"growth": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nrr": {
|
||||
"description": "Net Revenue Retention",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"differentiators": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["claim", "metric"],
|
||||
"properties": {
|
||||
"claim": {
|
||||
"type": "string"
|
||||
},
|
||||
"metric": {
|
||||
"type": "string"
|
||||
},
|
||||
"comparisonTarget": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent
|
||||
from time import perf_counter
|
||||
from collections import namedtuple
|
||||
import json
|
||||
import uuid
|
||||
from llama_cloud.types import (
|
||||
ExtractConfig,
|
||||
ExtractMode,
|
||||
LlamaParseParameters,
|
||||
LlamaExtractSettings,
|
||||
)
|
||||
from tests.extract.util import load_test_dotenv
|
||||
|
||||
|
||||
load_test_dotenv()
|
||||
|
||||
TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
|
||||
# Get configuration from environment
|
||||
LLAMA_CLOUD_API_KEY = os.getenv("LLAMA_CLOUD_API_KEY")
|
||||
LLAMA_CLOUD_BASE_URL = os.getenv("LLAMA_CLOUD_BASE_URL")
|
||||
LLAMA_CLOUD_PROJECT_ID = os.getenv("LLAMA_CLOUD_PROJECT_ID")
|
||||
|
||||
TestCase = namedtuple(
|
||||
"TestCase", ["name", "schema_path", "config", "input_file", "expected_output"]
|
||||
)
|
||||
|
||||
|
||||
def get_test_cases():
|
||||
"""Get all test cases from TEST_DIR.
|
||||
|
||||
Returns:
|
||||
List[TestCase]: List of test cases
|
||||
"""
|
||||
test_cases = []
|
||||
|
||||
for data_type in os.listdir(TEST_DIR):
|
||||
data_type_dir = os.path.join(TEST_DIR, data_type)
|
||||
if not os.path.isdir(data_type_dir):
|
||||
continue
|
||||
|
||||
schema_path = os.path.join(data_type_dir, "schema.json")
|
||||
if not os.path.exists(schema_path):
|
||||
continue
|
||||
|
||||
input_files = []
|
||||
|
||||
for file in os.listdir(data_type_dir):
|
||||
file_path = os.path.join(data_type_dir, file)
|
||||
if (
|
||||
not os.path.isfile(file_path)
|
||||
or file == "schema.json"
|
||||
or file.endswith(".test.json")
|
||||
):
|
||||
continue
|
||||
|
||||
input_files.append(file_path)
|
||||
|
||||
settings = [
|
||||
ExtractConfig(extraction_mode=ExtractMode.FAST),
|
||||
ExtractConfig(extraction_mode=ExtractMode.BALANCED),
|
||||
]
|
||||
|
||||
for input_file in sorted(input_files):
|
||||
base_name = os.path.splitext(os.path.basename(input_file))[0]
|
||||
expected_output = os.path.join(data_type_dir, f"{base_name}.test.json")
|
||||
|
||||
if not os.path.exists(expected_output):
|
||||
continue
|
||||
|
||||
test_name = f"{data_type}/{os.path.basename(input_file)}"
|
||||
for setting in settings:
|
||||
test_cases.append(
|
||||
TestCase(
|
||||
name=test_name,
|
||||
schema_path=schema_path,
|
||||
input_file=input_file,
|
||||
config=setting,
|
||||
expected_output=expected_output,
|
||||
)
|
||||
)
|
||||
|
||||
return test_cases
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def extractor():
|
||||
"""Create a single LlamaExtract instance for all tests."""
|
||||
extract = LlamaExtract(
|
||||
api_key=LLAMA_CLOUD_API_KEY,
|
||||
base_url=LLAMA_CLOUD_BASE_URL,
|
||||
project_id=LLAMA_CLOUD_PROJECT_ID,
|
||||
verbose=True,
|
||||
)
|
||||
yield extract
|
||||
# Cleanup thread pool at end of session
|
||||
extract._thread_pool.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def extraction_agent(test_case: TestCase, extractor: LlamaExtract):
|
||||
"""Fixture to create and cleanup extraction agent for each test."""
|
||||
# Create unique name with random UUID (important for CI to avoid conflicts)
|
||||
unique_id = uuid.uuid4().hex[:8]
|
||||
agent_name = f"{test_case.name}_{unique_id}"
|
||||
|
||||
with open(test_case.schema_path, "r") as f:
|
||||
schema = json.load(f)
|
||||
|
||||
# Clean up any existing agents with this name
|
||||
try:
|
||||
agents = extractor.list_agents()
|
||||
for agent in agents:
|
||||
if agent.name == agent_name:
|
||||
extractor.delete_agent(agent.id)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to cleanup existing agent: {str(e)}")
|
||||
|
||||
# Create new agent
|
||||
agent = extractor.create_agent(agent_name, schema, config=test_case.config)
|
||||
yield agent
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"CI" in os.environ,
|
||||
reason="CI environment is not suitable for benchmarking",
|
||||
)
|
||||
@pytest.mark.parametrize("test_case", get_test_cases(), ids=lambda x: x.name)
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_extraction(
|
||||
test_case: TestCase, extraction_agent: ExtractionAgent
|
||||
) -> None:
|
||||
start = perf_counter()
|
||||
result = await extraction_agent._run_extraction_test(
|
||||
test_case.input_file,
|
||||
extract_settings=LlamaExtractSettings(
|
||||
llama_parse_params=LlamaParseParameters(
|
||||
invalidate_cache=True,
|
||||
do_not_cache=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
end = perf_counter()
|
||||
print(f"Time taken: {end - start} seconds")
|
||||
print(result)
|
||||
@@ -0,0 +1,228 @@
|
||||
import os
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel
|
||||
|
||||
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent, SourceText
|
||||
from tests.extract.util import load_test_dotenv
|
||||
from .conftest import register_agent_for_cleanup
|
||||
|
||||
load_test_dotenv()
|
||||
|
||||
# Get configuration from environment
|
||||
LLAMA_CLOUD_API_KEY = os.getenv("LLAMA_CLOUD_API_KEY")
|
||||
LLAMA_CLOUD_BASE_URL = os.getenv("LLAMA_CLOUD_BASE_URL")
|
||||
LLAMA_CLOUD_PROJECT_ID = os.getenv("LLAMA_CLOUD_PROJECT_ID")
|
||||
|
||||
# Skip all tests if API key is not set
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not LLAMA_CLOUD_API_KEY, reason="LLAMA_CLOUD_API_KEY not set"
|
||||
)
|
||||
|
||||
|
||||
# Test data
|
||||
class TestSchema(BaseModel):
|
||||
title: str
|
||||
summary: str
|
||||
|
||||
|
||||
# Test data paths
|
||||
TEST_DIR = Path(__file__).parent / "data"
|
||||
TEST_PDF = TEST_DIR / "api_test" / "noisebridge_receipt.pdf"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def llama_extract():
|
||||
return LlamaExtract(
|
||||
api_key=LLAMA_CLOUD_API_KEY,
|
||||
base_url=LLAMA_CLOUD_BASE_URL,
|
||||
project_id=LLAMA_CLOUD_PROJECT_ID,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_agent_name():
|
||||
return "test-api-agent"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_schema_dict():
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"summary": {"type": "string"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_agent(llama_extract, test_agent_name, test_schema_dict, request):
|
||||
"""Creates a test agent and collects it for cleanup at the end of all tests"""
|
||||
test_id = request.node.nodeid
|
||||
test_hash = hex(hash(test_id))[-8:]
|
||||
base_name = test_agent_name
|
||||
|
||||
base_name = next(
|
||||
(marker.args[0] for marker in request.node.iter_markers("agent_name")),
|
||||
base_name,
|
||||
)
|
||||
name = f"{base_name}_{test_hash}"
|
||||
|
||||
schema = next(
|
||||
(
|
||||
marker.args[0][0] if isinstance(marker.args[0], tuple) else marker.args[0]
|
||||
for marker in request.node.iter_markers("agent_schema")
|
||||
),
|
||||
test_schema_dict,
|
||||
)
|
||||
|
||||
# Cleanup existing agent
|
||||
try:
|
||||
for agent in llama_extract.list_agents():
|
||||
if agent.name == name:
|
||||
llama_extract.delete_agent(agent.id)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to cleanup existing agent: {e}")
|
||||
|
||||
agent = llama_extract.create_agent(name=name, data_schema=schema)
|
||||
|
||||
# Add agent to cleanup list via conftest helper
|
||||
register_agent_for_cleanup(agent.id)
|
||||
|
||||
yield agent
|
||||
|
||||
|
||||
class TestLlamaExtract:
|
||||
def test_init_without_api_key(self):
|
||||
env_backup = os.getenv("LLAMA_CLOUD_API_KEY")
|
||||
del os.environ["LLAMA_CLOUD_API_KEY"]
|
||||
with pytest.raises(ValueError, match="The API key is required"):
|
||||
LlamaExtract(api_key=None, base_url=LLAMA_CLOUD_BASE_URL)
|
||||
os.environ["LLAMA_CLOUD_API_KEY"] = env_backup
|
||||
|
||||
@pytest.mark.agent_name("test-dict-schema-agent")
|
||||
def test_create_agent_with_dict_schema(self, test_agent):
|
||||
assert isinstance(test_agent, ExtractionAgent)
|
||||
|
||||
@pytest.mark.agent_name("test-pydantic-schema-agent")
|
||||
@pytest.mark.agent_schema((TestSchema,))
|
||||
def test_create_agent_with_pydantic_schema(self, test_agent):
|
||||
assert isinstance(test_agent, ExtractionAgent)
|
||||
|
||||
def test_get_agent_by_name(self, llama_extract, test_agent):
|
||||
agent = llama_extract.get_agent(name=test_agent.name)
|
||||
assert isinstance(agent, ExtractionAgent)
|
||||
assert agent.name == test_agent.name
|
||||
assert agent.id == test_agent.id
|
||||
assert agent.data_schema == test_agent.data_schema
|
||||
|
||||
def test_get_agent_by_id(self, llama_extract, test_agent):
|
||||
agent = llama_extract.get_agent(id=test_agent.id)
|
||||
assert isinstance(agent, ExtractionAgent)
|
||||
assert agent.id == test_agent.id
|
||||
assert agent.name == test_agent.name
|
||||
assert agent.data_schema == test_agent.data_schema
|
||||
|
||||
def test_list_agents(self, llama_extract, test_agent):
|
||||
agents = llama_extract.list_agents()
|
||||
assert isinstance(agents, list)
|
||||
assert any(a.id == test_agent.id for a in agents)
|
||||
|
||||
|
||||
class TestExtractionAgent:
|
||||
@pytest.mark.asyncio
|
||||
async def test_extract_single_file(self, test_agent):
|
||||
result = await test_agent.aextract(TEST_PDF)
|
||||
assert result.status == "SUCCESS"
|
||||
assert result.data is not None
|
||||
assert isinstance(result.data, dict)
|
||||
assert "title" in result.data
|
||||
assert "summary" in result.data
|
||||
|
||||
def test_sync_extract_single_file(self, test_agent):
|
||||
result = test_agent.extract(TEST_PDF)
|
||||
assert result.status == "SUCCESS"
|
||||
assert result.data is not None
|
||||
assert isinstance(result.data, dict)
|
||||
assert "title" in result.data
|
||||
assert "summary" in result.data
|
||||
|
||||
def test_extract_file_from_buffered_io(self, test_agent):
|
||||
result = test_agent.extract(SourceText(file=open(TEST_PDF, "rb")))
|
||||
assert result.status == "SUCCESS"
|
||||
assert result.data is not None
|
||||
assert isinstance(result.data, dict)
|
||||
assert "title" in result.data
|
||||
assert "summary" in result.data
|
||||
|
||||
def test_extract_file_from_bytes(self, test_agent):
|
||||
with open(TEST_PDF, "rb") as f:
|
||||
file_bytes = f.read()
|
||||
result = test_agent.extract(SourceText(file=file_bytes, filename=TEST_PDF.name))
|
||||
assert result.status == "SUCCESS"
|
||||
assert result.data is not None
|
||||
assert isinstance(result.data, dict)
|
||||
assert "title" in result.data
|
||||
assert "summary" in result.data
|
||||
|
||||
def test_extract_from_text_content(self, test_agent):
|
||||
TEST_TEXT = """
|
||||
# Llamas
|
||||
Llamas are social animals and live with others as a herd. Their wool is soft and
|
||||
contains only a small amount of lanolin.[2] Llamas can learn simple tasks after a
|
||||
few repetitions. When using a pack, they can carry about 25 to 30% of their body
|
||||
weight for 8 to 13 km (5–8 miles).[3] The name llama (also historically spelled
|
||||
"glama") was adopted by European settlers from native Peruvians.
|
||||
"""
|
||||
result = test_agent.extract(SourceText(text_content=TEST_TEXT))
|
||||
assert result.status == "SUCCESS"
|
||||
assert result.data is not None
|
||||
assert isinstance(result.data, dict)
|
||||
assert "title" in result.data
|
||||
assert "summary" in result.data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_extract_multiple_files(self, test_agent):
|
||||
files = [TEST_PDF, TEST_PDF] # Using same file twice for testing
|
||||
response = await test_agent.aextract(files)
|
||||
|
||||
assert len(response) == 2
|
||||
for result in response:
|
||||
assert result.status == "SUCCESS"
|
||||
assert result.data is not None
|
||||
assert isinstance(result.data, dict)
|
||||
assert "title" in result.data
|
||||
assert "summary" in result.data
|
||||
|
||||
def test_save_agent_updates(
|
||||
self, test_agent: ExtractionAgent, llama_extract: LlamaExtract
|
||||
):
|
||||
new_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"new_field": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"summary": {"type": "string"},
|
||||
},
|
||||
}
|
||||
test_agent.data_schema = new_schema
|
||||
test_agent.save()
|
||||
|
||||
# Verify the update by getting a fresh instance
|
||||
updated_agent = llama_extract.get_agent(name=test_agent.name)
|
||||
assert "new_field" in updated_agent.data_schema["properties"]
|
||||
|
||||
def test_list_extraction_runs(self, test_agent: ExtractionAgent):
|
||||
assert test_agent.list_extraction_runs().total == 0
|
||||
test_agent.extract(TEST_PDF)
|
||||
runs = test_agent.list_extraction_runs()
|
||||
assert runs.total > 0
|
||||
|
||||
def test_delete_extraction_run(self, test_agent: ExtractionAgent):
|
||||
assert test_agent.list_extraction_runs().total == 0
|
||||
run = test_agent.extract(TEST_PDF)
|
||||
test_agent.delete_extraction_run(run.id)
|
||||
runs = test_agent.list_extraction_runs()
|
||||
assert runs.total == 0
|
||||
@@ -0,0 +1,139 @@
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent
|
||||
from collections import namedtuple
|
||||
import json
|
||||
import uuid
|
||||
from llama_cloud.types import ExtractConfig, ExtractMode
|
||||
from deepdiff import DeepDiff
|
||||
from tests.extract.util import json_subset_match_score, load_test_dotenv
|
||||
|
||||
load_test_dotenv()
|
||||
|
||||
|
||||
TEST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
|
||||
# Get configuration from environment
|
||||
LLAMA_CLOUD_API_KEY = os.getenv("LLAMA_CLOUD_API_KEY")
|
||||
LLAMA_CLOUD_BASE_URL = os.getenv("LLAMA_CLOUD_BASE_URL")
|
||||
LLAMA_CLOUD_PROJECT_ID = os.getenv("LLAMA_CLOUD_PROJECT_ID")
|
||||
|
||||
TestCase = namedtuple(
|
||||
"TestCase", ["name", "schema_path", "config", "input_file", "expected_output"]
|
||||
)
|
||||
|
||||
|
||||
def get_test_cases():
|
||||
"""Get all test cases from TEST_DIR.
|
||||
|
||||
Returns:
|
||||
List[TestCase]: List of test cases
|
||||
"""
|
||||
test_cases = []
|
||||
|
||||
for data_type in os.listdir(TEST_DIR):
|
||||
data_type_dir = os.path.join(TEST_DIR, data_type)
|
||||
if not os.path.isdir(data_type_dir):
|
||||
continue
|
||||
|
||||
schema_path = os.path.join(data_type_dir, "schema.json")
|
||||
if not os.path.exists(schema_path):
|
||||
continue
|
||||
|
||||
input_files = []
|
||||
|
||||
for file in os.listdir(data_type_dir):
|
||||
file_path = os.path.join(data_type_dir, file)
|
||||
if (
|
||||
not os.path.isfile(file_path)
|
||||
or file == "schema.json"
|
||||
or file.endswith(".test.json")
|
||||
):
|
||||
continue
|
||||
|
||||
input_files.append(file_path)
|
||||
|
||||
settings = [
|
||||
ExtractConfig(extraction_mode=ExtractMode.FAST),
|
||||
ExtractConfig(extraction_mode=ExtractMode.BALANCED),
|
||||
]
|
||||
|
||||
for input_file in sorted(input_files):
|
||||
base_name = os.path.splitext(os.path.basename(input_file))[0]
|
||||
expected_output = os.path.join(data_type_dir, f"{base_name}.test.json")
|
||||
|
||||
if not os.path.exists(expected_output):
|
||||
continue
|
||||
|
||||
test_name = f"{data_type}/{os.path.basename(input_file)}"
|
||||
for setting in settings:
|
||||
test_cases.append(
|
||||
TestCase(
|
||||
name=test_name,
|
||||
schema_path=schema_path,
|
||||
input_file=input_file,
|
||||
config=setting,
|
||||
expected_output=expected_output,
|
||||
)
|
||||
)
|
||||
|
||||
return test_cases
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def extractor():
|
||||
"""Create a single LlamaExtract instance for all tests."""
|
||||
extract = LlamaExtract(
|
||||
api_key=LLAMA_CLOUD_API_KEY,
|
||||
base_url=LLAMA_CLOUD_BASE_URL,
|
||||
project_id=LLAMA_CLOUD_PROJECT_ID,
|
||||
verbose=True,
|
||||
)
|
||||
yield extract
|
||||
# Cleanup thread pool at end of session
|
||||
extract._thread_pool.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def extraction_agent(test_case: TestCase, extractor: LlamaExtract):
|
||||
"""Fixture to create and cleanup extraction agent for each test."""
|
||||
# Create unique name with random UUID (important for CI to avoid conflicts)
|
||||
unique_id = uuid.uuid4().hex[:8]
|
||||
agent_name = f"{test_case.name}_{unique_id}"
|
||||
|
||||
with open(test_case.schema_path, "r") as f:
|
||||
schema = json.load(f)
|
||||
|
||||
# Clean up any existing agents with this name
|
||||
try:
|
||||
agents = extractor.list_agents()
|
||||
for agent in agents:
|
||||
if agent.name == agent_name:
|
||||
extractor.delete_agent(agent.id)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to cleanup existing agent: {str(e)}")
|
||||
|
||||
# Create new agent
|
||||
agent = extractor.create_agent(agent_name, schema, config=test_case.config)
|
||||
yield agent
|
||||
|
||||
# Cleanup after test
|
||||
try:
|
||||
extractor.delete_agent(agent.id)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to delete agent {agent.id}: {str(e)}")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
|
||||
reason="LLAMA_CLOUD_API_KEY not set",
|
||||
)
|
||||
@pytest.mark.parametrize("test_case", get_test_cases(), ids=lambda x: x.name)
|
||||
def test_extraction(test_case: TestCase, extraction_agent: ExtractionAgent) -> None:
|
||||
result = extraction_agent.extract(test_case.input_file).data
|
||||
with open(test_case.expected_output, "r") as f:
|
||||
expected = json.load(f)
|
||||
# TODO: fix the saas_slide test
|
||||
assert json_subset_match_score(expected, result) > 0.3, DeepDiff(
|
||||
expected, result, ignore_order=True
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
from typing import Any
|
||||
|
||||
from autoevals.string import Levenshtein
|
||||
from autoevals.number import NumericDiff
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_test_dotenv():
|
||||
load_dotenv(Path(__file__).parent.parent.parent / ".env.dev", override=True)
|
||||
|
||||
|
||||
def json_subset_match_score(expected: Any, actual: Any) -> float:
|
||||
"""
|
||||
Adapted from autoevals.JsonDiff to only test on the subset of keys within the expected json.
|
||||
"""
|
||||
string_scorer = Levenshtein()
|
||||
number_scorer = NumericDiff()
|
||||
if isinstance(expected, dict) and isinstance(actual, dict):
|
||||
if len(expected) == 0 and len(actual) == 0:
|
||||
return 1
|
||||
keys = set(expected.keys())
|
||||
scores = [json_subset_match_score(expected.get(k), actual.get(k)) for k in keys]
|
||||
scores = [s for s in scores if s is not None]
|
||||
return sum(scores) / len(scores)
|
||||
elif isinstance(expected, list) and isinstance(actual, list):
|
||||
if len(expected) == 0 and len(actual) == 0:
|
||||
return 1
|
||||
scores = [json_subset_match_score(e1, e2) for (e1, e2) in zip(expected, actual)]
|
||||
scores = [s for s in scores if s is not None]
|
||||
return sum(scores) / max(len(expected), len(actual))
|
||||
elif isinstance(expected, str) and isinstance(actual, str):
|
||||
return string_scorer.eval(expected, actual).score
|
||||
elif (isinstance(expected, int) or isinstance(expected, float)) and (
|
||||
isinstance(actual, int) or isinstance(actual, float)
|
||||
):
|
||||
return number_scorer.eval(expected, actual).score
|
||||
elif expected is None and actual is None:
|
||||
return 1
|
||||
elif expected is None or actual is None:
|
||||
return 0
|
||||
else:
|
||||
return 0
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import pytest
|
||||
import shutil
|
||||
from typing import Optional, cast
|
||||
from fsspec.implementations.local import LocalFileSystem
|
||||
from httpx import AsyncClient
|
||||
|
||||
@@ -20,11 +21,15 @@ def test_simple_page_text() -> None:
|
||||
assert len(result[0].text) > 0
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def markdown_parser() -> LlamaParse:
|
||||
@pytest.fixture(params=[None, 2])
|
||||
def markdown_parser(request: pytest.FixtureRequest) -> LlamaParse:
|
||||
if os.environ.get("LLAMA_CLOUD_API_KEY", "") == "":
|
||||
pytest.skip("LLAMA_CLOUD_API_KEY not set")
|
||||
return LlamaParse(result_type="markdown", ignore_errors=False)
|
||||
return LlamaParse(
|
||||
result_type="markdown",
|
||||
ignore_errors=False,
|
||||
partition_pages=cast(Optional[int], request.param),
|
||||
)
|
||||
|
||||
|
||||
def test_simple_page_markdown(markdown_parser: LlamaParse) -> None:
|
||||
@@ -35,8 +40,6 @@ def test_simple_page_markdown(markdown_parser: LlamaParse) -> None:
|
||||
|
||||
|
||||
def test_simple_page_markdown_bytes(markdown_parser: LlamaParse) -> None:
|
||||
markdown_parser = LlamaParse(result_type="markdown", ignore_errors=False)
|
||||
|
||||
filepath = "tests/test_files/attention_is_all_you_need.pdf"
|
||||
with open(filepath, "rb") as f:
|
||||
file_bytes = f.read()
|
||||
@@ -51,8 +54,6 @@ def test_simple_page_markdown_bytes(markdown_parser: LlamaParse) -> None:
|
||||
|
||||
|
||||
def test_simple_page_markdown_buffer(markdown_parser: LlamaParse) -> None:
|
||||
markdown_parser = LlamaParse(result_type="markdown", ignore_errors=False)
|
||||
|
||||
filepath = "tests/test_files/attention_is_all_you_need.pdf"
|
||||
with open(filepath, "rb") as f:
|
||||
# client must provide extra_info with file_name
|
||||
@@ -161,9 +162,12 @@ async def test_mixing_input_types() -> None:
|
||||
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
|
||||
reason="LLAMA_CLOUD_API_KEY not set",
|
||||
)
|
||||
@pytest.mark.parametrize("partition_pages", [None, 2])
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_images() -> None:
|
||||
parser = LlamaParse(result_type="markdown", take_screenshot=True)
|
||||
async def test_download_images(partition_pages: Optional[int]) -> None:
|
||||
parser = LlamaParse(
|
||||
result_type="markdown", take_screenshot=True, partition_pages=partition_pages
|
||||
)
|
||||
filepath = "tests/test_files/attention_is_all_you_need.pdf"
|
||||
json_result = await parser.aget_json([filepath])
|
||||
|
||||
@@ -175,3 +179,17 @@ async def test_download_images() -> None:
|
||||
|
||||
await parser.aget_images(json_result, download_path)
|
||||
assert len(os.listdir(download_path)) == len(json_result[0]["pages"][0]["images"])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("split_by_page,expected", [(True, 4), (False, 1)])
|
||||
async def test_multiple_page_markdown(
|
||||
markdown_parser: LlamaParse,
|
||||
split_by_page: bool,
|
||||
expected: int,
|
||||
) -> None:
|
||||
markdown_parser.split_by_page = split_by_page
|
||||
filepath = "tests/test_files/TOS.pdf"
|
||||
result = await markdown_parser.aload_data(filepath)
|
||||
assert len(result) == expected
|
||||
assert all(len(doc.text) > 0 for doc in result)
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import tempfile
|
||||
import os
|
||||
import pytest
|
||||
from typing import Optional
|
||||
from llama_cloud_services import LlamaParse
|
||||
from llama_cloud_services.parse.types import JobResult
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def file_path() -> str:
|
||||
return "tests/test_files/attention_is_all_you_need.pdf"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chart_file_path() -> str:
|
||||
return "tests/test_files/attention_is_all_you_need_chart.pdf"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def multiple_page_path() -> str:
|
||||
return "tests/test_files/TOS.pdf"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
|
||||
reason="LLAMA_CLOUD_API_KEY not set",
|
||||
)
|
||||
@pytest.mark.parametrize("partition_pages", [None, 2])
|
||||
async def test_basic_parse_result(file_path: str, partition_pages: Optional[int]):
|
||||
parser = LlamaParse(
|
||||
take_screenshot=True,
|
||||
auto_mode=True,
|
||||
fast_mode=False,
|
||||
partition_pages=partition_pages,
|
||||
)
|
||||
result = await parser.aparse(file_path)
|
||||
|
||||
assert isinstance(result, JobResult)
|
||||
assert result.job_id is not None
|
||||
assert result.file_name == file_path
|
||||
assert len(result.pages) > 0
|
||||
|
||||
assert result.pages[0].text is not None
|
||||
assert len(result.pages[0].text) > 0
|
||||
|
||||
assert result.pages[0].md is not None
|
||||
assert len(result.pages[0].md) > 0
|
||||
|
||||
assert result.pages[0].md != result.pages[0].text
|
||||
|
||||
assert len(result.pages[0].images) > 0
|
||||
assert result.pages[0].images[0].name is not None
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
file_names = await result.asave_all_images(temp_dir)
|
||||
assert len(file_names) > 0
|
||||
for file_name in file_names:
|
||||
assert os.path.exists(file_name)
|
||||
assert os.path.getsize(file_name) > 0
|
||||
|
||||
assert result.job_metadata is not None
|
||||
|
||||
text_documents = result.get_text_documents(
|
||||
split_by_page=True,
|
||||
)
|
||||
assert len(text_documents) > 0
|
||||
assert text_documents[0].text is not None
|
||||
assert len(text_documents[0].text) > 0
|
||||
|
||||
markdown_documents = result.get_markdown_documents(
|
||||
split_by_page=True,
|
||||
)
|
||||
assert len(markdown_documents) > 0
|
||||
assert markdown_documents[0].text is not None
|
||||
assert len(markdown_documents[0].text) > 0
|
||||
|
||||
image_documents = await result.aget_image_documents(
|
||||
include_screenshot_images=True,
|
||||
include_object_images=False,
|
||||
)
|
||||
assert len(image_documents) > 0
|
||||
assert image_documents[0].image is not None
|
||||
assert len(image_documents[0].resolve_image().getvalue()) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skip(
|
||||
reason="TODO: I don't actually know how to trigger links in the output."
|
||||
)
|
||||
async def test_link_parse_result(file_path: str):
|
||||
parser = LlamaParse(
|
||||
annotate_links=True,
|
||||
)
|
||||
result = await parser.aparse(file_path)
|
||||
|
||||
assert isinstance(result, JobResult)
|
||||
assert len(result.pages) > 0
|
||||
assert len(result.pages[0].links) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
|
||||
reason="LLAMA_CLOUD_API_KEY not set",
|
||||
)
|
||||
@pytest.mark.skip(reason="TODO: Needs to be fixed in prod. Raising 500 error.")
|
||||
async def test_parse_structured_output(file_path: str):
|
||||
parser = LlamaParse(
|
||||
structured_output=True,
|
||||
structured_output_json_schema_name="imFeelingLucky",
|
||||
invalidate_cache=True,
|
||||
)
|
||||
result = await parser.aparse(file_path)
|
||||
assert isinstance(result, JobResult)
|
||||
assert len(result.pages) > 0
|
||||
assert len(result.pages[0].structuredData) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
|
||||
reason="LLAMA_CLOUD_API_KEY not set",
|
||||
)
|
||||
async def test_parse_charts(chart_file_path: str):
|
||||
parser = LlamaParse(
|
||||
extract_charts=True,
|
||||
)
|
||||
result = await parser.aparse(chart_file_path)
|
||||
assert isinstance(result, JobResult)
|
||||
assert len(result.pages) > 0
|
||||
assert len(result.pages[0].charts) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
|
||||
reason="LLAMA_CLOUD_API_KEY not set",
|
||||
)
|
||||
async def test_parse_layout(file_path: str):
|
||||
parser = LlamaParse(
|
||||
extract_layout=True,
|
||||
)
|
||||
result = await parser.aparse(file_path)
|
||||
|
||||
assert isinstance(result, JobResult)
|
||||
assert len(result.pages) > 0
|
||||
assert len(result.pages[0].layout) > 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
|
||||
reason="LLAMA_CLOUD_API_KEY not set",
|
||||
)
|
||||
@pytest.mark.parametrize("partition_pages", [None, 2])
|
||||
def test_parse_multiple_files(
|
||||
file_path: str, chart_file_path: str, partition_pages: Optional[int]
|
||||
):
|
||||
parser = LlamaParse(partition_pages=partition_pages)
|
||||
result = parser.parse([file_path, chart_file_path])
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], JobResult)
|
||||
assert isinstance(result[1], JobResult)
|
||||
assert result[0].file_name == file_path
|
||||
assert result[1].file_name == chart_file_path
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
|
||||
reason="LLAMA_CLOUD_API_KEY not set",
|
||||
)
|
||||
@pytest.mark.parametrize("partition_pages", [None, 2])
|
||||
async def test_multiple_page_parse_result(
|
||||
multiple_page_path: str, partition_pages: Optional[int]
|
||||
):
|
||||
parser = LlamaParse(
|
||||
take_screenshot=True,
|
||||
auto_mode=True,
|
||||
fast_mode=False,
|
||||
partition_pages=partition_pages,
|
||||
)
|
||||
results = await parser.aparse(multiple_page_path)
|
||||
if partition_pages is None:
|
||||
assert isinstance(results, JobResult)
|
||||
results = [results]
|
||||
else:
|
||||
assert isinstance(results, list)
|
||||
|
||||
for result in results:
|
||||
assert isinstance(result, JobResult)
|
||||
assert result.job_id is not None
|
||||
assert result.file_name == multiple_page_path
|
||||
assert len(result.pages) > 0
|
||||
|
||||
assert result.pages[0].text is not None
|
||||
assert len(result.pages[0].text) > 0
|
||||
|
||||
assert result.pages[0].md is not None
|
||||
assert len(result.pages[0].md) > 0
|
||||
|
||||
assert result.pages[0].md != result.pages[0].text
|
||||
@@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from llama_cloud_services.parse.utils import expand_target_pages, partition_pages
|
||||
|
||||
|
||||
def test_expand_target_pages() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
list(expand_target_pages("x"))
|
||||
with pytest.raises(ValueError):
|
||||
list(expand_target_pages("1-2-3"))
|
||||
with pytest.raises(ValueError):
|
||||
list(expand_target_pages("2-1"))
|
||||
result = list(expand_target_pages("0,2-3,5,8-10"))
|
||||
assert result == [0, 2, 3, 5, 8, 9, 10]
|
||||
|
||||
|
||||
def test_partion_pages() -> None:
|
||||
pages = [0, 2, 3, 5, 8, 9, 10]
|
||||
with pytest.raises(ValueError):
|
||||
list(partition_pages(pages, 0))
|
||||
result = list(partition_pages(pages, 3))
|
||||
assert result == ["0,2-3", "5,8-9", "10"]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
list(partition_pages(pages, 3, 0))
|
||||
result = list(partition_pages(pages, 3, max_pages=5))
|
||||
assert result == ["0,2-3", "5,8"]
|
||||
result = list(partition_pages(pages, 3, max_pages=10))
|
||||
assert result == ["0,2-3", "5,8-9", "10"]
|
||||
@@ -7,7 +7,8 @@ from llama_cloud_services.report import LlamaReport, ReportClient
|
||||
|
||||
# Skip tests if no API key is set
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.getenv("LLAMA_CLOUD_API_KEY"), reason="No API key provided"
|
||||
not os.getenv("LLAMA_CLOUD_API_KEY") or os.getenv("CI") == "true",
|
||||
reason="No API key provided",
|
||||
)
|
||||
|
||||
|
||||
@@ -62,6 +63,10 @@ async def report(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(
|
||||
condition=lambda: os.getenv("CI"),
|
||||
reason="Backend db issues; needs to be fixed.",
|
||||
)
|
||||
async def test_create_and_delete_report(
|
||||
client: LlamaReport, report: ReportClient
|
||||
) -> None:
|
||||
@@ -81,6 +86,11 @@ async def test_create_and_delete_report(
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(
|
||||
condition=lambda: os.getenv("CI"),
|
||||
reason="Report plan sometimes times out",
|
||||
raises=TimeoutError,
|
||||
)
|
||||
async def test_report_plan_workflow(report: ReportClient) -> None:
|
||||
"""Test the report planning workflow."""
|
||||
# Wait for the plan
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from pydantic import BaseModel
|
||||
from llama_cloud_services.utils import check_extra_params
|
||||
|
||||
|
||||
class MyModel(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
email: str
|
||||
is_active: bool
|
||||
|
||||
|
||||
def test_check_extra_params_no_extra():
|
||||
"""Test when all parameters are valid - should return empty lists."""
|
||||
data = {"name": "John", "age": 25, "email": "john@example.com", "is_active": True}
|
||||
|
||||
extra_params, suggestions = check_extra_params(MyModel, data)
|
||||
|
||||
assert extra_params == []
|
||||
assert suggestions == []
|
||||
|
||||
|
||||
def test_check_extra_params_with_typos():
|
||||
"""Test when there are extra parameters that are close to valid ones (typos)."""
|
||||
data = {
|
||||
"name": "John",
|
||||
"age": 25,
|
||||
"emial": "john@example.com", # typo: emial instead of email
|
||||
"is_activ": True, # typo: is_activ instead of is_active
|
||||
"address": "123 Main St", # completely different parameter
|
||||
}
|
||||
|
||||
extra_params, suggestions = check_extra_params(MyModel, data)
|
||||
|
||||
assert len(extra_params) == 3
|
||||
assert "emial" in extra_params
|
||||
assert "is_activ" in extra_params
|
||||
assert "address" in extra_params
|
||||
|
||||
# Check that typo suggestions are provided
|
||||
assert len(suggestions) == 3
|
||||
assert "Did you mean 'email' instead of 'emial'?" in suggestions[0]
|
||||
assert "Did you mean 'is_active' instead of 'is_activ'?" in suggestions[1]
|
||||
assert "check the documentation or update the package" in suggestions[2]
|
||||
|
||||
|
||||
def test_check_extra_params_completely_invalid():
|
||||
"""Test when there are extra parameters with no close matches."""
|
||||
data = {
|
||||
"name": "John",
|
||||
"xyz": "invalid",
|
||||
"random_field": 123,
|
||||
"completely_different": True,
|
||||
}
|
||||
|
||||
extra_params, suggestions = check_extra_params(MyModel, data)
|
||||
|
||||
assert len(extra_params) == 3
|
||||
assert "xyz" in extra_params
|
||||
assert "random_field" in extra_params
|
||||
assert "completely_different" in extra_params
|
||||
|
||||
# All suggestions should be generic (no close matches)
|
||||
assert len(suggestions) == 3
|
||||
for suggestion in suggestions:
|
||||
assert "check the documentation or update the package" in suggestion
|
||||
assert "Did you mean" not in suggestion
|
||||