Compare commits

...

66 Commits

Author SHA1 Message Date
Neeraj Pradhan d246a6ea3c also add file ids to Sourcetext 2025-10-21 11:55:43 -07:00
Neeraj Pradhan 1acc52e9ea Simplify names 2025-10-21 11:22:35 -07:00
Neeraj Pradhan c9caa7e439 address comments 2025-10-20 12:41:04 -07:00
Neeraj Pradhan 6f03b6a1b3 Add common SourceText class for classify/extract text inputs 2025-10-20 12:17:53 -07:00
Jerry Liu 970e864514 improve classify notebook (#983) 2025-10-20 10:07:35 -07:00
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
Adrian Lyjak 8ced6f6eab fix: explicitly tag. I thought the action did this (#948) 2025-10-03 16:59:41 -04:00
Preston Carlson 081ddeca34 Escaping dollar signs in md output when running in a jupyter notebook (#945) 2025-10-03 14:52:26 -06:00
Adrian Lyjak 2460908789 Disable npm release (#946) 2025-10-03 16:13:16 -04:00
Adrian Lyjak c226d6a54c Fix more bugs in publishing (#944) 2025-10-03 11:16:43 -04:00
Adrian Lyjak 5d4c682eb2 fix: theres just one publish token (#943) 2025-10-03 10:56:10 -04:00
github-actions[bot] f72d3535c8 chore: version packages (#941)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-03 10:25:11 -04:00
Adrian Lyjak 1ea09a366e Update llama-cloud dep (#940) 2025-10-03 09:56:56 -04:00
Adrian Lyjak d4bbeb6389 ignore nvmrc (#942)
ignore npmrc
2025-10-03 00:21:32 -04:00
Adrian Lyjak d028397603 version and release via changesets (#849) 2025-10-03 00:08:52 -04:00
Emanuel Ferreira 35ea8476db docs: parse -> classify -> extract (#931) 2025-09-24 18:52:15 -03:00
Logan 3e5f7c4f1e Update parse.md 2025-09-24 11:35:13 -06:00
Adrian Lyjak 9d9b816644 Handle reasoning field conflict (#929)
* Handle reasoning field conflict

* update version to 0.6.69
2025-09-22 11:29:11 -04:00
Adrian Lyjak 83555f76e6 Handle validation errors for agent data retrieval (#928)
* feat: Add untyped agent data retrieval and handling

Introduces methods to retrieve agent data as untyped dictionaries,
handling validation errors gracefully. This allows for more flexible
data access when strict typing is not required or when data may be
malformed.

Co-authored-by: adrian <adrian@runllama.ai>

* Expose raw api result

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-09-22 11:28:49 -04:00
Adrian Lyjak 5edf5f914a Support creating indexes in a specified project_id (#924)
* Support creating indexes in a specified project_id

* Bump
2025-09-18 11:07:07 -04:00
Adrian Lyjak 22e4975cb2 Refactor agent fields in llama_cloud_services (#921) 2025-09-17 15:14:40 -04:00
Peter Rowlands (변기호) bc2f04379b py: bump version to v.0.6.66 (#920) 2025-09-16 19:34:18 +09:00
Peter Rowlands (변기호) f9f951d5d8 parse: expose spreadsheet_force_formula_computation option (#919) 2025-09-16 19:28:03 +09:00
Emmanuel Ferdman 355129fea5 Fix colab broken links (#750)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2025-09-14 23:10:21 +02:00
Adrian Lyjak d9aed80ded fix: v prefix goes deeper. Fix more (#899) 2025-09-08 17:45:06 -04:00
Pierre-Loic Doulcet c07d2d70a8 update parse package (#911) 2025-09-08 09:46:32 -06:00
Neeraj Pradhan ed6937a5a9 Fix uv sync; remove poetry lock (#906) 2025-09-05 17:13:31 -07:00
Neeraj Pradhan 34c15932a3 Bump version to 0.6.64 (#904) 2025-09-05 17:05:21 -07:00
Neeraj Pradhan b18ea96d11 Remove report generation related code from llama_cloud_services (#905) 2025-09-05 16:41:28 -07:00
Clelia (Astra) Bertelli 196ab827f5 fix: make ts release beautiful again (#902) 2025-09-05 10:41:39 -06:00
Peter Rowlands (변기호) ba4cb4d5e9 parse: expose page.slideSpeakerNotes (#889) 2025-09-05 15:48:44 +09:00
Adrian Lyjak 58d883b825 fix: "v" prefix being added to js versions (#898) 2025-09-04 15:39:27 -04:00
Adrian Lyjak 5fc5ebfc6c client unification (#895)
read from the shared client
2025-09-04 14:12:28 -04:00
Adrian Lyjak fe3e20fd53 Update version script, and unify the linting so that prettier is more consistent (#897)
Add version script, and unify the linting so that prettier is more consistent
2025-09-04 14:09:27 -04:00
Jerry Liu e7e59459ab getting started LlamaCloudIndex notebook (#891) 2025-09-02 14:52:39 -06:00
Logan Markewich f4d7c84e19 remove stale param 2025-09-02 13:37:16 -06:00
Yannis Panagis 9050a346e4 Added "SourceText" to __init__.py (#892) 2025-09-02 13:28:24 -06:00
Sourabh Desai 9690ccf4ea Fix tag push command in CONTRIBUTING.md (#894)
seems to be missing one little `v`
2025-09-02 10:49:14 -07:00
Sourabh Desai 97745f0f1c version bump to 0.6.63 (#893) 2025-09-02 10:36:51 -07:00
Sourabh Desai 61a696b9db add file names in return values (#888) 2025-08-29 15:55:18 -07:00
Sourabh Desai 3e01adaf0e add alternative builder method (#887)
* add alternative builder method

* fix test
2025-08-29 15:55:04 -07:00
93 changed files with 21358 additions and 11831 deletions
+8
View File
@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+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 -3
View File
@@ -19,11 +19,9 @@ jobs:
uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
version: 10
- 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"
@@ -1,4 +1,4 @@
name: Lint - Python
name: Lint
on:
push:
@@ -22,14 +22,25 @@ 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 }}
- name: Set up Python
run: uv python install ${{ matrix.python-version }}
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version-file: "ts/llama_cloud_services/.nvmrc"
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Run linter
shell: bash
working-directory: py
run: uv run -- pre-commit run -a
# the js checks are run roundaboutly through lint-staged, and -a doesn't run it. Run them directly.
- run: pnpm -w --filter llama-cloud-services run lint
- run: pnpm -w --filter llama-cloud-services run format:check
-37
View File
@@ -1,37 +0,0 @@
name: Lint - TypeScript
on:
push:
branches:
- main
paths:
- "ts/**"
pull_request:
paths:
- "ts/**"
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_REMOTE_ONLY: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: "ts/llama_cloud_services/.nvmrc"
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Run lint
working-directory: ts/llama_cloud_services/
run: pnpm run lint
- name: Run Prettier
working-directory: ts/llama_cloud_services/
run: pnpm run format
-66
View File
@@ -1,66 +0,0 @@
name: Publish Release - Python
on:
push:
tags:
- "v*"
workflow_dispatch:
env:
UV_VERSION: "0.7.20"
jobs:
build-n-publish:
name: Build and publish to PyPI
if: github.repository == 'run-llama/llama_cloud_services'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: ${{ env.UV_VERSION }}
- name: Set up Python
run: uv python install
- name: Display Python version
run: python --version
- name: Build
working-directory: py
run: uv build
- name: Test installing built package
shell: bash
working-directory: py
run: |
uv venv
uv pip install dist/*.whl
- name: Publish package
shell: bash
working-directory: py
run: uv publish --token ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
- name: Build and publish llama-parse
working-directory: py/llama_parse/
run: |
uv build
uv publish --token ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }} - LlamaCloud Services PY
artifacts: "py/**/dist/*"
generateReleaseNotes: true
draft: false
prerelease: false
-54
View File
@@ -1,54 +0,0 @@
name: Publish Release - TypeScript
on:
push:
tags:
- "llama-cloud-services@*"
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: "ts/llama_cloud_services/.nvmrc"
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
- name: Run Build
working-directory: ts/llama_cloud_services/
run: pnpm build
- name: Build tarball
run: |
pnpm pack
working-directory: ts/llama_cloud_services
- name: Setup npm authentication
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Release
working-directory: ts/llama_cloud_services
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: pnpm publish --access public --no-git-checks
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: "ts/llama_cloud_services/llama-cloud-services*.tgz"
name: Release ${{ github.ref }} - LlamaCloud Services TS
bodyFile: "ts/llama_cloud_services/CHANGELOG.md"
token: ${{ secrets.GITHUB_TOKEN }}
+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 }}
+5 -8
View File
@@ -1,4 +1,4 @@
name: Lint - TypeScript
name: Test - TypeScript
on:
push:
@@ -23,17 +23,14 @@ jobs:
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with:
version: 10
- 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
run: pnpm install --no-frozen-lockfile
- name: Run Build
working-directory: ts/llama_cloud_services/
run: pnpm build
run: pnpm -r install --no-frozen-lockfile
- name: Build package
run: pnpm --filter llama-cloud-services build
- name: Run Tests
working-directory: ts/llama_cloud_services/
run: pnpm test
@@ -0,0 +1,61 @@
name: Version Bump and Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
# Only run on main branch pushes
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout Repo
uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "22"
cache: "pnpm"
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Install dependencies
run: pnpm install
- name: Add auth token to .npmrc file
run: |
cat << EOF >> ".npmrc"
//registry.npmjs.org/:_authToken=$NPM_TOKEN
EOF
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Create Release Pull Request or Publish packages
id: changesets
uses: changesets/action@v1
with:
commit: "chore: version packages"
title: "chore: version packages"
# Custom version script
version: pnpm -w run version
# Custom publish script
publish: pnpm -w run publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
LLAMA_PARSE_PYPI_TOKEN: ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
+1
View File
@@ -9,3 +9,4 @@ __pycache__/
node_modules/
.turbo/
dist/
.npmrc
+7 -5
View File
@@ -29,7 +29,7 @@ repos:
- id: black-jupyter
name: black-src
alias: black
exclude: ".*uv.lock"
exclude: ".*uv.lock|examples/extract/solar_panel_e2e_comparison.ipynb"
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.0.1
hooks:
@@ -60,11 +60,13 @@ repos:
additional_dependencies: [black==23.10.1]
# Using PEP 8's line length in docs prevents excess left/right scrolling
args: [--line-length=79]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3
- repo: local
hooks:
- id: prettier
exclude: ^(uv.lock|ts/llama_cloud_services/pnpm-lock.yaml|ts/e2e-tests)
- id: lint-staged
name: Run lint-staged for TS files
entry: pnpm -w exec lint-staged
language: system
pass_filenames: false
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
+1 -1
View File
@@ -18,7 +18,7 @@ versions need to be kept consistent to sidecar it with `llama_cloud_services`. B
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`.
Once the change is merged, push a tag `git tag -a v0.x.x -m 0.x.x` and `git push origin v0.x.x`.
This tagging step can be done with `./scripts/version-bump tag`.
-6
View File
@@ -9,7 +9,6 @@ This repository contains the code for hand-written SDKs and clients for interact
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.
- [LlamaCloud Index](./index.md) - A widely customizable and fully automated document ingestion pipeline that also serves retrieval purposes.
@@ -28,13 +27,11 @@ Then, you can use the services in your code:
```python
from llama_cloud_services import (
LlamaParse,
LlamaReport,
LlamaExtract,
LlamaCloudIndex,
)
parser = LlamaParse(api_key="YOUR_API_KEY")
report = LlamaReport(api_key="YOUR_API_KEY")
extract = LlamaExtract(api_key="YOUR_API_KEY")
index = LlamaCloudIndex(
"my_first_index", project_name="default", api_key="YOUR_API_KEY"
@@ -44,7 +41,6 @@ index = LlamaCloudIndex(
See the quickstart guides for each service for more information:
- [LlamaParse](./parse.md)
- [LlamaReport (beta/invite-only)](./report.md)
- [LlamaExtract](./extract.md)
- [LlamaCloud Index](./index.md)
@@ -57,13 +53,11 @@ You can also create your API key in the EU region [here](https://cloud.eu.llamai
```python
from llama_cloud_services import (
LlamaParse,
LlamaReport,
LlamaExtract,
EU_BASE_URL,
)
parser = LlamaParse(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
report = LlamaReport(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
extract = LlamaExtract(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
index = LlamaCloudIndex(
"my_first_index",
+1 -1
View File
@@ -4,6 +4,6 @@ In this folder you will find several python notebooks that contain examples rega
- [LlamaParse](./parse/)
- [LlamaExtract](./extract/)
- [LlamaReport](./report/)
- [LlamaCloudIndex](./index/)
Follow the instructions in each notebook to get started!
@@ -7,7 +7,7 @@
"source": [
"# Extraction and Analysis over a Fidelity Multi-Fund Annual Report\n",
"\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services-demo/blob/main/examples/extract/asset_manager_fund_analysis.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/extract/asset_manager_fund_analysis.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"\n",
"In this notebook we show you how to create an agentic document workflow over a complex document that contains annual reports for multiple funds - each fund reports financials in a standardized reporting structure, and it's all consolidated in the same document.\n",
"\n",
@@ -7,7 +7,7 @@
"source": [
"# Automotive Equity Research: A Multi-Step Agentic Workflow\n",
"\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services-demo/blob/main/examples/extract/automotive_sector_analysis.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/extract/automotive_sector_analysis.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"\n",
"This notebook demonstrates an endtoend agentic workflow using LlamaExtract and the LlamaIndex eventdriven workflow framework for automotive sector analysis.\n",
"\n",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@
"source": [
"# Dynamic Section Retrieval with LlamaParse\n",
"\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services-demo/blob/main/examples/parse/advanced_rag/dynamic_section_retrieval.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/advanced_rag/dynamic_section_retrieval.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"\n",
"This notebook showcases a concept called \"dynamic section retrieval\".\n",
"\n",
+1 -1
View File
@@ -6,7 +6,7 @@
"source": [
"# Advanced RAG with LlamaParse\n",
"\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_parse/blob/main/examples/demo_advanced.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_parse/blob/main/examples/parse/demo_advanced.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"\n",
"This notebook is a complete walkthrough for using LlamaParse with advanced indexing/retrieval techniques in LlamaIndex over the Apple 10K Filing. \n",
"\n",
+2 -2
View File
@@ -6,7 +6,7 @@
"source": [
"# RAG with Excel Spreadsheet using LlamaPrase\n",
"\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/demo_excel.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/demo_excel.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"\n",
"This notebook shows you using LlamaParse with Excel Spreadsheet.\n",
"\n",
@@ -43,7 +43,7 @@
"source": [
"from llama_cloud_services import LlamaParse\n",
"\n",
"api_key = \"llx-jwAQZL8T38onyL9hKBOXyRtnuCU0Fk3z7tmDhIT3L0GEfohJ\" # get from cloud.llamaindex.ai"
"api_key = \"llx-...\" # get from cloud.llamaindex.ai"
]
},
{
+1 -1
View File
@@ -7,7 +7,7 @@
"source": [
"# Download Charts\n",
"\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/demo_get_charts.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/demo_get_charts.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"\n",
"This notebook demonstrates how to download charts from a document using the result object.\n",
"\n",
+4 -4
View File
@@ -6,7 +6,7 @@
"source": [
"# LlamaParse - Fast checking Insurance Contract for Coverage\n",
"\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/demo_insurance.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/demo_insurance.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"\n",
"In this notebook we will look at how LlamaParse can be used to extract structured coverage information from an insurance policy.\n",
"\n",
@@ -36,7 +36,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## Download an insurance policy fron IRDAI\n",
"## Download an insurance policy from IRDAI\n",
"\n",
"The Insurance Regulatory and Development Authority of India (IRDAI) maintains a great resource: https://policyholder.gov.in/web/guest/non-life-insurance-products where all insurance policies available in India are publicly available for download! Let's download a complex health insurance policy as an example."
]
@@ -228,11 +228,11 @@
" result_type=\"markdown\",\n",
" system_prompt_append=\"\"\"\n",
"This document is an insurance policy.\n",
"When a benefits/coverage/exlusion is describe in the document ammend to it add a text in the follwing benefits string format (where coverage could be an exclusion).\n",
"When a benefits/coverage/exlusion is describe in the document amend to it add a text in the following benefits string format (where coverage could be an exclusion).\n",
"\n",
"For {nameofrisk} and in this condition {whenDoesThecoverageApply} the coverage is {coverageDescription}. \n",
" \n",
"If the document contain a benefits TABLE that describe coverage amounts, do not ouput it as a table, but instead as a list of benefits string.\n",
"If the document contain a benefits TABLE that describe coverage amounts, do not output it as a table, but instead as a list of benefits string.\n",
" \n",
"\"\"\",\n",
").aparse(\"./policy.pdf\")\n",
+1 -1
View File
@@ -7,7 +7,7 @@
"source": [
"# LlamaParse `JobResult` Tour\n",
"\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/demo_json.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/demo_json.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"\n",
"The `JobResult` object is the main object returned by the LlamaParse API. It contains all the information about the job, including the parsed data, metadata, and any errors.\n",
"\n",
+1 -1
View File
@@ -9,7 +9,7 @@
"\n",
"LlamaParse supports users to specify a `language` parameter before uploading documents, giving users better OCR capabilities over non-English PDFs, parsing images into more accurate representations.\n",
"\n",
"You can specify 80+ different languages: see this file for a full list of supported languages: https://github.com/run-llama/llama_cloud_services/blob/main/llama_parse/base.py.\n",
"You can specify 80+ different languages: see this file for a full list of supported languages: https://github.com/run-llama/llama_cloud_services/blob/main/py/llama_cloud_services/parse/base.py.\n",
"\n",
"This notebook shows a demo of this in action. \n",
"\n",
-1
View File
@@ -75,7 +75,6 @@
" adaptive_long_table=True,\n",
" outlined_table_extraction=True,\n",
" output_tables_as_HTML=True,\n",
" api_key=\"llx-jwAQZL8T38onyL9hKBOXyRtnuCU0Fk3z7tmDhIT3L0GEfohJ\",\n",
")\n",
"\n",
"result = await parser.aparse(\"./dcf_template.xlsx\")\n",
+1 -1
View File
@@ -4,7 +4,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/excel/o1_excel_rag.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/excel/o1_excel_rag.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
@@ -740,7 +740,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"In this example, these pages aren't going to be that different when parsed, but we can verify which pages triggered auto-made by looking at the [JSON output](https://github.com/run-llama/llama_cloud_services/blob/main/examples/demo_json_tour.ipynb) of LlamaParse:"
"In this example, these pages aren't going to be that different when parsed, but we can verify which pages triggered auto-made by looking at the [JSON output](https://github.com/run-llama/llama_cloud_services/blob/main/examples/parse/demo_json_tour.ipynb) of LlamaParse:"
]
},
{
-762
View File
@@ -1,762 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Report Generation with LlamaReport\n",
"\n",
"In this notebook, we'll walk through the basic process of generating a report with LlamaReport, and highlight some of the key features of the library.\n",
"\n",
"TLDR:\n",
"1. Download source data to use as knowledge base for the report\n",
"2. Kick off report generation with a template\n",
"3. Get the plan and review/accept/reject suggestions\n",
"4. Get the final report\n",
"5. Review/accept/reject suggestions to edit the final report\n",
"6. Print the final report"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%pip install llama-cloud-services"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. Download Source Data\n",
"\n",
"Here, we download the `Attention is All You Need` paper as a PDF.\n",
"\n",
"LlamaReport currently supports up to 5 files as input, and essentially any file type that can be parsed by LlamaParse.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!wget \"https://arxiv.org/pdf/1706.03762.pdf\" -O \"./attention.pdf\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Kick off Report Generation\n",
"\n",
"Here, we kick off report generation with a template.\n",
"\n",
"The template can either be a string or a file path, but here we'll use a string.\n",
"\n",
"In our experiments, anything works as a template, but some general guidelines:\n",
"\n",
"- Use markdown formatting + instructions in each section to guide the report generation\n",
"- If using an existing file as a template, provide extra instructions to guide the report generation\n",
"\n",
"**NOTE:** Since we are in a notebook, we will use async functions and `await` throughout. Synchronous methods that work without `await` are available by just removing the `a` from the method name and removing the `await` keyword."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from llama_cloud_services import LlamaReport\n",
"\n",
"llama_report = LlamaReport(\n",
" api_key=\"llx-...\",\n",
")\n",
"\n",
"report_client = await llama_report.acreate_report(\n",
" name=\"my_cool_report_on_attention\",\n",
" # can pass in file paths or bytes\n",
" input_files=[\"./attention.pdf\"],\n",
" template_text=\"\"\"\\\n",
"# [Some title]\\n\\n\n",
"## TLDR\\n\n",
"A quick summary of the paper.\\n\\n\n",
"## Details\\n\n",
"More details about the paper, possibly more than one section here.\\n\n",
"\"\"\",\n",
" # optional additional instructions for the report generation\n",
" # template_instructions=None,\n",
" # optional file path to an existing template instead of template_text\n",
" # template_file=None,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The returned `ReportClient` object is used to interact with the report generation process for this specific report."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Report(id=0a394b33-1a3e-463c-b5cb-7ff8ab827d0a, name=my_cool_report_on_attention)\n"
]
}
],
"source": [
"print(report_client)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. Get the plan\n",
"\n",
"The first phases of report generation involve ingesting the source data and generating a plan.\n",
"\n",
"The plan is a list of instructions for the report generation, and can be reviewed/accepted/rejected by the user.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plan = await report_client.await_for_plan(\n",
" timeout=10000,\n",
" poll_interval=10,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"# {title}\n",
"[ReportQuery(field='title', prompt='Generate a clear and concise title for this paper about the Transformer model and attention mechanisms', context='The paper discusses the Transformer architecture for sequence transduction using attention mechanisms, focusing on machine translation applications')]\n",
"==================\n",
"## TLDR\n",
"\n",
"{tldr_content}\n",
"[ReportQuery(field='tldr_content', prompt='Write a brief, clear summary of the key points about the Transformer model', context='Focus on the main innovations: attention mechanisms, efficiency improvements, and state-of-the-art results in machine translation')]\n",
"==================\n",
"## Details\n",
"\n",
"{details_content}\n",
"[ReportQuery(field='details_content', prompt='Provide detailed information about the Transformer model architecture and its applications', context='Include information about:\\n- The attention mechanism implementation\\n- Advantages over recurrent and convolutional models\\n- Performance in machine translation tasks\\n- Training efficiency improvements')]\n",
"==================\n"
]
}
],
"source": [
"for plan_block in plan.blocks:\n",
" print(plan_block.block.template)\n",
" print(plan_block.queries)\n",
" print(\"==================\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"With the plan, we can either use it to kick off generation of the final report, or we can edit the plan and adjust it as needed.\n",
"\n",
"While we could manually edit the objects here and use `await report_client.aupdate_plan(action=\"edit\", updated_plan=plan)`, we can also use `LlamaReport` to agentically edit the plan."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"suggestions = await report_client.asuggest_edits(\n",
" \"Can you split the details section into two sections?\"\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Justification for change: \n",
"I'll help you break down the details section into two distinct parts - one focusing on the architecture and another on the practical applications and performance. This will make the content more organized and easier to follow. The original block at index 2 will be replaced with these two new sections.\n",
"\n",
"Proposed changes:\n",
"\n",
"## Architecture Details\n",
"\n",
"{architecture_content}\n",
"\n",
"[ReportQuery(field='architecture_content', prompt='Describe the technical details of the Transformer model architecture', context='Focus on:\\n- Core components of the Transformer architecture\\n- Self-attention mechanism implementation\\n- Multi-head attention details\\n- Position encoding approach\\n- Feed-forward network structure')]\n",
"==================\n",
"\n",
"## Performance and Applications\n",
"\n",
"{applications_content}\n",
"\n",
"[ReportQuery(field='applications_content', prompt='Explain the practical applications and performance advantages of the Transformer model', context='Cover:\\n- Comparison with RNN and CNN models\\n- Machine translation results and benchmarks\\n- Training efficiency improvements\\n- Real-world applications and use cases\\n- Scalability benefits')]\n",
"==================\n"
]
}
],
"source": [
"for suggestion in suggestions:\n",
" print(\"Justification for change:\", suggestion.justification)\n",
" print(\"Proposed changes:\")\n",
" for plan_block in suggestion.blocks:\n",
" print(plan_block.block.template)\n",
" print(plan_block.queries)\n",
" print(\"==================\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This looks pretty good! We can also use the client to automatically accept and apply, or reject, these suggestions.\n",
"\n",
"This will (locally) keep track of the history of changes, so that future suggestions can be based on the previous changes."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for suggestion in suggestions:\n",
" await report_client.aaccept_edit(suggestion)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"What effect did that have on the tracked local history? Let's see!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[EditAction(block_idx=2, old_content='## Details\\n\\n{details_content}\\n\\nField: details_content, Prompt: Provide detailed information about the Transformer model architecture and its applications, Context: Include information about:\\n- The attention mechanism implementation\\n- Advantages over recurrent and convolutional models\\n- Performance in machine translation tasks\\n- Training efficiency improvements\\nDepends on: none', new_content='\\n## Architecture Details\\n\\n{architecture_content}\\n\\n\\nField: architecture_content, Prompt: Describe the technical details of the Transformer model architecture, Context: Focus on:\\n- Core components of the Transformer architecture\\n- Self-attention mechanism implementation\\n- Multi-head attention details\\n- Position encoding approach\\n- Feed-forward network structure\\nDepends on: none', action='approved', timestamp=datetime.datetime(2025, 2, 4, 20, 59, 55, 773558)),\n",
" EditAction(block_idx=3, old_content='[No old content]', new_content='\\n## Performance and Applications\\n\\n{applications_content}\\n\\n\\nField: applications_content, Prompt: Explain the practical applications and performance advantages of the Transformer model, Context: Cover:\\n- Comparison with RNN and CNN models\\n- Machine translation results and benchmarks\\n- Training efficiency improvements\\n- Real-world applications and use cases\\n- Scalability benefits\\nDepends on: previous', action='approved', timestamp=datetime.datetime(2025, 2, 4, 20, 59, 55, 773687))]"
]
},
"execution_count": null,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"report_client.edit_history"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[Message(role=<MessageRole.USER: 'user'>, content='Can you split the details section into two sections?', timestamp=datetime.datetime(2025, 2, 4, 20, 59, 47, 754848)),\n",
" Message(role=<MessageRole.ASSISTANT: 'assistant'>, content=\"\\nI'll help you break down the details section into two distinct parts - one focusing on the architecture and another on the practical applications and performance. This will make the content more organized and easier to follow. The original block at index 2 will be replaced with these two new sections.\\n\", timestamp=datetime.datetime(2025, 2, 4, 20, 59, 55, 482070))]"
]
},
"execution_count": null,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"report_client.chat_history"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"These two items are used to provide context for future suggestions! You can always clear this, or provide your own history."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# report_client.suggest_edits(\"....\", chat_history=[{\"role\": \"user\", \"content\": \"...\"}, ...])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. Get the final report\n",
"\n",
"Now that we have a plan, we can kick off generation of the final report."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# kicks off report generation\n",
"await report_client.aupdate_plan(action=\"approve\")\n",
"\n",
"# waits for report generation to complete\n",
"report = await report_client.await_completion(\n",
" timeout=10000,\n",
" poll_interval=10,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"# Attention Is All You Need: A Pure Attention-Based Architecture for Neural Machine Translation\n",
"\n",
"## TLDR\n",
"\n",
"The Transformer introduced a revolutionary architecture that relies entirely on attention mechanisms, eliminating the need for recurrence or convolution in sequence processing. Its key innovations include multi-head self-attention for parallel processing of input sequences, scaled dot-product attention for efficient computation, and positional encodings for sequence order awareness. The model achieved breakthrough results in machine translation (28.4 BLEU on English-to-German, 41.8 BLEU on English-to-French) while requiring significantly less training time than previous approaches, training in 3.5 days on 8 GPUs. This architecture demonstrated that attention mechanisms alone are sufficient for state-of-the-art sequence modeling, setting a new direction for natural language processing.\n",
"\n",
"\n",
"## Architecture Details\n",
"\n",
"The Transformer architecture represents a groundbreaking approach to sequence processing, built entirely on attention mechanisms without recurrence or convolution. Here are its key technical details:\n",
"\n",
"Core Components:\n",
"- Encoder-decoder architecture with stacked self-attention and point-wise feed-forward layers\n",
"- Each layer contains two main sub-layers: multi-head self-attention mechanism and position-wise feed-forward network\n",
"- Layer normalization and residual connections between sub-layers\n",
"- No recurrent or convolutional elements, enabling parallel processing\n",
"\n",
"Self-Attention Mechanism:\n",
"- Processes relationships between all positions in a sequence simultaneously\n",
"- Computes attention weights using queries, keys, and values derived from input representations\n",
"- Implements scaled dot-product attention to prevent gradient issues with large input dimensions\n",
"- Allows direct modeling of dependencies regardless of positional distance\n",
"- Uses masking in decoder to prevent leftward information flow and maintain auto-regressive property\n",
"\n",
"Multi-Head Attention:\n",
"- Employs multiple attention heads operating in parallel\n",
"- Each head processes information in different representation subspaces\n",
"- Three types of attention applications:\n",
" 1. Encoder self-attention (all positions attend to each other)\n",
" 2. Decoder self-attention (each position attends to previous positions)\n",
" 3. Encoder-decoder attention (decoder queries attend to encoder outputs)\n",
"- Counteracts reduced resolution from attention averaging through parallel processing\n",
"\n",
"Position-wise Feed-Forward Network:\n",
"- Applied identically to each position separately\n",
"- Consists of two linear transformations with ReLU activation\n",
"- Structure: FFN(x) = max(0, xW1 + b1)W2 + b2\n",
"- Input and output dimensionality: dmodel = 512\n",
"- Inner-layer dimensionality: dff = 2048\n",
"- Parameters vary between layers but remain constant across positions\n",
"\n",
"Position Encoding:\n",
"- Adds positional information to input embeddings\n",
"- Enables the model to consider sequential order without recurrence\n",
"- Implements sinusoidal position encodings to allow model to attend to relative positions\n",
"- Maintains constant number of operations between any two positions, unlike convolutional approaches\n",
"- Allows effective modeling of both local and long-range dependencies\n",
"\n",
"\n",
"\n",
"## Performance and Applications\n",
"\n",
"The Transformer model demonstrates significant performance advantages and practical applications across multiple domains:\n",
"\n",
"Performance Advantages over RNN/CNN Models:\n",
"- Eliminates sequential computation constraints present in RNNs, enabling superior parallelization\n",
"- Reduces operations needed for relating distant positions to a constant number, compared to linear/logarithmic scaling in CNNs\n",
"- Processes all input and output positions simultaneously through self-attention mechanisms\n",
"- Achieves state-of-the-art results while requiring significantly less computational resources\n",
"\n",
"Machine Translation Benchmarks:\n",
"- WMT 2014 English-to-German: 28.4 BLEU score, exceeding previous best results by over 2 BLEU points\n",
"- WMT 2014 English-to-French: 41.8 BLEU score (single-model state-of-the-art)\n",
"- Surpasses performance of existing model ensembles in translation tasks\n",
"\n",
"Training Efficiency:\n",
"- Requires only 3.5 days of training on eight GPUs for state-of-the-art performance\n",
"- Achieves superior results at \"a small fraction of the training costs\" compared to previous models\n",
"- Enables significantly faster training through parallel processing of input/output sequences\n",
"- Can reach production-quality performance in as little as twelve hours on modern GPU hardware\n",
"\n",
"Real-world Applications:\n",
"- Machine translation systems\n",
"- Natural language understanding tasks\n",
"- Reading comprehension\n",
"- Abstractive summarization\n",
"- Text entailment analysis\n",
"- Constituency parsing (achieving 92.7 F1 score in semi-supervised settings)\n",
"- Adaptable to both large and limited training data scenarios\n",
"\n",
"Scalability Benefits:\n",
"- Highly parallelizable architecture enables efficient scaling across multiple GPUs\n",
"- Constant computational complexity for relating any input/output positions\n",
"- Effective handling of long-range dependencies in sequences\n",
"- Maintains performance quality while scaling to larger datasets and model sizes\n",
"- Generalizes well across different tasks and domains without architectural changes\n",
"- Supports efficient inference and deployment in production environments\n",
"\n"
]
}
],
"source": [
"report_text = \"\\n\\n\".join([block.template for block in report.blocks])\n",
"print(report_text)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 5. Edit the final report\n",
"\n",
"Now that we have a report, we can edit it.\n",
"\n",
"We can use the `asuggest_edits` method to get suggestions for edits, and then use the `aaccept_edit`/`areject_edit` methods to apply them.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Justification for change: \n",
"I'd suggest changing \"TLDR\" to \"Executive Summary\" which is more appropriate for a professional or academic report. This term is widely used in formal documents and better reflects the nature of this concise overview section while maintaining the same function of providing a quick summary of the key points.\n",
"\n",
"Proposed changes:\n",
"## Executive Summary\n",
"\n",
"The Transformer introduced a revolutionary architecture that relies entirely on attention mechanisms, eliminating the need for recurrence or convolution in sequence processing. Its key innovations include multi-head self-attention for parallel processing of input sequences, scaled dot-product attention for efficient computation, and positional encodings for sequence order awareness. The model achieved breakthrough results in machine translation (28.4 BLEU on English-to-German, 41.8 BLEU on English-to-French) while requiring significantly less training time than previous approaches, training in 3.5 days on 8 GPUs. This architecture demonstrated that attention mechanisms alone are sufficient for state-of-the-art sequence modeling, setting a new direction for natural language processing.\n",
"==================\n"
]
}
],
"source": [
"suggestions = await report_client.asuggest_edits(\n",
" \"Can you change the TLDR header to something more professional?\"\n",
")\n",
"for suggestion in suggestions:\n",
" print(\"Justification for change:\", suggestion.justification)\n",
" print(\"Proposed changes:\")\n",
" for block in suggestion.blocks:\n",
" print(block.template)\n",
" print(\"==================\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Changing to \"Executive Summary\" sounds reasonable, lets accept that!\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for suggestion in suggestions:\n",
" await report_client.aaccept_edit(suggestion)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 7. Print the final report\n",
"\n",
"Now that we have a report, we can print it."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"# Attention Is All You Need: A Pure Attention-Based Architecture for Neural Machine Translation\n",
"\n",
"## Executive Summary\n",
"\n",
"The Transformer introduced a revolutionary architecture that relies entirely on attention mechanisms, eliminating the need for recurrence or convolution in sequence processing. Its key innovations include multi-head self-attention for parallel processing of input sequences, scaled dot-product attention for efficient computation, and positional encodings for sequence order awareness. The model achieved breakthrough results in machine translation (28.4 BLEU on English-to-German, 41.8 BLEU on English-to-French) while requiring significantly less training time than previous approaches, training in 3.5 days on 8 GPUs. This architecture demonstrated that attention mechanisms alone are sufficient for state-of-the-art sequence modeling, setting a new direction for natural language processing.\n",
"\n",
"\n",
"## Architecture Details\n",
"\n",
"The Transformer architecture represents a groundbreaking approach to sequence processing, built entirely on attention mechanisms without recurrence or convolution. Here are its key technical details:\n",
"\n",
"Core Components:\n",
"- Encoder-decoder architecture with stacked self-attention and point-wise feed-forward layers\n",
"- Each layer contains two main sub-layers: multi-head self-attention mechanism and position-wise feed-forward network\n",
"- Layer normalization and residual connections between sub-layers\n",
"- No recurrent or convolutional elements, enabling parallel processing\n",
"\n",
"Self-Attention Mechanism:\n",
"- Processes relationships between all positions in a sequence simultaneously\n",
"- Computes attention weights using queries, keys, and values derived from input representations\n",
"- Implements scaled dot-product attention to prevent gradient issues with large input dimensions\n",
"- Allows direct modeling of dependencies regardless of positional distance\n",
"- Uses masking in decoder to prevent leftward information flow and maintain auto-regressive property\n",
"\n",
"Multi-Head Attention:\n",
"- Employs multiple attention heads operating in parallel\n",
"- Each head processes information in different representation subspaces\n",
"- Three types of attention applications:\n",
" 1. Encoder self-attention (all positions attend to each other)\n",
" 2. Decoder self-attention (each position attends to previous positions)\n",
" 3. Encoder-decoder attention (decoder queries attend to encoder outputs)\n",
"- Counteracts reduced resolution from attention averaging through parallel processing\n",
"\n",
"Position-wise Feed-Forward Network:\n",
"- Applied identically to each position separately\n",
"- Consists of two linear transformations with ReLU activation\n",
"- Structure: FFN(x) = max(0, xW1 + b1)W2 + b2\n",
"- Input and output dimensionality: dmodel = 512\n",
"- Inner-layer dimensionality: dff = 2048\n",
"- Parameters vary between layers but remain constant across positions\n",
"\n",
"Position Encoding:\n",
"- Adds positional information to input embeddings\n",
"- Enables the model to consider sequential order without recurrence\n",
"- Implements sinusoidal position encodings to allow model to attend to relative positions\n",
"- Maintains constant number of operations between any two positions, unlike convolutional approaches\n",
"- Allows effective modeling of both local and long-range dependencies\n",
"\n",
"\n",
"\n",
"## Performance and Applications\n",
"\n",
"The Transformer model demonstrates significant performance advantages and practical applications across multiple domains:\n",
"\n",
"Performance Advantages over RNN/CNN Models:\n",
"- Eliminates sequential computation constraints present in RNNs, enabling superior parallelization\n",
"- Reduces operations needed for relating distant positions to a constant number, compared to linear/logarithmic scaling in CNNs\n",
"- Processes all input and output positions simultaneously through self-attention mechanisms\n",
"- Achieves state-of-the-art results while requiring significantly less computational resources\n",
"\n",
"Machine Translation Benchmarks:\n",
"- WMT 2014 English-to-German: 28.4 BLEU score, exceeding previous best results by over 2 BLEU points\n",
"- WMT 2014 English-to-French: 41.8 BLEU score (single-model state-of-the-art)\n",
"- Surpasses performance of existing model ensembles in translation tasks\n",
"\n",
"Training Efficiency:\n",
"- Requires only 3.5 days of training on eight GPUs for state-of-the-art performance\n",
"- Achieves superior results at \"a small fraction of the training costs\" compared to previous models\n",
"- Enables significantly faster training through parallel processing of input/output sequences\n",
"- Can reach production-quality performance in as little as twelve hours on modern GPU hardware\n",
"\n",
"Real-world Applications:\n",
"- Machine translation systems\n",
"- Natural language understanding tasks\n",
"- Reading comprehension\n",
"- Abstractive summarization\n",
"- Text entailment analysis\n",
"- Constituency parsing (achieving 92.7 F1 score in semi-supervised settings)\n",
"- Adaptable to both large and limited training data scenarios\n",
"\n",
"Scalability Benefits:\n",
"- Highly parallelizable architecture enables efficient scaling across multiple GPUs\n",
"- Constant computational complexity for relating any input/output positions\n",
"- Effective handling of long-range dependencies in sequences\n",
"- Maintains performance quality while scaling to larger datasets and model sizes\n",
"- Generalizes well across different tasks and domains without architectural changes\n",
"- Supports efficient inference and deployment in production environments\n",
"\n"
]
}
],
"source": [
"report_response = await report_client.aget()\n",
"report_text = \"\\n\\n\".join([block.template for block in report_response.report.blocks])\n",
"print(report_text)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can also see the sources for each block!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0.99687636\n",
"# Abstract\n",
"\n",
"The dominant sequence transduction models are based on complex recurrent or convolutiona\n",
"==================\n",
"0.99591404\n",
"# 2 Background\n",
"\n",
"The goal of reducing sequential computation also forms the foundation of the Extende\n",
"==================\n",
"0.9951325\n",
"# 1 Introduction\n",
"\n",
"Recurrent neural networks, long short-term memory [13] and gated recurrent [7] neu\n",
"==================\n",
"0.99442345\n",
"# 7 Conclusion\n",
"\n",
"In this work, we presented the Transformer, the first sequence transduction model ba\n",
"==================\n",
"0.9967649\n",
"# 3.2.3 Applications of Attention in our Model\n",
"\n",
"The Transformer uses multi-head attention in three d\n",
"==================\n",
"0.99533635\n",
"# 2 Background\n",
"\n",
"The goal of reducing sequential computation also forms the foundation of the Extende\n",
"==================\n",
"0.9935868\n",
"# Abstract\n",
"\n",
"The dominant sequence transduction models are based on complex recurrent or convolutiona\n",
"==================\n",
"0.98780584\n",
"# Outputs\n",
"\n",
"(shifted right)\n",
"\n",
"Figure 1: The Transformer - model architecture.\n",
"\n",
"The Transformer follows\n",
"==================\n",
"0.9205043\n",
"# 3.3 Position-wise Feed-Forward Networks\n",
"\n",
"In addition to attention sub-layers, each of the layers i\n",
"==================\n",
"0.79581684\n",
"# 1 Introduction\n",
"\n",
"Recurrent neural networks, long short-term memory [13] and gated recurrent [7] neu\n",
"==================\n",
"0.9946774\n",
"# Abstract\n",
"\n",
"The dominant sequence transduction models are based on complex recurrent or convolutiona\n",
"==================\n",
"0.97079873\n",
"# 7 Conclusion\n",
"\n",
"In this work, we presented the Transformer, the first sequence transduction model ba\n",
"==================\n",
"0.9535353\n",
"# 6.3 English Constituency Parsing\n",
"\n",
"To evaluate if the Transformer can generalize to other tasks we \n",
"==================\n",
"0.9514138\n",
"# 2 Background\n",
"\n",
"The goal of reducing sequential computation also forms the foundation of the Extende\n",
"==================\n",
"0.9790758\n",
"# 1 Introduction\n",
"\n",
"Recurrent neural networks, long short-term memory [13] and gated recurrent [7] neu\n",
"==================\n",
"0.92262185\n",
"# Outputs\n",
"\n",
"(shifted right)\n",
"\n",
"Figure 1: The Transformer - model architecture.\n",
"\n",
"The Transformer follows\n",
"==================\n"
]
}
],
"source": [
"for block in report_response.report.blocks:\n",
" # Each block has a list of sources, which are the nodes that were used to generate the block\n",
" for source in block.sources:\n",
" print(source.score)\n",
" print(source.node.text[:100])\n",
" print(\"==================\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "llama-parse-aNC435Vv-py3.10",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "llama-cloud-services-workspace",
"version": "0.0.1",
"description": "",
"private": true,
"keywords": [],
"author": "",
"scripts": {
"pre-commit-version": "pnpm changeset",
"version": "./scripts/changeset-version.py version",
"publish": "./scripts/changeset-version.py publish --tag"
},
"devDependencies": {
"prettier": "^3.6.2",
"lint-staged": "^15.4.2",
"@changesets/cli": "^2.29.5",
"changesets": "^1.0.2"
},
"lint-staged": {
"ts/llama_cloud_services/src/**/*.{ts,tsx,js,jsx}": [
"pnpm --filter llama-cloud-services exec eslint --fix",
"pnpm --filter llama-cloud-services exec prettier --write"
]
},
"packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912"
}
+1 -1
View File
@@ -147,7 +147,7 @@ documents = SimpleDirectoryReader(
).load_data()
```
Full documentation for `SimpleDirectoryReader` can be found on the [LlamaIndex Documentation](https://docs.llamaindex.ai/en/stable/module_guides/loading/simpledirectoryreader.html).
Full documentation for `SimpleDirectoryReader` can be found on the [LlamaIndex Documentation](https://developers.llamaindex.ai/python/framework/module_guides/loading/simpledirectoryreader/).
## Examples
+834 -10
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -1,2 +1,4 @@
packages:
- "ts/**"
- "ts/*"
- "py"
- "py/*"
+44
View File
@@ -0,0 +1,44 @@
# 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
- d028397: Update llama-cloud api version, and integrate with agent data deletion
+1 -7
View File
@@ -9,7 +9,6 @@ This repository contains the code for hand-written SDKs and clients for interact
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.
- [LlamaCloud Index](../index.md) - A widely customizable and fully automated document ingestion pipeline that also serves retrieval purposes.
@@ -28,14 +27,12 @@ Then, you can use the services in your code:
```python
from llama_cloud_services import (
LlamaParse,
LlamaReport,
LlamaExtract,
LlamaCloudIndex,
)
from llama_cloud_services import LlamaParse, LlamaReport, LlamaExtract
from llama_cloud_services import LlamaParse, LlamaExtract
parser = LlamaParse(api_key="YOUR_API_KEY")
report = LlamaReport(api_key="YOUR_API_KEY")
extract = LlamaExtract(api_key="YOUR_API_KEY")
index = LlamaCloudIndex(
"my_first_index", project_name="default", api_key="YOUR_API_KEY"
@@ -45,7 +42,6 @@ index = LlamaCloudIndex(
See the quickstart guides for each service for more information:
- [LlamaParse](../parse.md)
- [LlamaReport (beta/invite-only)](../report.md)
- [LlamaExtract](../extract.md)
- [LlamaCloud Index](../index.md)
@@ -58,13 +54,11 @@ You can also create your API key in the EU region [here](https://cloud.eu.llamai
```python
from llama_cloud_services import (
LlamaParse,
LlamaReport,
LlamaExtract,
EU_BASE_URL,
)
parser = LlamaParse(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
report = LlamaReport(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
extract = LlamaExtract(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
index = LlamaCloudIndex(
"my_first_index",
+3 -3
View File
@@ -1,6 +1,6 @@
from llama_cloud_services.parse import LlamaParse
from llama_cloud_services.report import ReportClient, LlamaReport
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent
from llama_cloud_services.utils import SourceText, FileInput
from llama_cloud_services.constants import EU_BASE_URL
from llama_cloud_services.index import (
LlamaCloudCompositeRetriever,
@@ -10,10 +10,10 @@ from llama_cloud_services.index import (
__all__ = [
"LlamaParse",
"ReportClient",
"LlamaReport",
"LlamaExtract",
"ExtractionAgent",
"SourceText",
"FileInput",
"EU_BASE_URL",
"LlamaCloudIndex",
"LlamaCloudRetriever",
@@ -1,6 +1,11 @@
import os
from typing import Any, Dict, Generic, List, Optional, Type
from llama_cloud import (
AgentData,
PaginatedResponseAgentData,
PaginatedResponseAggregateGroup,
)
from llama_cloud.client import AsyncLlamaCloud
from tenacity import (
WrappedFn,
@@ -86,7 +91,7 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
client=llama_client,
type=ExtractedPerson,
collection="extracted_people",
agent_url_id="person-extraction-agent"
deployment_name="person-extraction-agent"
)
# Create data
@@ -109,10 +114,12 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
self,
type: Type[AgentDataT],
collection: str = "default",
agent_url_id: Optional[str] = None,
deployment_name: Optional[str] = None,
client: Optional[AsyncLlamaCloud] = None,
token: Optional[str] = None,
base_url: Optional[str] = None,
# deprecated, use deployment_name instead
agent_url_id: Optional[str] = None,
):
"""
Initialize the AsyncAgentDataClient.
@@ -123,11 +130,11 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
collection: Named collection within the agent for organizing data.
Defaults to "default". Collections allow logical separation of
different data types or workflows within the same agent.
agent_url_id: Unique identifier for the agent. This normally appears in the
url of an agent within the llama cloud platform. If not provided,
will attempt to use the LLAMA_DEPLOY_DEPLOYMENT_NAME environment
variable. Data can only be added to an already existing agent in the
platform.
deployment_name: Unique identifier for the agent deployment. This normally
appears in the URL of an agent within the Llama Cloud platform. If not
provided, will attempt to use the LLAMA_DEPLOY_DEPLOYMENT_NAME
environment variable. Data can only be added to an already existing
agent in the platform.
client: AsyncLlamaCloud client instance for API communication. If not provided, will
construct one from the provided api token and base url
token: Llama Cloud API token. Reads from LLAMA_CLOUD_API_KEY if not provided
@@ -135,15 +142,14 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
defaults to https://api.cloud.llamaindex.ai
Raises:
ValueError: If agent_url_id is not provided and the
ValueError: If deployment_name is not provided and the
LLAMA_DEPLOY_DEPLOYMENT_NAME environment variable is not set
Note:
The client automatically applies retry logic to all API calls with
exponential backoff for timeout, connection, and HTTP status errors.
"""
self.agent_url_id = agent_url_id or get_default_agent_id()
self.deployment_name = deployment_name or agent_url_id or get_default_agent_id()
self.collection = collection
if not client:
@@ -156,15 +162,19 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
@agent_data_retry
async def get_item(self, item_id: str) -> TypedAgentData[AgentDataT]:
raw_data = await self.client.beta.get_agent_data(
raw_data = await self.untyped_get_item(item_id)
return TypedAgentData.from_raw(raw_data, self.type)
@agent_data_retry
async def untyped_get_item(self, item_id: str) -> AgentData:
return await self.client.beta.get_agent_data(
item_id=item_id,
)
return TypedAgentData.from_raw(raw_data, validator=self.type)
@agent_data_retry
async def create_item(self, data: AgentDataT) -> TypedAgentData[AgentDataT]:
raw_data = await self.client.beta.create_agent_data(
agent_slug=self.agent_url_id,
deployment_name=self.deployment_name,
collection=self.collection,
data=data.model_dump(),
)
@@ -184,6 +194,21 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
async def delete_item(self, item_id: str) -> None:
await self.client.beta.delete_agent_data(item_id=item_id)
@agent_data_retry
async def delete(
self, filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None
) -> int:
"""
Delete agent data by query, similar to search.
Returns the number of deleted items.
"""
response = await self.client.beta.delete_agent_data_by_query_api_v_1_beta_agent_data_delete_post(
deployment_name=self.deployment_name,
collection=self.collection,
filter=filter,
)
return response.deleted_count
@agent_data_retry
async def search(
self,
@@ -210,9 +235,7 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
offset: Number of items to skip from the beginning. Defaults to 0.
include_total: Whether to include the total count in the response. Defaults to False to improve performance. It's recommended to only request on the first page.
"""
raw = await self.client.beta.search_agent_data_api_v_1_beta_agent_data_search_post(
agent_slug=self.agent_url_id,
collection=self.collection,
raw = await self.untyped_search(
filter=filter,
order_by=order_by,
offset=offset,
@@ -227,6 +250,25 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
total=raw.total_size,
)
@agent_data_retry
async def untyped_search(
self,
filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None,
order_by: Optional[str] = None,
offset: Optional[int] = None,
page_size: Optional[int] = None,
include_total: bool = False,
) -> PaginatedResponseAgentData:
return await self.client.beta.search_agent_data_api_v_1_beta_agent_data_search_post(
deployment_name=self.deployment_name,
collection=self.collection,
filter=filter,
order_by=order_by,
offset=offset,
page_size=page_size,
include_total=include_total,
)
@agent_data_retry
async def aggregate(
self,
@@ -253,8 +295,38 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
offset: Number of groups to skip from the beginning. Defaults to 0.
page_size: Maximum number of groups to return per page.
"""
raw = await self.client.beta.aggregate_agent_data_api_v_1_beta_agent_data_aggregate_post(
agent_slug=self.agent_url_id,
raw = await self.untyped_aggregate(
filter=filter,
group_by=group_by,
count=count,
first=first,
order_by=order_by,
offset=offset,
page_size=page_size,
)
return TypedAggregateGroupItems(
items=[
TypedAggregateGroup.from_raw(grp, validator=self.type)
for grp in raw.items
],
has_more=raw.next_page_token is not None,
total=raw.total_size,
)
@agent_data_retry
async def untyped_aggregate(
self,
filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None,
group_by: Optional[List[str]] = None,
count: Optional[bool] = None,
first: Optional[bool] = None,
order_by: Optional[str] = None,
offset: Optional[int] = None,
page_size: Optional[int] = None,
) -> PaginatedResponseAggregateGroup:
return await self.client.beta.aggregate_agent_data_api_v_1_beta_agent_data_aggregate_post(
deployment_name=self.deployment_name,
collection=self.collection,
page_size=page_size,
filter=filter,
@@ -264,11 +336,3 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
first=first,
offset=offset,
)
return TypedAggregateGroupItems(
items=[
TypedAggregateGroup.from_raw(item, validator=self.type)
for item in raw.items
],
has_more=raw.next_page_token is not None,
total=raw.total_size,
)
@@ -10,7 +10,7 @@ CRUD operations, search capabilities, filtering, and aggregation functionality
for managing agent-generated data at scale.
Key Concepts:
- Agent Slug: Unique identifier for an agent instance
- Deployment Name: Unique identifier for an agent deployment
- Collection: Named grouping of data within an agent (defaults to "default"). Data within a collection should be of the same type.
- Agent Data: Individual structured data records with metadata and timestamps
@@ -26,7 +26,7 @@ Example Usage:
client=async_llama_cloud,
type=Person,
collection="people",
agent_url_id="my-extraction-agent-xyz"
deployment_name="my-extraction-agent-xyz"
)
# Create typed data
@@ -56,7 +56,6 @@ from typing import (
# Type variable for user-defined data models
AgentDataT = TypeVar("AgentDataT", bound=BaseModel)
# Type variable for extracted data (can be dict or Pydantic model)
ExtractedT = TypeVar("ExtractedT", bound=Union[BaseModel, dict])
@@ -78,7 +77,7 @@ class TypedAgentData(BaseModel, Generic[AgentDataT]):
Attributes:
id: Unique identifier for this data record
agent_url_id: Identifier of the agent that created this data
deployment_name: Identifier of the agent deployment that created this data
collection: Named collection within the agent (used for organization)
data: The actual structured data payload (typed as AgentDataT)
created_at: Timestamp when the record was first created
@@ -94,8 +93,8 @@ class TypedAgentData(BaseModel, Generic[AgentDataT]):
"""
id: Optional[str] = Field(description="Unique identifier for this data record")
agent_url_id: str = Field(
description="Identifier of the agent that created this data"
deployment_name: str = Field(
description="Identifier of the agent deployment that created this data"
)
collection: Optional[str] = Field(
description="Named collection within the agent for data organization"
@@ -116,15 +115,15 @@ class TypedAgentData(BaseModel, Generic[AgentDataT]):
Args:
raw_data: Raw agent data from the API
validator: Pydantic model class to validate the data field
Returns:
TypedAgentData instance with validated data
"""
data: AgentDataT = validator.model_validate(raw_data.data)
return cls(
id=raw_data.id,
agent_url_id=raw_data.agent_slug,
deployment_name=raw_data.deployment_name,
collection=raw_data.collection,
data=data,
created_at=raw_data.created_at,
@@ -222,12 +221,16 @@ def parse_extracted_field_metadata(
return {
k: _parse_extracted_field_metadata_recursive(v)
for k, v in field_metadata.items()
if k not in _METADATA_FIELDS_SIBLING_TO_LEAF
and k not in _ADDITIONAL_ROOT_METADATA_FIELDS
if not _is_reasoning_field(k, v) and k not in _ADDITIONAL_ROOT_METADATA_FIELDS
}
_METADATA_FIELDS_SIBLING_TO_LEAF = {"reasoning"}
def _is_reasoning_field(field_name: str, field_value: Any) -> bool:
# There can either be a user specified reasoning field (from the schema), or a reasoning metadata field for the
# dict of values
return field_name == "reasoning" and isinstance(field_value, str)
_ADDITIONAL_ROOT_METADATA_FIELDS = {"error"}
@@ -257,14 +260,12 @@ def _parse_extracted_field_metadata_recursive(
except ValidationError:
pass
additional_fields = {
k: v
for k, v in field_value.items()
if k in _METADATA_FIELDS_SIBLING_TO_LEAF
k: v for k, v in field_value.items() if _is_reasoning_field(k, v)
}
return {
k: _parse_extracted_field_metadata_recursive(v, additional_fields)
for k, v in field_value.items()
if k not in _METADATA_FIELDS_SIBLING_TO_LEAF
if not _is_reasoning_field(k, v)
}
elif isinstance(field_value, list):
return [_parse_extracted_field_metadata_recursive(item) for item in field_value]
@@ -0,0 +1,10 @@
from llama_cloud_services.beta.classifier.client import ClassifyClient
from llama_cloud_services.beta.classifier.types import ClassifyJobResultsWithFiles
from llama_cloud_services.utils import SourceText, FileInput
__all__ = [
"ClassifyClient",
"ClassifyJobResultsWithFiles",
"SourceText",
"FileInput",
]
+171 -31
View File
@@ -1,6 +1,7 @@
import asyncio
import time
from typing import Optional
import warnings
from typing import Optional, List, Union
from pydantic import BaseModel
from llama_cloud.client import AsyncLlamaCloud
from llama_cloud.types import (
@@ -9,14 +10,20 @@ from llama_cloud.types import (
ClassifyJobResults,
ClassifyParsingConfiguration,
StatusEnum,
ClassifyJobWithStatus,
File,
)
from llama_cloud.resources.classifier.client import OMIT
from llama_cloud_services.files.client import FileClient
from llama_cloud_services.constants import POLLING_TIMEOUT_SECONDS
from llama_cloud_services.utils import is_terminal_status, augment_async_errors
from llama_cloud_services.utils import (
is_terminal_status,
augment_async_errors,
FileInput,
)
from llama_index.core.async_utils import DEFAULT_NUM_WORKERS, run_jobs
from llama_cloud_services.beta.classifier.types import (
ClassifyJobResultsWithFiles,
)
class ClassificationOutput(BaseModel):
@@ -52,6 +59,24 @@ class ClassifyClient:
self.file_client = FileClient(client, project_id, organization_id)
self.polling_timeout = polling_timeout
@classmethod
def from_api_key(
cls,
api_key: str,
project_id: Optional[str] = None,
organization_id: Optional[str] = None,
base_url: Optional[str] = None,
) -> "ClassifyClient":
"""
Create a classify client from an API key.
"""
client = AsyncLlamaCloud(token=api_key, base_url=base_url)
return cls(
client,
project_id,
organization_id,
)
async def acreate_classify_job(
self,
rules: list[ClassifierRule],
@@ -146,16 +171,115 @@ class ClassifyClient:
)
)
async def aclassify(
self,
rules: list[ClassifierRule],
files: Union[FileInput, List[FileInput]],
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
raise_on_error: bool = True,
workers: int = DEFAULT_NUM_WORKERS,
show_progress: bool = False,
) -> ClassifyJobResultsWithFiles:
"""
Classify one or more files from various input types.
Args:
rules: The rules to use for classification.
files: The file(s) to classify. Can be a single file or list of files. Each can be:
- str/Path: File path
- SourceText: Text content or file with explicit filename
- File: Already uploaded file
- BufferedIOBase: File-like object
parsing_configuration: The parsing configuration to use for classification.
raise_on_error: Whether to raise an error if the classification job fails.
workers: Number of parallel workers for uploading files.
show_progress: Whether to show progress bars.
Returns:
The results of the classification job with file metadata.
"""
# Normalize to list
if not isinstance(files, list):
files = [files]
# Upload all files
coroutines = [
self.file_client.upload_content(file_input) for file_input in files
]
uploaded_files: List[File] = await run_jobs(
coroutines,
show_progress=show_progress,
workers=workers,
desc="Uploading files for classification",
)
# Classify
results = await self.aclassify_file_ids(
rules,
[file.id for file in uploaded_files],
parsing_configuration,
raise_on_error,
)
return ClassifyJobResultsWithFiles.from_classify_job_results(
results, uploaded_files
)
def classify(
self,
rules: list[ClassifierRule],
files: Union[FileInput, List[FileInput]],
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
raise_on_error: bool = True,
workers: int = DEFAULT_NUM_WORKERS,
show_progress: bool = False,
) -> ClassifyJobResultsWithFiles:
"""
Classify one or more files from various input types (synchronous version).
Args:
rules: The rules to use for classification.
files: The file(s) to classify. Can be a single file or list of files. Each can be:
- str/Path: File path
- SourceText: Text content or file with explicit filename
- File: Already uploaded file
- BufferedIOBase: File-like object
parsing_configuration: The parsing configuration to use for classification.
raise_on_error: Whether to raise an error if the classification job fails.
workers: Number of parallel workers for uploading files.
show_progress: Whether to show progress bars.
Returns:
The results of the classification job with file metadata.
"""
with augment_async_errors():
return asyncio.run(
self.aclassify(
rules,
files,
parsing_configuration,
raise_on_error,
workers,
show_progress,
)
)
async def aclassify_file_path(
self,
rules: list[ClassifierRule],
file_input_path: str,
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
raise_on_error: bool = True,
) -> ClassifyJobResults:
file = await self.file_client.upload_file(file_input_path)
return await self.aclassify_file_ids(
rules, [file.id], parsing_configuration, raise_on_error
) -> ClassifyJobResultsWithFiles:
"""
Deprecated: Use aclassify() instead.
"""
warnings.warn(
"aclassify_file_path is deprecated, use aclassify() instead",
DeprecationWarning,
stacklevel=2,
)
return await self.aclassify(
rules, file_input_path, parsing_configuration, raise_on_error
)
def classify_file_path(
@@ -164,13 +288,18 @@ class ClassifyClient:
file_input_path: str,
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
raise_on_error: bool = True,
) -> ClassifyJobResults:
with augment_async_errors():
return asyncio.run(
self.aclassify_file_path(
rules, file_input_path, parsing_configuration, raise_on_error
)
)
) -> ClassifyJobResultsWithFiles:
"""
Deprecated: Use classify() instead.
"""
warnings.warn(
"classify_file_path is deprecated, use classify() instead",
DeprecationWarning,
stacklevel=2,
)
return self.classify(
rules, file_input_path, parsing_configuration, raise_on_error
)
async def aclassify_file_paths(
self,
@@ -180,16 +309,22 @@ class ClassifyClient:
raise_on_error: bool = True,
workers: int = DEFAULT_NUM_WORKERS,
show_progress: bool = False,
) -> ClassifyJobResults:
coroutines = [self.file_client.upload_file(path) for path in file_input_paths]
files: list[File] = await run_jobs(
coroutines,
show_progress=show_progress,
workers=workers,
desc="Uploading files for classification",
) -> ClassifyJobResultsWithFiles:
"""
Deprecated: Use aclassify() instead.
"""
warnings.warn(
"aclassify_file_paths is deprecated, use aclassify() instead",
DeprecationWarning,
stacklevel=2,
)
return await self.aclassify_file_ids(
rules, [file.id for file in files], parsing_configuration, raise_on_error
return await self.aclassify(
rules,
file_input_paths,
parsing_configuration,
raise_on_error,
workers,
show_progress,
)
def classify_file_paths(
@@ -198,15 +333,20 @@ class ClassifyClient:
file_input_paths: list[str],
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
raise_on_error: bool = True,
) -> ClassifyJobResults:
with augment_async_errors():
return asyncio.run(
self.aclassify_file_paths(
rules, file_input_paths, parsing_configuration, raise_on_error
)
)
) -> ClassifyJobResultsWithFiles:
"""
Deprecated: Use classify() instead.
"""
warnings.warn(
"classify_file_paths is deprecated, use classify() instead",
DeprecationWarning,
stacklevel=2,
)
return self.classify(
rules, file_input_paths, parsing_configuration, raise_on_error
)
async def wait_for_job_completion(self, job_id: str) -> ClassifyJobWithStatus:
async def wait_for_job_completion(self, job_id: str) -> ClassifyJob:
"""
Wait for a classify job to complete.
Meant to expose lower level access to classifier jobs for advanced use cases.
@@ -0,0 +1,59 @@
from llama_cloud.types.classify_job_results import ClassifyJobResults
from llama_cloud.types.file_classification import FileClassification
from llama_cloud.types.file import File
class FileClassificationWithFile(FileClassification):
"""
File classification with file object.
"""
file: File
@classmethod
def from_file_classification(
cls, file_classification: FileClassification, file: File
) -> "FileClassificationWithFile":
if file_classification.file_id != file.id:
raise ValueError(
f"File classification ID {file_classification.id} does not match file ID {file.id}"
)
ctor_args = {
**file_classification.dict(),
"file": file,
}
return cls(**ctor_args)
class ClassifyJobResultsWithFiles(ClassifyJobResults):
"""
Classify job results with file objects.
"""
items: list[FileClassificationWithFile]
@classmethod
def from_classify_job_results(
cls, classify_job_results: ClassifyJobResults, files: list[File]
) -> "ClassifyJobResultsWithFiles":
if len(classify_job_results.items) != len(files):
raise ValueError(
f"Number of classify job results {len(classify_job_results.items)} does not match number of files {len(files)}"
)
# create mapping of file classification result to file object
file_id_to_file: dict[str, File] = {file.id: file for file in files}
file_classification_to_file: list[tuple[FileClassification, File]] = []
for item in classify_job_results.items:
if item.file_id not in file_id_to_file:
raise ValueError(
f"File classification result {item.id} has file ID {item.file_id} that does not match any provided file ID"
)
file_classification_to_file.append((item, file_id_to_file[item.file_id]))
# create a list of file classification with file objects
ctor_args = classify_job_results.dict()
ctor_args["items"] = [
FileClassificationWithFile.from_file_classification(item, file)
for item, file in file_classification_to_file
]
return cls(**ctor_args)
+2 -1
View File
@@ -2,15 +2,16 @@ from llama_cloud_services.extract.extract import (
LlamaExtract,
ExtractConfig,
ExtractionAgent,
SourceText,
ExtractTarget,
ExtractMode,
)
from llama_cloud_services.utils import SourceText, FileInput
__all__ = [
"LlamaExtract",
"ExtractionAgent",
"SourceText",
"FileInput",
"ExtractConfig",
"ExtractTarget",
"ExtractMode",
+11 -158
View File
@@ -2,10 +2,9 @@ import asyncio
import base64
import os
import time
from io import BufferedIOBase, BufferedReader, BytesIO, TextIOWrapper
from io import BufferedIOBase, TextIOWrapper
from pathlib import Path
from typing import List, Optional, Type, Union, Coroutine, Any, TypeVar
import secrets
import warnings
import httpx
from pydantic import BaseModel
@@ -19,14 +18,12 @@ from llama_cloud import (
ExtractAgent as CloudExtractAgent,
ExtractConfig,
ExtractJob,
ExtractJobCreate,
ExtractRun,
File,
FileData,
ExtractMode,
StatusEnum,
ExtractTarget,
LlamaExtractSettings,
PaginatedExtractRunsResponse,
)
from llama_cloud.client import AsyncLlamaCloud
@@ -35,7 +32,8 @@ from llama_cloud_services.extract.utils import (
JSONObjectType,
ExperimentalWarning,
)
from llama_cloud_services.utils import augment_async_errors
from llama_cloud_services.utils import augment_async_errors, SourceText, FileInput
from llama_cloud_services.files.client import FileClient
from llama_index.core.schema import BaseComponent
from llama_index.core.async_utils import run_jobs
from llama_index.core.bridge.pydantic import Field, PrivateAttr
@@ -190,46 +188,6 @@ async def _wait_for_job_result(
)
class SourceText:
def __init__(
self,
*,
file: Union[bytes, BufferedIOBase, TextIOWrapper, str, Path, None] = None,
text_content: Optional[str] = None,
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:
random_hex = secrets.token_hex(4)
self.filename = f"text_input_{random_hex}.txt"
return
if isinstance(self.file, (bytes, BufferedIOBase, TextIOWrapper)):
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, File]
def run_in_thread(
coro: Coroutine[Any, Any, T],
thread_pool: ThreadPoolExecutor,
@@ -322,6 +280,7 @@ class ExtractionAgent:
self._thread_pool = ThreadPoolExecutor(
max_workers=min(10, (os.cpu_count() or 1) + 4)
)
self._file_client = FileClient(client, project_id, organization_id)
@property
def id(self) -> str:
@@ -371,65 +330,11 @@ class ExtractionAgent:
ValueError: If filename is not provided for bytes input or for file-like objects
without a name attribute.
"""
file_contents: Optional[Union[BufferedIOBase, BytesIO]] = None
try:
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
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)}")
# Add name attribute to file object if needed
if not hasattr(file_contents, "name"):
file_contents.name = file_input.filename # type: ignore
return await self._client.files.upload_file(
project_id=self._project_id, upload_file=file_contents
)
finally:
if file_contents is not None and isinstance(
file_contents, (BufferedReader, BytesIO)
):
file_contents.close()
return await self._file_client.upload_content(file_input)
async def _upload_file(self, file_input: FileInput) -> File:
source_text = None
if isinstance(file_input, File):
return file_input
if isinstance(file_input, SourceText):
source_text = file_input
elif isinstance(file_input, (str, Path)):
path = Path(file_input)
source_text = SourceText(file=path, filename=path.name)
else:
# Try to get filename from the file object if not provided
filename = None
if hasattr(file_input, "name"):
filename = os.path.basename(str(file_input.name))
if filename is None:
raise ValueError(
"Use SourceText to provide filename when uploading bytes or file-like objects."
)
warnings.warn(
"Use SourceText instead of bytes or file-like objects",
DeprecationWarning,
)
source_text = SourceText(file=file_input, filename=filename)
return await self.upload_file(source_text)
"""Upload a file from various input types using FileClient."""
return await self._file_client.upload_content(file_input)
async def _wait_for_job_result(self, job_id: str) -> Optional[ExtractRun]:
"""Wait for and return the results of an extraction job."""
@@ -463,56 +368,6 @@ class ExtractionAgent:
)
)
async def _run_extraction_test(
self,
files: Union[FileInput, List[FileInput]],
extract_settings: LlamaExtractSettings,
) -> Union[ExtractJob, List[ExtractJob]]:
if not isinstance(files, list):
files = [files]
single_file = True
else:
single_file = False
upload_tasks = [self._upload_file(file) for file in files]
with augment_async_errors():
uploaded_files = await run_jobs(
upload_tasks,
workers=self.num_workers,
desc="Uploading files",
show_progress=self.show_progress,
)
async def run_job(file: File) -> ExtractRun:
job_queued = await self._client.llama_extract.run_job_test_user(
job_create=ExtractJobCreate(
extraction_agent_id=self.id,
file_id=file.id,
data_schema_override=self.data_schema,
config_override=self.config,
),
extract_settings=extract_settings,
)
return await self._wait_for_job_result(job_queued.id)
job_tasks = [run_job(file) for file in uploaded_files]
with augment_async_errors():
extract_results = await run_jobs(
job_tasks,
workers=self.num_workers,
desc="Running extraction jobs",
show_progress=self.show_progress,
)
if self._verbose:
for file, job in zip(files, extract_results):
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}")
return extract_results[0] if single_file else extract_results
async def queue_extraction(
self,
files: Union[FileInput, List[FileInput]],
@@ -544,12 +399,10 @@ class ExtractionAgent:
job_tasks = [
self._client.llama_extract.run_job(
request=ExtractJobCreate(
extraction_agent_id=self.id,
file_id=file.id,
data_schema_override=self.data_schema,
config_override=self.config,
),
extraction_agent_id=self.id,
file_id=file.id,
data_schema_override=self.data_schema,
config_override=self.config,
)
for file in uploaded_files
]
+82
View File
@@ -1,9 +1,11 @@
from io import BytesIO
from typing import BinaryIO
import os
from pathlib import Path
from llama_cloud.client import AsyncLlamaCloud
from llama_cloud.types import File, FileCreate
from typing import Optional
from llama_cloud_services.utils import SourceText, FileInput
class FileClient:
@@ -95,3 +97,83 @@ class FileClient:
project_id=self.project_id,
organization_id=self.organization_id,
)
async def upload_content(
self, file_input: FileInput, external_file_id: Optional[str] = None
) -> File:
"""
Upload content from various input types or fetch an already-uploaded file.
Args:
file_input: The content to upload. Can be:
- File: Already uploaded file (returned as-is)
- str/Path: Path to a file on disk
- SourceText: Text content, file, or file_id with explicit filename
- BufferedIOBase: File-like binary object
external_file_id: Optional external identifier for the file
Returns:
File: The uploaded (or fetched) file object
Raises:
ValueError: If the input type is not supported or required info is missing
"""
# If already a File object, return it
if isinstance(file_input, File):
return file_input
# Handle SourceText
if isinstance(file_input, SourceText):
# If file_id is provided, fetch the file object
if file_input.file_id is not None:
return await self.get_file(file_input.file_id)
elif file_input.text_content is not None:
# Handle direct text content
text_bytes = file_input.text_content.encode("utf-8")
return await self.upload_bytes(
text_bytes, external_file_id or file_input.filename or "file"
)
elif isinstance(file_input.file, (str, Path)):
# Handle file paths using the existing upload_file method
return await self.upload_file(
str(file_input.file), external_file_id or file_input.filename
)
elif isinstance(file_input.file, bytes):
# Handle bytes
return await self.upload_bytes(
file_input.file, external_file_id or file_input.filename or "file"
)
elif hasattr(file_input.file, "read"):
# Handle any file-like object (TextIOWrapper, BytesIO, BufferedReader, BufferedIOBase, etc.)
content = file_input.file.read() # type: ignore
if isinstance(content, str):
content = content.encode("utf-8")
return await self.upload_bytes(
content, external_file_id or file_input.filename or "file"
)
else:
raise ValueError(f"Unsupported file type: {type(file_input.file)}")
# Handle string/Path directly
elif isinstance(file_input, (str, Path)):
return await self.upload_file(str(file_input), external_file_id)
# Handle raw file-like objects
elif hasattr(file_input, "read"):
if hasattr(file_input, "name"):
filename = os.path.basename(str(file_input.name))
else:
filename = external_file_id or "file"
# Read content to determine size
content = file_input.read()
if isinstance(content, str):
content = content.encode("utf-8")
return await self.upload_bytes(content, external_file_id or filename)
else:
raise ValueError(
f"Unsupported file input type: {type(file_input)}. "
f"Supported types: str, Path, SourceText, BufferedIOBase, or File."
)
+14 -12
View File
@@ -489,6 +489,7 @@ class LlamaCloudIndex(BaseManagedIndex):
name: str,
project_name: str = DEFAULT_PROJECT_NAME,
organization_id: Optional[str] = None,
project_id: Optional[str] = None,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
app_url: Optional[str] = None,
@@ -504,15 +505,15 @@ class LlamaCloudIndex(BaseManagedIndex):
app_url = app_url or os.environ.get("LLAMA_CLOUD_APP_URL", DEFAULT_APP_URL)
client = get_client(api_key, base_url, app_url, timeout)
# create project if it doesn't exist
project = client.projects.upsert_project(
organization_id=organization_id, request=ProjectCreate(name=project_name)
)
if project.id is None:
raise ValueError(f"Failed to create/get project {project_name}")
if verbose:
print(f"Created project {project.id} with name {project.name}")
if project_id is None:
# create project if it doesn't exist
project = client.projects.upsert_project(
organization_id=organization_id,
request=ProjectCreate(name=project_name),
)
project_id = project.id
if verbose:
print(f"Created project {project_id} with name {project_name}")
# create pipeline
pipeline_create = PipelineCreate(
@@ -523,7 +524,7 @@ class LlamaCloudIndex(BaseManagedIndex):
llama_parse_parameters=llama_parse_parameters or LlamaParseParameters(),
)
pipeline = client.pipelines.upsert_pipeline(
project_id=project.id, request=pipeline_create
project_id=project_id, request=pipeline_create
)
if pipeline.id is None:
raise ValueError(f"Failed to create/get pipeline {name}")
@@ -532,8 +533,7 @@ class LlamaCloudIndex(BaseManagedIndex):
return cls(
name,
project_name=project.name,
organization_id=project.organization_id,
project_id=project_id,
api_key=api_key,
base_url=base_url,
app_url=app_url,
@@ -606,6 +606,7 @@ class LlamaCloudIndex(BaseManagedIndex):
name: str,
project_name: str = DEFAULT_PROJECT_NAME,
organization_id: Optional[str] = None,
project_id: Optional[str] = None,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
app_url: Optional[str] = None,
@@ -631,6 +632,7 @@ class LlamaCloudIndex(BaseManagedIndex):
verbose=verbose,
embedding_config=embedding_config,
transform_config=transform_config,
project_id=project_id,
)
app_url = app_url or os.environ.get("LLAMA_CLOUD_APP_URL", DEFAULT_APP_URL)
+48 -1
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.",
@@ -396,6 +400,10 @@ class LlamaParse(BasePydanticReader):
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).",
)
precise_bounding_box: Optional[bool] = Field(
default=False,
description="If set to true, the parser will use a more precise bounding box to extract text from documents. This will increase the accuracy of the parsing job, but reduce the speed.",
)
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.",
@@ -416,7 +424,22 @@ class LlamaParse(BasePydanticReader):
default=False,
description="If set to true, the parser will extract sub-tables from the spreadsheet when possible (more than one table per sheet).",
)
spreadsheet_force_formula_computation: Optional[bool] = Field(
default=False,
description="If set to true, the parser will re-compute values for all spreadsheet cells containing formulas.",
)
specialized_chart_parsing_agentic: Optional[bool] = Field(
default=False,
description="If set to true, the parser will use a specialized agentic chart parsing model to extract data from charts. This model is able to understand the chart type and extract the data accordingly.",
)
specialized_chart_parsing_efficient: Optional[bool] = Field(
default=False,
description="If set to true, the parser will use a specialized efficient chart parsing model to extract data from charts. This model is faster and cheaper than the agentic model, but may be less accurate.",
)
specialized_chart_parsing_plus: Optional[bool] = Field(
default=False,
description="If set to true, the parser will use a specialized one-shot chart parsing model to extract data from charts. This model is able to understand the chart type and extract the data accordingly. It is more accurate than the efficient model, but also more expensive.",
)
strict_mode_buggy_font: Optional[bool] = Field(
default=False,
description="If set to true, the parser will fail if it can't extract text from a document because of a buggy font.",
@@ -694,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
@@ -928,6 +954,9 @@ class LlamaParse(BasePydanticReader):
if self.preset is not None:
data["preset"] = self.preset
if self.precise_bounding_box:
data["precise_bounding_box"] = self.precise_bounding_box
if self.replace_failed_page_mode is not None:
data["replace_failed_page_mode"] = self.replace_failed_page_mode.value
@@ -947,6 +976,24 @@ class LlamaParse(BasePydanticReader):
if self.spreadsheet_extract_sub_tables:
data["spreadsheet_extract_sub_tables"] = self.spreadsheet_extract_sub_tables
if self.spreadsheet_force_formula_computation:
data[
"spreadsheet_force_formula_computation"
] = self.spreadsheet_force_formula_computation
if self.specialized_chart_parsing_agentic:
data[
"specialized_chart_parsing_agentic"
] = self.specialized_chart_parsing_agentic
if self.specialized_chart_parsing_efficient:
data[
"specialized_chart_parsing_efficient"
] = self.specialized_chart_parsing_efficient
if self.specialized_chart_parsing_plus:
data["specialized_chart_parsing_plus"] = self.specialized_chart_parsing_plus
if self.strict_mode_buggy_font:
data["strict_mode_buggy_font"] = self.strict_mode_buggy_font
+149 -27
View File
@@ -1,17 +1,87 @@
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
from llama_cloud_services.parse.utils import (
make_api_request,
is_jupyter,
)
from llama_index.core.async_utils import asyncio_run
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.")
@@ -24,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."
)
@@ -58,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."
)
@@ -81,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."
)
@@ -109,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.")
@@ -159,9 +247,12 @@ class Page(BaseModel):
durationInSeconds: Optional[float] = Field(
default=None, description="The duration of the audio transcript in seconds."
)
slideSpeakerNotes: Optional[str] = Field(
default=None, description="The speaker notes for the slide."
)
class JobResult(BaseModel):
class JobResult(SafeBaseModel):
"""The raw JSON result from the LlamaParse API."""
pages: List[Page] = Field(
@@ -255,6 +346,29 @@ class JobResult(BaseModel):
documents = await self.aget_text_documents(split_by_page)
return [TextNode(text=doc.text, metadata=doc.metadata) for doc in documents]
def _format_markdown_for_notebook(self, text: Optional[str]) -> Optional[str]:
"""Format markdown text for Jupyter notebook display by escaping dollar signs."""
if text is None:
return None
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 single dollar signs escaped
"""
# 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_single_dollar_signs(text)
def get_markdown_documents(self, split_by_page: bool = False) -> List[Document]:
"""
Get the markdown documents from the job.
@@ -265,17 +379,22 @@ class JobResult(BaseModel):
if split_by_page:
return [
Document(
text=page.md,
text=self._format_markdown_for_notebook(page.md)
if is_jupyter()
else page.md,
metadata={"page_number": page.page, "file_name": self.file_name},
)
for page in self.pages
]
else:
text = self._page_separator.join(
[page.md if page.md is not None else "" for page in self.pages]
)
return [
Document(
text=self._page_separator.join(
[page.md if page.md is not None else "" for page in self.pages]
),
text=self._format_markdown_for_notebook(text)
if is_jupyter()
else text,
metadata={"file_name": self.file_name},
)
]
@@ -325,7 +444,10 @@ class JobResult(BaseModel):
"""
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")
markdown = response.content.decode("utf-8")
return (
self._format_markdown_for_notebook(markdown) if is_jupyter() else markdown
)
def get_text(self) -> str:
"""
+12
View File
@@ -1,3 +1,4 @@
import functools
import httpx
import itertools
import logging
@@ -356,6 +357,17 @@ def partition_pages(
return
@functools.lru_cache(maxsize=1)
def is_jupyter() -> bool:
"""Check if we're running in a Jupyter environment."""
try:
from IPython import get_ipython
return get_ipython().__class__.__name__ == "ZMQInteractiveShell"
except (ImportError, AttributeError):
return False
def extract_tables_from_json_results(
json_results: List[dict], download_path: str
) -> List[str]:
@@ -1,4 +0,0 @@
from llama_cloud_services.report.report import ReportClient
from llama_cloud_services.report.base import LlamaReport
__all__ = ["ReportClient", "LlamaReport"]
-269
View File
@@ -1,269 +0,0 @@
import asyncio
import httpx
import os
import io
from concurrent.futures import ThreadPoolExecutor
from typing import Optional, List, Union, Any, Coroutine, TypeVar
from urllib.parse import urljoin
from llama_cloud.types import ReportMetadata
from llama_cloud_services.report.report import ReportClient
T = TypeVar("T")
class LlamaReport:
"""Client for managing reports and general report operations."""
def __init__(
self,
api_key: Optional[str] = None,
project_id: Optional[str] = None,
organization_id: Optional[str] = None,
base_url: Optional[str] = None,
timeout: Optional[int] = None,
async_httpx_client: Optional[httpx.AsyncClient] = None,
):
self.api_key = api_key or os.getenv("LLAMA_CLOUD_API_KEY", None)
if not self.api_key:
raise ValueError("No API key provided.")
self.base_url = base_url or os.getenv(
"LLAMA_CLOUD_BASE_URL", "https://api.cloud.llamaindex.ai"
)
self.timeout = timeout or 60
# Initialize HTTP clients
self._aclient = async_httpx_client or httpx.AsyncClient(timeout=self.timeout)
# Set auth headers
self.headers = {
"Authorization": f"Bearer {self.api_key}",
}
self.organization_id = organization_id
self.project_id = project_id
self._client_params = {
"timeout": self._aclient.timeout,
"headers": self._aclient.headers,
"base_url": self._aclient.base_url,
"auth": self._aclient.auth,
"event_hooks": self._aclient.event_hooks,
"cookies": self._aclient.cookies,
"max_redirects": self._aclient.max_redirects,
"params": self._aclient.params,
"trust_env": self._aclient.trust_env,
}
self._thread_pool = ThreadPoolExecutor(
max_workers=min(10, (os.cpu_count() or 1) + 4)
)
@property
def aclient(self) -> httpx.AsyncClient:
if self._aclient is None:
self._aclient = httpx.AsyncClient(**self._client_params)
return self._aclient
def _run_sync(self, coro: Coroutine[Any, Any, T]) -> T:
"""Run coroutine in a separate thread to avoid event loop issues"""
# force a new client for this thread/event loop
original_client = self._aclient
self._aclient = None
def run_coro() -> T:
async def wrapped_coro() -> T:
return await coro
return asyncio.run(wrapped_coro())
result = self._thread_pool.submit(run_coro).result()
# restore the original client
self._aclient = original_client
return result
async def _get_default_project(self) -> str:
response = await self.aclient.get(
urljoin(str(self.base_url), "/api/v1/projects"), headers=self.headers
)
response.raise_for_status()
projects = response.json()
default_project = [p for p in projects if p.get("is_default")]
return default_project[0]["id"]
async def _build_url(
self, endpoint: str, extra_params: Optional[List[str]] = None
) -> str:
"""Helper method to build URLs with common query parameters."""
url = urljoin(str(self.base_url), endpoint)
if not self.project_id:
self.project_id = await self._get_default_project()
query_params = []
if self.organization_id:
query_params.append(f"organization_id={self.organization_id}")
if self.project_id:
query_params.append(f"project_id={self.project_id}")
if extra_params:
query_params.extend([p for p in extra_params if p is not None])
if query_params:
url += "?" + "&".join(query_params)
return url
async def acreate_report(
self,
name: str,
template_instructions: Optional[str] = None,
template_text: Optional[str] = None,
template_file: Optional[Union[str, tuple[str, bytes]]] = None,
input_files: Optional[List[Union[str, tuple[str, bytes]]]] = None,
existing_retriever_id: Optional[str] = None,
) -> ReportClient:
"""Create a new report asynchronously."""
url = await self._build_url("/api/v1/reports/")
open_files: List[io.BufferedReader] = []
data = {"name": name}
if template_instructions:
data["template_instructions"] = template_instructions
if template_text:
data["template_text"] = template_text
if existing_retriever_id:
data["existing_retriever_id"] = str(existing_retriever_id)
files: List[tuple[str, io.BufferedReader | bytes]] = []
if template_file:
if isinstance(template_file, str):
open_files.append(open(template_file, "rb"))
files.append(("template_file", open_files[-1]))
else:
files.append(("template_file", template_file[1]))
if input_files:
for f in input_files:
if isinstance(f, str):
open_files.append(open(f, "rb"))
files.append(("files", open_files[-1]))
else:
files.append(("files", f[1]))
response = await self.aclient.post(
url, headers=self.headers, data=data, files=files
)
try:
response.raise_for_status()
report_id = response.json()["id"]
return ReportClient(report_id, name, self)
except httpx.HTTPStatusError as e:
raise ValueError(
f"Failed to create report: {e.response.text}\nError Code: {e.response.status_code}"
)
finally:
for open_file in open_files:
open_file.close()
def create_report(
self,
name: str,
template_instructions: Optional[str] = None,
template_text: Optional[str] = None,
template_file: Optional[Union[str, tuple[str, bytes]]] = None,
input_files: Optional[List[Union[str, tuple[str, bytes]]]] = None,
existing_retriever_id: Optional[str] = None,
) -> ReportClient:
"""Create a new report."""
return self._run_sync(
self.acreate_report(
name=name,
template_instructions=template_instructions,
template_text=template_text,
template_file=template_file,
input_files=input_files,
existing_retriever_id=existing_retriever_id,
)
)
async def alist_reports(
self, state: Optional[str] = None, limit: int = 100, offset: int = 0
) -> List[ReportClient]:
"""List all reports asynchronously."""
params = []
if state:
params.append(f"state={state}")
if limit:
params.append(f"limit={limit}")
if offset:
params.append(f"offset={offset}")
url = await self._build_url(
"/api/v1/reports/list",
extra_params=params,
)
response = await self.aclient.get(url, headers=self.headers)
response.raise_for_status()
data = response.json()
return [
ReportClient(r["report_id"], r["name"], self)
for r in data["report_responses"]
]
def list_reports(
self, state: Optional[str] = None, limit: int = 100, offset: int = 0
) -> List[ReportClient]:
"""Synchronous wrapper for listing reports."""
return self._run_sync(self.alist_reports(state, limit, offset))
async def aget_report(self, report_id: str) -> ReportClient:
"""Get a Report instance for working with a specific report."""
url = await self._build_url(f"/api/v1/reports/{report_id}")
response = await self.aclient.get(url, headers=self.headers)
response.raise_for_status()
data = response.json()
return ReportClient(data["report_id"], data["name"], self)
def get_report(self, report_id: str) -> ReportClient:
"""Synchronous wrapper for getting a report."""
return self._run_sync(self.aget_report(report_id))
async def aget_report_metadata(self, report_id: str) -> ReportMetadata:
"""Get metadata for a specific report asynchronously.
Returns:
dict containing:
- id: Report ID
- name: Report name
- state: Current report state
- report_metadata: Additional metadata
- template_file: Name of template file if used
- template_instructions: Template instructions if provided
- input_files: List of input file names
"""
url = await self._build_url(f"/api/v1/reports/{report_id}/metadata")
response = await self.aclient.get(url, headers=self.headers)
response.raise_for_status()
return ReportMetadata(**response.json())
def get_report_metadata(self, report_id: str) -> ReportMetadata:
"""Synchronous wrapper for getting report metadata."""
return self._run_sync(self.aget_report_metadata(report_id))
async def adelete_report(self, report_id: str) -> None:
"""Delete a specific report asynchronously."""
url = await self._build_url(f"/api/v1/reports/{report_id}")
response = await self.aclient.delete(url, headers=self.headers)
response.raise_for_status()
def delete_report(self, report_id: str) -> None:
"""Synchronous wrapper for deleting a report."""
return self._run_sync(self.adelete_report(report_id))
-527
View File
@@ -1,527 +0,0 @@
import asyncio
import httpx
import time
from typing import Optional, List, Literal, Union, TYPE_CHECKING
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from llama_cloud.types import (
ReportEventItemEventData_Progress,
ReportMetadata,
EditSuggestion,
ReportResponse,
ReportPlan,
ReportBlock,
ReportPlanBlock,
Report,
)
if TYPE_CHECKING:
from llama_cloud_services.report.base import LlamaReport
class MessageRole(str, Enum):
USER = "user"
ASSISTANT = "assistant"
@dataclass
class Message:
role: MessageRole
content: str
timestamp: datetime
@dataclass
class EditAction:
block_idx: int
old_content: str
new_content: Optional[str]
action: Literal["approved", "rejected"]
timestamp: datetime
DEFAULT_POLL_INTERVAL = 5
DEFAULT_TIMEOUT = 600
class ReportClient:
"""Client for operations on a specific report."""
def __init__(self, report_id: str, name: str, parent_client: "LlamaReport"):
self.report_id = report_id
self.name = name
self._client = parent_client
self._headers = parent_client.headers
self._run_sync = parent_client._run_sync
self._build_url = parent_client._build_url
self.chat_history: List[Message] = []
self.edit_history: List[EditAction] = []
@property
def aclient(self) -> httpx.AsyncClient:
return self._client.aclient
def __str__(self) -> str:
return f"Report(id={self.report_id}, name={self.name})"
def __repr__(self) -> str:
return f"Report(id={self.report_id}, name={self.name})"
def _get_block_content(self, block: Union[ReportBlock, ReportPlanBlock]) -> str:
if isinstance(block, ReportBlock):
return block.template
elif isinstance(block, ReportPlanBlock):
return block.block.template
else:
raise ValueError(f"Invalid block type: {type(block)}")
def _get_block_idx(self, block: Union[ReportBlock, ReportPlanBlock]) -> int:
if isinstance(block, ReportBlock):
return block.idx
elif isinstance(block, ReportPlanBlock):
return block.block.idx
else:
raise ValueError(f"Invalid block type: {type(block)}")
async def aget(self, version: Optional[int] = None) -> ReportResponse:
"""Get this report's details asynchronously."""
extra_params = []
if version is not None:
extra_params.append(f"version={version}")
url = await self._build_url(f"/api/v1/reports/{self.report_id}", extra_params)
response = await self.aclient.get(url, headers=self._headers)
response.raise_for_status()
return ReportResponse(**response.json())
def get(self, version: Optional[int] = None) -> ReportResponse:
"""Synchronous wrapper for getting this report's details."""
return self._run_sync(self.aget(version))
async def aupdate_report(self, updated_report: Report) -> ReportResponse:
"""Update this report's content asynchronously."""
url = await self._build_url(f"/api/v1/reports/{self.report_id}")
response = await self.aclient.patch(
url, headers=self._headers, json={"content": updated_report.dict()}
)
response.raise_for_status()
return ReportResponse(**response.json())
def update_report(self, updated_report: Report) -> ReportResponse:
"""Synchronous wrapper for updating this report's content."""
return self._run_sync(self.aupdate_report(updated_report))
async def aupdate_plan(
self,
action: Literal["approve", "reject", "edit"],
updated_plan: Optional[ReportPlan] = None,
) -> ReportResponse:
"""Update this report's plan asynchronously."""
if action == "edit" and not updated_plan:
raise ValueError("updated_plan is required when action is 'edit'")
url = await self._build_url(
f"/api/v1/reports/{self.report_id}/plan", [f"action={action}"]
)
data = None
if updated_plan is not None:
plan_dict = updated_plan.dict()
plan_dict.pop("generated_at", None)
data = plan_dict
if updated_plan is None and action == "edit":
raise ValueError("updated_plan is required when action is 'edit'")
response = await self.aclient.patch(url, headers=self._headers, json=data)
response.raise_for_status()
return ReportResponse(**response.json())
def update_plan(
self,
action: Literal["approve", "reject", "edit"],
updated_plan: Optional[ReportPlan] = None,
) -> ReportResponse:
"""Synchronous wrapper for updating this report's plan."""
return self._run_sync(self.aupdate_plan(action, updated_plan))
async def asuggest_edits(
self,
user_query: str,
auto_history: bool = True,
chat_history: Optional[List[dict]] = None,
) -> List[EditSuggestion]:
"""Get AI suggestions for edits to this report asynchronously.
Args:
user_query: The user's request/question about what to edit
auto_history: Whether to automatically add the user's message to the chat history
chat_history:
A list of chat messages to include in the chat history.
The format being a list of dictionaries with "role" and "content" keys.
"""
# Add user message to history
self.chat_history.append(
Message(role=MessageRole.USER, content=user_query, timestamp=datetime.now())
)
# Format chat history with edit summaries
chat_history_dicts = []
for msg in self.chat_history[:-1]: # Exclude current message
content = msg.content
if msg.role == MessageRole.USER:
# Add edit summary for user messages
edit_summary = self._get_edit_summary_after_message(msg.timestamp)
if edit_summary:
content = f"{content}\n\nActions taken:\n{edit_summary}"
chat_history_dicts.append({"role": msg.role.value, "content": content})
# decide whether to include chat history or not
if chat_history:
chat_history_dicts = chat_history
elif auto_history:
chat_history_dicts = chat_history_dicts
else:
chat_history_dicts = []
# Make the API call
url = await self._build_url(f"/api/v1/reports/{self.report_id}/suggest_edits")
data = {"user_query": user_query, "chat_history": chat_history_dicts}
response = await self.aclient.post(url, headers=self._headers, json=data)
response.raise_for_status()
suggestions = response.json()
suggestions = [EditSuggestion(**suggestion) for suggestion in suggestions]
# Add assistant response to history
if suggestions:
for suggestion in suggestions:
self.chat_history.append(
Message(
role=MessageRole.ASSISTANT,
content=suggestion.justification,
timestamp=datetime.now(),
)
)
return suggestions
def suggest_edits(
self,
user_query: str,
auto_history: bool = True,
chat_history: Optional[List[dict]] = None,
) -> List[EditSuggestion]:
"""Synchronous wrapper for getting edit suggestions."""
return self._run_sync(
self.asuggest_edits(user_query, auto_history, chat_history)
)
async def await_completion(
self, timeout: int = DEFAULT_TIMEOUT, poll_interval: int = DEFAULT_POLL_INTERVAL
) -> Report:
"""Wait for this report to complete processing."""
start_time = time.time()
while True:
report_response = await self.aget()
status = report_response.status
if status == "completed":
return report_response.report
elif status == "error":
events = await self.aget_events()
raise ValueError(f"Report entered error state: {events[-1].msg}")
elif time.time() - start_time > timeout:
raise TimeoutError(f"Report did not complete within {timeout} seconds")
await asyncio.sleep(poll_interval)
def wait_for_completion(
self, timeout: int = DEFAULT_TIMEOUT, poll_interval: int = DEFAULT_POLL_INTERVAL
) -> Report:
"""Synchronous wrapper for awaiting report completion."""
return self._run_sync(self.await_completion(timeout, poll_interval))
async def await_for_plan(
self, timeout: int = DEFAULT_TIMEOUT, poll_interval: int = DEFAULT_POLL_INTERVAL
) -> ReportPlan:
"""Wait for this report's plan to be ready for review."""
start_time = time.time()
while True:
report_metadata = await self.aget_metadata()
state = report_metadata.state
if state == "waiting_approval":
report_response = await self.aget()
return report_response.plan
elif state == "error":
events = await self.aget_events()
raise ValueError(f"Report entered error state: {events[-1].msg}")
elif time.time() - start_time > timeout:
raise TimeoutError(f"Plan was not ready within {timeout} seconds")
await asyncio.sleep(poll_interval)
def wait_for_plan(
self, timeout: int = DEFAULT_TIMEOUT, poll_interval: int = DEFAULT_POLL_INTERVAL
) -> ReportPlan:
"""Synchronous wrapper for awaiting plan readiness."""
return self._run_sync(self.await_for_plan(timeout, poll_interval))
async def aget_metadata(self) -> ReportMetadata:
"""Get this report's metadata asynchronously."""
return await self._client.aget_report_metadata(self.report_id)
def get_metadata(self) -> ReportMetadata:
"""Synchronous wrapper for getting this report's metadata."""
return self._run_sync(self.aget_metadata())
async def adelete(self) -> None:
"""Delete this report asynchronously."""
return await self._client.adelete_report(self.report_id)
def delete(self) -> None:
"""Synchronous wrapper for deleting this report."""
return self._run_sync(self.adelete())
async def aaccept_edit(self, suggestion: EditSuggestion) -> None:
"""Accept a suggested edit.
Args:
suggestion: The EditSuggestion to accept, typically from suggest_edits()
"""
if len(suggestion.blocks) == 0:
return
# Determine if we're editing a plan or report based on first block type
is_plan_edit = isinstance(suggestion.blocks[0], ReportPlanBlock)
# Get current content
report_response = await self.aget()
current_blocks = (
report_response.plan.blocks
if is_plan_edit
else report_response.report.blocks
)
# Track the edit
new_blocks = []
for edit_block in suggestion.blocks:
# Find matching block in current content
old_block = next(
(
b
for b in current_blocks
if self._get_block_idx(b) == self._get_block_idx(edit_block)
),
None,
)
old_content = (
self._get_block_content(old_block) if old_block else "[No old content]"
)
new_content = self._get_block_content(edit_block)
if is_plan_edit:
new_queries_str = "\n".join(
[
f"Field: {q.field}, Prompt: {q.prompt}, Context: {q.context}"
for q in edit_block.queries
]
)
new_dependency_str = (
f"Depends on: {edit_block.dependency}"
if edit_block.dependency
else ""
)
new_content += f"\n\n{new_queries_str}\n{new_dependency_str}"
if old_block:
old_queries_str = "\n".join(
[
f"Field: {q.field}, Prompt: {q.prompt}, Context: {q.context}"
for q in old_block.queries
]
)
old_dependency_str = (
f"Depends on: {old_block.dependency}"
if old_block.dependency
else ""
)
old_content += f"\n\n{old_queries_str}\n{old_dependency_str}"
self.edit_history.append(
EditAction(
block_idx=self._get_block_idx(edit_block),
old_content=old_content,
new_content=new_content,
action="approved",
timestamp=datetime.now(),
)
)
# Create updated block
if is_plan_edit:
new_blocks.append(
ReportPlanBlock(
block=ReportBlock(
idx=edit_block.block.idx,
template=self._get_block_content(edit_block),
sources=edit_block.block.sources,
),
queries=edit_block.queries,
dependency=edit_block.dependency,
)
)
else:
new_blocks.append(
ReportBlock(
idx=edit_block.idx,
template=self._get_block_content(edit_block),
sources=edit_block.sources,
)
)
if new_blocks:
if is_plan_edit:
# Update plan in place
plan = report_response.plan
# Replace edited blocks and add new ones
for new_block in new_blocks:
block_idx = self._get_block_idx(new_block)
existing_block_idx = next(
(
i
for i, b in enumerate(plan.blocks)
if b.block.idx == block_idx
),
None,
)
if existing_block_idx is not None:
# Replace existing block
plan.blocks[existing_block_idx] = new_block
else:
# Add new block to end
plan.blocks.append(new_block)
await self.aupdate_plan("edit", plan)
else:
# Update report in place
report = report_response.report
# Replace edited blocks and add new ones
for new_block in new_blocks:
block_idx = self._get_block_idx(new_block)
existing_block_idx = next(
(i for i, b in enumerate(report.blocks) if b.idx == block_idx),
None,
)
if existing_block_idx is not None:
# Replace existing block
report.blocks[existing_block_idx] = new_block
else:
# Add new block to end
report.blocks.append(new_block)
await self.aupdate_report(report)
def accept_edit(self, suggestion: EditSuggestion) -> None:
"""Synchronous wrapper for accepting an edit."""
return self._run_sync(self.aaccept_edit(suggestion))
async def areject_edit(self, suggestion: EditSuggestion) -> None:
"""Reject a suggested edit.
Args:
suggestion: The EditSuggestion to reject, typically from suggest_edits()
"""
# Track the rejections
for edit_block in suggestion.blocks:
self.edit_history.append(
EditAction(
block_idx=self._get_block_idx(edit_block),
old_content=self._get_block_content(edit_block),
new_content=None,
action="rejected",
timestamp=datetime.now(),
)
)
def reject_edit(self, suggestion: EditSuggestion) -> None:
"""Synchronous wrapper for rejecting an edit."""
return self._run_sync(self.areject_edit(suggestion))
def _get_edit_summary_after_message(
self, message_timestamp: datetime
) -> Optional[str]:
"""Get a summary of edits that occurred after a specific message."""
relevant_edits = [
edit for edit in self.edit_history if edit.timestamp > message_timestamp
]
if not relevant_edits:
return None
approved = [edit for edit in relevant_edits if edit.action == "approved"]
rejected = [edit for edit in relevant_edits if edit.action == "rejected"]
summary = []
if approved:
summary.append("Approved edits:")
for edit in approved:
summary.append(
f'Block {edit.block_idx}: "{edit.old_content}" -> "{edit.new_content}"'
)
if rejected:
if approved: # Add spacing if we had approved edits
summary.append("")
summary.append("Rejected edits:")
for edit in rejected:
summary.append(f'Block {edit.block_idx}: "{edit.old_content}"')
return "\n".join(summary)
async def aget_events(
self, last_sequence: Optional[int] = None
) -> List[ReportEventItemEventData_Progress]:
"""Get all events for this report asynchronously.
Args:
last_sequence: If provided, only get events after this sequence number
Returns:
List of ReportEvent objects
"""
extra_params = []
if last_sequence is not None:
extra_params.append(f"last_sequence={last_sequence}")
url = await self._build_url(
f"/api/v1/reports/{self.report_id}/events", extra_params
)
response = await self.aclient.get(url, headers=self._headers)
response.raise_for_status()
progress_events = []
for event in response.json():
if event["event_type"] == "progress":
progress_events.append(
ReportEventItemEventData_Progress(**event["event_data"])
)
return progress_events
def get_events(
self, last_sequence: Optional[int] = None
) -> List[ReportEventItemEventData_Progress]:
"""Synchronous wrapper for getting report events."""
return self._run_sync(self.aget_events(last_sequence))
+102 -2
View File
@@ -3,11 +3,14 @@ import importlib.metadata
from contextlib import contextmanager
from typing import Generator
import difflib
from llama_cloud.types import StatusEnum
from llama_cloud.types import StatusEnum, File
import httpx
import packaging.version
from pydantic import BaseModel
from typing import Any, Dict, List, Tuple, Type
from typing import Any, Dict, List, Tuple, Type, Union, Optional
from io import BufferedIOBase, TextIOWrapper
from pathlib import Path
import secrets
# Asyncio error messages
nest_asyncio_err = "cannot be called from a running event loop"
@@ -104,3 +107,100 @@ def augment_async_errors() -> Generator[None, None, None]:
if nest_asyncio_err in str(e):
raise RuntimeError(nest_asyncio_msg)
raise
class SourceText:
"""
A wrapper class for providing text or file input with optional filename specification.
This class allows you to provide input in multiple ways:
- Direct text content via text_content parameter
- File paths as strings or Path objects
- Raw bytes
- File-like objects (BufferedIOBase, TextIOWrapper)
- Already-uploaded file ID via file_id parameter
Args:
file: The file input (bytes, file-like object, str path, or Path).
Mutually exclusive with text_content and file_id.
text_content: Raw text content to process. Mutually exclusive with file and file_id.
file_id: ID of an already-uploaded file. Mutually exclusive with file and text_content.
filename: Optional filename. Required for bytes/file-like objects without names.
If not provided, will be auto-generated for text_content or inferred from paths.
Examples:
# Direct text input
source = SourceText(text_content="Hello world")
# File path
source = SourceText(file="document.pdf")
# Bytes with filename
source = SourceText(file=b"...", filename="document.pdf")
# File-like object (will read from current position)
with open("document.pdf", "rb") as f:
source = SourceText(file=f)
# Already-uploaded file
source = SourceText(file_id="file_abc123")
"""
def __init__(
self,
*,
file: Union[bytes, BufferedIOBase, TextIOWrapper, str, Path, None] = None,
text_content: Optional[str] = None,
file_id: Optional[str] = None,
filename: Optional[str] = None,
):
self.file = file
self.filename = filename
self.text_content = text_content
self.file_id = file_id
self._validate()
def _validate(self) -> None:
"""Ensure filename is provided when needed."""
# Check that exactly one of file, text_content, or file_id is provided
provided = sum(
[
self.file is not None,
self.text_content is not None,
self.file_id is not None,
]
)
if provided == 0:
raise ValueError("One of file, text_content, or file_id must be provided.")
elif provided > 1:
raise ValueError(
"Only one of file, text_content, or file_id can be provided."
)
# If file_id is provided, we don't need filename validation
if self.file_id is not None:
return
if self.text_content is not None:
if not self.filename:
random_hex = secrets.token_hex(4)
self.filename = f"text_input_{random_hex}.txt"
return
if isinstance(self.file, (bytes, BufferedIOBase, TextIOWrapper)):
if not self.filename and hasattr(self.file, "name"):
self.filename = os.path.basename(str(self.file.name))
elif self.filename is None and not hasattr(self.file, "name"):
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)}")
# Type alias for file input that can be used across services
FileInput = Union[str, Path, BufferedIOBase, SourceText, File]
+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.62"
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.62"]
dependencies = ["llama-cloud-services>=0.6.76"]
[project.scripts]
llama-parse = "llama_parse.cli.main:parse"
+10
View File
@@ -0,0 +1,10 @@
{
"name": "llama-cloud-services-py",
"version": "0.6.76",
"private": false,
"license": "MIT",
"scripts": {},
"devDependencies": {
"changesets": "^1.0.2"
}
}
+3 -3
View File
@@ -19,7 +19,7 @@ dev = [
[project]
name = "llama-cloud-services"
version = "0.6.62"
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"
@@ -27,14 +27,14 @@ readme = "README.md"
license = "MIT"
dependencies = [
"llama-index-core>=0.12.0",
"llama-cloud==0.1.37",
"llama-cloud==0.1.43",
"pydantic>=2.8,!=2.10",
"click>=8.1.7,<9",
"python-dotenv>=1.0.1,<2",
"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]
@@ -68,7 +68,7 @@ async def test_agent_data_crud_operations():
client=client,
type=ExampleData,
collection=f"test-collection-{test_id[:8]}",
agent_url_id=LLAMA_DEPLOY_DEPLOYMENT_NAME,
deployment_name=LLAMA_DEPLOY_DEPLOYMENT_NAME,
)
# Create test data
+42 -4
View File
@@ -2,6 +2,7 @@ import os
import pytest
from llama_cloud.client import AsyncLlamaCloud
from llama_cloud.types import Project, ClassifierRule, ClassifyJobResults
from llama_cloud_services.beta.classifier.types import ClassifyJobResultsWithFiles
from llama_cloud_services.beta.classifier.client import ClassifyClient
from llama_cloud_services.files.client import FileClient
from llama_cloud.errors.unprocessable_entity_error import UnprocessableEntityError
@@ -130,6 +131,44 @@ async def test_classify_file_ids(
assert item.result.type == expected_type
@pytest.mark.asyncio
async def test_classify_file_ids_from_api_key(
e2e_test_settings: EndToEndTestSettings,
file_client: FileClient,
simple_pdf_file_path: str,
research_paper_path: str,
classification_rules: list[ClassifierRule],
):
"""Test classifying files by their IDs"""
# Upload test files first to get their IDs
pdf_file = await file_client.upload_file(simple_pdf_file_path)
research_paper_file = await file_client.upload_file(research_paper_path)
classify_client = ClassifyClient.from_api_key(
api_key=e2e_test_settings.LLAMA_CLOUD_API_KEY.get_secret_value(),
base_url=e2e_test_settings.LLAMA_CLOUD_BASE_URL,
project_id=pdf_file.project_id,
organization_id=e2e_test_settings.LLAMA_CLOUD_ORGANIZATION_ID,
)
# Classify the uploaded files
results = await classify_client.aclassify_file_ids(
rules=classification_rules, file_ids=[pdf_file.id, research_paper_file.id]
)
assert isinstance(results, ClassifyJobResults)
assert len(results.items) == 2
file_id_to_expected_type = {
pdf_file.id: "number",
research_paper_file.id: "research_paper",
}
# Verify each file got classified
for item in results.items:
expected_type = file_id_to_expected_type[item.file_id]
assert item.result.type == expected_type
@parameterize_sync_and_async
@pytest.mark.asyncio
async def test_classify_file_path(
@@ -149,7 +188,7 @@ async def test_classify_file_path(
rules=classification_rules, file_input_path=simple_pdf_file_path
)
assert isinstance(results, ClassifyJobResults)
assert isinstance(results, ClassifyJobResultsWithFiles)
assert len(results.items) == 1
# Verify the file got classified
@@ -180,7 +219,7 @@ async def test_classify_file_paths(
file_input_paths=[simple_pdf_file_path, research_paper_path],
)
assert isinstance(results, ClassifyJobResults)
assert isinstance(results, ClassifyJobResultsWithFiles)
assert len(results.items) == 2
file_name_to_expected_type = {
@@ -189,8 +228,7 @@ async def test_classify_file_paths(
}
# Verify each file got classified
for item in results.items:
file = await file_client.get_file(item.file_id)
expected_type = file_name_to_expected_type[file.name]
expected_type = file_name_to_expected_type[item.file.name]
assert item.result.type == expected_type
+1 -28
View File
@@ -1,16 +1,13 @@
import os
import pytest
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent
from time import perf_counter
from llama_cloud_services.extract import LlamaExtract
from collections import namedtuple
import json
import uuid
from llama_cloud.types import (
ExtractConfig,
ExtractMode,
LlamaParseParameters,
LlamaExtractSettings,
)
from tests.extract.util import load_test_dotenv
@@ -122,27 +119,3 @@ def extraction_agent(test_case: BenchmarkTestCase, extractor: LlamaExtract):
# Create new agent
agent = extractor.create_agent(agent_name, schema, config=test_case.config)
yield agent
@pytest.mark.skipif(
"CI" in os.environ or not LLAMA_CLOUD_API_KEY,
reason="LLAMA_CLOUD_API_KEY not set or CI environment not suitable for benchmarking",
)
@pytest.mark.parametrize("test_case", get_test_cases(), ids=lambda x: x.name)
@pytest.mark.asyncio(loop_scope="session")
async def test_extraction(
test_case: BenchmarkTestCase, extraction_agent: ExtractionAgent
) -> None:
start = perf_counter()
result = await extraction_agent._run_extraction_test(
test_case.input_file,
extract_settings=LlamaExtractSettings(
llama_parse_params=LlamaParseParameters(
invalidate_cache=True,
do_not_cache=True,
)
),
)
end = perf_counter()
print(f"Time taken: {end - start} seconds")
print(result)
+1 -1
View File
@@ -7,7 +7,7 @@ from pathlib import Path
def load_test_dotenv():
load_dotenv(Path(__file__).parent.parent.parent / ".env.dev", override=True)
load_dotenv(Path(__file__).parent.parent.parent.parent / ".env.dev", override=True)
def json_subset_match_score(expected: Any, actual: Any) -> float:
+3
View File
@@ -304,6 +304,9 @@ async def test_page_screenshot_retrieval(index_name: str, local_file: str):
not base_url or not api_key, reason="No platform base url or api key set"
)
@pytest.mark.asyncio
@pytest.mark.skip(
reason="Consistently failing with FAILED tests/index/test_index.py::test_page_figure_retrieval - assert 0 > 0 + where 0 = len([])"
)
async def test_page_figure_retrieval(index_name: str, local_figures_file: str):
index = await LlamaCloudIndex.acreate_index(
name=index_name,
+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"
View File
-129
View File
@@ -1,129 +0,0 @@
import os
import pytest
import uuid
from typing import AsyncGenerator
from pytest_asyncio import fixture as async_fixture
from llama_cloud_services.report import LlamaReport, ReportClient
# Skip tests if no API key is set
pytestmark = pytest.mark.skipif(
not os.getenv("LLAMA_CLOUD_API_KEY") or os.getenv("CI") == "true",
reason="No API key provided",
)
@async_fixture(scope="function")
async def client() -> AsyncGenerator[LlamaReport, None]:
"""Create a LlamaReport client."""
client = LlamaReport()
reports_before = await client.alist_reports()
reports_before_ids = [r.report_id for r in reports_before]
try:
yield client
finally:
# clean up reports
try:
reports_after = await client.alist_reports()
reports_after_ids = [r.report_id for r in reports_after]
for report_id in reports_before_ids:
if report_id not in reports_after_ids:
await client.adelete_report(report_id)
except Exception:
pass
finally:
await client.aclient.aclose()
@pytest.fixture(scope="function")
def unique_name() -> str:
"""Generate a unique report name."""
return f"test-report-{uuid.uuid4()}"
@async_fixture(scope="function")
async def report(
client: LlamaReport, unique_name: str
) -> AsyncGenerator[ReportClient, None]:
"""Create a report."""
report = await client.acreate_report(
name=unique_name,
template_text=(
"# [Some title]\n\n"
" ## TLDR\n"
"A quick summary of the paper.\n\n"
"## Details\n"
"More details about the paper, possible more than one section here.\n"
),
input_files=["tests/test_files/paper.md"],
)
try:
yield report
finally:
await report.adelete()
@pytest.mark.asyncio
@pytest.mark.xfail(
condition=lambda: os.getenv("CI"),
reason="Backend db issues; needs to be fixed.",
)
async def test_create_and_delete_report(
client: LlamaReport, report: ReportClient
) -> None:
"""Test basic report creation and deletion."""
# Verify the report exists
metadata = await report.aget_metadata()
assert metadata.name == report.name
# Test listing reports
reports = await client.alist_reports()
assert any(r.report_id == report.report_id for r in reports)
# Test getting report by ID
fetched_report = await client.aget_report(report.report_id)
assert fetched_report.report_id == report.report_id
assert fetched_report.name == report.name
@pytest.mark.asyncio
@pytest.mark.xfail(
condition=lambda: os.getenv("CI"),
reason="Report plan sometimes times out",
raises=TimeoutError,
)
async def test_report_plan_workflow(report: ReportClient) -> None:
"""Test the report planning workflow."""
# Wait for the plan
plan = await report.await_for_plan()
assert plan is not None
# Approve the plan
response = await report.aupdate_plan(action="approve")
assert response is not None
# Wait for completion
completed_report = await report.await_completion()
assert len(completed_report.blocks) > 0
# Get edit suggestions
suggestions = await report.asuggest_edits(
"TLDR section header more formal.", auto_history=True
)
assert len(suggestions) > 0
# Test accepting an edit
await report.aaccept_edit(suggestions[0])
# Get more suggestions and test rejecting
more_suggestions = await report.asuggest_edits(
"Add a section about machine learning.", auto_history=True
)
assert len(more_suggestions) > 0
await report.areject_edit(more_suggestions[0])
# Verify chat history is maintained
assert len(report.chat_history) >= 4 # 2 user messages + 2 assistant responses
# get events
events = await report.aget_events()
assert len(events) > 0
@@ -0,0 +1,158 @@
import pytest
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
from datetime import datetime
from llama_cloud.types.agent_data import AgentData
from llama_cloud.types.aggregate_group import AggregateGroup
from llama_cloud_services.beta.agent_data.client import AsyncAgentDataClient
class Person(BaseModel):
name: str
age: int
class FakeBeta:
def __init__(self) -> None:
self._get_item_response: Optional[AgentData] = None
self._search_items: List[AgentData] = []
self._aggregate_items: List[AggregateGroup] = []
self._total_size: Optional[int] = None
self._next_page_token: Optional[str] = None
# Single get
async def get_agent_data(self, item_id: str) -> AgentData:
assert self._get_item_response is not None, "_get_item_response not set"
return self._get_item_response
# Search
async def search_agent_data_api_v_1_beta_agent_data_search_post(
self,
*,
deployment_name: str,
collection: str,
filter: Optional[Dict[str, Any]] = None,
order_by: Optional[str] = None,
offset: Optional[int] = None,
page_size: Optional[int] = None,
include_total: bool = False,
) -> Any:
class Resp:
def __init__(
self,
items: List[AgentData],
total_size: Optional[int],
next_page_token: Optional[str],
) -> None:
self.items = items
self.total_size = total_size
self.next_page_token = next_page_token
return Resp(self._search_items, self._total_size, self._next_page_token)
# Aggregate
async def aggregate_agent_data_api_v_1_beta_agent_data_aggregate_post(
self,
*,
deployment_name: str,
collection: str,
page_size: Optional[int] = None,
filter: Optional[Dict[str, Any]] = None,
order_by: Optional[str] = None,
group_by: Optional[List[str]] = None,
count: Optional[bool] = None,
first: Optional[bool] = None,
offset: Optional[int] = None,
) -> Any:
class Resp:
def __init__(
self,
items: List[AggregateGroup],
total_size: Optional[int],
next_page_token: Optional[str],
) -> None:
self.items = items
self.total_size = total_size
self.next_page_token = next_page_token
return Resp(self._aggregate_items, self._total_size, self._next_page_token)
class FakeClient:
def __init__(self) -> None:
self.beta = FakeBeta()
def make_agent_data(data: Dict[str, Any]) -> AgentData:
return AgentData(
id="id-1",
deployment_name="dep",
collection="col",
data=data,
created_at=datetime.now(),
updated_at=datetime.now(),
)
def make_group(
group_key: Dict[str, Any],
first_item: Optional[Dict[str, Any]],
count: Optional[int] = None,
) -> AggregateGroup:
return AggregateGroup(group_key=group_key, count=count, first_item=first_item)
@pytest.mark.asyncio
async def test_untyped_get_item_valid_to_dict() -> None:
client = FakeClient()
client.beta._get_item_response = make_agent_data({"name": "Alice", "age": 30})
adc = AsyncAgentDataClient(type=Person, client=client, deployment_name="dep")
item = await adc.untyped_get_item("id-1")
assert item.data == {"name": "Alice", "age": 30}
@pytest.mark.asyncio
async def test_untyped_get_item_invalid_retains_dict() -> None:
client = FakeClient()
# age wrong type; will fail validation and should be returned as dict
client.beta._get_item_response = make_agent_data({"name": "Bob", "age": "x"})
adc = AsyncAgentDataClient(type=Person, client=client, deployment_name="dep")
item = await adc.untyped_get_item("id-1")
assert item.data == {"name": "Bob", "age": "x"}
@pytest.mark.asyncio
async def test_untyped_search_mixed_items() -> None:
client = FakeClient()
client.beta._search_items = [
make_agent_data({"name": "Carol", "age": 22}),
make_agent_data({"name": "Dave", "age": "bad"}),
]
client.beta._total_size = 2
adc = AsyncAgentDataClient(type=Person, client=client, deployment_name="dep")
results = await adc.untyped_search(include_total=True)
assert len(results.items) == 2
assert results.items[0].data == {"name": "Carol", "age": 22}
assert results.items[1].data == {"name": "Dave", "age": "bad"}
assert results.total_size == 2
@pytest.mark.asyncio
async def test_untyped_aggregate_first_item_dict() -> None:
client = FakeClient()
client.beta._aggregate_items = [
make_group({"k": 1}, {"name": "Eve", "age": 40}),
make_group({"k": 2}, {"name": "Frank", "age": "bad"}),
]
client.beta._total_size = 2
adc = AsyncAgentDataClient(type=Person, client=client, deployment_name="dep")
results = await adc.untyped_aggregate(group_by=["k"], first=True)
assert len(results.items) == 2
assert results.items[0].first_item == {"name": "Eve", "age": 40}
assert results.items[1].first_item == {"name": "Frank", "age": "bad"}
@@ -38,7 +38,7 @@ def test_typed_agent_data_from_raw():
"""Test TypedAgentData.from_raw class method."""
raw_data = AgentData(
id="456",
agent_slug="extraction-agent",
deployment_name="extraction-agent",
collection="employees",
data={"name": "Jane Smith", "age": 25, "email": "jane@company.com"},
created_at=datetime.now(),
@@ -48,7 +48,7 @@ def test_typed_agent_data_from_raw():
typed_data = TypedAgentData.from_raw(raw_data, Person)
assert typed_data.id == "456"
assert typed_data.agent_url_id == "extraction-agent"
assert typed_data.deployment_name == "extraction-agent"
assert typed_data.collection == "employees"
assert typed_data.data.name == "Jane Smith"
assert typed_data.data.age == 25
@@ -56,10 +56,10 @@ def test_typed_agent_data_from_raw():
def test_typed_agent_data_from_raw_validation_error():
"""Test TypedAgentData.from_raw with invalid data."""
"""Test TypedAgentData.from_raw with invalid data now raises InvalidTypedAgentData."""
raw_data = AgentData(
id="789",
agent_slug="test-agent",
deployment_name="test-agent",
collection="people",
data={"name": "Invalid Person", "age": "not_a_number"}, # Invalid age
created_at=datetime.now(),
@@ -613,3 +613,51 @@ def test_parses_field_metadata_with_error_field():
}
assert parsed.metadata.get("field_errors") == "This is an error"
assert parsed.metadata.get("job_id") == "job-123"
REASONING_IN_SCHEMA = {
"majority_opinion": {
"type": {
"citation": [
{
"page": 4,
"matching_text": "BARRETT, J., delivered the opinion for a unanimous Court.",
},
{"page": 11, "matching_text": "Opinion of the Court"},
],
"parsing_confidence": 1.0,
"extraction_confidence": 0.9999998919950147,
"confidence": 0.9999998919950147,
},
"reasoning": {
"citation": [
{
"page": 15,
"matching_text": "We hold that §5110(b)(1) is not subject to equitable tolling and affirm the judg...",
}
],
"parsing_confidence": 1.0,
"extraction_confidence": 0.414292785946868,
"confidence": 0.414292785946868,
},
},
"reasoning": {
"citation": [
{
"page": 15,
"matching_text": "We hold that §5110(b)(1) is not subject to equitable tolling and affirm the judg...",
}
],
"parsing_confidence": 1.0,
"extraction_confidence": 0.414292785946868,
"confidence": 0.414292785946868,
},
}
def test_field_conflict_in_schema():
extracted = parse_extracted_field_metadata(REASONING_IN_SCHEMA)
assert isinstance(extracted["reasoning"], ExtractedFieldMetadata)
assert isinstance(
extracted["majority_opinion"]["reasoning"], ExtractedFieldMetadata
)
+2 -4
View File
@@ -118,10 +118,8 @@ async def test_extraction_agent_aextract_accepts_llama_file(
dummy_llama_extract_iface = SimpleNamespace()
async def fake_run_job(**kwargs):
# Ensure we are receiving a request with the right file_id
request = kwargs.get("request")
assert hasattr(request, "file_id")
assert request.file_id == llama_file.id
file_id = kwargs.get("file_id")
assert file_id == llama_file.id
return SimpleNamespace(id="job_42")
dummy_llama_extract_iface.run_job = fake_run_job
+143 -4
View File
@@ -1,16 +1,155 @@
import pytest
from unittest.mock import MagicMock, patch
import llama_cloud_services.index.base as base
from llama_cloud import (
PipelineEmbeddingConfig_ManagedOpenaiEmbedding,
Project,
Pipeline,
CloudDocument,
)
from llama_index.core.constants import DEFAULT_PROJECT_NAME
from llama_index.core.indices.managed.base import BaseManagedIndex
from llama_cloud_services.index import (
LlamaCloudIndex,
from llama_index.core.schema import Document
from llama_cloud_services.index import LlamaCloudIndex
# Simple test data as values, not fixtures
TEST_PROJECT = Project(id="proj-123", name="test-project", organization_id="org-123")
EMBEDDING_CONFIG = PipelineEmbeddingConfig_ManagedOpenaiEmbedding(
type="MANAGED_OPENAI_EMBEDDING"
)
TEST_PIPELINE = Pipeline(
id="pipe-456",
name="test-pipeline",
project_id="proj-123",
embedding_config=PipelineEmbeddingConfig_ManagedOpenaiEmbedding(
type="MANAGED_OPENAI_EMBEDDING"
),
)
def test_class():
@pytest.fixture
def mock_client() -> MagicMock:
"""Mock client with sensible defaults."""
client = MagicMock()
client.projects.upsert_project.return_value = Project(
id="default-proj", name=DEFAULT_PROJECT_NAME, organization_id="default-org"
)
client.pipelines.upsert_pipeline.return_value = Pipeline(
id="default-pipe",
name="default",
project_id="default-proj",
embedding_config=EMBEDDING_CONFIG,
)
client.pipelines.upsert_batch_pipeline_documents.return_value = [
CloudDocument(id="doc-1", text="test", metadata={})
]
return client
@pytest.fixture(autouse=True)
def base_patches(mock_client: MagicMock) -> None:
"""Auto-applied patches for all tests."""
with (
patch.object(base, "get_client", return_value=mock_client),
patch.object(
base,
"resolve_project_and_pipeline",
return_value=(TEST_PROJECT, TEST_PIPELINE),
),
patch.object(base.LlamaCloudIndex, "wait_for_completion"),
):
yield
def test_class() -> None:
names_of_base_classes = [b.__name__ for b in LlamaCloudIndex.__mro__]
assert BaseManagedIndex.__name__ in names_of_base_classes
def test_conflicting_index_identifiers():
def test_conflicting_index_identifiers() -> None:
with pytest.raises(ValueError):
LlamaCloudIndex(name="test", pipeline_id="test", index_id="test")
def test_from_documents_uses_provided_project_id(mock_client: MagicMock) -> None:
provided_project_id = "proj-123"
organization_id = "org-abc"
index_name = "my_new_index"
# Override resolve to return project with provided ID
test_project = Project(
id=provided_project_id, name="my_project", organization_id=organization_id
)
test_pipeline = Pipeline(
id="pipe-xyz",
name=index_name,
project_id=provided_project_id,
embedding_config=EMBEDDING_CONFIG,
)
with patch.object(
base, "resolve_project_and_pipeline", return_value=(test_project, test_pipeline)
):
docs = [Document(text="hello")]
index = LlamaCloudIndex.from_documents(
documents=docs,
name=index_name,
project_id=provided_project_id,
)
# Assert - project upsert not called; pipeline uses provided project_id
mock_client.projects.upsert_project.assert_not_called()
assert mock_client.pipelines.upsert_pipeline.call_count == 1
assert (
mock_client.pipelines.upsert_pipeline.call_args.kwargs["project_id"]
== provided_project_id
)
assert index.project.id == provided_project_id
def test_from_documents_upserts_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(
id="proj-999", name=DEFAULT_PROJECT_NAME, organization_id=organization_id
)
mock_client.projects.upsert_project.return_value = upserted_project
test_pipeline = Pipeline(
id="pipe-xyz",
name=index_name,
project_id=upserted_project.id,
embedding_config=EMBEDDING_CONFIG,
)
with patch.object(
base,
"resolve_project_and_pipeline",
return_value=(upserted_project, test_pipeline),
):
docs = [Document(text="world")]
index = LlamaCloudIndex.from_documents(
documents=docs,
name=index_name,
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 kwargs["organization_id"] == organization_id
assert kwargs["request"].name == DEFAULT_PROJECT_NAME
# Pipeline created under the upserted project id
assert (
mock_client.pipelines.upsert_pipeline.call_args.kwargs["project_id"]
== upserted_project.id
)
assert index.project.id == upserted_project.id
Generated
+6 -6
View File
@@ -1582,21 +1582,21 @@ wheels = [
[[package]]
name = "llama-cloud"
version = "0.1.37"
version = "0.1.43"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "httpx" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9c/dd/c4f2516523778a0d4b284fa4b66a0136afdd59694f105102838cf793e675/llama_cloud-0.1.37.tar.gz", hash = "sha256:b6d62e7386d1aa85905b7e3f7c19a40694be54c1596668bceaa456cd84ede666", size = 108707, upload-time = "2025-08-04T22:00:37.18Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9b/33/33a8bd3a617c071caf450ca2627969f8b28272d0692f122997c10a32247e/llama_cloud-0.1.43.tar.gz", hash = "sha256:00429f05aea515449d90cde91ef3ed3687fcd93e46f6246d08cbea02f9b397a9", size = 112992, upload-time = "2025-10-02T21:55:38.355Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/ca/a7f874b041d2566f000ecc88bf1b76819a8bdbbaea85036ca6905ae3f0f7/llama_cloud-0.1.37-py3-none-any.whl", hash = "sha256:3109ec74575f53311ec4957ca8aea2d6e30556a43d24c6316ceab91c4fed6ab7", size = 314244, upload-time = "2025-08-04T22:00:35.696Z" },
{ url = "https://files.pythonhosted.org/packages/2b/54/559a67542396d5660a71115b29e0160e9dd784e570e1f4ef55ad22bf5b39/llama_cloud-0.1.43-py3-none-any.whl", hash = "sha256:540605d4dd13c6536a3b75cd4d04b211f29b16d17faee9381e3793a651f1dec1", size = 311460, upload-time = "2025-10-02T21:55:37.282Z" },
]
[[package]]
name = "llama-cloud-services"
version = "0.6.61"
version = "0.6.76"
source = { editable = "." }
dependencies = [
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
@@ -1631,9 +1631,9 @@ 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.37" },
{ 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" },
-101
View File
@@ -1,101 +0,0 @@
# LlamaReport (beta/invite-only)
LlamaReport is a prebuilt agentic report builder that can be used to build reports from a variety of data sources.
The python SDK for interacting with the LlamaReport API. The SDK provides two main classes:
- `LlamaReport`: For managing reports (create, list, delete)
- `ReportClient`: For working with a specific report (editing, approving, etc.)
## Quickstart
```bash
pip install llama-cloud-services
```
```python
from llama_cloud_services import LlamaReport
# Initialize the client
client = LlamaReport(
api_key="your-api-key",
# Optional: Specify project_id, organization_id, async_httpx_client
)
# Create a new report
report = client.create_report(
"My Report",
# must have one of template_text or template_instructions
template_text="Your template text",
template_instructions="Instructions for the template",
# must have one of input_files or retriever_id
input_files=["data1.pdf", "data2.pdf"],
retriever_id="retriever-id",
)
```
## Working with Reports
The typical workflow for a report involves:
1. Creating the report
2. Waiting for and approving the plan
3. Waiting for report generation
4. Making edits to the report
Here's a complete example:
```python
# Create a report
report = client.create_report(
"Quarterly Analysis", input_files=["q1_data.pdf", "q2_data.pdf"]
)
# Wait for the plan to be ready
plan = report.wait_for_plan()
# Option 1: Directly approve the plan
report.update_plan(action="approve")
# Option 2: Suggest and review edits to the plan
suggestions = report.suggest_edits(
"Can you add a section about market trends?"
)
for suggestion in suggestions:
print(suggestion)
# Accept or reject the suggestion
if input("Accept? (y/n): ").lower() == "y":
report.accept_edit(suggestion)
else:
report.reject_edit(suggestion)
# Wait for the report to complete
report = report.wait_for_completion()
# Make edits to the final report
suggestions = report.suggest_edits("Make the executive summary more concise")
# Review and accept/reject suggestions as above
...
```
### Getting the Final Report
Once you are satisfied with the report, you can get the final report object and use the content as you see fit.
Here's an example of printing out the final report:
```python
report = report.get()
report_text = "\n\n".join([block.template for block in report.blocks])
print(report_text)
```
## Additional Features
- **Async Support**: All methods have async counterparts: `create_report` -> `acreate_report`, `wait_for_plan` -> `await_for_plan`, etc.
- **Automatic Chat History**: The SDK automatically keeps track of chat history for each suggestion, unless you specify `auto_history=False` in `suggest_edits`.
- **Custom HTTP Client**: You can provide your own `httpx.AsyncClient` to the `LlamaReport` class.
- **Project and Organization IDs**: You can specify `project_id` and `organization_id` to use a specific project or organization.
+290
View File
@@ -0,0 +1,290 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["click", "tomlkit", "packaging"]
# ///
"""
This is a script called by the changeset bot. Normally changeset can do the following things, but this is a mixed ts and python repo, so we need to do some extra things.
There's 2 things this does:
- Versioning: Makes changes that may be committed with the newest version.
- Releasing/Tagging: After versions are changed, we check each package to see if its released, and if not, we release it and tag it.
"""
from dataclasses import dataclass
import json
import os
import subprocess
from pathlib import Path
from typing import Any, List, cast
import urllib.request
import urllib.error
import re
import click
import tomlkit
from packaging.version import Version
def _run_command(
cmd: List[str], cwd: Path | None = None, env: dict[str, str] | None = None
) -> None:
"""Run a command, streaming output to the console, and raise on failure."""
subprocess.run(cmd, check=True, text=True, cwd=cwd or Path.cwd(), env=env)
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
@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.
"""
pyproject_path = package_dir / "pyproject.toml"
if not pyproject_path.exists():
return
package_version = packages[js_package_name].version
py_doc = tomlkit.parse(pyproject_path.read_text())
by_python_name = {
pkg.python_package_name(): pkg
for pkg in packages.values()
if pkg.python_package_name()
}
current_version = py_doc["project"]["version"]
assert isinstance(current_version, str)
# 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:
"""Lock Python dependencies."""
try:
_run_command(["uv", "lock"])
click.echo("Locked Python dependencies")
except subprocess.CalledProcessError as e:
click.echo(f"Warning: Failed to lock Python dependencies: {e}", err=True)
@click.group()
def cli() -> None:
"""Changeset-based version management for llama-cloud-services."""
pass
@cli.command()
def version() -> None:
"""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"])
# 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()
@click.option("--tag", is_flag=True, help="Tag the packages after publishing")
@click.option("--dry-run", is_flag=True, help="Dry run the publish")
@click.option("--js/--no-js", default=True, help="Publish the js package")
@click.option("--py/--no-py", default=True, help="Publish the py package")
def publish(tag: bool, dry_run: bool, js: bool, py: bool) -> None:
"""Publish all packages."""
# move to the root
os.chdir(Path(__file__).parent.parent)
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:
maybe_publish_npm(dry_run)
if py:
maybe_publish_pypi(dry_run)
if tag:
if dry_run:
click.echo("Dry run, skipping tag. Would run:")
click.echo(" npx @changesets/cli tag")
click.echo(" git push --tags")
else:
# Let changesets create JS-related tags as usual
_run_command(["npx", "@changesets/cli", "tag"])
_run_command(["git", "push", "--tags"])
def maybe_publish_npm(dry_run: bool) -> None:
"""Publish the ts package if it needs to be published."""
target_dir = Path("ts/llama_cloud_services")
ts_path_package = target_dir / "package.json"
package_json = json.loads(ts_path_package.read_text())
version = package_json["version"]
# Check if this version is already published on npm
result = subprocess.run(
["npm", "view", "llama-cloud-services", "versions", "--json"],
check=True,
capture_output=True,
text=True,
cwd=target_dir,
)
published_versions = json.loads(result.stdout)
if version in published_versions:
click.echo(
f"npm package llama-cloud-services@{version} already published, skipping"
)
return
click.echo(f"Publishing npm package llama-cloud-services@{version}")
# defer to the package.json publish script
if dry_run:
click.echo("Dry run, skipping publish. Would run:")
click.echo(" pnpm run publish")
return
else:
_run_command(["pnpm", "run", "build"], cwd=target_dir)
_run_command(["pnpm", "publish"], cwd=target_dir)
def maybe_publish_pypi(dry_run: bool) -> None:
"""Publish the py packages if they need to be published."""
for pyproject in list(Path("py").glob("*/pyproject.toml")) + [
Path("py/pyproject.toml")
]:
name, version = current_version(pyproject)
if is_published(name, version):
click.echo(f"PyPI package {name}@{version} already published, skipping")
continue
click.echo(f"Publishing PyPI package {name}@{version}")
# Use different tokens for different packages
env = os.environ.copy()
token = os.environ["LLAMA_PARSE_PYPI_TOKEN"]
env["UV_PUBLISH_TOKEN"] = token
if dry_run:
summary = (token[:3] + "***") if len(token) <= 6 else token[:6] + "****"
click.echo(
f"Dry run, skipping publish. Would run with publish token {summary}:"
)
click.echo(" uv build")
click.echo(" uv publish")
else:
_run_command(["uv", "build"], cwd=pyproject.parent)
_run_command(["uv", "publish"], cwd=pyproject.parent, env=env)
def current_version(pyproject: Path) -> tuple[str, str]:
"""Return (package_name, version_str) taken from the given pyproject.toml."""
doc = tomlkit.parse(pyproject.read_text())
name = doc["project"]["name"]
version = str(Version(doc["project"]["version"])) # normalise
return name, version
def is_published(
name: str, version: str, index_url: str = "https://pypi.org/pypi"
) -> bool:
"""
True → `<name>==<version>` exists on the given index
False → package missing *or* version missing
"""
url = f"{index_url.rstrip('/')}/{name}/json"
try:
data = json.load(urllib.request.urlopen(url))
except urllib.error.HTTPError as e: # 404 → package not published at all
if e.code == 404:
return False
raise # any other error should surface
return version in data["releases"] # keys are version strings
if __name__ == "__main__":
cli()
-180
View File
@@ -1,180 +0,0 @@
#!/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("py/pyproject.toml").read_text()
main_doc = tomlkit.parse(main_content)
main_version = main_doc["project"]["version"]
# Read llama_parse/pyproject.toml
llama_parse_content = Path("py/llama_parse/pyproject.toml").read_text()
llama_parse_doc = tomlkit.parse(llama_parse_content)
llama_parse_version = llama_parse_doc["project"]["version"]
# Find llama-cloud-services dependency in the dependencies list
dependency_version = None
for dep in llama_parse_doc["project"]["dependencies"]:
if isinstance(dep, str) and dep.startswith("llama-cloud-services"):
dependency_version = (
dep.split("==")[1]
if "==" in dep
else dep.split(">=")[1]
if ">=" in dep
else None
)
break
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("py/pyproject.toml").read_text()
main_doc = tomlkit.parse(main_content)
main_doc["project"]["version"] = version
Path("py/pyproject.toml").write_text(tomlkit.dumps(main_doc))
# Update llama_parse/pyproject.toml
llama_parse_content = Path("py/llama_parse/pyproject.toml").read_text()
llama_parse_doc = tomlkit.parse(llama_parse_content)
llama_parse_doc["project"]["version"] = version
for dep_index, dep in enumerate(llama_parse_doc["project"]["dependencies"]):
if isinstance(dep, str) and dep.startswith("llama-cloud-services"):
llama_parse_doc["project"]["dependencies"][
dep_index
] = f"llama-cloud-services>={version}"
break
Path("py/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_if_not_exists(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}"
if not tag_exists(version):
# Create tag
subprocess.run(["git", "tag", tag_name], check=True)
click.echo(f"Created tag {tag_name}")
else:
click.echo(f"Tag {tag_name} already exists")
def tag_exists(version: str) -> bool:
"""Check if a git tag exists."""
tag_name = f"v{version}"
result = subprocess.run(
["git", "tag", "-l", tag_name], capture_output=True, text=True, check=True
)
return tag_name in result.stdout.strip()
def push_tag(version: str) -> None:
"""Push a git tag."""
tag_name = f"v{version}"
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)"
)
@click.option(
"--push",
is_flag=True,
help="Push the tag to the remote repository",
)
def tag(version: str | None = None, push: bool = False) -> None:
"""Create and push a git tag for the current version."""
if not version:
main_version, _, _ = get_current_versions()
version = main_version
create_if_not_exists(version)
if push:
push_tag(version)
if __name__ == "__main__":
cli()
+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);
});
+18
View File
@@ -1,5 +1,23 @@
# 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
- d028397: Update llama-cloud api version, and integrate with agent data deletion
## v0.1.0
First release for `llama-cloud-services`.
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -1,18 +1,21 @@
{
"name": "llama-cloud-services",
"version": "0.3.3",
"version": "0.3.9",
"type": "module",
"license": "MIT",
"scripts": {
"get-openapi": "node ./scripts/get-openapi.js",
"generate": "./node_modules/.bin/openapi-ts",
"build": "pnpm run generate && bunchee",
"dev": "bunchee --watch",
"lint": "eslint src/ --ignore-pattern client/*.ts --no-warn-ignored",
"format": "prettier --write ./src/",
"format:check": "prettier --check ./src/",
"test": "vitest run --testTimeout=60000",
"test:watch": "vitest --watch",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
"test:coverage": "vitest --coverage",
"release": "pnpm run build && pnpm publish"
},
"files": [
"openapi.json",
@@ -0,0 +1,21 @@
import fs from 'fs';
async function downloadOpenApiSpec() {
try {
const response = await fetch('https://api.cloud.llamaindex.ai/api/openapi.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
fs.writeFileSync('openapi.json', JSON.stringify(data, null, 2));
console.log('Successfully downloaded openapi.json');
} catch (error) {
console.error('Error downloading OpenAPI spec:', error);
process.exit(1);
}
}
downloadOpenApiSpec();
@@ -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];
}
}
@@ -1,9 +1,10 @@
import { createClient, createConfig } from "@hey-api/client-fetch";
import { getEnv } from "@llamaindex/env";
import { createClient } from "@hey-api/client-fetch";
import { client as defaultClient } from "../../api";
import {
aggregateAgentDataApiV1BetaAgentDataAggregatePost,
createAgentDataApiV1BetaAgentDataPost,
deleteAgentDataApiV1BetaAgentDataItemIdDelete,
deleteAgentDataByQueryApiV1BetaAgentDataDeletePost,
getAgentDataApiV1BetaAgentDataItemIdGet,
searchAgentDataApiV1BetaAgentDataSearchPost,
updateAgentDataApiV1BetaAgentDataItemIdPut,
@@ -12,6 +13,7 @@ import {
} from "../../client";
import type {
AggregateAgentDataOptions,
DeleteAgentDataOptions,
SearchAgentDataOptions,
TypedAgentData,
TypedAgentDataItems,
@@ -24,38 +26,24 @@ import type {
*/
export class AgentClient<T = unknown> {
private client: ReturnType<typeof createClient>;
private baseUrl: string;
private headers: Record<string, string>;
private collection: string;
private agentUrlId: string;
private deploymentName: string;
constructor({
apiKey = getEnv("LLAMA_CLOUD_API_KEY"),
baseUrl = "https://api.cloud.llamaindex.ai/",
client = defaultClient,
collection = "default",
agentUrlId = "_public",
deploymentName = "_public",
agentUrlId,
}: {
apiKey?: string;
baseUrl?: string;
client?: ReturnType<typeof createClient>;
collection?: string;
deploymentName?: string;
// deprecated, use deploymentName instead
agentUrlId?: string;
}) {
this.baseUrl = baseUrl;
this.headers = {
"X-SDK-Name": "llamaindex-ts",
...(apiKey && { Authorization: `Bearer ${apiKey}` }),
};
this.client = createClient(
createConfig({
baseUrl: this.baseUrl,
headers: this.headers,
}),
);
this.client = client;
this.collection = collection;
this.agentUrlId = agentUrlId;
this.deploymentName = agentUrlId || deploymentName;
}
/**
@@ -65,7 +53,7 @@ export class AgentClient<T = unknown> {
const response = await createAgentDataApiV1BetaAgentDataPost({
throwOnError: true,
body: {
agent_slug: this.agentUrlId,
deployment_name: this.deploymentName,
collection: this.collection,
data: data as Record<string, unknown>,
},
@@ -126,6 +114,24 @@ export class AgentClient<T = unknown> {
});
}
/**
* Delete all matching agent data, returns the total number of deleted items
*/
async delete(options: DeleteAgentDataOptions): Promise<number> {
const response = await deleteAgentDataByQueryApiV1BetaAgentDataDeletePost({
throwOnError: true,
body: {
deployment_name: this.deploymentName,
...(this.collection !== undefined && {
collection: this.collection,
}),
...(options.filter !== undefined && { filter: options.filter }),
},
client: this.client,
});
return response.data.deleted_count;
}
/**
* Search agent data
*/
@@ -135,7 +141,7 @@ export class AgentClient<T = unknown> {
const response = await searchAgentDataApiV1BetaAgentDataSearchPost({
throwOnError: true,
body: {
agent_slug: this.agentUrlId,
deployment_name: this.deploymentName,
...(this.collection !== undefined && {
collection: this.collection,
}),
@@ -182,7 +188,7 @@ export class AgentClient<T = unknown> {
const response = await aggregateAgentDataApiV1BetaAgentDataAggregatePost({
throwOnError: true,
body: {
agent_slug: this.agentUrlId,
deployment_name: this.deploymentName,
...(this.collection !== undefined && {
collection: this.collection,
}),
@@ -226,7 +232,7 @@ export class AgentClient<T = unknown> {
private transformResponse(data: AgentData): TypedAgentData<T> {
const result: TypedAgentData<T> = {
id: data.id!,
agentUrlId: data.agent_slug,
deploymentName: data.deployment_name,
data: data.data as T,
createdAt: new Date(data.created_at!),
updatedAt: new Date(data.updated_at!),
@@ -267,10 +273,10 @@ export interface AgentDataClientOptions {
/** Base URL for the client */
/** Base URL of the llama cloud api */
baseUrl?: string;
/** If running in an agent runtime, optionally provide the window url to infer the agent url id */
/** If running in an agent runtime, optionally provide the window url to infer the deployment name */
windowUrl?: string;
/** Agent URL ID for the client, if not provided, it will be inferred from the window url, or fall back to "default" */
agentUrlId?: string;
/** Deployment name for the client, if not provided, it will be inferred from the window url, or fall back to "default" */
deploymentName?: string;
/** Collection name for the client, defaults to "default" */
collection?: string;
}
@@ -281,27 +287,28 @@ export interface AgentDataClientOptions {
* @returns A new AgentClient instance
*/
export function createAgentDataClient<T = unknown>({
apiKey,
baseUrl,
client = defaultClient,
windowUrl,
env,
deploymentName,
agentUrlId,
collection = "default",
}: {
apiKey?: string;
baseUrl?: string;
client?: ReturnType<typeof createClient>;
windowUrl?: string;
env?: Record<string, string>;
deploymentName?: string;
// deprecated, use deploymentName instead
agentUrlId?: string;
collection?: string;
} = {}): AgentClient<T> {
if (env && !agentUrlId) {
agentUrlId =
if (env && !deploymentName) {
deploymentName =
env.LLAMA_DEPLOY_DEPLOYMENT_NAME ||
env.NEXT_PUBLIC_LLAMA_DEPLOY_DEPLOYMENT_NAME ||
env.VITE_LLAMA_DEPLOY_DEPLOYMENT_NAME;
}
if (windowUrl && !agentUrlId) {
if (windowUrl && !deploymentName) {
try {
const url = new URL(windowUrl);
const path = url.pathname;
@@ -310,20 +317,20 @@ export function createAgentDataClient<T = unknown>({
url.hostname.includes("127.0.0.1");
if (path.startsWith("/deployments/") && !isLocalhost) {
// /deployments/<agent-url-id>/ui/ -> ["", "deployments", "<agent-url-id>", "ui"]
agentUrlId = path.split("/")[2];
deploymentName = path.split("/")[2];
}
} catch (error) {
console.warn(
"Failed to infer agent url id from window url, falling back to default",
"Failed to infer deployment name from window url, falling back to default",
error,
);
}
}
return new AgentClient({
...(apiKey && { apiKey }),
...(baseUrl && { baseUrl }),
...(deploymentName && { deploymentName }),
...(agentUrlId && { agentUrlId }),
collection,
client,
});
}
@@ -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 {
@@ -87,8 +87,8 @@ export interface ExtractedData<T = unknown> {
export interface TypedAgentData<T = unknown> {
/** The unique ID of the agent data record. */
id: string;
/** The ID of the agent that created the data. */
agentUrlId: string;
/** The deployment name of the agent that created the data. */
deploymentName: string;
/** The collection of the agent data. */
collection?: string;
/** The data of the agent data. Usually an ExtractedData&lt;SomeOtherType&gt; */
@@ -127,6 +127,14 @@ export interface SearchAgentDataOptions {
includeTotal?: boolean;
}
/**
* Options for deleting agent data
*/
export interface DeleteAgentDataOptions {
/** Filter options for the deletion. */
filter?: Record<string, FilterOperation>;
}
/**
* Options for aggregating agent data
*/
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -147,6 +147,10 @@ export class LlamaParseReader extends FileReader {
output_s3_region?: string | undefined;
preserve_layout_alignment_across_pages?: boolean | undefined;
spreadsheet_extract_sub_tables?: boolean | undefined;
specialized_chart_parsing_agentic?: boolean | undefined;
specialized_chart_parsing_efficient?: boolean | undefined;
specialized_chart_parsing_plus?: boolean | undefined;
precise_bounding_box?: boolean | undefined;
formatting_instruction?: string | undefined;
parse_mode?: ParsingMode | undefined;
system_prompt?: string | undefined;
@@ -194,7 +198,7 @@ export class LlamaParseReader extends FileReader {
? this.language
: [this.language];
this.stdout =
params.stdout ?? typeof process !== "undefined"
(params.stdout ?? typeof process !== "undefined")
? process!.stdout
: undefined;
const apiKey = params.apiKey ?? getEnv("LLAMA_CLOUD_API_KEY");
@@ -331,6 +335,11 @@ export class LlamaParseReader extends FileReader {
preserve_layout_alignment_across_pages:
this.preserve_layout_alignment_across_pages,
spreadsheet_extract_sub_tables: this.spreadsheet_extract_sub_tables,
specialized_chart_parsing_agentic: this.specialized_chart_parsing_agentic,
specialized_chart_parsing_efficient:
this.specialized_chart_parsing_efficient,
specialized_chart_parsing_plus: this.specialized_chart_parsing_plus,
precise_bounding_box: this.precise_bounding_box,
formatting_instruction: this.formatting_instruction,
parse_mode: this.parse_mode,
system_prompt: this.system_prompt,
+4
View File
@@ -87,6 +87,10 @@ export const parseFormSchema = z.object({
preserve_layout_alignment_across_pages: z.boolean().optional(),
skip_diagonal_text: z.boolean().optional(),
spreadsheet_extract_sub_tables: z.boolean().optional(),
specialized_chart_parsing_agentic: z.boolean().optional(),
specialized_chart_parsing_efficient: z.boolean().optional(),
specialized_chart_parsing_plus: z.boolean().optional(),
precise_bounding_box: z.boolean().optional(),
structured_output: z.boolean().optional(),
structured_output_json_schema: z.string().optional(),
structured_output_json_schema_name: z.string().optional(),
@@ -0,0 +1,246 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { AgentClient, createAgentDataClient } from "../src/beta/agent/index.js";
import * as sdk from "../src/client/index.js";
describe("AgentClient", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("createItem sends correct payload and returns typed data", async () => {
const spy = vi
.spyOn(sdk, "createAgentDataApiV1BetaAgentDataPost")
.mockResolvedValue({
data: {
id: "1",
deployment_name: "dep",
collection: "col",
data: { foo: "bar" },
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
},
} as any);
const client = new AgentClient<{ foo: string }>({
deploymentName: "dep",
collection: "col",
});
const result = await client.createItem({ foo: "bar" });
expect(spy).toHaveBeenCalledOnce();
const call = spy.mock.calls[0][0];
expect(call.body.deployment_name).toBe("dep");
expect(call.body.collection).toBe("col");
expect(call.body.data).toEqual({ foo: "bar" });
expect(result.id).toBe("1");
expect(result.deploymentName).toBe("dep");
expect(result.collection).toBe("col");
expect(result.data).toEqual({ foo: "bar" });
expect(result.createdAt).toEqual(new Date("2024-01-01T00:00:00Z"));
expect(result.updatedAt).toEqual(new Date("2024-01-01T00:00:00Z"));
});
it("getItem returns null for 404 errors", async () => {
const spy = vi
.spyOn(sdk, "getAgentDataApiV1BetaAgentDataItemIdGet")
.mockImplementation(async () => {
const err: any = new Error("Not found");
err.response = { status: 404 };
throw err;
});
const client = new AgentClient({ deploymentName: "dep" });
const res = await client.getItem("missing-id");
expect(spy).toHaveBeenCalledOnce();
expect(res).toBeNull();
});
it("updateItem updates and returns typed data", async () => {
const spy = vi
.spyOn(sdk, "updateAgentDataApiV1BetaAgentDataItemIdPut")
.mockResolvedValue({
data: {
id: "123",
deployment_name: "dep",
collection: "col",
data: { foo: "baz" },
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-02T00:00:00Z",
},
} as any);
const client = new AgentClient<{ foo: string }>({
deploymentName: "dep",
collection: "col",
});
const res = await client.updateItem("123", { foo: "baz" });
expect(spy).toHaveBeenCalledOnce();
const call = spy.mock.calls[0][0];
expect(call.path.item_id).toBe("123");
expect(call.body.data).toEqual({ foo: "baz" });
expect(res.id).toBe("123");
expect(res.updatedAt).toEqual(new Date("2024-01-02T00:00:00Z"));
});
it("deleteItem calls delete endpoint with correct path", async () => {
const spy = vi
.spyOn(sdk, "deleteAgentDataApiV1BetaAgentDataItemIdDelete")
.mockResolvedValue({} as any);
const client = new AgentClient({ deploymentName: "dep" });
await client.deleteItem("abc");
expect(spy).toHaveBeenCalledOnce();
expect(spy.mock.calls[0][0].path.item_id).toBe("abc");
});
it("delete by query returns deleted count", async () => {
const spy = vi
.spyOn(sdk, "deleteAgentDataByQueryApiV1BetaAgentDataDeletePost")
.mockResolvedValue({ data: { deleted_count: 7 } } as any);
const client = new AgentClient({
deploymentName: "dep",
collection: "col",
});
const count = await client.delete({
filter: { status: { op: "eq", value: "accepted" } as any },
});
expect(spy).toHaveBeenCalledOnce();
const body = spy.mock.calls[0][0].body;
expect(body.deployment_name).toBe("dep");
expect(body.collection).toBe("col");
expect(count).toBe(7);
});
it("search maps items and optional fields correctly", async () => {
const now = "2024-01-01T00:00:00Z";
const spy = vi
.spyOn(sdk, "searchAgentDataApiV1BetaAgentDataSearchPost")
.mockResolvedValue({
data: {
items: [
{
id: "1",
deployment_name: "dep",
collection: "col",
data: { foo: "bar" },
created_at: now,
updated_at: now,
},
],
total_size: 1,
next_page_token: "next",
},
} as any);
const client = new AgentClient<{ foo: string }>({
deploymentName: "dep",
collection: "col",
});
const result = await client.search({
includeTotal: true,
orderBy: "created_at desc",
pageSize: 1,
offset: 0,
});
expect(spy).toHaveBeenCalledOnce();
const body = spy.mock.calls[0][0].body;
expect(body.deployment_name).toBe("dep");
expect(body.collection).toBe("col");
expect(body.include_total).toBe(true);
expect(body.order_by).toBe("created_at desc");
expect(body.page_size).toBe(1);
expect(body.offset).toBe(0);
expect(result.items).toHaveLength(1);
expect(result.totalSize).toBe(1);
expect(result.nextPageToken).toBe("next");
expect(result.items[0].createdAt).toEqual(new Date(now));
});
it("aggregate maps groups and optional fields correctly", async () => {
const spy = vi
.spyOn(sdk, "aggregateAgentDataApiV1BetaAgentDataAggregatePost")
.mockResolvedValue({
data: {
items: [
{
group_key: { status: "accepted" },
count: 3,
first_item: { foo: "bar" },
},
],
total_size: 1,
next_page_token: "tok",
},
} as any);
const client = new AgentClient<{ foo: string }>({
deploymentName: "dep",
collection: "col",
});
const result = await client.aggregate({
groupBy: ["status"],
count: true,
first: true,
pageSize: 1,
offset: 0,
});
expect(spy).toHaveBeenCalledOnce();
const body = spy.mock.calls[0][0].body;
expect(body.deployment_name).toBe("dep");
expect(body.collection).toBe("col");
expect(body.group_by).toEqual(["status"]);
expect(body.count).toBe(true);
expect(body.first).toBe(true);
expect(body.page_size).toBe(1);
expect(body.offset).toBe(0);
expect(result.items).toHaveLength(1);
expect(result.totalSize).toBe(1);
expect(result.nextPageToken).toBe("tok");
expect(result.items[0].groupKey).toEqual({ status: "accepted" });
expect(result.items[0].count).toBe(3);
expect(result.items[0].firstItem).toEqual({ foo: "bar" });
});
it("createAgentDataClient infers deployment name from env", async () => {
const spy = vi
.spyOn(sdk, "searchAgentDataApiV1BetaAgentDataSearchPost")
.mockResolvedValue({
data: { items: [], total_size: 0 },
} as any);
const client = createAgentDataClient({
env: { LLAMA_DEPLOY_DEPLOYMENT_NAME: "env-dep" },
});
await client.search({});
const body = spy.mock.calls[0][0].body;
expect(body.deployment_name).toBe("env-dep");
});
it("createAgentDataClient infers deployment name from windowUrl (non-local)", async () => {
const spy = vi
.spyOn(sdk, "deleteAgentDataByQueryApiV1BetaAgentDataDeletePost")
.mockResolvedValue({
data: { deleted_count: 0 },
} as any);
const client = createAgentDataClient({
windowUrl: "https://app.llamaindex.ai/deployments/abc/ui/",
});
await client.delete({});
const body = spy.mock.calls[0][0].body;
expect(body.deployment_name).toBe("abc");
});
});
@@ -0,0 +1,80 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { listProjectsApiV1ProjectsGet, client } from "../src/api.js";
describe("Global client configuration", () => {
const originalFetch = globalThis.fetch;
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("adds X-SDK-Name header from global client config", async () => {
const fetchSpy = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async (input, init) => {
// Validate the header is present on the outgoing request
let headers: Headers;
if (input && typeof input === "object" && "headers" in (input as any)) {
headers = (input as Request).headers;
} else {
headers = new Headers((init && init.headers) || {});
}
expect(headers.get("X-SDK-Name")).toBe("llamaindex-ts");
return new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
});
// Trigger any request via the generated SDK (imported through src/api.ts)
await listProjectsApiV1ProjectsGet({ throwOnError: false });
expect(fetchSpy).toHaveBeenCalledOnce();
});
it("respects additional custom headers set via setConfig", async () => {
const prevConfig = client.getConfig();
try {
client.setConfig({
...prevConfig,
headers: {
...(prevConfig.headers || {}),
"X-Custom-Header": "custom-value",
},
});
const fetchSpy = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async (input, init) => {
let headers: Headers;
if (
input &&
typeof input === "object" &&
"headers" in (input as any)
) {
headers = (input as Request).headers;
} else {
headers = new Headers((init && init.headers) || {});
}
expect(headers.get("X-SDK-Name")).toBe("llamaindex-ts");
expect(headers.get("X-Custom-Header")).toBe("custom-value");
return new Response(JSON.stringify([]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
});
await listProjectsApiV1ProjectsGet({ throwOnError: false });
expect(fetchSpy).toHaveBeenCalledOnce();
} finally {
// Restore original configuration to avoid test cross-talk
client.setConfig(prevConfig);
}
});
});