Compare commits

...

26 Commits

Author SHA1 Message Date
github-actions[bot] d0649ece6e chore: version packages (#982) 2025-10-16 16:58:29 -06:00
MartijnLeplae 5d4cabd843 Add ImageNode support in TypeScript (#969) 2025-10-16 16:56:28 -06:00
github-actions[bot] 9070a6ac16 chore: version packages (#981) 2025-10-15 12:01:34 -06:00
Bogdan Gheorghe 4f24f537f6 Add agressive table extraction argument (#980) 2025-10-15 11:57:34 -06:00
github-actions[bot] 8859a203e2 chore: version packages (#977) 2025-10-14 19:03:36 -06:00
dependabot[bot] b091364054 build(deps): bump astral-sh/setup-uv from 6 to 7 (#974) 2025-10-14 19:02:32 -06:00
dependabot[bot] 43b1a013ca build(deps): bump github/codeql-action from 3 to 4 (#973) 2025-10-14 19:02:20 -06:00
Logan f81532e7f2 safest types possible for parse (#976) 2025-10-14 19:02:07 -06:00
github-actions[bot] 986d3987d3 chore: version packages (#965)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-14 08:14:49 -06:00
Logan 1bf522311f fix default bbox values (#975) 2025-10-14 07:44:35 -06:00
Preston Carlson 24166dcfc8 Only escape single dollar sign in notebook md (#964)
* Limit escaping to lone dollar signs - preserve double dollar for latex equations

* Updated uv.lock via make lint

* Patch bump

* Unit test for _format_markdown_for_notebook

Test doesn't depend on getting real results/is just testing a string manipulation function, so inserting before other tests. Should move to its own file if we add additional formatting configurations
2025-10-07 08:06:03 -07:00
github-actions[bot] bfb7f3973f chore: version packages (#956)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-06 11:15:55 -04:00
dependabot[bot] 979f643c77 build(deps): bump actions/checkout from 4 to 5 (#961) 2025-10-06 09:12:38 -06:00
dependabot[bot] aefd89cf1b build(deps): bump actions/setup-python from 5 to 6 (#960) 2025-10-06 09:12:30 -06:00
dependabot[bot] 8ea2b2c64e build(deps): bump pnpm/action-setup from 3 to 4 (#959) 2025-10-06 09:12:20 -06:00
dependabot[bot] 4a9a2a21d8 build(deps): bump astral-sh/setup-uv from 3 to 6 (#958) 2025-10-06 09:12:08 -06:00
Logan e6a7939206 loosen packaging requirements (#962) 2025-10-06 09:11:57 -06:00
Adrian Lyjak 104a03e829 fix: re-enable js publishing (#963) 2025-10-06 11:10:46 -04:00
Terry Zhao 6e0f2f4ca0 citation can be null (#869)
* citation can be null

* Add changeset

---------

Co-authored-by: Terry Zhao <terryzhao@runllama.ai>
Co-authored-by: Adrian Lyjak <adrianlyjak@gmail.com>
2025-10-04 16:26:11 -04:00
dependabot[bot] 0708d11f8a Bump actions/setup-node from 4 to 5 (#909)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-04 16:21:50 -04:00
github-actions[bot] be19185503 chore: version packages (#954)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-03 20:14:04 -04:00
Adrian Lyjak 7571b0d6c4 Missed some things again with tag fixes (#955)
guh
2025-10-03 20:12:53 -04:00
Adrian Lyjak ad6734bf80 fixup tagging more better (#953)
* fix: correct private field type in py/package.json to be recognized by pnpm

* use packages more directly, make public

* add bump

* fix crash
2025-10-03 19:53:57 -04:00
github-actions[bot] 9ec2a8322e chore: version packages (#952) 2025-10-03 15:11:14 -06:00
Logan 51011b9f30 fix changeset harder (#951) 2025-10-03 15:09:58 -06:00
Logan 09805f9e15 swap changesets (#949) 2025-10-03 15:06:00 -06:00
28 changed files with 580 additions and 114 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"llama-cloud-services-py": minor
---
Escaping dollar signs in markdown output in jupyter notebooks to prevent them being interpreted as equation delimiters
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: "ts/llama_cloud_services/.nvmrc"
+2 -2
View File
@@ -30,12 +30,12 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: python
dependency-caching: true
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:python"
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
@@ -31,7 +31,7 @@ jobs:
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: "ts/llama_cloud_services/.nvmrc"
- name: Install dependencies
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version-file: "ts/llama_cloud_services/.nvmrc"
- name: Install dependencies
@@ -15,23 +15,23 @@ jobs:
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout Repo
uses: actions/checkout@v4
uses: actions/checkout@v5
- uses: pnpm/action-setup@v3
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: "22"
cache: "pnpm"
- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: pnpm install
+1 -1
View File
@@ -8,7 +8,7 @@
"scripts": {
"pre-commit-version": "pnpm changeset",
"version": "./scripts/changeset-version.py version",
"publish": "./scripts/changeset-version.py publish --no-js --tag"
"publish": "./scripts/changeset-version.py publish --tag"
},
"devDependencies": {
"prettier": "^3.6.2",
+15 -1
View File
@@ -21,7 +21,21 @@ importers:
specifier: ^3.6.2
version: 3.6.2
py: {}
py:
devDependencies:
changesets:
specifier: ^1.0.2
version: 1.0.2
py/llama_parse:
dependencies:
llama-cloud-services-py:
specifier: workspace:*
version: link:..
devDependencies:
changesets:
specifier: ^1.0.2
version: 1.0.2
ts/e2e-tests:
devDependencies:
+1
View File
@@ -1,3 +1,4 @@
packages:
- "ts/*"
- "py"
- "py/*"
+37
View File
@@ -1,5 +1,42 @@
# llama-cloud-services-py
## 0.6.76
### Patch Changes
- 4f24f53: Add aggressive_table_extraction flag in python sdk
## 0.6.75
### Patch Changes
- f81532e: Safest types possible for parse
## 0.6.74
### Patch Changes
- 1bf5223: Fix default bbox values
- 24166dc: Now only escape single dollar signs - preserve double for latex equations
## 0.6.73
### Patch Changes
- e6a7939: Loosen packaging dep requirement
## 0.6.72
### Patch Changes
- ad6734b: Fixup and test versioning
## 0.6.71
### Patch Changes
- 51011b9: Escape dollar signs in jupyter notebooks
## 0.6.70
### Patch Changes
+7
View File
@@ -188,6 +188,10 @@ class LlamaParse(BasePydanticReader):
default=False,
description="If set to true, LlamaParse will try to detect long table and adapt the output.",
)
aggressive_table_extraction: Optional[bool] = Field(
default=False,
description="If set to true, LlamaParse will try to extract tables aggressively, may lead to false positives.",
)
annotate_links: Optional[bool] = Field(
default=False,
description="Annotate links found in the document to extract their URL.",
@@ -713,6 +717,9 @@ class LlamaParse(BasePydanticReader):
if self.adaptive_long_table:
data["adaptive_long_table"] = self.adaptive_long_table
if self.aggressive_table_extraction:
data["aggressive_table_extraction"] = self.aggressive_table_extraction
if self.annotate_links:
data["annotate_links"] = self.annotate_links
+116 -26
View File
@@ -1,8 +1,8 @@
import httpx
import os
import re
from pydantic import BaseModel, Field, SerializeAsAny
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny, model_validator
from typing import Dict, Any, List, Optional, get_origin, get_args
from llama_cloud_services.parse.utils import (
make_api_request,
@@ -13,8 +13,75 @@ from llama_index.core.schema import Document, ImageDocument, ImageNode, TextNode
PAGE_REGEX = r"page[-_](\d+)\.jpg$"
SAFE_MODEL_CONFIGS = ConfigDict(
extra="allow",
validate_assignment=False,
arbitrary_types_allowed=True,
validate_default=False,
)
class JobMetadata(BaseModel):
class SafeBaseModel(BaseModel):
"""Base model that gracefully handles None values from unstable backend responses."""
model_config = SAFE_MODEL_CONFIGS
@model_validator(mode="before")
@classmethod
def coerce_none_to_defaults(cls, data: Any) -> Any:
"""
Replace None values with appropriate defaults based on field type annotations.
This prevents validation errors when the backend returns None for non-optional fields.
"""
if not isinstance(data, dict):
return data
# Process each field that has a None value
result = {}
for key, value in data.items():
if value is not None or key not in cls.model_fields:
result[key] = value
continue
# Value is None and field exists in model
field_info = cls.model_fields[key]
# If field has a default or default_factory, let Pydantic handle it
from pydantic_core import PydanticUndefined
if (
field_info.default is not PydanticUndefined
or field_info.default_factory is not None
):
continue
# Otherwise, provide a sensible default based on the type annotation
annotation = field_info.annotation
origin = get_origin(annotation)
# Handle List types
if origin is list:
result[key] = []
# Handle Dict types
elif origin is dict:
result[key] = {}
# Handle basic types
elif annotation == str or (origin and str in get_args(annotation)):
result[key] = ""
elif annotation == int or (origin and int in get_args(annotation)):
result[key] = 0
elif annotation == float or (origin and float in get_args(annotation)):
result[key] = 0.0
elif annotation == bool or (origin and bool in get_args(annotation)):
result[key] = False
# If we can't determine a safe default, skip (let Pydantic try)
else:
result[key] = value
return result
class JobMetadata(SafeBaseModel):
"""Metadata about the job."""
job_pages: int = Field(default=0, description="The number of pages in the job.")
@@ -27,19 +94,31 @@ class JobMetadata(BaseModel):
)
class BBox(BaseModel):
class BBox(SafeBaseModel):
"""A bounding box."""
x: float = Field(description="The x-coordinate of the bounding box.")
y: float = Field(description="The y-coordinate of the bounding box.")
w: float = Field(description="The width of the bounding box.")
h: float = Field(description="The height of the bounding box.")
x: Optional[float] = Field(
default=None,
description="The x-coordinate of the bounding box.",
)
y: Optional[float] = Field(
default=None,
description="The y-coordinate of the bounding box.",
)
w: Optional[float] = Field(
default=None,
description="The width of the bounding box.",
)
h: Optional[float] = Field(
default=None,
description="The height of the bounding box.",
)
class PageItem(BaseModel):
class PageItem(SafeBaseModel):
"""An item in a page."""
type: str = Field(description="The type of the item.")
type: str = Field(default="", description="The type of the item.")
lvl: Optional[int] = Field(
default=None, description="The level of indentation of the item."
)
@@ -61,10 +140,10 @@ class PageItem(BaseModel):
)
class ImageItem(BaseModel):
class ImageItem(SafeBaseModel):
"""An image in a page."""
name: str = Field(description="The name of the image.")
name: str = Field(default="", description="The name of the image.")
height: Optional[float] = Field(
default=None, description="The height of the image."
)
@@ -84,22 +163,28 @@ class ImageItem(BaseModel):
type: Optional[str] = Field(default=None, description="The type of the image.")
class LayoutItem(BaseModel):
class LayoutItem(SafeBaseModel):
"""The layout of a page."""
image: str = Field(description="The name of the image containing the layout item")
confidence: float = Field(description="The confidence of the layout item.")
label: str = Field(description="The label of the layout item.")
image: str = Field(
default="", description="The name of the image containing the layout item"
)
confidence: float = Field(
default=0.0, description="The confidence of the layout item."
)
label: str = Field(default="", description="The label of the layout item.")
bbox: Optional[BBox] = Field(
default=None, description="The bounding box of the layout item."
)
isLikelyNoise: bool = Field(description="Whether the layout item is likely noise.")
isLikelyNoise: bool = Field(
default=False, description="Whether the layout item is likely noise."
)
class ChartItem(BaseModel):
class ChartItem(SafeBaseModel):
"""A chart in a page."""
name: str = Field(description="The name of the chart.")
name: str = Field(default="", description="The name of the chart.")
x: Optional[float] = Field(
default=None, description="The x-coordinate of the chart."
)
@@ -112,7 +197,7 @@ class ChartItem(BaseModel):
)
class Page(BaseModel):
class Page(SafeBaseModel):
"""A page of the document."""
page: int = Field(default=0, description="The page number.")
@@ -167,7 +252,7 @@ class Page(BaseModel):
)
class JobResult(BaseModel):
class JobResult(SafeBaseModel):
"""The raw JSON result from the LlamaParse API."""
pages: List[Page] = Field(
@@ -266,18 +351,23 @@ class JobResult(BaseModel):
if text is None:
return None
def escape_dollar_signs(text: str) -> str:
"""Escape dollar signs in text to prevent Jupyter from interpreting them as LaTeX.
def escape_single_dollar_signs(text: str) -> str:
"""Escape single dollar signs in text to prevent Jupyter from interpreting them as LaTeX.
Preserves all strings of dollar signs greater than length 1,
especially preserving double dollar signs ($$) which denote LaTeX equations.
Args:
text: The text to escape
Returns:
Text with dollar signs escaped
Text with single dollar signs escaped
"""
return text.replace("$", r"\$")
# Replace single $ with \$, but preserve $$
# Use negative lookahead and lookbehind to match $ not preceded or followed by $
return re.sub(r"(?<!\$)\$(?!\$)", r"\$", text)
return escape_dollar_signs(text)
return escape_single_dollar_signs(text)
def get_markdown_documents(self, split_by_page: bool = False) -> List[Document]:
"""
+37
View File
@@ -0,0 +1,37 @@
# llama_parse
## 0.6.76
### Patch Changes
- Updated dependencies [4f24f53]
- llama-cloud-services-py@0.6.76
## 0.6.75
### Patch Changes
- Updated dependencies [f81532e]
- llama-cloud-services-py@0.6.75
## 0.6.74
### Patch Changes
- Updated dependencies [1bf5223]
- Updated dependencies [24166dc]
- llama-cloud-services-py@0.6.74
## 0.6.73
### Patch Changes
- Updated dependencies [e6a7939]
- llama-cloud-services-py@0.6.73
## 0.6.72
### Patch Changes
- Updated dependencies [ad6734b]
- llama-cloud-services-py@0.6.72
+20
View File
@@ -0,0 +1,20 @@
{
"name": "llama_parse",
"version": "0.6.76",
"description": "",
"main": "index.js",
"private": false,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"llama-cloud-services-py": "workspace:*"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.11.1",
"devDependencies": {
"changesets": "^1.0.2"
}
}
+2 -2
View File
@@ -11,13 +11,13 @@ dev = [
[project]
name = "llama-parse"
version = "0.6.70"
version = "0.6.76"
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.70"]
dependencies = ["llama-cloud-services>=0.6.76"]
[project.scripts]
llama-parse = "llama_parse.cli.main:parse"
+6 -3
View File
@@ -1,7 +1,10 @@
{
"name": "llama-cloud-services-py",
"version": "0.6.70",
"private": "true",
"version": "0.6.76",
"private": false,
"license": "MIT",
"scripts": {}
"scripts": {},
"devDependencies": {
"changesets": "^1.0.2"
}
}
+2 -2
View File
@@ -19,7 +19,7 @@ dev = [
[project]
name = "llama-cloud-services"
version = "0.6.70"
version = "0.6.76"
description = "Tailored SDK clients for LlamaCloud services."
authors = [{name = "Logan Markewich", email = "logan@runllama.ai"}]
requires-python = ">=3.9,<4.0"
@@ -34,7 +34,7 @@ dependencies = [
"eval-type-backport>=0.2.0,<0.3 ; python_version < '3.10'",
"platformdirs>=4.3.7,<5",
"tenacity>=8.5.0, <10.0",
"packaging>=25.0"
"packaging>=23.0"
]
[project.scripts]
+34
View File
@@ -6,6 +6,40 @@ from llama_cloud_services import LlamaParse
from llama_cloud_services.parse.types import JobResult
def test_format_parse_result_markdown_for_notebook():
"""Test the _format_markdown_for_notebook function.
Right now, the only work it does is escape single dollar signs."""
result = JobResult(job_id="test", file_name="test.pdf", job_result={})
# Test None input
assert result._format_markdown_for_notebook(None) is None
# Test single dollar sign gets escaped
assert result._format_markdown_for_notebook("This costs $5") == "This costs \\$5"
# Test double dollar signs are preserved (LaTeX equations)
assert (
result._format_markdown_for_notebook("$$x^2 + y^2 = z^2$$")
== "$$x^2 + y^2 = z^2$$"
)
# Test mixed single and double dollar signs
text = "This costs $5, but $$E = mc^2$$ is priceless"
expected = "This costs \\$5, but $$E = mc^2$$ is priceless"
assert result._format_markdown_for_notebook(text) == expected
# Test multiple single dollar signs
assert result._format_markdown_for_notebook("$10 and $20") == "\\$10 and \\$20"
# Test three or more consecutive dollar signs (preserve them)
assert result._format_markdown_for_notebook("$$$") == "$$$"
# Test adjacent dollar signs with text in between
text = "$$inline$$ and $separate"
expected = "$$inline$$ and \\$separate"
assert result._format_markdown_for_notebook(text) == expected
@pytest.fixture
def file_path() -> str:
return "tests/test_files/attention_is_all_you_need.pdf"
Generated
+2 -2
View File
@@ -1596,7 +1596,7 @@ wheels = [
[[package]]
name = "llama-cloud-services"
version = "0.6.70"
version = "0.6.73"
source = { editable = "." }
dependencies = [
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
@@ -1633,7 +1633,7 @@ requires-dist = [
{ name = "eval-type-backport", marker = "python_full_version < '3.10'", specifier = ">=0.2.0,<0.3" },
{ name = "llama-cloud", specifier = "==0.1.43" },
{ name = "llama-index-core", specifier = ">=0.12.0" },
{ name = "packaging", specifier = ">=25.0" },
{ name = "packaging", specifier = ">=23.0" },
{ name = "platformdirs", specifier = ">=4.3.7,<5" },
{ name = "pydantic", specifier = ">=2.8,!=2.10" },
{ name = "python-dotenv", specifier = ">=1.0.1,<2" },
+114 -53
View File
@@ -12,14 +12,15 @@ There's 2 things this does:
"""
from dataclasses import dataclass
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import List
from typing import Any, List, cast
import urllib.request
import urllib.error
import re
import click
import tomlkit
@@ -33,40 +34,97 @@ def _run_command(
subprocess.run(cmd, check=True, text=True, cwd=cwd or Path.cwd(), env=env)
def update_python_versions(version: str) -> None:
"""llama-cloud-services and llama-parse share a version. llama-parse is just a silly sidecar that proxies to llama-cloud-services
for compatibility.
def _run_and_capture(
cmd: List[str], cwd: Path | None = None, env: dict[str, str] | None = None
) -> str:
"""Run a command and return stdout as text, raising on failure."""
result = subprocess.run(
cmd,
check=True,
text=True,
cwd=cwd or Path.cwd(),
env=env,
capture_output=True,
)
return result.stdout
This function updates the version in both pyproject.toml files.
@dataclass
class Package:
name: str
version: str
path: Path
def python_package_name(self) -> str | None:
if "/py/" in str(self.path) or str(self.path).endswith("/py"):
return self.name.removesuffix("-py")
return None
def _get_pnpm_workspace_packages() -> list[Package]:
"""Return directories for all workspace packages from pnpm list JSON output."""
output = _run_and_capture(["pnpm", "list", "-r", "--depth=-1", "--json"])
data = cast(list[dict[str, Any]], json.loads(output))
packages: list[Package] = [
Package(name=data["name"], version=data["version"], path=Path(data["path"]))
for data in data
]
return packages
def _sync_package_version_with_pyproject(
package_dir: Path, packages: dict[str, Package], js_package_name: str
) -> None:
"""Sync version from package.json to pyproject.toml.
Returns True if pyproject was changed, else False.
"""
# Update main pyproject.toml
main_path = Path("py/pyproject.toml")
main_content = main_path.read_text()
main_doc = tomlkit.parse(main_content)
if main_doc["project"]["version"] != version:
click.echo(f"Updating llama-cloud-services version to {version}")
main_doc["project"]["version"] = version
main_path.write_text(tomlkit.dumps(main_doc))
pyproject_path = package_dir / "pyproject.toml"
if not pyproject_path.exists():
return
# Update llama_parse/pyproject.toml
parse_path = Path("py/llama_parse/pyproject.toml")
parse_content = parse_path.read_text()
parse_doc = tomlkit.parse(parse_content)
if parse_doc["project"]["version"] != version:
click.echo(f"Updating llama-parse version to {version}")
parse_doc["project"]["version"] = version
parse_path.write_text(tomlkit.dumps(parse_doc))
package_version = packages[js_package_name].version
py_doc = tomlkit.parse(pyproject_path.read_text())
# Update the dependency reference
dependencies = parse_doc["project"]["dependencies"]
for i, dep in enumerate(dependencies):
if isinstance(dep, str) and dep.startswith("llama-cloud-services"):
dependencies[i] = f"llama-cloud-services>={version}"
break
by_python_name = {
pkg.python_package_name(): pkg
for pkg in packages.values()
if pkg.python_package_name()
}
parse_path.write_text(tomlkit.dumps(parse_doc))
current_version = py_doc["project"]["version"]
assert isinstance(current_version, str)
click.echo(f"Updated Python packages to version {version}")
# update workspace dependency strings by replacing the first version after == or >=
deps = py_doc["project"]["dependencies"] or []
changed = False
for i, dep in enumerate(deps):
if not isinstance(dep, str):
continue
pkg = (cast(str, dep).split("==")[0]).split(">=")[0]
if pkg not in by_python_name:
continue
target_version = by_python_name[pkg].version
new_dep = re.sub(
r"(==|>=)\s*([0-9A-Za-z_.+-]+)",
lambda m: m.group(1) + target_version,
dep,
count=1,
)
if new_dep != dep:
deps[i] = new_dep
changed = True
if current_version != package_version:
py_doc["project"]["version"] = package_version
changed = True
if changed:
pyproject_path.write_text(tomlkit.dumps(py_doc))
click.echo(
f"Updated {pyproject_path} version to {package_version} and synced dependency specs"
)
def lock_python_dependencies() -> None:
@@ -86,24 +144,25 @@ def cli() -> None:
@cli.command()
def version() -> None:
"""Apply changeset versions, and propagate them to Python packages."""
# First, run changeset version to update all package.json files (including py/package.json)
"""Apply changeset versions, then sync versions for co-located JS/Py packages.
- Runs changesets to bump package.json versions.
- Discovers all workspace packages via pnpm.
- For any directory containing both package.json and pyproject.toml, and with
package.json private: false, set pyproject [project].version to match the JS version.
- If a pyproject is updated, run `uv sync` in that directory to update its lock file.
"""
# Ensure we're at the repo root
os.chdir(Path(__file__).parent.parent)
# First, run changeset version to update all package.json files
_run_command(["npx", "@changesets/cli", "version"])
# Get the updated Python package version from py/package.json (updated by changesets)
py_package_path = Path("py/package.json")
if not py_package_path.exists():
click.echo("Python package.json not found", err=True)
sys.exit(1)
with open(py_package_path) as f:
py_package = json.load(f)
new_version = py_package["version"]
# Update Python pyproject.toml files based on the package.json version
update_python_versions(new_version)
click.echo(f"Successfully propagated version {new_version} to all Python packages")
# Enumerate workspace packages and perform syncs
packages = _get_pnpm_workspace_packages()
version_map = {pkg.name: pkg for pkg in packages}
for pkg in packages:
_sync_package_version_with_pyproject(pkg.path, version_map, pkg.name)
@cli.command()
@@ -116,12 +175,14 @@ def publish(tag: bool, dry_run: bool, js: bool, py: bool) -> None:
# move to the root
os.chdir(Path(__file__).parent.parent)
if not os.getenv("NPM_TOKEN"):
click.echo("NPM_TOKEN is not set, skipping publish", err=True)
raise click.Abort("No token set")
if not os.getenv("LLAMA_PARSE_PYPI_TOKEN"):
click.echo("LLAMA_PARSE_PYPI_TOKEN is not set, skipping publish", err=True)
raise click.Abort("No token set")
if js:
if not os.getenv("NPM_TOKEN"):
click.echo("NPM_TOKEN is not set, skipping publish", err=True)
raise click.Abort("No token set")
if py:
if not os.getenv("LLAMA_PARSE_PYPI_TOKEN"):
click.echo("LLAMA_PARSE_PYPI_TOKEN is not set, skipping publish", err=True)
raise click.Abort("No token set")
# not general script. Just checks each of the 2 packages to see if they need to be published.
if js:
@@ -134,8 +195,8 @@ def publish(tag: bool, dry_run: bool, js: bool, py: bool) -> None:
click.echo("Dry run, skipping tag. Would run:")
click.echo(" npx @changesets/cli tag")
click.echo(" git push --tags")
return
else:
# Let changesets create JS-related tags as usual
_run_command(["npx", "@changesets/cli", "tag"])
_run_command(["git", "push", "--tags"])
+3
View File
@@ -9,10 +9,12 @@ test("LlamaIndex module resolution test", async (t) => {
const index = new LlamaCloudIndex({
name: "test-index",
projectName: "Default",
apiKey: process.env.LLAMA_CLOUD_API_KEY || "test-key",
});
const reader = new LlamaParseReader({
resultType: "markdown",
verbose: false,
apiKey: process.env.LLAMA_CLOUD_API_KEY || "test-key",
});
ok(index !== undefined);
ok(reader !== undefined);
@@ -24,6 +26,7 @@ test("LlamaIndex module resolution test", async (t) => {
const index = new mod.LlamaCloudIndex({
name: "test-index",
projectName: "Default",
apiKey: process.env.LLAMA_CLOUD_API_KEY || "test-key",
});
ok(index !== undefined);
});
+12
View File
@@ -1,5 +1,17 @@
# llama-cloud-services
## 0.3.9
### Patch Changes
- 5d4cabd: Add ImageNode support in TypeScript
## 0.3.8
### Patch Changes
- 6e0f2f4: Agent data extraction citations can be undefined
## 0.3.7
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama-cloud-services",
"version": "0.3.7",
"version": "0.3.9",
"type": "module",
"license": "MIT",
"scripts": {
@@ -9,10 +9,16 @@ import { DEFAULT_PROJECT_NAME } from "@llamaindex/core/global";
import type { QueryBundle } from "@llamaindex/core/query-engine";
import { BaseRetriever } from "@llamaindex/core/retriever";
import type { NodeWithScore } from "@llamaindex/core/schema";
import { jsonToNode, ObjectType } from "@llamaindex/core/schema";
import { jsonToNode, ObjectType, ImageNode } from "@llamaindex/core/schema";
import { extractText } from "@llamaindex/core/utils";
import type { ClientParams, CloudConstructorParams } from "./type.js";
import { getPipelineId, initService } from "./utils.js";
import { getPipelineId, getProjectId, initService } from "./utils.js";
import {
type PageScreenshotNodeWithScore,
type PageFigureNodeWithScore,
generateFilePageScreenshotPresignedUrlApiV1FilesIdPageScreenshotsPageIndexPresignedUrlPost,
generateFilePageFigurePresignedUrlApiV1FilesIdPageFiguresPageIndexFigureNamePresignedUrlPost,
} from "./api";
export type CloudRetrieveParams = Omit<
RetrievalParams,
@@ -43,6 +49,95 @@ export class LlamaCloudRetriever extends BaseRetriever {
});
}
private async fetchBase64FromPresignedUrl(url: string): Promise<string> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to fetch media from presigned URL: ${response.status} ${response.statusText}`,
);
}
const buffer = Buffer.from(await response.arrayBuffer());
return buffer.toString("base64");
}
private async pageScreenshotNodesToNodeWithScore(
nodes: PageScreenshotNodeWithScore[] | undefined,
projectId: string,
): Promise<NodeWithScore[]> {
if (!nodes || nodes.length === 0) return [];
const results = await Promise.all(
nodes.map(async (n) => {
const { data: presigned } =
await generateFilePageScreenshotPresignedUrlApiV1FilesIdPageScreenshotsPageIndexPresignedUrlPost(
{
throwOnError: true,
path: {
id: n.node.file_id,
page_index: n.node.page_index,
},
query: {
project_id: projectId,
organization_id: this.organizationId ?? null,
},
},
);
const base64 = await this.fetchBase64FromPresignedUrl(presigned.url);
const imageNode = new ImageNode({
image: base64,
metadata: {
...(n.node.metadata ?? {}),
file_id: n.node.file_id,
page_index: n.node.page_index,
},
});
return { node: imageNode, score: n.score } satisfies NodeWithScore;
}),
);
return results;
}
private async pageFigureNodesToNodeWithScore(
nodes: PageFigureNodeWithScore[] | undefined,
projectId: string,
): Promise<NodeWithScore[]> {
if (!nodes || nodes.length === 0) return [];
const results = await Promise.all(
nodes.map(async (n) => {
const { data: presigned } =
await generateFilePageFigurePresignedUrlApiV1FilesIdPageFiguresPageIndexFigureNamePresignedUrlPost(
{
throwOnError: true,
path: {
id: n.node.file_id,
page_index: n.node.page_index,
figure_name: n.node.figure_name,
},
query: {
project_id: projectId,
organization_id: this.organizationId ?? null,
},
},
);
const base64 = await this.fetchBase64FromPresignedUrl(presigned.url);
const imageNode = new ImageNode({
image: base64,
metadata: {
...(n.node.metadata ?? {}),
file_id: n.node.file_id,
page_index: n.node.page_index,
figure_name: n.node.figure_name,
},
});
return { node: imageNode, score: n.score } satisfies NodeWithScore;
}),
);
return results;
}
// LlamaCloud expects null values for filters, but LlamaIndexTS uses undefined for empty values
// This function converts the undefined values to null
private convertFilter(filters?: MetadataFilters): MetadataFilters | null {
@@ -76,6 +171,35 @@ export class LlamaCloudRetriever extends BaseRetriever {
}
async _retrieve(query: QueryBundle): Promise<NodeWithScore[]> {
// Handle deprecated image retrieval flag
const retrieveImageNodes = (this.retrieveParams as RetrievalParams)
.retrieve_image_nodes;
if (typeof retrieveImageNodes !== "undefined") {
console.warn(
"The `retrieve_image_nodes` parameter is deprecated. Use `retrieve_page_screenshot_nodes` and `retrieve_page_figure_nodes` instead.",
);
}
const retrievePageScreenshotNodes = (this.retrieveParams as RetrievalParams)
.retrieve_page_screenshot_nodes;
const retrievePageFigureNodes = (this.retrieveParams as RetrievalParams)
.retrieve_page_figure_nodes;
if (retrieveImageNodes) {
if (
retrievePageScreenshotNodes === false ||
retrievePageFigureNodes === false
) {
throw new Error(
"If `retrieve_image_nodes` is set to true, both `retrieve_page_screenshot_nodes` and `retrieve_page_figure_nodes` must also be set to true or omitted.",
);
}
(this.retrieveParams as RetrievalParams).retrieve_page_screenshot_nodes =
true;
(this.retrieveParams as RetrievalParams).retrieve_page_figure_nodes =
true;
}
const pipelineId = await getPipelineId(
this.pipelineName,
this.projectName,
@@ -98,6 +222,34 @@ export class LlamaCloudRetriever extends BaseRetriever {
},
});
return this.resultNodesToNodeWithScore(results.retrieval_nodes);
const textNodes = this.resultNodesToNodeWithScore(results.retrieval_nodes);
const needScreenshots = (this.retrieveParams as RetrievalParams)
.retrieve_page_screenshot_nodes;
const needFigures = (this.retrieveParams as RetrievalParams)
.retrieve_page_figure_nodes;
if (!needScreenshots && !needFigures) {
return textNodes;
}
const projectId = await getProjectId(this.projectName, this.organizationId);
const [screenshotNodes, figureNodes] = await Promise.all([
needScreenshots
? this.pageScreenshotNodesToNodeWithScore(
results.image_nodes,
projectId,
)
: Promise.resolve([] as NodeWithScore[]),
needFigures
? this.pageFigureNodesToNodeWithScore(
results.page_figure_nodes,
projectId,
)
: Promise.resolve([] as NodeWithScore[]),
]);
return [...textNodes, ...screenshotNodes, ...figureNodes];
}
}
@@ -38,7 +38,7 @@ export interface ExtractedFieldMetadata {
confidence?: number;
/** The confidence score for the field based on the extracted text only */
extraction_confidence?: number;
citation: FieldCitation[];
citation?: FieldCitation[];
}
export interface FieldCitation {