mirror of
https://github.com/run-llama/llama_cloud_services.git
synced 2026-07-20 00:54:09 -04:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59b99a472f | |||
| 7241ffee7c | |||
| dc82270724 | |||
| d880a48dd0 | |||
| 7567e8b45e | |||
| 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 |
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Wait for PyPI to update
|
||||
run: |
|
||||
sleep 60
|
||||
sleep 120
|
||||
|
||||
- name: Update llama-parse lock file
|
||||
run: |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
@@ -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
@@ -8,6 +8,12 @@ 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,
|
||||
@@ -17,12 +23,12 @@ from llama_cloud import (
|
||||
File,
|
||||
ExtractMode,
|
||||
StatusEnum,
|
||||
Project,
|
||||
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,
|
||||
@@ -45,6 +51,17 @@ DEFAULT_EXTRACT_CONFIG = ExtractConfig(
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
@@ -232,9 +249,8 @@ class ExtractionAgent:
|
||||
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:
|
||||
file_contents: Union[BufferedIOBase, BytesIO]
|
||||
|
||||
if file_input.text_content is not None:
|
||||
# Handle direct text content
|
||||
file_contents = BytesIO(file_input.text_content.encode("utf-8"))
|
||||
@@ -261,7 +277,7 @@ class ExtractionAgent:
|
||||
project_id=self._project_id, upload_file=file_contents
|
||||
)
|
||||
finally:
|
||||
if isinstance(file_contents, BufferedReader):
|
||||
if file_contents is not None and isinstance(file_contents, BufferedReader):
|
||||
file_contents.close()
|
||||
|
||||
async def _upload_file(self, file_input: FileInput) -> File:
|
||||
@@ -289,35 +305,60 @@ class ExtractionAgent:
|
||||
|
||||
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
|
||||
job = await self._client.llama_extract.get_job(
|
||||
job_id=job_id,
|
||||
)
|
||||
|
||||
if job.status == StatusEnum.SUCCESS:
|
||||
return await self._client.llama_extract.get_run_by_job_id(
|
||||
job_id=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._client.llama_extract.get_run_by_job_id(
|
||||
job_id=job_id,
|
||||
)
|
||||
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.
|
||||
@@ -633,21 +674,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 +739,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(
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import mimetypes
|
||||
import os
|
||||
import time
|
||||
import warnings
|
||||
from contextlib import asynccontextmanager
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
@@ -13,21 +14,29 @@ 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
|
||||
@@ -57,6 +66,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."""
|
||||
|
||||
@@ -234,6 +273,10 @@ class LlamaParse(BasePydanticReader):
|
||||
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.",
|
||||
@@ -297,6 +340,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.",
|
||||
@@ -410,10 +457,42 @@ 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.",
|
||||
)
|
||||
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.",
|
||||
)
|
||||
hide_headers: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Whether to hide page header in output markdown.",
|
||||
)
|
||||
hide_footers: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Whether to hide page footers in output markdown.",
|
||||
)
|
||||
page_header_suffix: Optional[str] = Field(
|
||||
default=None,
|
||||
description="A suffix to add to the page header in the output markdown.",
|
||||
)
|
||||
page_header_prefix: Optional[str] = Field(
|
||||
default=None,
|
||||
description="A prefix to add to the page header in the output markdown.",
|
||||
)
|
||||
page_footer_suffix: Optional[str] = Field(
|
||||
default=None,
|
||||
description="A suffix to add to the page footer in the output markdown.",
|
||||
)
|
||||
page_footer_prefix: Optional[str] = Field(
|
||||
default=None,
|
||||
description="A prefix to add to the page footer in the output markdown.",
|
||||
)
|
||||
|
||||
# Deprecated
|
||||
bounding_box: Optional[str] = Field(
|
||||
@@ -453,6 +532,21 @@ class LlamaParse(BasePydanticReader):
|
||||
description="Whether to use the vendor multimodal API.",
|
||||
)
|
||||
|
||||
@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:
|
||||
@@ -540,6 +634,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
|
||||
@@ -690,6 +785,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
|
||||
|
||||
@@ -752,12 +850,33 @@ 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
|
||||
|
||||
if self.hide_headers:
|
||||
data["hide_headers"] = self.hide_headers
|
||||
|
||||
if self.hide_footers:
|
||||
data["hide_footers"] = self.hide_footers
|
||||
|
||||
if self.page_header_suffix is not None:
|
||||
data["page_header_suffix"] = self.page_header_suffix
|
||||
|
||||
if self.page_header_prefix is not None:
|
||||
data["page_header_prefix"] = self.page_header_prefix
|
||||
|
||||
if self.page_footer_suffix is not None:
|
||||
data["page_footer_suffix"] = self.page_footer_suffix
|
||||
|
||||
if self.page_footer_prefix is not None:
|
||||
data["page_footer_prefix"] = self.page_footer_prefix
|
||||
|
||||
# only send page separator to server if it is not None
|
||||
# as if a null, "" string is sent the server will then ignore the page separator instead of using the default
|
||||
if self.page_separator is not None:
|
||||
@@ -834,7 +953,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
|
||||
@@ -847,6 +968,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
|
||||
|
||||
@@ -928,15 +1052,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,
|
||||
@@ -945,6 +1061,7 @@ class LlamaParse(BasePydanticReader):
|
||||
httpx.ReadTimeout,
|
||||
httpx.WriteTimeout,
|
||||
httpx.HTTPStatusError,
|
||||
httpx.RemoteProtocolError,
|
||||
) as err:
|
||||
error_count += 1
|
||||
end = time.time()
|
||||
@@ -965,9 +1082,39 @@ class LlamaParse(BasePydanticReader):
|
||||
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)
|
||||
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(
|
||||
@@ -975,21 +1122,105 @@ class LlamaParse(BasePydanticReader):
|
||||
)
|
||||
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, result = await self._parse_one(
|
||||
file_path, extra_info=extra_info, fs=fs
|
||||
)
|
||||
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 {},
|
||||
)
|
||||
]
|
||||
@@ -1012,7 +1243,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
|
||||
@@ -1024,6 +1259,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
|
||||
]
|
||||
@@ -1062,6 +1298,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],
|
||||
@@ -1080,7 +1344,7 @@ 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)):
|
||||
@@ -1092,22 +1356,10 @@ class LlamaParse(BasePydanticReader):
|
||||
file_name = extra_info["file_name"]
|
||||
else:
|
||||
file_name = str(file_path)
|
||||
|
||||
job_id, job_result = await self._parse_one(
|
||||
file_path,
|
||||
extra_info=extra_info,
|
||||
fs=fs,
|
||||
result_type=ResultType.JSON.value,
|
||||
)
|
||||
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):
|
||||
file_names = []
|
||||
@@ -1121,35 +1373,25 @@ class LlamaParse(BasePydanticReader):
|
||||
else:
|
||||
file_names.append(str(f))
|
||||
|
||||
job_results = []
|
||||
try:
|
||||
job_results = await run_jobs(
|
||||
for result in await run_jobs(
|
||||
[
|
||||
self._parse_one(
|
||||
self._aparse_one(
|
||||
f,
|
||||
file_names[i],
|
||||
extra_info=extra_info,
|
||||
fs=fs,
|
||||
result_type=ResultType.JSON.value,
|
||||
num_workers=1,
|
||||
)
|
||||
for f in file_path
|
||||
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
|
||||
return [
|
||||
JobResult(
|
||||
job_id=job_id,
|
||||
file_name=file_names[i],
|
||||
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 i, (job_id, job_result) in enumerate(job_results)
|
||||
]
|
||||
):
|
||||
job_results.extend(result)
|
||||
return job_results
|
||||
|
||||
except RuntimeError as e:
|
||||
if nest_asyncio_err in str(e):
|
||||
@@ -1190,21 +1432,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, result = await self._parse_one(
|
||||
job_results = await self._parse_one(
|
||||
file_path,
|
||||
extra_info=extra_info,
|
||||
result_type=ResultType.JSON.value,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
result["job_id"] = job_id
|
||||
|
||||
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)
|
||||
@@ -1219,7 +1467,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]
|
||||
@@ -1240,7 +1488,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(
|
||||
|
||||
@@ -14,11 +14,8 @@ 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(
|
||||
job_auto_mode_triggered_pages: Optional[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.")
|
||||
@@ -46,7 +43,7 @@ class PageItem(BaseModel):
|
||||
md: Optional[str] = Field(
|
||||
default=None, description="The markdown-formatted content of the item."
|
||||
)
|
||||
rows: Optional[List[List[str]]] = Field(
|
||||
rows: Optional[List[List[Any]]] = Field(
|
||||
default=None, description="The rows of the item."
|
||||
)
|
||||
bBox: Optional[BBox] = Field(
|
||||
@@ -127,23 +124,28 @@ class Page(BaseModel):
|
||||
items: List[PageItem] = Field(
|
||||
default_factory=list, description="The items in the page."
|
||||
)
|
||||
status: str = Field(description="The status of 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(
|
||||
description="Whether the page triggered auto mode (thus increasing the cost)."
|
||||
triggeredAutoMode: Optional[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."
|
||||
)
|
||||
parsingMode: str = Field(description="The parsing mode used for the page.")
|
||||
structuredData: Optional[Dict[str, Any]] = Field(
|
||||
description="The structured data of the page."
|
||||
default=None, description="The structured data of the page."
|
||||
)
|
||||
noStructuredContent: bool = Field(
|
||||
description="Whether the page has no structured data."
|
||||
default=True, description="Whether the page has no structured data."
|
||||
)
|
||||
noTextContent: bool = Field(
|
||||
default=False, description="Whether the page has no text content."
|
||||
)
|
||||
noTextContent: bool = Field(description="Whether the page has no text content.")
|
||||
|
||||
|
||||
class JobResult(BaseModel):
|
||||
|
||||
@@ -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[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
|
||||
Generated
+47
-46
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
@@ -257,7 +257,7 @@ version = "2025.1.31"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
|
||||
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
|
||||
@@ -350,7 +350,7 @@ version = "3.4.1"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
|
||||
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
|
||||
@@ -593,7 +593,7 @@ description = "Like `typing._eval_type`, but lets older Python versions use newe
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.9\""
|
||||
markers = "python_version < \"3.10\""
|
||||
files = [
|
||||
{file = "eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a"},
|
||||
{file = "eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1"},
|
||||
@@ -892,31 +892,31 @@ colorama = ">=0.4"
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
description = "A minimal low-level HTTP client."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be"},
|
||||
{file = "httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad"},
|
||||
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
h11 = ">=0.16"
|
||||
|
||||
[package.extras]
|
||||
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||
@@ -955,7 +955,7 @@ version = "3.10"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
@@ -971,7 +971,7 @@ description = "Read metadata from Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
markers = "python_version == \"3.9\""
|
||||
markers = "python_version < \"3.10\""
|
||||
files = [
|
||||
{file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"},
|
||||
{file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"},
|
||||
@@ -1170,14 +1170,14 @@ test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"
|
||||
|
||||
[[package]]
|
||||
name = "llama-cloud"
|
||||
version = "0.1.19"
|
||||
version = "0.1.23"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = "<4,>=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "llama_cloud-0.1.19-py3-none-any.whl", hash = "sha256:d2d551baa4b63f7717f8e04cbb81b0f817e5450a66870c5487dd371f81dab8ec"},
|
||||
{file = "llama_cloud-0.1.19.tar.gz", hash = "sha256:b0a5424ae0099ca27df2a2d7e5aec99066de9ca860ab65987c9f931f1ea7abff"},
|
||||
{file = "llama_cloud-0.1.23-py3-none-any.whl", hash = "sha256:ce95b0705d85c99b3b27b0af0d16a17d9a81b14c96bf13c1063a1bd13d8d0446"},
|
||||
{file = "llama_cloud-0.1.23.tar.gz", hash = "sha256:3d84a24a860f046d39a106c06742ec0ea39a574ac42bbf91706fe025f44e233e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1187,23 +1187,23 @@ pydantic = ">=1.10"
|
||||
|
||||
[[package]]
|
||||
name = "llama-cloud-services"
|
||||
version = "0.6.18"
|
||||
version = "0.6.30"
|
||||
description = "Tailored SDK clients for LlamaCloud services."
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "llama_cloud_services-0.6.18-py3-none-any.whl", hash = "sha256:ba24ef36f4f78619722e5fe2cac8739175ba63f67cb20f0432be16801c78162c"},
|
||||
{file = "llama_cloud_services-0.6.18.tar.gz", hash = "sha256:070a0e1397e2dd6da59e5f6a0437e3fd27e1c156170eea66001c4bcbec2f8783"},
|
||||
{file = "llama_cloud_services-0.6.30-py3-none-any.whl", hash = "sha256:4d5817a9841fc3ba3409865c52d082090f4ef827931f0e5e4a89f5818c0d4e36"},
|
||||
{file = "llama_cloud_services-0.6.30.tar.gz", hash = "sha256:2cb5004d13127aac52888ae9b3d70f899d598633520b2a2542bb62682d08d776"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.1.7,<9.0.0"
|
||||
eval-type-backport = {version = ">=0.2.0,<0.3.0", markers = "python_version < \"3.10\""}
|
||||
llama-cloud = "0.1.19"
|
||||
llama-index-core = ">=0.11.0"
|
||||
llama-cloud = "0.1.23"
|
||||
llama-index-core = ">=0.12.0"
|
||||
platformdirs = ">=4.3.7,<5.0.0"
|
||||
pydantic = "!=2.10"
|
||||
pydantic = ">=2.8,<2.10 || >2.10"
|
||||
python-dotenv = ">=1.0.1,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
@@ -2469,19 +2469,19 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
version = "2.32.4"
|
||||
description = "Python HTTP for Humans."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||
{file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"},
|
||||
{file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = ">=2,<4"
|
||||
charset_normalizer = ">=2,<4"
|
||||
idna = ">=2.5,<4"
|
||||
urllib3 = ">=1.21.1,<3"
|
||||
|
||||
@@ -2738,23 +2738,24 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.4.2"
|
||||
version = "6.5.1"
|
||||
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"},
|
||||
{file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b"},
|
||||
{file = "tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7"},
|
||||
{file = "tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2806,7 +2807,7 @@ files = [
|
||||
{file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"},
|
||||
{file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"},
|
||||
]
|
||||
markers = {dev = "python_version == \"3.9\""}
|
||||
markers = {dev = "python_version < \"3.10\""}
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspect"
|
||||
@@ -2845,7 +2846,7 @@ version = "2.4.0"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"},
|
||||
{file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"},
|
||||
@@ -3067,7 +3068,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
markers = "python_version == \"3.9\""
|
||||
markers = "python_version < \"3.10\""
|
||||
files = [
|
||||
{file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"},
|
||||
{file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"},
|
||||
@@ -3084,4 +3085,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.9,<4.0"
|
||||
content-hash = "5630c89272551940c2c689fc2c78f5ced7b6dd904ae239ac8987e2e5fb2c0186"
|
||||
content-hash = "1be767f62c8a8e1c9acb76347055a837ea2c44130004703361d0303b049bede7"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "llama-parse"
|
||||
version = "0.6.21"
|
||||
version = "0.6.41"
|
||||
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.21"
|
||||
llama-cloud-services = ">=0.6.41"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.0.0"
|
||||
|
||||
Generated
+212
-37
File diff suppressed because it is too large
Load Diff
+5
-4
@@ -8,7 +8,7 @@ python_version = "3.10"
|
||||
|
||||
[tool.poetry]
|
||||
name = "llama-cloud-services"
|
||||
version = "0.6.21"
|
||||
version = "0.6.41"
|
||||
description = "Tailored SDK clients for LlamaCloud services."
|
||||
authors = ["Logan Markewich <logan@runllama.ai>"]
|
||||
license = "MIT"
|
||||
@@ -17,13 +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.19"
|
||||
pydantic = "!=2.10"
|
||||
llama-index-core = ">=0.12.0"
|
||||
llama-cloud = "==0.1.30"
|
||||
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)
|
||||
Binary file not shown.
@@ -5,6 +5,7 @@ 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()
|
||||
|
||||
@@ -27,7 +28,7 @@ class TestSchema(BaseModel):
|
||||
|
||||
# Test data paths
|
||||
TEST_DIR = Path(__file__).parent / "data"
|
||||
TEST_PDF = TEST_DIR / "slide" / "saas_slide.pdf"
|
||||
TEST_PDF = TEST_DIR / "api_test" / "noisebridge_receipt.pdf"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -58,7 +59,7 @@ def test_schema_dict():
|
||||
|
||||
@pytest.fixture
|
||||
def test_agent(llama_extract, test_agent_name, test_schema_dict, request):
|
||||
"""Creates a test agent and cleans it up after the test"""
|
||||
"""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
|
||||
@@ -86,13 +87,11 @@ def test_agent(llama_extract, test_agent_name, test_schema_dict, request):
|
||||
print(f"Warning: Failed to cleanup existing agent: {e}")
|
||||
|
||||
agent = llama_extract.create_agent(name=name, data_schema=schema)
|
||||
yield agent
|
||||
|
||||
# Cleanup after test
|
||||
try:
|
||||
llama_extract.delete_agent(agent.id)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to delete agent {agent.id}: {e}")
|
||||
# Add agent to cleanup list via conftest helper
|
||||
register_agent_for_cleanup(agent.id)
|
||||
|
||||
yield agent
|
||||
|
||||
|
||||
class TestLlamaExtract:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -96,10 +104,12 @@ async def test_link_parse_result(file_path: str):
|
||||
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)
|
||||
@@ -142,8 +152,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 +165,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
|
||||
|
||||
@@ -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.
@@ -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
|
||||
Reference in New Issue
Block a user