Compare commits

..

3 Commits

Author SHA1 Message Date
Neeraj Pradhan 356f806688 compat python 3.9 2025-04-09 09:52:37 -07:00
Neeraj Pradhan d06c17ed37 backwards compatibility 2025-04-08 22:24:58 -07:00
Neeraj Pradhan e1ec231484 Fix bytes input for LlamaExtract 2025-04-08 21:33:41 -07:00
9 changed files with 675 additions and 795 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ This includes:
- [LlamaParse](./parse.md) - A GenAI-native document parser that can parse complex document data for any downstream LLM use case (Agents, RAG, data processing, etc.).
- [LlamaReport (beta/invite-only)](./report.md) - A prebuilt agentic report builder that can be used to build reports from a variety of data sources.
- [LlamaExtract](./extract.md) - A prebuilt agentic data extractor that can be used to transform data into a structured JSON representation.
- [LlamaExtract (beta/invite-only)](./extract.md) - A prebuilt agentic data extractor that can be used to transform data into a structured JSON representation.
## Getting Started
@@ -36,7 +36,7 @@ See the quickstart guides for each service for more information:
- [LlamaParse](./parse.md)
- [LlamaReport (beta/invite-only)](./report.md)
- [LlamaExtract](./extract.md)
- [LlamaExtract (beta/invite-only)](./extract.md)
## Switch to EU SaaS 🇪🇺
+6 -35
View File
@@ -1,6 +1,6 @@
# LlamaExtract
LlamaExtract provides a simple API for extracting structured data from unstructured documents like PDFs, text files and images.
LlamaExtract provides a simple API for extracting structured data from unstructured documents like PDFs, text files and images (upcoming).
## Quick Start
@@ -97,12 +97,11 @@ _LlamaExtract only supports a subset of the JSON Schema specification._ While li
be sufficient for a wide variety of use-cases.
- All fields are required by default. Nullable fields must be explicitly marked as such,
using `anyOf` with a `null` type. See `"start_date"` field above.
- Root node must be of type `object`.
using `"anyOf"` with a `"null"` type. See `"start_date"` field above.
- Root node must be of type `"object"`.
- Schema nesting must be limited to within 5 levels.
- The important fields are key names/titles, type and description. Fields for
formatting, default values, etc. are **not supported**. If you need these, you can add the
restrictions to your field description and/or use a post-processing step. e.g. default values can be supported by making a field optional and then setting `"null"` values from the extraction result to the default value.
formatting, default values, etc. are not supported.
- There are other restrictions on number of keys, size of the schema, etc. that you may
hit for complex extraction use cases. In such cases, it is worth thinking how to restructure
your extraction workflow to fit within these constraints, e.g. by extracting subset of fields
@@ -110,23 +109,6 @@ be sufficient for a wide variety of use-cases.
## Other Extraction APIs
### Extraction over bytes or text
You can use the `SourceText` class to extract from bytes or text directly without using a file. If passing the file bytes,
you will need to pass the filename to the `SourceText` class.
```python
with open("resume.pdf", "rb") as f:
file_bytes = f.read()
result = test_agent.extract(SourceText(file=file_bytes, filename="resume.pdf"))
```
```python
result = test_agent.extract(
SourceText(text_content="Candidate Name: Jane Doe")
)
```
### Batch Processing
Process multiple files asynchronously:
@@ -177,18 +159,16 @@ pip install llama-extract==0.1.0
## Tips & Best Practices
At the core of LlamaExtract is the schema, which defines the structure of the data you want to extract from your documents.
1. **Schema Design**:
- Try to limit schema nesting to 3-4 levels.
- Make fields optional when data might not always be present. Having required fields may force the model
to hallucinate when these fields are not present in the documents.
- When you want to extract a variable number of entities, use an `array` type. However, note that you cannot use
- When you want to extract a variable number of entities, use an `array` type. Note that you cannot use
an `array` type for the root node.
- Use descriptive field names and detailed descriptions. Use descriptions to pass formatting
instructions or few-shot examples.
- Above all, start simple and iteratively build your schema to incorporate requirements.
- Start simple and iteratively build your schema to incorporate requirements.
2. **Running Extractions**:
- Note that resetting `agent.schema` will not save the schema to the database,
@@ -197,15 +177,6 @@ At the core of LlamaExtract is the schema, which defines the structure of the da
part of `job.error` or `extraction_run.error` fields for debugging.
- Consider async operations (`queue_extraction`) for large-scale extraction once you have finalized your schema.
### Hitting "The response was too long to be processed" Error
This implies that the extraction response is hitting output token limits of the LLM. In such cases, it is worth rethinking the design of your schema to enable a more efficient/scalable extraction. e.g.
- Instead of one field that extracts a complex object, you can use multiple fields to distribute the extraction logic.
- You can also use multiple schemas to extract different subsets of fields from the same document and merge them later.
Another option (orthogonal to the above) is to break the document into smaller sections and extract from each section individually, when possible. LlamaExtract will in most cases be able to handle both document and schema chunking automatically, but there are cases where you may need to do this manually.
## Additional Resources
- [Example Notebook](examples/resume_screening.ipynb) - Detailed walkthrough of resume parsing
+62 -105
View File
@@ -1,7 +1,7 @@
import asyncio
import os
import time
from io import BufferedIOBase, BufferedReader, BytesIO, TextIOWrapper
from io import BufferedIOBase, BufferedReader, BytesIO
from pathlib import Path
from typing import List, Optional, Type, Union, Coroutine, Any, TypeVar
import warnings
@@ -46,76 +46,27 @@ DEFAULT_EXTRACT_CONFIG = ExtractConfig(
class SourceText:
def __init__(
self,
*,
file: Union[bytes, BufferedIOBase, TextIOWrapper, str, Path, None] = None,
text_content: Optional[str] = None,
file: Union[bytes, BufferedIOBase, str, Path],
filename: Optional[str] = None,
):
self.file = file
self.filename = filename
self.text_content = text_content
self._validate()
def _validate(self) -> None:
"""Ensure filename is provided when needed."""
if not ((self.file is None) ^ (self.text_content is None)):
raise ValueError("Either file or text_content must be provided.")
if self.text_content is not None:
if not self.filename:
self.filename = "text_input.txt"
return
if isinstance(self.file, (bytes, BufferedIOBase, TextIOWrapper)):
if isinstance(self.file, (bytes, BufferedIOBase)):
if not self.filename and hasattr(self.file, "name"):
self.filename = os.path.basename(str(self.file.name))
elif not hasattr(self.file, "name") and self.filename is None:
raise ValueError(
"filename must be provided when file is bytes or a file-like object without a name"
)
elif isinstance(self.file, (str, Path)):
if not self.filename:
self.filename = os.path.basename(str(self.file))
else:
raise ValueError(f"Unsupported file type: {type(self.file)}")
FileInput = Union[str, Path, BufferedIOBase, SourceText]
def run_in_thread(
coro: Coroutine[Any, Any, T],
thread_pool: ThreadPoolExecutor,
verify: bool,
httpx_timeout: float,
client_wrapper: Any,
) -> T:
"""Run coroutine in a thread with proper client management."""
async def wrapped_coro() -> T:
client = httpx.AsyncClient(
verify=verify,
timeout=httpx_timeout,
limits=httpx.Limits(max_keepalive_connections=100, max_connections=100),
)
original_client = client_wrapper.httpx_client
try:
client_wrapper.httpx_client = client
return await coro
finally:
client_wrapper.httpx_client = original_client
await client.aclose()
def run_coro() -> T:
try:
return asyncio.run(wrapped_coro())
except httpx.TimeoutException as e:
raise TimeoutError(f"Request timed out: {str(e)}") from e
except httpx.NetworkError as e:
raise ConnectionError(f"Network error: {str(e)}") from e
return thread_pool.submit(run_coro).result()
class ExtractionAgent:
"""Class representing a single extraction agent with methods for extraction operations."""
@@ -150,6 +101,31 @@ class ExtractionAgent:
max_workers=min(10, (os.cpu_count() or 1) + 4)
)
def _run_in_thread(self, coro: Coroutine[Any, Any, T]) -> T:
"""Run coroutine in a separate thread to avoid event loop issues"""
def run_coro() -> T:
async def wrapped_coro() -> T:
# Get the original client to preserve its configuration
original_client = self._client._client_wrapper.httpx_client
# Create a new client with the same configuration as the original
async with httpx.AsyncClient(
verify=self.verify,
timeout=self.httpx_timeout,
) as client:
# Temporarily replace the client
self._client._client_wrapper.httpx_client = client
try:
return await coro
finally:
# Restore the original client
self._client._client_wrapper.httpx_client = original_client
return asyncio.run(wrapped_coro())
return self._thread_pool.submit(run_coro).result()
@property
def id(self) -> str:
return self._agent.id
@@ -189,16 +165,6 @@ class ExtractionAgent:
def config(self, config: ExtractConfig) -> None:
self._config = config
def _run_in_thread(self, coro: Coroutine[Any, Any, T]) -> T:
"""Run coroutine in a separate thread to avoid event loop issues"""
return run_in_thread(
coro,
self._thread_pool,
self.verify, # type: ignore
self.httpx_timeout, # type: ignore
self._client._client_wrapper,
)
async def upload_file(self, file_input: SourceText) -> File:
"""Upload a file for extraction.
@@ -211,25 +177,12 @@ class ExtractionAgent:
"""
try:
file_contents: Union[BufferedIOBase, BytesIO]
if file_input.text_content is not None:
# Handle direct text content
file_contents = BytesIO(file_input.text_content.encode("utf-8"))
elif isinstance(file_input.file, TextIOWrapper):
# Handle text-based IO objects
file_contents = BytesIO(file_input.file.read().encode("utf-8"))
elif isinstance(file_input.file, (str, Path)):
# Handle file paths
if isinstance(file_input.file, (str, Path)):
file_contents = open(file_input.file, "rb")
elif isinstance(file_input.file, bytes):
# Handle bytes
file_contents = BytesIO(file_input.file)
elif isinstance(file_input.file, BufferedIOBase):
# Handle binary IO objects
file_contents = file_input.file
else:
raise ValueError(f"Unsupported file type: {type(file_input.file)}")
file_contents = file_input.file
# Add name attribute to file object if needed
if not hasattr(file_contents, "name"):
file_contents.name = file_input.filename # type: ignore
@@ -312,7 +265,7 @@ class ExtractionAgent:
)
)
async def _run_extraction_test(
async def _queue_extraction_test(
self,
files: Union[FileInput, List[FileInput]],
extract_settings: LlamaExtractSettings,
@@ -346,7 +299,7 @@ class ExtractionAgent:
job_tasks = [run_job(file) for file in uploaded_files]
with augment_async_errors():
extract_results = await run_jobs(
extract_jobs = await run_jobs(
job_tasks,
workers=self.num_workers,
desc="Running extraction jobs",
@@ -354,13 +307,15 @@ class ExtractionAgent:
)
if self._verbose:
for file, job in zip(files, extract_results):
for file, job in zip(files, extract_jobs):
file_repr = (
str(file) if isinstance(file, (str, Path)) else "<bytes/buffer>"
)
print(f"Running extraction for file {file_repr} under job_id {job.id}")
print(
f"Queued file extraction for file {file_repr} under job_id {job.id}"
)
return extract_results[0] if single_file else extract_results
return extract_jobs[0] if single_file else extract_jobs
async def queue_extraction(
self,
@@ -522,14 +477,6 @@ class ExtractionAgent:
def __repr__(self) -> str:
return f"ExtractionAgent(id={self.id}, name={self.name})"
def __del__(self) -> None:
"""Cleanup resources properly."""
try:
if hasattr(self, "_thread_pool"):
self._thread_pool.shutdown(wait=True)
except Exception:
pass # Suppress exceptions during cleanup
class LlamaExtract(BaseComponent):
"""Factory class for creating and managing extraction agents."""
@@ -632,13 +579,31 @@ class LlamaExtract(BaseComponent):
def _run_in_thread(self, coro: Coroutine[Any, Any, T]) -> T:
"""Run coroutine in a separate thread to avoid event loop issues"""
return run_in_thread(
coro,
self._thread_pool,
self.verify, # type: ignore
self.httpx_timeout, # type: ignore
self._async_client._client_wrapper,
)
def run_coro() -> T:
# Create a new client for this thread
async def wrapped_coro() -> T:
assert (
self._httpx_client is not None
), "httpx_client should be initialized"
# Create a new client with the same configuration as the original
async with httpx.AsyncClient(
verify=self.verify,
timeout=self.httpx_timeout,
) as client:
# Temporarily replace the client
self._async_client._client_wrapper.httpx_client = client
try:
return await coro
finally:
# Restore the original client
self._async_client._client_wrapper.httpx_client = (
self._httpx_client
)
return asyncio.run(wrapped_coro())
return self._thread_pool.submit(run_coro).result()
def create_agent(
self,
@@ -783,14 +748,6 @@ class LlamaExtract(BaseComponent):
)
)
def __del__(self) -> None:
"""Cleanup resources properly."""
try:
if hasattr(self, "_thread_pool"):
self._thread_pool.shutdown(wait=True)
except Exception:
pass # Suppress exceptions during cleanup
if __name__ == "__main__":
from dotenv import load_dotenv
+1 -7
View File
@@ -185,10 +185,7 @@ class LlamaParse(BasePydanticReader):
default=None,
description="The top margin of the bounding box to use to extract text from documents expressed as a float between 0 and 1 representing the percentage of the page height.",
)
compact_markdown_table: Optional[bool] = Field(
default=False,
description="If set to true, the parser will output compact markdown table (without trailing spaces in cells).",
)
continuous_mode: Optional[bool] = Field(
default=False,
description="Parse documents continuously, leading to better results on documents where tables span across two pages.",
@@ -602,9 +599,6 @@ class LlamaParse(BasePydanticReader):
if self.bbox_top is not None:
data["bbox_top"] = self.bbox_top
if self.compact_markdown_table:
data["compact_markdown_table"] = self.compact_markdown_table
if self.complemental_formatting_instruction:
print(
"WARNING: complemental_formatting_instruction is deprecated and may be remove in a future release. Use system_prompt, system_prompt_append or user_prompt instead."
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "llama-parse"
version = "0.6.11"
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.11"
llama-cloud-services = ">=0.6.9"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
Generated
+599 -617
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -8,7 +8,7 @@ python_version = "3.10"
[tool.poetry]
name = "llama-cloud-services"
version = "0.6.11"
version = "0.6.9"
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.11.0"
llama-cloud = "^0.1.18"
llama-cloud = "^0.1.17"
pydantic = "!=2.10"
click = "^8.1.7"
python-dotenv = "^1.0.1"
+1 -1
View File
@@ -133,7 +133,7 @@ async def test_extraction(
test_case: TestCase, extraction_agent: ExtractionAgent
) -> None:
start = perf_counter()
result = await extraction_agent._run_extraction_test(
result = await extraction_agent._queue_extraction_test(
test_case.input_file,
extract_settings=LlamaExtractSettings(
llama_parse_params=LlamaParseParameters(
-24
View File
@@ -150,14 +150,6 @@ class TestExtractionAgent:
assert "title" in result.data
assert "summary" in result.data
def test_extract_file_from_buffered_io(self, test_agent):
result = test_agent.extract(SourceText(file=open(TEST_PDF, "rb")))
assert result.status == "SUCCESS"
assert result.data is not None
assert isinstance(result.data, dict)
assert "title" in result.data
assert "summary" in result.data
def test_extract_file_from_bytes(self, test_agent):
with open(TEST_PDF, "rb") as f:
file_bytes = f.read()
@@ -168,22 +160,6 @@ class TestExtractionAgent:
assert "title" in result.data
assert "summary" in result.data
def test_extract_from_text_content(self, test_agent):
TEST_TEXT = """
# Llamas
Llamas are social animals and live with others as a herd. Their wool is soft and
contains only a small amount of lanolin.[2] Llamas can learn simple tasks after a
few repetitions. When using a pack, they can carry about 25 to 30% of their body
weight for 8 to 13 km (58 miles).[3] The name llama (also historically spelled
"glama") was adopted by European settlers from native Peruvians.
"""
result = test_agent.extract(SourceText(text_content=TEST_TEXT))
assert result.status == "SUCCESS"
assert result.data is not None
assert isinstance(result.data, dict)
assert "title" in result.data
assert "summary" in result.data
@pytest.mark.asyncio
async def test_extract_multiple_files(self, test_agent):
files = [TEST_PDF, TEST_PDF] # Using same file twice for testing