Compare commits

..

2 Commits

Author SHA1 Message Date
Neeraj Pradhan b66b47a708 Bump to version 0.6.9 (#663)
* Bump to version 0.6.8

* add banks as dep

* Add platformdirs to poetry

* Fix version number
2025-03-28 17:07:46 -07:00
George He fe485ff62e fix:Add retry handling to parse and backoff patterns - catching 5XX errors and HTTP errors (#648)
* Add parse retry logic

* Update code cleanliness

* Update errors

* Fix lint

* Fix backoff strategies

* Update docs

* Fix errors

* Add base
2025-03-26 12:09:56 +01:00
7 changed files with 559 additions and 437 deletions
+10 -2
View File
@@ -39,7 +39,7 @@ SchemaInput = Union[JSONObjectType, Type[BaseModel]]
DEFAULT_EXTRACT_CONFIG = ExtractConfig(
extraction_target=ExtractTarget.PER_DOC,
extraction_mode=ExtractMode.ACCURATE,
extraction_mode=ExtractMode.BALANCED,
)
@@ -564,6 +564,14 @@ class LlamaExtract(BaseComponent):
Returns:
ExtractionAgent: The created extraction agent
"""
if config is not None:
if config.extraction_mode == ExtractMode.ACCURATE:
warnings.warn(
"ACCURATE extraction mode is deprecated. Using BALANCED instead."
)
config.extraction_mode = ExtractMode.BALANCED
else:
config = DEFAULT_EXTRACT_CONFIG
if isinstance(data_schema, dict):
data_schema = data_schema
@@ -581,7 +589,7 @@ class LlamaExtract(BaseComponent):
request=ExtractAgentCreate(
name=name,
data_schema=data_schema,
config=config or DEFAULT_EXTRACT_CONFIG,
config=config,
),
)
)
+85 -34
View File
@@ -4,6 +4,7 @@ import os
import time
from contextlib import asynccontextmanager
from copy import deepcopy
from enum import Enum
from io import BufferedIOBase
from pathlib import Path, PurePath, PurePosixPath
from typing import Any, AsyncGenerator, Dict, List, Optional, Union
@@ -52,6 +53,14 @@ def build_url(
return base_url
class BackoffPattern(str, Enum):
"""Backoff pattern for polling."""
CONSTANT = "constant"
LINEAR = "linear"
EXPONENTIAL = "exponential"
class LlamaParse(BasePydanticReader):
"""A smart-parser for files."""
@@ -78,6 +87,15 @@ class LlamaParse(BasePydanticReader):
description="The interval in seconds to check if the parsing is done.",
)
backoff_pattern: BackoffPattern = Field(
default=BackoffPattern.LINEAR,
description="Controls the backoff pattern when retrying failed requests: 'constant', 'linear', or 'exponential'.",
)
max_check_interval: int = Field(
default=5,
description="Maximum interval in seconds between polling attempts when checking job status.",
)
custom_client: Optional[httpx.AsyncClient] = Field(
default=None, description="A custom HTTPX client to use for sending requests."
)
@@ -794,54 +812,87 @@ class LlamaParse(BasePydanticReader):
if file_handle is not None:
file_handle.close()
def _calculate_backoff(self, current_interval: float) -> float:
"""Calculate the next backoff interval based on the backoff pattern.
Args:
current_interval: The current interval in seconds
Returns:
The next interval in seconds
"""
if self.backoff_pattern == BackoffPattern.CONSTANT:
return current_interval
elif self.backoff_pattern == BackoffPattern.LINEAR:
return min(current_interval + 1, float(self.max_check_interval))
elif self.backoff_pattern == BackoffPattern.EXPONENTIAL:
return min(current_interval * 2, float(self.max_check_interval))
return current_interval # Default fallback
async def _get_job_result(
self, job_id: str, result_type: str, verbose: bool = False
) -> Dict[str, Any]:
start = time.time()
tries = 0
error_count = 0
current_interval: float = float(self.check_interval)
# so we're not re-setting the headers & stuff on each
# usage... assume that there is not some other
# coro also modifying base_url and the other client related configs.
client = self.aclient
while True:
await asyncio.sleep(self.check_interval)
tries += 1
result = await client.get(JOB_STATUS_ROUTE.format(job_id=job_id))
if result.status_code != 200:
try:
await asyncio.sleep(current_interval)
tries += 1
result = await client.get(JOB_STATUS_ROUTE.format(job_id=job_id))
result.raise_for_status() # this raises if status is not 2xx
# Allowed values "PENDING", "SUCCESS", "ERROR", "CANCELED"
result_json = result.json()
status = result_json["status"]
if status == "SUCCESS":
parsed_result = await client.get(
JOB_RESULT_URL.format(job_id=job_id, result_type=result_type),
)
return parsed_result.json()
elif status == "PENDING":
end = time.time()
if end - start > self.max_timeout:
raise Exception(f"Timeout while parsing the file: {job_id}")
if verbose and tries % 10 == 0:
print(".", end="", flush=True)
current_interval = self._calculate_backoff(current_interval)
else:
error_code = result_json.get("error_code", "No error code found")
error_message = result_json.get(
"error_message", "No error message found"
)
exception_str = (
f"Job ID: {job_id} failed with status: {status}, "
f"Error code: {error_code}, Error message: {error_message}"
)
raise Exception(exception_str)
except (
httpx.ConnectError,
httpx.ReadError,
httpx.WriteError,
httpx.ConnectTimeout,
httpx.ReadTimeout,
httpx.WriteTimeout,
httpx.HTTPStatusError,
) as err:
error_count += 1
end = time.time()
if end - start > self.max_timeout:
raise Exception(f"Timeout while parsing the file: {job_id}")
raise Exception(
f"Timeout while parsing the file: {job_id}"
) from err
if verbose and tries % 10 == 0:
print(".", end="", flush=True)
await asyncio.sleep(self.check_interval)
continue
# Allowed values "PENDING", "SUCCESS", "ERROR", "CANCELED"
result_json = result.json()
status = result_json["status"]
if status == "SUCCESS":
parsed_result = await client.get(
JOB_RESULT_URL.format(job_id=job_id, result_type=result_type),
)
return parsed_result.json()
elif status == "PENDING":
end = time.time()
if end - start > self.max_timeout:
raise Exception(f"Timeout while parsing the file: {job_id}")
if verbose and tries % 10 == 0:
print(".", end="", flush=True)
await asyncio.sleep(self.check_interval)
else:
error_code = result_json.get("error_code", "No error code found")
error_message = result_json.get(
"error_message", "No error message found"
)
exception_str = f"Job ID: {job_id} failed with status: {status}, Error code: {error_code}, Error message: {error_message}"
raise Exception(exception_str)
print(
f"HTTP error: {err}...",
flush=True,
)
current_interval = self._calculate_backoff(current_interval)
async def _aload_data(
self,
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "llama-parse"
version = "0.6.8"
version = "0.6.9"
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.7"
llama-cloud-services = ">=0.6.9"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
Generated
+457 -395
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -8,7 +8,7 @@ python_version = "3.10"
[tool.poetry]
name = "llama-cloud-services"
version = "0.6.8"
version = "0.6.9"
description = "Tailored SDK clients for LlamaCloud services."
authors = ["Logan Markewich <logan@runllama.ai>"]
license = "MIT"
@@ -18,11 +18,12 @@ packages = [{include = "llama_cloud_services"}]
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
llama-index-core = ">=0.11.0"
llama-cloud = "^0.1.16"
llama-cloud = "^0.1.17"
pydantic = "!=2.10"
click = "^8.1.7"
python-dotenv = "^1.0.1"
eval-type-backport = {python = "<3.10", version = "^0.2.0"}
platformdirs = "^4.3.7"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
+1 -1
View File
@@ -60,7 +60,7 @@ def get_test_cases():
settings = [
ExtractConfig(extraction_mode=ExtractMode.FAST),
ExtractConfig(extraction_mode=ExtractMode.ACCURATE),
ExtractConfig(extraction_mode=ExtractMode.BALANCED),
]
for input_file in sorted(input_files):
+1 -1
View File
@@ -55,7 +55,7 @@ def get_test_cases():
settings = [
ExtractConfig(extraction_mode=ExtractMode.FAST),
ExtractConfig(extraction_mode=ExtractMode.ACCURATE),
ExtractConfig(extraction_mode=ExtractMode.BALANCED),
]
for input_file in sorted(input_files):