Compare commits

...

7 Commits

Author SHA1 Message Date
Adrian Lyjak 083d8109c2 Make versioning a little easier, and fix llama_parse version (#808)
* Make versioning a little easier

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

* add JobResult.get_text/get_markdown/get_json

* add tests
2025-07-18 18:50:29 +09:00
Adrian Lyjak 3f311c0669 Bump v0.6.49 (#797) 2025-07-16 19:42:09 -04:00
Adrian Lyjak b1a2f9d42b Add new method to fetch the full, non-paginated markdown (#796)
Add new method to fetch the full, non-paginated markdown for proper merge_tables_across_pages_in_markdown support
2025-07-16 19:29:57 -04:00
11 changed files with 331 additions and 20 deletions
+19
View File
@@ -0,0 +1,19 @@
# Installation
This project uses poetry. Create a virtual environment, and run `poetry install`
# Versioning (Maintainers only)
Before merging your changes, make sure to bump the versions.
Make a version bump to `pyproject.toml`. If the underlying dependency on the llamacloud platform OpenAPI
sdk needs bumping, make sure to bring that in as well. If updating dependencies, run `poetry lock`.
The legacy `llama_parse` package re-exports some of `llama_cloud_services` in the old namespace. The
versions need to be kept consistent to sidecar it with `llama_cloud_services`. Bump it's version in `llama_parse/pyproject.toml`, and also bump it's dependency version of `llama-cloud-services` to match.
You can also do this with `./scripts/version-bump.py set 0.x.x` if you have `uv` installed.
Once the change is merged, push a tag `git tag -a v0.x.x -m 0.x.x` and `git push origin 0.x.x`.
This tagging step can be done with `./scripts/version-bump tag`.
@@ -42,7 +42,7 @@ def agent_data_retry(func: WrappedFn) -> WrappedFn:
)(func)
def get_default_agent_id() -> Optional[str]:
def get_default_agent_id() -> str:
"""
Retrieve the default agent ID from environment variables.
@@ -55,7 +55,7 @@ def get_default_agent_id() -> Optional[str]:
via environment variables instead of passing it explicitly
to each client instance.
"""
return os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME")
return os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME") or "_public"
class AsyncAgentDataClient(Generic[AgentDataT]):
@@ -144,10 +144,6 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
"""
self.agent_url_id = agent_url_id or get_default_agent_id()
if not self.agent_url_id:
raise ValueError(
"Agent ID is required, or set the LLAMA_DEPLOY_DEPLOYMENT_NAME environment variable"
)
self.collection = collection
if not client:
+83
View File
@@ -381,6 +381,10 @@ class LlamaParse(BasePydanticReader):
default=False,
description="Preserve grid alignment across page in text mode.",
)
preserve_very_small_text: Optional[bool] = Field(
default=False,
description="If set, the parser will try to preserve very small text lines. This can be useful for documents containing vector graphics with very small text lines that may not be recognized by OCR or a vision model (such as in CAD drawings).",
)
replace_failed_page_mode: Optional[FailedPageMode] = Field(
default=None,
description="The mode to use to replace the failed page, see FailedPageMode enum for possible value. If set, the parser will replace the failed page with the specified mode. If not set, the default mode (raw_text) will be used.",
@@ -912,6 +916,9 @@ class LlamaParse(BasePydanticReader):
"preserve_layout_alignment_across_pages"
] = self.preserve_layout_alignment_across_pages
if self.preserve_very_small_text:
data["preserve_very_small_text"] = self.preserve_very_small_text
if self.preset is not None:
data["preset"] = self.preset
@@ -1697,3 +1704,79 @@ class LlamaParse(BasePydanticReader):
sub_docs.append(sub_doc)
return sub_docs
async def aget_result(
self, job_id: Union[str, List[str]]
) -> Union[JobResult, List[JobResult]]:
"""
Return JobResult object for previously parsed job(s).
If the job is still pending, the result will not be returned until it is completed.
Args:
job_id: Job ID or list of multiple Job IDs to be retrieved.
Returns:
JobResult object or list of JobResult objects if multiple job IDs were provided.
"""
if isinstance(job_id, str):
result = await self._get_job_result(
job_id, ResultType.JSON.value, verbose=self.verbose
)
return JobResult(
job_id=job_id,
file_name="",
job_result=result,
api_key=self.api_key,
base_url=self.base_url,
client=self.aclient,
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
)
elif isinstance(job_id, list):
results = []
jobs = [
self._get_job_result(id_, ResultType.JSON.value, verbose=self.verbose)
for id_ in job_id
]
results = await run_jobs(
jobs,
workers=self.num_workers,
desc="Getting job results",
show_progress=self.show_progress,
)
return [
JobResult(
job_id=job_id[i],
file_name="",
job_result=result,
api_key=self.api_key,
base_url=self.base_url,
client=self.aclient,
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
)
for i, result in enumerate(results)
]
else:
raise ValueError("The input job_id must be a string or a list of strings.")
def get_result(
self, job_id: Union[str, List[str]]
) -> Union[JobResult, List[JobResult]]:
"""
Return JobResult object for previously parsed job(s).
If the job is still pending, the result will not be returned until it is completed.
Args:
job_id: Job ID or list of multiple Job IDs to be retrieved.
Returns:
JobResult object or list of JobResult objects if multiple job IDs were provided.
"""
try:
return asyncio_run(self.aget_result(job_id))
except RuntimeError as e:
if nest_asyncio_err in str(e):
raise RuntimeError(nest_asyncio_msg)
else:
raise e
+52 -1
View File
@@ -295,13 +295,64 @@ class JobResult(BaseModel):
async def aget_markdown_nodes(self, split_by_page: bool = False) -> List[TextNode]:
"""
Get the markdown nodes from the job.
Args:
split_by_page: Whether to split the pages into separate documents
"""
documents = await self.aget_markdown_documents(split_by_page)
return [TextNode(text=doc.text, metadata=doc.metadata) for doc in documents]
def get_markdown(self) -> str:
"""
Get the raw parsed markdown from the job, distinct from the markdown documents.
This does not include page separators, e.g. if merge_tables_across_pages_in_markdown is True
"""
return asyncio_run(self.aget_markdown())
async def aget_markdown(self) -> str:
"""
Get the raw parsed markdown from the job, distinct from the markdown documents.
This does not include page separators, e.g. if merge_tables_across_pages_in_markdown is True
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/raw/markdown"
response = await make_api_request(self._client, "GET", url)
return response.content.decode("utf-8")
def get_text(self) -> str:
"""
Get the raw parsed text from the job.
"""
return asyncio_run(self.aget_text())
async def aget_text(self) -> str:
"""
Get the raw parsed text from the job.
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/raw/text"
response = await make_api_request(self._client, "GET", url)
return response.content.decode("utf-8")
def get_json(self) -> Dict[str, Any]:
"""
Get the full parsed JSON result from the job.
Note:
This is not the same as JobResult.json(), which is a
JSON serialized version of the JobResult Page Documents.
"""
return asyncio_run(self.aget_json())
async def aget_json(self) -> Dict[str, Any]:
"""
Get the full parsed JSON result from the job.
Note:
This is not the same as JobResult.json(), which is a
JSON serialized version of the JobResult Page Documents.
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/json"
response = await make_api_request(self._client, "GET", url)
return response.json()
async def _get_image_document_with_bytes(
self, image: ImageItem, page: Page
) -> ImageDocument:
+8 -8
View File
@@ -1170,14 +1170,14 @@ test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"
[[package]]
name = "llama-cloud"
version = "0.1.33"
version = "0.1.34"
description = ""
optional = false
python-versions = "<4,>=3.8"
groups = ["main"]
files = [
{file = "llama_cloud-0.1.33-py3-none-any.whl", hash = "sha256:35b7d4a30b013f0a343f7e09126b531c697d65bffd4eb4d2d79bf7d65f256178"},
{file = "llama_cloud-0.1.33.tar.gz", hash = "sha256:a0bb900d5a6e86f8c767b48686c5253679ad7ca1b57612dc39b0767e57ad3d78"},
{file = "llama_cloud-0.1.34-py3-none-any.whl", hash = "sha256:9b06fb109b1d9f652095a11732ae3dbe84e48cc00c580b2eeb19e71e901267be"},
{file = "llama_cloud-0.1.34.tar.gz", hash = "sha256:6866e4bab47d2c1840bdf169c13c06176931c1d30697ac1fa71bab7942a041e9"},
]
[package.dependencies]
@@ -1187,20 +1187,20 @@ pydantic = ">=1.10"
[[package]]
name = "llama-cloud-services"
version = "0.6.46"
version = "0.6.48"
description = "Tailored SDK clients for LlamaCloud services."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "llama_cloud_services-0.6.46-py3-none-any.whl", hash = "sha256:2cdf4de19cfb5548e9d0155f1bc203affca4e26ac0201f8d09d18315b1350479"},
{file = "llama_cloud_services-0.6.46.tar.gz", hash = "sha256:cf50c5cfd481982fd548f600adfcc666f3939b6f04718775d853a4e4efebb750"},
{file = "llama_cloud_services-0.6.48-py3-none-any.whl", hash = "sha256:5faadcfa270f3be516dd7d3568cbb54b3014b2c8878c5bd7d3252ab1bd635869"},
{file = "llama_cloud_services-0.6.48.tar.gz", hash = "sha256:64fe8d8ce7434d6a481646c35be55973ae10765d20b6be0067a262e6be61f2df"},
]
[package.dependencies]
click = ">=8.1.7,<9.0.0"
eval-type-backport = {version = ">=0.2.0,<0.3.0", markers = "python_version < \"3.10\""}
llama-cloud = "0.1.33"
llama-cloud = "0.1.34"
llama-index-core = ">=0.12.0"
platformdirs = ">=4.3.7,<5.0.0"
pydantic = ">=2.8,<2.10 || >2.10"
@@ -3086,4 +3086,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9,<4.0"
content-hash = "b18a0bb31a806df8e673634c4916d5001fd80e3e328e26f632ff6a55c6a1a196"
content-hash = "0b5ab34c44ce7802b02e01b6535b9bb0e08f99f26a43a76be88534dd8a417666"
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "llama-parse"
version = "0.6.48"
version = "0.6.51"
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.48"
llama-cloud-services = ">=0.6.51"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
+1 -1
View File
@@ -8,7 +8,7 @@ python_version = "3.10"
[tool.poetry]
name = "llama-cloud-services"
version = "0.6.48"
version = "0.6.51"
description = "Tailored SDK clients for LlamaCloud services."
authors = ["Logan Markewich <logan@runllama.ai>"]
license = "MIT"
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["click", "tomlkit"]
# ///
import click
import subprocess
import sys
import tomlkit
from pathlib import Path
def get_current_versions() -> tuple[str, str, str]:
"""Get current versions from both pyproject.toml files."""
# Read main pyproject.toml
main_content = Path("pyproject.toml").read_text()
main_doc = tomlkit.parse(main_content)
main_version = main_doc["tool"]["poetry"]["version"]
# Read llama_parse/pyproject.toml
llama_parse_content = Path("llama_parse/pyproject.toml").read_text()
llama_parse_doc = tomlkit.parse(llama_parse_content)
llama_parse_version = llama_parse_doc["tool"]["poetry"]["version"]
dependency_version = llama_parse_doc["tool"]["poetry"]["dependencies"][
"llama-cloud-services"
]
return str(main_version), str(llama_parse_version), str(dependency_version)
def validate_versions(
main_version: str, llama_parse_version: str, dependency_version: str
) -> list[str]:
"""Validate that versions are consistent and return warnings."""
warnings = []
if main_version != llama_parse_version:
warnings.append(
f"Version mismatch: main={main_version}, llama_parse={llama_parse_version}"
)
# Extract version from dependency string (e.g., ">=0.6.51" -> "0.6.51")
if dependency_version and dependency_version.startswith(">="):
dep_ver = dependency_version[2:]
if dep_ver != main_version:
warnings.append(
f"Dependency version mismatch: dependency={dep_ver}, main={main_version}"
)
return warnings
def set_version(version: str) -> None:
"""Set version across all pyproject.toml files using tomlkit to preserve formatting."""
# Update main pyproject.toml
main_content = Path("pyproject.toml").read_text()
main_doc = tomlkit.parse(main_content)
main_doc["tool"]["poetry"]["version"] = version
Path("pyproject.toml").write_text(tomlkit.dumps(main_doc))
# Update llama_parse/pyproject.toml
llama_parse_content = Path("llama_parse/pyproject.toml").read_text()
llama_parse_doc = tomlkit.parse(llama_parse_content)
llama_parse_doc["tool"]["poetry"]["version"] = version
llama_parse_doc["tool"]["poetry"]["dependencies"][
"llama-cloud-services"
] = f">={version}"
Path("llama_parse/pyproject.toml").write_text(tomlkit.dumps(llama_parse_doc))
click.echo(f"Updated all versions to {version}")
def get_current_branch() -> str:
"""Get the current git branch."""
result = subprocess.run(
["git", "branch", "--show-current"], capture_output=True, text=True, check=True
)
return result.stdout.strip()
def create_and_push_tag(version: str) -> None:
"""Create a git tag and push it."""
current_branch = get_current_branch()
if current_branch != "main":
click.echo(
f"Error: Not on main branch (currently on {current_branch})", err=True
)
sys.exit(1)
tag_name = f"v{version}"
# Create tag
subprocess.run(["git", "tag", tag_name], check=True)
click.echo(f"Created tag {tag_name}")
# Push tag
subprocess.run(["git", "push", "origin", tag_name], check=True)
click.echo(f"Pushed tag {tag_name}")
@click.group()
def cli() -> None:
"""Version management for llama-cloud-services."""
pass
@cli.command()
def get() -> None:
"""Get current versions and show validation warnings."""
main_version, llama_parse_version, dependency_version = get_current_versions()
click.echo("Current versions:")
click.echo(f" llama-cloud-services: {main_version}")
click.echo(f" llama-parse: {llama_parse_version}")
click.echo(f" dependency reference: {dependency_version}")
warnings = validate_versions(main_version, llama_parse_version, dependency_version)
if warnings:
click.echo("\nValidation warnings:")
for warning in warnings:
click.echo(f" ⚠️ {warning}")
else:
click.echo("\n✅ All versions are consistent")
@cli.command()
@click.argument("version")
def set(version: str) -> None:
"""Set version across all pyproject.toml files."""
set_version(version)
@cli.command()
@click.option(
"--version", help="Version to tag (uses current version if not specified)"
)
def tag(version: str | None = None) -> None:
"""Create and push a git tag for the current version."""
if not version:
main_version, _, _ = get_current_versions()
version = main_version
create_and_push_tag(version)
if __name__ == "__main__":
cli()
@@ -48,8 +48,8 @@ class TestData(BaseModel):
# Skip all tests if API key is not set
@pytest.mark.asyncio
@pytest.mark.skipif(
not LLAMA_CLOUD_API_KEY or not LLAMA_DEPLOY_DEPLOYMENT_NAME,
reason="LLAMA_CLOUD_API_KEY or LLAMA_DEPLOY_DEPLOYMENT_NAME not set",
not LLAMA_CLOUD_API_KEY,
reason="LLAMA_CLOUD_API_KEY not set",
)
async def test_agent_data_crud_operations():
"""Test basic CRUD operations for agent data with automatic cleanup"""
+9
View File
@@ -193,3 +193,12 @@ async def test_multiple_page_markdown(
result = await markdown_parser.aload_data(filepath)
assert len(result) == expected
assert all(len(doc.text) > 0 for doc in result)
@pytest.mark.asyncio
async def test_get_result(markdown_parser: LlamaParse) -> None:
filepath = "tests/test_files/attention_is_all_you_need.pdf"
expected = await markdown_parser.aparse(filepath)
result = await markdown_parser.aget_result(expected.job_id)
assert result.job_id == expected.job_id
assert len(result.pages) == len(expected.pages)
+6
View File
@@ -83,6 +83,12 @@ async def test_basic_parse_result(file_path: str, partition_pages: Optional[int]
assert image_documents[0].image is not None
assert len(image_documents[0].resolve_image().getvalue()) > 0
assert len(await result.aget_text()) > 0
assert len(await result.aget_markdown()) > 0
json = await result.aget_json()
assert json.get("job_metadata")
assert len(json.get("pages", [])) == len(result.pages)
@pytest.mark.asyncio
@pytest.mark.skip(