mirror of
https://github.com/run-llama/llama_cloud_services.git
synced 2026-07-21 03:55:22 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 230a110e52 | |||
| 83e2b031cd | |||
| 4844e26e5c | |||
| 70a049af3c | |||
| dc11776c86 |
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
from .schema import (
|
||||
TypedAgentData,
|
||||
ExtractedData,
|
||||
TypedAgentDataItems,
|
||||
StatusType,
|
||||
ExtractedT,
|
||||
AgentDataT,
|
||||
ComparisonOperator,
|
||||
)
|
||||
from .client import AsyncAgentDataClient
|
||||
|
||||
__all__ = [
|
||||
"TypedAgentData",
|
||||
"AsyncAgentDataClient",
|
||||
"ExtractedData",
|
||||
"TypedAgentDataItems",
|
||||
"StatusType",
|
||||
"ExtractedT",
|
||||
"AgentDataT",
|
||||
"ComparisonOperator",
|
||||
]
|
||||
@@ -0,0 +1,278 @@
|
||||
import os
|
||||
from typing import Any, Dict, Generic, List, Optional, Type
|
||||
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from tenacity import (
|
||||
WrappedFn,
|
||||
retry,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
retry_if_exception_type,
|
||||
)
|
||||
import httpx
|
||||
|
||||
from .schema import (
|
||||
AgentDataT,
|
||||
ComparisonOperator,
|
||||
TypedAgentData,
|
||||
TypedAgentDataItems,
|
||||
TypedAggregateGroup,
|
||||
TypedAggregateGroupItems,
|
||||
)
|
||||
|
||||
|
||||
def agent_data_retry(func: WrappedFn) -> WrappedFn:
|
||||
"""
|
||||
Decorator that adds automatic retry logic to agent data API calls.
|
||||
|
||||
Applies exponential backoff retry strategy for common network-related exceptions:
|
||||
- Up to 3 retry attempts
|
||||
- Exponential wait time between 0.5s and 10s
|
||||
- Retries on timeout, connection, and HTTP status errors
|
||||
|
||||
This ensures resilient API communication in distributed environments where
|
||||
temporary network issues or service unavailability may occur.
|
||||
"""
|
||||
return retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(min=0.5, max=10),
|
||||
retry=retry_if_exception_type(
|
||||
(httpx.TimeoutException, httpx.ConnectError, httpx.HTTPStatusError)
|
||||
),
|
||||
)(func)
|
||||
|
||||
|
||||
def get_default_agent_id() -> Optional[str]:
|
||||
"""
|
||||
Retrieve the default agent ID from environment variables.
|
||||
|
||||
Returns:
|
||||
The value of LLAMA_DEPLOY_DEPLOYMENT_NAME environment variable,
|
||||
or None if not set
|
||||
|
||||
Note:
|
||||
This provides a convenient way to configure agent ID globally
|
||||
via environment variables instead of passing it explicitly
|
||||
to each client instance.
|
||||
"""
|
||||
return os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME")
|
||||
|
||||
|
||||
class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
"""
|
||||
Async client for managing agent-generated structured data with type safety.
|
||||
|
||||
This client provides a high-level interface for CRUD operations, searching, and
|
||||
aggregation of structured data created by agents. It enforces type safety by
|
||||
validating all data against a specified Pydantic model type.
|
||||
|
||||
The client is generic over AgentDataT, which must be a Pydantic BaseModel that
|
||||
defines the structure of your agent's data output.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from llama_cloud_services.beta.agent_data import AsyncAgentDataClient
|
||||
|
||||
class ExtractedPerson(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
email: str
|
||||
|
||||
# Initialize client
|
||||
llama_client = AsyncLlamaCloud(token="your-api-key")
|
||||
agent_client = AsyncAgentDataClient(
|
||||
client=llama_client,
|
||||
type=ExtractedPerson,
|
||||
collection="extracted_people",
|
||||
agent_url_id="person-extraction-agent"
|
||||
)
|
||||
|
||||
# Create data
|
||||
person = ExtractedPerson(name="John Doe", age=30, email="john@example.com")
|
||||
result = await agent_client.create_agent_data(person)
|
||||
|
||||
# Search data
|
||||
results = await agent_client.search(
|
||||
filter={"age": {"gt": 25}},
|
||||
order_by="data.name",
|
||||
page_size=20
|
||||
)
|
||||
```
|
||||
|
||||
Type Parameters:
|
||||
AgentDataT: Pydantic BaseModel type that defines the structure of agent data
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type: Type[AgentDataT],
|
||||
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:
|
||||
type: Pydantic BaseModel class that defines the data structure.
|
||||
All agent data will be validated against this type.
|
||||
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
|
||||
url of an agent within the llama cloud platform. If not provided,
|
||||
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
|
||||
LLAMA_DEPLOY_DEPLOYMENT_NAME environment variable is not set
|
||||
|
||||
Note:
|
||||
The client automatically applies retry logic to all API calls with
|
||||
exponential backoff for timeout, connection, and HTTP status errors.
|
||||
"""
|
||||
|
||||
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 = 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_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_item(self, data: AgentDataT) -> TypedAgentData[AgentDataT]:
|
||||
raw_data = await self.client.beta.create_agent_data(
|
||||
agent_slug=self.agent_url_id,
|
||||
collection=self.collection,
|
||||
data=data.model_dump(),
|
||||
)
|
||||
return TypedAgentData.from_raw(raw_data, validator=self.type)
|
||||
|
||||
@agent_data_retry
|
||||
async def update_item(
|
||||
self, item_id: str, data: AgentDataT
|
||||
) -> TypedAgentData[AgentDataT]:
|
||||
raw_data = await self.client.beta.update_agent_data(
|
||||
item_id=item_id,
|
||||
data=data.model_dump(),
|
||||
)
|
||||
return TypedAgentData.from_raw(raw_data, validator=self.type)
|
||||
|
||||
@agent_data_retry
|
||||
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(
|
||||
self,
|
||||
filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None,
|
||||
order_by: Optional[str] = None,
|
||||
offset: Optional[int] = None,
|
||||
page_size: Optional[int] = None,
|
||||
include_total: bool = False,
|
||||
) -> TypedAgentDataItems[AgentDataT]:
|
||||
"""
|
||||
Search agent data with filtering, sorting, and pagination.
|
||||
Args:
|
||||
filter: Filter conditions to apply to the search. Dict mapping field names to FilterOperation objects. Filters only by data fields
|
||||
Examples:
|
||||
- {"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
|
||||
page_size: Maximum number of items to return per page. Defaults to 10.
|
||||
offset: Number of items to skip from the beginning. Defaults to 0.
|
||||
include_total: Whether to include the total count in the response. Defaults to False to improve performance. It's recommended to only request on the first page.
|
||||
"""
|
||||
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,
|
||||
filter=filter,
|
||||
order_by=order_by,
|
||||
offset=offset,
|
||||
page_size=page_size,
|
||||
include_total=include_total,
|
||||
)
|
||||
return TypedAgentDataItems(
|
||||
items=[
|
||||
TypedAgentData.from_raw(item, validator=self.type) for item in raw.items
|
||||
],
|
||||
has_more=raw.next_page_token is not None,
|
||||
total=raw.total_size,
|
||||
)
|
||||
|
||||
@agent_data_retry
|
||||
async def aggregate(
|
||||
self,
|
||||
filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None,
|
||||
group_by: Optional[List[str]] = None,
|
||||
count: Optional[bool] = None,
|
||||
first: Optional[bool] = None,
|
||||
order_by: Optional[str] = None,
|
||||
offset: Optional[int] = None,
|
||||
page_size: Optional[int] = None,
|
||||
) -> TypedAggregateGroupItems[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 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 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,
|
||||
page_size=page_size,
|
||||
filter=filter,
|
||||
order_by=order_by,
|
||||
group_by=group_by,
|
||||
count=count,
|
||||
first=first,
|
||||
offset=offset,
|
||||
)
|
||||
return TypedAggregateGroupItems(
|
||||
items=[
|
||||
TypedAggregateGroup.from_raw(item, validator=self.type)
|
||||
for item in raw.items
|
||||
],
|
||||
has_more=raw.next_page_token is not None,
|
||||
total=raw.total_size,
|
||||
)
|
||||
@@ -0,0 +1,386 @@
|
||||
"""
|
||||
Agent Data API Schema Definitions
|
||||
|
||||
This module provides typed wrappers around the raw LlamaCloud agent data API,
|
||||
enabling type-safe interactions with agent-generated structured data.
|
||||
|
||||
The agent data API serves as a persistent storage system for structured data
|
||||
produced by LlamaCloud agents (particularly extraction agents). It provides
|
||||
CRUD operations, search capabilities, filtering, and aggregation functionality
|
||||
for managing agent-generated data at scale.
|
||||
|
||||
Key Concepts:
|
||||
- Agent Slug: Unique identifier for an agent instance
|
||||
- Collection: Named grouping of data within an agent (defaults to "default"). Data within a collection should be of the same type.
|
||||
- Agent Data: Individual structured data records with metadata and timestamps
|
||||
|
||||
Example Usage:
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Person(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
client = AsyncAgentDataClient(
|
||||
client=async_llama_cloud,
|
||||
type=Person,
|
||||
collection="people",
|
||||
agent_url_id="my-extraction-agent-xyz"
|
||||
)
|
||||
|
||||
# Create typed data
|
||||
person = Person(name="John", age=30)
|
||||
result = await client.create_agent_data(person)
|
||||
print(result.data.name) # Type-safe access
|
||||
```
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from llama_cloud.types.agent_data import AgentData
|
||||
from llama_cloud.types.aggregate_group import AggregateGroup
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import (
|
||||
Generic,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Dict,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
Any,
|
||||
)
|
||||
|
||||
|
||||
# Type variable for user-defined data models
|
||||
AgentDataT = TypeVar("AgentDataT", bound=BaseModel)
|
||||
|
||||
# Type variable for extracted data (can be dict or Pydantic model)
|
||||
ExtractedT = TypeVar("ExtractedT", bound=Union[BaseModel, dict])
|
||||
|
||||
# Status types for extracted data workflow
|
||||
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]):
|
||||
"""
|
||||
Type-safe wrapper for agent data records.
|
||||
|
||||
This class represents a single data record stored in the agent data API,
|
||||
combining the structured data payload with metadata about when and where
|
||||
it was created.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for this data record
|
||||
agent_url_id: Identifier of the agent that created this data
|
||||
collection: Named collection within the agent (used for organization)
|
||||
data: The actual structured data payload (typed as AgentDataT)
|
||||
created_at: Timestamp when the record was first created
|
||||
updated_at: Timestamp when the record was last modified
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Access typed data
|
||||
person_data: TypedAgentData[Person] = await client.get_agent_data(id)
|
||||
print(person_data.data.name) # Type-safe access to Person fields
|
||||
print(person_data.created_at) # Access metadata
|
||||
```
|
||||
"""
|
||||
|
||||
id: Optional[str] = Field(description="Unique identifier for this data record")
|
||||
agent_url_id: str = Field(
|
||||
description="Identifier of the agent that created this data"
|
||||
)
|
||||
collection: Optional[str] = Field(
|
||||
description="Named collection within the agent for data organization"
|
||||
)
|
||||
data: AgentDataT = Field(description="The structured data payload")
|
||||
created_at: Optional[datetime] = Field(description="When this record was created")
|
||||
updated_at: Optional[datetime] = Field(
|
||||
description="When this record was last modified"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_raw(
|
||||
cls, raw_data: AgentData, validator: Type[AgentDataT]
|
||||
) -> "TypedAgentData[AgentDataT]":
|
||||
"""
|
||||
Convert raw API response to typed agent data.
|
||||
|
||||
Args:
|
||||
raw_data: Raw agent data from the API
|
||||
validator: Pydantic model class to validate the data field
|
||||
|
||||
Returns:
|
||||
TypedAgentData instance with validated data
|
||||
"""
|
||||
data: AgentDataT = validator.model_validate(raw_data.data)
|
||||
|
||||
return cls(
|
||||
id=raw_data.id,
|
||||
agent_url_id=raw_data.agent_slug,
|
||||
collection=raw_data.collection,
|
||||
data=data,
|
||||
created_at=raw_data.created_at,
|
||||
updated_at=raw_data.updated_at,
|
||||
)
|
||||
|
||||
|
||||
class TypedAgentDataItems(BaseModel, Generic[AgentDataT]):
|
||||
"""
|
||||
Paginated collection of agent data records.
|
||||
|
||||
This class represents a page of search results from the agent data API,
|
||||
providing both the data records and pagination metadata.
|
||||
|
||||
Attributes:
|
||||
items: List of agent data records in this page
|
||||
total: Total number of records matching the query (only present if requested)
|
||||
has_more: Whether there are more records available beyond this page
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Search with pagination
|
||||
results = await client.search(
|
||||
page_size=10,
|
||||
include_total=True
|
||||
)
|
||||
|
||||
for item in results.items:
|
||||
print(item.data.name)
|
||||
|
||||
if results.has_more:
|
||||
# Load next page
|
||||
next_page = await client.search(
|
||||
page_size=10,
|
||||
offset=10
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
items: List[TypedAgentData[AgentDataT]] = Field(
|
||||
description="List of agent data records in this page"
|
||||
)
|
||||
total: Optional[int] = Field(
|
||||
description="Total number of records matching the query (only present if requested)"
|
||||
)
|
||||
has_more: bool = Field(
|
||||
description="Whether there are more records available beyond this page"
|
||||
)
|
||||
|
||||
|
||||
class ExtractedData(BaseModel, Generic[ExtractedT]):
|
||||
"""
|
||||
Wrapper for extracted data with workflow status tracking.
|
||||
|
||||
This class is designed for extraction workflows where data goes through
|
||||
review and approval stages. It maintains both the original extracted data
|
||||
and the current state after any modifications.
|
||||
|
||||
Attributes:
|
||||
original_data: The data as originally extracted from the source
|
||||
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:
|
||||
- "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
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Create extracted data for review
|
||||
extracted = ExtractedData.create(
|
||||
data=person_data,
|
||||
status="pending_review",
|
||||
confidence={"name": 0.95, "age": 0.87}
|
||||
)
|
||||
|
||||
# Later, after review
|
||||
if extracted.status == "accepted":
|
||||
# Use the data
|
||||
process_person(extracted.data)
|
||||
```
|
||||
"""
|
||||
|
||||
original_data: ExtractedT = Field(
|
||||
description="The original data that was extracted from the document"
|
||||
)
|
||||
data: ExtractedT = Field(
|
||||
description="The latest state of the data. Will differ if data has been updated"
|
||||
)
|
||||
status: StatusType = Field(description="The status of the extracted data")
|
||||
confidence: Dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Confidence scores, if any, for each primitive field in the original_data data",
|
||||
)
|
||||
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,
|
||||
data: ExtractedT,
|
||||
status: StatusType = "pending_review",
|
||||
confidence: Optional[Dict[str, Any]] = None,
|
||||
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.
|
||||
|
||||
Args:
|
||||
extracted_data: The extracted data payload
|
||||
status: Initial workflow status
|
||||
confidence: Optional confidence scores 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
|
||||
|
||||
Returns:
|
||||
New ExtractedData instance ready for storage
|
||||
"""
|
||||
return cls(
|
||||
original_data=data,
|
||||
data=data,
|
||||
status=status,
|
||||
confidence=confidence or {},
|
||||
file_id=file_id,
|
||||
file_name=file_name,
|
||||
file_hash=file_hash,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
|
||||
|
||||
class TypedAggregateGroup(BaseModel, Generic[AgentDataT]):
|
||||
"""
|
||||
Represents a group of agent data records aggregated by common field values.
|
||||
|
||||
This class is used for grouping and analyzing agent data based on shared
|
||||
characteristics. It's particularly useful for generating summaries and
|
||||
statistics across large datasets.
|
||||
|
||||
Attributes:
|
||||
group_key: The field values that define this group
|
||||
count: Number of records in this group (if count aggregation was requested)
|
||||
first_item: Representative data record from this group (if requested)
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Group by age range
|
||||
groups = await client.aggregate_agent_data(
|
||||
group_by=["age_range"],
|
||||
count=True,
|
||||
first=True
|
||||
)
|
||||
|
||||
for group in groups.items:
|
||||
print(f"Age range {group.group_key['age_range']}: {group.count} people")
|
||||
if group.first_item:
|
||||
print(f"Example: {group.first_item.name}")
|
||||
```
|
||||
"""
|
||||
|
||||
group_key: Dict[str, Any] = Field(
|
||||
description="The field values that define this group"
|
||||
)
|
||||
count: Optional[int] = Field(
|
||||
description="Number of records in this group (if count aggregation was requested)"
|
||||
)
|
||||
first_item: Optional[AgentDataT] = Field(
|
||||
description="Representative data record from this group (if requested)"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_raw(
|
||||
cls, raw_data: AggregateGroup, validator: Type[AgentDataT]
|
||||
) -> "TypedAggregateGroup[AgentDataT]":
|
||||
"""
|
||||
Convert raw API response to typed aggregate group.
|
||||
|
||||
Args:
|
||||
raw_data: Raw aggregate group from the API
|
||||
validator: Pydantic model class to validate the first_item field
|
||||
|
||||
Returns:
|
||||
TypedAggregateGroup instance with validated first_item
|
||||
"""
|
||||
first_item: Optional[AgentDataT] = raw_data.first_item
|
||||
if first_item is not None:
|
||||
first_item = validator.model_validate(first_item)
|
||||
|
||||
return cls(
|
||||
group_key=raw_data.group_key,
|
||||
count=raw_data.count,
|
||||
first_item=first_item,
|
||||
)
|
||||
|
||||
|
||||
class TypedAggregateGroupItems(BaseModel, Generic[AgentDataT]):
|
||||
"""
|
||||
Paginated collection of aggregate groups.
|
||||
|
||||
This class represents a page of aggregation results from the agent data API,
|
||||
providing both the grouped data and pagination metadata.
|
||||
|
||||
Attributes:
|
||||
items: List of aggregate groups in this page
|
||||
total: Total number of groups matching the query (only present if requested)
|
||||
has_more: Whether there are more groups available beyond this page
|
||||
|
||||
Example:
|
||||
```python
|
||||
# Get first page of groups
|
||||
results = await client.aggregate_agent_data(
|
||||
group_by=["department"],
|
||||
count=True,
|
||||
page_size=20
|
||||
)
|
||||
|
||||
for group in results.items:
|
||||
dept = group.group_key["department"]
|
||||
print(f"{dept}: {group.count} employees")
|
||||
|
||||
# Load more if needed
|
||||
if results.has_more:
|
||||
next_page = await client.aggregate_agent_data(
|
||||
group_by=["department"],
|
||||
count=True,
|
||||
page_size=20,
|
||||
offset=20
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
items: List[TypedAggregateGroup[AgentDataT]] = Field(
|
||||
description="List of aggregate groups in this page"
|
||||
)
|
||||
total: Optional[int] = Field(
|
||||
description="Total number of groups matching the query (only present if requested)"
|
||||
)
|
||||
has_more: bool = Field(
|
||||
description="Whether there are more groups available beyond this page"
|
||||
)
|
||||
@@ -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)",
|
||||
@@ -859,6 +864,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
|
||||
|
||||
@@ -1611,6 +1621,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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+20
-19
@@ -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.33"
|
||||
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.33-py3-none-any.whl", hash = "sha256:35b7d4a30b013f0a343f7e09126b531c697d65bffd4eb4d2d79bf7d65f256178"},
|
||||
{file = "llama_cloud-0.1.33.tar.gz", hash = "sha256:a0bb900d5a6e86f8c767b48686c5253679ad7ca1b57612dc39b0767e57ad3d78"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1187,24 +1187,25 @@ pydantic = ">=1.10"
|
||||
|
||||
[[package]]
|
||||
name = "llama-cloud-services"
|
||||
version = "0.6.30"
|
||||
version = "0.6.46"
|
||||
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.46-py3-none-any.whl", hash = "sha256:2cdf4de19cfb5548e9d0155f1bc203affca4e26ac0201f8d09d18315b1350479"},
|
||||
{file = "llama_cloud_services-0.6.46.tar.gz", hash = "sha256:cf50c5cfd481982fd548f600adfcc666f3939b6f04718775d853a4e4efebb750"},
|
||||
]
|
||||
|
||||
[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.33"
|
||||
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 = "b18a0bb31a806df8e673634c4916d5001fd80e3e328e26f632ff6a55c6a1a196"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "llama-parse"
|
||||
version = "0.6.43"
|
||||
version = "0.6.47"
|
||||
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.43"
|
||||
llama-cloud-services = ">=0.6.47"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.0.0"
|
||||
|
||||
Generated
+4
-4
@@ -1925,14 +1925,14 @@ rapidfuzz = ">=3.9.0,<4.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "llama-cloud"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = "<4,>=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "llama_cloud-0.1.32-py3-none-any.whl", hash = "sha256:c42b2d5fb24acc8595bcc3626fb84c872909a16ab6d6879a1cb1101b21c238bd"},
|
||||
{file = "llama_cloud-0.1.32.tar.gz", hash = "sha256:cea98241127311ea91f191c3c006aa6558f01d16f9539ed93b24d716b888f10e"},
|
||||
{file = "llama_cloud-0.1.33-py3-none-any.whl", hash = "sha256:35b7d4a30b013f0a343f7e09126b531c697d65bffd4eb4d2d79bf7d65f256178"},
|
||||
{file = "llama_cloud-0.1.33.tar.gz", hash = "sha256:a0bb900d5a6e86f8c767b48686c5253679ad7ca1b57612dc39b0767e57ad3d78"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -4623,4 +4623,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.9,<4.0"
|
||||
content-hash = "112c1ccc4187a295dc07bebb14753f27f2e153dfdab895f763297f5391d3fb10"
|
||||
content-hash = "487717a0bbe7ff67360e3e9f187e15a33c77e4942a7c909cb37b95425a81544c"
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ python_version = "3.10"
|
||||
|
||||
[tool.poetry]
|
||||
name = "llama-cloud-services"
|
||||
version = "0.6.43"
|
||||
version = "0.6.47"
|
||||
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.32"
|
||||
llama-cloud = "==0.1.33"
|
||||
pydantic = ">=2.8,!=2.10"
|
||||
click = "^8.1.7"
|
||||
python-dotenv = "^1.0.1"
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import os
|
||||
import httpx
|
||||
import pytest
|
||||
import uuid
|
||||
from pydantic import BaseModel
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from llama_cloud_services.beta.agent_data import AsyncAgentDataClient
|
||||
|
||||
|
||||
class TrailingSlashHttpxClient(httpx.AsyncClient):
|
||||
"""Custom httpx client that ensures all URLs have trailing slashes"""
|
||||
|
||||
async def request(self, method, url, **kwargs):
|
||||
# Convert URL to string and ensure trailing slash
|
||||
url_str = str(url)
|
||||
if not url_str.endswith("/") and "?" not in url_str:
|
||||
url_str += "/"
|
||||
self.headers["Authorization"] = f"Bearer {LLAMA_CLOUD_API_KEY}"
|
||||
kwargs.pop("headers", None)
|
||||
return await super().request(method, url_str, headers=self.headers, **kwargs)
|
||||
|
||||
|
||||
# Load environment variables
|
||||
def load_test_dotenv():
|
||||
dotenv_path = Path(__file__).parent.parent.parent.parent / ".env.dev"
|
||||
load_dotenv(dotenv_path, override=True)
|
||||
|
||||
|
||||
load_test_dotenv()
|
||||
|
||||
# Get configuration from environment
|
||||
LLAMA_CLOUD_API_KEY = os.getenv("LLAMA_CLOUD_API_KEY")
|
||||
LLAMA_CLOUD_BASE_URL = os.getenv("LLAMA_CLOUD_BASE_URL")
|
||||
LLAMA_DEPLOY_DEPLOYMENT_NAME = os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME")
|
||||
|
||||
|
||||
class TestData(BaseModel):
|
||||
"""Simple test data model for agent data testing"""
|
||||
|
||||
name: str
|
||||
test_id: str
|
||||
value: int
|
||||
|
||||
|
||||
# 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",
|
||||
)
|
||||
async def test_agent_data_crud_operations():
|
||||
"""Test basic CRUD operations for agent data with automatic cleanup"""
|
||||
# Create unique test identifier to avoid conflicts
|
||||
test_id = str(uuid.uuid4())
|
||||
|
||||
# Set up client
|
||||
client = AsyncLlamaCloud(
|
||||
token=LLAMA_CLOUD_API_KEY,
|
||||
base_url=LLAMA_CLOUD_BASE_URL,
|
||||
httpx_client=TrailingSlashHttpxClient(timeout=60, follow_redirects=True),
|
||||
)
|
||||
|
||||
# Create agent data client with unique collection name
|
||||
agent_data_client = AsyncAgentDataClient(
|
||||
client=client,
|
||||
type=TestData,
|
||||
collection=f"test-collection-{test_id[:8]}",
|
||||
agent_url_id=LLAMA_DEPLOY_DEPLOYMENT_NAME,
|
||||
)
|
||||
|
||||
# Create test data
|
||||
test_data = TestData(name="test-item", test_id=test_id, value=42)
|
||||
|
||||
created_item = None
|
||||
try:
|
||||
# Test CREATE
|
||||
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_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(
|
||||
filter={"test_id": {"eq": test_id}}, page_size=10, include_total=True
|
||||
)
|
||||
assert len(search_results.items) == 1
|
||||
assert search_results.items[0].data.test_id == test_id
|
||||
assert search_results.total == 1
|
||||
|
||||
# Test AGGREGATE
|
||||
aggregate_results = await agent_data_client.aggregate(
|
||||
group_by=["test_id"], count=True
|
||||
)
|
||||
assert len(aggregate_results.items) == 1
|
||||
assert aggregate_results.items[0].group_key["test_id"] == test_id
|
||||
assert aggregate_results.items[0].count == 1
|
||||
|
||||
# Test UPDATE
|
||||
updated_data = TestData(name="updated-item", test_id=test_id, value=84)
|
||||
updated_item = await agent_data_client.update_item(
|
||||
created_item.id, updated_data
|
||||
)
|
||||
assert updated_item.data.name == "updated-item"
|
||||
assert updated_item.data.value == 84
|
||||
assert updated_item.id == created_item.id
|
||||
|
||||
# Verify update persisted
|
||||
verified_item = await agent_data_client.get_item(created_item.id)
|
||||
assert verified_item.data.name == "updated-item"
|
||||
assert verified_item.data.value == 84
|
||||
|
||||
finally:
|
||||
# Clean up test data
|
||||
if created_item is not None:
|
||||
try:
|
||||
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}")
|
||||
@@ -0,0 +1,109 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
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,
|
||||
TypedAgentData,
|
||||
TypedAggregateGroup,
|
||||
)
|
||||
|
||||
|
||||
# Test data models
|
||||
class Person(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
email: str
|
||||
|
||||
|
||||
class Company(BaseModel):
|
||||
name: str
|
||||
industry: str
|
||||
employees: int
|
||||
|
||||
|
||||
def test_typed_agent_data_from_raw():
|
||||
"""Test TypedAgentData.from_raw class method."""
|
||||
raw_data = AgentData(
|
||||
id="456",
|
||||
agent_slug="extraction-agent",
|
||||
collection="employees",
|
||||
data={"name": "Jane Smith", "age": 25, "email": "jane@company.com"},
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
typed_data = TypedAgentData.from_raw(raw_data, Person)
|
||||
|
||||
assert typed_data.id == "456"
|
||||
assert typed_data.agent_url_id == "extraction-agent"
|
||||
assert typed_data.collection == "employees"
|
||||
assert typed_data.data.name == "Jane Smith"
|
||||
assert typed_data.data.age == 25
|
||||
assert typed_data.data.email == "jane@company.com"
|
||||
|
||||
|
||||
def test_typed_agent_data_from_raw_validation_error():
|
||||
"""Test TypedAgentData.from_raw with invalid data."""
|
||||
raw_data = AgentData(
|
||||
id="789",
|
||||
agent_slug="test-agent",
|
||||
collection="people",
|
||||
data={"name": "Invalid Person", "age": "not_a_number"}, # Invalid age
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
TypedAgentData.from_raw(raw_data, Person)
|
||||
|
||||
|
||||
def test_extracted_data_create_method():
|
||||
"""Test ExtractedData.create class method."""
|
||||
person = Person(name="Created Person", age=35, email="created@example.com")
|
||||
|
||||
# Test with defaults
|
||||
extracted = ExtractedData.create(person)
|
||||
assert extracted.original_data == person
|
||||
assert extracted.data == person
|
||||
assert extracted.status == "pending_review"
|
||||
assert extracted.confidence == {}
|
||||
|
||||
# Test with custom values
|
||||
extracted_custom = ExtractedData.create(
|
||||
person, status="accepted", confidence={"name": 0.99}
|
||||
)
|
||||
assert extracted_custom.status == "accepted"
|
||||
assert extracted_custom.confidence["name"] == 0.99
|
||||
|
||||
|
||||
def test_extracted_data_with_dict():
|
||||
"""Test ExtractedData with dict data instead of Pydantic model."""
|
||||
data_dict = {"name": "Dict Person", "age": 45, "email": "dict@example.com"}
|
||||
|
||||
extracted = ExtractedData[Dict[str, Any]](
|
||||
original_data=data_dict, data=data_dict, status="accepted", confidence={}
|
||||
)
|
||||
|
||||
assert extracted.original_data["name"] == "Dict Person"
|
||||
assert extracted.data["age"] == 45
|
||||
|
||||
|
||||
def test_typed_aggregate_group_from_raw():
|
||||
"""Test TypedAggregateGroup.from_raw class method."""
|
||||
raw_group = AggregateGroup(
|
||||
group_key={"industry": "Technology"},
|
||||
count=25,
|
||||
first_item={"name": "Tech Corp", "industry": "Technology", "employees": 500},
|
||||
)
|
||||
|
||||
typed_group = TypedAggregateGroup.from_raw(raw_group, Company)
|
||||
|
||||
assert typed_group.group_key["industry"] == "Technology"
|
||||
assert typed_group.count == 25
|
||||
assert typed_group.first_item.name == "Tech Corp"
|
||||
assert typed_group.first_item.employees == 500
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user