Compare commits

..

8 Commits

Author SHA1 Message Date
github-actions[bot] b12ffef916 chore: version packages (#1087)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-21 12:44:43 -08:00
Neeraj Pradhan 07ec282257 Bump up patch version for python packages (#1086) 2026-01-21 12:30:23 -08:00
Neeraj Pradhan 013b689812 Bump up minor version for python packages (#1085) 2026-01-21 12:13:13 -08:00
Adrian Lyjak 3040951cb8 Use error description in invalid extraction error (#1081)
* fix: display extraction job error in InvalidExtractionData exception

Refactored InvalidExtractionData to read the `error` field from
ExtractRun and prominently display it in the exception message.
The job-level error is now stored in the `extraction_error` attribute
and included in the invalid_item's metadata as `job_error`.

* Create three-yaks-beg.md

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 17:43:21 -05:00
github-actions[bot] 9239498945 chore: version packages (#1076)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-14 19:15:05 +01:00
Pierre-Loic Doulcet 19cbb25631 remove extension filter (#1075)
* remove extension filter

* changeset

* Update ninety-goats-look.md

Make it a patch version

* Update package.json

back out of version bump

* Update pyproject.toml

back out of version bump

* Update package.json

back out of version bump

* Update pyproject.toml

back out of version bump

---------

Co-authored-by: Adrian Lyjak <adrianlyjak@gmail.com>
2026-01-14 19:13:39 +01:00
github-actions[bot] 812e2f7d72 chore: version packages (#1073)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-12 19:03:13 +01:00
Clelia (Astra) Bertelli d7864afe3f fix: bug fix retry logic in Classify and Extract (#1066)
* fix: bug fix retry logic in Classify and Extract

* chore: apply suggestion

* chore: add PARTIAL_SUCCESS to classify
2026-01-12 18:57:40 +01:00
21 changed files with 220 additions and 81 deletions
+13
View File
@@ -1,5 +1,18 @@
# llama-cloud-services-py
## 0.6.91
### Patch Changes
- 07ec282: Bump up patch versions for python packages
- 3040951: Use error description in ExtractedData invalid extraction error
## 0.6.90
### Patch Changes
- 19cbb25: Remove extension filter
## 0.6.89
### Patch Changes
@@ -475,26 +475,49 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
},
)
except ValidationError as e:
# Capture the job-level error from the extraction run if available
job_error = result.error
invalid_item = ExtractedData[Dict[str, Any]].create(
data=result.data or {},
status="error",
field_metadata=field_metadata,
metadata={"extraction_error": str(e), **(metadata or {})},
metadata={
"extraction_error": str(e),
**({"job_error": job_error} if job_error else {}),
**(metadata or {}),
},
file_id=file_id,
file_name=file_name,
file_hash=file_hash,
)
raise InvalidExtractionData(invalid_item) from e
raise InvalidExtractionData(invalid_item, extraction_error=job_error) from e
class InvalidExtractionData(Exception):
"""
Exception raised when the extracted data does not conform to the schema.
Attributes:
invalid_item: The ExtractedData instance containing the invalid data and metadata
extraction_error: The error message from the extraction job, if available
"""
def __init__(self, invalid_item: ExtractedData[Dict[str, Any]]):
def __init__(
self,
invalid_item: ExtractedData[Dict[str, Any]],
extraction_error: Optional[str] = None,
):
self.invalid_item = invalid_item
super().__init__("Not able to parse the extracted data, parsed invalid format")
self.extraction_error = extraction_error
# Build an informative error message
if extraction_error:
message = f"Extraction error: {extraction_error}"
else:
message = "Not able to parse the extracted data, parsed invalid format"
super().__init__(message)
def calculate_overall_confidence(
+4 -6
View File
@@ -3,7 +3,7 @@ from typing import BinaryIO
import os
from pathlib import Path
from llama_cloud.client import AsyncLlamaCloud
from llama_cloud.types import File, FileCreate
from llama_cloud.types import File
from typing import Optional
from llama_cloud_services.utils import SourceText, FileInput
@@ -73,11 +73,9 @@ class FileClient:
presigned_url = await self.client.files.generate_presigned_url(
project_id=self.project_id,
organization_id=self.organization_id,
request=FileCreate(
name=name,
external_file_id=external_file_id,
file_size=file_size,
),
name=name,
external_file_id=external_file_id,
file_size=file_size,
)
httpx_client = self.client._client_wrapper.httpx_client
upload_response = await httpx_client.put(
+19 -10
View File
@@ -21,7 +21,6 @@ from llama_cloud import (
PipelineCreateTransformConfig,
PipelineFileCreateCustomMetadataValue,
PipelineType,
ProjectCreate,
ManagedIngestionStatus,
CloudDocumentCreate,
CloudDocument,
@@ -507,14 +506,19 @@ class LlamaCloudIndex(BaseManagedIndex):
client = get_client(api_key, base_url, app_url, timeout)
if project_id is None:
# create project if it doesn't exist
project = client.projects.upsert_project(
# get project by name
projects = client.projects.list_projects(
organization_id=organization_id,
request=ProjectCreate(name=project_name),
project_name=project_name,
)
if not projects:
raise ValueError(
f"Project '{project_name}' not found. Please create it first in the LlamaCloud UI."
)
project = projects[0]
project_id = project.id
if verbose:
print(f"Created project {project_id} with name {project_name}")
print(f"Found project {project_id} with name {project_name}")
# create pipeline
pipeline_create = PipelineCreate(
@@ -563,15 +567,20 @@ class LlamaCloudIndex(BaseManagedIndex):
app_url = app_url or os.environ.get("LLAMA_CLOUD_APP_URL", DEFAULT_APP_URL)
aclient = get_aclient(api_key, base_url, app_url, timeout)
# create project if it doesn't exist
project = await aclient.projects.upsert_project(
organization_id=organization_id, request=ProjectCreate(name=project_name)
# get project by name
projects = await aclient.projects.list_projects(
organization_id=organization_id, project_name=project_name
)
if not projects:
raise ValueError(
f"Project '{project_name}' not found. Please create it first in the LlamaCloud UI."
)
project = projects[0]
if project.id is None:
raise ValueError(f"Failed to create/get project {project_name}")
raise ValueError(f"Failed to get project {project_name}")
if verbose:
print(f"Created project {project.id} with name {project.name}")
print(f"Found project {project.id} with name {project.name}")
# create pipeline
pipeline_create = PipelineCreate(
+3 -5
View File
@@ -751,11 +751,9 @@ class LlamaParse(BasePydanticReader):
file_path = str(file_input)
file_ext = os.path.splitext(file_path)[1].lower()
if file_ext not in SUPPORTED_FILE_TYPES:
raise Exception(
f"Currently, only the following file types are supported: {SUPPORTED_FILE_TYPES}\n"
f"Current file type: {file_ext}"
)
mime_type = mimetypes.guess_type(file_path)[0]
mime_type = "application/octet-stream"
else:
mime_type = mimetypes.guess_type(file_path)[0]
# Open the file here for the duration of the async context
# load data, set the mime type
fs = fs or get_default_fs()
+17
View File
@@ -1,5 +1,22 @@
# llama_parse
## 0.6.91
### Patch Changes
- 07ec282: Bump up patch versions for python packages
- Updated dependencies [07ec282]
- Updated dependencies [3040951]
- llama-cloud-services-py@0.6.91
## 0.6.90
### Patch Changes
- 19cbb25: Remove extension filter
- Updated dependencies [19cbb25]
- llama-cloud-services-py@0.6.90
## 0.6.89
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama_parse",
"version": "0.6.89",
"version": "0.6.91",
"description": "",
"main": "index.js",
"private": false,
+2 -2
View File
@@ -11,13 +11,13 @@ dev = [
[project]
name = "llama-parse"
version = "0.6.89"
version = "0.6.91"
description = "Parse files into RAG-Optimized formats."
authors = [{name = "Logan Markewich", email = "logan@llamaindex.ai"}]
requires-python = ">=3.9,<4.0"
readme = "README.md"
license = "MIT"
dependencies = ["llama-cloud-services>=0.6.89"]
dependencies = ["llama-cloud-services>=0.6.91"]
[project.scripts]
llama-parse = "llama_parse.cli.main:parse"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama-cloud-services-py",
"version": "0.6.89",
"version": "0.6.91",
"private": false,
"license": "MIT",
"scripts": {},
+2 -2
View File
@@ -23,7 +23,7 @@ dev = [
[project]
name = "llama-cloud-services"
version = "0.6.89"
version = "0.6.91"
description = "Tailored SDK clients for LlamaCloud services."
authors = [{name = "Logan Markewich", email = "logan@runllama.ai"}]
requires-python = ">=3.9,<4.0"
@@ -31,7 +31,7 @@ readme = "README.md"
license = "MIT"
dependencies = [
"llama-index-core>=0.12.0",
"llama-cloud==0.1.45",
"llama-cloud==0.1.46",
"pydantic>=2.8,!=2.10",
"click>=8.1.7,<9",
"python-dotenv>=1.0.1,<2",
+34 -2
View File
@@ -1,11 +1,43 @@
import os
from typing import List
from llama_cloud_services.extract import LlamaExtract
from typing import Any, Dict, List, Optional, Union
from llama_cloud.core.api_error import ApiError
from llama_cloud.types import ExtractConfig
from pydantic import BaseModel
from tenacity import (
retry,
retry_if_exception,
stop_after_attempt,
wait_exponential,
)
from llama_cloud_services.extract import ExtractionAgent, LlamaExtract
# Global storage for agents to cleanup
_TEST_AGENTS_TO_CLEANUP: List[str] = []
def _is_rate_limit_error(exception: BaseException) -> bool:
"""Check if the exception is a rate limit error (429)."""
return isinstance(exception, ApiError) and exception.status_code == 429
@retry(
retry=retry_if_exception(_is_rate_limit_error),
wait=wait_exponential(multiplier=1, min=1, max=30),
stop=stop_after_attempt(5),
reraise=True,
)
def create_agent_with_retry(
extractor: LlamaExtract,
name: str,
data_schema: Union[Dict[str, Any], type[BaseModel]],
config: Optional[ExtractConfig] = None,
) -> ExtractionAgent:
"""Create an extraction agent with retry logic for rate limiting."""
return extractor.create_agent(name=name, data_schema=data_schema, config=config)
def pytest_configure(config):
"""Register custom markers for extract tests."""
config.addinivalue_line("markers", "agent_name: custom agent name for test")
+2 -2
View File
@@ -6,7 +6,7 @@ from pydantic import BaseModel
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent, SourceText
from llama_cloud.types import ExtractConfig, ExtractMode, ExtractRun
from tests.extract.util import load_test_dotenv
from .conftest import register_agent_for_cleanup
from .conftest import register_agent_for_cleanup, create_agent_with_retry
load_test_dotenv()
@@ -87,7 +87,7 @@ def test_agent(llama_extract, test_agent_name, test_schema_dict, request):
except Exception as e:
print(f"Warning: Failed to cleanup existing agent: {e}")
agent = llama_extract.create_agent(name=name, data_schema=schema)
agent = create_agent_with_retry(llama_extract, name=name, data_schema=schema)
# Add agent to cleanup list via conftest helper
register_agent_for_cleanup(agent.id)
+5 -3
View File
@@ -8,7 +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
from .conftest import register_agent_for_cleanup, create_agent_with_retry
load_test_dotenv()
@@ -117,8 +117,10 @@ def extraction_agent(test_case: ExtractionTestCase, extractor: LlamaExtract):
except Exception as e:
print(f"Warning: Failed to cleanup existing agent: {str(e)}")
# Create new agent
agent = extractor.create_agent(agent_name, schema, config=test_case.config)
# Create new agent with retry logic for rate limiting
agent = create_agent_with_retry(
extractor, name=agent_name, data_schema=schema, config=test_case.config
)
# Register agent for cleanup at the end of the test session
register_agent_for_cleanup(agent.id)
+8 -5
View File
@@ -8,7 +8,6 @@ from llama_cloud import (
AutoTransformConfig,
PipelineCreate,
PipelineFileCreate,
ProjectCreate,
CompositeRetrievalMode,
LlamaParseParameters,
ReRankConfig,
@@ -60,11 +59,15 @@ def local_figures_file() -> str:
def _setup_index_with_file(
client: LlamaCloud, index_name: str, remote_file: Tuple[str, str]
) -> LlamaCloudIndex:
# create project if it doesn't exist
project_create = ProjectCreate(name=project_name)
project = client.projects.upsert_project(
organization_id=organization_id, request=project_create
# get project by name
projects = client.projects.list_projects(
organization_id=organization_id, project_name=project_name
)
if not projects:
raise ValueError(
f"Project '{project_name}' not found. Please create it first in the LlamaCloud UI."
)
project = projects[0]
# create pipeline
pipeline_create = PipelineCreate(
@@ -423,6 +423,7 @@ def create_extract_run(
},
data_schema: Dict[str, Any] = {},
file: File = create_file(),
error: Optional[str] = None,
) -> ExtractRun:
return ExtractRun.parse_obj(
{
@@ -439,6 +440,7 @@ def create_extract_run(
"status": "SUCCESS",
"project_id": str(uuid.uuid4()),
"from_ui": False,
"error": error,
}
)
@@ -544,6 +546,46 @@ def test_extracted_data_from_extraction_result_invalid_data():
assert invalid_data.field_metadata["name"].confidence == 0.9
assert invalid_data.overall_confidence == 0.9
# Verify default error message when no job error present
assert exc_info.value.extraction_error is None
assert "Not able to parse the extracted data" in str(exc_info.value)
def test_extracted_data_from_extraction_result_with_job_error():
"""Test ExtractedData.from_extraction_result with job-level error prominently displayed."""
job_error_message = "Failed to process document: unsupported file format"
# Create ExtractRun with both invalid data AND a job-level error
extract_run = create_extract_run(
data={
"missing_name": "Valid Name",
"age": "not_a_number",
}, # Invalid age, missing name
extraction_metadata={
"name": {"confidence": 0.9},
},
data_schema={},
file=create_file(id="error-file", name="bad_data.pdf"),
error=job_error_message,
)
# Should raise InvalidExtractionData with the job error prominently displayed
with pytest.raises(InvalidExtractionData) as exc_info:
ExtractedData.from_extraction_result(
extract_run, Person, metadata={"test": "metadata"}
)
# Verify the exception message prominently shows the job error
exception = exc_info.value
assert exception.extraction_error == job_error_message
assert f"Extraction error: {job_error_message}" == str(exception)
# Verify the invalid_item contains both errors in metadata
invalid_data = exception.invalid_item
assert invalid_data.metadata.get("job_error") == job_error_message
assert "extraction_error" in invalid_data.metadata # Validation error still present
assert "test" in invalid_data.metadata # Original metadata preserved
class Dimensions(BaseModel):
length: Optional[str] = Field(
+17 -16
View File
@@ -34,9 +34,10 @@ TEST_PIPELINE = Pipeline(
def mock_client() -> MagicMock:
"""Mock client with sensible defaults."""
client = MagicMock()
client.projects.upsert_project.return_value = Project(
default_project = Project(
id="default-proj", name=DEFAULT_PROJECT_NAME, organization_id="default-org"
)
client.projects.list_projects.return_value = [default_project]
client.pipelines.upsert_pipeline.return_value = Pipeline(
id="default-pipe",
name="default",
@@ -100,8 +101,8 @@ def test_from_documents_uses_provided_project_id(mock_client: MagicMock) -> None
project_id=provided_project_id,
)
# Assert - project upsert not called; pipeline uses provided project_id
mock_client.projects.upsert_project.assert_not_called()
# Assert - project list not called (project_id provided); pipeline uses provided project_id
mock_client.projects.list_projects.assert_not_called()
assert mock_client.pipelines.upsert_pipeline.call_count == 1
assert (
mock_client.pipelines.upsert_pipeline.call_args.kwargs["project_id"]
@@ -110,29 +111,29 @@ def test_from_documents_uses_provided_project_id(mock_client: MagicMock) -> None
assert index.project.id == provided_project_id
def test_from_documents_upserts_project_when_project_id_missing(
def test_from_documents_lists_project_when_project_id_missing(
mock_client: MagicMock,
) -> None:
organization_id = "org-xyz"
index_name = "my_new_index"
# Project is created when project_id is not provided
upserted_project = Project(
# Project is found by name when project_id is not provided
found_project = Project(
id="proj-999", name=DEFAULT_PROJECT_NAME, organization_id=organization_id
)
mock_client.projects.upsert_project.return_value = upserted_project
mock_client.projects.list_projects.return_value = [found_project]
test_pipeline = Pipeline(
id="pipe-xyz",
name=index_name,
project_id=upserted_project.id,
project_id=found_project.id,
embedding_config=EMBEDDING_CONFIG,
)
with patch.object(
base,
"resolve_project_and_pipeline",
return_value=(upserted_project, test_pipeline),
return_value=(found_project, test_pipeline),
):
docs = [Document(text="world")]
index = LlamaCloudIndex.from_documents(
@@ -141,15 +142,15 @@ def test_from_documents_upserts_project_when_project_id_missing(
organization_id=organization_id,
)
# Assert - project was upserted with org id and default project name
mock_client.projects.upsert_project.assert_called_once()
kwargs = mock_client.projects.upsert_project.call_args.kwargs
# Assert - project was listed with org id and default project name
mock_client.projects.list_projects.assert_called_once()
kwargs = mock_client.projects.list_projects.call_args.kwargs
assert kwargs["organization_id"] == organization_id
assert kwargs["request"].name == DEFAULT_PROJECT_NAME
assert kwargs["project_name"] == DEFAULT_PROJECT_NAME
# Pipeline created under the upserted project id
# Pipeline created under the found project id
assert (
mock_client.pipelines.upsert_pipeline.call_args.kwargs["project_id"]
== upserted_project.id
== found_project.id
)
assert index.project.id == upserted_project.id
assert index.project.id == found_project.id
Generated
+5 -5
View File
@@ -1595,21 +1595,21 @@ wheels = [
[[package]]
name = "llama-cloud"
version = "0.1.45"
version = "0.1.46"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "httpx" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/3a2a209f1c3fa516de172cb13e03f5a897adea5523f2ee0f544d035e3704/llama_cloud-0.1.45.tar.gz", hash = "sha256:140244008cc5710e31ae97c6043973a3a9969a51b0f38155fa33a8434078e8aa", size = 140968, upload-time = "2025-12-03T02:22:49.484Z" }
sdist = { url = "https://files.pythonhosted.org/packages/40/f3/f4d6520f8d546e6c5a02f6ebeed5c09774a074b8d2c24ad559ace97a56a6/llama_cloud-0.1.46.tar.gz", hash = "sha256:e86f8791c053590d70cc59e0fc13ce72f9b681a8e658bc61df86d0285288d8ee", size = 127752, upload-time = "2026-01-21T18:40:57.103Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/1d/466b0df69b81ce9410ad6ec7229a1e6601ff69640f02f246e06cfcc7428c/llama_cloud-0.1.45-py3-none-any.whl", hash = "sha256:500299a6d3f25f97bcf6755d6338523023564fa8f376955c2cf299bbc9561cc2", size = 397184, upload-time = "2025-12-03T02:22:48.335Z" },
{ url = "https://files.pythonhosted.org/packages/c4/3a/6caaea28c8c804add33c91d356ed7d5a5412d6c9598e1450af95a15e0bcd/llama_cloud-0.1.46-py3-none-any.whl", hash = "sha256:6c6546c09c04a038c86d84d42f00eae8fd3bff49991ad3aab844bd866ecdf352", size = 361989, upload-time = "2026-01-21T18:40:54.863Z" },
]
[[package]]
name = "llama-cloud-services"
version = "0.6.88"
version = "0.6.90"
source = { editable = "." }
dependencies = [
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
@@ -1649,7 +1649,7 @@ dev = [
requires-dist = [
{ name = "click", specifier = ">=8.1.7,<9" },
{ name = "eval-type-backport", marker = "python_full_version < '3.10'", specifier = ">=0.2.0,<0.3" },
{ name = "llama-cloud", specifier = "==0.1.45" },
{ name = "llama-cloud", specifier = "==0.1.46" },
{ name = "llama-index-core", specifier = ">=0.12.0" },
{ name = "packaging", specifier = ">=23.0" },
{ name = "platformdirs", specifier = ">=4.3.7,<5" },
+6
View File
@@ -1,5 +1,11 @@
# llama-cloud-services
## 0.5.3
### Patch Changes
- d7864af: bugfixes in retry logic for LlamaExtract and LlamaClassify
## 0.5.2
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama-cloud-services",
"version": "0.5.2",
"version": "0.5.3",
"type": "module",
"license": "MIT",
"scripts": {
+8 -9
View File
@@ -108,20 +108,19 @@ async function pollForJobCompletion({
}
const response =
await getClassifyJobApiV1ClassifierJobsClassifyJobIdGet(jobOptions);
if (!response.response.ok) {
numIterations++;
}
if (typeof response.data != "undefined") {
status = response.data.status as StatusEnum;
if (status == StatusEnum.CANCELLED || status == StatusEnum.ERROR) {
throw new Error("There was an error during the classification job.");
} else if (status == StatusEnum.SUCCESS) {
throw new Error("There was an error extracting data from your file.");
} else if (
status == StatusEnum.SUCCESS ||
status == StatusEnum.PARTIAL_SUCCESS
) {
return true;
} else {
numIterations++;
await sleep(interval * 1000);
}
}
numIterations++;
await sleep(interval * 1000);
}
}
@@ -169,7 +168,7 @@ async function getJobResult({
retries++;
await sleep(retryInterval * 1000);
}
if (typeof response.data != "undefined") {
if (response.response.ok && typeof response.data != "undefined") {
return response.data as ClassifyJobResults;
} else {
throw new Error(
+3 -7
View File
@@ -296,20 +296,16 @@ async function pollForJobCompletion(
return false;
}
const response = await getJobApiV1ExtractionJobsJobIdGet(jobOptions);
if (!response.response.ok) {
numIterations++;
}
if (typeof response.data != "undefined") {
status = response.data.status as StatusEnum;
if (status == StatusEnum.CANCELLED || status == StatusEnum.ERROR) {
throw new Error("There was an error extracting data from your file.");
} else if (status == StatusEnum.SUCCESS) {
return true;
} else {
numIterations++;
await sleep(interval * 1000);
}
}
numIterations++;
await sleep(interval * 1000);
}
}
@@ -350,7 +346,7 @@ async function getJobResult(
retries++;
await sleep(retryInterval * 1000);
}
if (typeof response.data != "undefined") {
if (response.response.ok && typeof response.data != "undefined") {
return {
data: response.data.data,
extractionMetadata: response.data.extraction_metadata,