Compare commits

...

19 Commits

Author SHA1 Message Date
Adrian Lyjak 837cb7844f fix tests 2025-07-29 18:17:02 -04:00
Adrian Lyjak 37ce4e48c1 bump versions 2025-07-29 17:53:47 -04:00
Adrian Lyjak 9e9e9540d6 lint fix 2025-07-29 16:54:48 -04:00
Adrian Lyjak c16408cf38 Add tests 2025-07-29 16:27:37 -04:00
Adrian Lyjak 07b4e9d71b huh 2025-07-29 16:16:48 -04:00
Javier Torres cf879a1a58 Bump llama-cloud version (#814) 2025-07-28 16:06:31 -05:00
Tuana Çelik fcdf2ab63e Fixes to multimodal report generation (#809) 2025-07-23 16:28:53 -06:00
Adrian Lyjak 083d8109c2 Make versioning a little easier, and fix llama_parse version (#808)
* Make versioning a little easier

* fix up ci
2025-07-21 18:49:07 -04:00
Adrian Lyjak 89cfc8b25f feat: default to _public agent data (#803)
* feat: default to _public agent data
* version bump
2025-07-21 15:58:03 -04:00
Peter Rowlands (변기호) c46e157f92 parse: expose preserve_very_small_text option (#806) 2025-07-21 14:19:15 +09:00
Peter Rowlands (변기호) 05d6026d37 bump version to v0.6.50 (#802) 2025-07-18 18:59:25 +09:00
Peter Rowlands (변기호) 8e98d5c146 parse: expose functionality to get raw job results (#801)
* add LlamaParse.get_result()

* add JobResult.get_text/get_markdown/get_json

* add tests
2025-07-18 18:50:29 +09:00
Adrian Lyjak 3f311c0669 Bump v0.6.49 (#797) 2025-07-16 19:42:09 -04:00
Adrian Lyjak b1a2f9d42b Add new method to fetch the full, non-paginated markdown (#796)
Add new method to fetch the full, non-paginated markdown for proper merge_tables_across_pages_in_markdown support
2025-07-16 19:29:57 -04:00
Neeraj Pradhan 142f55c94c Update to version 0.6.48 (#795)
* Update to version 0.6.48

* pin version

* poetry lock

* adjust warnings

* collect all agents for cleanup
2025-07-16 13:24:44 -07:00
Clelia (Astra) Bertelli 230a110e52 chore: vbump to 0.6.47 and example notebook (#794)
* chore: vbump to 0.6.47 and example notebook

* chore: update llama-parse pyproject.toml
2025-07-16 19:08:44 +02:00
Clelia (Astra) Bertelli 83e2b031cd feat: add table extraction for LlamaParse as CSV files (#793)
* feat: add table extraction for LlamaParse as CSV files

* chore: poetry lock

* chore: add tests

* fix: handle the case where no tables are present

* chore: implement suggestions
2025-07-16 17:08:09 +02:00
Adrian Lyjak 4844e26e5c Improve Agent Data interface, and add file related fields to extracted data for file tracking (#785)
Add file related fields for file tracking. Simplify API
2025-07-09 14:27:24 -04:00
Pierre-Loic Doulcet 70a049af3c merge_tables_across_pages_in_markdown parse parameter (#786)
* merge_tables_across_pages_in_markdown parse parameter

* base.py
2025-07-09 19:03:48 +02:00
21 changed files with 1897 additions and 471 deletions
+21
View File
@@ -0,0 +1,21 @@
# Installation
This project uses poetry. Create a virtual environment, and run `poetry install`
# Versioning (Maintainers only)
Before merging your changes, make sure to bump the versions.
Make a version bump to `pyproject.toml`. If the underlying dependency on the llamacloud platform OpenAPI
sdk needs bumping, make sure to bring that in as well. If updating dependencies, run `poetry lock`.
The legacy `llama_parse` package re-exports some of `llama_cloud_services` in the old namespace. The
versions need to be kept consistent to sidecar it with `llama_cloud_services`. Bump it's version in `llama_parse/pyproject.toml`, and also bump it's dependency version of `llama-cloud-services` to match.
**Note**: Don't worry about updating the `llama_parse/poetry.lock` file when bumping versions. The GitHub action will automatically run `poetry lock` for the llama_parse package during the build process (though it doesn't commit the updated lockfile back to the repo).
You can also do this with `./scripts/version-bump.py set 0.x.x` if you have `uv` installed.
Once the change is merged, push a tag `git tag -a v0.x.x -m 0.x.x` and `git push origin 0.x.x`.
This tagging step can be done with `./scripts/version-bump tag`.
+516
View File
@@ -0,0 +1,516 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Table Extraction with LlamaParse\n",
"\n",
"This notebook will show you how to extract tables and save them as CSV files thanks to LlamaParse advanced parsing capabilities."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**1. Install needed dependencies**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"! pip install llama-cloud-services pandas"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**2. Set you LLAMA_CLOUD_API_KEY as env variable**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"LLAMA_CLOUD_API_KEY: ··········\n"
]
}
],
"source": [
"import os\n",
"from getpass import getpass\n",
"\n",
"os.environ[\"LLAMA_CLOUD_API_KEY\"] = getpass(\"LLAMA_CLOUD_API_KEY: \")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**3. Initialiaze the parser**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from llama_cloud_services import LlamaParse\n",
"\n",
"parser = LlamaParse(result_type=\"markdown\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**4. Get data**\n",
"\n",
"This is a PDF with _lots_ of tables!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--2025-07-16 16:20:41-- https://assets.accessible-digital-documents.com/uploads/2017/01/sample-tables.pdf\n",
"Resolving assets.accessible-digital-documents.com (assets.accessible-digital-documents.com)... 3.166.135.2, 3.166.135.62, 3.166.135.51, ...\n",
"Connecting to assets.accessible-digital-documents.com (assets.accessible-digital-documents.com)|3.166.135.2|:443... connected.\n",
"HTTP request sent, awaiting response... 200 OK\n",
"Length: 145494 (142K) [application/pdf]\n",
"Saving to: sample-tables.pdf\n",
"\n",
"sample-tables.pdf 100%[===================>] 142.08K --.-KB/s in 0.04s \n",
"\n",
"2025-07-16 16:20:41 (3.72 MB/s) - sample-tables.pdf saved [145494/145494]\n",
"\n"
]
}
],
"source": [
"! wget https://assets.accessible-digital-documents.com/uploads/2017/01/sample-tables.pdf"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**5. Parse document**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Started parsing the file under job_id b53949f7-9017-4b6a-b30c-be6227271ed2\n"
]
}
],
"source": [
"json_result = parser.get_json_result(\"sample-tables.pdf\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**6. Get tables!**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"tables = parser.get_tables(json_result, \"tables/\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**7. Load tables**\n",
"\n",
"Let's show one example table!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.google.colaboratory.intrinsic+json": {
"summary": "{\n \"name\": \"display(df\",\n \"rows\": 8,\n \"fields\": [\n {\n \"column\": \"Rainfall\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Average\",\n \"\",\n \"24 hour high\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Americas\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 908,\n \"min\": 9,\n \"max\": 2010,\n \"num_unique_values\": 8,\n \"samples\": [\n 104,\n 133,\n 2010\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Asia\",\n \"properties\": {\n \"dtype\": \"object\",\n \"num_unique_values\": 7,\n \"samples\": [\n \"\",\n 201.0,\n 28.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Europe\",\n \"properties\": {\n \"dtype\": \"object\",\n \"num_unique_values\": 7,\n \"samples\": [\n \"\",\n 193.0,\n 29.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Africa\",\n \"properties\": {\n \"dtype\": \"object\",\n \"num_unique_values\": 7,\n \"samples\": [\n \"\",\n 144.0,\n 20.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}",
"type": "dataframe"
},
"text/html": [
"\n",
" <div id=\"df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb\" class=\"colab-df-container\">\n",
" <div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Rainfall</th>\n",
" <th>Americas</th>\n",
" <th>Asia</th>\n",
" <th>Europe</th>\n",
" <th>Africa</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>(inches)</td>\n",
" <td>2010</td>\n",
" <td></td>\n",
" <td></td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>Average</td>\n",
" <td>104</td>\n",
" <td>201.0</td>\n",
" <td>193.0</td>\n",
" <td>144.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>24 hour high</td>\n",
" <td>15</td>\n",
" <td>26.0</td>\n",
" <td>27.0</td>\n",
" <td>18.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>12 hour high</td>\n",
" <td>9</td>\n",
" <td>10.0</td>\n",
" <td>11.0</td>\n",
" <td>12.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td></td>\n",
" <td>2009</td>\n",
" <td></td>\n",
" <td></td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>Average</td>\n",
" <td>133</td>\n",
" <td>244.0</td>\n",
" <td>155.0</td>\n",
" <td>166.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>24 hour high</td>\n",
" <td>27</td>\n",
" <td>28.0</td>\n",
" <td>29.0</td>\n",
" <td>20.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>12 hour high</td>\n",
" <td>11</td>\n",
" <td>12.0</td>\n",
" <td>13.0</td>\n",
" <td>16.0</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>\n",
" <div class=\"colab-df-buttons\">\n",
"\n",
" <div class=\"colab-df-container\">\n",
" <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb')\"\n",
" title=\"Convert this dataframe to an interactive table.\"\n",
" style=\"display:none;\">\n",
"\n",
" <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
" <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
" </svg>\n",
" </button>\n",
"\n",
" <style>\n",
" .colab-df-container {\n",
" display:flex;\n",
" gap: 12px;\n",
" }\n",
"\n",
" .colab-df-convert {\n",
" background-color: #E8F0FE;\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: #1967D2;\n",
" height: 32px;\n",
" padding: 0 0 0 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-convert:hover {\n",
" background-color: #E2EBFA;\n",
" box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: #174EA6;\n",
" }\n",
"\n",
" .colab-df-buttons div {\n",
" margin-bottom: 4px;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert {\n",
" background-color: #3B4455;\n",
" fill: #D2E3FC;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert:hover {\n",
" background-color: #434B5C;\n",
" box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
" filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
" fill: #FFFFFF;\n",
" }\n",
" </style>\n",
"\n",
" <script>\n",
" const buttonEl =\n",
" document.querySelector('#df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb button.colab-df-convert');\n",
" buttonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
"\n",
" async function convertToInteractive(key) {\n",
" const element = document.querySelector('#df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb');\n",
" const dataTable =\n",
" await google.colab.kernel.invokeFunction('convertToInteractive',\n",
" [key], {});\n",
" if (!dataTable) return;\n",
"\n",
" const docLinkHtml = 'Like what you see? Visit the ' +\n",
" '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
" + ' to learn more about interactive tables.';\n",
" element.innerHTML = '';\n",
" dataTable['output_type'] = 'display_data';\n",
" await google.colab.output.renderOutput(dataTable, element);\n",
" const docLink = document.createElement('div');\n",
" docLink.innerHTML = docLinkHtml;\n",
" element.appendChild(docLink);\n",
" }\n",
" </script>\n",
" </div>\n",
"\n",
"\n",
" <div id=\"df-54b2aa43-838b-47d3-9209-2fb18153cf87\">\n",
" <button class=\"colab-df-quickchart\" onclick=\"quickchart('df-54b2aa43-838b-47d3-9209-2fb18153cf87')\"\n",
" title=\"Suggest charts\"\n",
" style=\"display:none;\">\n",
"\n",
"<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
" width=\"24px\">\n",
" <g>\n",
" <path d=\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z\"/>\n",
" </g>\n",
"</svg>\n",
" </button>\n",
"\n",
"<style>\n",
" .colab-df-quickchart {\n",
" --bg-color: #E8F0FE;\n",
" --fill-color: #1967D2;\n",
" --hover-bg-color: #E2EBFA;\n",
" --hover-fill-color: #174EA6;\n",
" --disabled-fill-color: #AAA;\n",
" --disabled-bg-color: #DDD;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-quickchart {\n",
" --bg-color: #3B4455;\n",
" --fill-color: #D2E3FC;\n",
" --hover-bg-color: #434B5C;\n",
" --hover-fill-color: #FFFFFF;\n",
" --disabled-bg-color: #3B4455;\n",
" --disabled-fill-color: #666;\n",
" }\n",
"\n",
" .colab-df-quickchart {\n",
" background-color: var(--bg-color);\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: var(--fill-color);\n",
" height: 32px;\n",
" padding: 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-quickchart:hover {\n",
" background-color: var(--hover-bg-color);\n",
" box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: var(--button-hover-fill-color);\n",
" }\n",
"\n",
" .colab-df-quickchart-complete:disabled,\n",
" .colab-df-quickchart-complete:disabled:hover {\n",
" background-color: var(--disabled-bg-color);\n",
" fill: var(--disabled-fill-color);\n",
" box-shadow: none;\n",
" }\n",
"\n",
" .colab-df-spinner {\n",
" border: 2px solid var(--fill-color);\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" animation:\n",
" spin 1s steps(1) infinite;\n",
" }\n",
"\n",
" @keyframes spin {\n",
" 0% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" border-left-color: var(--fill-color);\n",
" }\n",
" 20% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 30% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 40% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 60% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 80% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" 90% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" }\n",
"</style>\n",
"\n",
" <script>\n",
" async function quickchart(key) {\n",
" const quickchartButtonEl =\n",
" document.querySelector('#' + key + ' button');\n",
" quickchartButtonEl.disabled = true; // To prevent multiple clicks.\n",
" quickchartButtonEl.classList.add('colab-df-spinner');\n",
" try {\n",
" const charts = await google.colab.kernel.invokeFunction(\n",
" 'suggestCharts', [key], {});\n",
" } catch (error) {\n",
" console.error('Error during call to suggestCharts:', error);\n",
" }\n",
" quickchartButtonEl.classList.remove('colab-df-spinner');\n",
" quickchartButtonEl.classList.add('colab-df-quickchart-complete');\n",
" }\n",
" (() => {\n",
" let quickchartButtonEl =\n",
" document.querySelector('#df-54b2aa43-838b-47d3-9209-2fb18153cf87 button');\n",
" quickchartButtonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
" })();\n",
" </script>\n",
" </div>\n",
"\n",
" </div>\n",
" </div>\n"
],
"text/plain": [
" Rainfall Americas Asia Europe Africa\n",
"0 (inches) 2010 \n",
"1 Average 104 201.0 193.0 144.0\n",
"2 24 hour high 15 26.0 27.0 18.0\n",
"3 12 hour high 9 10.0 11.0 12.0\n",
"4 2009 \n",
"5 Average 133 244.0 155.0 166.0\n",
"6 24 hour high 27 28.0 29.0 20.0\n",
"7 12 hour high 11 12.0 13.0 16.0"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import pandas as pd\n",
"from IPython.display import display\n",
"\n",
"df = pd.read_csv(\n",
" \"/content/tables/table_2025_16_07_16_30_01_569.csv\",\n",
")\n",
"display(df.fillna(\"\"))"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
File diff suppressed because one or more lines are too long
@@ -5,6 +5,12 @@ from .schema import (
StatusType,
ExtractedT,
AgentDataT,
ComparisonOperator,
parse_extracted_field_metadata,
calculate_overall_confidence,
InvalidExtractionData,
ExtractedFieldMetadata,
ExtractedFieldMetaDataDict,
)
from .client import AsyncAgentDataClient
@@ -16,4 +22,10 @@ __all__ = [
"StatusType",
"ExtractedT",
"AgentDataT",
"ComparisonOperator",
"parse_extracted_field_metadata",
"calculate_overall_confidence",
"InvalidExtractionData",
"ExtractedFieldMetadata",
"ExtractedFieldMetaDataDict",
]
+41 -34
View File
@@ -1,7 +1,6 @@
import os
from typing import Dict, Generic, List, Optional, Type
from typing import Any, Dict, Generic, List, Optional, Type
from llama_cloud import FilterOperation
from llama_cloud.client import AsyncLlamaCloud
from tenacity import (
WrappedFn,
@@ -14,6 +13,7 @@ import httpx
from .schema import (
AgentDataT,
ComparisonOperator,
TypedAgentData,
TypedAgentDataItems,
TypedAggregateGroup,
@@ -42,7 +42,7 @@ def agent_data_retry(func: WrappedFn) -> WrappedFn:
)(func)
def get_default_agent_id() -> Optional[str]:
def get_default_agent_id() -> str:
"""
Retrieve the default agent ID from environment variables.
@@ -55,7 +55,7 @@ def get_default_agent_id() -> Optional[str]:
via environment variables instead of passing it explicitly
to each client instance.
"""
return os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME")
return os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME") or "_public"
class AsyncAgentDataClient(Generic[AgentDataT]):
@@ -85,7 +85,7 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
agent_client = AsyncAgentDataClient(
client=llama_client,
type=ExtractedPerson,
collection_name="extracted_people",
collection="extracted_people",
agent_url_id="person-extraction-agent"
)
@@ -94,8 +94,8 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
result = await agent_client.create_agent_data(person)
# Search data
results = await agent_client.search_agent_data(
filter={"age": FilterOperation(gt=25)},
results = await agent_client.search(
filter={"age": {"gt": 25}},
order_by="data.name",
page_size=20
)
@@ -107,19 +107,20 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
def __init__(
self,
client: AsyncLlamaCloud,
type: Type[AgentDataT],
collection_name: str = "default",
collection: str = "default",
agent_url_id: Optional[str] = None,
client: Optional[AsyncLlamaCloud] = None,
token: Optional[str] = None,
base_url: Optional[str] = None,
):
"""
Initialize the AsyncAgentDataClient.
Args:
client: AsyncLlamaCloud client instance for API communication
type: Pydantic BaseModel class that defines the data structure.
All agent data will be validated against this type.
collection_name: Named collection within the agent for organizing data.
collection: Named collection within the agent for organizing data.
Defaults to "default". Collections allow logical separation of
different data types or workflows within the same agent.
agent_url_id: Unique identifier for the agent. This normally appears in the
@@ -127,6 +128,11 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
will attempt to use the LLAMA_DEPLOY_DEPLOYMENT_NAME environment
variable. Data can only be added to an already existing agent in the
platform.
client: AsyncLlamaCloud client instance for API communication. If not provided, will
construct one from the provided api token and base url
token: Llama Cloud API token. Reads from LLAMA_CLOUD_API_KEY if not provided
base_url: Llama Cloud API token. Reads from LLAMA_CLOUD_BASE_URL if not provided, and
defaults to https://api.cloud.llamaindex.ai
Raises:
ValueError: If agent_url_id is not provided and the
@@ -138,33 +144,34 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
"""
self.agent_url_id = agent_url_id or get_default_agent_id()
if not self.agent_url_id:
raise ValueError(
"Agent ID is required, or set the LLAMA_DEPLOY_DEPLOYMENT_NAME environment variable"
)
self.collection_name = collection_name
self.collection = collection
if not client:
client = AsyncLlamaCloud(
token=token or os.getenv("LLAMA_CLOUD_API_KEY"),
base_url=base_url or os.getenv("LLAMA_CLOUD_BASE_URL"),
)
self.client = client
self.type = type
@agent_data_retry
async def get_agent_data(self, item_id: str) -> TypedAgentData[AgentDataT]:
async def get_item(self, item_id: str) -> TypedAgentData[AgentDataT]:
raw_data = await self.client.beta.get_agent_data(
item_id=item_id,
)
return TypedAgentData.from_raw(raw_data, validator=self.type)
@agent_data_retry
async def create_agent_data(self, data: AgentDataT) -> TypedAgentData[AgentDataT]:
async def create_item(self, data: AgentDataT) -> TypedAgentData[AgentDataT]:
raw_data = await self.client.beta.create_agent_data(
agent_slug=self.agent_url_id,
collection=self.collection_name,
collection=self.collection,
data=data.model_dump(),
)
return TypedAgentData.from_raw(raw_data, validator=self.type)
@agent_data_retry
async def update_agent_data(
async def update_item(
self, item_id: str, data: AgentDataT
) -> TypedAgentData[AgentDataT]:
raw_data = await self.client.beta.update_agent_data(
@@ -174,13 +181,13 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
return TypedAgentData.from_raw(raw_data, validator=self.type)
@agent_data_retry
async def delete_agent_data(self, item_id: str) -> None:
async def delete_item(self, item_id: str) -> None:
await self.client.beta.delete_agent_data(item_id=item_id)
@agent_data_retry
async def search_agent_data(
async def search(
self,
filter: Optional[Dict[str, Optional[FilterOperation]]] = None,
filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None,
order_by: Optional[str] = None,
offset: Optional[int] = None,
page_size: Optional[int] = None,
@@ -191,11 +198,11 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
Args:
filter: Filter conditions to apply to the search. Dict mapping field names to FilterOperation objects. Filters only by data fields
Examples:
- {"age": FilterOperation(gt=18)} - age greater than 18
- {"status": FilterOperation(eq="active")} - status equals "active"
- {"tags": FilterOperation(includes=["python", "ml"])} - tags include "python" or "ml"
- {"created_at": FilterOperation(gte="2024-01-01")} - created after date
- {"score": FilterOperation(lt=100, gte=50)} - score between 50 and 100
- {"age": {"gt": 18}} - age greater than 18
- {"status": {"eq": "active"}} - status equals "active"
- {"tags": {"includes": ["python", "ml"]}} - tags include "python" or "ml"
- {"created_at": {"gte": "2024-01-01"}} - created after date
- {"score": {"lt": 100, "gte": 50}} - score between 50 and 100
order_by: Comma delimited list of fields to sort results by. Can order by standard agent fields like created_at, or by data fields. Data fields must be prefixed with "data.". If ordering desceding, use a " desc" suffix.
Examples:
- "data.name desc, created_at" - sort by name in descending order, and then by creation date
@@ -205,7 +212,7 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
"""
raw = await self.client.beta.search_agent_data_api_v_1_beta_agent_data_search_post(
agent_slug=self.agent_url_id,
collection=self.collection_name,
collection=self.collection,
filter=filter,
order_by=order_by,
offset=offset,
@@ -221,9 +228,9 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
)
@agent_data_retry
async def aggregate_agent_data(
async def aggregate(
self,
filter: Optional[Dict[str, Optional[FilterOperation]]] = None,
filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None,
group_by: Optional[List[str]] = None,
count: Optional[bool] = None,
first: Optional[bool] = None,
@@ -235,20 +242,20 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
Aggregate agent data into groups according to the group_by fields.
Args:
filter: Filter conditions to apply to the search. Dict mapping field names to FilterOperation objects. Filters only by data fields
See search_agent_data for more details on filtering.
See search for more details on filtering.
group_by: List of fields to group by. Groups strictly by equality. Can only group by data fields.
Examples:
- ["name"] - group by name
- ["name", "age"] - group by name and age
count: Whether to include the count of items in each group.
first: Whether to include the first item in each group.
order_by: Comma delimited list of fields to sort results by. See search_agent_data for more details on ordering.
order_by: Comma delimited list of fields to sort results by. See search for more details on ordering.
offset: Number of groups to skip from the beginning. Defaults to 0.
page_size: Maximum number of groups to return per page.
"""
raw = await self.client.beta.aggregate_agent_data_api_v_1_beta_agent_data_aggregate_post(
agent_slug=self.agent_url_id,
collection=self.collection_name,
collection=self.collection,
page_size=page_size,
filter=filter,
order_by=order_by,
+224 -18
View File
@@ -37,9 +37,11 @@ Example Usage:
"""
from datetime import datetime
import numbers
from llama_cloud import ExtractRun
from llama_cloud.types.agent_data import AgentData
from llama_cloud.types.aggregate_group import AggregateGroup
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, ValidationError
from typing import (
Generic,
List,
@@ -60,7 +62,11 @@ AgentDataT = TypeVar("AgentDataT", bound=BaseModel)
ExtractedT = TypeVar("ExtractedT", bound=Union[BaseModel, dict])
# Status types for extracted data workflow
StatusType = Union[Literal["error", "accepted", "rejected", "in_review"], str]
StatusType = Union[Literal["error", "accepted", "rejected", "pending_review"], str]
ComparisonOperator = Dict[
str, Dict[Literal["gt", "gte", "lt", "lte", "eq", "includes"], Any]
]
class TypedAgentData(BaseModel, Generic[AgentDataT]):
@@ -142,7 +148,7 @@ class TypedAgentDataItems(BaseModel, Generic[AgentDataT]):
Example:
```python
# Search with pagination
results = await client.search_agent_data(
results = await client.search(
page_size=10,
include_total=True
)
@@ -152,7 +158,7 @@ class TypedAgentDataItems(BaseModel, Generic[AgentDataT]):
if results.has_more:
# Load next page
next_page = await client.search_agent_data(
next_page = await client.search(
page_size=10,
offset=10
)
@@ -170,6 +176,74 @@ class TypedAgentDataItems(BaseModel, Generic[AgentDataT]):
)
class ExtractedFieldMetadata(BaseModel):
"""
Metadata for an extracted data field, such as confidence, and citation information.
"""
confidence: Optional[float] = Field(
None,
description="The confidence score for the field, combined with parsing confidence if applicable",
)
extracted_confidence: Optional[float] = Field(
None,
description="The confidence score for the field based on the extracted text only",
)
page_number: Optional[int] = Field(
None, description="The page number that the field occurred on"
)
matching_text: Optional[str] = Field(
None,
description="The original text this field's value was derived from",
)
ExtractedFieldMetaDataDict = Dict[
str, Union[ExtractedFieldMetadata, Dict[str, Any], list[Any]]
]
def parse_extracted_field_metadata(
field_metadata: dict[str, Any],
) -> ExtractedFieldMetaDataDict:
"""
Parse the extracted field metadata into a dictionary of field names to field metadata.
"""
result: ExtractedFieldMetaDataDict = {}
for field_name, field_value in field_metadata.items():
if isinstance(field_value, ExtractedFieldMetadata):
# support running this multiple times
result[field_name] = field_value
elif isinstance(field_value, dict):
if "confidence" in field_value or "citations" in field_value:
try:
validated = ExtractedFieldMetadata.model_validate(field_value)
# grab the citation from the array. This is just an array for backwards compatibility.
if "citations" in field_value and len(field_value["citations"]) > 0:
first_citation = field_value["citations"][0]
if "page_number" in first_citation and isinstance(
first_citation["page_number"], numbers.Number
):
validated.page_number = int(first_citation["page_number"]) # type: ignore
if "matching_text" in first_citation and isinstance(
first_citation["matching_text"], str
):
validated.matching_text = first_citation["matching_text"]
result[field_name] = validated
continue
except ValidationError:
pass
result[field_name] = parse_extracted_field_metadata(field_value)
elif isinstance(field_value, list):
result[field_name] = [
parse_extracted_field_metadata(item) for item in field_value
]
else:
result[field_name] = field_value
return result
class ExtractedData(BaseModel, Generic[ExtractedT]):
"""
Wrapper for extracted data with workflow status tracking.
@@ -183,9 +257,12 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
data: The current state of the data (may differ from original after edits)
status: Current workflow status (in_review, accepted, rejected, error)
confidence: Confidence scores for individual fields (if available)
file_id: The llamacloud file ID of the file that was used to extract the data
file_name: The name of the file that was used to extract the data
file_hash: A content hash of the file that was used to extract the data, for de-duplication
Status Workflow:
- "in_review": Initial state, awaiting human review
- "pending_review": Initial state, awaiting human review
- "accepted": Data approved and ready for use
- "rejected": Data rejected, needs re-extraction or manual fix
- "error": Processing error occurred
@@ -194,8 +271,8 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
```python
# Create extracted data for review
extracted = ExtractedData.create(
extracted_data=person_data,
status="in_review",
data=person_data,
status="pending_review",
confidence={"name": 0.95, "age": 0.87}
)
@@ -212,20 +289,39 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
data: ExtractedT = Field(
description="The latest state of the data. Will differ if data has been updated"
)
status: Union[Literal["error", "accepted", "rejected", "in_review"], str] = Field(
description="The status of the extracted data"
status: StatusType = Field(description="The status of the extracted data")
overall_confidence: Optional[float] = Field(
None,
description="The overall confidence score for the extracted data",
)
confidence: Dict[str, Union[float, Dict]] = Field(
field_metadata: ExtractedFieldMetaDataDict = Field(
default_factory=dict,
description="Confidence scores, if any, for each primitive field in the original_data data",
description="Page links, and perhaps eventually bounding boxes, for individual fields in the extracted data. Structure is expected to have a ",
)
file_id: Optional[str] = Field(
None, description="The ID of the file that was used to extract the data"
)
file_name: Optional[str] = Field(
None, description="The name of the file that was used to extract the data"
)
file_hash: Optional[str] = Field(
None, description="The hash of the file that was used to extract the data"
)
metadata: Optional[Dict[str, Any]] = Field(
default_factory=dict,
description="Additional metadata about the extracted data, such as errors, tokens, etc.",
)
@classmethod
def create(
cls,
extracted_data: ExtractedT,
status: StatusType = "in_review",
confidence: Optional[Dict[str, Union[float, Dict]]] = None,
data: ExtractedT,
status: StatusType = "pending_review",
field_metadata: ExtractedFieldMetaDataDict = {},
file_id: Optional[str] = None,
file_name: Optional[str] = None,
file_hash: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> "ExtractedData[ExtractedT]":
"""
Create a new ExtractedData instance with sensible defaults.
@@ -233,18 +329,128 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
Args:
extracted_data: The extracted data payload
status: Initial workflow status
confidence: Optional confidence scores for fields
field_metadata: Optional confidence scores, citations, and other metadata for fields
file_id: The llamacloud file ID of the file that was used to extract the data
file_name: The name of the file that was used to extract the data
file_hash: A content hash of the file that was used to extract the data, for de-duplication
metadata: Arbitrary additional application-specific data about the extracted data
Returns:
New ExtractedData instance ready for storage
"""
normalized_field_metadata = parse_extracted_field_metadata(field_metadata)
return cls(
original_data=extracted_data,
data=extracted_data,
original_data=data,
data=data,
status=status,
confidence=confidence or {},
field_metadata=normalized_field_metadata,
overall_confidence=calculate_overall_confidence(normalized_field_metadata),
file_id=file_id,
file_name=file_name,
file_hash=file_hash,
metadata=metadata or {},
)
@classmethod
def from_extraction_result(
cls,
result: ExtractRun,
schema: Type[ExtractedT],
file_hash: Optional[str] = None,
file_name: Optional[str] = None,
file_id: Optional[str] = None,
status: StatusType = "pending_review",
metadata: Optional[Dict[str, Any]] = None,
) -> "ExtractedData[ExtractedT]":
"""
Create an ExtractedData instance from an extraction result.
"""
file_id = file_id or result.file.id
file_name = file_name or result.file.name
try:
field_metadata = parse_extracted_field_metadata(
result.extraction_metadata.get("field_metadata", {})
)
except ValidationError:
field_metadata = {}
try:
data = schema.model_validate(result.data) # type: ignore
return cls.create(
data=data,
status=status,
field_metadata=field_metadata,
file_id=file_id,
file_name=file_name,
file_hash=file_hash,
metadata=metadata or {},
)
except ValidationError as e:
invalid_item = ExtractedData[Dict[str, Any]].create(
data=result.data or {},
status="error",
field_metadata=field_metadata,
metadata={"extraction_error": str(e), **(metadata or {})},
file_id=file_id,
file_name=file_name,
file_hash=file_hash,
)
raise InvalidExtractionData(invalid_item) from e
class InvalidExtractionData(Exception):
"""
Exception raised when the extracted data does not conform to the schema.
"""
def __init__(self, invalid_item: ExtractedData[Dict[str, Any]]):
self.invalid_item = invalid_item
super().__init__("Not able to parse the extracted data, parsed invalid format")
def calculate_overall_confidence(
metadata: ExtractedFieldMetaDataDict,
) -> Optional[float]:
"""
Calculate the overall confidence score for the extracted data.
"""
numerator, denominator = _calculate_overall_confidence_recursive(metadata)
if denominator == 0:
return None
return numerator / denominator
def _calculate_overall_confidence_recursive(
confidence: Union[ExtractedFieldMetadata, Dict[str, Any], list[Any]],
) -> tuple[float, int]:
"""
Calculate the overall confidence score for the extracted data.
"""
if isinstance(confidence, ExtractedFieldMetadata):
if confidence.confidence is not None:
return confidence.confidence, 1
else:
return 0, 0
if isinstance(confidence, dict):
numerator: float = 0
denominator: int = 0
for value in confidence.values():
num, den = _calculate_overall_confidence_recursive(value)
numerator += num
denominator += den
return numerator, denominator
elif isinstance(confidence, list):
numerator = 0
denominator = 0
for value in confidence:
num, den = _calculate_overall_confidence_recursive(value)
numerator += num
denominator += den
return numerator, denominator
else:
return 0, 0
class TypedAggregateGroup(BaseModel, Generic[AgentDataT]):
"""
+2 -8
View File
@@ -137,15 +137,9 @@ def run_in_thread(
def _extraction_config_warning(config: ExtractConfig) -> None:
if config.use_reasoning:
if config.cite_sources or config.confidence_scores:
warnings.warn(
"`use_reasoning` is an experimental feature. Results will be available in "
"the `extraction_metadata` field for the extraction run.",
ExperimentalWarning,
)
if config.cite_sources:
warnings.warn(
"`cite_sources` is an experimental feature. This may greatly increase the "
"`cite_sources`/`confidence_scores` could greatly increase the "
"size of the response, and slow down the extraction. Results will be "
"available in the `extraction_metadata` field for the extraction run.",
ExperimentalWarning,
+103
View File
@@ -37,6 +37,7 @@ from llama_cloud_services.parse.utils import (
nest_asyncio_msg,
make_api_request,
partition_pages,
extract_tables_from_json_results,
)
# can put in a path to the file or the file bytes itself
@@ -324,6 +325,10 @@ class LlamaParse(BasePydanticReader):
default=None,
description="The maximum number of pages to extract text from documents. If set to 0 or not set, all pages will be that should be extracted will be extracted (can work in combination with targetPages).",
)
merge_tables_across_pages_in_markdown: Optional[bool] = Field(
default=False,
description="If set to true, the parser will merge tables across pages in the markdown output. This is useful for documents with tables that span across multiple pages.",
)
output_pdf_of_document: Optional[bool] = Field(
default=False,
description="If set to true, the parser will also output a PDF of the document. (except for spreadsheets)",
@@ -376,6 +381,10 @@ class LlamaParse(BasePydanticReader):
default=False,
description="Preserve grid alignment across page in text mode.",
)
preserve_very_small_text: Optional[bool] = Field(
default=False,
description="If set, the parser will try to preserve very small text lines. This can be useful for documents containing vector graphics with very small text lines that may not be recognized by OCR or a vision model (such as in CAD drawings).",
)
replace_failed_page_mode: Optional[FailedPageMode] = Field(
default=None,
description="The mode to use to replace the failed page, see FailedPageMode enum for possible value. If set, the parser will replace the failed page with the specified mode. If not set, the default mode (raw_text) will be used.",
@@ -859,6 +868,11 @@ class LlamaParse(BasePydanticReader):
if self.page_prefix is not None:
data["page_prefix"] = self.page_prefix
if self.merge_tables_across_pages_in_markdown:
data[
"merge_tables_across_pages_in_markdown"
] = self.merge_tables_across_pages_in_markdown
if self.hide_headers:
data["hide_headers"] = self.hide_headers
@@ -902,6 +916,9 @@ class LlamaParse(BasePydanticReader):
"preserve_layout_alignment_across_pages"
] = self.preserve_layout_alignment_across_pages
if self.preserve_very_small_text:
data["preserve_very_small_text"] = self.preserve_very_small_text
if self.preset is not None:
data["preset"] = self.preset
@@ -1611,6 +1628,16 @@ class LlamaParse(BasePydanticReader):
else:
raise e
def get_tables(self, json_results: List[dict], download_path: str) -> List[str]:
if not os.path.exists(download_path):
os.makedirs(download_path)
return extract_tables_from_json_results(json_results, download_path)
async def aget_tables(
self, json_result: List[dict], download_path: str
) -> List[str]:
return await asyncio.to_thread(self.get_tables, json_result, download_path)
async def aget_xlsx(
self, json_result: List[dict], download_path: str
) -> List[dict]:
@@ -1677,3 +1704,79 @@ class LlamaParse(BasePydanticReader):
sub_docs.append(sub_doc)
return sub_docs
async def aget_result(
self, job_id: Union[str, List[str]]
) -> Union[JobResult, List[JobResult]]:
"""
Return JobResult object for previously parsed job(s).
If the job is still pending, the result will not be returned until it is completed.
Args:
job_id: Job ID or list of multiple Job IDs to be retrieved.
Returns:
JobResult object or list of JobResult objects if multiple job IDs were provided.
"""
if isinstance(job_id, str):
result = await self._get_job_result(
job_id, ResultType.JSON.value, verbose=self.verbose
)
return JobResult(
job_id=job_id,
file_name="",
job_result=result,
api_key=self.api_key,
base_url=self.base_url,
client=self.aclient,
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
)
elif isinstance(job_id, list):
results = []
jobs = [
self._get_job_result(id_, ResultType.JSON.value, verbose=self.verbose)
for id_ in job_id
]
results = await run_jobs(
jobs,
workers=self.num_workers,
desc="Getting job results",
show_progress=self.show_progress,
)
return [
JobResult(
job_id=job_id[i],
file_name="",
job_result=result,
api_key=self.api_key,
base_url=self.base_url,
client=self.aclient,
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
)
for i, result in enumerate(results)
]
else:
raise ValueError("The input job_id must be a string or a list of strings.")
def get_result(
self, job_id: Union[str, List[str]]
) -> Union[JobResult, List[JobResult]]:
"""
Return JobResult object for previously parsed job(s).
If the job is still pending, the result will not be returned until it is completed.
Args:
job_id: Job ID or list of multiple Job IDs to be retrieved.
Returns:
JobResult object or list of JobResult objects if multiple job IDs were provided.
"""
try:
return asyncio_run(self.aget_result(job_id))
except RuntimeError as e:
if nest_asyncio_err in str(e):
raise RuntimeError(nest_asyncio_msg)
else:
raise e
+52 -1
View File
@@ -295,13 +295,64 @@ class JobResult(BaseModel):
async def aget_markdown_nodes(self, split_by_page: bool = False) -> List[TextNode]:
"""
Get the markdown nodes from the job.
Args:
split_by_page: Whether to split the pages into separate documents
"""
documents = await self.aget_markdown_documents(split_by_page)
return [TextNode(text=doc.text, metadata=doc.metadata) for doc in documents]
def get_markdown(self) -> str:
"""
Get the raw parsed markdown from the job, distinct from the markdown documents.
This does not include page separators, e.g. if merge_tables_across_pages_in_markdown is True
"""
return asyncio_run(self.aget_markdown())
async def aget_markdown(self) -> str:
"""
Get the raw parsed markdown from the job, distinct from the markdown documents.
This does not include page separators, e.g. if merge_tables_across_pages_in_markdown is True
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/raw/markdown"
response = await make_api_request(self._client, "GET", url)
return response.content.decode("utf-8")
def get_text(self) -> str:
"""
Get the raw parsed text from the job.
"""
return asyncio_run(self.aget_text())
async def aget_text(self) -> str:
"""
Get the raw parsed text from the job.
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/raw/text"
response = await make_api_request(self._client, "GET", url)
return response.content.decode("utf-8")
def get_json(self) -> Dict[str, Any]:
"""
Get the full parsed JSON result from the job.
Note:
This is not the same as JobResult.json(), which is a
JSON serialized version of the JobResult Page Documents.
"""
return asyncio_run(self.aget_json())
async def aget_json(self) -> Dict[str, Any]:
"""
Get the full parsed JSON result from the job.
Note:
This is not the same as JobResult.json(), which is a
JSON serialized version of the JobResult Page Documents.
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/json"
response = await make_api_request(self._client, "GET", url)
return response.json()
async def _get_image_document_with_bytes(
self, image: ImageItem, page: Page
) -> ImageDocument:
+31 -1
View File
@@ -1,7 +1,10 @@
import httpx
import itertools
import logging
import os
from enum import Enum
from pathlib import Path
from datetime import datetime
from tenacity import (
retry,
stop_after_attempt,
@@ -9,7 +12,7 @@ from tenacity import (
retry_if_exception,
before_sleep_log,
)
from typing import Any, Iterable, Iterator, Optional
from typing import Any, Iterable, Iterator, Optional, List, cast
logger = logging.getLogger(__name__)
@@ -351,3 +354,30 @@ def partition_pages(
yield ",".join(targets)
else:
return
def extract_tables_from_json_results(
json_results: List[dict], download_path: str
) -> List[str]:
tables = []
for json_result in json_results:
pages = json_result["pages"]
for page in pages:
page = cast(dict, page)
items = page.get("items", [])
if items:
for i, item in enumerate(items):
item = cast(dict, item)
if item.get("type", "") == "table" and item.get("csv", ""):
savepath = os.path.join(
download_path,
f"table_{datetime.now().strftime('%Y_%d_%m_%H_%M_%S_%f')[:-3]}.csv",
)
if Path(savepath).exists():
savepath = (
savepath.replace(".csv", "_")[0] + str(i) + ".csv"
)
with open(savepath, "w") as f:
f.write(item["csv"])
tables.append(savepath)
return tables
+20 -19
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.3 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", "dev"]
groups = ["main"]
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", "dev"]
groups = ["main"]
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.10\""
markers = "python_version == \"3.9\""
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"},
@@ -896,7 +896,7 @@ version = "0.16.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
groups = ["main"]
files = [
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
@@ -955,7 +955,7 @@ version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
groups = ["main", "dev"]
groups = ["main"]
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.10\""
markers = "python_version == \"3.9\""
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.23"
version = "0.1.34"
description = ""
optional = false
python-versions = "<4,>=3.8"
groups = ["main"]
files = [
{file = "llama_cloud-0.1.23-py3-none-any.whl", hash = "sha256:ce95b0705d85c99b3b27b0af0d16a17d9a81b14c96bf13c1063a1bd13d8d0446"},
{file = "llama_cloud-0.1.23.tar.gz", hash = "sha256:3d84a24a860f046d39a106c06742ec0ea39a574ac42bbf91706fe025f44e233e"},
{file = "llama_cloud-0.1.34-py3-none-any.whl", hash = "sha256:9b06fb109b1d9f652095a11732ae3dbe84e48cc00c580b2eeb19e71e901267be"},
{file = "llama_cloud-0.1.34.tar.gz", hash = "sha256:6866e4bab47d2c1840bdf169c13c06176931c1d30697ac1fa71bab7942a041e9"},
]
[package.dependencies]
@@ -1187,24 +1187,25 @@ pydantic = ">=1.10"
[[package]]
name = "llama-cloud-services"
version = "0.6.30"
version = "0.6.48"
description = "Tailored SDK clients for LlamaCloud services."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{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"},
{file = "llama_cloud_services-0.6.48-py3-none-any.whl", hash = "sha256:5faadcfa270f3be516dd7d3568cbb54b3014b2c8878c5bd7d3252ab1bd635869"},
{file = "llama_cloud_services-0.6.48.tar.gz", hash = "sha256:64fe8d8ce7434d6a481646c35be55973ae10765d20b6be0067a262e6be61f2df"},
]
[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.23"
llama-cloud = "0.1.34"
llama-index-core = ">=0.12.0"
platformdirs = ">=4.3.7,<5.0.0"
pydantic = ">=2.8,<2.10 || >2.10"
python-dotenv = ">=1.0.1,<2.0.0"
tenacity = ">=8.5.0,<10.0"
[[package]]
name = "llama-index-core"
@@ -2473,7 +2474,7 @@ version = "2.32.4"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
groups = ["main", "dev"]
groups = ["main"]
files = [
{file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"},
{file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"},
@@ -2807,7 +2808,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.10\""}
markers = {dev = "python_version == \"3.9\""}
[[package]]
name = "typing-inspect"
@@ -2846,7 +2847,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", "dev"]
groups = ["main"]
files = [
{file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"},
{file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"},
@@ -3068,7 +3069,7 @@ description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
markers = "python_version < \"3.10\""
markers = "python_version == \"3.9\""
files = [
{file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"},
{file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"},
@@ -3085,4 +3086,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9,<4.0"
content-hash = "1be767f62c8a8e1c9acb76347055a837ea2c44130004703361d0303b049bede7"
content-hash = "0b5ab34c44ce7802b02e01b6535b9bb0e08f99f26a43a76be88534dd8a417666"
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "llama-parse"
version = "0.6.44"
version = "0.6.53"
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.44"
llama-cloud-services = ">=0.6.53"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
Generated
+4 -4
View File
@@ -1925,14 +1925,14 @@ rapidfuzz = ">=3.9.0,<4.0.0"
[[package]]
name = "llama-cloud"
version = "0.1.33"
version = "0.1.35"
description = ""
optional = false
python-versions = "<4,>=3.8"
groups = ["main"]
files = [
{file = "llama_cloud-0.1.33-py3-none-any.whl", hash = "sha256:35b7d4a30b013f0a343f7e09126b531c697d65bffd4eb4d2d79bf7d65f256178"},
{file = "llama_cloud-0.1.33.tar.gz", hash = "sha256:a0bb900d5a6e86f8c767b48686c5253679ad7ca1b57612dc39b0767e57ad3d78"},
{file = "llama_cloud-0.1.35-py3-none-any.whl", hash = "sha256:b7abab4423118e6f638d2f326749e7a07c6426543bea6da99b623c715b22af71"},
{file = "llama_cloud-0.1.35.tar.gz", hash = "sha256:200349d5d57424d7461f304cdb1355a58eea3e6ca1e6b0d75c66b2e937216983"},
]
[package.dependencies]
@@ -4623,4 +4623,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9,<4.0"
content-hash = "487717a0bbe7ff67360e3e9f187e15a33c77e4942a7c909cb37b95425a81544c"
content-hash = "025b27407a221b35c3d970294ba1fd734d77a7742d41a317d6b62e08022376ae"
+2 -2
View File
@@ -8,7 +8,7 @@ python_version = "3.10"
[tool.poetry]
name = "llama-cloud-services"
version = "0.6.44"
version = "0.6.53"
description = "Tailored SDK clients for LlamaCloud services."
authors = ["Logan Markewich <logan@runllama.ai>"]
license = "MIT"
@@ -18,7 +18,7 @@ packages = [{include = "llama_cloud_services"}]
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
llama-index-core = ">=0.12.0"
llama-cloud = "==0.1.33"
llama-cloud = "==0.1.35"
pydantic = ">=2.8,!=2.10"
click = "^8.1.7"
python-dotenv = "^1.0.1"
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["click", "tomlkit"]
# ///
import click
import subprocess
import sys
import tomlkit
from pathlib import Path
def get_current_versions() -> tuple[str, str, str]:
"""Get current versions from both pyproject.toml files."""
# Read main pyproject.toml
main_content = Path("pyproject.toml").read_text()
main_doc = tomlkit.parse(main_content)
main_version = main_doc["tool"]["poetry"]["version"]
# Read llama_parse/pyproject.toml
llama_parse_content = Path("llama_parse/pyproject.toml").read_text()
llama_parse_doc = tomlkit.parse(llama_parse_content)
llama_parse_version = llama_parse_doc["tool"]["poetry"]["version"]
dependency_version = llama_parse_doc["tool"]["poetry"]["dependencies"][
"llama-cloud-services"
]
return str(main_version), str(llama_parse_version), str(dependency_version)
def validate_versions(
main_version: str, llama_parse_version: str, dependency_version: str
) -> list[str]:
"""Validate that versions are consistent and return warnings."""
warnings = []
if main_version != llama_parse_version:
warnings.append(
f"Version mismatch: main={main_version}, llama_parse={llama_parse_version}"
)
# Extract version from dependency string (e.g., ">=0.6.51" -> "0.6.51")
if dependency_version and dependency_version.startswith(">="):
dep_ver = dependency_version[2:]
if dep_ver != main_version:
warnings.append(
f"Dependency version mismatch: dependency={dep_ver}, main={main_version}"
)
return warnings
def set_version(version: str) -> None:
"""Set version across all pyproject.toml files using tomlkit to preserve formatting."""
# Update main pyproject.toml
main_content = Path("pyproject.toml").read_text()
main_doc = tomlkit.parse(main_content)
main_doc["tool"]["poetry"]["version"] = version
Path("pyproject.toml").write_text(tomlkit.dumps(main_doc))
# Update llama_parse/pyproject.toml
llama_parse_content = Path("llama_parse/pyproject.toml").read_text()
llama_parse_doc = tomlkit.parse(llama_parse_content)
llama_parse_doc["tool"]["poetry"]["version"] = version
llama_parse_doc["tool"]["poetry"]["dependencies"][
"llama-cloud-services"
] = f">={version}"
Path("llama_parse/pyproject.toml").write_text(tomlkit.dumps(llama_parse_doc))
click.echo(f"Updated all versions to {version}")
def get_current_branch() -> str:
"""Get the current git branch."""
result = subprocess.run(
["git", "branch", "--show-current"], capture_output=True, text=True, check=True
)
return result.stdout.strip()
def create_and_push_tag(version: str) -> None:
"""Create a git tag and push it."""
current_branch = get_current_branch()
if current_branch != "main":
click.echo(
f"Error: Not on main branch (currently on {current_branch})", err=True
)
sys.exit(1)
tag_name = f"v{version}"
# Create tag
subprocess.run(["git", "tag", tag_name], check=True)
click.echo(f"Created tag {tag_name}")
# Push tag
subprocess.run(["git", "push", "origin", tag_name], check=True)
click.echo(f"Pushed tag {tag_name}")
@click.group()
def cli() -> None:
"""Version management for llama-cloud-services."""
pass
@cli.command()
def get() -> None:
"""Get current versions and show validation warnings."""
main_version, llama_parse_version, dependency_version = get_current_versions()
click.echo("Current versions:")
click.echo(f" llama-cloud-services: {main_version}")
click.echo(f" llama-parse: {llama_parse_version}")
click.echo(f" dependency reference: {dependency_version}")
warnings = validate_versions(main_version, llama_parse_version, dependency_version)
if warnings:
click.echo("\nValidation warnings:")
for warning in warnings:
click.echo(f" ⚠️ {warning}")
else:
click.echo("\n✅ All versions are consistent")
@cli.command()
@click.argument("version")
def set(version: str) -> None:
"""Set version across all pyproject.toml files."""
set_version(version)
@cli.command()
@click.option(
"--version", help="Version to tag (uses current version if not specified)"
)
def tag(version: str | None = None) -> None:
"""Create and push a git tag for the current version."""
if not version:
main_version, _, _ = get_current_versions()
version = main_version
create_and_push_tag(version)
if __name__ == "__main__":
cli()
+10 -10
View File
@@ -48,8 +48,8 @@ class TestData(BaseModel):
# Skip all tests if API key is not set
@pytest.mark.asyncio
@pytest.mark.skipif(
not LLAMA_CLOUD_API_KEY or not LLAMA_DEPLOY_DEPLOYMENT_NAME,
reason="LLAMA_CLOUD_API_KEY or LLAMA_DEPLOY_DEPLOYMENT_NAME not set",
not LLAMA_CLOUD_API_KEY,
reason="LLAMA_CLOUD_API_KEY not set",
)
async def test_agent_data_crud_operations():
"""Test basic CRUD operations for agent data with automatic cleanup"""
@@ -67,7 +67,7 @@ async def test_agent_data_crud_operations():
agent_data_client = AsyncAgentDataClient(
client=client,
type=TestData,
collection_name=f"test-collection-{test_id[:8]}",
collection=f"test-collection-{test_id[:8]}",
agent_url_id=LLAMA_DEPLOY_DEPLOYMENT_NAME,
)
@@ -77,21 +77,21 @@ async def test_agent_data_crud_operations():
created_item = None
try:
# Test CREATE
created_item = await agent_data_client.create_agent_data(test_data)
created_item = await agent_data_client.create_item(test_data)
assert created_item.data.name == "test-item"
assert created_item.data.test_id == test_id
assert created_item.data.value == 42
assert created_item.id is not None
# Test READ
retrieved_item = await agent_data_client.get_agent_data(created_item.id)
retrieved_item = await agent_data_client.get_item(created_item.id)
assert retrieved_item.id == created_item.id
assert retrieved_item.data.name == "test-item"
assert retrieved_item.data.test_id == test_id
assert retrieved_item.data.value == 42
# Test SEARCH
search_results = await agent_data_client.search_agent_data(
search_results = await agent_data_client.search(
filter={"test_id": {"eq": test_id}}, page_size=10, include_total=True
)
assert len(search_results.items) == 1
@@ -99,7 +99,7 @@ async def test_agent_data_crud_operations():
assert search_results.total == 1
# Test AGGREGATE
aggregate_results = await agent_data_client.aggregate_agent_data(
aggregate_results = await agent_data_client.aggregate(
group_by=["test_id"], count=True
)
assert len(aggregate_results.items) == 1
@@ -108,7 +108,7 @@ async def test_agent_data_crud_operations():
# Test UPDATE
updated_data = TestData(name="updated-item", test_id=test_id, value=84)
updated_item = await agent_data_client.update_agent_data(
updated_item = await agent_data_client.update_item(
created_item.id, updated_data
)
assert updated_item.data.name == "updated-item"
@@ -116,7 +116,7 @@ async def test_agent_data_crud_operations():
assert updated_item.id == created_item.id
# Verify update persisted
verified_item = await agent_data_client.get_agent_data(created_item.id)
verified_item = await agent_data_client.get_item(created_item.id)
assert verified_item.data.name == "updated-item"
assert verified_item.data.value == 84
@@ -124,6 +124,6 @@ async def test_agent_data_crud_operations():
# Clean up test data
if created_item is not None:
try:
await agent_data_client.delete_agent_data(created_item.id)
await agent_data_client.delete_item(created_item.id)
except Exception as e:
print(f"Warning: Failed to cleanup test data {created_item.id}: {e}")
+317 -5
View File
@@ -2,14 +2,19 @@ from datetime import datetime
from typing import Any, Dict
import pytest
from llama_cloud import ExtractRun, File
from llama_cloud.types.agent_data import AgentData
from llama_cloud.types.aggregate_group import AggregateGroup
from pydantic import BaseModel, ValidationError
from llama_cloud_services.beta.agent_data.schema import (
ExtractedData,
ExtractedFieldMetadata,
InvalidExtractionData,
TypedAgentData,
TypedAggregateGroup,
calculate_overall_confidence,
parse_extracted_field_metadata,
)
@@ -70,15 +75,22 @@ def test_extracted_data_create_method():
extracted = ExtractedData.create(person)
assert extracted.original_data == person
assert extracted.data == person
assert extracted.status == "in_review"
assert extracted.confidence == {}
assert extracted.status == "pending_review"
assert extracted.field_metadata == {}
assert extracted.overall_confidence is None
# Test with custom values
# Test with custom values using ExtractedFieldMetadata
field_metadata = {
"name": ExtractedFieldMetadata(confidence=0.99, page_number=1),
"age": ExtractedFieldMetadata(confidence=0.85, page_number=1),
}
extracted_custom = ExtractedData.create(
person, status="accepted", confidence={"name": 0.99}
person, status="accepted", field_metadata=field_metadata
)
assert extracted_custom.status == "accepted"
assert extracted_custom.confidence["name"] == 0.99
assert extracted_custom.field_metadata["name"].confidence == 0.99
assert extracted_custom.field_metadata["age"].confidence == 0.85
assert extracted_custom.overall_confidence == pytest.approx((0.99 + 0.85) / 2)
def test_extracted_data_with_dict():
@@ -107,3 +119,303 @@ def test_typed_aggregate_group_from_raw():
assert typed_group.count == 25
assert typed_group.first_item.name == "Tech Corp"
assert typed_group.first_item.employees == 500
def test_calculate_overall_confidence_simple_flat():
"""Test calculate_overall_confidence with simple flat dictionary of ExtractedFieldMetadata."""
field_metadata = {
"name": ExtractedFieldMetadata(confidence=0.9),
"age": ExtractedFieldMetadata(confidence=0.8),
"email": ExtractedFieldMetadata(confidence=0.95),
}
result = calculate_overall_confidence(field_metadata)
expected = (0.9 + 0.8 + 0.95) / 3
assert result == pytest.approx(expected, rel=1e-9)
def test_calculate_overall_confidence_nested():
"""Test calculate_overall_confidence with nested dictionary structure."""
field_metadata = {
"person": {
"name": ExtractedFieldMetadata(confidence=0.9),
"age": ExtractedFieldMetadata(confidence=0.8),
},
"contact": {
"email": ExtractedFieldMetadata(confidence=0.95),
"phone": ExtractedFieldMetadata(confidence=0.85),
},
"score": ExtractedFieldMetadata(confidence=0.7),
}
result = calculate_overall_confidence(field_metadata)
# Should average all leaf values: (0.9 + 0.8 + 0.95 + 0.85 + 0.7) / 5
expected = (0.9 + 0.8 + 0.95 + 0.85 + 0.7) / 5
assert result == pytest.approx(expected, rel=1e-9)
def test_calculate_overall_confidence_with_lists():
"""Test calculate_overall_confidence with lists of ExtractedFieldMetadata and nested structures."""
field_metadata = {
"scores": [
ExtractedFieldMetadata(confidence=0.9),
ExtractedFieldMetadata(confidence=0.8),
ExtractedFieldMetadata(confidence=0.95),
],
"nested_data": [
{
"field1": ExtractedFieldMetadata(confidence=0.7),
"field2": ExtractedFieldMetadata(confidence=0.6),
},
{
"field1": ExtractedFieldMetadata(confidence=0.8),
"field2": ExtractedFieldMetadata(confidence=0.9),
},
],
"single_value": ExtractedFieldMetadata(confidence=0.85),
}
result = calculate_overall_confidence(field_metadata)
# Should count: [0.9, 0.8, 0.95] + [0.7, 0.6, 0.8, 0.9] + [0.85] = 8 values
expected = (0.9 + 0.8 + 0.95 + 0.7 + 0.6 + 0.8 + 0.9 + 0.85) / 8
assert result == pytest.approx(expected, rel=1e-9)
def test_calculate_overall_confidence_invalid_types():
"""Test calculate_overall_confidence with invalid/mixed types alongside valid ExtractedFieldMetadata."""
field_metadata = {
"valid_metadata": ExtractedFieldMetadata(confidence=0.8),
"valid_metadata_no_confidence": ExtractedFieldMetadata(), # No confidence
"valid_list": [
ExtractedFieldMetadata(confidence=0.5),
ExtractedFieldMetadata(confidence=0.6),
],
"invalid_string": "not_a_number",
"invalid_list_mixed": [
ExtractedFieldMetadata(confidence=0.7),
"invalid",
ExtractedFieldMetadata(confidence=0.8),
],
"invalid_none": None,
"random_dict": {"a": 1, "b": 2},
}
result = calculate_overall_confidence(field_metadata)
expected = (0.8 + 0.5 + 0.6 + 0.7 + 0.8) / 5
assert result == pytest.approx(expected, rel=1e-9)
def test_calculate_overall_confidence_empty():
"""Test calculate_overall_confidence with empty inputs."""
# Empty dict
assert calculate_overall_confidence({}) is None
# Empty list
assert calculate_overall_confidence([]) is None
# Dict with only invalid values
field_metadata_invalid = {"invalid": "not_a_number", "also_invalid": None}
assert calculate_overall_confidence(field_metadata_invalid) is None
# List with only invalid values
field_metadata_invalid_list = ["invalid", None, {}]
assert calculate_overall_confidence(field_metadata_invalid_list) is None
# Dict with ExtractedFieldMetadata but no confidence values
field_metadata_no_confidence = {
"field1": ExtractedFieldMetadata(),
"field2": ExtractedFieldMetadata(),
}
assert calculate_overall_confidence(field_metadata_no_confidence) is None
def test_parse_extracted_field_metadata():
"""Test parse_extracted_field_metadata with legacy citation format."""
raw_metadata = {
"name": {
"confidence": 0.95,
"citations": [{"page_number": 1, "matching_text": "John Smith"}],
},
"age": {
"confidence": 0.87,
"citations": [
{
"page_number": 2.0, # Float page number
"matching_text": "25 years old",
}
],
},
"email": {
"confidence": 0.92,
"citations": [], # Empty citations
},
}
result = parse_extracted_field_metadata(raw_metadata)
result2 = parse_extracted_field_metadata(result)
assert result2 == result
# name should have parsed citation data
assert isinstance(result["name"], ExtractedFieldMetadata)
assert result["name"].confidence == 0.95
assert result["name"].page_number == 1
assert result["name"].matching_text == "John Smith"
# age should handle float page number
assert isinstance(result["age"], ExtractedFieldMetadata)
assert result["age"].confidence == 0.87
assert result["age"].page_number == 2 # Should be converted to int
assert result["age"].matching_text == "25 years old"
# email should handle empty citations
assert isinstance(result["email"], ExtractedFieldMetadata)
assert result["email"].confidence == 0.92
def create_file(
id: str = "file-456",
name: str = "resume.pdf",
external_file_id: str = "external-file-id",
project_id: str = "project-123",
) -> File:
return File.parse_obj(
{
"id": id,
"name": name,
"external_file_id": external_file_id,
"project_id": project_id,
}
)
def create_extract_run(
id: str = "extract-123",
data: Dict[str, Any] = {"name": "John Doe", "age": 30, "email": "john@example.com"},
extraction_metadata: Dict[str, Any] = {
"name": {
"confidence": 0.95,
"citations": [{"page_number": 1, "matching_text": "John Doe"}],
},
"age": {"confidence": 0.87},
"email": {
"confidence": 0.92,
"citations": [{"page_number": 1, "matching_text": "john@example.com"}],
},
},
data_schema: Dict[str, Any] = {},
file: File = create_file(),
) -> ExtractRun:
return ExtractRun.parse_obj(
{
"id": id,
"data": data,
"extraction_metadata": {
"field_metadata": extraction_metadata,
},
"data_schema": data_schema,
"file": file,
"extraction_agent_id": "extraction-agent-123",
"config": {},
"status": "SUCCESS",
"from_ui": False,
}
)
def test_extracted_data_from_extraction_result_success():
"""Test ExtractedData.from_extraction_result with valid data."""
# Create mock ExtractRun with valid data
extract_run = create_extract_run(
file=create_file(id="file-456", name="resume.pdf"),
)
# Create with file object
extracted: ExtractedData[Person] = ExtractedData.from_extraction_result(
extract_run,
Person,
file_hash="abc123",
status="accepted",
)
# Verify the extracted data
assert isinstance(extracted.data, Person)
assert extracted.data.name == "John Doe"
assert extracted.data.age == 30
assert extracted.data.email == "john@example.com"
assert extracted.status == "accepted"
assert extracted.file_id == "file-456"
assert extracted.file_name == "resume.pdf"
assert extracted.file_hash == "abc123"
# Verify field metadata was parsed
assert isinstance(extracted.field_metadata["name"], ExtractedFieldMetadata)
assert extracted.field_metadata["name"].confidence == 0.95
assert extracted.field_metadata["name"].page_number == 1
assert extracted.field_metadata["name"].matching_text == "John Doe"
# Verify overall confidence was calculated
expected_confidence = (0.95 + 0.87 + 0.92) / 3
assert extracted.overall_confidence == pytest.approx(expected_confidence)
def test_extracted_data_from_extraction_result_with_file_params():
"""Test ExtractedData.from_extraction_result with explicit file parameters."""
extract_run = create_extract_run(
file=create_file(id="original-file", name="original.pdf"),
)
# Override file parameters
extracted: ExtractedData[Person] = ExtractedData.from_extraction_result(
extract_run,
Person,
file_id="custom-file-id", # Should override file.id
file_name="custom-name.pdf", # Should override file.name
file_hash="custom-hash",
metadata={"source": "api_test"},
)
assert extracted.file_id == "custom-file-id" # Overridden
assert extracted.file_name == "custom-name.pdf" # Overridden
assert extracted.file_hash == "custom-hash"
assert extracted.metadata["source"] == "api_test"
def test_extracted_data_from_extraction_result_invalid_data():
"""Test ExtractedData.from_extraction_result with invalid data raises custom exception."""
# Create ExtractRun with data that doesn't match Person schema
extract_run = create_extract_run(
data={
"name": "Valid Name",
"age": "not_a_number",
"missing_email": True,
}, # Invalid age, missing email
extraction_metadata={
"name": {"confidence": 0.9},
},
data_schema={},
file=create_file(id="error-file", name="bad_data.pdf"),
)
# Should raise InvalidExtractionData with ExtractedData containing error info
with pytest.raises(InvalidExtractionData) as exc_info:
ExtractedData.from_extraction_result(
extract_run, Person, metadata={"test": "metadata"}
)
# Verify the exception contains the invalid ExtractedData
invalid_data = exc_info.value.invalid_item
assert isinstance(invalid_data, ExtractedData)
assert invalid_data.status == "error"
assert invalid_data.data == {
"name": "Valid Name",
"age": "not_a_number",
"missing_email": True,
}
assert invalid_data.file_id == "error-file"
assert invalid_data.file_name == "bad_data.pdf"
# Check error metadata was added
assert "extraction_error" in invalid_data.metadata
assert "test" in invalid_data.metadata # Original metadata preserved
assert "2 validation errors" in invalid_data.metadata["extraction_error"]
# Verify field metadata was still parsed (before validation failed)
assert isinstance(invalid_data.field_metadata["name"], ExtractedFieldMetadata)
assert invalid_data.field_metadata["name"].confidence == 0.9
assert invalid_data.overall_confidence == 0.9
+6 -7
View File
@@ -8,6 +8,7 @@ import uuid
from llama_cloud.types import ExtractConfig, ExtractMode
from deepdiff import DeepDiff
from tests.extract.util import json_subset_match_score, load_test_dotenv
from .conftest import register_agent_for_cleanup
load_test_dotenv()
@@ -115,13 +116,11 @@ def extraction_agent(test_case: TestCase, extractor: LlamaExtract):
# Create new agent
agent = extractor.create_agent(agent_name, schema, config=test_case.config)
yield agent
# Cleanup after test
try:
extractor.delete_agent(agent.id)
except Exception as e:
print(f"Warning: Failed to delete agent {agent.id}: {str(e)}")
# Register agent for cleanup at the end of the test session
register_agent_for_cleanup(agent.id)
yield agent
@pytest.mark.skipif(
@@ -130,7 +129,7 @@ def extraction_agent(test_case: TestCase, extractor: LlamaExtract):
)
@pytest.mark.parametrize("test_case", get_test_cases(), ids=lambda x: x.name)
def test_extraction(test_case: TestCase, extraction_agent: ExtractionAgent) -> None:
result = extraction_agent.extract(test_case.input_file).data
result = extraction_agent.extract(test_case.input_file).data # type: ignore
with open(test_case.expected_output, "r") as f:
expected = json.load(f)
# TODO: fix the saas_slide test
+9
View File
@@ -193,3 +193,12 @@ async def test_multiple_page_markdown(
result = await markdown_parser.aload_data(filepath)
assert len(result) == expected
assert all(len(doc.text) > 0 for doc in result)
@pytest.mark.asyncio
async def test_get_result(markdown_parser: LlamaParse) -> None:
filepath = "tests/test_files/attention_is_all_you_need.pdf"
expected = await markdown_parser.aparse(filepath)
result = await markdown_parser.aget_result(expected.job_id)
assert result.job_id == expected.job_id
assert len(result.pages) == len(expected.pages)
+6
View File
@@ -83,6 +83,12 @@ async def test_basic_parse_result(file_path: str, partition_pages: Optional[int]
assert image_documents[0].image is not None
assert len(image_documents[0].resolve_image().getvalue()) > 0
assert len(await result.aget_text()) > 0
assert len(await result.aget_markdown()) > 0
json = await result.aget_json()
assert json.get("job_metadata")
assert len(json.get("pages", [])) == len(result.pages)
@pytest.mark.asyncio
@pytest.mark.skip(
+33 -1
View File
@@ -1,7 +1,30 @@
import pytest
from pathlib import Path
from llama_cloud_services.parse.utils import (
expand_target_pages,
partition_pages,
extract_tables_from_json_results,
)
from typing import List
from llama_cloud_services.parse.utils import expand_target_pages, partition_pages
@pytest.fixture()
def pseudo_json_results() -> List[dict]:
return [
{
"pages": [
{
"items": [
{
"type": "table",
"csv": "Name,Age,Height (cm)\nAnna,12,140\nBob,22,175\nClaire,33,173\nDenis,44,185\n",
}
]
}
]
}
]
def test_expand_target_pages() -> None:
@@ -28,3 +51,12 @@ def test_partion_pages() -> None:
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"]
def test_table_extraction(pseudo_json_results: List[dict], tmpdir: Path) -> None:
tables = extract_tables_from_json_results(pseudo_json_results, tmpdir)
assert len(tables) == 1
for table in tables:
assert Path(table).exists()
with open(table) as t:
assert t.read() == pseudo_json_results[0]["pages"][0]["items"][0]["csv"]