Compare commits

...

20 Commits

Author SHA1 Message Date
Neeraj Pradhan 1bae950447 read project_id from env 2025-06-05 14:08:26 -07:00
Simon Suo f644afdaf2 wip 2025-06-05 12:43:27 -07:00
Simon Suo f98c0cefb3 wip 2025-06-04 23:17:36 -07:00
Peter Rowlands (변기호) 584ba8d48e 0.6.28: fix job result format after partitioning changes (#741)
* parse: fix job result format

* bump to 0.6.28
2025-06-02 15:25:30 -07:00
Peter Rowlands (변기호) 925805ee11 parse: support partitioning files before parsing (#709)
* parse: add utils for handling target_pages

* parse: support partitioning docs into multiple parse jobs

* tests: add tests for partitioned parse

* drop unneeded get_job_result call

* add parse JobFailedException and expected error handling

* bump to 0.6.27
2025-06-02 12:27:58 -07:00
Logan 76fb73c971 v0.6.26 (#740) 2025-06-02 09:59:45 -06:00
Abhik Bhattacharjee 6d19ea9ac0 parse: fix the "model" parameter mismatch between playground and Python client (#737) 2025-06-02 09:35:30 -06:00
Pierre-Loic Doulcet 90431090e9 0.6.25 outlined_table_extraction (#736) 2025-05-30 11:37:21 +02:00
Neeraj Pradhan 6dff35b204 Add notebook for Form 4 extraction (#731)
* Add notebook for Form 4 extraction

* fix comments

* heavier caching; add mermaid diag

* add output directory

* save notebook
2025-05-29 18:31:56 -07:00
Logan e634c7978d v0.6.24 (#732) 2025-05-28 20:11:51 -06:00
Neeraj Pradhan 7a9e99bba2 Bump to version 0.6.23 (#729) 2025-05-20 09:43:06 -07:00
Adrian Lyjak efcdd4405b Pass through verify and timeout config to the extraction agent (#726) 2025-05-17 12:51:16 -07:00
Javier Torres bf3614690f Remove credits from parse metadata (#720) 2025-05-09 16:03:09 -05:00
Logan 7463e00da3 v0.6.22 (#718) 2025-05-08 11:44:41 -06:00
Tuana Çelik cbe9de0c57 Adding example for extracting with citations (#716)
* Adding example for extracting with citations

* removing TOC and installation output
2025-05-06 23:32:17 +02:00
Logan a023507d42 even more optional (#711) 2025-05-01 15:52:38 -06:00
Peter Rowlands (변기호) e48f544ddc parse: fix num_workers/parse job batching (#708) 2025-05-01 09:30:35 -06:00
Logan 4aa7ad5642 v0.6.20 (#707) 2025-04-29 08:53:55 -06:00
Sacha Bron c39cdbcd01 v0.6.19 (#706) 2025-04-29 12:28:21 +02:00
Pierre-Loic Doulcet 71eaa8bcc6 add auto_mode_configuration_jon for llamaParse (#704) 2025-04-29 12:23:03 +02:00
18 changed files with 16689 additions and 174 deletions
+9 -1
View File
@@ -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 60
- 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"
@@ -0,0 +1 @@
sec_form_4_dump.json
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 202 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
}
File diff suppressed because it is too large Load Diff
+2 -14
View File
@@ -17,7 +17,6 @@ from llama_cloud import (
File,
ExtractMode,
StatusEnum,
Project,
ExtractTarget,
LlamaExtractSettings,
PaginatedExtractRunsResponse,
@@ -633,21 +632,8 @@ class LlamaExtract(BaseComponent):
self._thread_pool = ThreadPoolExecutor(
max_workers=min(10, (os.cpu_count() or 1) + 4)
)
# Fetch default project id if not provided
if not project_id:
project_id = os.getenv("LLAMA_CLOUD_PROJECT_ID", None)
if not project_id:
print("No project_id provided, fetching default project.")
projects: List[Project] = self._run_in_thread(
self._async_client.projects.list_projects()
)
default_project = [p for p in projects if p.is_default]
if not default_project:
raise ValueError(
"No default project found. Please provide a project_id."
)
project_id = default_project[0].id
self._project_id = project_id
self._organization_id = organization_id
@@ -711,6 +697,8 @@ class LlamaExtract(BaseComponent):
num_workers=self.num_workers,
show_progress=self.show_progress,
verbose=self.verbose,
verify=self.verify,
httpx_timeout=self.httpx_timeout,
)
def get_agent(
+265 -84
View File
@@ -7,7 +7,7 @@ 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
@@ -25,9 +25,11 @@ from llama_cloud_services.parse.utils import (
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
@@ -57,6 +59,36 @@ 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."""
@@ -115,7 +147,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(
@@ -145,6 +177,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.",
@@ -293,6 +329,10 @@ 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.",
@@ -406,6 +446,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.",
@@ -449,6 +493,11 @@ class LlamaParse(BasePydanticReader):
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.",
)
@field_validator("api_key", mode="before", check_fields=True)
@classmethod
def validate_api_key(cls, v: str) -> str:
@@ -536,6 +585,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
@@ -586,6 +636,9 @@ class LlamaParse(BasePydanticReader):
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"
@@ -745,6 +798,9 @@ 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
@@ -827,7 +883,9 @@ class LlamaParse(BasePydanticReader):
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
@@ -840,6 +898,9 @@ 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
@@ -921,15 +982,7 @@ class LlamaParse(BasePydanticReader):
print(".", end="", flush=True)
current_interval = self._calculate_backoff(current_interval)
else:
error_code = result_json.get("error_code", "No error code found")
error_message = result_json.get(
"error_message", "No error message found"
)
exception_str = (
f"Job ID: {job_id} failed with status: {status}, "
f"Error code: {error_code}, Error message: {error_message}"
)
raise Exception(exception_str)
raise JobFailedException.from_result(result_json)
except (
httpx.ConnectError,
httpx.ReadError,
@@ -952,26 +1005,151 @@ class LlamaParse(BasePydanticReader):
)
current_interval = self._calculate_backoff(current_interval)
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,
)
]
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
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:
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}",
)
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,
file_path: FileInput,
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 {},
)
]
@@ -994,7 +1172,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
@@ -1006,6 +1188,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
]
@@ -1044,6 +1227,34 @@ 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],
@@ -1062,14 +1273,10 @@ class LlamaParse(BasePydanticReader):
fs: Optional filesystem to use for reading files.
Returns:
JobResult object or list of JobResult objects if multiple files were provided
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)):
job_id = await self._create_job(file_path, extra_info=extra_info, fs=fs)
if self.verbose:
print("Started parsing the file under job_id %s" % job_id)
if isinstance(file_path, (bytes, BufferedIOBase)):
if not extra_info or "file_name" not in extra_info:
raise ValueError(
@@ -1078,29 +1285,12 @@ class LlamaParse(BasePydanticReader):
file_name = extra_info["file_name"]
else:
file_name = str(file_path)
job_result = await self._get_job_result(
job_id, ResultType.JSON.value, verbose=self.verbose
)
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,
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):
jobs = [
self._create_job(
f,
extra_info=extra_info,
fs=fs,
)
for f in file_path
]
file_names = []
for f in file_path:
if isinstance(f, (bytes, BufferedIOBase)):
@@ -1112,40 +1302,24 @@ class LlamaParse(BasePydanticReader):
else:
file_names.append(str(f))
job_results = []
try:
job_ids = await run_jobs(
jobs,
workers=self.num_workers,
desc="Creating parsing jobs",
show_progress=self.show_progress,
)
job_results = await run_jobs(
for result in await run_jobs(
[
self._get_job_result(
job_id, ResultType.JSON.value, verbose=self.verbose
self._aparse_one(
f,
file_names[i],
extra_info=extra_info,
fs=fs,
num_workers=1,
)
for job_id in job_ids
for i, f in enumerate(file_path)
],
workers=self.num_workers,
desc="Getting job results",
show_progress=self.show_progress,
)
# Create JobResults just using the job_ids and job_results
job_results = [
JobResult(
job_id=job_id,
file_name=file_names[i],
job_result=job_results[i],
api_key=self.api_key,
base_url=self.base_url,
client=self.aclient,
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
)
for i, job_id in enumerate(job_ids)
]
):
job_results.extend(result)
return job_results
except RuntimeError as e:
@@ -1187,20 +1361,27 @@ class LlamaParse(BasePydanticReader):
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)
+36 -19
View File
@@ -14,9 +14,6 @@ PAGE_REGEX = r"page[-_](\d+)\.jpg$"
class JobMetadata(BaseModel):
"""Metadata about the job."""
job_credits_usage: int = Field(
default_factory=dict, description="The credits usage for 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)."
@@ -49,17 +46,25 @@ class PageItem(BaseModel):
rows: Optional[List[List[str]]] = Field(
default=None, description="The rows of the item."
)
bBox: BBox = Field(description="The bounding box 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: float = Field(description="The height of the image.")
width: float = Field(description="The width of the image.")
x: float = Field(description="The x-coordinate of the image.")
y: float = Field(description="The y-coordinate 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."
)
@@ -75,7 +80,9 @@ class LayoutItem(BaseModel):
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: BBox = Field(description="The bounding box 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.")
@@ -83,18 +90,24 @@ class ChartItem(BaseModel):
"""A chart in a page."""
name: str = Field(description="The name of the chart.")
x: float = Field(description="The x-coordinate of the chart.")
y: float = Field(description="The y-coordinate of the chart.")
width: float = Field(description="The width of the chart.")
height: float = Field(description="The height 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: str = Field(description="The text of the page.")
md: str = Field(description="The markdown of the page.")
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.",
@@ -115,8 +128,8 @@ class Page(BaseModel):
links: List[SerializeAsAny[Any]] = Field(
default_factory=list, description="The links in the page."
)
width: float = Field(description="The width of the page.")
height: float = Field(description="The height of 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(
description="Whether the page triggered auto mode (thus increasing the cost)."
)
@@ -189,7 +202,9 @@ class JobResult(BaseModel):
for page in self.pages
]
else:
text = self._page_separator.join([page.text for page in self.pages])
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]:
@@ -234,7 +249,9 @@ class JobResult(BaseModel):
else:
return [
Document(
text=self._page_separator.join([page.md for page in self.pages]),
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},
)
]
+55 -1
View File
@@ -1,4 +1,5 @@
import httpx
import itertools
import logging
from enum import Enum
from tenacity import (
@@ -8,7 +9,7 @@ from tenacity import (
retry_if_exception,
before_sleep_log,
)
from typing import Any
from typing import Any, Iterable, Iterator, Optional
logger = logging.getLogger(__name__)
@@ -297,3 +298,56 @@ async def make_api_request(
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
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
+3087
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "llama-parse"
version = "0.6.18"
version = "0.6.28"
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.17"
llama-cloud-services = ">=0.6.28"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
Generated
+212 -37
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -8,7 +8,7 @@ python_version = "3.10"
[tool.poetry]
name = "llama-cloud-services"
version = "0.6.18"
version = "0.6.28"
description = "Tailored SDK clients for LlamaCloud services."
authors = ["Logan Markewich <logan@runllama.ai>"]
license = "MIT"
@@ -17,9 +17,9 @@ packages = [{include = "llama_cloud_services"}]
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
llama-index-core = ">=0.11.0"
llama-cloud = "==0.1.19"
pydantic = "!=2.10"
llama-index-core = ">=0.12.0"
llama-cloud = "==0.1.23"
pydantic = ">=2.8,!=2.10"
click = "^8.1.7"
python-dotenv = "^1.0.1"
eval-type-backport = {python = "<3.10", version = "^0.2.0"}
+27 -9
View File
@@ -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)
+51 -3
View File
@@ -1,6 +1,7 @@
import tempfile
import os
import pytest
from typing import Optional
from llama_cloud_services import LlamaParse
from llama_cloud_services.parse.types import JobResult
@@ -15,16 +16,23 @@ 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",
)
async def test_basic_parse_result(file_path: str):
@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)
@@ -142,8 +150,11 @@ async def test_parse_layout(file_path: str):
os.environ.get("LLAMA_CLOUD_API_KEY", "") == "",
reason="LLAMA_CLOUD_API_KEY not set",
)
def test_parse_multiple_files(file_path: str, chart_file_path: str):
parser = LlamaParse()
@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)
@@ -152,3 +163,40 @@ def test_parse_multiple_files(file_path: str, chart_file_path: str):
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
+30
View File
@@ -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"]
Binary file not shown.