Compare commits

..

101 Commits

Author SHA1 Message Date
Clelia (Astra) Bertelli acfc90d0a8 fix: change name to test_index in unit_tests 2025-08-05 19:26:30 +02:00
Clelia (Astra) Bertelli e80376c97e chore: moving tests 2025-08-05 19:17:28 +02:00
Clelia (Astra) Bertelli 5acca1fc98 ci: run all tests using explicit patterns 2025-08-05 11:16:06 +02:00
Clelia (Astra) Bertelli 68cb4dacd3 chore: differentiate between e2e and non-e2e tests 2025-08-05 11:09:51 +02:00
Clelia (Astra) Bertelli 0b16edf058 fix: test e2e only on PR 2025-08-04 12:29:33 +02:00
Clelia (Astra) Bertelli 5bb9816f04 ci: job name correction 🤦 2025-08-04 12:28:00 +02:00
Clelia (Astra) Bertelli d55f8cf153 ci: workflow name and linting 2025-08-04 12:24:50 +02:00
Clelia (Astra) Bertelli 09a911ee62 fix: run e2e only on 3.12 2025-08-04 12:20:11 +02:00
Marcus Schiesser 98db5eeeae chore: remove llamaindex dep (#826)
* chore: remove llamaindex dep

* chore: remove all dependency on llamaindex

* feat: restructure docs/examples

* chore: remove llamaindex dep

* chore: remove all dependency on llamaindex

* simplify querytool

* fix tests

* revert version

* add missing import

* remove unused file

* feat: change default description to adapt it to LlamaCloud Index

---------

Co-authored-by: Clelia (Astra) Bertelli <clelia@runllama.ai>
2025-08-04 11:48:24 +02:00
Adrian Lyjak c21cb34ff6 fix: Fix bugs in ExtractedFieldMetadata parser (#834)
* fix: Fix bugs in ExtractedFieldMetadata parser

- Wasn't recursing through lists properly
- Fix field names, names changed or I copied incorrectly
- Handle reasoning on a parent object

* version script fixes

* update versions

* skip the unrelated failing test for now
2025-08-01 16:08:16 -04:00
Adrian Lyjak e28c7b9d92 Copy extracted citations to the new repo (#832)
* Copy extracted citations to the new repo

* fix spell check

* ignore examples too

* tweak timeout

* add changes to github actions

* shrug
2025-07-31 19:34:24 +02:00
Clelia (Astra) Bertelli ee4e565604 Example Notebooks (#829)
* fix: add symlink to avoid breaking links

* feat: copy examples
2025-07-31 16:54:12 +02:00
Clelia (Astra) Bertelli 6dbb089f4c delete examples (#830) 2025-07-31 16:53:54 +02:00
Logan Markewich c4b694db8d update symlink 2025-07-31 08:44:30 -06:00
Clelia (Astra) Bertelli 97f428ad06 fix: add symlink to avoid breaking links (#828) 2025-07-31 08:39:44 -06:00
Clelia (Astra) Bertelli ef92ee5408 feat: add ts examples (clean) (#822)
* feat: add ts examples (clean)

* chore: correct title
2025-07-31 11:25:29 +02:00
Logan d094668d03 Update extract.md 2025-07-30 14:58:25 -06:00
Logan 5bb5fc1625 Update parse.md 2025-07-30 14:58:09 -06:00
Logan 1d57e0071d Update parse.md 2025-07-30 14:57:31 -06:00
Logan 2a344c4f5c Update extract.md 2025-07-30 14:56:33 -06:00
Logan ce02559b8d Update README.md (#824) 2025-07-30 14:55:21 -06:00
Harshit Budhiraja e42746e372 docs(readme): update hyperlinks to correct targets (#820) 2025-07-30 14:53:43 -06:00
Clelia (Astra) Bertelli 3149dfd03a fix: no git checks on pnpm publish (#823) 2025-07-30 21:25:23 +02:00
Clelia (Astra) Bertelli e499fdbdab fix: add release to NPM (#819) 2025-07-30 20:55:41 +02:00
Clelia (Astra) Bertelli e57df39248 Merge index into main (#821)
* wip: monorepo changes

* fix ci for the time being

* fix ci for the time being pt2

* wip: first cloud refactoring for ts

* chore: restore original package

* fix: imports, package.json, tsconfig.json, client, reader

* feat: adjustments after local testing

* ci: github actions for typescript

* ci: typescript ci

* ci: nvmrc 🤦

* ci: remove cache 🤦

* ci: actions

* ci: actions (i lost count)

* ci: pnpm run format

* ci: pnpm run format

* chore: migrate llama-parse to uv

* add tests

* remove unneeded readme

* update workflows

* feat: modify py release workflow, adding uv version, bump version for llama-cloud-services to latest

* uv lock

* ci: python tests all tests

* fix: lock file pulling in wrong version of numpy

* feat: add index to llama-cloud-services (#817)

---------

Co-authored-by: Logan Markewich <logan.markewich@live.com>
Co-authored-by: Adrian Lyjak <adrianlyjak@gmail.com>
2025-07-30 19:46:36 +02:00
Clelia (Astra) Bertelli 09b192b98b Adding TS llama-cloud-services and moving llama-parse to uv (#811)
* wip: monorepo changes

* fix ci for the time being

* fix ci for the time being pt2

* wip: first cloud refactoring for ts

* chore: restore original package

* fix: imports, package.json, tsconfig.json, client, reader

* feat: adjustments after local testing

* ci: github actions for typescript

* ci: typescript ci

* ci: nvmrc 🤦

* ci: remove cache 🤦

* ci: actions

* ci: actions (i lost count)

* ci: pnpm run format

* ci: pnpm run format

* chore: migrate llama-parse to uv

* add tests

* remove unneeded readme

* update workflows

* feat: modify py release workflow, adding uv version, bump version for llama-cloud-services to latest

* uv lock

* ci: python tests all tests

* fix: lock file pulling in wrong version of numpy

---------

Co-authored-by: Logan Markewich <logan.markewich@live.com>
Co-authored-by: Adrian Lyjak <adrianlyjak@gmail.com>
2025-07-30 17:59:08 +02:00
Adrian Lyjak 13f01a0621 Adding support for page citations, and refactor the confidence into the field metadata (#815) 2025-07-30 10:55:29 -04:00
Javier Torres cf879a1a58 Bump llama-cloud version (#814) 2025-07-28 16:06:31 -05:00
Tuana Çelik fcdf2ab63e Fixes to multimodal report generation (#809) 2025-07-23 16:28:53 -06:00
Adrian Lyjak 083d8109c2 Make versioning a little easier, and fix llama_parse version (#808)
* Make versioning a little easier

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

* add JobResult.get_text/get_markdown/get_json

* add tests
2025-07-18 18:50:29 +09:00
Adrian Lyjak 3f311c0669 Bump v0.6.49 (#797) 2025-07-16 19:42:09 -04:00
Adrian Lyjak b1a2f9d42b Add new method to fetch the full, non-paginated markdown (#796)
Add new method to fetch the full, non-paginated markdown for proper merge_tables_across_pages_in_markdown support
2025-07-16 19:29:57 -04:00
Neeraj Pradhan 142f55c94c Update to version 0.6.48 (#795)
* Update to version 0.6.48

* pin version

* poetry lock

* adjust warnings

* collect all agents for cleanup
2025-07-16 13:24:44 -07:00
Clelia (Astra) Bertelli 230a110e52 chore: vbump to 0.6.47 and example notebook (#794)
* chore: vbump to 0.6.47 and example notebook

* chore: update llama-parse pyproject.toml
2025-07-16 19:08:44 +02:00
Clelia (Astra) Bertelli 83e2b031cd feat: add table extraction for LlamaParse as CSV files (#793)
* feat: add table extraction for LlamaParse as CSV files

* chore: poetry lock

* chore: add tests

* fix: handle the case where no tables are present

* chore: implement suggestions
2025-07-16 17:08:09 +02:00
Adrian Lyjak 4844e26e5c Improve Agent Data interface, and add file related fields to extracted data for file tracking (#785)
Add file related fields for file tracking. Simplify API
2025-07-09 14:27:24 -04:00
Pierre-Loic Doulcet 70a049af3c merge_tables_across_pages_in_markdown parse parameter (#786)
* merge_tables_across_pages_in_markdown parse parameter

* base.py
2025-07-09 19:03:48 +02:00
Adrian Lyjak dc11776c86 Add nicer hand-written agent data interface (#782)
* Add nicer hand-written agent data interface

* bump to 0.6.44
2025-07-08 17:49:00 -04:00
Logan 2448a42b90 relax pydantic job object (#784) 2025-07-08 12:12:56 -06:00
Neeraj Pradhan c75a900174 Bump up version to 0.6.42 (#783) 2025-07-08 09:16:46 -07:00
Peter Rowlands (변기호) 2fb7adfe0e parse: loosen PageItem.rows type hint (v0.6.41) (#776)
* parse: loosen PageItem.rows type hint

* bump version to 0.6.41
2025-06-30 21:47:40 +09:00
Pierre-Loic Doulcet dc82270724 header footer control in llamaparse (#775) 2025-06-30 16:02:59 +08:00
Neeraj Pradhan d880a48dd0 Bump to version 0.6.39 (#772)
* Bump to version 0.6.39

* lock file update
2025-06-27 16:04:40 -07:00
Logan 7567e8b45e except one more error type (#771) 2025-06-27 10:17:57 -06:00
Neeraj Pradhan 0d59a90151 Relax tenacity version; bump up version to 0.6.37 (#769) 2025-06-25 15:32:20 -07:00
Neeraj Pradhan 98ad550b1a Manage extract agent lifecycle in pytest (#766) 2025-06-24 08:59:38 -07:00
Neeraj Pradhan b58f43ce9f Bump up version to 0.6.36 (#763) 2025-06-23 14:26:05 -07:00
Neeraj Pradhan acf6adcd91 Make job fetching more robust to connection errors (#764) 2025-06-23 13:17:28 -07:00
Neeraj Pradhan daf6576c3c Bump version to 0.6.35 (#762) 2025-06-20 09:33:21 -07:00
Logan 8caa4defa6 fix partition (#758) 2025-06-16 17:37:52 -06:00
Pierre-Loic Doulcet 26918b8de4 add high_res_ocr to the package (#757) 2025-06-16 16:28:23 +08:00
Pierre-Loic Doulcet 6fb5ebe2f9 6.32 warning on unused parameters (#755) 2025-06-12 22:35:48 -06:00
dependabot[bot] c0aa67995b Bump requests from 2.32.3 to 2.32.4 in /llama_parse (#754) 2025-06-10 18:14:44 -06:00
dependabot[bot] 9f841f8328 Bump tornado from 6.4.2 to 6.5.1 in /llama_parse (#753) 2025-06-10 18:14:35 -06:00
dependabot[bot] 99c75eece9 Bump h11 from 0.14.0 to 0.16.0 in /llama_parse (#752) 2025-06-10 18:14:27 -06:00
Logan 57d2586ee3 v0.6.31 (#751) 2025-06-10 17:58:36 -06:00
Jerry Liu 4280a43ec8 add multi-fund analysis notebook (#739) 2025-06-07 11:25:25 -07:00
Neeraj Pradhan 7f1082bbb2 Bump to version 0.6.30 (#748) 2025-06-05 14:34:20 -07:00
Simon Suo 57cfc45804 Directly pass None project_id (#743) 2025-06-05 14:16:54 -07:00
Soumil.Binhani 30e8913875 0.6.29: Standerdize the parsing input format for both .aget_json() and .aload_data() (#745) 2025-06-05 10:58:07 -06:00
Logan 0ce6d4d7a4 more optional types marked (#747) 2025-06-05 10:50:29 -06:00
Peter Rowlands (변기호) 584ba8d48e 0.6.28: fix job result format after partitioning changes (#741)
* parse: fix job result format

* bump to 0.6.28
2025-06-02 15:25:30 -07:00
Peter Rowlands (변기호) 925805ee11 parse: support partitioning files before parsing (#709)
* parse: add utils for handling target_pages

* parse: support partitioning docs into multiple parse jobs

* tests: add tests for partitioned parse

* drop unneeded get_job_result call

* add parse JobFailedException and expected error handling

* bump to 0.6.27
2025-06-02 12:27:58 -07:00
Logan 76fb73c971 v0.6.26 (#740) 2025-06-02 09:59:45 -06:00
Abhik Bhattacharjee 6d19ea9ac0 parse: fix the "model" parameter mismatch between playground and Python client (#737) 2025-06-02 09:35:30 -06:00
Pierre-Loic Doulcet 90431090e9 0.6.25 outlined_table_extraction (#736) 2025-05-30 11:37:21 +02:00
Neeraj Pradhan 6dff35b204 Add notebook for Form 4 extraction (#731)
* Add notebook for Form 4 extraction

* fix comments

* heavier caching; add mermaid diag

* add output directory

* save notebook
2025-05-29 18:31:56 -07:00
Logan e634c7978d v0.6.24 (#732) 2025-05-28 20:11:51 -06:00
Neeraj Pradhan 7a9e99bba2 Bump to version 0.6.23 (#729) 2025-05-20 09:43:06 -07:00
Adrian Lyjak efcdd4405b Pass through verify and timeout config to the extraction agent (#726) 2025-05-17 12:51:16 -07:00
Javier Torres bf3614690f Remove credits from parse metadata (#720) 2025-05-09 16:03:09 -05:00
Logan 7463e00da3 v0.6.22 (#718) 2025-05-08 11:44:41 -06:00
Tuana Çelik cbe9de0c57 Adding example for extracting with citations (#716)
* Adding example for extracting with citations

* removing TOC and installation output
2025-05-06 23:32:17 +02:00
Logan a023507d42 even more optional (#711) 2025-05-01 15:52:38 -06:00
Peter Rowlands (변기호) e48f544ddc parse: fix num_workers/parse job batching (#708) 2025-05-01 09:30:35 -06:00
Logan 4aa7ad5642 v0.6.20 (#707) 2025-04-29 08:53:55 -06:00
Sacha Bron c39cdbcd01 v0.6.19 (#706) 2025-04-29 12:28:21 +02:00
Pierre-Loic Doulcet 71eaa8bcc6 add auto_mode_configuration_jon for llamaParse (#704) 2025-04-29 12:23:03 +02:00
Pierre-Loic Doulcet 1e1cbdfc79 add support for presets (#703) 2025-04-29 11:54:54 +08:00
Logan cc8af4a43a make original height + width optional in the parse result (#702) 2025-04-27 18:31:35 -06:00
dependabot[bot] 43fbd48ab8 Bump actions/setup-python from 4 to 5 (#701)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  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-04-27 13:08:44 -06:00
dependabot[bot] 5ec66e9452 Bump actions/checkout from 3 to 4 (#700)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '4'
  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-04-27 13:08:31 -06:00
Scott Brenner 211521c82e Dependabot configuration to update actions in workflow (#698) 2025-04-27 12:52:11 -06:00
Scott Brenner 4ddaab1efb Refactor CodeQL workflow (#699)
* Refactor CodeQL workflow

* Update .github/workflows/codeql.yml
2025-04-27 12:51:56 -06:00
Neeraj Pradhan 53e5ce2e83 Bump to v0.6.16 (#697) 2025-04-25 14:39:52 -07:00
Neeraj Pradhan 9f4bd1cb64 Update to latest version of llama-cloud (#696)
update to latest version of llama-cloud
2025-04-25 14:14:49 -07:00
Logan 456863752b small enum nit for FailedPageMode (#693) 2025-04-23 21:34:26 -06:00
Pierre-Loic Doulcet c2dc34bbd6 Page error parameters (#691) 2025-04-23 20:47:57 -06:00
Logan fcabb04baf skip llama-report tests in cicd (#692)
* skip llama-report tests in cicd

* skip llama-report tests in cicd
2025-04-23 20:47:00 -06:00
Sacha Bron 8e7c32d3d6 Add markdown_table_multiline_header_separator support (#683)
* Add markdown_table_multiline_header_separator support

* Lint
2025-04-15 17:39:46 +02:00
Neeraj Pradhan 7e3013d914 Use unique filename to avoid db collisions later (#682)
* Use unique filename to avoid db collisions later

* add xfail marker to test_create_and_delete_report
2025-04-11 11:03:15 -07:00
Logan 4a664c33d2 parse readme nits (#681) 2025-04-10 19:25:06 -06:00
Logan 6d049ee2e4 v0.6.12 (#680) 2025-04-10 19:18:49 -06:00
Logan fa73e73664 new result object (#650) 2025-04-10 19:17:23 -06:00
Neeraj Pradhan bf67ee6056 Update docs for LlamaExtract (#679) 2025-04-10 12:16:32 -07:00
Neeraj Pradhan a1abef2ee9 Bump version to v0.6.11 (#678) 2025-04-10 11:23:06 -07:00
Neeraj Pradhan a753e01d3c Support text as input directly in the SDK (#676) 2025-04-09 21:40:56 -07:00
180 changed files with 122106 additions and 5359 deletions
+11
View File
@@ -0,0 +1,11 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# and
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
-48
View File
@@ -1,48 +0,0 @@
name: Build Package
# Build package on its own without additional pip install
on:
push:
branches:
- main
pull_request:
env:
POETRY_VERSION: "1.6.1"
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
# You can use PyPy versions in python-version.
# For example, pypy-2.7 and pypy-3.8
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.9"]
steps:
- uses: actions/checkout@v3
- name: Set up python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: ${{ env.POETRY_VERSION }}
- name: Install deps
shell: bash
run: poetry install
- name: Ensure lock works
shell: bash
run: poetry lock
- name: Build
shell: bash
run: poetry build
- name: Test installing built package
shell: bash
run: python -m pip install .
- name: Test import
shell: bash
working-directory: ${{ vars.RUNNER_TEMP }}
run: python -c "import llama_cloud_services"
+53
View File
@@ -0,0 +1,53 @@
name: Build Package - Python
# Build package on its own without additional pip install
on:
push:
branches:
- main
paths:
- "py/**"
pull_request:
paths:
- "py/**"
env:
UV_VERSION: "0.7.20"
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
# You can use PyPy versions in python-version.
# For example, pypy-2.7 and pypy-3.8
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.9"]
steps:
- uses: actions/checkout@v4
- 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: Test import
working-directory: py
run: uv run -- python -c "import llama_cloud_services"
+36
View File
@@ -0,0 +1,36 @@
name: Build Package - TypeScript
on:
push:
branches:
- main
paths:
- "ts/**"
pull_request:
paths:
- "ts/**"
jobs:
pre_release:
name: Pre Release
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- 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
working-directory: ts/llama_cloud_services/
run: pnpm install --no-frozen-lockfile
- name: Build
working-directory: ts/llama_cloud_services/
run: pnpm run build
+8 -48
View File
@@ -1,14 +1,3 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
@@ -28,54 +17,25 @@ jobs:
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners
# Consider using larger runners for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
runs-on: "ubuntu-latest"
timeout-minutes: 360
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["python"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
languages: python
dependency-caching: true
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
category: "/language:python"
-37
View File
@@ -1,37 +0,0 @@
name: Linting
on:
push:
branches:
- main
pull_request:
env:
POETRY_VERSION: "1.6.1"
jobs:
build:
runs-on: ubuntu-latest
strategy:
# You can use PyPy versions in python-version.
# For example, pypy-2.7 and pypy-3.8
matrix:
python-version: ["3.9"]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}
- name: Set up python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: ${{ env.POETRY_VERSION }}
- name: Install pre-commit
shell: bash
run: poetry run pip install pre-commit
- name: Run linter
shell: bash
run: poetry run make lint
+35
View File
@@ -0,0 +1,35 @@
name: Lint - Python
on:
push:
branches:
- main
pull_request:
env:
UV_VERSION: "0.7.20"
jobs:
build:
runs-on: ubuntu-latest
strategy:
# You can use PyPy versions in python-version.
# For example, pypy-2.7 and pypy-3.8
matrix:
python-version: ["3.9"]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: ${{ env.UV_VERSION }}
- name: Set up Python
run: uv python install ${{ matrix.python-version }}
- name: Run linter
shell: bash
working-directory: py
run: uv run -- pre-commit run -a
+38
View File
@@ -0,0 +1,38 @@
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@v4
- 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
working-directory: ts/llama_cloud_services/
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
-75
View File
@@ -1,75 +0,0 @@
name: Publish llama-parse to PyPI / GitHub
on:
push:
tags:
- "v*"
workflow_dispatch:
env:
POETRY_VERSION: "1.6.1"
PYTHON_VERSION: "3.9"
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@v3
- name: Set up python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: ${{ env.POETRY_VERSION }}
- name: Install deps
shell: bash
run: pip install -e .
- name: Build and publish llama-cloud-services
uses: JRubics/poetry-publish@v2.1
with:
pypi_token: ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
poetry_install_options: "--without dev"
- name: Build and publish llama-parse
uses: JRubics/poetry-publish@v2.1
with:
working_directory: "llama_parse"
pypi_token: ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
poetry_install_options: "--without dev"
- 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 }}
draft: false
prerelease: false
- name: Get Asset name
run: |
export PKG=$(ls dist/ | grep tar)
set -- $PKG
echo "name=$1" >> $GITHUB_ENV
- name: Upload Release Asset (sdist) to GitHub
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: dist/${{ env.name }}
asset_name: ${{ env.name }}
asset_content_type: application/zip
+66
View File
@@ -0,0 +1,66 @@
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@v4
- 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
+51
View File
@@ -0,0 +1,51 @@
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@v4
- 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
working-directory: ts/llama_cloud_services
run: pnpm install --no-frozen-lockfile
- 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 }}
+38
View File
@@ -0,0 +1,38 @@
name: Test end-to-end - Python
on:
pull_request:
paths:
- "py/**"
env:
UV_VERSION: "0.7.20"
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
jobs:
test_e2e:
runs-on: ubuntu-latest
strategy:
# You can use PyPy versions in python-version.
# For example, pypy-2.7 and pypy-3.8
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: ${{ env.UV_VERSION }}
- name: Set up Python
run: uv python install ${{ matrix.python-version }} && uv python pin ${{ matrix.python-version }}
- name: Run Tests
working-directory: py
run: uv run pytest unit_tests/ tests/ -v
- name: Remove virtual environment
working-directory: py
run: rm -rf .venv/
+42
View File
@@ -0,0 +1,42 @@
name: Test - Python
on:
push:
branches:
- main
paths:
- "py/**"
pull_request:
paths:
- "py/**"
env:
UV_VERSION: "0.7.20"
jobs:
test:
runs-on: ubuntu-latest
strategy:
# You can use PyPy versions in python-version.
# For example, pypy-2.7 and pypy-3.8
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: ${{ env.UV_VERSION }}
- name: Set up Python
run: uv python install ${{ matrix.python-version }} && uv python pin ${{ matrix.python-version }}
- name: Run Tests
working-directory: py
run: uv run pytest unit_tests/ -v
- name: Remove virtual environment
working-directory: py
run: rm -rf .venv/
+37
View File
@@ -0,0 +1,37 @@
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
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
jobs:
test:
name: Test - TypeScript
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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
working-directory: ts/llama_cloud_services/
run: pnpm install --no-frozen-lockfile
- name: Run tests
working-directory: ts/llama_cloud_services/
run: pnpm test
-40
View File
@@ -1,40 +0,0 @@
name: Unit Testing
on:
push:
branches:
- main
pull_request:
env:
POETRY_VERSION: "1.6.1"
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
jobs:
test:
runs-on: ubuntu-latest
strategy:
# You can use PyPy versions in python-version.
# For example, pypy-2.7 and pypy-3.8
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: ${{ env.POETRY_VERSION }}
- name: Install deps
shell: bash
run: poetry install --with dev
- name: Run testing
env:
CI: true
shell: bash
run: poetry run pytest tests
+4
View File
@@ -5,3 +5,7 @@ __pycache__/
.idea
.env*
.ipynb_checkpoints*
*_cache/
node_modules/
.turbo/
dist/
+6 -6
View File
@@ -21,19 +21,19 @@ repos:
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
exclude: ".*poetry.lock"
exclude: ".*uv.lock"
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.10.1
hooks:
- id: black-jupyter
name: black-src
alias: black
exclude: ".*poetry.lock"
exclude: ".*uv.lock"
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.0.1
hooks:
- id: mypy
exclude: ^tests/
exclude: ^py/tests|^py/unit_tests
additional_dependencies:
[
"types-requests",
@@ -63,13 +63,13 @@ repos:
rev: v3.0.3
hooks:
- id: prettier
exclude: poetry.lock
exclude: ^(uv.lock|ts/llama_cloud_services/pnpm-lock.yaml)
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
additional_dependencies: [tomli]
exclude: ^(poetry.lock|examples)
exclude: ^(uv.lock|docs|ts|examples)
args:
[
"--ignore-words-list",
@@ -84,6 +84,6 @@ repos:
rev: v0.23.1
hooks:
- id: toml-sort-fix
exclude: ".*poetry.lock"
exclude: ".*uv.lock"
exclude: .github/ISSUE_TEMPLATE
+33
View File
@@ -0,0 +1,33 @@
# Python
## Installation
This project uses uv. Create a virtual environment, and run `uv sync`
## Versioning (Maintainers only)
Before merging your changes, make sure to bump the versions.
Make a version bump to `pyproject.toml`. If the underlying dependency on the llamacloud platform OpenAPI
sdk needs bumping, make sure to bring that in as well. If updating dependencies, run `uv lock`.
The legacy `llama_parse` package re-exports some of `llama_cloud_services` in the old namespace. The
versions need to be kept consistent to sidecar it with `llama_cloud_services`. Bump it's version in `llama_parse/pyproject.toml`, and also bump it's dependency version of `llama-cloud-services` to match.
**Note**: Don't worry about updating the `llama_parse/poetry.lock` file when bumping versions. The GitHub action will automatically run `poetry lock` for the llama_parse package during the build process (though it doesn't commit the updated lockfile back to the repo).
You can also do this with `./scripts/version-bump.py set 0.x.x` if you have `uv` installed.
Once the change is merged, push a tag `git tag -a v0.x.x -m 0.x.x` and `git push origin 0.x.x`.
This tagging step can be done with `./scripts/version-bump tag`.
# Typescript
## Installation
...
## Versioning
...
+19 -3
View File
@@ -10,7 +10,8 @@ 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 (beta/invite-only)](./extract.md) - A prebuilt agentic data extractor that can be used to transform data into a structured JSON representation.
- [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.
## Getting Started
@@ -25,18 +26,27 @@ Then, get your API key from [LlamaCloud](https://cloud.llamaindex.ai/).
Then, you can use the services in your code:
```python
from llama_cloud_services import LlamaParse, LlamaReport, LlamaExtract
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"
)
```
See the quickstart guides for each service for more information:
- [LlamaParse](./parse.md)
- [LlamaReport (beta/invite-only)](./report.md)
- [LlamaExtract (beta/invite-only)](./extract.md)
- [LlamaExtract](./extract.md)
- [LlamaCloud Index](./index.md)
## Switch to EU SaaS 🇪🇺
@@ -55,6 +65,12 @@ from llama_cloud_services import (
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",
project_name="default",
api_key="YOUR_API_KEY",
base_url=EU_BASE_URL,
)
```
## Documentation
+11
View File
@@ -0,0 +1,11 @@
# LlamaCloud Services Examples
In this folder you will find several python notebooks and two end-to-end typescript applications that contain examples regarding:
- [LlamaParse - Python](./parse/)
- [LlamaParse - TypeScript](./parse-ts/)
- [LlamaExtract - Python](./extract/)
- [LlamaReport - Python](./report/)
- [LlamaCloud Index - TypeScript](./index-ts/)
Follow the instructions of each notebook/application to get started!
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

@@ -0,0 +1 @@
sec_form_4_dump.json
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

@@ -0,0 +1,440 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Extract Data from Financial Reports - with Citations and Reasoning\n",
"\n",
"Given complex files like financial reports, contracts, invoices etc, Llama Extract allows you to make use of an LLM to extract the information relevant to you, in a structured format.\n",
"\n",
"In this example, we'll be using [LlamaExtract](https://docs.cloud.llamaindex.ai/llamaextract/getting_started?utm_campaign=extract&utm_medium=recipe) to extract structured data from an SEC filing (specifically, the filing by Nvidia for fiscal year 2025).\n",
"\n",
"On top of simple data extraction, we'll ask our extraction agent to provide citations and reasoning for each extracted field. This allows us to:\n",
"- Confirm the accuracy of the extracted field\n",
"- Understand the reasoning behind why the LLM extracted a given piece of information\n",
"- This last point allows us an opportunity to adjust the system prompt or field descriptions and improve on results where needed.\n",
"\n",
"\n",
"The example we go through below is also replicable within Llama Cloud as well, where you will also be able to pick between a number of pre-defined schemas, instead of building your own."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!pip install llama-cloud-services"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Connect to Llama Cloud\n",
"\n",
"To get started, make sure you provide your [Llama Cloud](https://cloud.llamaindex.ai?utm_campaign=extract&utm_medium=recipe) API key."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Enter your Llama Cloud API Key: ··········\n"
]
}
],
"source": [
"import os\n",
"from getpass import getpass\n",
"\n",
"if \"LLAMA_CLOUD_API_KEY\" not in os.environ:\n",
" os.environ[\"LLAMA_CLOUD_API_KEY\"] = getpass(\"Enter your Llama Cloud API Key: \")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Extract Data with Llama Extract Agent"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"No project_id provided, fetching default project.\n"
]
}
],
"source": [
"from llama_cloud_services import LlamaExtract\n",
"\n",
"# Optionally, provide your project id, if not, it will use the 'Default' project\n",
"llama_extract = LlamaExtract()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Provide Your Custom Schema\n",
"\n",
"When using LlamaExtract via the API, you provide your own schema that describes what you want extracted from files and data provided to your agent. Here, we are essentially building an SEC filings extraction agent."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pydantic import BaseModel, Field\n",
"from enum import Enum\n",
"\n",
"\n",
"class FilingType(str, Enum):\n",
" ten_k = \"10 K\"\n",
" ten_q = \"10-Q\"\n",
" ten_ka = \"10-K/A\"\n",
" ten_qa = \"10-Q/A\"\n",
"\n",
"\n",
"class FinancialReport(BaseModel):\n",
" company_name: str = Field(description=\"The name of the company\")\n",
" description: str = Field(\n",
" description=\"Short description of the filing and what it contains\"\n",
" )\n",
" filing_type: FilingType = Field(description=\"Type of SEC filing\")\n",
" filing_date: str = Field(description=\"Date when filing was submitted to SEC\")\n",
" fiscal_year: int = Field(description=\"Fiscal year\")\n",
" unit: str = Field(\n",
" description=\"Unit of financial figures (thousands, millions, etc.)\"\n",
" )\n",
" revenue: int = Field(description=\"Total revenue for period\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Set Up Citations and Reasoning\n",
"\n",
"Optionally, we can set the `ExtractConfig` to extract citations for each field the agent extracts. These cications will cite the specific pages and sections of the file from which a given field was extractedd.\n",
"\n",
"By setting `use_reasoning` to True, we als ask the agent to do an additional reasoning step, explaining why a given field was extracted."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from llama_cloud.types import ExtractConfig, ExtractMode\n",
"\n",
"config = ExtractConfig(\n",
" use_reasoning=True, cite_sources=True, extraction_mode=ExtractMode.MULTIMODAL\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/usr/local/lib/python3.11/dist-packages/llama_cloud_services/extract/extract.py:127: ExperimentalWarning: `use_reasoning` is an experimental feature. Results will be available in the `extraction_metadata` field for the extraction run.\n",
" warnings.warn(\n",
"/usr/local/lib/python3.11/dist-packages/llama_cloud_services/extract/extract.py:133: ExperimentalWarning: `cite_sources` is an experimental feature. This may greatly increase the size of the response, and slow down the extraction. Results will be available in the `extraction_metadata` field for the extraction run.\n",
" warnings.warn(\n"
]
}
],
"source": [
"agent = llama_extract.create_agent(\n",
" name=\"filing-parser\", data_schema=FinancialReport, config=config\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Demo Time - Download a PDF and Extract Data with Citations"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"PDF downloaded successfully.\n"
]
}
],
"source": [
"import requests\n",
"\n",
"url = \"https://raw.githubusercontent.com/run-llama/llama_cloud_services/refs/heads/main/examples/extract/data/sec_filings/nvda_10k.pdf\"\n",
"\n",
"response = requests.get(url)\n",
"\n",
"if response.status_code == 200:\n",
" with open(\"/content/nvda_10k.pdf\", \"wb\") as f:\n",
" f.write(response.content)\n",
" print(\"PDF downloaded successfully.\")\n",
"else:\n",
" print(f\"Failed to download. Status code: {response.status_code}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Uploading files: 100%|██████████| 1/1 [00:00<00:00, 1.83it/s]\n",
"Creating extraction jobs: 100%|██████████| 1/1 [00:00<00:00, 4.38it/s]\n",
"Extracting files: 100%|██████████| 1/1 [02:03<00:00, 123.40s/it]\n"
]
}
],
"source": [
"filing_info = agent.extract(\"/content/nvda_10k.pdf\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'company_name': 'NVIDIA Corporation',\n",
" 'description': \"The filing provides a detailed overview of NVIDIA's business as a full-stack computing infrastructure company, discusses various technologies including digital avatars and autonomous vehicles, outlines numerous risk factors affecting operations such as supply chain issues and geopolitical tensions, and describes employee stock purchase plans and related compliance requirements.\",\n",
" 'filing_type': '10 K',\n",
" 'filing_date': 'February 26, 2025',\n",
" 'fiscal_year': 2025,\n",
" 'unit': 'millions',\n",
" 'revenue': 130497}"
]
},
"execution_count": null,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"filing_info.data"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Inspect Citations and Reasoning"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'field_metadata': {'company_name': {'reasoning': 'VERBATIM EXTRACTION',\n",
" 'citation': [{'page': 1, 'matching_text': 'NVIDIA CORPORATION'},\n",
" {'page': 2, 'matching_text': 'NVIDIA Corporation'},\n",
" {'page': 3,\n",
" 'matching_text': 'All references to \"NVIDIA,\" \"we,\" \"us,\" \"our,\" or the \"Company\" mean NVIDIA Corporation and its subsidiaries.'},\n",
" {'page': 35,\n",
" 'matching_text': 'Comparison of 5 Year Cumulative Total Return* Among NVIDIA Corporation'},\n",
" {'page': 49,\n",
" 'matching_text': 'To the Board of Directors and Shareholders of NVIDIA Corporation'},\n",
" {'page': 90, 'matching_text': 'NVIDIA Corporation'},\n",
" {'page': 119,\n",
" 'matching_text': '*\"Company\"* means NVIDIA Corporation, a Delaware corporation.'},\n",
" {'page': 126,\n",
" 'matching_text': 'Annual Report on Form 10-K of NVIDIA Corporation'}]},\n",
" 'filing_type': {'reasoning': \"VERBATIM EXTRACTION from multiple sources confirming the filing type as '10 K'.\",\n",
" 'citation': [{'page': 1, 'matching_text': 'FORM 10-K'},\n",
" {'page': 2, 'matching_text': 'Item 16. | Form 10-K Summary'},\n",
" {'page': 3,\n",
" 'matching_text': 'This Annual Report on Form 10-K contains forward-looking statements...'},\n",
" {'page': 13, 'matching_text': 'this Annual Report on Form 10-K'},\n",
" {'page': 15, 'matching_text': 'this Annual Report on Form 10-K'},\n",
" {'page': 32,\n",
" 'matching_text': 'Annual Report on Form 10-K, which information is hereby incorporated by reference.'},\n",
" {'page': 36, 'matching_text': 'this Annual Report on Form 10-K'},\n",
" {'page': 43,\n",
" 'matching_text': 'Annual Report on Form 10-K for additional information'},\n",
" {'page': 45, 'matching_text': 'Annual Report on Form 10-K'},\n",
" {'page': 46, 'matching_text': 'this Annual Report on Form 10-K'},\n",
" {'page': 62, 'matching_text': 'Annual Report on Form 10-K'},\n",
" {'page': 83,\n",
" 'matching_text': 'Restated Certificate of Incorporation | 10-K'},\n",
" {'page': 84, 'matching_text': 'Item 16. Form 10-K Summary'},\n",
" {'page': 126, 'matching_text': 'which appears in this Form 10-K'},\n",
" {'page': 127, 'matching_text': 'Annual Report on Form 10-K'},\n",
" {'page': 128, 'matching_text': 'Annual Report on Form 10-K'},\n",
" {'page': 129, 'matching_text': \"The Company's Annual Report on Form 10-K\"},\n",
" {'page': 130,\n",
" 'matching_text': \"The Company's Annual Report on Form 10-K for the year ended January 26, 2025\"}]},\n",
" 'fiscal_year': {'reasoning': 'The fiscal year ended January 26, 2025, indicates the fiscal year is 2025. Additionally, multiple references throughout the text confirm the fiscal year 2025 in various contexts.',\n",
" 'citation': [{'page': 1,\n",
" 'matching_text': 'For the fiscal year ended January 26, 2025'},\n",
" {'page': 6,\n",
" 'matching_text': 'In fiscal year 2025, we launched the NVIDIA Blackwell architecture'},\n",
" {'page': 12, 'matching_text': 'fiscal year 2025'},\n",
" {'page': 17,\n",
" 'matching_text': 'our gross margins in the second quarter of fiscal year 2025 were negatively impacted'},\n",
" {'page': 20,\n",
" 'matching_text': 'we generated 53% of our revenue in fiscal year 2025 from sales outside the United States.'},\n",
" {'page': 23,\n",
" 'matching_text': 'For fiscal year 2025, an indirect customer which primarily purchases our products through system integrators...'},\n",
" {'page': 33,\n",
" 'matching_text': 'In fiscal year 2025, we repurchased 310 million shares of our common stock for $34.0 billion.'},\n",
" {'page': 37,\n",
" 'matching_text': 'Our Data Center revenue in China grew in fiscal year 2025.'},\n",
" {'page': 44,\n",
" 'matching_text': 'Cash provided by operating activities increased in fiscal year 2025 compared to fiscal year 2024'},\n",
" {'page': 57,\n",
" 'matching_text': 'Fiscal years 2025, 2024 and 2023 were all 52-week years.'},\n",
" {'page': 65,\n",
" 'matching_text': 'Beginning in the second quarter of fiscal year 2025'},\n",
" {'page': 69, 'matching_text': 'In the fourth quarter of fiscal year 2025'},\n",
" {'page': 78,\n",
" 'matching_text': 'Depreciation and amortization expense attributable to our Compute and Networking segment for fiscal years 2025'},\n",
" {'page': 129, 'matching_text': 'for the year ended January 26, 2025'}]},\n",
" 'description': {'reasoning': 'The extracted data combines multiple descriptions from the source text, ensuring no duplication while maintaining the order and context of the information. Each section of the filing is summarized to reflect the key points without losing the essence of the original text.',\n",
" 'citation': [{'page': 4,\n",
" 'matching_text': 'NVIDIA is now a full-stack computing infrastructure company with data-center-scale offerings that are reshaping industry.'},\n",
" {'page': 8,\n",
" 'matching_text': 'a suite of technologies that help developers bring digital avatars to life with generative Al...autonomous vehicles, or AV, and electric vehicles, or EV, is revolutionizing the transportation industry...Our worldwide sales and marketing strategy is key to achieving our objective of providing markets with our high-performance and efficient computing platforms and software.'},\n",
" {'page': 14, 'matching_text': 'Risk Factors Summary'},\n",
" {'page': 16,\n",
" 'matching_text': 'Risks Related to Demand, Supply, and Manufacturing\\n\\nLong manufacturing lead times and uncertain supply and component availability...'},\n",
" {'page': 18,\n",
" 'matching_text': 'cryptocurrency mining, on demand for our products. Volatility in the cryptocurrency market, including new compute technologies...'},\n",
" {'page': 21,\n",
" 'matching_text': 'supply-chain attacks or other business disruptions. We cannot guarantee that third parties and infrastructure in our supply chain...'},\n",
" {'page': 22,\n",
" 'matching_text': 'We are monitoring the impact of the geopolitical conflict in and around Israel on our operations... Climate change may have a long-term impact on our business.'},\n",
" {'page': 25,\n",
" 'matching_text': 'We are subject to complex laws, rules, regulations, and political and other actions, including restrictions on the export of our products, which may adversely impact our business.'},\n",
" {'page': 28,\n",
" 'matching_text': 'Our competitive position has been harmed by the existing export controls, and our competitive position and future results may be further harmed'},\n",
" {'page': 29,\n",
" 'matching_text': 'restrictions imposed by the Chinese government on the duration of gaming activities and access to games may adversely affect our Gaming revenue'},\n",
" {'page': 29,\n",
" 'matching_text': 'our business depends on our ability to receive consistent and reliable supply from our overseas partners, especially in Taiwan and South Korea'},\n",
" {'page': 29,\n",
" 'matching_text': 'Increased scrutiny from shareholders, regulators and others regarding our corporate sustainability practices could result in additional costs'},\n",
" {'page': 29,\n",
" 'matching_text': 'Concerns relating to the responsible use of new and evolving technologies, such as Al, in our products and services may result in reputational or financial harm'},\n",
" {'page': 31,\n",
" 'matching_text': 'Data protection laws around the world are quickly changing and may be interpreted and applied in an increasingly stringent fashion...'}]},\n",
" 'filing_date': {'reasoning': 'The filing date is consistently mentioned as February 26, 2025 across multiple entries, making it the most reliable date for the filing.',\n",
" 'citation': [{'page': 51, 'matching_text': 'February 26, 2025'},\n",
" {'page': 86, 'matching_text': 'on February 26, 2025.'},\n",
" {'page': 87, 'matching_text': 'February 26, 2025'},\n",
" {'page': 126, 'matching_text': 'our report dated February 26, 2025'},\n",
" {'page': 127, 'matching_text': 'Date: February 26, 2025'},\n",
" {'page': 128, 'matching_text': 'Date: February 26, 2025'},\n",
" {'page': 129, 'matching_text': 'Date: February 26, 2025'},\n",
" {'page': 130, 'matching_text': 'Date: February 26, 2025'}]},\n",
" 'unit': {'reasoning': \"The unit of financial figures is explicitly mentioned multiple times in the text as 'millions', including in table headers and notes. This is confirmed by various citations from pages 38, 42, 43, 52, 53, 54, 56, 65, 71, 72, 73, 75, 77, 79, 80, and 82.\",\n",
" 'citation': [{'page': 38,\n",
" 'matching_text': '($ in millions, except per share data)'},\n",
" {'page': 42, 'matching_text': '($ in millions)'},\n",
" {'page': 43, 'matching_text': '($ in millions)'},\n",
" {'page': 52, 'matching_text': '(In millions, except per share data)'},\n",
" {'page': 53,\n",
" 'matching_text': 'Consolidated Statements of Comprehensive Income (In millions)'},\n",
" {'page': 54,\n",
" 'matching_text': 'Consolidated Balance Sheets (In millions, except par value)'},\n",
" {'page': 55, 'matching_text': '(In millions, except per share data)'},\n",
" {'page': 56,\n",
" 'matching_text': 'Consolidated Statements of Cash Flows (In millions)'},\n",
" {'page': 65,\n",
" 'matching_text': 'Year Ended<br/>Jan 26, 2025<br/>(In millions, except per share data)'},\n",
" {'page': 71, 'matching_text': '(In millions) | (In millions)'},\n",
" {'page': 72, 'matching_text': '(In millions)'}]},\n",
" 'revenue': {'reasoning': 'The total revenue for fiscal year 2025 is extracted from multiple sources within the text, all confirming the same figure of $130,497 million. The revenue recognized for fiscal year 2025 is also noted as $4,607 million, which is a separate figure. However, the primary focus is on the total revenue figure, which is consistently cited.',\n",
" 'citation': [{'page': 38,\n",
" 'matching_text': 'Revenue for fiscal year 2025 was $130.5 billion'},\n",
" {'page': 41,\n",
" 'matching_text': 'Total | $ 130,497 | $ | 60,922'},\n",
" {'page': 52, 'matching_text': 'Revenue | $ 130,497'},\n",
" {'page': 78,\n",
" 'matching_text': 'Revenue | $ 116,193 | $ 14,304 | $ - | $ 130,497'},\n",
" {'page': 79, 'matching_text': 'Total revenue | $ 130,497'},\n",
" {'page': 80, 'matching_text': 'Total revenue | $ 130,497'}]}},\n",
" 'usage': {'num_pages_extracted': 130,\n",
" 'num_document_tokens': 105932,\n",
" 'num_output_tokens': 31306}}"
]
},
"execution_count": null,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"filing_info.extraction_metadata"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## What's Next?\n",
"\n",
"In this example, we built an Extraction Agent that is capable of citing it's sources from the document it's extracting data from, and reasoning about its reponse. To further customize and improve on the results, you can also try to customize the `system_prompt` in the `ExtractConfig`.\n",
"\n",
"#### Learn More\n",
"\n",
"- [LlamaExtract Documentation](https://docs.cloud.llamaindex.ai/llamaextract/getting_started)\n",
"- [Example Notebooks](https://github.com/run-llama/llama_cloud_services/tree/main/examples/extract)"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
File diff suppressed because it is too large Load Diff
+131
View File
@@ -0,0 +1,131 @@
# LlamaCloud Index Demo
A TypeScript demo application showcasing the power of **LlamaCloud Index** - a fully automated document ingestion and retrieval serviced offered within [LlamaCloud](https://cloud.llamaindex.ai). This demo allows you to ask questions, retrieve relevant contextual information and generate AI-powered responses using OpenAI's GPT models.
## Table of Contents
- [Features](#features)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Usage](#usage)
- [Start the Demo](#start-the-demo)
- [Development Mode](#development-mode)
- [Build the Project](#build-the-project)
- [Code Quality](#code-quality)
- [Quick Commands Reference](#quick-commands-reference)
- [How It Works](#how-it-works)
- [API Dependencies](#api-dependencies)
- [Troubleshooting](#troubleshooting)
- [Common Issues](#common-issues)
- [License](#license)
- [Contributing](#contributing)
## Features
- 🤖 **RAG**: Simple-yet-effective Retrieval Augmented Generation pipeline built on top of LlamaCloud Index and OpenAI
- 🎨 **Beautiful CLI**: Styled console interface with colors and ASCII art
-**Fast Development**: Hot reload support with watch mode
- 🛠️ **TypeScript**: Full TypeScript support with strict type checking
## Prerequisites
- Node.js (version 18 or higher)
- pnpm package manager
- OpenAI API key
- LlamaCloud API key
- An existing LlamaCloud Index pipeline
## Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd llamaparse-demo
```
2. Install dependencies:
```bash
pnpm install
```
3. Set up your environment variables:
```bash
export OPENAI_API_KEY="your-openai-api-key"
export LLAMA_CLOUD_API_KEY="your-llamacloud-api-key"
export PIPELINE_NAME="your-pipeline-name"
```
4. Or write them into a `.env` file:
```env
OPENAI_API_KEY="your-openai-api-key"
LLAMA_CLOUD_API_KEY="your-llamacloud-api-key"
PIPELINE_NAME="your-pipeline-name"
```
## Usage
### Start the Demo
```bash
pnpm run start
```
The application will display a welcome screen and prompt you to start chatting!
### Development Mode
For development with hot reload:
```bash
pnpm run dev
```
### Build the Project
```bash
pnpm run build
```
### Code Quality
Format code:
```bash
pnpm run format
```
Lint code:
```bash
pnpm run lint
```
## How It Works
1. **Message Input**: Enter a message
2. **Retrieval**: Several nodes are retrieved from the LlamaCloud index you specified
3. **AI Response Generation**: The retrieved information is passed on to the AI model, along with its relevance score, and a reply to your original message is generated starting from that.
4. **Results**: View the AI-generated summary in your terminal
## Troubleshooting
### Common Issues
1. **Module Resolution Errors**: Ensure you're using Node.js 18+ and have all dependencies installed
2. **API Key Issues**: Verify your OpenAI and LlamaCloud API keys are correctly set
## License
MIT License - see the [LICENSE](../../../LICENSE) file for details.
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Run `pnpm format` and `pnpm lint`
5. Submit a pull request
+15
View File
@@ -0,0 +1,15 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.browser },
},
{ files: ["**/*.js"], languageOptions: { sourceType: "script" } },
tseslint.configs.recommended,
]);
+48
View File
@@ -0,0 +1,48 @@
{
"name": "llama-chat",
"version": "0.1.0",
"description": "Demo for LlamaCloud Index in TypeScript",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"There are no tests\"",
"start": "pnpm exec tsx src/index.ts",
"lint": "eslint ./src/",
"format": "prettier --write ./src/",
"build": "tsc",
"dev": "pnpm exec tsx --watch src/index.ts"
},
"keywords": [
"ai",
"rag",
"retrieval",
"pipeline",
"llms",
"chatbot"
],
"author": "LlamaIndex",
"license": "MIT",
"packageManager": "pnpm@10.12.4",
"devDependencies": {
"@eslint/js": "^9.32.0",
"@types/figlet": "^1.7.0",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"eslint": "^9.32.0",
"globals": "^16.3.0",
"jiti": "^2.5.1",
"prettier": "^3.6.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0"
},
"dependencies": {
"@ai-sdk/openai": "^1.3.23",
"ai": "^4.3.19",
"consola": "^3.4.2",
"dotenv": "^17.2.1",
"figlet": "^1.8.2",
"llama-cloud-services": "link:../../../ts/llama_cloud_services",
"picocolors": "^1.1.1"
}
}
+1770
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
import { LlamaCloudIndex } from "llama-cloud-services";
import { logger } from "./logger";
import pc from "picocolors";
import {
consoleInput,
retrievalAugmentedGeneration,
renderLogo,
} from "./utils";
import dotenv from "dotenv";
dotenv.config();
export async function main(): Promise<number> {
const index = new LlamaCloudIndex({
name: process.env.PIPELINE_NAME as string,
projectName: "Default",
apiKey: process.env.LLAMA_CLOUD_API_KEY, // can provide API-key in the constructor or in the env
});
const retriever = index.asRetriever({
similarityTopK: 5,
});
await renderLogo();
logger.log(
`Welcome to ${pc.bold(
pc.magentaBright("✨LlamaChat✨"),
)}, our demo for ${pc.bold(pc.green("Index🦙"))}, a ${pc.bold(
pc.cyan("LlamaCloud☁️"),
)} (https://cloud.llamaindex.ai) product!.\nType a question below, and you will get an answer!👇\nIf you wish to exit, just type ${pc.bold(
pc.gray("quit"),
)}.\n`,
);
while (true) {
const userInput = await consoleInput();
if (userInput.toLowerCase() == "quit") {
break;
}
try {
const nodes = await retriever.retrieve(userInput);
const summary = await retrievalAugmentedGeneration(nodes, userInput);
logger.log(`${pc.bold(pc.magentaBright("LlamaChat✨:"))}\n${summary}`);
} catch (error) {
logger.error(`Error processing your request: ${error}`);
}
}
return 0;
}
main().catch(console.error);
+8
View File
@@ -0,0 +1,8 @@
import { createConsola } from "consola";
import type { ConsolaInstance } from "consola";
export const logger: ConsolaInstance = createConsola({
formatOptions: {
date: false,
},
});
+56
View File
@@ -0,0 +1,56 @@
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { NodeWithScore, MetadataMode } from "llamaindex";
import * as readline from "readline/promises";
import figlet from "figlet";
import pc from "picocolors";
export async function renderLogo(): Promise<void> {
const logoText = figlet.textSync("LlamaChat", {
font: "ANSI Shadow",
horizontalLayout: "default",
verticalLayout: "default",
width: 100,
whitespaceBreak: true,
});
// Add some styling with picocolors
const styledLogo = pc.bold(pc.yellowBright(logoText));
// Add some padding/margin
console.log("\n");
console.log(styledLogo);
console.log(pc.gray("─".repeat(60)));
console.log("\n");
}
export async function consoleInput(): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await rl.question(pc.cyanBright("You✨:"));
rl.close();
return answer;
}
export async function retrievalAugmentedGeneration(
nodes: NodeWithScore[],
prompt: string,
): Promise<string> {
let mainText: string = "";
for (const node of nodes) {
mainText += `\t{information: '${node.node.getContent(
MetadataMode.ALL,
)}', relevanceScore: '${node.score ?? "no score"}'}\n`;
}
const { text } = await generateText({
model: openai("gpt-4.1"),
prompt: `[\n${mainText}\n]\n\nBased on the information you are given and on the relevance score of that (where -1 means no score available), answer to this user prompt: '${prompt}'`,
});
return text;
}
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"],
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+124
View File
@@ -0,0 +1,124 @@
# LlamaParse Demo
A TypeScript demo application showcasing the power of **LlamaParse** - an intelligent document parsing service from [LlamaCloud](https://cloud.llamaindex.ai). This demo allows you to parse various document formats and generate AI-powered summaries using OpenAI's GPT models.
## Table of Contents
- [Features](#features)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Usage](#usage)
- [Start the Demo](#start-the-demo)
- [Development Mode](#development-mode)
- [Build the Project](#build-the-project)
- [Code Quality](#code-quality)
- [Quick Commands Reference](#quick-commands-reference)
- [How It Works](#how-it-works)
- [API Dependencies](#api-dependencies)
- [Troubleshooting](#troubleshooting)
- [Common Issues](#common-issues)
- [License](#license)
- [Contributing](#contributing)
## Features
- 📄 **Document Parsing**: Parse PDFs, Word docs, and other formats using LlamaParse
- 🤖 **AI Summaries**: Generate intelligent summaries using OpenAI GPT-4
- 🎨 **Beautiful CLI**: Styled console interface with colors and ASCII art
-**Fast Development**: Hot reload support with watch mode
- 🛠️ **TypeScript**: Full TypeScript support with strict type checking
## Prerequisites
- Node.js (version 18 or higher)
- pnpm package manager
- OpenAI API key
- LlamaCloud API key
## Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd llamaparse-demo
```
2. Install dependencies:
```bash
pnpm install
```
3. Set up your environment variables:
```bash
# Add your API keys to your environment
export OPENAI_API_KEY="your-openai-api-key"
export LLAMA_CLOUD_API_KEY="your-llamacloud-api-key"
```
## Usage
### Start the Demo
```bash
pnpm run start
```
The application will display a welcome screen and prompt you to enter the path to a document you'd like to process.
### Development Mode
For development with hot reload:
```bash
pnpm run dev
```
### Build the Project
```bash
pnpm run build
```
### Code Quality
Format code:
```bash
pnpm run format
```
Lint code:
```bash
pnpm run lint
```
## How It Works
1. **Document Input**: Enter the path to your document when prompted
2. **Parsing**: LlamaParse processes the document and extracts structured content
3. **AI Summary**: The extracted content is sent to OpenAI GPT-4 for summarization
4. **Results**: View the AI-generated summary in your terminal
## Troubleshooting
### Common Issues
1. **Module Resolution Errors**: Ensure you're using Node.js 18+ and have all dependencies installed
2. **API Key Issues**: Verify your OpenAI and LlamaCloud API keys are correctly set
3. **File Path Errors**: Use absolute paths or ensure relative paths are correct from the project root
## License
MIT License - see the [LICENSE](../../../LICENSE) file for details.
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Run `pnpm format` and `pnpm lint`
5. Submit a pull request
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import { defineConfig } from "eslint/config";
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.browser },
},
{ files: ["**/*.js"], languageOptions: { sourceType: "script" } },
tseslint.configs.recommended,
]);
+47
View File
@@ -0,0 +1,47 @@
{
"name": "llamaparse-demo",
"version": "0.1.0",
"description": "Demo for LlamaParse in TypeScript",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"There are no tests\"",
"start": "pnpm exec tsx src/index.ts",
"lint": "eslint ./src/",
"format": "prettier --write ./src/",
"build": "tsc",
"dev": "pnpm exec tsx --watch src/index.ts"
},
"keywords": [
"ai",
"ocr",
"parsing",
"intelligent-document-processing",
"pdf",
"llms"
],
"author": "LlamaIndex",
"license": "MIT",
"packageManager": "pnpm@10.12.4",
"devDependencies": {
"@eslint/js": "^9.32.0",
"@types/figlet": "^1.7.0",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"eslint": "^9.32.0",
"globals": "^16.3.0",
"jiti": "^2.5.1",
"prettier": "^3.6.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0"
},
"dependencies": {
"@ai-sdk/openai": "^1.3.23",
"ai": "^4.3.19",
"consola": "^3.4.2",
"figlet": "^1.8.2",
"llama-cloud-services": "link:../../../ts/llama_cloud_services",
"picocolors": "^1.1.1"
}
}
+1758
View File
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
import { LlamaParseReader } from "llama-cloud-services";
import { logger } from "./logger";
import pc from "picocolors";
import { consoleInput, generateSummary, renderLogo } from "./utils";
export async function main(): Promise<number> {
const reader = new LlamaParseReader({ resultType: "markdown" });
await renderLogo();
logger.log(
`Welcome to ${pc.bold(
pc.magentaBright("✨LlamaParse Demo✨"),
)}, our demo for ${pc.bold(pc.green("LlamaParse🦙"))}, a ${pc.bold(
pc.cyan("LlamaCloud☁️"),
)} (https://cloud.llamaindex.ai) product!.\nType the path to the document you would like to process below👇\nIf you wish to exit, just type ${pc.bold(
pc.gray("quit"),
)}.\n`,
);
while (true) {
const userInput = await consoleInput();
if (userInput.toLowerCase() == "quit") {
break;
}
try {
const documents = await reader.loadData(userInput);
const summary = await generateSummary(documents); // Added await here
logger.log(`${pc.bold(pc.cyan("AI-generated summary✨"))}:\n${summary}`);
} catch (error) {
logger.error(`Error processing file: ${error}`);
}
}
return 0;
}
main().catch(console.error);
+8
View File
@@ -0,0 +1,8 @@
import { createConsola } from "consola";
import type { ConsolaInstance } from "consola";
export const logger: ConsolaInstance = createConsola({
formatOptions: {
date: false,
},
});
+51
View File
@@ -0,0 +1,51 @@
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { Document } from "llamaindex";
import * as readline from "readline/promises";
import figlet from "figlet";
import pc from "picocolors";
export async function renderLogo(): Promise<void> {
const logoText = figlet.textSync("LlamaParse Demo", {
font: "ANSI Shadow",
horizontalLayout: "default",
verticalLayout: "default",
width: 100,
whitespaceBreak: true,
});
// Add some styling with picocolors
const styledLogo = pc.bold(pc.magentaBright(logoText));
// Add some padding/margin
console.log("\n");
console.log(styledLogo);
console.log(pc.gray("─".repeat(60)));
console.log("\n");
}
export async function consoleInput(): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await rl.question("Path to your file: ");
rl.close();
return answer;
}
export async function generateSummary(documents: Document[]): Promise<string> {
let mainText: string = "";
for (const document of documents) {
mainText += `${document.text}\n\n---\n\n`;
}
const { text } = await generateText({
model: openai("gpt-4.1"),
prompt: `</chat>\n\t<text>${mainText}</text>\n\t<instructions>Could you please generate a summary of the given text?</instructions>\n</chat>`,
});
return text;
}
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"],
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+516
View File
@@ -0,0 +1,516 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Table Extraction with LlamaParse\n",
"\n",
"This notebook will show you how to extract tables and save them as CSV files thanks to LlamaParse advanced parsing capabilities."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**1. Install needed dependencies**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"! pip install llama-cloud-services pandas"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**2. Set you LLAMA_CLOUD_API_KEY as env variable**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"LLAMA_CLOUD_API_KEY: ··········\n"
]
}
],
"source": [
"import os\n",
"from getpass import getpass\n",
"\n",
"os.environ[\"LLAMA_CLOUD_API_KEY\"] = getpass(\"LLAMA_CLOUD_API_KEY: \")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**3. Initialiaze the parser**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from llama_cloud_services import LlamaParse\n",
"\n",
"parser = LlamaParse(result_type=\"markdown\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**4. Get data**\n",
"\n",
"This is a PDF with _lots_ of tables!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--2025-07-16 16:20:41-- https://assets.accessible-digital-documents.com/uploads/2017/01/sample-tables.pdf\n",
"Resolving assets.accessible-digital-documents.com (assets.accessible-digital-documents.com)... 3.166.135.2, 3.166.135.62, 3.166.135.51, ...\n",
"Connecting to assets.accessible-digital-documents.com (assets.accessible-digital-documents.com)|3.166.135.2|:443... connected.\n",
"HTTP request sent, awaiting response... 200 OK\n",
"Length: 145494 (142K) [application/pdf]\n",
"Saving to: sample-tables.pdf\n",
"\n",
"sample-tables.pdf 100%[===================>] 142.08K --.-KB/s in 0.04s \n",
"\n",
"2025-07-16 16:20:41 (3.72 MB/s) - sample-tables.pdf saved [145494/145494]\n",
"\n"
]
}
],
"source": [
"! wget https://assets.accessible-digital-documents.com/uploads/2017/01/sample-tables.pdf"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**5. Parse document**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Started parsing the file under job_id b53949f7-9017-4b6a-b30c-be6227271ed2\n"
]
}
],
"source": [
"json_result = parser.get_json_result(\"sample-tables.pdf\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**6. Get tables!**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"tables = parser.get_tables(json_result, \"tables/\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**7. Load tables**\n",
"\n",
"Let's show one example table!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.google.colaboratory.intrinsic+json": {
"summary": "{\n \"name\": \"display(df\",\n \"rows\": 8,\n \"fields\": [\n {\n \"column\": \"Rainfall\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Average\",\n \"\",\n \"24 hour high\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Americas\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 908,\n \"min\": 9,\n \"max\": 2010,\n \"num_unique_values\": 8,\n \"samples\": [\n 104,\n 133,\n 2010\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Asia\",\n \"properties\": {\n \"dtype\": \"object\",\n \"num_unique_values\": 7,\n \"samples\": [\n \"\",\n 201.0,\n 28.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Europe\",\n \"properties\": {\n \"dtype\": \"object\",\n \"num_unique_values\": 7,\n \"samples\": [\n \"\",\n 193.0,\n 29.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Africa\",\n \"properties\": {\n \"dtype\": \"object\",\n \"num_unique_values\": 7,\n \"samples\": [\n \"\",\n 144.0,\n 20.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}",
"type": "dataframe"
},
"text/html": [
"\n",
" <div id=\"df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb\" class=\"colab-df-container\">\n",
" <div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Rainfall</th>\n",
" <th>Americas</th>\n",
" <th>Asia</th>\n",
" <th>Europe</th>\n",
" <th>Africa</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>(inches)</td>\n",
" <td>2010</td>\n",
" <td></td>\n",
" <td></td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>Average</td>\n",
" <td>104</td>\n",
" <td>201.0</td>\n",
" <td>193.0</td>\n",
" <td>144.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>24 hour high</td>\n",
" <td>15</td>\n",
" <td>26.0</td>\n",
" <td>27.0</td>\n",
" <td>18.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>12 hour high</td>\n",
" <td>9</td>\n",
" <td>10.0</td>\n",
" <td>11.0</td>\n",
" <td>12.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td></td>\n",
" <td>2009</td>\n",
" <td></td>\n",
" <td></td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>Average</td>\n",
" <td>133</td>\n",
" <td>244.0</td>\n",
" <td>155.0</td>\n",
" <td>166.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>24 hour high</td>\n",
" <td>27</td>\n",
" <td>28.0</td>\n",
" <td>29.0</td>\n",
" <td>20.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>12 hour high</td>\n",
" <td>11</td>\n",
" <td>12.0</td>\n",
" <td>13.0</td>\n",
" <td>16.0</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>\n",
" <div class=\"colab-df-buttons\">\n",
"\n",
" <div class=\"colab-df-container\">\n",
" <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb')\"\n",
" title=\"Convert this dataframe to an interactive table.\"\n",
" style=\"display:none;\">\n",
"\n",
" <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
" <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
" </svg>\n",
" </button>\n",
"\n",
" <style>\n",
" .colab-df-container {\n",
" display:flex;\n",
" gap: 12px;\n",
" }\n",
"\n",
" .colab-df-convert {\n",
" background-color: #E8F0FE;\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: #1967D2;\n",
" height: 32px;\n",
" padding: 0 0 0 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-convert:hover {\n",
" background-color: #E2EBFA;\n",
" box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: #174EA6;\n",
" }\n",
"\n",
" .colab-df-buttons div {\n",
" margin-bottom: 4px;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert {\n",
" background-color: #3B4455;\n",
" fill: #D2E3FC;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert:hover {\n",
" background-color: #434B5C;\n",
" box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
" filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
" fill: #FFFFFF;\n",
" }\n",
" </style>\n",
"\n",
" <script>\n",
" const buttonEl =\n",
" document.querySelector('#df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb button.colab-df-convert');\n",
" buttonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
"\n",
" async function convertToInteractive(key) {\n",
" const element = document.querySelector('#df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb');\n",
" const dataTable =\n",
" await google.colab.kernel.invokeFunction('convertToInteractive',\n",
" [key], {});\n",
" if (!dataTable) return;\n",
"\n",
" const docLinkHtml = 'Like what you see? Visit the ' +\n",
" '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
" + ' to learn more about interactive tables.';\n",
" element.innerHTML = '';\n",
" dataTable['output_type'] = 'display_data';\n",
" await google.colab.output.renderOutput(dataTable, element);\n",
" const docLink = document.createElement('div');\n",
" docLink.innerHTML = docLinkHtml;\n",
" element.appendChild(docLink);\n",
" }\n",
" </script>\n",
" </div>\n",
"\n",
"\n",
" <div id=\"df-54b2aa43-838b-47d3-9209-2fb18153cf87\">\n",
" <button class=\"colab-df-quickchart\" onclick=\"quickchart('df-54b2aa43-838b-47d3-9209-2fb18153cf87')\"\n",
" title=\"Suggest charts\"\n",
" style=\"display:none;\">\n",
"\n",
"<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
" width=\"24px\">\n",
" <g>\n",
" <path d=\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z\"/>\n",
" </g>\n",
"</svg>\n",
" </button>\n",
"\n",
"<style>\n",
" .colab-df-quickchart {\n",
" --bg-color: #E8F0FE;\n",
" --fill-color: #1967D2;\n",
" --hover-bg-color: #E2EBFA;\n",
" --hover-fill-color: #174EA6;\n",
" --disabled-fill-color: #AAA;\n",
" --disabled-bg-color: #DDD;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-quickchart {\n",
" --bg-color: #3B4455;\n",
" --fill-color: #D2E3FC;\n",
" --hover-bg-color: #434B5C;\n",
" --hover-fill-color: #FFFFFF;\n",
" --disabled-bg-color: #3B4455;\n",
" --disabled-fill-color: #666;\n",
" }\n",
"\n",
" .colab-df-quickchart {\n",
" background-color: var(--bg-color);\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: var(--fill-color);\n",
" height: 32px;\n",
" padding: 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-quickchart:hover {\n",
" background-color: var(--hover-bg-color);\n",
" box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: var(--button-hover-fill-color);\n",
" }\n",
"\n",
" .colab-df-quickchart-complete:disabled,\n",
" .colab-df-quickchart-complete:disabled:hover {\n",
" background-color: var(--disabled-bg-color);\n",
" fill: var(--disabled-fill-color);\n",
" box-shadow: none;\n",
" }\n",
"\n",
" .colab-df-spinner {\n",
" border: 2px solid var(--fill-color);\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" animation:\n",
" spin 1s steps(1) infinite;\n",
" }\n",
"\n",
" @keyframes spin {\n",
" 0% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" border-left-color: var(--fill-color);\n",
" }\n",
" 20% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 30% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 40% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 60% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 80% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" 90% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" }\n",
"</style>\n",
"\n",
" <script>\n",
" async function quickchart(key) {\n",
" const quickchartButtonEl =\n",
" document.querySelector('#' + key + ' button');\n",
" quickchartButtonEl.disabled = true; // To prevent multiple clicks.\n",
" quickchartButtonEl.classList.add('colab-df-spinner');\n",
" try {\n",
" const charts = await google.colab.kernel.invokeFunction(\n",
" 'suggestCharts', [key], {});\n",
" } catch (error) {\n",
" console.error('Error during call to suggestCharts:', error);\n",
" }\n",
" quickchartButtonEl.classList.remove('colab-df-spinner');\n",
" quickchartButtonEl.classList.add('colab-df-quickchart-complete');\n",
" }\n",
" (() => {\n",
" let quickchartButtonEl =\n",
" document.querySelector('#df-54b2aa43-838b-47d3-9209-2fb18153cf87 button');\n",
" quickchartButtonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
" })();\n",
" </script>\n",
" </div>\n",
"\n",
" </div>\n",
" </div>\n"
],
"text/plain": [
" Rainfall Americas Asia Europe Africa\n",
"0 (inches) 2010 \n",
"1 Average 104 201.0 193.0 144.0\n",
"2 24 hour high 15 26.0 27.0 18.0\n",
"3 12 hour high 9 10.0 11.0 12.0\n",
"4 2009 \n",
"5 Average 133 244.0 155.0 166.0\n",
"6 24 hour high 27 28.0 29.0 20.0\n",
"7 12 hour high 11 12.0 13.0 16.0"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import pandas as pd\n",
"from IPython.display import display\n",
"\n",
"df = pd.read_csv(\n",
" \"/content/tables/table_2025_16_07_16_30_01_569.csv\",\n",
")\n",
"display(df.fillna(\"\"))"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
File diff suppressed because one or more lines are too long
@@ -147,7 +147,7 @@
"documents = []\n",
"\n",
"for i, page in enumerate(pages):\n",
" # loop trough items of the page\n",
" # loop through items of the page\n",
" for item in page[\"items\"]:\n",
" document = Document(\n",
" text=item[\"md\"], extra_info={\"bbox\": item[\"bBox\"], \"page\": i}\n",
+36 -7
View File
@@ -1,6 +1,6 @@
# LlamaExtract
LlamaExtract provides a simple API for extracting structured data from unstructured documents like PDFs, text files and images (upcoming).
LlamaExtract provides a simple API for extracting structured data from unstructured documents like PDFs, text files and images.
## Quick Start
@@ -97,11 +97,12 @@ _LlamaExtract only supports a subset of the JSON Schema specification._ While li
be sufficient for a wide variety of use-cases.
- All fields are required by default. Nullable fields must be explicitly marked as such,
using `"anyOf"` with a `"null"` type. See `"start_date"` field above.
- Root node must be of type `"object"`.
using `anyOf` with a `null` type. See `"start_date"` field above.
- Root node must be of type `object`.
- Schema nesting must be limited to within 5 levels.
- The important fields are key names/titles, type and description. Fields for
formatting, default values, etc. are not supported.
formatting, default values, etc. are **not supported**. If you need these, you can add the
restrictions to your field description and/or use a post-processing step. e.g. default values can be supported by making a field optional and then setting `"null"` values from the extraction result to the default value.
- There are other restrictions on number of keys, size of the schema, etc. that you may
hit for complex extraction use cases. In such cases, it is worth thinking how to restructure
your extraction workflow to fit within these constraints, e.g. by extracting subset of fields
@@ -109,6 +110,23 @@ be sufficient for a wide variety of use-cases.
## Other Extraction APIs
### Extraction over bytes or text
You can use the `SourceText` class to extract from bytes or text directly without using a file. If passing the file bytes,
you will need to pass the filename to the `SourceText` class.
```python
with open("resume.pdf", "rb") as f:
file_bytes = f.read()
result = test_agent.extract(SourceText(file=file_bytes, filename="resume.pdf"))
```
```python
result = test_agent.extract(
SourceText(text_content="Candidate Name: Jane Doe")
)
```
### Batch Processing
Process multiple files asynchronously:
@@ -159,16 +177,18 @@ pip install llama-extract==0.1.0
## Tips & Best Practices
At the core of LlamaExtract is the schema, which defines the structure of the data you want to extract from your documents.
1. **Schema Design**:
- Try to limit schema nesting to 3-4 levels.
- Make fields optional when data might not always be present. Having required fields may force the model
to hallucinate when these fields are not present in the documents.
- When you want to extract a variable number of entities, use an `array` type. Note that you cannot use
- When you want to extract a variable number of entities, use an `array` type. However, note that you cannot use
an `array` type for the root node.
- Use descriptive field names and detailed descriptions. Use descriptions to pass formatting
instructions or few-shot examples.
- Start simple and iteratively build your schema to incorporate requirements.
- Above all, start simple and iteratively build your schema to incorporate requirements.
2. **Running Extractions**:
- Note that resetting `agent.schema` will not save the schema to the database,
@@ -177,7 +197,16 @@ pip install llama-extract==0.1.0
part of `job.error` or `extraction_run.error` fields for debugging.
- Consider async operations (`queue_extraction`) for large-scale extraction once you have finalized your schema.
### Hitting "The response was too long to be processed" Error
This implies that the extraction response is hitting output token limits of the LLM. In such cases, it is worth rethinking the design of your schema to enable a more efficient/scalable extraction. e.g.
- Instead of one field that extracts a complex object, you can use multiple fields to distribute the extraction logic.
- You can also use multiple schemas to extract different subsets of fields from the same document and merge them later.
Another option (orthogonal to the above) is to break the document into smaller sections and extract from each section individually, when possible. LlamaExtract will in most cases be able to handle both document and schema chunking automatically, but there are cases where you may need to do this manually.
## Additional Resources
- [Example Notebook](examples/resume_screening.ipynb) - Detailed walkthrough of resume parsing
- [Example Notebook](docs/examples-py/extract/resume_screening.ipynb) - Detailed walkthrough of resume parsing
- [Discord Community](https://discord.com/invite/eN6D2HQ4aX) - Get help and share feedback
+86
View File
@@ -0,0 +1,86 @@
# LlamaCloud Index + Retriever
LlamaCloud is a new generation of managed parsing, ingestion, and retrieval services, designed to bring production-grade context-augmentation to your LLM and RAG applications.
Currently, LlamaCloud supports
- Managed Ingestion API, handling parsing and document management
- Managed Retrieval API, configuring optimal retrieval for your RAG system
## Access
We are opening up a private beta to a limited set of enterprise partners for the managed ingestion and retrieval API. If youre interested in centralizing your data pipelines and spending more time working on your actual RAG use cases, come [talk to us.](https://www.llamaindex.ai/contact)
If you have access to LlamaCloud, you can visit [LlamaCloud](https://cloud.llamaindex.ai) to sign in and get an API key.
## Setup
First, make sure you have the latest LlamaIndex version installed.
```
pip uninstall llama-index # run this if upgrading from v0.9.x or older
pip install -U llama-index --upgrade --no-cache-dir --force-reinstall
```
The `llama-index-indices-managed-llama-cloud` package is included with the above install, but you can also install directly
```
pip install -U llama-index-indices-managed-llama-cloud
```
## Usage
You can create an index on LlamaCloud using the following code. By default, new indexes use managed embeddings (OpenAI text-embedding-3-small, 1536 dimensions, 1 credit/page):
```python
import os
os.environ[
"LLAMA_CLOUD_API_KEY"
] = "llx-..." # can provide API-key in env or in the constructor later on
from llama_index.core import SimpleDirectoryReader
from llama_cloud_services import LlamaCloudIndex
# create a new index (uses managed embeddings by default)
index = LlamaCloudIndex.from_documents(
documents,
"my_first_index",
project_name="default",
api_key="llx-...",
verbose=True,
)
# connect to an existing index
index = LlamaCloudIndex("my_first_index", project_name="default")
```
You can also configure a retriever for managed retrieval:
```python
# from the existing index
index.as_retriever()
# from scratch
from llama_index.indices.managed.llama_cloud import LlamaCloudRetriever
retriever = LlamaCloudRetriever("my_first_index", project_name="default")
```
And of course, you can use other index shortcuts to get use out of your new managed index:
```python
query_engine = index.as_query_engine(llm=llm)
chat_engine = index.as_chat_engine(llm=llm)
```
## Retriever Settings
A full list of retriever settings/kwargs is below:
- `dense_similarity_top_k`: Optional[int] -- If greater than 0, retrieve `k` nodes using dense retrieval
- `sparse_similarity_top_k`: Optional[int] -- If greater than 0, retrieve `k` nodes using sparse retrieval
- `enable_reranking`: Optional[bool] -- Whether to enable reranking or not. Sacrifices some speed for accuracy
- `rerank_top_n`: Optional[int] -- The number of nodes to return after reranking initial retrieval results
- `alpha` Optional[float] -- The weighting between dense and sparse retrieval. 1 = Full dense retrieval, 0 = Full sparse retrieval.
-7
View File
@@ -1,7 +0,0 @@
from llama_cloud_services.extract.extract import (
LlamaExtract,
ExtractionAgent,
SourceText,
)
__all__ = ["LlamaExtract", "ExtractionAgent", "SourceText"]
-3
View File
@@ -1,3 +0,0 @@
from llama_cloud_services.parse.base import LlamaParse, ResultType
__all__ = ["LlamaParse", "ResultType"]
-3
View File
@@ -1,3 +0,0 @@
from llama_cloud_services.parse import LlamaParse, ResultType
__all__ = ["LlamaParse", "ResultType"]
-24
View File
@@ -1,24 +0,0 @@
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "llama-parse"
version = "0.6.10"
description = "Parse files into RAG-Optimized formats."
authors = ["Logan Markewich <logan@llamaindex.ai>"]
license = "MIT"
readme = "README.md"
packages = [{include = "llama_parse"}]
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
llama-cloud-services = ">=0.6.10"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
pytest-asyncio = "*"
ipykernel = "^6.29.0"
[tool.poetry.scripts]
llama-parse = "llama_parse.cli.main:parse"
+11 -8
View File
@@ -25,6 +25,8 @@ Then, install the package:
`pip install llama-cloud-services`
## CLI Usage
Now you can parse your first PDF file using the command line interface. Use the command `llama-parse [file_paths]`. See the help text with `llama-parse --help`.
```bash
@@ -40,6 +42,8 @@ llama-parse my_file.pdf --result-type markdown --output-file output.md
llama-parse my_file.pdf --output-raw-json --output-file output.json
```
## Python Usage
You can also create simple scripts:
```python
@@ -93,9 +97,9 @@ for page in result.pages:
print(page.structuredData)
```
See more details about the result object in the [example notebook](./examples/parse/demo_json_tour.ipynb).
See more details about the result object in the [example notebook](./docs/examples-py/parse/demo_json_tour.ipynb).
## Using with file object
### Using with file object / bytes
You can parse a file object directly:
@@ -104,7 +108,6 @@ from llama_cloud_services import LlamaParse
parser = LlamaParse(
api_key="llx-...", # can also be set in your env as LLAMA_CLOUD_API_KEY
result_type="markdown", # "markdown" and "text" are available
num_workers=4, # if multiple files passed, split in `num_workers` API calls
verbose=True,
language="en", # Optionally you can define a language, default=en
@@ -124,7 +127,7 @@ with open(f"./{file_name}", "rb") as f:
result = parser.parse(file_bytes, extra_info=extra_info)
```
## Using with `SimpleDirectoryReader`
### Using with `SimpleDirectoryReader`
You can also integrate the parser as the default PDF loader in `SimpleDirectoryReader`:
@@ -150,10 +153,10 @@ Full documentation for `SimpleDirectoryReader` can be found on the [LlamaIndex D
Several end-to-end indexing examples can be found in the examples folder
- [Getting Started](examples/parse/demo_basic.ipynb)
- [Advanced RAG Example](examples/parse/demo_advanced.ipynb)
- [Raw API Usage](examples/parse/demo_api.ipynb)
- [Result Object Tour](examples/parse/demo_json_tour.ipynb)
- [Getting Started](docs/examples-py/parse/demo_basic.ipynb)
- [Advanced RAG Example](docs/examples-py/parse/demo_advanced.ipynb)
- [Raw API Usage](docs/examples-py/parse/demo_api.ipynb)
- [Result Object Tour](docs/examples-py/parse/demo_json_tour.ipynb)
## Documentation
+1107
View File
File diff suppressed because it is too large Load Diff
Generated
-4429
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 LlamaIndex
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
View File
+87
View File
@@ -0,0 +1,87 @@
[![PyPI - Downloads](https://img.shields.io/pypi/dm/llama-cloud-services)](https://pypi.org/project/llama-cloud-services/)
[![GitHub contributors](https://img.shields.io/github/contributors/run-llama/llama_cloud_services)](https://github.com/run-llama/llama_cloud_services/graphs/contributors)
[![Discord](https://img.shields.io/discord/1059199217496772688)](https://discord.gg/dGcwcsnxhU)
# Llama Cloud Services
This repository contains the code for hand-written SDKs and clients for interacting with LlamaCloud.
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.
## Getting Started
Install the package:
```bash
pip install llama-cloud-services
```
Then, get your API key from [LlamaCloud](https://cloud.llamaindex.ai/).
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
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"
)
```
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)
## Switch to EU SaaS 🇪🇺
If you are interested in using LlamaCloud services in the EU, you can adjust your base URL to `https://api.cloud.eu.llamaindex.ai`.
You can also create your API key in the EU region [here](https://cloud.eu.llamaindex.ai).
```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",
project_name="default",
api_key="YOUR_API_KEY",
base_url=EU_BASE_URL,
)
```
## Documentation
You can see complete SDK and API documentation for each service on [our official docs](https://docs.cloud.llamaindex.ai/).
## Terms of Service
See the [Terms of Service Here](../TOS.pdf).
## Get in Touch (LlamaCloud)
You can get in touch with us by following our [contact link](https://www.llamaindex.ai/contact).
@@ -2,6 +2,11 @@ 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.constants import EU_BASE_URL
from llama_cloud_services.index import (
LlamaCloudCompositeRetriever,
LlamaCloudIndex,
LlamaCloudRetriever,
)
__all__ = [
"LlamaParse",
@@ -10,4 +15,7 @@ __all__ = [
"LlamaExtract",
"ExtractionAgent",
"EU_BASE_URL",
"LlamaCloudIndex",
"LlamaCloudRetriever",
"LlamaCloudCompositeRetriever",
]
@@ -0,0 +1,31 @@
from .schema import (
TypedAgentData,
ExtractedData,
TypedAgentDataItems,
StatusType,
ExtractedT,
AgentDataT,
ComparisonOperator,
parse_extracted_field_metadata,
calculate_overall_confidence,
InvalidExtractionData,
ExtractedFieldMetadata,
ExtractedFieldMetaDataDict,
)
from .client import AsyncAgentDataClient
__all__ = [
"TypedAgentData",
"AsyncAgentDataClient",
"ExtractedData",
"TypedAgentDataItems",
"StatusType",
"ExtractedT",
"AgentDataT",
"ComparisonOperator",
"parse_extracted_field_metadata",
"calculate_overall_confidence",
"InvalidExtractionData",
"ExtractedFieldMetadata",
"ExtractedFieldMetaDataDict",
]
@@ -0,0 +1,274 @@
import os
from typing import Any, Dict, Generic, List, Optional, Type
from llama_cloud.client import AsyncLlamaCloud
from tenacity import (
WrappedFn,
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
)
import httpx
from .schema import (
AgentDataT,
ComparisonOperator,
TypedAgentData,
TypedAgentDataItems,
TypedAggregateGroup,
TypedAggregateGroupItems,
)
def agent_data_retry(func: WrappedFn) -> WrappedFn:
"""
Decorator that adds automatic retry logic to agent data API calls.
Applies exponential backoff retry strategy for common network-related exceptions:
- Up to 3 retry attempts
- Exponential wait time between 0.5s and 10s
- Retries on timeout, connection, and HTTP status errors
This ensures resilient API communication in distributed environments where
temporary network issues or service unavailability may occur.
"""
return retry(
stop=stop_after_attempt(3),
wait=wait_exponential(min=0.5, max=10),
retry=retry_if_exception_type(
(httpx.TimeoutException, httpx.ConnectError, httpx.HTTPStatusError)
),
)(func)
def get_default_agent_id() -> str:
"""
Retrieve the default agent ID from environment variables.
Returns:
The value of LLAMA_DEPLOY_DEPLOYMENT_NAME environment variable,
or None if not set
Note:
This provides a convenient way to configure agent ID globally
via environment variables instead of passing it explicitly
to each client instance.
"""
return os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME") or "_public"
class AsyncAgentDataClient(Generic[AgentDataT]):
"""
Async client for managing agent-generated structured data with type safety.
This client provides a high-level interface for CRUD operations, searching, and
aggregation of structured data created by agents. It enforces type safety by
validating all data against a specified Pydantic model type.
The client is generic over AgentDataT, which must be a Pydantic BaseModel that
defines the structure of your agent's data output.
Example:
```python
from pydantic import BaseModel
from llama_cloud.client import AsyncLlamaCloud
from llama_cloud_services.beta.agent_data import AsyncAgentDataClient
class ExtractedPerson(BaseModel):
name: str
age: int
email: str
# Initialize client
llama_client = AsyncLlamaCloud(token="your-api-key")
agent_client = AsyncAgentDataClient(
client=llama_client,
type=ExtractedPerson,
collection="extracted_people",
agent_url_id="person-extraction-agent"
)
# Create data
person = ExtractedPerson(name="John Doe", age=30, email="john@example.com")
result = await agent_client.create_agent_data(person)
# Search data
results = await agent_client.search(
filter={"age": {"gt": 25}},
order_by="data.name",
page_size=20
)
```
Type Parameters:
AgentDataT: Pydantic BaseModel type that defines the structure of agent data
"""
def __init__(
self,
type: Type[AgentDataT],
collection: str = "default",
agent_url_id: Optional[str] = None,
client: Optional[AsyncLlamaCloud] = None,
token: Optional[str] = None,
base_url: Optional[str] = None,
):
"""
Initialize the AsyncAgentDataClient.
Args:
type: Pydantic BaseModel class that defines the data structure.
All agent data will be validated against this type.
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.
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
base_url: Llama Cloud API token. Reads from LLAMA_CLOUD_BASE_URL if not provided, and
defaults to https://api.cloud.llamaindex.ai
Raises:
ValueError: If agent_url_id 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.collection = collection
if not client:
client = AsyncLlamaCloud(
token=token or os.getenv("LLAMA_CLOUD_API_KEY"),
base_url=base_url or os.getenv("LLAMA_CLOUD_BASE_URL"),
)
self.client = client
self.type = type
@agent_data_retry
async def get_item(self, item_id: str) -> TypedAgentData[AgentDataT]:
raw_data = 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,
collection=self.collection,
data=data.model_dump(),
)
return TypedAgentData.from_raw(raw_data, validator=self.type)
@agent_data_retry
async def update_item(
self, item_id: str, data: AgentDataT
) -> TypedAgentData[AgentDataT]:
raw_data = await self.client.beta.update_agent_data(
item_id=item_id,
data=data.model_dump(),
)
return TypedAgentData.from_raw(raw_data, validator=self.type)
@agent_data_retry
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 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,
) -> TypedAgentDataItems[AgentDataT]:
"""
Search agent data with filtering, sorting, and pagination.
Args:
filter: Filter conditions to apply to the search. Dict mapping field names to FilterOperation objects. Filters only by data fields
Examples:
- {"age": {"gt": 18}} - age greater than 18
- {"status": {"eq": "active"}} - status equals "active"
- {"tags": {"includes": ["python", "ml"]}} - tags include "python" or "ml"
- {"created_at": {"gte": "2024-01-01"}} - created after date
- {"score": {"lt": 100, "gte": 50}} - score between 50 and 100
order_by: Comma delimited list of fields to sort results by. Can order by standard agent fields like created_at, or by data fields. Data fields must be prefixed with "data.". If ordering desceding, use a " desc" suffix.
Examples:
- "data.name desc, created_at" - sort by name in descending order, and then by creation date
page_size: Maximum number of items to return per page. Defaults to 10.
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,
filter=filter,
order_by=order_by,
offset=offset,
page_size=page_size,
include_total=include_total,
)
return TypedAgentDataItems(
items=[
TypedAgentData.from_raw(item, validator=self.type) for item in raw.items
],
has_more=raw.next_page_token is not None,
total=raw.total_size,
)
@agent_data_retry
async def 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,
) -> TypedAggregateGroupItems[AgentDataT]:
"""
Aggregate agent data into groups according to the group_by fields.
Args:
filter: Filter conditions to apply to the search. Dict mapping field names to FilterOperation objects. Filters only by data fields
See search for more details on filtering.
group_by: List of fields to group by. Groups strictly by equality. Can only group by data fields.
Examples:
- ["name"] - group by name
- ["name", "age"] - group by name and age
count: Whether to include the count of items in each group.
first: Whether to include the first item in each group.
order_by: Comma delimited list of fields to sort results by. See search for more details on ordering.
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,
collection=self.collection,
page_size=page_size,
filter=filter,
order_by=order_by,
group_by=group_by,
count=count,
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,
)
@@ -0,0 +1,591 @@
"""
Agent Data API Schema Definitions
This module provides typed wrappers around the raw LlamaCloud agent data API,
enabling type-safe interactions with agent-generated structured data.
The agent data API serves as a persistent storage system for structured data
produced by LlamaCloud agents (particularly extraction agents). It provides
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
- 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
Example Usage:
```python
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
client = AsyncAgentDataClient(
client=async_llama_cloud,
type=Person,
collection="people",
agent_url_id="my-extraction-agent-xyz"
)
# Create typed data
person = Person(name="John", age=30)
result = await client.create_agent_data(person)
print(result.data.name) # Type-safe access
```
"""
from datetime import datetime
import numbers
from llama_cloud import ExtractRun
from llama_cloud.types.agent_data import AgentData
from llama_cloud.types.aggregate_group import AggregateGroup
from pydantic import BaseModel, Field, ValidationError
from typing import (
Generic,
List,
Literal,
Optional,
Dict,
Type,
TypeVar,
Union,
Any,
)
# 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])
# Status types for extracted data workflow
StatusType = Union[Literal["error", "accepted", "rejected", "pending_review"], str]
ComparisonOperator = Dict[
str, Dict[Literal["gt", "gte", "lt", "lte", "eq", "includes"], Any]
]
class TypedAgentData(BaseModel, Generic[AgentDataT]):
"""
Type-safe wrapper for agent data records.
This class represents a single data record stored in the agent data API,
combining the structured data payload with metadata about when and where
it was created.
Attributes:
id: Unique identifier for this data record
agent_url_id: Identifier of the agent 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
updated_at: Timestamp when the record was last modified
Example:
```python
# Access typed data
person_data: TypedAgentData[Person] = await client.get_agent_data(id)
print(person_data.data.name) # Type-safe access to Person fields
print(person_data.created_at) # Access metadata
```
"""
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"
)
collection: Optional[str] = Field(
description="Named collection within the agent for data organization"
)
data: AgentDataT = Field(description="The structured data payload")
created_at: Optional[datetime] = Field(description="When this record was created")
updated_at: Optional[datetime] = Field(
description="When this record was last modified"
)
@classmethod
def from_raw(
cls, raw_data: AgentData, validator: Type[AgentDataT]
) -> "TypedAgentData[AgentDataT]":
"""
Convert raw API response to typed agent data.
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,
collection=raw_data.collection,
data=data,
created_at=raw_data.created_at,
updated_at=raw_data.updated_at,
)
class TypedAgentDataItems(BaseModel, Generic[AgentDataT]):
"""
Paginated collection of agent data records.
This class represents a page of search results from the agent data API,
providing both the data records and pagination metadata.
Attributes:
items: List of agent data records in this page
total: Total number of records matching the query (only present if requested)
has_more: Whether there are more records available beyond this page
Example:
```python
# Search with pagination
results = await client.search(
page_size=10,
include_total=True
)
for item in results.items:
print(item.data.name)
if results.has_more:
# Load next page
next_page = await client.search(
page_size=10,
offset=10
)
```
"""
items: List[TypedAgentData[AgentDataT]] = Field(
description="List of agent data records in this page"
)
total: Optional[int] = Field(
description="Total number of records matching the query (only present if requested)"
)
has_more: bool = Field(
description="Whether there are more records available beyond this page"
)
class ExtractedFieldMetadata(BaseModel):
"""
Metadata for an extracted data field, such as confidence, and citation information.
"""
reasoning: Optional[str] = Field(
None,
description="symbol for how the citation/confidence was derived: 'INFERRED FROM TEXT', 'VERBATIM EXTRACTION'",
)
confidence: Optional[float] = Field(
None,
description="The confidence score for the field, combined with parsing confidence if applicable",
)
extraction_confidence: Optional[float] = Field(
None,
description="The confidence score for the field based on the extracted text only",
)
page_number: Optional[int] = Field(
None, description="The page number that the field occurred on"
)
matching_text: Optional[str] = Field(
None,
description="The original text this field's value was derived from",
)
ExtractedFieldMetaDataDict = Dict[
str, Union[ExtractedFieldMetadata, Dict[str, Any], list[Any]]
]
def parse_extracted_field_metadata(
field_metadata: dict[str, Any],
) -> ExtractedFieldMetaDataDict:
return {
k: _parse_extracted_field_metadata_recursive(v)
for k, v in field_metadata.items()
if k not in _METADATA_FIELDS_SIBLING_TO_LEAF
}
_METADATA_FIELDS_SIBLING_TO_LEAF = {"reasoning"}
def _parse_extracted_field_metadata_recursive(
field_value: Any,
additional_fields: dict[str, Any] = {},
) -> Union[ExtractedFieldMetadata, Dict[str, Any], list[Any]]:
"""
Parse the extracted field metadata into a dictionary of field names to field metadata.
"""
if isinstance(field_value, ExtractedFieldMetadata):
# support running this multiple times
return field_value
elif isinstance(field_value, dict):
# reasoning explicitly excluded, as it is included next to subfields, for example
# "dimensions.width" is a leaf, but there will still potentially be a "dimensions.reasoning"
indicator_fields = {"confidence", "extraction_confidence", "citation"}
if len(indicator_fields.intersection(field_value.keys())) > 0:
try:
merged = {**field_value, **additional_fields}
validated = ExtractedFieldMetadata.model_validate(merged)
# grab the citation from the array. This is just an array for backwards compatibility.
if "citation" in field_value and len(field_value["citation"]) > 0:
first_citation = field_value["citation"][0]
if "page" in first_citation and isinstance(
first_citation["page"], numbers.Number
):
validated.page_number = int(first_citation["page"]) # type: ignore
if "matching_text" in first_citation and isinstance(
first_citation["matching_text"], str
):
validated.matching_text = first_citation["matching_text"]
return validated
except ValidationError:
pass
additional_fields = {
k: v
for k, v in field_value.items()
if k in _METADATA_FIELDS_SIBLING_TO_LEAF
}
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
}
elif isinstance(field_value, list):
return [_parse_extracted_field_metadata_recursive(item) for item in field_value]
else:
raise ValueError(
f"Invalid field value: {field_value}. Expected ExtractedFieldMetadata, dict, or list"
)
class ExtractedData(BaseModel, Generic[ExtractedT]):
"""
Wrapper for extracted data with workflow status tracking.
This class is designed for extraction workflows where data goes through
review and approval stages. It maintains both the original extracted data
and the current state after any modifications.
Attributes:
original_data: The data as originally extracted from the source
data: The current state of the data (may differ from original after edits)
status: Current workflow status (in_review, accepted, rejected, error)
confidence: Confidence scores for individual fields (if available)
file_id: The llamacloud file ID of the file that was used to extract the data
file_name: The name of the file that was used to extract the data
file_hash: A content hash of the file that was used to extract the data, for de-duplication
Status Workflow:
- "pending_review": Initial state, awaiting human review
- "accepted": Data approved and ready for use
- "rejected": Data rejected, needs re-extraction or manual fix
- "error": Processing error occurred
Example:
```python
# Create extracted data for review
extracted = ExtractedData.create(
data=person_data,
status="pending_review",
confidence={"name": 0.95, "age": 0.87}
)
# Later, after review
if extracted.status == "accepted":
# Use the data
process_person(extracted.data)
```
"""
original_data: ExtractedT = Field(
description="The original data that was extracted from the document"
)
data: ExtractedT = Field(
description="The latest state of the data. Will differ if data has been updated"
)
status: StatusType = Field(description="The status of the extracted data")
overall_confidence: Optional[float] = Field(
None,
description="The overall confidence score for the extracted data",
)
field_metadata: ExtractedFieldMetaDataDict = Field(
default_factory=dict,
description="Page links, and perhaps eventually bounding boxes, for individual fields in the extracted data. Structure is expected to have a ",
)
file_id: Optional[str] = Field(
None, description="The ID of the file that was used to extract the data"
)
file_name: Optional[str] = Field(
None, description="The name of the file that was used to extract the data"
)
file_hash: Optional[str] = Field(
None, description="The hash of the file that was used to extract the data"
)
metadata: Optional[Dict[str, Any]] = Field(
default_factory=dict,
description="Additional metadata about the extracted data, such as errors, tokens, etc.",
)
@classmethod
def create(
cls,
data: ExtractedT,
status: StatusType = "pending_review",
field_metadata: ExtractedFieldMetaDataDict = {},
file_id: Optional[str] = None,
file_name: Optional[str] = None,
file_hash: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> "ExtractedData[ExtractedT]":
"""
Create a new ExtractedData instance with sensible defaults.
Args:
extracted_data: The extracted data payload
status: Initial workflow status
field_metadata: Optional confidence scores, citations, and other metadata for fields
file_id: The llamacloud file ID of the file that was used to extract the data
file_name: The name of the file that was used to extract the data
file_hash: A content hash of the file that was used to extract the data, for de-duplication
metadata: Arbitrary additional application-specific data about the extracted data
Returns:
New ExtractedData instance ready for storage
"""
normalized_field_metadata = parse_extracted_field_metadata(field_metadata)
return cls(
original_data=data,
data=data,
status=status,
field_metadata=normalized_field_metadata,
overall_confidence=calculate_overall_confidence(normalized_field_metadata),
file_id=file_id,
file_name=file_name,
file_hash=file_hash,
metadata=metadata or {},
)
@classmethod
def from_extraction_result(
cls,
result: ExtractRun,
schema: Type[ExtractedT],
file_hash: Optional[str] = None,
file_name: Optional[str] = None,
file_id: Optional[str] = None,
status: StatusType = "pending_review",
metadata: Optional[Dict[str, Any]] = None,
) -> "ExtractedData[ExtractedT]":
"""
Create an ExtractedData instance from an extraction result.
"""
file_id = file_id or result.file.id
file_name = file_name or result.file.name
try:
field_metadata = parse_extracted_field_metadata(
result.extraction_metadata.get("field_metadata", {})
)
except ValidationError:
field_metadata = {}
try:
data = schema.model_validate(result.data) # type: ignore
return cls.create(
data=data,
status=status,
field_metadata=field_metadata,
file_id=file_id,
file_name=file_name,
file_hash=file_hash,
metadata=metadata or {},
)
except ValidationError as e:
invalid_item = ExtractedData[Dict[str, Any]].create(
data=result.data or {},
status="error",
field_metadata=field_metadata,
metadata={"extraction_error": str(e), **(metadata or {})},
file_id=file_id,
file_name=file_name,
file_hash=file_hash,
)
raise InvalidExtractionData(invalid_item) from e
class InvalidExtractionData(Exception):
"""
Exception raised when the extracted data does not conform to the schema.
"""
def __init__(self, invalid_item: ExtractedData[Dict[str, Any]]):
self.invalid_item = invalid_item
super().__init__("Not able to parse the extracted data, parsed invalid format")
def calculate_overall_confidence(
metadata: ExtractedFieldMetaDataDict,
) -> Optional[float]:
"""
Calculate the overall confidence score for the extracted data.
"""
numerator, denominator = _calculate_overall_confidence_recursive(metadata)
if denominator == 0:
return None
return numerator / denominator
def _calculate_overall_confidence_recursive(
confidence: Union[ExtractedFieldMetadata, Dict[str, Any], list[Any]],
) -> tuple[float, int]:
"""
Calculate the overall confidence score for the extracted data.
"""
if isinstance(confidence, ExtractedFieldMetadata):
if confidence.confidence is not None:
return confidence.confidence, 1
else:
return 0, 0
if isinstance(confidence, dict):
numerator: float = 0
denominator: int = 0
for value in confidence.values():
num, den = _calculate_overall_confidence_recursive(value)
numerator += num
denominator += den
return numerator, denominator
elif isinstance(confidence, list):
numerator = 0
denominator = 0
for value in confidence:
num, den = _calculate_overall_confidence_recursive(value)
numerator += num
denominator += den
return numerator, denominator
else:
return 0, 0
class TypedAggregateGroup(BaseModel, Generic[AgentDataT]):
"""
Represents a group of agent data records aggregated by common field values.
This class is used for grouping and analyzing agent data based on shared
characteristics. It's particularly useful for generating summaries and
statistics across large datasets.
Attributes:
group_key: The field values that define this group
count: Number of records in this group (if count aggregation was requested)
first_item: Representative data record from this group (if requested)
Example:
```python
# Group by age range
groups = await client.aggregate_agent_data(
group_by=["age_range"],
count=True,
first=True
)
for group in groups.items:
print(f"Age range {group.group_key['age_range']}: {group.count} people")
if group.first_item:
print(f"Example: {group.first_item.name}")
```
"""
group_key: Dict[str, Any] = Field(
description="The field values that define this group"
)
count: Optional[int] = Field(
description="Number of records in this group (if count aggregation was requested)"
)
first_item: Optional[AgentDataT] = Field(
description="Representative data record from this group (if requested)"
)
@classmethod
def from_raw(
cls, raw_data: AggregateGroup, validator: Type[AgentDataT]
) -> "TypedAggregateGroup[AgentDataT]":
"""
Convert raw API response to typed aggregate group.
Args:
raw_data: Raw aggregate group from the API
validator: Pydantic model class to validate the first_item field
Returns:
TypedAggregateGroup instance with validated first_item
"""
first_item: Optional[AgentDataT] = raw_data.first_item
if first_item is not None:
first_item = validator.model_validate(first_item)
return cls(
group_key=raw_data.group_key,
count=raw_data.count,
first_item=first_item,
)
class TypedAggregateGroupItems(BaseModel, Generic[AgentDataT]):
"""
Paginated collection of aggregate groups.
This class represents a page of aggregation results from the agent data API,
providing both the grouped data and pagination metadata.
Attributes:
items: List of aggregate groups in this page
total: Total number of groups matching the query (only present if requested)
has_more: Whether there are more groups available beyond this page
Example:
```python
# Get first page of groups
results = await client.aggregate_agent_data(
group_by=["department"],
count=True,
page_size=20
)
for group in results.items:
dept = group.group_key["department"]
print(f"{dept}: {group.count} employees")
# Load more if needed
if results.has_more:
next_page = await client.aggregate_agent_data(
group_by=["department"],
count=True,
page_size=20,
offset=20
)
```
"""
items: List[TypedAggregateGroup[AgentDataT]] = Field(
description="List of aggregate groups in this page"
)
total: Optional[int] = Field(
description="Total number of groups matching the query (only present if requested)"
)
has_more: bool = Field(
description="Whether there are more groups available beyond this page"
)
@@ -0,0 +1,17 @@
from llama_cloud_services.extract.extract import (
LlamaExtract,
ExtractConfig,
ExtractionAgent,
SourceText,
ExtractTarget,
ExtractMode,
)
__all__ = [
"LlamaExtract",
"ExtractionAgent",
"SourceText",
"ExtractConfig",
"ExtractTarget",
"ExtractMode",
]
@@ -1,31 +1,39 @@
import asyncio
import os
import time
from io import BufferedIOBase, BufferedReader, BytesIO
from io import BufferedIOBase, BufferedReader, BytesIO, 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
from tenacity import (
retry_if_exception,
stop_after_attempt,
wait_exponential_jitter,
AsyncRetrying,
)
from llama_cloud import (
ExtractAgent as CloudExtractAgent,
ExtractAgentCreate,
ExtractConfig,
ExtractJob,
ExtractJobCreate,
ExtractRun,
ExtractSchemaValidateRequest,
ExtractAgentUpdate,
File,
ExtractMode,
StatusEnum,
Project,
ExtractTarget,
LlamaExtractSettings,
PaginatedExtractRunsResponse,
)
from llama_cloud.client import AsyncLlamaCloud
from llama_cloud_services.extract.utils import JSONObjectType, augment_async_errors
from llama_cloud.core.api_error import ApiError
from llama_cloud_services.extract.utils import (
JSONObjectType,
augment_async_errors,
ExperimentalWarning,
)
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
@@ -43,25 +51,52 @@ DEFAULT_EXTRACT_CONFIG = ExtractConfig(
)
def _is_retryable_error(exception: BaseException) -> bool:
"""Check if an exception is retryable."""
if isinstance(exception, ApiError):
return exception.status_code in (502, 503, 504, 425, 408)
elif isinstance(
exception, (httpx.HTTPStatusError, httpx.RequestError, httpx.TimeoutException)
):
return True
return False
class SourceText:
def __init__(
self,
file: Union[bytes, BufferedIOBase, str, Path],
*,
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 isinstance(self.file, (bytes, BufferedIOBase)):
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]
@@ -101,6 +136,16 @@ def run_in_thread(
return thread_pool.submit(run_coro).result()
def _extraction_config_warning(config: ExtractConfig) -> None:
if config.cite_sources or config.confidence_scores:
warnings.warn(
"`cite_sources`/`confidence_scores` could greatly increase the "
"size of the response, and slow down the extraction. Results will be "
"available in the `extraction_metadata` field for the extraction run.",
ExperimentalWarning,
)
class ExtractionAgent:
"""Class representing a single extraction agent with methods for extraction operations."""
@@ -161,7 +206,7 @@ class ExtractionAgent:
)
validated_schema = self._run_in_thread(
self._client.llama_extract.validate_extraction_schema(
request=ExtractSchemaValidateRequest(data_schema=processed_schema)
data_schema=processed_schema
)
)
self._data_schema = validated_schema.data_schema
@@ -172,6 +217,7 @@ class ExtractionAgent:
@config.setter
def config(self, config: ExtractConfig) -> None:
_extraction_config_warning(config)
self._config = config
def _run_in_thread(self, coro: Coroutine[Any, Any, T]) -> T:
@@ -194,14 +240,26 @@ 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:
file_contents: Union[BufferedIOBase, BytesIO]
if isinstance(file_input.file, (str, Path)):
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)
else:
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
@@ -210,7 +268,7 @@ class ExtractionAgent:
project_id=self._project_id, upload_file=file_contents
)
finally:
if isinstance(file_contents, BufferedReader):
if file_contents is not None and isinstance(file_contents, BufferedReader):
file_contents.close()
async def _upload_file(self, file_input: FileInput) -> File:
@@ -238,35 +296,60 @@ class ExtractionAgent:
return await self.upload_file(source_text)
async def _get_job_with_retry(self, job_id: str) -> ExtractJob:
"""Get job with retry logic for transient errors."""
async for attempt in AsyncRetrying(
retry=retry_if_exception(_is_retryable_error),
stop=stop_after_attempt(5),
wait=wait_exponential_jitter(initial=1, max=60, jitter=5),
reraise=True,
):
with attempt:
return await self._client.llama_extract.get_job(job_id=job_id)
async def _get_run_with_retry(self, job_id: str) -> ExtractRun:
"""Get extraction run with retry logic for transient errors."""
async for attempt in AsyncRetrying(
retry=retry_if_exception(_is_retryable_error),
stop=stop_after_attempt(3),
wait=wait_exponential_jitter(initial=1, max=20, jitter=3),
reraise=True,
):
with attempt:
return await self._client.llama_extract.get_run_by_job_id(job_id=job_id)
async def _wait_for_job_result(self, job_id: str) -> Optional[ExtractRun]:
"""Wait for and return the results of an extraction job."""
start = time.perf_counter()
tries = 0
while True:
await asyncio.sleep(self.check_interval)
tries += 1
job = await self._client.llama_extract.get_job(
job_id=job_id,
)
if job.status == StatusEnum.SUCCESS:
return await self._client.llama_extract.get_run_by_job_id(
job_id=job_id,
)
elif job.status == StatusEnum.PENDING:
end = time.perf_counter()
if end - start > self.max_timeout:
raise Exception(f"Timeout while extracting the file: {job_id}")
if self._verbose and tries % 10 == 0:
print(".", end="", flush=True)
continue
else:
warnings.warn(
f"Failure in job: {job_id}, status: {job.status}, error: {job.error}"
)
return await self._client.llama_extract.get_run_by_job_id(
job_id=job_id,
)
try:
job = await self._get_job_with_retry(job_id)
if job.status == StatusEnum.SUCCESS:
return await self._get_run_with_retry(job_id)
elif job.status == StatusEnum.PENDING:
end = time.perf_counter()
if end - start > self.max_timeout:
raise Exception(f"Timeout while extracting the file: {job_id}")
if self._verbose and tries % 10 == 0:
print(".", end="", flush=True)
continue
else:
warnings.warn(
f"Failure in job: {job_id}, status: {job.status}, error: {job.error}"
)
return await self._get_run_with_retry(job_id)
except Exception as e:
# If we get a non-retryable error or all retries are exhausted, re-raise
if self._verbose:
print(f"\nError in job polling for {job_id}: {e}")
raise e
def save(self) -> None:
"""Persist the extraction agent's schema and config to the database.
@@ -277,10 +360,8 @@ class ExtractionAgent:
self._agent = self._run_in_thread(
self._client.llama_extract.update_extraction_agent(
extraction_agent_id=self.id,
request=ExtractAgentUpdate(
data_schema=self.data_schema,
config=self.config,
),
data_schema=self.data_schema,
config=self.config,
)
)
@@ -572,7 +653,7 @@ class LlamaExtract(BaseComponent):
httpx_timeout=httpx_timeout,
verbose=verbose,
)
self._httpx_client = httpx.AsyncClient(verify=verify, timeout=httpx_timeout)
self._httpx_client = httpx.AsyncClient(verify=verify, timeout=httpx_timeout) # type: ignore
self.verify = verify
self.httpx_timeout = httpx_timeout
@@ -584,21 +665,8 @@ class LlamaExtract(BaseComponent):
self._thread_pool = ThreadPoolExecutor(
max_workers=min(10, (os.cpu_count() or 1) + 4)
)
# Fetch default project id if not provided
if not project_id:
project_id = os.getenv("LLAMA_CLOUD_PROJECT_ID", None)
if not project_id:
print("No project_id provided, fetching default project.")
projects: List[Project] = self._run_in_thread(
self._async_client.projects.list_projects()
)
default_project = [p for p in projects if p.is_default]
if not default_project:
raise ValueError(
"No default project found. Please provide a project_id."
)
project_id = default_project[0].id
self._project_id = project_id
self._organization_id = organization_id
@@ -629,11 +697,7 @@ class LlamaExtract(BaseComponent):
ExtractionAgent: The created extraction agent
"""
if config is not None:
if config.extraction_mode == ExtractMode.ACCURATE:
warnings.warn(
"ACCURATE extraction mode is deprecated. Using BALANCED instead."
)
config.extraction_mode = ExtractMode.BALANCED
_extraction_config_warning(config)
else:
config = DEFAULT_EXTRACT_CONFIG
@@ -650,11 +714,9 @@ class LlamaExtract(BaseComponent):
self._async_client.llama_extract.create_extraction_agent(
project_id=self._project_id,
organization_id=self._organization_id,
request=ExtractAgentCreate(
name=name,
data_schema=data_schema,
config=config,
),
name=name,
data_schema=data_schema,
config=config,
)
)
@@ -668,6 +730,8 @@ class LlamaExtract(BaseComponent):
num_workers=self.num_workers,
show_progress=self.show_progress,
verbose=self.verbose,
verify=self.verify,
httpx_timeout=self.httpx_timeout,
)
def get_agent(
@@ -32,3 +32,9 @@ def augment_async_errors() -> Generator[None, None, None]:
JSONType = Union[Dict[str, Any], List[Any], str, int, float, bool, None]
JSONObjectType = Dict[str, JSONType]
class ExperimentalWarning(Warning):
"""Warning for experimental features."""
pass
+11
View File
@@ -0,0 +1,11 @@
from .base import LlamaCloudIndex
from .retriever import LlamaCloudRetriever
from .composite_retriever import (
LlamaCloudCompositeRetriever,
)
__all__ = [
"LlamaCloudIndex",
"LlamaCloudRetriever",
"LlamaCloudCompositeRetriever",
]
+422
View File
@@ -0,0 +1,422 @@
import base64
import urllib.parse
import uuid
from httpx import Request, HTTPStatusError
from tenacity import (
retry,
wait_exponential_jitter,
stop_after_attempt,
retry_if_exception,
)
from typing import Any, Optional, Tuple, List, Union, Dict, Callable
from llama_index.core.async_utils import run_jobs
from llama_index.core.schema import NodeWithScore, ImageNode
from llama_cloud import (
AutoTransformConfig,
PageFigureNodeWithScore,
PageScreenshotNodeWithScore,
Pipeline,
PipelineCreateTransformConfig,
PipelineType,
Project,
Retriever,
)
from llama_cloud.core import remove_none_from_dict
from llama_cloud.client import LlamaCloud, AsyncLlamaCloud
from llama_cloud.core.api_error import ApiError
def is_retryable_http_error(exception: Exception) -> bool:
# Retry for ApiError with 5xx status codes
if isinstance(exception, ApiError):
return 500 <= exception.status_code < 600
# Also retry for HTTPError with 5xx status codes
elif isinstance(exception, HTTPStatusError):
return 500 <= exception.response.status_code < 600
return False
def retry_on_failure(func: Callable) -> Callable:
"""Decorator to apply tenacity retry with exponential backoff and jitter for 5xx errors."""
return retry(
wait=wait_exponential_jitter(exp_base=2, max=10),
stop=stop_after_attempt(5),
retry=retry_if_exception(is_retryable_http_error),
reraise=True,
)(func)
def default_transform_config() -> PipelineCreateTransformConfig:
return AutoTransformConfig()
def resolve_retriever(
client: LlamaCloud,
project: Project,
retriever_name: Optional[str] = None,
retriever_id: Optional[str] = None,
persisted: bool = True,
) -> Optional[Retriever]:
if not persisted:
return Retriever(
id=str(uuid.uuid4()),
project_id=project.id,
name=retriever_name,
pipelines=[],
)
if retriever_id:
return client.retrievers.get_retriever(
retriever_id=retriever_id, project_id=project.id
)
elif retriever_name:
retrievers = client.retrievers.list_retrievers(
project_id=project.id, name=retriever_name
)
return next(
(retriever for retriever in retrievers if retriever.name == retriever_name),
None,
)
else:
return None
def resolve_project(
client: LlamaCloud,
project_name: Optional[str],
project_id: Optional[str],
organization_id: Optional[str],
) -> Project:
if project_id is not None:
return client.projects.get_project(project_id=project_id)
else:
projects = client.projects.list_projects(
project_name=project_name, organization_id=organization_id
)
if len(projects) == 0:
raise ValueError(f"No project found with name {project_name}")
elif len(projects) > 1:
raise ValueError(
f"Multiple projects found with name {project_name}. Please specify organization_id."
)
return projects[0]
def resolve_pipeline(
client: LlamaCloud,
pipeline_id: Optional[str],
project: Optional[Project],
pipeline_name: Optional[str],
) -> Pipeline:
if pipeline_id is not None:
return client.pipelines.get_pipeline(pipeline_id=pipeline_id)
else:
pipelines = client.pipelines.search_pipelines(
project_id=project.id, # type: ignore [union-attr]
pipeline_name=pipeline_name,
pipeline_type=PipelineType.MANAGED.value, # type: ignore [union-attr]
)
if len(pipelines) == 0:
raise ValueError(
f"Unknown index name {pipeline_name}. Please confirm an index with this name exists."
)
elif len(pipelines) > 1:
raise ValueError(
f"Multiple pipelines found with name {pipeline_name} in project {project.name}" # type: ignore [union-attr]
)
return pipelines[0]
def resolve_project_and_pipeline(
client: LlamaCloud,
pipeline_name: Optional[str],
pipeline_id: Optional[str],
project_name: Optional[str],
project_id: Optional[str],
organization_id: Optional[str],
) -> Tuple[Project, Pipeline]:
# resolve pipeline by ID
if pipeline_id is not None:
pipeline = resolve_pipeline(
client, pipeline_id=pipeline_id, project=None, pipeline_name=None
)
project_id = pipeline.project_id
# resolve project
project = resolve_project(client, project_name, project_id, organization_id)
# resolve pipeline by name
if pipeline_id is None:
pipeline = resolve_pipeline(
client, pipeline_id=None, project=project, pipeline_name=pipeline_name
)
return project, pipeline
def _build_get_page_screenshot_request(
client: Union[LlamaCloud, AsyncLlamaCloud],
file_id: str,
page_index: int,
project_id: str,
) -> Request:
return client._client_wrapper.httpx_client.build_request(
"GET",
urllib.parse.urljoin(
f"{client._client_wrapper.get_base_url()}/",
f"api/v1/files/{file_id}/page_screenshots/{page_index}",
),
params=remove_none_from_dict({"project_id": project_id}),
headers=client._client_wrapper.get_headers(),
timeout=60,
)
def _build_get_page_figure_request(
client: Union[LlamaCloud, AsyncLlamaCloud],
file_id: str,
page_index: int,
figure_name: str,
project_id: str,
) -> Request:
return client._client_wrapper.httpx_client.build_request(
"GET",
urllib.parse.urljoin(
f"{client._client_wrapper.get_base_url()}/",
f"api/v1/files/{file_id}/page-figures/{page_index}/{figure_name}",
),
params=remove_none_from_dict({"project_id": project_id}),
headers=client._client_wrapper.get_headers(),
timeout=60,
)
@retry_on_failure
def get_page_screenshot(
client: LlamaCloud, file_id: str, page_index: int, project_id: str
) -> str:
"""Get the page screenshot."""
# TODO: this currently uses requests, should be replaced with the client
request = _build_get_page_screenshot_request(
client, file_id, page_index, project_id
)
_response = client._client_wrapper.httpx_client.send(request)
if 200 <= _response.status_code < 300:
return _response.content
else:
raise ApiError(status_code=_response.status_code, body=_response.text)
@retry_on_failure
def get_page_figure(
client: LlamaCloud, file_id: str, page_index: int, figure_name: str, project_id: str
) -> str:
request = _build_get_page_figure_request(
client, file_id, page_index, figure_name, project_id
)
_response = client._client_wrapper.httpx_client.send(request)
if 200 <= _response.status_code < 300:
return _response.content
else:
raise ApiError(status_code=_response.status_code, body=_response.text)
@retry_on_failure
async def aget_page_screenshot(
client: AsyncLlamaCloud, file_id: str, page_index: int, project_id: str
) -> str:
"""Get the page screenshot (async)."""
request = _build_get_page_screenshot_request(
client, file_id, page_index, project_id
)
_response = await client._client_wrapper.httpx_client.send(request)
if 200 <= _response.status_code < 300:
return _response.content
else:
raise ApiError(status_code=_response.status_code, body=_response.text)
@retry_on_failure
async def aget_page_figure(
client: AsyncLlamaCloud,
file_id: str,
page_index: int,
figure_name: str,
project_id: str,
) -> str:
request = _build_get_page_figure_request(
client, file_id, page_index, figure_name, project_id
)
_response = await client._client_wrapper.httpx_client.send(request)
if 200 <= _response.status_code < 300:
return _response.content
else:
raise ApiError(status_code=_response.status_code, body=_response.text)
def page_screenshot_nodes_to_node_with_score(
client: LlamaCloud,
raw_image_nodes: Optional[List[PageScreenshotNodeWithScore]],
project_id: str,
) -> List[NodeWithScore]:
if not raw_image_nodes:
return []
image_nodes = []
for raw_image_node in raw_image_nodes:
image_bytes = get_page_screenshot(
client=client,
file_id=raw_image_node.node.file_id,
page_index=raw_image_node.node.page_index,
project_id=project_id,
)
image_base64 = base64.b64encode(image_bytes).decode("utf-8")
image_node_metadata: Dict[str, Any] = {
**(raw_image_node.node.metadata or {}),
"file_id": raw_image_node.node.file_id,
"page_index": raw_image_node.node.page_index,
}
image_node_with_score = NodeWithScore(
node=ImageNode(image=image_base64, metadata=image_node_metadata),
score=raw_image_node.score,
)
image_nodes.append(image_node_with_score)
return image_nodes
def image_nodes_to_node_with_score(
client: LlamaCloud,
raw_image_nodes: Optional[List[PageScreenshotNodeWithScore]],
project_id: str,
) -> List[NodeWithScore]:
"""
Legacy method to alias page_screenshot_nodes_to_node_with_score.
"""
if not raw_image_nodes:
return []
return page_screenshot_nodes_to_node_with_score(
client=client, raw_image_nodes=raw_image_nodes, project_id=project_id
)
def page_figure_nodes_to_node_with_score(
client: LlamaCloud,
raw_figure_nodes: Optional[List[PageFigureNodeWithScore]],
project_id: str,
) -> List[NodeWithScore]:
if not raw_figure_nodes:
return []
figure_nodes = []
for raw_figure_node in raw_figure_nodes:
figure_bytes = get_page_figure(
client=client,
file_id=raw_figure_node.node.file_id,
page_index=raw_figure_node.node.page_index,
figure_name=raw_figure_node.node.figure_name,
project_id=project_id,
)
figure_base64 = base64.b64encode(figure_bytes).decode("utf-8")
figure_node_metadata: Dict[str, Any] = {
**(raw_figure_node.node.metadata or {}),
"file_id": raw_figure_node.node.file_id,
"page_index": raw_figure_node.node.page_index,
"figure_name": raw_figure_node.node.figure_name,
}
figure_node_with_score = NodeWithScore(
node=ImageNode(image=figure_base64, metadata=figure_node_metadata),
score=raw_figure_node.score,
)
figure_nodes.append(figure_node_with_score)
return figure_nodes
async def apage_screenshot_nodes_to_node_with_score(
client: AsyncLlamaCloud,
raw_image_nodes: Optional[List[PageScreenshotNodeWithScore]],
project_id: str,
) -> List[NodeWithScore]:
if not raw_image_nodes:
return []
image_nodes = []
tasks = [
aget_page_screenshot(
client=client,
file_id=raw_image_node.node.file_id,
page_index=raw_image_node.node.page_index,
project_id=project_id,
)
for raw_image_node in raw_image_nodes
]
image_bytes_list = await run_jobs(tasks)
for image_bytes, raw_image_node in zip(image_bytes_list, raw_image_nodes):
image_base64 = base64.b64encode(image_bytes).decode("utf-8")
image_node_metadata: Dict[str, Any] = {
**(raw_image_node.node.metadata or {}),
"file_id": raw_image_node.node.file_id,
"page_index": raw_image_node.node.page_index,
}
image_node_with_score = NodeWithScore(
node=ImageNode(image=image_base64, metadata=image_node_metadata),
score=raw_image_node.score,
)
image_nodes.append(image_node_with_score)
return image_nodes
async def aimage_nodes_to_node_with_score(
client: AsyncLlamaCloud,
raw_image_nodes: Optional[List[PageScreenshotNodeWithScore]],
project_id: str,
) -> List[NodeWithScore]:
"""
Legacy method to alias apage_screenshot_nodes_to_node_with_score.
"""
if not raw_image_nodes:
return []
return await apage_screenshot_nodes_to_node_with_score(
client=client, raw_image_nodes=raw_image_nodes, project_id=project_id
)
async def apage_figure_nodes_to_node_with_score(
client: AsyncLlamaCloud,
raw_figure_nodes: Optional[List[PageFigureNodeWithScore]],
project_id: str,
) -> List[NodeWithScore]:
if not raw_figure_nodes:
return []
figure_nodes = []
tasks = [
aget_page_figure(
client=client,
file_id=raw_figure_node.node.file_id,
page_index=raw_figure_node.node.page_index,
figure_name=raw_figure_node.node.figure_name,
project_id=project_id,
)
for raw_figure_node in raw_figure_nodes
]
figure_bytes_list = await run_jobs(tasks)
for figure_bytes, raw_figure_node in zip(figure_bytes_list, raw_figure_nodes):
figure_base64 = base64.b64encode(figure_bytes).decode("utf-8")
figure_node_metadata: Dict[str, Any] = {
**(raw_figure_node.node.metadata or {}),
"file_id": raw_figure_node.node.file_id,
"page_index": raw_figure_node.node.page_index,
"figure_name": raw_figure_node.node.figure_name,
}
figure_node_with_score = NodeWithScore(
node=ImageNode(image=figure_base64, metadata=figure_node_metadata),
score=raw_figure_node.score,
)
figure_nodes.append(figure_node_with_score)
return figure_nodes
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,291 @@
from typing import Any, List, Optional
import httpx
from llama_cloud import (
CompositeRetrievalMode,
CompositeRetrievedTextNodeWithScore,
RetrieverCreate,
Retriever,
RetrieverPipeline,
PresetRetrievalParams,
ReRankConfig,
)
from llama_cloud.resources.pipelines.client import OMIT
from llama_index.core.base.base_retriever import BaseRetriever
from llama_index.core.constants import DEFAULT_PROJECT_NAME
from llama_index.core.ingestion.api_utils import get_aclient, get_client
from llama_index.core.schema import NodeWithScore, QueryBundle, TextNode
from .base import LlamaCloudIndex
from .api_utils import (
resolve_project,
resolve_retriever,
page_screenshot_nodes_to_node_with_score,
)
class LlamaCloudCompositeRetriever(BaseRetriever):
def __init__(
self,
# retriever identifier
name: Optional[str] = None,
retriever_id: Optional[str] = None,
# project identifier
project_name: Optional[str] = DEFAULT_PROJECT_NAME,
project_id: Optional[str] = None,
organization_id: Optional[str] = None,
# creation options
create_if_not_exists: bool = False,
# connection params
api_key: Optional[str] = None,
base_url: Optional[str] = None,
app_url: Optional[str] = None,
timeout: int = 60,
httpx_client: Optional[httpx.Client] = None,
async_httpx_client: Optional[httpx.AsyncClient] = None,
# composite retrieval params
mode: Optional[CompositeRetrievalMode] = None,
rerank_top_n: Optional[int] = None,
rerank_config: Optional[ReRankConfig] = None,
persisted: Optional[bool] = True,
**kwargs: Any,
) -> None:
"""Initialize the Composite Retriever."""
# initialize clients
self._client = get_client(api_key, base_url, app_url, timeout, httpx_client)
self._aclient = get_aclient(
api_key, base_url, app_url, timeout, async_httpx_client
)
self.project = resolve_project(
self._client, project_name, project_id, organization_id
)
self.name = name
self.project_name = self.project.name
self._persisted = persisted
self.retriever = resolve_retriever(
self._client, self.project, name, retriever_id, persisted # type: ignore [arg-type]
)
if self.retriever is None and persisted:
if create_if_not_exists:
self.retriever = self._client.retrievers.upsert_retriever(
project_id=self.project.id,
request=RetrieverCreate(name=self.name, pipelines=[]),
)
else:
raise ValueError(
f"Retriever with name '{self.name}' does not exist in project."
)
# composite retrieval params
self._mode = mode if mode is not None else OMIT
self._rerank_top_n = rerank_top_n if rerank_top_n is not None else OMIT
self._rerank_config = rerank_config if rerank_config is not None else OMIT
super().__init__(
callback_manager=kwargs.get("callback_manager"),
verbose=kwargs.get("verbose", False),
)
@property
def retriever_pipelines(self) -> List[RetrieverPipeline]:
return self.retriever.pipelines or [] # type: ignore [union-attr]
def update_retriever_pipelines(
self, pipelines: List[RetrieverPipeline]
) -> Retriever:
if self._persisted:
self.retriever = self._client.retrievers.update_retriever(
self.retriever.id, pipelines=pipelines # type: ignore [union-attr]
)
else:
# Update in-memory retriever for non-persisted case using copy
self.retriever = self.retriever.copy(update={"pipelines": pipelines}) # type: ignore [union-attr]
return self.retriever
def add_index(
self,
index: LlamaCloudIndex,
name: Optional[str] = None,
description: Optional[str] = None,
preset_retrieval_parameters: Optional[PresetRetrievalParams] = None,
) -> Retriever:
name = name or index.name
preset_retrieval_parameters = (
preset_retrieval_parameters or index.pipeline.preset_retrieval_parameters
)
retriever_pipeline = RetrieverPipeline(
pipeline_id=index.id,
name=name,
description=description,
preset_retrieval_parameters=preset_retrieval_parameters,
)
current_retriever_pipelines_by_name = {
pipeline.name: pipeline for pipeline in (self.retriever_pipelines or [])
}
current_retriever_pipelines_by_name[
retriever_pipeline.name
] = retriever_pipeline
return self.update_retriever_pipelines(
list(current_retriever_pipelines_by_name.values())
)
def remove_index(self, name: str) -> bool:
current_retriever_pipeline_names = self.retriever.pipelines or [] # type: ignore [union-attr]
new_retriever_pipelines = [
pipeline
for pipeline in current_retriever_pipeline_names
if pipeline.name != name
]
if len(new_retriever_pipelines) == len(current_retriever_pipeline_names):
return False
self.update_retriever_pipelines(new_retriever_pipelines)
return True
async def aupdate_retriever_pipelines(
self, pipelines: List[RetrieverPipeline]
) -> Retriever:
if self._persisted:
self.retriever = await self._aclient.retrievers.update_retriever(
self.retriever.id, pipelines=pipelines # type: ignore [union-attr]
)
else:
# Update in-memory retriever for non-persisted case using copy
self.retriever = self.retriever.copy(update={"pipelines": pipelines}) # type: ignore [union-attr]
return self.retriever
async def async_add_index(
self,
index: LlamaCloudIndex,
name: Optional[str] = None,
description: Optional[str] = None,
preset_retrieval_parameters: Optional[PresetRetrievalParams] = None,
) -> Retriever:
name = name or index.name
preset_retrieval_parameters = (
preset_retrieval_parameters or index.pipeline.preset_retrieval_parameters
)
retriever_pipeline = RetrieverPipeline(
pipeline_id=index.id,
name=name,
description=description,
preset_retrieval_parameters=preset_retrieval_parameters,
)
current_retriever_pipelines_by_name = {
pipeline.name: pipeline for pipeline in (self.retriever_pipelines or [])
}
current_retriever_pipelines_by_name[
retriever_pipeline.name
] = retriever_pipeline
return await self.aupdate_retriever_pipelines(
list(current_retriever_pipelines_by_name.values())
)
async def aremove_index(self, name: str) -> bool:
current_retriever_pipeline_names = self.retriever.pipelines or [] # type: ignore [union-attr]
new_retriever_pipelines = [
pipeline
for pipeline in current_retriever_pipeline_names
if pipeline.name != name
]
if len(new_retriever_pipelines) == len(current_retriever_pipeline_names):
return False
await self.aupdate_retriever_pipelines(new_retriever_pipelines)
return True
def _result_nodes_to_node_with_score(
self, composite_retrieval_node: CompositeRetrievedTextNodeWithScore
) -> NodeWithScore:
return NodeWithScore(
node=TextNode(
id=composite_retrieval_node.node.id,
text=composite_retrieval_node.node.text,
metadata=composite_retrieval_node.node.metadata,
),
score=composite_retrieval_node.score,
)
def _retrieve(
self,
query_bundle: QueryBundle,
mode: Optional[CompositeRetrievalMode] = None,
rerank_top_n: Optional[int] = None,
rerank_config: Optional[ReRankConfig] = None,
) -> List[NodeWithScore]:
mode = mode if mode is not None else self._mode
rerank_top_n = rerank_top_n if rerank_top_n is not None else self._rerank_top_n
rerank_config = (
rerank_config if rerank_config is not None else self._rerank_config
)
if self._persisted:
result = self._client.retrievers.retrieve(
self.retriever.id, # type: ignore [union-attr]
mode=mode,
rerank_top_n=rerank_top_n,
rerank_config=rerank_config,
query=query_bundle.query_str,
)
else:
result = self._client.retrievers.direct_retrieve(
project_id=self.project.id,
mode=mode,
rerank_top_n=rerank_top_n,
rerank_config=rerank_config,
query=query_bundle.query_str,
pipelines=self.retriever.pipelines, # type: ignore [union-attr]
)
node_w_scores = [
self._result_nodes_to_node_with_score(node) for node in result.nodes # type: ignore [union-attr]
]
image_nodes_w_scores = page_screenshot_nodes_to_node_with_score(
self._client, result.image_nodes, self.retriever.project_id # type: ignore [union-attr]
)
return sorted(
node_w_scores + image_nodes_w_scores, key=lambda x: x.score, reverse=True
)
async def _aretrieve(
self,
query_bundle: QueryBundle,
mode: Optional[CompositeRetrievalMode] = None,
rerank_top_n: Optional[int] = None,
rerank_config: Optional[ReRankConfig] = None,
) -> List[NodeWithScore]:
mode = mode if mode is not None else self._mode
rerank_top_n = rerank_top_n if rerank_top_n is not None else self._rerank_top_n
rerank_config = (
rerank_config if rerank_config is not None else self._rerank_config
)
if self._persisted:
result = await self._aclient.retrievers.retrieve(
self.retriever.id, # type: ignore [union-attr]
mode=mode,
rerank_config=rerank_config,
rerank_top_n=rerank_top_n,
query=query_bundle.query_str,
)
else:
result = await self._aclient.retrievers.direct_retrieve(
project_id=self.project.id,
mode=mode,
rerank_top_n=rerank_top_n,
rerank_config=rerank_config,
query=query_bundle.query_str,
pipelines=self.retriever.pipelines, # type: ignore [union-attr]
)
node_w_scores = [
self._result_nodes_to_node_with_score(node) for node in result.nodes # type: ignore [union-attr]
]
image_nodes_w_scores = page_screenshot_nodes_to_node_with_score(
self._aclient, result.image_nodes, self.retriever.project_id # type: ignore [union-attr]
)
return sorted(
node_w_scores + image_nodes_w_scores, key=lambda x: x.score, reverse=True
)
+217
View File
@@ -0,0 +1,217 @@
from typing import Any, List, Optional
import httpx
from llama_cloud import (
TextNodeWithScore,
)
from llama_cloud.resources.pipelines.client import OMIT
from llama_index.core.base.base_retriever import BaseRetriever
from llama_index.core.bridge.pydantic import BaseModel
from llama_index.core.constants import DEFAULT_PROJECT_NAME
from llama_index.core.ingestion.api_utils import get_aclient, get_client
from llama_index.core.schema import NodeWithScore, QueryBundle, TextNode
from llama_index.core.vector_stores.types import MetadataFilters
from .api_utils import (
resolve_project_and_pipeline,
page_screenshot_nodes_to_node_with_score,
page_figure_nodes_to_node_with_score,
apage_screenshot_nodes_to_node_with_score,
apage_figure_nodes_to_node_with_score,
)
import logging
logger = logging.getLogger(__name__)
class LlamaCloudRetriever(BaseRetriever):
def __init__(
self,
# index identifier
name: Optional[str] = None,
index_id: Optional[str] = None, # alias for pipeline_id
id: Optional[str] = None, # alias for pipeline_id
pipeline_id: Optional[str] = None,
# project identifier
project_name: Optional[str] = DEFAULT_PROJECT_NAME,
project_id: Optional[str] = None,
organization_id: Optional[str] = None,
# connection params
api_key: Optional[str] = None,
base_url: Optional[str] = None,
app_url: Optional[str] = None,
timeout: int = 60,
httpx_client: Optional[httpx.Client] = None,
async_httpx_client: Optional[httpx.AsyncClient] = None,
# retrieval params
dense_similarity_top_k: Optional[int] = None,
sparse_similarity_top_k: Optional[int] = None,
enable_reranking: Optional[bool] = None,
rerank_top_n: Optional[int] = None,
alpha: Optional[float] = None,
filters: Optional[MetadataFilters] = None,
retrieval_mode: Optional[str] = None,
files_top_k: Optional[int] = None,
retrieve_image_nodes: Optional[bool] = None,
retrieve_page_screenshot_nodes: Optional[bool] = None,
retrieve_page_figure_nodes: Optional[bool] = None,
search_filters_inference_schema: Optional[BaseModel] = None,
**kwargs: Any,
) -> None:
"""Initialize the Platform Retriever."""
if sum([bool(id), bool(index_id), bool(pipeline_id), bool(name)]) != 1:
raise ValueError(
"Exactly one of `name`, `id`, `pipeline_id` or `index_id` must be provided to identify the index."
)
# initialize clients
self._httpx_client = httpx_client
self._async_httpx_client = async_httpx_client
self._client = get_client(api_key, base_url, app_url, timeout, httpx_client)
self._aclient = get_aclient(
api_key, base_url, app_url, timeout, async_httpx_client
)
pipeline_id = id or index_id or pipeline_id
self.project, self.pipeline = resolve_project_and_pipeline(
self._client, name, pipeline_id, project_name, project_id, organization_id
)
self.name = self.pipeline.name
self.project_name = self.project.name
# retrieval params
self._dense_similarity_top_k = (
dense_similarity_top_k if dense_similarity_top_k is not None else OMIT
)
self._sparse_similarity_top_k = (
sparse_similarity_top_k if sparse_similarity_top_k is not None else OMIT
)
self._enable_reranking = (
enable_reranking if enable_reranking is not None else OMIT
)
self._rerank_top_n = rerank_top_n if rerank_top_n is not None else OMIT
self._alpha = alpha if alpha is not None else OMIT
self._filters = filters if filters is not None else OMIT
self._retrieval_mode = retrieval_mode if retrieval_mode is not None else OMIT
self._files_top_k = files_top_k if files_top_k is not None else OMIT
if retrieve_image_nodes is not None:
logger.warning(
"The `retrieve_image_nodes` parameter is deprecated. "
"Use `retrieve_page_screenshot_nodes` and `retrieve_page_figure_nodes` instead."
)
if retrieve_image_nodes:
if (
retrieve_page_screenshot_nodes is False
or retrieve_page_figure_nodes is False
):
raise ValueError(
"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."
)
retrieve_page_screenshot_nodes = True
retrieve_page_figure_nodes = True
self._retrieve_page_screenshot_nodes = (
retrieve_page_screenshot_nodes
if retrieve_page_screenshot_nodes is not None
else OMIT
)
self._retrieve_page_figure_nodes = (
retrieve_page_figure_nodes
if retrieve_page_figure_nodes is not None
else OMIT
)
self._search_filters_inference_schema = search_filters_inference_schema
super().__init__(
callback_manager=kwargs.get("callback_manager"),
verbose=kwargs.get("verbose", False),
)
def _result_nodes_to_node_with_score(
self, result_nodes: List[TextNodeWithScore]
) -> List[NodeWithScore]:
nodes = []
for res in result_nodes:
text_node = TextNode.parse_obj(res.node.dict())
nodes.append(NodeWithScore(node=text_node, score=res.score))
return nodes
def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]:
"""Retrieve from the platform."""
search_filters_inference_schema = OMIT
if self._search_filters_inference_schema is not None:
search_filters_inference_schema = (
self._search_filters_inference_schema.model_json_schema()
)
results = self._client.pipelines.run_search(
query=query_bundle.query_str,
pipeline_id=self.pipeline.id,
dense_similarity_top_k=self._dense_similarity_top_k,
sparse_similarity_top_k=self._sparse_similarity_top_k,
enable_reranking=self._enable_reranking,
rerank_top_n=self._rerank_top_n,
alpha=self._alpha,
search_filters=self._filters,
files_top_k=self._files_top_k,
retrieval_mode=self._retrieval_mode,
retrieve_page_screenshot_nodes=self._retrieve_page_screenshot_nodes,
retrieve_page_figure_nodes=self._retrieve_page_figure_nodes,
search_filters_inference_schema=search_filters_inference_schema,
)
result_nodes = self._result_nodes_to_node_with_score(results.retrieval_nodes)
if self._retrieve_page_screenshot_nodes:
result_nodes.extend(
page_screenshot_nodes_to_node_with_score(
self._client, results.image_nodes, self.project.id
)
)
if self._retrieve_page_figure_nodes:
result_nodes.extend(
page_figure_nodes_to_node_with_score(
self._client, results.page_figure_nodes, self.project.id
)
)
return result_nodes
async def _aretrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]:
"""Asynchronously retrieve from the platform."""
search_filters_inference_schema = OMIT
if self._search_filters_inference_schema is not None:
search_filters_inference_schema = (
self._search_filters_inference_schema.model_json_schema()
)
results = await self._aclient.pipelines.run_search(
query=query_bundle.query_str,
pipeline_id=self.pipeline.id,
dense_similarity_top_k=self._dense_similarity_top_k,
sparse_similarity_top_k=self._sparse_similarity_top_k,
enable_reranking=self._enable_reranking,
rerank_top_n=self._rerank_top_n,
alpha=self._alpha,
search_filters=self._filters,
files_top_k=self._files_top_k,
retrieval_mode=self._retrieval_mode,
retrieve_page_screenshot_nodes=self._retrieve_page_screenshot_nodes,
retrieve_page_figure_nodes=self._retrieve_page_figure_nodes,
search_filters_inference_schema=search_filters_inference_schema,
)
result_nodes = self._result_nodes_to_node_with_score(results.retrieval_nodes)
if self._retrieve_page_screenshot_nodes:
result_nodes.extend(
await apage_screenshot_nodes_to_node_with_score(
self._aclient, results.image_nodes, self.project.id
)
)
if self._retrieve_page_figure_nodes:
result_nodes.extend(
await apage_figure_nodes_to_node_with_score(
self._aclient, results.page_figure_nodes, self.project.id
)
)
return result_nodes
@@ -0,0 +1,8 @@
from llama_cloud_services.parse.base import (
LlamaParse,
ResultType,
ParsingMode,
FailedPageMode,
)
__all__ = ["LlamaParse", "ResultType", "ParsingMode", "FailedPageMode"]
@@ -2,30 +2,42 @@ import asyncio
import mimetypes
import os
import time
import warnings
from contextlib import asynccontextmanager
from copy import deepcopy
from enum import Enum
from io import BufferedIOBase
from pathlib import Path, PurePath, PurePosixPath
from typing import Any, AsyncGenerator, Dict, List, Optional, Union
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
from urllib.parse import urlparse
import httpx
from fsspec import AbstractFileSystem
from llama_index.core.async_utils import asyncio_run, run_jobs
from llama_index.core.bridge.pydantic import Field, PrivateAttr, field_validator
from llama_index.core.bridge.pydantic import (
Field,
PrivateAttr,
field_validator,
model_validator,
)
from llama_index.core.constants import DEFAULT_BASE_URL
from llama_index.core.readers.base import BasePydanticReader
from llama_index.core.readers.file.base import get_default_fs
from llama_index.core.schema import Document
from llama_cloud_services.utils import check_extra_params
from llama_cloud_services.parse.types import JobResult
from llama_cloud_services.parse.utils import (
SUPPORTED_FILE_TYPES,
ResultType,
ParsingMode,
FailedPageMode,
expand_target_pages,
nest_asyncio_err,
nest_asyncio_msg,
make_api_request,
partition_pages,
extract_tables_from_json_results,
)
# can put in a path to the file or the file bytes itself
@@ -55,6 +67,36 @@ def build_url(
return base_url
class JobFailedException(Exception):
"""Parse job failed exception."""
def __init__(
self,
job_id: str,
status: str,
error_code: Optional[str] = None,
error_message: Optional[str] = None,
):
exception_str = (
f"Job ID: {job_id} failed with status: {status}, "
f'Error code: {error_code or "No error code found"}, '
f'Error message: {error_message or "No error message found"}'
)
super().__init__(exception_str)
self.job_id = job_id
self.status = status
self.error_code = error_code
self.error_message = error_message
@classmethod
def from_result(cls, result_json: Dict[str, Any]) -> "JobFailedException":
job_id = result_json["id"]
status = result_json["status"]
error_code = result_json.get("error_code")
error_message = result_json.get("error_message")
return cls(job_id, status, error_code=error_code, error_message=error_message)
class BackoffPattern(str, Enum):
"""Backoff pattern for polling."""
@@ -113,7 +155,7 @@ class LlamaParse(BasePydanticReader):
num_workers: int = Field(
default=4,
gt=0,
lt=10,
lt=20,
description="The number of workers to use sending API requests for parsing.",
)
result_type: ResultType = Field(
@@ -143,6 +185,10 @@ class LlamaParse(BasePydanticReader):
default=False,
description="If set to true, the parser will automatically select the best mode to extract text from documents based on the rules provide. Will use the 'accurate' default mode by default and will upgrade page that match the rule to Premium mode.",
)
auto_mode_configuration_json: Optional[str] = Field(
default=None,
description="A JSON string containing the configuration for the auto mode. If set, the parser will use the provided configuration for the auto mode.",
)
auto_mode_trigger_on_image_in_page: Optional[bool] = Field(
default=False,
description="If auto_mode is set to true, the parser will upgrade the page that contain an image to Premium mode.",
@@ -228,6 +274,10 @@ class LlamaParse(BasePydanticReader):
default=False,
description="Whether to guess the sheet names of the xlsx file.",
)
high_res_ocr: Optional[bool] = Field(
default=False,
description="If set to true, the parser will use high resolution OCR to extract text from images. This will increase the accuracy of the parsing job, but reduce the speed.",
)
html_make_all_elements_visible: Optional[bool] = Field(
default=False,
description="If set to true, when parsing HTML the parser will consider all elements display not element as display block.",
@@ -267,10 +317,18 @@ class LlamaParse(BasePydanticReader):
language: Optional[str] = Field(
default="en", description="The language of the text to parse."
)
markdown_table_multiline_header_separator: Optional[str] = Field(
default=None,
description="The separator to use to split the header of the markdown table into multiple lines. Default is: <br/>",
)
max_pages: Optional[int] = Field(
default=None,
description="The maximum number of pages to extract text from documents. If set to 0 or not set, all pages will be that should be extracted will be extracted (can work in combination with targetPages).",
)
merge_tables_across_pages_in_markdown: Optional[bool] = Field(
default=False,
description="If set to true, the parser will merge tables across pages in the markdown output. This is useful for documents with tables that span across multiple pages.",
)
output_pdf_of_document: Optional[bool] = Field(
default=False,
description="If set to true, the parser will also output a PDF of the document. (except for spreadsheets)",
@@ -287,6 +345,14 @@ class LlamaParse(BasePydanticReader):
default=False,
description="If set to true, the parser will output tables as HTML in the markdown.",
)
outlined_table_extraction: Optional[bool] = Field(
default=False,
description="If set to true, the parser will use a dedicated approach to extract tables with outlined cells. This is useful for documents with spreadsheet-like tables where cells are outlined with borders. This could lead to false positives, so use with caution.",
)
page_error_tolerance: Optional[float] = Field(
default=None,
description="The error tolerance for the number of pages with error in a doc (percentage express as 0-1). If we fail to parse a greater percentage of pages than the tolerance value we fail the job.",
)
page_prefix: Optional[str] = Field(
default=None,
description="A templated prefix to add to the beginning of each page. If it contain `{page_number}`, it will be replaced by the page number.",
@@ -299,7 +365,7 @@ class LlamaParse(BasePydanticReader):
default=None,
description="A templated suffix to add to the beginning of each page. If it contain `{page_number}`, it will be replaced by the page number.",
)
parse_mode: Optional[str] = Field(
parse_mode: Optional[Union[ParsingMode, str]] = Field(
default=None,
description="The parsing mode to use, see ParsingMode enum for possible values ",
)
@@ -307,10 +373,30 @@ class LlamaParse(BasePydanticReader):
default=False,
description="Use our best parser mode if set to True.",
)
preset: Optional[str] = Field(
default=None,
description="The preset to use for the parser. If set, the parser will use the preset configuration. See LlamaParse documentation for available presets. Preset override most other parameters.",
)
preserve_layout_alignment_across_pages: Optional[bool] = Field(
default=False,
description="Preserve grid alignment across page in text mode.",
)
preserve_very_small_text: Optional[bool] = Field(
default=False,
description="If set, the parser will try to preserve very small text lines. This can be useful for documents containing vector graphics with very small text lines that may not be recognized by OCR or a vision model (such as in CAD drawings).",
)
replace_failed_page_mode: Optional[FailedPageMode] = Field(
default=None,
description="The mode to use to replace the failed page, see FailedPageMode enum for possible value. If set, the parser will replace the failed page with the specified mode. If not set, the default mode (raw_text) will be used.",
)
replace_failed_page_with_error_message_prefix: Optional[str] = Field(
default=None,
description="A prefix to add before error message in failed pages. If not set, no prefix will be used.",
)
replace_failed_page_with_error_message_suffix: Optional[str] = Field(
default=None,
description="A suffix to add after error message in failed pages. If not set, no suffix will be used.",
)
skip_diagonal_text: Optional[bool] = Field(
default=False,
description="If set to true, the parser will ignore diagonal text (when the text rotation in degrees modulo 90 is not 0).",
@@ -380,10 +466,42 @@ class LlamaParse(BasePydanticReader):
default=None,
description="The model name for the vendor multimodal API.",
)
model: Optional[str] = Field(
default=None,
description="The document model name to be used with `parse_with_agent`.",
)
webhook_url: Optional[str] = Field(
default=None,
description="A URL that needs to be called at the end of the parsing job.",
)
partition_pages: Optional[int] = Field(
default=None,
description="If set, documents will automatically be partitioned into segments containing the specified number of pages at most. Parsing will be split into separate jobs for each partition segment. Can be used in combination with targetPages and maxPages.",
)
hide_headers: Optional[bool] = Field(
default=False,
description="Whether to hide page header in output markdown.",
)
hide_footers: Optional[bool] = Field(
default=False,
description="Whether to hide page footers in output markdown.",
)
page_header_suffix: Optional[str] = Field(
default=None,
description="A suffix to add to the page header in the output markdown.",
)
page_header_prefix: Optional[str] = Field(
default=None,
description="A prefix to add to the page header in the output markdown.",
)
page_footer_suffix: Optional[str] = Field(
default=None,
description="A suffix to add to the page footer in the output markdown.",
)
page_footer_prefix: Optional[str] = Field(
default=None,
description="A prefix to add to the page footer in the output markdown.",
)
# Deprecated
bounding_box: Optional[str] = Field(
@@ -423,6 +541,21 @@ class LlamaParse(BasePydanticReader):
description="Whether to use the vendor multimodal API.",
)
@model_validator(mode="before")
@classmethod
def warn_extra_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
extra_params, suggestions = check_extra_params(cls, data)
if extra_params:
suggestions = [f"\n - {suggestion}" for suggestion in suggestions]
suggestions_str = "".join(suggestions)
warnings.warn(
"The following parameters are unused: "
+ ", ".join(extra_params)
+ f".\n{suggestions_str}",
)
return data
@field_validator("api_key", mode="before", check_fields=True)
@classmethod
def validate_api_key(cls, v: str) -> str:
@@ -510,6 +643,7 @@ class LlamaParse(BasePydanticReader):
file_input: FileInput,
extra_info: Optional[dict] = None,
fs: Optional[AbstractFileSystem] = None,
partition_target_pages: Optional[str] = None,
) -> str:
files = None
file_handle = None
@@ -560,6 +694,9 @@ class LlamaParse(BasePydanticReader):
if self.auto_mode:
data["auto_mode"] = self.auto_mode
if self.auto_mode_configuration_json is not None:
data["auto_mode_configuration_json"] = self.auto_mode_configuration_json
if self.auto_mode_trigger_on_image_in_page:
data[
"auto_mode_trigger_on_image_in_page"
@@ -657,6 +794,9 @@ class LlamaParse(BasePydanticReader):
if self.html_make_all_elements_visible:
data["html_make_all_elements_visible"] = self.html_make_all_elements_visible
if self.high_res_ocr:
data["high_res_ocr"] = self.high_res_ocr
if self.html_remove_fixed_elements:
data["html_remove_fixed_elements"] = self.html_remove_fixed_elements
@@ -719,9 +859,38 @@ class LlamaParse(BasePydanticReader):
if self.output_tables_as_HTML:
data["output_tables_as_HTML"] = self.output_tables_as_HTML
if self.outlined_table_extraction:
data["outlined_table_extraction"] = self.outlined_table_extraction
if self.page_error_tolerance is not None:
data["page_error_tolerance"] = self.page_error_tolerance
if self.page_prefix is not None:
data["page_prefix"] = self.page_prefix
if self.merge_tables_across_pages_in_markdown:
data[
"merge_tables_across_pages_in_markdown"
] = self.merge_tables_across_pages_in_markdown
if self.hide_headers:
data["hide_headers"] = self.hide_headers
if self.hide_footers:
data["hide_footers"] = self.hide_footers
if self.page_header_suffix is not None:
data["page_header_suffix"] = self.page_header_suffix
if self.page_header_prefix is not None:
data["page_header_prefix"] = self.page_header_prefix
if self.page_footer_suffix is not None:
data["page_footer_suffix"] = self.page_footer_suffix
if self.page_footer_prefix is not None:
data["page_footer_prefix"] = self.page_footer_prefix
# only send page separator to server if it is not None
# as if a null, "" string is sent the server will then ignore the page separator instead of using the default
if self.page_separator is not None:
@@ -747,6 +916,25 @@ class LlamaParse(BasePydanticReader):
"preserve_layout_alignment_across_pages"
] = self.preserve_layout_alignment_across_pages
if self.preserve_very_small_text:
data["preserve_very_small_text"] = self.preserve_very_small_text
if self.preset is not None:
data["preset"] = self.preset
if self.replace_failed_page_mode is not None:
data["replace_failed_page_mode"] = self.replace_failed_page_mode.value
if self.replace_failed_page_with_error_message_prefix is not None:
data[
"replace_failed_page_with_error_message_prefix"
] = self.replace_failed_page_with_error_message_prefix
if self.replace_failed_page_with_error_message_suffix is not None:
data[
"replace_failed_page_with_error_message_suffix"
] = self.replace_failed_page_with_error_message_suffix
if self.skip_diagonal_text:
data["skip_diagonal_text"] = self.skip_diagonal_text
@@ -782,7 +970,9 @@ class LlamaParse(BasePydanticReader):
if self.take_screenshot:
data["take_screenshot"] = self.take_screenshot
if self.target_pages is not None:
if partition_target_pages is not None:
data["target_pages"] = partition_target_pages
elif self.target_pages is not None:
data["target_pages"] = self.target_pages
if self.user_prompt is not None:
data["user_prompt"] = self.user_prompt
@@ -795,9 +985,17 @@ class LlamaParse(BasePydanticReader):
if self.vendor_multimodal_model_name is not None:
data["vendor_multimodal_model_name"] = self.vendor_multimodal_model_name
if self.model is not None:
data["model"] = self.model
if self.webhook_url is not None:
data["webhook_url"] = self.webhook_url
if self.markdown_table_multiline_header_separator is not None:
data[
"markdown_table_multiline_header_separator"
] = self.markdown_table_multiline_header_separator
# Deprecated
if self.bounding_box is not None:
data["bounding_box"] = self.bounding_box
@@ -871,15 +1069,7 @@ class LlamaParse(BasePydanticReader):
print(".", end="", flush=True)
current_interval = self._calculate_backoff(current_interval)
else:
error_code = result_json.get("error_code", "No error code found")
error_message = result_json.get(
"error_message", "No error message found"
)
exception_str = (
f"Job ID: {job_id} failed with status: {status}, "
f"Error code: {error_code}, Error message: {error_message}"
)
raise Exception(exception_str)
raise JobFailedException.from_result(result_json)
except (
httpx.ConnectError,
httpx.ReadError,
@@ -888,6 +1078,7 @@ class LlamaParse(BasePydanticReader):
httpx.ReadTimeout,
httpx.WriteTimeout,
httpx.HTTPStatusError,
httpx.RemoteProtocolError,
) as err:
error_count += 1
end = time.time()
@@ -902,26 +1093,151 @@ class LlamaParse(BasePydanticReader):
)
current_interval = self._calculate_backoff(current_interval)
async def _parse_one(
self,
file_path: FileInput,
extra_info: Optional[dict] = None,
fs: Optional[AbstractFileSystem] = None,
result_type: Optional[str] = None,
num_workers: Optional[int] = None,
) -> List[Tuple[str, Dict[str, Any]]]:
if self.partition_pages is None:
job_results = [
await self._parse_one_unpartitioned(
file_path,
extra_info=extra_info,
fs=fs,
result_type=result_type,
)
]
else:
job_results = await self._parse_one_partitioned(
file_path,
extra_info,
fs=fs,
result_type=result_type,
num_workers=num_workers,
)
return job_results
async def _parse_one_unpartitioned(
self,
file_path: FileInput,
extra_info: Optional[dict] = None,
fs: Optional[AbstractFileSystem] = None,
result_type: Optional[str] = None,
**create_kwargs: Any,
) -> Tuple[str, Dict[str, Any]]:
"""Create one parse job and wait for the result."""
job_id = await self._create_job(
file_path, extra_info=extra_info, fs=fs, **create_kwargs
)
if self.verbose:
print("Started parsing the file under job_id %s" % job_id)
result = await self._get_job_result(
job_id, result_type or self.result_type.value, verbose=self.verbose
)
return job_id, result
async def _parse_one_partitioned(
self,
file_path: FileInput,
extra_info: Optional[dict] = None,
fs: Optional[AbstractFileSystem] = None,
result_type: Optional[str] = None,
num_workers: Optional[int] = None,
) -> List[Tuple[str, Dict[str, Any]]]:
"""Partition a file and run separate parse jobs per partition segment."""
assert self.partition_pages is not None
num_workers = num_workers or self.num_workers
if num_workers < 1:
raise ValueError("Invalid number of workers")
if self.target_pages is not None:
jobs = [
self._parse_one_unpartitioned(
file_path,
extra_info=extra_info,
fs=fs,
result_type=result_type,
partition_target_pages=target_pages,
)
for target_pages in partition_pages(
expand_target_pages(self.target_pages),
self.partition_pages,
max_pages=self.max_pages,
)
]
return await run_jobs(
jobs,
workers=num_workers,
desc="Getting job results",
show_progress=self.show_progress,
)
total = 0
results: List[Tuple[str, Dict[str, Any]]] = []
while self.max_pages is None or total < self.max_pages:
if (
self.max_pages is not None
and total + self.partition_pages >= self.max_pages
):
size = self.max_pages - total
else:
size = self.partition_pages
if not size:
break
try:
# Fetch JSON result type first to get accurate pagination data
# and then fetch the user's desired result type if needed
job_id, json_result = await self._parse_one_unpartitioned(
file_path,
extra_info=extra_info,
fs=fs,
result_type=ResultType.JSON.value,
partition_target_pages=f"{total}-{total + size - 1}",
)
result_type = result_type or self.result_type.value
if result_type == ResultType.JSON.value:
job_result = json_result
else:
job_result = await self._get_job_result(
job_id, result_type, verbose=self.verbose
)
except JobFailedException as e:
if results and e.error_code == "NO_DATA_FOUND_IN_FILE":
# Expected when we try to read past the end of the file
return results
raise
results.append((job_id, job_result))
if len(json_result["pages"]) < size:
break
total += size
return results
async def _aload_data(
self,
file_path: FileInput,
extra_info: Optional[dict] = None,
fs: Optional[AbstractFileSystem] = None,
verbose: bool = False,
num_workers: Optional[int] = None,
) -> List[Document]:
"""Load data from the input path."""
try:
job_id = await self._create_job(file_path, extra_info=extra_info, fs=fs)
if verbose:
print("Started parsing the file under job_id %s" % job_id)
result = await self._get_job_result(
job_id, self.result_type.value, verbose=verbose
)
results = [
job_result
for _, job_result in await self._parse_one(
file_path, extra_info, fs=fs, num_workers=num_workers
)
]
# Flatten the resulting doc if it was partitioned
separator = self.page_separator or _DEFAULT_SEPARATOR
docs = [
Document(
text=result[self.result_type.value],
text=separator.join(
result[self.result_type.value] for result in results
),
metadata=extra_info or {},
)
]
@@ -944,7 +1260,11 @@ class LlamaParse(BasePydanticReader):
extra_info: Optional[dict] = None,
fs: Optional[AbstractFileSystem] = None,
) -> List[Document]:
"""Load data from the input path."""
"""Load data from the input path.
File(s) which were partitioned before parsing will be loaded as a single
re-assembled Document.
"""
if isinstance(file_path, (str, PurePosixPath, Path, bytes, BufferedIOBase)):
return await self._aload_data(
file_path, extra_info=extra_info, fs=fs, verbose=self.verbose
@@ -956,6 +1276,7 @@ class LlamaParse(BasePydanticReader):
extra_info=extra_info,
fs=fs,
verbose=self.verbose and not self.show_progress,
num_workers=1,
)
for f in file_path
]
@@ -994,6 +1315,34 @@ class LlamaParse(BasePydanticReader):
else:
raise e
async def _aparse_one(
self,
file_path: FileInput,
file_name: str,
extra_info: Optional[dict] = None,
fs: Optional[AbstractFileSystem] = None,
num_workers: Optional[int] = None,
) -> List[JobResult]:
job_results = await self._parse_one(
file_path,
extra_info,
fs=fs,
result_type=ResultType.JSON.value,
num_workers=num_workers,
)
return [
JobResult(
job_id=job_id,
file_name=file_name,
job_result=job_result,
api_key=self.api_key,
base_url=self.base_url,
client=self.aclient,
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
)
for job_id, job_result in job_results
]
async def aparse(
self,
file_path: Union[List[FileInput], FileInput],
@@ -1012,14 +1361,10 @@ class LlamaParse(BasePydanticReader):
fs: Optional filesystem to use for reading files.
Returns:
JobResult object or list of JobResult objects if multiple files were provided
JobResult object or list of JobResult objects if either multiple files were provided or file(s) were partitioned before parsing.
"""
if isinstance(file_path, (str, PurePosixPath, Path, bytes, BufferedIOBase)):
job_id = await self._create_job(file_path, extra_info=extra_info, fs=fs)
if self.verbose:
print("Started parsing the file under job_id %s" % job_id)
if isinstance(file_path, (bytes, BufferedIOBase)):
if not extra_info or "file_name" not in extra_info:
raise ValueError(
@@ -1028,29 +1373,12 @@ class LlamaParse(BasePydanticReader):
file_name = extra_info["file_name"]
else:
file_name = str(file_path)
job_result = await self._get_job_result(
job_id, ResultType.JSON.value, verbose=self.verbose
)
return JobResult(
job_id=job_id,
file_name=file_name,
job_result=job_result,
api_key=self.api_key,
base_url=self.base_url,
client=self.aclient,
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
result = await self._aparse_one(
file_path, file_name, extra_info=extra_info, fs=fs
)
return result[0] if len(result) == 1 else result
elif isinstance(file_path, list):
jobs = [
self._create_job(
f,
extra_info=extra_info,
fs=fs,
)
for f in file_path
]
file_names = []
for f in file_path:
if isinstance(f, (bytes, BufferedIOBase)):
@@ -1062,40 +1390,24 @@ class LlamaParse(BasePydanticReader):
else:
file_names.append(str(f))
job_results = []
try:
job_ids = await run_jobs(
jobs,
workers=self.num_workers,
desc="Creating parsing jobs",
show_progress=self.show_progress,
)
job_results = await run_jobs(
for result in await run_jobs(
[
self._get_job_result(
job_id, ResultType.JSON.value, verbose=self.verbose
self._aparse_one(
f,
file_names[i],
extra_info=extra_info,
fs=fs,
num_workers=1,
)
for job_id in job_ids
for i, f in enumerate(file_path)
],
workers=self.num_workers,
desc="Getting job results",
show_progress=self.show_progress,
)
# Create JobResults just using the job_ids and job_results
job_results = [
JobResult(
job_id=job_id,
file_name=file_names[i],
job_result=job_results[i],
api_key=self.api_key,
base_url=self.base_url,
client=self.aclient,
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
)
for i, job_id in enumerate(job_ids)
]
):
job_results.extend(result)
return job_results
except RuntimeError as e:
@@ -1137,20 +1449,27 @@ class LlamaParse(BasePydanticReader):
raise e
async def _aget_json(
self, file_path: FileInput, extra_info: Optional[dict] = None
self,
file_path: FileInput,
extra_info: Optional[dict] = None,
num_workers: Optional[int] = None,
) -> List[dict]:
"""Load data from the input path."""
try:
job_id = await self._create_job(file_path, extra_info=extra_info)
if self.verbose:
print("Started parsing the file under job_id %s" % job_id)
result = await self._get_job_result(job_id, "json")
result["job_id"] = job_id
job_results = await self._parse_one(
file_path,
extra_info=extra_info,
result_type=ResultType.JSON.value,
num_workers=num_workers,
)
if not isinstance(file_path, (bytes, BufferedIOBase)):
result["file_path"] = str(file_path)
return [result]
results = []
for job_id, job_result in job_results:
job_result["job_id"] = job_id
if not isinstance(file_path, (bytes, BufferedIOBase)):
job_result["file_path"] = str(file_path)
results.append(job_result)
return results
except Exception as e:
file_repr = file_path if isinstance(file_path, str) else "<bytes/buffer>"
print(f"Error while parsing the file '{file_repr}':", e)
@@ -1165,7 +1484,7 @@ class LlamaParse(BasePydanticReader):
extra_info: Optional[dict] = None,
) -> List[dict]:
"""Load data from the input path."""
if isinstance(file_path, (str, Path)):
if isinstance(file_path, (str, PurePosixPath, Path, bytes, BufferedIOBase)):
return await self._aget_json(file_path, extra_info=extra_info)
elif isinstance(file_path, list):
jobs = [self._aget_json(f, extra_info=extra_info) for f in file_path]
@@ -1186,7 +1505,7 @@ class LlamaParse(BasePydanticReader):
raise e
else:
raise ValueError(
"The input file_path must be a string or a list of strings."
"The input file_path must be a string, Path, bytes, BufferedIOBase, or a list of these types."
)
def get_json_result(
@@ -1309,6 +1628,16 @@ class LlamaParse(BasePydanticReader):
else:
raise e
def get_tables(self, json_results: List[dict], download_path: str) -> List[str]:
if not os.path.exists(download_path):
os.makedirs(download_path)
return extract_tables_from_json_results(json_results, download_path)
async def aget_tables(
self, json_result: List[dict], download_path: str
) -> List[str]:
return await asyncio.to_thread(self.get_tables, json_result, download_path)
async def aget_xlsx(
self, json_result: List[dict], download_path: str
) -> List[dict]:
@@ -1375,3 +1704,79 @@ class LlamaParse(BasePydanticReader):
sub_docs.append(sub_doc)
return sub_docs
async def aget_result(
self, job_id: Union[str, List[str]]
) -> Union[JobResult, List[JobResult]]:
"""
Return JobResult object for previously parsed job(s).
If the job is still pending, the result will not be returned until it is completed.
Args:
job_id: Job ID or list of multiple Job IDs to be retrieved.
Returns:
JobResult object or list of JobResult objects if multiple job IDs were provided.
"""
if isinstance(job_id, str):
result = await self._get_job_result(
job_id, ResultType.JSON.value, verbose=self.verbose
)
return JobResult(
job_id=job_id,
file_name="",
job_result=result,
api_key=self.api_key,
base_url=self.base_url,
client=self.aclient,
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
)
elif isinstance(job_id, list):
results = []
jobs = [
self._get_job_result(id_, ResultType.JSON.value, verbose=self.verbose)
for id_ in job_id
]
results = await run_jobs(
jobs,
workers=self.num_workers,
desc="Getting job results",
show_progress=self.show_progress,
)
return [
JobResult(
job_id=job_id[i],
file_name="",
job_result=result,
api_key=self.api_key,
base_url=self.base_url,
client=self.aclient,
page_separator=self.page_separator or _DEFAULT_SEPARATOR,
)
for i, result in enumerate(results)
]
else:
raise ValueError("The input job_id must be a string or a list of strings.")
def get_result(
self, job_id: Union[str, List[str]]
) -> Union[JobResult, List[JobResult]]:
"""
Return JobResult object for previously parsed job(s).
If the job is still pending, the result will not be returned until it is completed.
Args:
job_id: Job ID or list of multiple Job IDs to be retrieved.
Returns:
JobResult object or list of JobResult objects if multiple job IDs were provided.
"""
try:
return asyncio_run(self.aget_result(job_id))
except RuntimeError as e:
if nest_asyncio_err in str(e):
raise RuntimeError(nest_asyncio_msg)
else:
raise e
@@ -14,14 +14,14 @@ PAGE_REGEX = r"page[-_](\d+)\.jpg$"
class JobMetadata(BaseModel):
"""Metadata about the job."""
job_credits_usage: int = Field(
default_factory=dict, description="The credits usage for the job."
job_pages: int = Field(default=0, description="The number of pages in the job.")
job_auto_mode_triggered_pages: Optional[int] = Field(
default=None,
description="The number of pages that triggered auto mode (thus increasing the cost).",
)
job_pages: int = Field(description="The number of pages in the job.")
job_auto_mode_triggered_pages: int = Field(
description="The number of pages that triggered auto mode (thus increasing the cost)."
job_is_cache_hit: bool = Field(
default=False, description="Whether the job was a cache hit."
)
job_is_cache_hit: bool = Field(description="Whether the job was a cache hit.")
class BBox(BaseModel):
@@ -46,22 +46,34 @@ class PageItem(BaseModel):
md: Optional[str] = Field(
default=None, description="The markdown-formatted content of the item."
)
rows: Optional[List[List[str]]] = Field(
rows: Optional[List[List[Any]]] = Field(
default=None, description="The rows of the item."
)
bBox: BBox = Field(description="The bounding box of the item.")
bBox: Optional[BBox] = Field(
default=None, description="The bounding box of the item."
)
class ImageItem(BaseModel):
"""An image in a page."""
name: str = Field(description="The name of the image.")
height: float = Field(description="The height of the image.")
width: float = Field(description="The width of the image.")
x: float = Field(description="The x-coordinate of the image.")
y: float = Field(description="The y-coordinate of the image.")
original_width: int = Field(description="The original width of the image.")
original_height: int = Field(description="The original height of the image.")
height: Optional[float] = Field(
default=None, description="The height of the image."
)
width: Optional[float] = Field(default=None, description="The width of the image.")
x: Optional[float] = Field(
default=None, description="The x-coordinate of the image."
)
y: Optional[float] = Field(
default=None, description="The y-coordinate of the image."
)
original_width: Optional[int] = Field(
default=None, description="The original width of the image."
)
original_height: Optional[int] = Field(
default=None, description="The original height of the image."
)
type: Optional[str] = Field(default=None, description="The type of the image.")
@@ -71,7 +83,9 @@ class LayoutItem(BaseModel):
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.")
bbox: BBox = Field(description="The bounding box 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.")
@@ -79,18 +93,24 @@ class ChartItem(BaseModel):
"""A chart in a page."""
name: str = Field(description="The name of the chart.")
x: float = Field(description="The x-coordinate of the chart.")
y: float = Field(description="The y-coordinate of the chart.")
width: float = Field(description="The width of the chart.")
height: float = Field(description="The height of the chart.")
x: Optional[float] = Field(
default=None, description="The x-coordinate of the chart."
)
y: Optional[float] = Field(
default=None, description="The y-coordinate of the chart."
)
width: Optional[float] = Field(default=None, description="The width of the chart.")
height: Optional[float] = Field(
default=None, description="The height of the chart."
)
class Page(BaseModel):
"""A page of the document."""
page: int = Field(description="The page number.")
text: str = Field(description="The text of the page.")
md: str = Field(description="The markdown of the page.")
text: Optional[str] = Field(default=None, description="The text of the page.")
md: Optional[str] = Field(default=None, description="The markdown of the page.")
images: List[ImageItem] = Field(
default_factory=list,
description="The names of the image IDs in the page, including both objects and page screenshots.",
@@ -107,32 +127,43 @@ class Page(BaseModel):
items: List[PageItem] = Field(
default_factory=list, description="The items in the page."
)
status: str = Field(description="The status of the page.")
status: Optional[str] = Field(default=None, description="The status of the page.")
links: List[SerializeAsAny[Any]] = Field(
default_factory=list, description="The links in the page."
)
width: float = Field(description="The width of the page.")
height: float = Field(description="The height of the page.")
triggeredAutoMode: bool = Field(
description="Whether the page triggered auto mode (thus increasing the cost)."
width: Optional[float] = Field(default=None, description="The width of the page.")
height: Optional[float] = Field(default=None, description="The height of the page.")
triggeredAutoMode: Optional[bool] = Field(
default=False,
description="Whether the page triggered auto mode (thus increasing the cost).",
)
parsingMode: str = Field(
default="", description="The parsing mode used for the page."
)
parsingMode: str = Field(description="The parsing mode used for the page.")
structuredData: Optional[Dict[str, Any]] = Field(
description="The structured data of the page."
default=None, description="The structured data of the page."
)
noStructuredContent: bool = Field(
description="Whether the page has no structured data."
default=True, description="Whether the page has no structured data."
)
noTextContent: bool = Field(
default=False, description="Whether the page has no text content."
)
noTextContent: bool = Field(description="Whether the page has no text content.")
class JobResult(BaseModel):
"""The raw JSON result from the LlamaParse API."""
pages: List[Page] = Field(description="The pages of the document.")
job_metadata: JobMetadata = Field(description="The metadata of the job.")
file_name: str = Field(description="The path to the file that was parsed.")
job_id: str = Field(description="The ID of the job.")
pages: List[Page] = Field(
default_factory=list, description="The pages of the document."
)
job_metadata: JobMetadata = Field(
default_factory=JobMetadata, description="The metadata of the job."
)
file_name: str = Field(
default="", description="The path to the file that was parsed."
)
job_id: str = Field(default="", description="The ID of the job.")
is_done: bool = Field(default=False, description="Whether the job is done.")
error: Optional[str] = Field(
default=None, description="The error message if the job failed."
@@ -185,7 +216,9 @@ class JobResult(BaseModel):
for page in self.pages
]
else:
text = self._page_separator.join([page.text for page in self.pages])
text = self._page_separator.join(
[page.text if page.text is not None else "" for page in self.pages]
)
return [Document(text=text, metadata={"file_name": self.file_name})]
async def aget_text_documents(self, split_by_page: bool = False) -> List[Document]:
@@ -230,7 +263,9 @@ class JobResult(BaseModel):
else:
return [
Document(
text=self._page_separator.join([page.md for page in self.pages]),
text=self._page_separator.join(
[page.md if page.md is not None else "" for page in self.pages]
),
metadata={"file_name": self.file_name},
)
]
@@ -260,13 +295,64 @@ class JobResult(BaseModel):
async def aget_markdown_nodes(self, split_by_page: bool = False) -> List[TextNode]:
"""
Get the markdown nodes from the job.
Args:
split_by_page: Whether to split the pages into separate documents
"""
documents = await self.aget_markdown_documents(split_by_page)
return [TextNode(text=doc.text, metadata=doc.metadata) for doc in documents]
def get_markdown(self) -> str:
"""
Get the raw parsed markdown from the job, distinct from the markdown documents.
This does not include page separators, e.g. if merge_tables_across_pages_in_markdown is True
"""
return asyncio_run(self.aget_markdown())
async def aget_markdown(self) -> str:
"""
Get the raw parsed markdown from the job, distinct from the markdown documents.
This does not include page separators, e.g. if merge_tables_across_pages_in_markdown is True
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/raw/markdown"
response = await make_api_request(self._client, "GET", url)
return response.content.decode("utf-8")
def get_text(self) -> str:
"""
Get the raw parsed text from the job.
"""
return asyncio_run(self.aget_text())
async def aget_text(self) -> str:
"""
Get the raw parsed text from the job.
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/raw/text"
response = await make_api_request(self._client, "GET", url)
return response.content.decode("utf-8")
def get_json(self) -> Dict[str, Any]:
"""
Get the full parsed JSON result from the job.
Note:
This is not the same as JobResult.json(), which is a
JSON serialized version of the JobResult Page Documents.
"""
return asyncio_run(self.aget_json())
async def aget_json(self) -> Dict[str, Any]:
"""
Get the full parsed JSON result from the job.
Note:
This is not the same as JobResult.json(), which is a
JSON serialized version of the JobResult Page Documents.
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/json"
response = await make_api_request(self._client, "GET", url)
return response.json()
async def _get_image_document_with_bytes(
self, image: ImageItem, page: Page
) -> ImageDocument:
@@ -1,6 +1,10 @@
import httpx
import itertools
import logging
import os
from enum import Enum
from pathlib import Path
from datetime import datetime
from tenacity import (
retry,
stop_after_attempt,
@@ -8,7 +12,7 @@ from tenacity import (
retry_if_exception,
before_sleep_log,
)
from typing import Any
from typing import Any, Iterable, Iterator, Optional, List, cast
logger = logging.getLogger(__name__)
@@ -34,6 +38,17 @@ class ParsingMode(str, Enum):
parse_page_with_lvm = "parse_page_with_lvm"
parse_page_with_agent = "parse_page_with_agent"
parse_document_with_llm = "parse_document_with_llm"
parse_document_with_agent = "parse_document_with_agent"
class FailedPageMode(str, Enum):
"""
Enum for representing the different available page error handling modes
"""
raw_text = "raw_text"
blank_page = "blank_page"
error_message = "error_message"
class Language(str, Enum):
@@ -286,3 +301,83 @@ async def make_api_request(
return response
return await _make_request(url, **httpx_kwargs)
def expand_target_pages(target_pages: str) -> Iterator[int]:
"""Yield all values in target_pages."""
for target in target_pages.strip().split(","):
if "-" in target:
try:
start, end = map(int, target.strip().split("-"))
if start > end:
raise ValueError
yield from range(start, end + 1)
except ValueError as e:
raise ValueError(f"Invalid page range: {target}") from e
else:
try:
yield int(target)
except ValueError as e:
raise ValueError(f"Invalid page number: {target}") from e
def partition_pages(
pages: Iterable[int], size: int, max_pages: Optional[int] = None
) -> Iterator[str]:
"""Yield partitioned target_pages segments."""
if size < 1:
raise ValueError(f"Invalid partition segment size: {size}")
if max_pages is not None and max_pages < 1:
raise ValueError("Max pages must be > 0")
it = iter(pages)
total = 0
while max_pages is None or total < max_pages:
segment = tuple(itertools.islice(it, size))
if segment:
targets = []
for _k, g in itertools.groupby(enumerate(segment), lambda x: x[0] - x[1]):
group = [item[1] for item in g]
if len(group) > 1:
start, end = group[0], group[-1]
group_size = end - start + 1
if max_pages is not None and total + group_size > max_pages:
end -= total + group_size - max_pages
group_size = end - start + 1
if group_size > 1:
targets.append(f"{start}-{end}")
else:
targets.append(str(start))
total += group_size
else:
targets.append(str(group[0]))
total += 1
yield ",".join(targets)
else:
return
def extract_tables_from_json_results(
json_results: List[dict], download_path: str
) -> List[str]:
tables = []
for json_result in json_results:
pages = json_result["pages"]
for page in pages:
page = cast(dict, page)
items = page.get("items", [])
if items:
for i, item in enumerate(items):
item = cast(dict, item)
if item.get("type", "") == "table" and item.get("csv", ""):
savepath = os.path.join(
download_path,
f"table_{datetime.now().strftime('%Y_%d_%m_%H_%M_%S_%f')[:-3]}.csv",
)
if Path(savepath).exists():
savepath = (
savepath.replace(".csv", "_")[0] + str(i) + ".csv"
)
with open(savepath, "w") as f:
f.write(item["csv"])
tables.append(savepath)
return tables
+29
View File
@@ -0,0 +1,29 @@
import difflib
from pydantic import BaseModel
from typing import Any, Dict, List, Tuple, Type
def check_extra_params(
model_cls: Type[BaseModel], data: Dict[str, Any]
) -> Tuple[List[str], List[str]]:
# check if one of the parameters is unused, and warn the user
model_attributes = set(model_cls.model_fields.keys())
extra_params = [param for param in data.keys() if param not in model_attributes]
suggestions: List[str] = []
if extra_params:
# for each unused parameter, check if it is similar to a valid parameter and suggest a typo correction, else suggest to check the documentation / update the package
for param in extra_params:
similar_params = difflib.get_close_matches(
param, model_attributes, n=1, cutoff=0.8
)
if similar_params:
suggestions.append(
f"'{param}' is not a valid parameter. Did you mean '{similar_params[0]}' instead of '{param}'?"
)
else:
suggestions.append(
f"'{param}' is not a valid parameter. Please check the documentation or update the package."
)
return extra_params, suggestions
@@ -146,9 +146,9 @@ Full documentation for `SimpleDirectoryReader` can be found on the [LlamaIndex D
Several end-to-end indexing examples can be found in the examples folder
- [Getting Started](/examples/parse/demo_basic.ipynb)
- [Advanced RAG Example](/examples/parse/demo_advanced.ipynb)
- [Raw API Usage](/examples/parse/demo_api.ipynb)
- [Getting Started](/docs/examples-py/parse/demo_basic.ipynb)
- [Advanced RAG Example](/docs/examples-py/parse/demo_advanced.ipynb)
- [Raw API Usage](/docs/examples-py/parse/demo_api.ipynb)
## Documentation
+8
View File
@@ -0,0 +1,8 @@
from llama_cloud_services.parse import (
LlamaParse,
ResultType,
ParsingMode,
FailedPageMode,
)
__all__ = ["LlamaParse", "ResultType", "ParsingMode", "FailedPageMode"]
@@ -1,6 +1,8 @@
from llama_cloud_services.parse.base import (
LlamaParse,
ResultType,
ParsingMode,
FailedPageMode,
FileInput,
_DEFAULT_SEPARATOR,
JOB_RESULT_URL,
@@ -12,6 +14,8 @@ __all__ = [
"LlamaParse",
"ResultType",
"FileInput",
"ParsingMode",
"FailedPageMode",
"_DEFAULT_SEPARATOR",
"JOB_RESULT_URL",
"JOB_STATUS_ROUTE",
@@ -2,10 +2,14 @@ from llama_cloud_services.parse.utils import (
SUPPORTED_FILE_TYPES,
Language,
ResultType,
ParsingMode,
FailedPageMode,
)
__all__ = [
"SUPPORTED_FILE_TYPES",
"Language",
"ResultType",
"ParsingMode",
"FailedPageMode",
]
+29
View File
@@ -0,0 +1,29 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"pytest>=8.0.0,<9",
"pytest-asyncio",
"ipykernel>=6.29.0,<7"
]
[project]
name = "llama-parse"
version = "0.6.54"
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.54"]
[project.scripts]
llama-parse = "llama_parse.cli.main:parse"
[tool.hatch.build.targets.sdist]
include = ["llama_parse"]
[tool.hatch.build.targets.wheel]
include = ["llama_parse"]
+2844
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"pytest>=8.0.0,<9",
"pytest-asyncio",
"ipykernel>=6.29.0,<7",
"pre-commit==3.2.0",
"autoevals>=0.0.114,<0.0.115",
"deepdiff>=8.1.1,<9",
"ipython>=8.12.3,<9",
"jupyter>=1.1.1,<2",
"mypy>=1.14.1,<2"
]
[project]
name = "llama-cloud-services"
version = "0.6.54"
description = "Tailored SDK clients for LlamaCloud services."
authors = [{name = "Logan Markewich", email = "logan@runllama.ai"}]
requires-python = ">=3.9,<4.0"
readme = "README.md"
license = "MIT"
dependencies = [
"llama-index-core>=0.12.0",
"llama-cloud==0.1.35",
"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"
]
[project.scripts]
llama-parse = "llama_cloud_services.parse.cli.main:parse"
[tool.hatch.build.targets.sdist]
include = ["llama_cloud_services"]
[tool.hatch.build.targets.wheel]
include = ["llama_cloud_services"]
[tool.mypy]
files = ["llama_cloud_services"]
python_version = "3.10"
@@ -0,0 +1,129 @@
import os
import httpx
import pytest
import uuid
from pydantic import BaseModel
from dotenv import load_dotenv
from pathlib import Path
from llama_cloud.client import AsyncLlamaCloud
from llama_cloud_services.beta.agent_data import AsyncAgentDataClient
class TrailingSlashHttpxClient(httpx.AsyncClient):
"""Custom httpx client that ensures all URLs have trailing slashes"""
async def request(self, method, url, **kwargs):
# Convert URL to string and ensure trailing slash
url_str = str(url)
if not url_str.endswith("/") and "?" not in url_str:
url_str += "/"
self.headers["Authorization"] = f"Bearer {LLAMA_CLOUD_API_KEY}"
kwargs.pop("headers", None)
return await super().request(method, url_str, headers=self.headers, **kwargs)
# Load environment variables
def load_test_dotenv():
dotenv_path = Path(__file__).parent.parent.parent.parent / ".env.dev"
load_dotenv(dotenv_path, override=True)
load_test_dotenv()
# Get configuration from environment
LLAMA_CLOUD_API_KEY = os.getenv("LLAMA_CLOUD_API_KEY")
LLAMA_CLOUD_BASE_URL = os.getenv("LLAMA_CLOUD_BASE_URL")
LLAMA_DEPLOY_DEPLOYMENT_NAME = os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME")
class TestData(BaseModel):
"""Simple test data model for agent data testing"""
name: str
test_id: str
value: int
# Skip all tests if API key is not set
@pytest.mark.asyncio
@pytest.mark.skipif(
not LLAMA_CLOUD_API_KEY,
reason="LLAMA_CLOUD_API_KEY not set",
)
async def test_agent_data_crud_operations():
"""Test basic CRUD operations for agent data with automatic cleanup"""
# Create unique test identifier to avoid conflicts
test_id = str(uuid.uuid4())
# Set up client
client = AsyncLlamaCloud(
token=LLAMA_CLOUD_API_KEY,
base_url=LLAMA_CLOUD_BASE_URL,
httpx_client=TrailingSlashHttpxClient(timeout=60, follow_redirects=True),
)
# Create agent data client with unique collection name
agent_data_client = AsyncAgentDataClient(
client=client,
type=TestData,
collection=f"test-collection-{test_id[:8]}",
agent_url_id=LLAMA_DEPLOY_DEPLOYMENT_NAME,
)
# Create test data
test_data = TestData(name="test-item", test_id=test_id, value=42)
created_item = None
try:
# Test CREATE
created_item = await agent_data_client.create_item(test_data)
assert created_item.data.name == "test-item"
assert created_item.data.test_id == test_id
assert created_item.data.value == 42
assert created_item.id is not None
# Test READ
retrieved_item = await agent_data_client.get_item(created_item.id)
assert retrieved_item.id == created_item.id
assert retrieved_item.data.name == "test-item"
assert retrieved_item.data.test_id == test_id
assert retrieved_item.data.value == 42
# Test SEARCH
search_results = await agent_data_client.search(
filter={"test_id": {"eq": test_id}}, page_size=10, include_total=True
)
assert len(search_results.items) == 1
assert search_results.items[0].data.test_id == test_id
assert search_results.total == 1
# Test AGGREGATE
aggregate_results = await agent_data_client.aggregate(
group_by=["test_id"], count=True
)
assert len(aggregate_results.items) == 1
assert aggregate_results.items[0].group_key["test_id"] == test_id
assert aggregate_results.items[0].count == 1
# Test UPDATE
updated_data = TestData(name="updated-item", test_id=test_id, value=84)
updated_item = await agent_data_client.update_item(
created_item.id, updated_data
)
assert updated_item.data.name == "updated-item"
assert updated_item.data.value == 84
assert updated_item.id == created_item.id
# Verify update persisted
verified_item = await agent_data_client.get_item(created_item.id)
assert verified_item.data.name == "updated-item"
assert verified_item.data.value == 84
finally:
# Clean up test data
if created_item is not None:
try:
await agent_data_client.delete_item(created_item.id)
except Exception as e:
print(f"Warning: Failed to cleanup test data {created_item.id}: {e}")
+41
View File
@@ -0,0 +1,41 @@
import os
from typing import List
from llama_cloud_services.extract import LlamaExtract
# Global storage for agents to cleanup
_TEST_AGENTS_TO_CLEANUP: List[str] = []
def pytest_sessionfinish(session, exitstatus):
"""Hook that runs after all tests complete - cleanup agents here"""
print(
f"pytest_sessionfinish hook called! Agents to cleanup: {_TEST_AGENTS_TO_CLEANUP}"
)
if _TEST_AGENTS_TO_CLEANUP:
print("Creating cleanup client...")
# Create a fresh client just for cleanup
cleanup_client = LlamaExtract(
api_key=os.getenv("LLAMA_CLOUD_API_KEY"),
base_url=os.getenv("LLAMA_CLOUD_BASE_URL"),
project_id=os.getenv("LLAMA_CLOUD_PROJECT_ID"),
verbose=True,
)
for agent_id in _TEST_AGENTS_TO_CLEANUP:
try:
print(f"Deleting agent {agent_id}...")
cleanup_client.delete_agent(agent_id)
print(f"Cleaned up agent {agent_id}")
except Exception as e:
print(f"Warning: Failed to delete agent {agent_id}: {e}")
_TEST_AGENTS_TO_CLEANUP.clear()
print("Agent cleanup completed")
else:
print("No agents to cleanup")
def register_agent_for_cleanup(agent_id: str):
"""Register an agent ID for cleanup at the end of the test session"""
_TEST_AGENTS_TO_CLEANUP.append(agent_id)

Some files were not shown because too many files have changed in this diff Show More