mirror of
https://github.com/run-llama/llama_cloud_services.git
synced 2026-07-18 15:54:38 -04:00
Compare commits
80 Commits
v0.6.26
...
adrian/xdist
| Author | SHA1 | Date | |
|---|---|---|---|
| e6966b677d | |||
| 79fe1930cf | |||
| ab225c3eab | |||
| 6f1de75909 | |||
| 230ed64e41 | |||
| ef126c3a93 | |||
| 51a7534733 | |||
| 4f5d2bde13 | |||
| 3d05fe5d77 | |||
| c16ca673af | |||
| 6619034bce | |||
| c56fb5d8f7 | |||
| b407a5edb5 | |||
| e6a27d17fb | |||
| 34077fd479 | |||
| 7a68ad5a7f | |||
| 74a1b6c2f2 | |||
| 9a90ae5264 | |||
| 310c1bc105 | |||
| cd20b29299 | |||
| 0cb7aeb81c | |||
| 98db5eeeae | |||
| c21cb34ff6 | |||
| e28c7b9d92 | |||
| ee4e565604 | |||
| 6dbb089f4c | |||
| c4b694db8d | |||
| 97f428ad06 | |||
| ef92ee5408 | |||
| d094668d03 | |||
| 5bb5fc1625 | |||
| 1d57e0071d | |||
| 2a344c4f5c | |||
| ce02559b8d | |||
| e42746e372 | |||
| 3149dfd03a | |||
| e499fdbdab | |||
| e57df39248 | |||
| 09b192b98b | |||
| 13f01a0621 | |||
| cf879a1a58 | |||
| fcdf2ab63e | |||
| 083d8109c2 | |||
| 89cfc8b25f | |||
| c46e157f92 | |||
| 05d6026d37 | |||
| 8e98d5c146 | |||
| 3f311c0669 | |||
| b1a2f9d42b | |||
| 142f55c94c | |||
| 230a110e52 | |||
| 83e2b031cd | |||
| 4844e26e5c | |||
| 70a049af3c | |||
| dc11776c86 | |||
| 2448a42b90 | |||
| c75a900174 | |||
| 2fb7adfe0e | |||
| dc82270724 | |||
| d880a48dd0 | |||
| 7567e8b45e | |||
| 0d59a90151 | |||
| 98ad550b1a | |||
| b58f43ce9f | |||
| acf6adcd91 | |||
| daf6576c3c | |||
| 8caa4defa6 | |||
| 26918b8de4 | |||
| 6fb5ebe2f9 | |||
| c0aa67995b | |||
| 9f841f8328 | |||
| 99c75eece9 | |||
| 57d2586ee3 | |||
| 4280a43ec8 | |||
| 7f1082bbb2 | |||
| 57cfc45804 | |||
| 30e8913875 | |||
| 0ce6d4d7a4 | |||
| 584ba8d48e | |||
| 925805ee11 |
@@ -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@v4
|
||||
- name: Set up python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1,95 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check repository access
|
||||
id: check-access
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Get the user who triggered the event
|
||||
case "${{ github.event_name }}" in
|
||||
"issue_comment")
|
||||
USER="${{ github.event.comment.user.login }}"
|
||||
;;
|
||||
"pull_request_review_comment")
|
||||
USER="${{ github.event.comment.user.login }}"
|
||||
;;
|
||||
"pull_request_review")
|
||||
USER="${{ github.event.review.user.login }}"
|
||||
;;
|
||||
"issues")
|
||||
USER="${{ github.event.issue.user.login }}"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Checking repository access for user: $USER"
|
||||
|
||||
# Check if user has write access to the repository
|
||||
REPO="${{ github.repository }}"
|
||||
if gh api repos/$REPO/collaborators/$USER/permission --jq '.permission' | grep -E "(admin|write)" > /dev/null 2>&1; then
|
||||
echo "User $USER has write access to the repository"
|
||||
echo "authorized=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "User $USER does not have write access to the repository"
|
||||
echo "authorized=false" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
if: steps.check-access.outputs.authorized == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
if: steps.check-access.outputs.authorized == 'true'
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_GITHUB_API_KEY }}
|
||||
|
||||
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
|
||||
# model: "claude-opus-4-20250514"
|
||||
|
||||
# Optional: Customize the trigger phrase (default: @claude)
|
||||
# trigger_phrase: "/claude"
|
||||
|
||||
# Optional: Trigger when specific user is assigned to an issue
|
||||
# assignee_trigger: "claude-bot"
|
||||
|
||||
# Optional: Allow Claude to run specific commands
|
||||
# Allow bash commands to be run, for things like running tests, linting, etc.
|
||||
allowed_tools: "Bash(rg:*),Bash(find:*),Bash(grep:*),Bash(pnpm:*),Bash(npm:*),Bash(uv:*),Bash(pip:*),Bash(pipx:*),Bash(make:*),Bash(cd:*),WebFetch"
|
||||
|
||||
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
||||
# custom_instructions: |
|
||||
# Follow our coding standards
|
||||
# Ensure all new code has tests
|
||||
# Use TypeScript for new files
|
||||
|
||||
# Optional: Custom environment variables for Claude
|
||||
# claude_env: |
|
||||
# NODE_ENV: test
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Linting
|
||||
name: Lint - Python
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
POETRY_VERSION: "1.6.1"
|
||||
UV_VERSION: "0.7.20"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -21,17 +21,15 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}
|
||||
- name: Set up python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
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
|
||||
version: ${{ env.UV_VERSION }}
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install ${{ matrix.python-version }}
|
||||
|
||||
- name: Run linter
|
||||
shell: bash
|
||||
run: poetry run make lint
|
||||
working-directory: py
|
||||
run: uv run -- pre-commit run -a
|
||||
@@ -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
|
||||
|
||||
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
|
||||
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
|
||||
@@ -1,83 +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@v4
|
||||
- name: Set up python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
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: Wait for PyPI to update
|
||||
run: |
|
||||
sleep 60
|
||||
|
||||
- name: Update llama-parse lock file
|
||||
run: |
|
||||
cd llama_parse && poetry lock
|
||||
|
||||
- name: Build and publish llama-parse
|
||||
uses: JRubics/poetry-publish@v2.1
|
||||
with:
|
||||
package_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
|
||||
@@ -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
|
||||
@@ -0,0 +1,54 @@
|
||||
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
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Run Build
|
||||
working-directory: ts/llama_cloud_services/
|
||||
run: pnpm build
|
||||
|
||||
- name: Build tarball
|
||||
run: |
|
||||
pnpm pack
|
||||
working-directory: ts/llama_cloud_services
|
||||
|
||||
- name: Setup npm authentication
|
||||
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Release
|
||||
working-directory: ts/llama_cloud_services
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: pnpm publish --access public --no-git-checks
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "ts/llama_cloud_services/llama-cloud-services*.tgz"
|
||||
name: Release ${{ github.ref }} - LlamaCloud Services TS
|
||||
bodyFile: "ts/llama_cloud_services/CHANGELOG.md"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -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: make e2e
|
||||
|
||||
- name: Remove virtual environment
|
||||
working-directory: py
|
||||
run: rm -rf .venv/
|
||||
@@ -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/
|
||||
@@ -0,0 +1,42 @@
|
||||
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
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: Run Build
|
||||
working-directory: ts/llama_cloud_services/
|
||||
run: pnpm build
|
||||
- name: Run Tests
|
||||
working-directory: ts/llama_cloud_services/
|
||||
run: pnpm test
|
||||
- name: Run e2e tests
|
||||
working-directory: ts/e2e-tests/
|
||||
run: pnpm test
|
||||
@@ -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@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
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
|
||||
@@ -5,3 +5,7 @@ __pycache__/
|
||||
.idea
|
||||
.env*
|
||||
.ipynb_checkpoints*
|
||||
*_cache/
|
||||
node_modules/
|
||||
.turbo/
|
||||
dist/
|
||||
|
||||
@@ -15,25 +15,26 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
- id: trailing-whitespace
|
||||
exclude: ^ts/llama_cloud_services/src/client/
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.1.5
|
||||
|
||||
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 +64,13 @@ repos:
|
||||
rev: v3.0.3
|
||||
hooks:
|
||||
- id: prettier
|
||||
exclude: poetry.lock
|
||||
exclude: ^(uv.lock|ts/llama_cloud_services/pnpm-lock.yaml|ts/e2e-tests)
|
||||
- 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|pnpm-lock.yaml)
|
||||
args:
|
||||
[
|
||||
"--ignore-words-list",
|
||||
@@ -84,6 +85,6 @@ repos:
|
||||
rev: v0.23.1
|
||||
hooks:
|
||||
- id: toml-sort-fix
|
||||
exclude: ".*poetry.lock"
|
||||
exclude: ".*uv.lock"
|
||||
|
||||
exclude: .github/ISSUE_TEMPLATE
|
||||
exclude: ^(.github/ISSUE_TEMPLATE|ts/llama_cloud_services/src/client|pnpm-lock.yaml)
|
||||
|
||||
@@ -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
|
||||
|
||||
...
|
||||
@@ -1,14 +0,0 @@
|
||||
GIT_ROOT ?= $(shell git rev-parse --show-toplevel)
|
||||
|
||||
help: ## Show all Makefile targets.
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
format: ## Run code autoformatters (black).
|
||||
pre-commit install
|
||||
git ls-files | xargs pre-commit run black --files
|
||||
|
||||
lint: ## Run linters: pre-commit (black, ruff, codespell) and mypy
|
||||
pre-commit install && git ls-files | xargs pre-commit run --show-diff-on-failure --files
|
||||
|
||||
test: ## Run tests via pytest
|
||||
pytest tests
|
||||
@@ -11,6 +11,7 @@ This includes:
|
||||
- [LlamaParse](./parse.md) - A GenAI-native document parser that can parse complex document data for any downstream LLM use case (Agents, RAG, data processing, etc.).
|
||||
- [LlamaReport (beta/invite-only)](./report.md) - A prebuilt agentic report builder that can be used to build reports from a variety of data sources.
|
||||
- [LlamaExtract](./extract.md) - A prebuilt agentic data extractor that can be used to transform data into a structured JSON representation.
|
||||
- [LlamaCloud Index](./index.md) - A widely customizable and fully automated document ingestion pipeline that also serves retrieval purposes.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -25,11 +26,19 @@ 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:
|
||||
@@ -37,6 +46,7 @@ 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 🇪🇺
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# LlamaCloud Services Examples - Python
|
||||
|
||||
In this folder you will find several TypeScript end-to-end applications that contain examples regarding:
|
||||
|
||||
- [LlamaParse](./parse/)
|
||||
- [LlamaCloud Index](./index/)
|
||||
|
||||
Follow the instructions in each example folder to get started!
|
||||
@@ -0,0 +1,122 @@
|
||||
# LlamaExtract Demo
|
||||
|
||||
A TypeScript demo application showcasing the power of **LlamaExract** - a structured data extraction agentic service from [LlamaCloud](https://cloud.llamaindex.ai). This demo allows you to extract structured information from scientific papers and get them into a nice markdown format.
|
||||
|
||||
## 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
|
||||
|
||||
- 📄 **Structured Data Extraction**: Extract data from your files effortlessly, and structure them the way you want!
|
||||
- 🤖 **Markdown Rendering**: Generate markdown directly from your extracted data
|
||||
- 🎨 **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
|
||||
- LlamaCloud API key
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/run-llama/llama_cloud_services
|
||||
cd lama_cloud_services/examples-ts/extract/
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Set up your environment variables:
|
||||
|
||||
```bash
|
||||
# Add your API key to your environment
|
||||
export LLAMA_CLOUD_API_KEY="your-llamacloud-api-key"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Start the Demo
|
||||
|
||||
```bash
|
||||
npm 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
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Build the Project
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
Format code:
|
||||
|
||||
```bash
|
||||
npm run format
|
||||
```
|
||||
|
||||
Lint code:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Document Input**: Enter the path to your document when prompted
|
||||
2. **Parsing**: LlamaExtract, based on the schema you can find [here](./src/schema.ts), processes the document and extracts structured data
|
||||
3. **Markdown Rendering**: The extracted content is rendered into beautiful markdown
|
||||
4. **Results**: View the results directly 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 LlamaCloud API key is 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 `npm run format` and `npm run lint`
|
||||
5. Submit a pull request
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
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 },
|
||||
},
|
||||
tseslint.configs.recommended,
|
||||
]);
|
||||
Generated
+3276
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "llama-extract-demo",
|
||||
"version": "0.1.0",
|
||||
"description": "Demo for LlamaExtract in TypeScript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"There are no tests\"",
|
||||
"start": "npm exec tsx src/index.ts",
|
||||
"lint": "eslint ./src/",
|
||||
"format": "prettier --write ./src/",
|
||||
"build": "tsc",
|
||||
"dev": "npm exec tsx --watch src/index.ts"
|
||||
},
|
||||
"author": "LlamaIndex",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cli-markdown": "^3.5.1",
|
||||
"consola": "^3.4.2",
|
||||
"figlet": "^1.8.2",
|
||||
"llama-cloud-services": "file:../../ts/llama_cloud_services",
|
||||
"marked": "^15.0.12",
|
||||
"marked-terminal": "^7.3.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@types/figlet": "^1.7.0",
|
||||
"@types/marked-terminal": "^6.1.1",
|
||||
"@types/node": "^24.2.0",
|
||||
"eslint": "^9.32.0",
|
||||
"globals": "^16.3.0",
|
||||
"jiti": "^2.5.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.39.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { LlamaExtract, ExtractConfig } from "llama-cloud-services";
|
||||
import cliMarkdown from "cli-markdown";
|
||||
import { logger } from "./logger";
|
||||
import pc from "picocolors";
|
||||
import { consoleInput, renderLogo } from "./utils";
|
||||
import { dataSchema } from "./schema";
|
||||
import { renderMarkdown, ResearchData } from "./markdown";
|
||||
|
||||
export async function main(): Promise<number> {
|
||||
const extractClient = new LlamaExtract(
|
||||
process.env.LLAMA_CLOUD_API_KEY!,
|
||||
"https://api.cloud.llamaindex.ai",
|
||||
);
|
||||
await renderLogo();
|
||||
logger.log(
|
||||
`Welcome to ${pc.bold(
|
||||
pc.magentaBright("LlamaExtract Demo✨"),
|
||||
)}, our demo for ${pc.bold(pc.green("LlamaExtract"))}, a ${pc.bold(
|
||||
pc.cyan("LlamaCloud☁️"),
|
||||
)} (https://cloud.llamaindex.ai) product!.\nIn this demo we are going to try extracting relevant information ${pc.bold(
|
||||
pc.yellowBright("from scientific papers"),
|
||||
)}. Type the path to the paper 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 generatedData = await extractClient.extract(
|
||||
dataSchema,
|
||||
{} as ExtractConfig,
|
||||
userInput,
|
||||
);
|
||||
const research = renderMarkdown(generatedData?.data as ResearchData); // Added await here
|
||||
logger.log(`${pc.bold(pc.cyan("Extracted information:✨"))}:\n`);
|
||||
logger.log(cliMarkdown(research));
|
||||
} catch (error) {
|
||||
logger.error(`Error processing file: ${error}`);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createConsola } from "consola";
|
||||
import type { ConsolaInstance } from "consola";
|
||||
|
||||
export const logger: ConsolaInstance = createConsola({
|
||||
formatOptions: {
|
||||
date: false,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
type Author = {
|
||||
name: string;
|
||||
affiliation?: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
type Methodology = {
|
||||
approach?: string;
|
||||
participants?: string;
|
||||
methods?: string[];
|
||||
};
|
||||
|
||||
type Result = {
|
||||
finding?: string;
|
||||
significance?: string;
|
||||
supportingData?: string;
|
||||
};
|
||||
|
||||
type Reference = {
|
||||
title: string;
|
||||
authors: string;
|
||||
year?: string;
|
||||
relevance?: string;
|
||||
};
|
||||
|
||||
type Discussion = {
|
||||
implications?: string[];
|
||||
limitations?: string[];
|
||||
futureWork?: string[];
|
||||
};
|
||||
|
||||
type Publication = {
|
||||
journal?: string;
|
||||
year: string;
|
||||
doi?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export type ResearchData = {
|
||||
title: string;
|
||||
authors: Author[];
|
||||
abstract: string;
|
||||
keywords?: string[];
|
||||
mainFindings: string[];
|
||||
methodology?: Methodology;
|
||||
results?: Result[];
|
||||
discussion?: Discussion;
|
||||
references?: Reference[];
|
||||
publication?: Publication;
|
||||
};
|
||||
|
||||
export function renderMarkdown(data: ResearchData): string {
|
||||
const {
|
||||
title,
|
||||
authors,
|
||||
abstract,
|
||||
keywords,
|
||||
mainFindings,
|
||||
methodology,
|
||||
results,
|
||||
discussion,
|
||||
references,
|
||||
publication,
|
||||
} = data;
|
||||
|
||||
const md: string[] = [];
|
||||
|
||||
md.push(`# ${title}\n`);
|
||||
|
||||
// Authors
|
||||
md.push(`## Authors`);
|
||||
md.push(
|
||||
authors
|
||||
.map(
|
||||
(author) =>
|
||||
`- **${author.name}**${
|
||||
author.affiliation ? `, *${author.affiliation}*` : ""
|
||||
}${author.email ? ` (${author.email})` : ""}`,
|
||||
)
|
||||
.join("\n"),
|
||||
);
|
||||
|
||||
// Abstract
|
||||
md.push(`\n## Abstract\n${abstract}`);
|
||||
|
||||
// Keywords
|
||||
if (keywords && keywords.length > 0) {
|
||||
md.push(`\n## Keywords\n${keywords.map((k) => `- ${k}`).join("\n")}`);
|
||||
}
|
||||
|
||||
// Main Findings
|
||||
md.push(
|
||||
`\n## Main Findings\n${mainFindings.map((f) => `- ${f}`).join("\n")}`,
|
||||
);
|
||||
|
||||
// Methodology
|
||||
if (methodology) {
|
||||
md.push(`\n## Methodology`);
|
||||
if (methodology.approach) md.push(`**Approach:** ${methodology.approach}`);
|
||||
if (methodology.participants)
|
||||
md.push(`**Participants:** ${methodology.participants}`);
|
||||
if (methodology.methods?.length) {
|
||||
md.push(
|
||||
`**Methods:**\n${methodology.methods.map((m) => `- ${m}`).join("\n")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Results
|
||||
if (results?.length) {
|
||||
md.push(`\n## Results`);
|
||||
results.forEach((result, i) => {
|
||||
md.push(`\n### Result ${i + 1}`);
|
||||
if (result.finding) md.push(`- **Finding:** ${result.finding}`);
|
||||
if (result.significance)
|
||||
md.push(`- **Significance:** ${result.significance}`);
|
||||
if (result.supportingData)
|
||||
md.push(`- **Supporting Data:** ${result.supportingData}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Discussion
|
||||
if (discussion) {
|
||||
md.push(`\n## Discussion`);
|
||||
if (discussion.implications?.length) {
|
||||
md.push(
|
||||
`### Implications\n${discussion.implications
|
||||
.map((d) => `- ${d}`)
|
||||
.join("\n")}`,
|
||||
);
|
||||
}
|
||||
if (discussion.limitations?.length) {
|
||||
md.push(
|
||||
`### Limitations\n${discussion.limitations
|
||||
.map((d) => `- ${d}`)
|
||||
.join("\n")}`,
|
||||
);
|
||||
}
|
||||
if (discussion.futureWork?.length) {
|
||||
md.push(
|
||||
`### Future Work\n${discussion.futureWork
|
||||
.map((d) => `- ${d}`)
|
||||
.join("\n")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// References
|
||||
if (references?.length) {
|
||||
md.push(`\n## References`);
|
||||
references.forEach((ref, i) => {
|
||||
md.push(
|
||||
`\n**[${i + 1}]** ${ref.title} — *${ref.authors}*${
|
||||
ref.year ? ` (${ref.year})` : ""
|
||||
}`,
|
||||
);
|
||||
if (ref.relevance) md.push(`> ${ref.relevance}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Publication Info
|
||||
if (publication) {
|
||||
md.push(`\n## Publication`);
|
||||
if (publication.journal) md.push(`- **Journal:** ${publication.journal}`);
|
||||
if (publication.year) md.push(`- **Year:** ${publication.year}`);
|
||||
if (publication.doi) md.push(`- **DOI:** ${publication.doi}`);
|
||||
if (publication.url)
|
||||
md.push(`- **URL:** [${publication.url}](${publication.url})`);
|
||||
}
|
||||
|
||||
return md.join("\n");
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
export const dataSchema = {
|
||||
type: "object",
|
||||
required: ["title", "authors", "abstract", "mainFindings"],
|
||||
properties: {
|
||||
title: {
|
||||
type: "string",
|
||||
description: "The full title of the research paper",
|
||||
},
|
||||
authors: {
|
||||
type: "array",
|
||||
description: "List of all authors of the paper",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
description: "Full name of the author",
|
||||
},
|
||||
affiliation: {
|
||||
type: "string",
|
||||
description:
|
||||
"Institution or organization the author is affiliated with",
|
||||
},
|
||||
email: {
|
||||
type: "string",
|
||||
description: "Contact email of the author if provided",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
abstract: {
|
||||
type: "string",
|
||||
description: "Complete abstract or summary of the paper",
|
||||
},
|
||||
keywords: {
|
||||
type: "array",
|
||||
description:
|
||||
"Key terms and phrases that describe the paper's main topics",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
mainFindings: {
|
||||
type: "array",
|
||||
description: "Key findings, conclusions, or contributions of the paper",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
methodology: {
|
||||
type: "object",
|
||||
description: "Research methods and approaches used",
|
||||
properties: {
|
||||
approach: {
|
||||
type: "string",
|
||||
description: "Overall research approach or study design",
|
||||
},
|
||||
participants: {
|
||||
type: "string",
|
||||
description: "Description of study participants or data sources",
|
||||
},
|
||||
methods: {
|
||||
type: "array",
|
||||
description: "Specific methods, techniques, or tools used",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
results: {
|
||||
type: "array",
|
||||
description: "Main results and outcomes of the research",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
finding: {
|
||||
type: "string",
|
||||
description: "Description of the specific result or finding",
|
||||
},
|
||||
significance: {
|
||||
type: "string",
|
||||
description:
|
||||
"Statistical significance or importance of the finding",
|
||||
},
|
||||
supportingData: {
|
||||
type: "string",
|
||||
description: "Relevant statistics, measurements, or data points",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discussion: {
|
||||
type: "object",
|
||||
properties: {
|
||||
implications: {
|
||||
type: "array",
|
||||
description: "Theoretical or practical implications of the findings",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
limitations: {
|
||||
type: "array",
|
||||
description: "Study limitations or constraints",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
futureWork: {
|
||||
type: "array",
|
||||
description: "Suggested future research directions",
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
references: {
|
||||
type: "array",
|
||||
description:
|
||||
"Key papers cited that are crucial to understanding this work",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
title: {
|
||||
type: "string",
|
||||
description: "Title of the cited paper",
|
||||
},
|
||||
authors: {
|
||||
type: "string",
|
||||
description: "Authors of the cited paper",
|
||||
},
|
||||
year: {
|
||||
type: "string",
|
||||
description: "Publication year",
|
||||
},
|
||||
relevance: {
|
||||
type: "string",
|
||||
description: "Why this reference is important to the current paper",
|
||||
},
|
||||
},
|
||||
required: ["title", "authors"],
|
||||
},
|
||||
},
|
||||
publication: {
|
||||
type: "object",
|
||||
properties: {
|
||||
journal: {
|
||||
type: "string",
|
||||
description: "Name of the journal or conference",
|
||||
},
|
||||
year: {
|
||||
type: "string",
|
||||
description: "Year of publication",
|
||||
},
|
||||
doi: {
|
||||
type: "string",
|
||||
description: "Digital Object Identifier (DOI) of the paper",
|
||||
},
|
||||
url: {
|
||||
type: "string",
|
||||
description: "URL where the paper can be accessed",
|
||||
},
|
||||
},
|
||||
required: ["year"],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
declare module "cli-markdown" {
|
||||
function cliMarkdown(input: string): string;
|
||||
export default cliMarkdown;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as readline from "readline/promises";
|
||||
import figlet from "figlet";
|
||||
import pc from "picocolors";
|
||||
|
||||
export async function renderLogo(): Promise<void> {
|
||||
const logoText = figlet.textSync("Extract Demo", {
|
||||
font: "ANSI Shadow",
|
||||
horizontalLayout: "default",
|
||||
verticalLayout: "default",
|
||||
width: 100,
|
||||
whitespaceBreak: true,
|
||||
});
|
||||
|
||||
// Add some styling with picocolors
|
||||
const styledLogo = pc.bold(pc.redBright(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;
|
||||
}
|
||||
@@ -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 https://github.com/run-llama/llama_cloud_services
|
||||
cd lama_cloud_services/examples-ts/index/
|
||||
```
|
||||
|
||||
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 run format` and `pnpm run lint`
|
||||
5. Submit a pull request
|
||||
@@ -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,
|
||||
]);
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+1770
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createConsola } from "consola";
|
||||
import type { ConsolaInstance } from "consola";
|
||||
|
||||
export const logger: ConsolaInstance = createConsola({
|
||||
formatOptions: {
|
||||
date: false,
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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 https://github.com/run-llama/llama_cloud_services
|
||||
cd lama_cloud_services/examples-ts/parse/
|
||||
```
|
||||
|
||||
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 run format` and `pnpm run lint`
|
||||
5. Submit a pull request
|
||||
Binary file not shown.
@@ -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,
|
||||
]);
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+1758
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createConsola } from "consola";
|
||||
import type { ConsolaInstance } from "consola";
|
||||
|
||||
export const logger: ConsolaInstance = createConsola({
|
||||
formatOptions: {
|
||||
date: false,
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# LlamaCloud Services Examples - Python
|
||||
|
||||
In this folder you will find several python notebooks that contain examples regarding:
|
||||
|
||||
- [LlamaParse](./parse/)
|
||||
- [LlamaExtract](./extract/)
|
||||
- [LlamaReport](./report/)
|
||||
|
||||
Follow the instructions in each notebook 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,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",
|
||||
|
||||
+219
-28
@@ -2,14 +2,40 @@
|
||||
|
||||
LlamaExtract provides a simple API for extracting structured data from unstructured documents like PDFs, text files and images.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [Supported File Types](#supported-file-types)
|
||||
- [Different Input Types](#different-input-types)
|
||||
- [Async Extraction](#async-extraction)
|
||||
- [Core Concepts](#core-concepts)
|
||||
- [Defining Schemas](#defining-schemas)
|
||||
- [Using Pydantic (Recommended)](#using-pydantic-recommended)
|
||||
- [Using JSON Schema](#using-json-schema)
|
||||
- [Important restrictions on JSON/Pydantic Schema](#important-restrictions-on-jsonpydantic-schema)
|
||||
- [Extraction Configuration](#extraction-configuration)
|
||||
- [Configuration Options](#configuration-options)
|
||||
- [Extraction Agents (Advanced)](#extraction-agents-advanced)
|
||||
- [Creating Agents](#creating-agents)
|
||||
- [Agent Batch Processing](#agent-batch-processing)
|
||||
- [Updating Agent Schemas](#updating-agent-schemas)
|
||||
- [Managing Agents](#managing-agents)
|
||||
- [When to Use Agents vs Direct Extraction](#when-to-use-agents-vs-direct-extraction)
|
||||
- [Installation](#installation)
|
||||
- [Tips & Best Practices](#tips--best-practices)
|
||||
- [Additional Resources](#additional-resources)
|
||||
|
||||
## Quick Start
|
||||
|
||||
The simplest way to get started is to use the stateless API with the extraction configuration and the file/text to extract from:
|
||||
|
||||
```python
|
||||
from llama_cloud_services import LlamaExtract
|
||||
from llama_cloud import ExtractConfig, ExtractMode
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Initialize client
|
||||
extractor = LlamaExtract()
|
||||
extractor = LlamaExtract(api_key="YOUR_API_KEY")
|
||||
|
||||
|
||||
# Define schema using Pydantic
|
||||
@@ -19,29 +45,97 @@ class Resume(BaseModel):
|
||||
skills: list[str] = Field(description="Technical skills and technologies")
|
||||
|
||||
|
||||
# Create extraction agent
|
||||
agent = extractor.create_agent(name="resume-parser", data_schema=Resume)
|
||||
# Configure extraction settings
|
||||
config = ExtractConfig(extraction_mode=ExtractMode.FAST)
|
||||
|
||||
# Extract data from document
|
||||
result = agent.extract("resume.pdf")
|
||||
# Extract data directly from document - no agent needed!
|
||||
result = extractor.extract(Resume, config, "resume.pdf")
|
||||
print(result.data)
|
||||
```
|
||||
|
||||
### Supported File Types
|
||||
|
||||
LlamaExtract supports the following file formats:
|
||||
|
||||
- **Documents**: PDF (.pdf), Word (.docx)
|
||||
- **Text files**: Plain text (.txt), CSV (.csv), JSON (.json), HTML (.html, .htm), Markdown (.md)
|
||||
- **Images**: PNG (.png), JPEG (.jpg, .jpeg)
|
||||
|
||||
### Different Input Types
|
||||
|
||||
```python
|
||||
# From file path (string or Path)
|
||||
result = extractor.extract(Resume, config, "resume.pdf")
|
||||
|
||||
# From file handle
|
||||
with open("resume.pdf", "rb") as f:
|
||||
result = extractor.extract(Resume, config, f)
|
||||
|
||||
# From bytes with filename
|
||||
with open("resume.pdf", "rb") as f:
|
||||
file_bytes = f.read()
|
||||
from llama_cloud_services.extract import SourceText
|
||||
|
||||
result = extractor.extract(
|
||||
Resume, config, SourceText(file=file_bytes, filename="resume.pdf")
|
||||
)
|
||||
|
||||
# From text content
|
||||
text = "Name: John Doe\nEmail: john@example.com\nSkills: Python, AI"
|
||||
result = extractor.extract(Resume, config, SourceText(text_content=text))
|
||||
```
|
||||
|
||||
### Async Extraction
|
||||
|
||||
For better performance with multiple files or when integrating with async applications.
|
||||
Here `queue_extraction` will enqueue the extraction jobs and exit. Alternatively, you
|
||||
can use `aextract` to poll for the job and return the extraction results.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
|
||||
async def extract_resumes():
|
||||
# Async extraction
|
||||
result = await extractor.aextract(Resume, config, "resume.pdf")
|
||||
print(result.data)
|
||||
|
||||
# Queue extraction jobs (returns immediately)
|
||||
jobs = await extractor.queue_extraction(
|
||||
Resume, config, ["resume1.pdf", "resume2.pdf"]
|
||||
)
|
||||
print(f"Queued {len(jobs)} extraction jobs")
|
||||
return jobs
|
||||
|
||||
|
||||
# Run async function
|
||||
jobs = asyncio.run(extract_resumes())
|
||||
# Check job status
|
||||
for job in jobs:
|
||||
status = agent.get_extraction_job(job.id).status
|
||||
print(f"Job {job.id}: {status}")
|
||||
|
||||
# Get results when complete
|
||||
results = [agent.get_extraction_run_for_job(job.id) for job in jobs]
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
- **Extraction Agents**: Reusable extractors configured with a specific schema and extraction settings.
|
||||
- **Data Schema**: Structure definition for the data you want to extract in the form of a JSON schema or a Pydantic model.
|
||||
- **Extraction Config**: Settings that control how extraction is performed (e.g., speed vs accuracy trade-offs).
|
||||
- **Extraction Jobs**: Asynchronous extraction tasks that can be monitored.
|
||||
- **Extraction Agents** (Advanced): Reusable extractors configured with a specific schema and extraction settings.
|
||||
|
||||
## Defining Schemas
|
||||
|
||||
Schemas can be defined using either Pydantic models or JSON Schema:
|
||||
Schemas define the structure of data you want to extract. You can use either Pydantic models or JSON Schema:
|
||||
|
||||
### Using Pydantic (Recommended)
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from llama_cloud import ExtractConfig, ExtractMode
|
||||
|
||||
|
||||
class Experience(BaseModel):
|
||||
@@ -54,6 +148,11 @@ class Experience(BaseModel):
|
||||
class Resume(BaseModel):
|
||||
name: str = Field(description="Candidate name")
|
||||
experience: List[Experience] = Field(description="Work history")
|
||||
|
||||
|
||||
# Use the schema for extraction
|
||||
config = ExtractConfig(extraction_mode=ExtractMode.FAST)
|
||||
result = extractor.extract(Resume, config, "resume.pdf")
|
||||
```
|
||||
|
||||
### Using JSON Schema
|
||||
@@ -88,7 +187,9 @@ schema = {
|
||||
},
|
||||
}
|
||||
|
||||
agent = extractor.create_agent(name="resume-parser", data_schema=schema)
|
||||
# Use the schema for extraction
|
||||
config = ExtractConfig(extraction_mode=ExtractMode.FAST)
|
||||
result = extractor.extract(schema, config, "resume.pdf")
|
||||
```
|
||||
|
||||
### Important restrictions on JSON/Pydantic Schema
|
||||
@@ -108,28 +209,100 @@ be sufficient for a wide variety of use-cases.
|
||||
your extraction workflow to fit within these constraints, e.g. by extracting subset of fields
|
||||
and later merging them together.
|
||||
|
||||
## Other Extraction APIs
|
||||
## Extraction Configuration
|
||||
|
||||
### 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.
|
||||
Configure how extraction is performed using `ExtractConfig`. The schema is the most important part, but several configuration options can significantly impact the extraction process.
|
||||
|
||||
```python
|
||||
with open("resume.pdf", "rb") as f:
|
||||
file_bytes = f.read()
|
||||
result = test_agent.extract(SourceText(file=file_bytes, filename="resume.pdf"))
|
||||
```
|
||||
from llama_cloud import ExtractConfig, ExtractMode, ChunkMode, ExtractTarget
|
||||
|
||||
```python
|
||||
result = test_agent.extract(
|
||||
SourceText(text_content="Candidate Name: Jane Doe")
|
||||
# Basic configuration
|
||||
config = ExtractConfig(
|
||||
extraction_mode=ExtractMode.BALANCED, # FAST, BALANCED, MULTIMODAL, PREMIUM
|
||||
extraction_target=ExtractTarget.PER_DOC, # PER_DOC, PER_PAGE
|
||||
system_prompt="Focus on the most recent data",
|
||||
page_range="1-5,10-15", # Extract from specific pages
|
||||
)
|
||||
|
||||
# Advanced configuration
|
||||
advanced_config = ExtractConfig(
|
||||
extraction_mode=ExtractMode.MULTIMODAL,
|
||||
chunk_mode=ChunkMode.PAGE, # PAGE, SECTION
|
||||
high_resolution_mode=True, # Better OCR accuracy
|
||||
invalidate_cache=False, # Bypass cached results
|
||||
cite_sources=True, # Enable source citations
|
||||
use_reasoning=True, # Enable reasoning (not in FAST mode)
|
||||
confidence_scores=True, # MULTIMODAL/PREMIUM only
|
||||
)
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
### Key Configuration Options
|
||||
|
||||
Process multiple files asynchronously:
|
||||
**Extraction Mode**: Controls processing quality and speed
|
||||
|
||||
- `FAST`: Fastest processing, suitable for simple documents with no OCR
|
||||
- `BALANCED`: Good speed/accuracy tradeoff for text-rich documents
|
||||
- `MULTIMODAL`: For visually rich documents with text, tables, and images (recommended)
|
||||
- `PREMIUM`: Highest accuracy with OCR, complex table/header detection
|
||||
|
||||
**Extraction Target**: Defines extraction scope
|
||||
|
||||
- `PER_DOC`: Apply schema to entire document (default)
|
||||
- `PER_PAGE`: Apply schema to each page, returns array of results
|
||||
|
||||
**Advanced Options**:
|
||||
|
||||
- `system_prompt`: Additional system-level instructions
|
||||
- `page_range`: Specific pages to extract (e.g., "1,3,5-7,9")
|
||||
- `chunk_mode`: Document splitting strategy (`PAGE` or `SECTION`)
|
||||
- `high_resolution_mode`: Better OCR for small text (slower processing)
|
||||
|
||||
**Extensions** (return additional metadata):
|
||||
|
||||
- `cite_sources`: Source tracing for extracted fields
|
||||
- `use_reasoning`: Explanations for extraction decisions
|
||||
- `confidence_scores`: Quantitative confidence measures (MULTIMODAL/PREMIUM only)
|
||||
|
||||
For complete configuration options, advanced settings, and detailed examples, see the [LlamaExtract Configuration Documentation](https://docs.cloud.llamaindex.ai/llamaextract/features/options).
|
||||
|
||||
## Extraction Agents (Advanced)
|
||||
|
||||
For reusable extraction workflows, you can create extraction agents that encapsulate both schema and configuration:
|
||||
|
||||
### Creating Agents
|
||||
|
||||
```python
|
||||
from llama_cloud_services import LlamaExtract
|
||||
from llama_cloud import ExtractConfig, ExtractMode
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Initialize client
|
||||
extractor = LlamaExtract()
|
||||
|
||||
|
||||
# Define schema
|
||||
class Resume(BaseModel):
|
||||
name: str = Field(description="Full name of candidate")
|
||||
email: str = Field(description="Email address")
|
||||
skills: list[str] = Field(description="Technical skills and technologies")
|
||||
|
||||
|
||||
# Configure extraction settings
|
||||
config = ExtractConfig(extraction_mode=ExtractMode.FAST)
|
||||
|
||||
# Create extraction agent
|
||||
agent = extractor.create_agent(
|
||||
name="resume-parser", data_schema=Resume, config=config
|
||||
)
|
||||
|
||||
# Use the agent
|
||||
result = agent.extract("resume.pdf")
|
||||
print(result.data)
|
||||
```
|
||||
|
||||
### Agent Batch Processing
|
||||
|
||||
Process multiple files with an agent:
|
||||
|
||||
```python
|
||||
# Queue multiple files for extraction
|
||||
@@ -144,7 +317,7 @@ for job in jobs:
|
||||
results = [agent.get_extraction_run_for_job(job.id) for job in jobs]
|
||||
```
|
||||
|
||||
### Updating Schemas
|
||||
### Updating Agent Schemas
|
||||
|
||||
Schemas can be modified and updated after creation:
|
||||
|
||||
@@ -169,10 +342,26 @@ agent = extractor.get_agent(name="resume-parser")
|
||||
extractor.delete_agent(agent.id)
|
||||
```
|
||||
|
||||
### When to Use Agents vs Direct Extraction
|
||||
|
||||
**Use Direct Extraction When:**
|
||||
|
||||
- One-off extractions
|
||||
- Different schemas for different documents
|
||||
- Simple workflows
|
||||
- Getting started quickly
|
||||
|
||||
**Use Extraction Agents When:**
|
||||
|
||||
- Repeated extractions with the same schema
|
||||
- Team collaboration (shared, named extractors)
|
||||
- Complex workflows requiring state management
|
||||
- Production systems with consistent extraction patterns
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install llama-extract==0.1.0
|
||||
pip install llama-cloud-services
|
||||
```
|
||||
|
||||
## Tips & Best Practices
|
||||
@@ -193,9 +382,9 @@ At the core of LlamaExtract is the schema, which defines the structure of the da
|
||||
2. **Running Extractions**:
|
||||
- Note that resetting `agent.schema` will not save the schema to the database,
|
||||
until you call `agent.save`, but it will be used for running extractions.
|
||||
- Check job status prior to accessing results. Any extraction error should be available as
|
||||
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.
|
||||
- Check extraction results for any errors. Error information is available in the `result.error` field for debugging.
|
||||
- Consider async operations (`aextract` or `queue_extraction`) for large-scale extraction or when processing multiple files.
|
||||
- For repeated extractions with the same schema, consider creating an extraction agent to avoid redefining the schema each time.
|
||||
|
||||
### Hitting "The response was too long to be processed" Error
|
||||
|
||||
@@ -208,5 +397,7 @@ Another option (orthogonal to the above) is to break the document into smaller s
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Example Notebook](examples/resume_screening.ipynb) - Detailed walkthrough of resume parsing
|
||||
- [Extract Documentation](https://docs.cloud.llamaindex.ai/llamaextract/getting_started) - Details on Extract features, API and examples.
|
||||
- [Example Notebook](docs/examples-py/extract/resume_screening.ipynb) - Detailed walkthrough of resume parsing
|
||||
- [Example Application with TypeScript](./examples-ts/extract/) - End-to-end examples using LlamaExtract TypeScript client.
|
||||
- [Discord Community](https://discord.com/invite/eN6D2HQ4aX) - Get help and share feedback
|
||||
|
||||
@@ -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 you’re 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.
|
||||
@@ -1,40 +0,0 @@
|
||||
from typing import Any, Dict, List, Union, Generator
|
||||
from contextlib import contextmanager
|
||||
|
||||
# Asyncio error messages
|
||||
nest_asyncio_err = "cannot be called from a running event loop"
|
||||
nest_asyncio_msg = (
|
||||
"The event loop is already running. "
|
||||
"Add `import nest_asyncio; nest_asyncio.apply()` to your code to fix this issue."
|
||||
)
|
||||
|
||||
|
||||
def is_jupyter() -> bool:
|
||||
"""Check if we're running in a Jupyter environment."""
|
||||
try:
|
||||
from IPython import get_ipython
|
||||
|
||||
return get_ipython().__class__.__name__ == "ZMQInteractiveShell"
|
||||
except (ImportError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
@contextmanager
|
||||
def augment_async_errors() -> Generator[None, None, None]:
|
||||
"""Context manager to add helpful information for errors due to nested event loops."""
|
||||
try:
|
||||
yield
|
||||
except RuntimeError as e:
|
||||
if nest_asyncio_err in str(e):
|
||||
raise RuntimeError(nest_asyncio_msg)
|
||||
raise
|
||||
|
||||
|
||||
JSONType = Union[Dict[str, Any], List[Any], str, int, float, bool, None]
|
||||
JSONObjectType = Dict[str, JSONType]
|
||||
|
||||
|
||||
class ExperimentalWarning(Warning):
|
||||
"""Warning for experimental features."""
|
||||
|
||||
pass
|
||||
Generated
-3087
File diff suppressed because it is too large
Load Diff
@@ -1,24 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "llama-parse"
|
||||
version = "0.6.26"
|
||||
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.26"
|
||||
|
||||
[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"
|
||||
@@ -97,7 +97,7 @@ 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 / bytes
|
||||
|
||||
@@ -153,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
|
||||
|
||||
|
||||
Generated
+3609
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- "ts/**"
|
||||
Generated
-4626
File diff suppressed because it is too large
Load Diff
+21
@@ -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.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
GIT_ROOT ?= $(shell git rev-parse --show-toplevel)
|
||||
|
||||
help: ## Show all Makefile targets.
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
format: ## Run code autoformatters (black).
|
||||
uv run pre-commit install
|
||||
git ls-files | xargs uv run pre-commit run black --files
|
||||
|
||||
lint: ## Run linters: pre-commit (black, ruff, codespell) and mypy
|
||||
uv run pre-commit install && git ls-files | xargs uv run pre-commit run --show-diff-on-failure --files
|
||||
|
||||
test: ## Run unit tests via pytest
|
||||
uv run pytest -v unit_tests/
|
||||
|
||||
.PHONY: e2e
|
||||
e2e: ## Run all tests. Run with high parallelism using xdist since tests are bottlenecked bound by the slow backend parsing
|
||||
uv run pytest -v -n 32 tests/
|
||||
@@ -0,0 +1,87 @@
|
||||
[](https://pypi.org/project/llama-cloud-services/)
|
||||
[](https://github.com/run-llama/llama_cloud_services/graphs/contributors)
|
||||
[](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,613 @@
|
||||
"""
|
||||
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
|
||||
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, model_validator, ConfigDict
|
||||
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 FieldCitation(BaseModel):
|
||||
page: 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",
|
||||
)
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
citation: Optional[List[FieldCitation]] = Field(
|
||||
None,
|
||||
description="The citation for the field, including page number and matching text",
|
||||
)
|
||||
|
||||
# Forbid unknown keys to avoid swallowing nested dicts
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
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}
|
||||
allowed_fields = ExtractedFieldMetadata.model_fields.keys()
|
||||
merged = {k: v for k, v in merged.items() if k in allowed_fields}
|
||||
validated = ExtractedFieldMetadata.model_validate(merged)
|
||||
|
||||
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.",
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _normalize_field_metadata_on_input(cls, value: Any) -> Any:
|
||||
# Ensure any inbound representation (including JSON round-trips)
|
||||
# gets normalized so nested dicts become ExtractedFieldMetadata where appropriate.
|
||||
if (
|
||||
isinstance(value, dict)
|
||||
and "field_metadata" in value
|
||||
and isinstance(value["field_metadata"], dict)
|
||||
):
|
||||
try:
|
||||
value = {
|
||||
**value,
|
||||
"field_metadata": parse_extracted_field_metadata(
|
||||
value["field_metadata"]
|
||||
),
|
||||
}
|
||||
except Exception:
|
||||
# Let pydantic surface detailed errors later rather than swallowing completely
|
||||
pass
|
||||
return value
|
||||
|
||||
@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,187 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from llama_cloud.types import (
|
||||
ClassifierRule,
|
||||
ClassifyJobResults,
|
||||
ClassifyParsingConfiguration,
|
||||
StatusEnum,
|
||||
ClassifyJobWithStatus,
|
||||
File,
|
||||
)
|
||||
from llama_cloud.resources.classifier.client import OMIT
|
||||
from llama_cloud_services.files.client import FileClient
|
||||
from llama_cloud_services.constants import POLLING_TIMEOUT_SECONDS
|
||||
from llama_cloud_services.utils import is_terminal_status, augment_async_errors
|
||||
from llama_index.core.async_utils import DEFAULT_NUM_WORKERS, run_jobs
|
||||
|
||||
|
||||
class ClassificationOutput(BaseModel):
|
||||
file_id: str
|
||||
classification: str
|
||||
|
||||
|
||||
class ClassifyClient:
|
||||
"""
|
||||
Experimental - Client for interacting with the LlamaCloud Classifier API.
|
||||
The Classification API is currently in beta and may change in the future without notice.
|
||||
|
||||
Args:
|
||||
client: The LlamaCloud client to use.
|
||||
project_id: The project ID to use.
|
||||
organization_id: The organization ID to use.
|
||||
polling_interval: The interval to poll for job completion in seconds.
|
||||
polling_timeout: The timeout for the job to complete in seconds.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: AsyncLlamaCloud,
|
||||
project_id: Optional[str] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
polling_interval: float = 1.0,
|
||||
polling_timeout: float = POLLING_TIMEOUT_SECONDS,
|
||||
):
|
||||
self.client = client
|
||||
self.project_id = project_id
|
||||
self.organization_id = organization_id
|
||||
self.polling_interval = polling_interval
|
||||
self.file_client = FileClient(client, project_id, organization_id)
|
||||
self.polling_timeout = polling_timeout
|
||||
|
||||
async def aclassify_file_ids(
|
||||
self,
|
||||
rules: list[ClassifierRule],
|
||||
file_ids: list[str],
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
) -> ClassifyJobResults:
|
||||
"""
|
||||
Classify a list of files by their IDs.
|
||||
Note that even if a job fails, some of the files may have been classified successfully.
|
||||
In this case, you may want to set raise_on_error to False and check the results for successful classifications.
|
||||
|
||||
Args:
|
||||
rules: The rules to use for classification.
|
||||
file_ids: The IDs of the files to classify.
|
||||
parsing_configuration: The parsing configuration to use for classification.
|
||||
raise_on_error: Whether to raise an error if the classification job fails.
|
||||
|
||||
Returns:
|
||||
The results of the classification job.
|
||||
"""
|
||||
classify_job = await self.client.classifier.create_classify_job(
|
||||
rules=rules,
|
||||
file_ids=file_ids,
|
||||
parsing_configuration=parsing_configuration or OMIT,
|
||||
project_id=self.project_id,
|
||||
organization_id=self.organization_id,
|
||||
)
|
||||
|
||||
classify_job_with_status = await self._wait_for_job_completion(classify_job.id)
|
||||
|
||||
if raise_on_error and classify_job_with_status.status == StatusEnum.ERROR:
|
||||
raise ValueError(
|
||||
f"Error classifying files under job ID {classify_job_with_status.id}"
|
||||
)
|
||||
|
||||
results = await self.client.classifier.get_classification_job_results(
|
||||
classify_job_with_status.id,
|
||||
project_id=self.project_id,
|
||||
organization_id=self.organization_id,
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def classify_file_ids(
|
||||
self,
|
||||
rules: list[ClassifierRule],
|
||||
file_ids: list[str],
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
) -> ClassifyJobResults:
|
||||
with augment_async_errors():
|
||||
return asyncio.run(
|
||||
self.aclassify_file_ids(
|
||||
rules, file_ids, parsing_configuration, raise_on_error
|
||||
)
|
||||
)
|
||||
|
||||
async def aclassify_file_path(
|
||||
self,
|
||||
rules: list[ClassifierRule],
|
||||
file_input_path: str,
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
) -> ClassifyJobResults:
|
||||
file = await self.file_client.upload_file(file_input_path)
|
||||
return await self.aclassify_file_ids(
|
||||
rules, [file.id], parsing_configuration, raise_on_error
|
||||
)
|
||||
|
||||
def classify_file_path(
|
||||
self,
|
||||
rules: list[ClassifierRule],
|
||||
file_input_path: str,
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
) -> ClassifyJobResults:
|
||||
with augment_async_errors():
|
||||
return asyncio.run(
|
||||
self.aclassify_file_path(
|
||||
rules, file_input_path, parsing_configuration, raise_on_error
|
||||
)
|
||||
)
|
||||
|
||||
async def aclassify_file_paths(
|
||||
self,
|
||||
rules: list[ClassifierRule],
|
||||
file_input_paths: list[str],
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
workers: int = DEFAULT_NUM_WORKERS,
|
||||
show_progress: bool = False,
|
||||
) -> ClassifyJobResults:
|
||||
coroutines = [self.file_client.upload_file(path) for path in file_input_paths]
|
||||
files: list[File] = await run_jobs(
|
||||
coroutines,
|
||||
show_progress=show_progress,
|
||||
workers=workers,
|
||||
desc="Uploading files for classification",
|
||||
)
|
||||
return await self.aclassify_file_ids(
|
||||
rules, [file.id for file in files], parsing_configuration, raise_on_error
|
||||
)
|
||||
|
||||
def classify_file_paths(
|
||||
self,
|
||||
rules: list[ClassifierRule],
|
||||
file_input_paths: list[str],
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
) -> ClassifyJobResults:
|
||||
with augment_async_errors():
|
||||
return asyncio.run(
|
||||
self.aclassify_file_paths(
|
||||
rules, file_input_paths, parsing_configuration, raise_on_error
|
||||
)
|
||||
)
|
||||
|
||||
async def _wait_for_job_completion(self, job_id: str) -> ClassifyJobWithStatus:
|
||||
job = await self.client.classifier.get_classify_job(
|
||||
job_id, project_id=self.project_id, organization_id=self.organization_id
|
||||
)
|
||||
start_time = time.time()
|
||||
while not is_terminal_status(job.status):
|
||||
polling_duration = time.time() - start_time
|
||||
if polling_duration > self.polling_timeout:
|
||||
raise TimeoutError(
|
||||
f"Job {job_id} timed out after {polling_duration} seconds"
|
||||
)
|
||||
await asyncio.sleep(self.polling_interval)
|
||||
job = await self.client.classifier.get_classify_job(
|
||||
job_id, project_id=self.project_id, organization_id=self.organization_id
|
||||
)
|
||||
return job
|
||||
@@ -1 +1,2 @@
|
||||
EU_BASE_URL = "https://api.cloud.eu.llamaindex.ai"
|
||||
POLLING_TIMEOUT_SECONDS = 300.0
|
||||
+447
-75
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
from io import BufferedIOBase, BufferedReader, BytesIO, TextIOWrapper
|
||||
@@ -8,6 +9,12 @@ 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,
|
||||
ExtractConfig,
|
||||
@@ -15,19 +22,20 @@ from llama_cloud import (
|
||||
ExtractJobCreate,
|
||||
ExtractRun,
|
||||
File,
|
||||
FileData,
|
||||
ExtractMode,
|
||||
StatusEnum,
|
||||
Project,
|
||||
ExtractTarget,
|
||||
LlamaExtractSettings,
|
||||
PaginatedExtractRunsResponse,
|
||||
)
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from llama_cloud.core.api_error import ApiError
|
||||
from llama_cloud_services.extract.utils import (
|
||||
JSONObjectType,
|
||||
augment_async_errors,
|
||||
ExperimentalWarning,
|
||||
)
|
||||
from llama_cloud_services.utils import augment_async_errors
|
||||
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
|
||||
@@ -41,10 +49,147 @@ SchemaInput = Union[JSONObjectType, Type[BaseModel]]
|
||||
|
||||
DEFAULT_EXTRACT_CONFIG = ExtractConfig(
|
||||
extraction_target=ExtractTarget.PER_DOC,
|
||||
extraction_mode=ExtractMode.BALANCED,
|
||||
extraction_mode=ExtractMode.MULTIMODAL,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def _validate_schema(
|
||||
client: AsyncLlamaCloud, data_schema: SchemaInput
|
||||
) -> JSONObjectType:
|
||||
"""Convert SchemaInput to a validated JSON schema dictionary."""
|
||||
processed_schema: JSONObjectType
|
||||
if isinstance(data_schema, dict):
|
||||
# TODO: if we expose a get_validated JSON schema method, we can use it here
|
||||
processed_schema = data_schema # type: ignore
|
||||
elif isinstance(data_schema, type) and issubclass(data_schema, BaseModel):
|
||||
processed_schema = data_schema.model_json_schema()
|
||||
else:
|
||||
raise ValueError("data_schema must be either a dictionary or a Pydantic model")
|
||||
|
||||
# Validate schema via API
|
||||
validated_schema = await client.llama_extract.validate_extraction_schema(
|
||||
data_schema=processed_schema
|
||||
)
|
||||
return validated_schema.data_schema
|
||||
|
||||
|
||||
async def _get_job_with_retry(
|
||||
client: AsyncLlamaCloud,
|
||||
job_id: str,
|
||||
max_attempts: int = 5,
|
||||
initial_wait: float = 1,
|
||||
max_wait: float = 60,
|
||||
jitter: float = 5,
|
||||
) -> ExtractJob:
|
||||
"""Get extraction job with retry logic."""
|
||||
async for attempt in AsyncRetrying(
|
||||
retry=retry_if_exception(_is_retryable_error),
|
||||
stop=stop_after_attempt(max_attempts),
|
||||
wait=wait_exponential_jitter(initial=initial_wait, max=max_wait, jitter=jitter),
|
||||
reraise=True,
|
||||
):
|
||||
with attempt:
|
||||
return await client.llama_extract.get_job(job_id=job_id)
|
||||
|
||||
|
||||
async def _get_run_with_retry(
|
||||
client: AsyncLlamaCloud,
|
||||
job_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
max_attempts: int = 3,
|
||||
initial_wait: float = 1,
|
||||
max_wait: float = 20,
|
||||
jitter: float = 3,
|
||||
) -> ExtractRun:
|
||||
"""Get extraction run with retry logic."""
|
||||
async for attempt in AsyncRetrying(
|
||||
retry=retry_if_exception(_is_retryable_error),
|
||||
stop=stop_after_attempt(max_attempts),
|
||||
wait=wait_exponential_jitter(initial=initial_wait, max=max_wait, jitter=jitter),
|
||||
reraise=True,
|
||||
):
|
||||
with attempt:
|
||||
return await client.llama_extract.get_run_by_job_id(
|
||||
job_id=job_id,
|
||||
project_id=project_id,
|
||||
organization_id=organization_id,
|
||||
)
|
||||
|
||||
|
||||
async def _wait_for_job_result(
|
||||
client: AsyncLlamaCloud,
|
||||
job_id: str,
|
||||
check_interval: int = 1,
|
||||
max_timeout: int = 2000,
|
||||
verbose: bool = False,
|
||||
project_id: Optional[str] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
job_retry_attempts: int = 5,
|
||||
job_max_wait: float = 60,
|
||||
job_jitter: float = 5,
|
||||
run_retry_attempts: int = 3,
|
||||
run_max_wait: float = 20,
|
||||
run_jitter: float = 3,
|
||||
) -> Optional[ExtractRun]:
|
||||
"""Wait for and return the results of an extraction job."""
|
||||
start = time.perf_counter()
|
||||
poll_count = 0
|
||||
|
||||
while True:
|
||||
await asyncio.sleep(check_interval)
|
||||
poll_count += 1
|
||||
job = await _get_job_with_retry(
|
||||
client,
|
||||
job_id,
|
||||
max_attempts=job_retry_attempts,
|
||||
max_wait=job_max_wait,
|
||||
jitter=job_jitter,
|
||||
)
|
||||
|
||||
if job.status == StatusEnum.SUCCESS:
|
||||
return await _get_run_with_retry(
|
||||
client,
|
||||
job_id,
|
||||
project_id,
|
||||
organization_id,
|
||||
max_attempts=run_retry_attempts,
|
||||
max_wait=run_max_wait,
|
||||
jitter=run_jitter,
|
||||
)
|
||||
elif job.status == StatusEnum.PENDING:
|
||||
end = time.perf_counter()
|
||||
if end - start > max_timeout:
|
||||
raise Exception(f"Timeout while extracting the file: {job_id}")
|
||||
if verbose and poll_count % 10 == 0:
|
||||
print(".", end="", flush=True)
|
||||
continue
|
||||
else:
|
||||
warnings.warn(
|
||||
f"Failure in job: {job_id}, status: {job.status}, error: {job.error}"
|
||||
)
|
||||
return await _get_run_with_retry(
|
||||
client,
|
||||
job_id,
|
||||
project_id,
|
||||
organization_id,
|
||||
max_attempts=run_retry_attempts,
|
||||
max_wait=run_max_wait,
|
||||
jitter=run_jitter,
|
||||
)
|
||||
|
||||
|
||||
class SourceText:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -120,22 +265,28 @@ def run_in_thread(
|
||||
|
||||
|
||||
def _extraction_config_warning(config: ExtractConfig) -> None:
|
||||
if config.extraction_mode == ExtractMode.ACCURATE:
|
||||
warnings.warn("ACCURATE extraction mode is deprecated. Using BALANCED instead.")
|
||||
config.extraction_mode = ExtractMode.BALANCED
|
||||
if config.use_reasoning:
|
||||
if config.cite_sources or config.confidence_scores:
|
||||
warnings.warn(
|
||||
"`use_reasoning` is an experimental feature. Results will be available in "
|
||||
"the `extraction_metadata` field for the extraction run.",
|
||||
ExperimentalWarning,
|
||||
)
|
||||
if config.cite_sources:
|
||||
warnings.warn(
|
||||
"`cite_sources` is an experimental feature. This may greatly increase the "
|
||||
"`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,
|
||||
)
|
||||
if config.use_reasoning:
|
||||
if config.extraction_mode == ExtractMode.FAST:
|
||||
raise ValueError(
|
||||
"`reasoning` is only supported with BALANCED, MULTIMODAL, or PREMIUM extraction modes."
|
||||
)
|
||||
if config.cite_sources:
|
||||
if config.extraction_mode in (ExtractMode.FAST, ExtractMode.BALANCED):
|
||||
raise ValueError(
|
||||
"`cite_sources` is only supported with MULTIMODAL or PREMIUM extraction modes."
|
||||
)
|
||||
if config.confidence_scores:
|
||||
if config.extraction_mode in (ExtractMode.FAST, ExtractMode.BALANCED):
|
||||
raise ValueError(
|
||||
"`confidence_scores` is only supported with MULTIMODAL or PREMIUM extraction modes."
|
||||
)
|
||||
|
||||
|
||||
class ExtractionAgent:
|
||||
@@ -186,22 +337,10 @@ class ExtractionAgent:
|
||||
|
||||
@data_schema.setter
|
||||
def data_schema(self, data_schema: SchemaInput) -> None:
|
||||
processed_schema: JSONObjectType
|
||||
if isinstance(data_schema, dict):
|
||||
# TODO: if we expose a get_validated JSON schema method, we can use it here
|
||||
processed_schema = data_schema # type: ignore
|
||||
elif isinstance(data_schema, type) and issubclass(data_schema, BaseModel):
|
||||
processed_schema = data_schema.model_json_schema()
|
||||
else:
|
||||
raise ValueError(
|
||||
"data_schema must be either a dictionary or a Pydantic model"
|
||||
)
|
||||
validated_schema = self._run_in_thread(
|
||||
self._client.llama_extract.validate_extraction_schema(
|
||||
data_schema=processed_schema
|
||||
)
|
||||
# Use the shared schema processing and validation function
|
||||
self._data_schema = self._run_in_thread(
|
||||
_validate_schema(self._client, data_schema)
|
||||
)
|
||||
self._data_schema = validated_schema.data_schema
|
||||
|
||||
@property
|
||||
def config(self) -> ExtractConfig:
|
||||
@@ -232,9 +371,8 @@ 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 file_input.text_content is not None:
|
||||
# Handle direct text content
|
||||
file_contents = BytesIO(file_input.text_content.encode("utf-8"))
|
||||
@@ -261,7 +399,9 @@ 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, BytesIO)
|
||||
):
|
||||
file_contents.close()
|
||||
|
||||
async def _upload_file(self, file_input: FileInput) -> File:
|
||||
@@ -291,33 +431,21 @@ class ExtractionAgent:
|
||||
|
||||
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,
|
||||
)
|
||||
return await _wait_for_job_result(
|
||||
client=self._client,
|
||||
job_id=job_id,
|
||||
check_interval=self.check_interval,
|
||||
max_timeout=self.max_timeout,
|
||||
verbose=self._verbose,
|
||||
project_id=self._project_id,
|
||||
organization_id=self._organization_id,
|
||||
job_retry_attempts=5,
|
||||
job_max_wait=60,
|
||||
job_jitter=5,
|
||||
run_retry_attempts=3,
|
||||
run_max_wait=20,
|
||||
run_jitter=3,
|
||||
)
|
||||
|
||||
def save(self) -> None:
|
||||
"""Persist the extraction agent's schema and config to the database.
|
||||
@@ -611,12 +739,14 @@ class LlamaExtract(BaseComponent):
|
||||
base_url = os.getenv("LLAMA_CLOUD_BASE_URL", None) or DEFAULT_BASE_URL
|
||||
|
||||
super().__init__(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
api_key=api_key, # type: ignore
|
||||
base_url=base_url, # type: ignore
|
||||
check_interval=check_interval,
|
||||
max_timeout=max_timeout,
|
||||
num_workers=num_workers,
|
||||
show_progress=show_progress,
|
||||
project_id=project_id,
|
||||
organization_id=organization_id,
|
||||
verify=verify,
|
||||
httpx_timeout=httpx_timeout,
|
||||
verbose=verbose,
|
||||
@@ -633,21 +763,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
|
||||
|
||||
@@ -683,7 +800,7 @@ class LlamaExtract(BaseComponent):
|
||||
config = DEFAULT_EXTRACT_CONFIG
|
||||
|
||||
if isinstance(data_schema, dict):
|
||||
data_schema = data_schema
|
||||
pass
|
||||
elif issubclass(data_schema, BaseModel):
|
||||
data_schema = data_schema.model_json_schema()
|
||||
else:
|
||||
@@ -784,6 +901,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,
|
||||
)
|
||||
for agent in agents
|
||||
]
|
||||
@@ -800,6 +919,245 @@ class LlamaExtract(BaseComponent):
|
||||
)
|
||||
)
|
||||
|
||||
async def _wait_for_job_result(self, job_id: str) -> Optional[ExtractRun]:
|
||||
"""Wait for and return the results of an extraction job."""
|
||||
return await _wait_for_job_result(
|
||||
client=self._async_client,
|
||||
job_id=job_id,
|
||||
check_interval=self.check_interval,
|
||||
max_timeout=self.max_timeout,
|
||||
verbose=self.verbose,
|
||||
project_id=self._project_id,
|
||||
organization_id=self._organization_id,
|
||||
job_retry_attempts=3,
|
||||
job_max_wait=4,
|
||||
job_jitter=5,
|
||||
run_retry_attempts=3,
|
||||
run_max_wait=4,
|
||||
run_jitter=3,
|
||||
)
|
||||
|
||||
def _get_mime_type(
|
||||
self,
|
||||
filename: Optional[str] = None,
|
||||
file_path: Optional[Union[str, Path]] = None,
|
||||
) -> str:
|
||||
"""Determine MIME type for a file based on filename or path."""
|
||||
# MIME type mappings for supported formats
|
||||
MIME_TYPE_MAP = {
|
||||
# Text files
|
||||
".txt": "text/plain",
|
||||
".csv": "text/csv",
|
||||
".json": "application/json",
|
||||
".html": "text/html",
|
||||
".htm": "text/html",
|
||||
".md": "text/markdown",
|
||||
# Document files
|
||||
".pdf": "application/pdf",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
# Image files
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
}
|
||||
|
||||
# Try to get extension from filename or file_path
|
||||
extension = None
|
||||
if filename:
|
||||
extension = Path(filename).suffix.lower()
|
||||
elif file_path:
|
||||
extension = Path(file_path).suffix.lower()
|
||||
|
||||
# Check if the extension is supported
|
||||
if extension and extension in MIME_TYPE_MAP:
|
||||
return MIME_TYPE_MAP[extension]
|
||||
|
||||
# If we don't have a supported extension, provide helpful error message
|
||||
supported_extensions = [ext[1:] for ext in MIME_TYPE_MAP.keys()] # Remove dots
|
||||
supported_list = ", ".join(sorted(supported_extensions))
|
||||
|
||||
if extension:
|
||||
ext_without_dot = extension[1:] # Remove the leading dot
|
||||
raise ValueError(
|
||||
f"Unsupported file type: '{ext_without_dot}'. "
|
||||
f"Supported formats are: {supported_list}"
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Could not determine file type. Please provide a filename with one of these supported extensions: {supported_list}"
|
||||
)
|
||||
|
||||
def _convert_file_to_file_data(self, file_input: FileInput) -> Union[FileData, str]:
|
||||
"""Convert FileInput to FileData or text string for stateless extraction."""
|
||||
if isinstance(file_input, SourceText):
|
||||
if file_input.text_content is not None:
|
||||
return file_input.text_content
|
||||
elif file_input.file is not None:
|
||||
if isinstance(file_input.file, bytes):
|
||||
data = file_input.file
|
||||
filename = file_input.filename
|
||||
elif isinstance(file_input.file, (str, Path)):
|
||||
with open(file_input.file, "rb") as f:
|
||||
data = f.read()
|
||||
filename = file_input.filename or str(file_input.file)
|
||||
elif isinstance(file_input.file, (BufferedIOBase, TextIOWrapper)):
|
||||
if hasattr(file_input.file, "read"):
|
||||
content = file_input.file.read()
|
||||
if isinstance(content, str):
|
||||
data = content.encode("utf-8")
|
||||
else:
|
||||
data = content
|
||||
else:
|
||||
raise ValueError("File object must have a read method")
|
||||
filename = file_input.filename or getattr(
|
||||
file_input.file, "name", None
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported file type: {type(file_input.file)}")
|
||||
|
||||
# Encode as base64
|
||||
encoded_data = base64.b64encode(data).decode("utf-8")
|
||||
|
||||
# Determine mime type
|
||||
mime_type = self._get_mime_type(filename=filename)
|
||||
|
||||
return FileData(data=encoded_data, mime_type=mime_type)
|
||||
else:
|
||||
raise ValueError("SourceText must have either text_content or file")
|
||||
|
||||
elif isinstance(file_input, (str, Path)):
|
||||
with open(file_input, "rb") as f:
|
||||
data = f.read()
|
||||
encoded_data = base64.b64encode(data).decode("utf-8")
|
||||
mime_type = self._get_mime_type(file_path=file_input)
|
||||
return FileData(data=encoded_data, mime_type=mime_type)
|
||||
|
||||
elif isinstance(file_input, bytes):
|
||||
# For raw bytes, we can't determine the file type, so we need to raise an error
|
||||
raise ValueError(
|
||||
"Cannot determine file type from raw bytes. Please use SourceText with a filename, or provide a file path."
|
||||
)
|
||||
|
||||
elif isinstance(file_input, (BufferedIOBase, TextIOWrapper)):
|
||||
if hasattr(file_input, "read"):
|
||||
content = file_input.read()
|
||||
if isinstance(content, str):
|
||||
data = content.encode("utf-8")
|
||||
else:
|
||||
data = content
|
||||
encoded_data = base64.b64encode(data).decode("utf-8")
|
||||
|
||||
# Try to get filename from the file object
|
||||
filename = getattr(file_input, "name", None)
|
||||
mime_type = self._get_mime_type(filename=filename)
|
||||
|
||||
return FileData(data=encoded_data, mime_type=mime_type)
|
||||
else:
|
||||
raise ValueError("File object must have a read method")
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported file input type: {type(file_input)}")
|
||||
|
||||
async def queue_extraction(
|
||||
self,
|
||||
data_schema: SchemaInput,
|
||||
config: ExtractConfig,
|
||||
files: Union[FileInput, List[FileInput]],
|
||||
) -> Union[ExtractJob, List[ExtractJob]]:
|
||||
"""Queue extraction jobs using stateless extraction (no agent required).
|
||||
|
||||
Args:
|
||||
data_schema: The schema defining what data to extract
|
||||
config: The extraction configuration
|
||||
files: File(s) to extract from
|
||||
|
||||
Returns:
|
||||
ExtractJob or list of ExtractJobs
|
||||
"""
|
||||
_extraction_config_warning(config)
|
||||
processed_schema = await _validate_schema(self._async_client, data_schema)
|
||||
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
|
||||
jobs = []
|
||||
for file_input in files:
|
||||
file_data_or_text = self._convert_file_to_file_data(file_input)
|
||||
|
||||
if isinstance(file_data_or_text, str):
|
||||
# It's text content
|
||||
job = await self._async_client.llama_extract.extract_stateless(
|
||||
project_id=self._project_id,
|
||||
organization_id=self._organization_id,
|
||||
data_schema=processed_schema,
|
||||
config=config,
|
||||
text=file_data_or_text,
|
||||
)
|
||||
else:
|
||||
# It's FileData
|
||||
job = await self._async_client.llama_extract.extract_stateless(
|
||||
project_id=self._project_id,
|
||||
organization_id=self._organization_id,
|
||||
data_schema=processed_schema,
|
||||
config=config,
|
||||
file=file_data_or_text,
|
||||
)
|
||||
jobs.append(job)
|
||||
|
||||
return jobs[0] if len(jobs) == 1 else jobs
|
||||
|
||||
async def aextract(
|
||||
self,
|
||||
data_schema: SchemaInput,
|
||||
config: ExtractConfig,
|
||||
files: Union[FileInput, List[FileInput]],
|
||||
) -> Union[ExtractRun, List[ExtractRun]]:
|
||||
"""Run stateless extraction and wait for results.
|
||||
|
||||
Args:
|
||||
data_schema: The schema defining what data to extract
|
||||
config: The extraction configuration
|
||||
files: File(s) to extract from
|
||||
|
||||
Returns:
|
||||
ExtractRun or list of ExtractRuns with the extraction results
|
||||
"""
|
||||
jobs = await self.queue_extraction(data_schema, config, files)
|
||||
|
||||
if isinstance(jobs, list):
|
||||
runs = []
|
||||
for job in jobs:
|
||||
run = await self._wait_for_job_result(job.id)
|
||||
if run is None:
|
||||
raise RuntimeError(
|
||||
f"Failed to get extraction result for job {job.id}"
|
||||
)
|
||||
runs.append(run)
|
||||
return runs
|
||||
else:
|
||||
run = await self._wait_for_job_result(jobs.id)
|
||||
if run is None:
|
||||
raise RuntimeError(f"Failed to get extraction result for job {jobs.id}")
|
||||
return run
|
||||
|
||||
def extract(
|
||||
self,
|
||||
data_schema: SchemaInput,
|
||||
config: ExtractConfig,
|
||||
files: Union[FileInput, List[FileInput]],
|
||||
) -> Union[ExtractRun, List[ExtractRun]]:
|
||||
"""Run stateless extraction and wait for results (synchronous version).
|
||||
|
||||
Args:
|
||||
data_schema: The schema defining what data to extract
|
||||
config: The extraction configuration
|
||||
files: File(s) to extract from
|
||||
|
||||
Returns:
|
||||
ExtractRun or list of ExtractRuns with the extraction results
|
||||
"""
|
||||
return self._run_in_thread(self.aextract(data_schema, config, files))
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""Cleanup resources properly."""
|
||||
try:
|
||||
@@ -814,6 +1172,20 @@ if __name__ == "__main__":
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Example usage:
|
||||
#
|
||||
# # Basic usage with stateless extraction (no agent required)
|
||||
# extractor = LlamaExtract()
|
||||
# schema = {"name": {"type": "string"}, "email": {"type": "string"}}
|
||||
# config = ExtractConfig(extraction_mode=ExtractMode.FAST)
|
||||
# files = ["path/to/document.pdf"]
|
||||
#
|
||||
# # Queue extraction jobs
|
||||
# jobs = extractor.queue_extraction(schema, config, files)
|
||||
#
|
||||
# # Or run extraction and wait for results
|
||||
# results = extractor.extract(schema, config, files)
|
||||
|
||||
data_dir = Path(__file__).parent.parent / "tests" / "data"
|
||||
extractor = LlamaExtract()
|
||||
try:
|
||||
@@ -0,0 +1,21 @@
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
|
||||
def is_jupyter() -> bool:
|
||||
"""Check if we're running in a Jupyter environment."""
|
||||
try:
|
||||
from IPython import get_ipython
|
||||
|
||||
return get_ipython().__class__.__name__ == "ZMQInteractiveShell"
|
||||
except (ImportError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
JSONType = Union[Dict[str, Any], List[Any], str, int, float, bool, None]
|
||||
JSONObjectType = Dict[str, JSONType]
|
||||
|
||||
|
||||
class ExperimentalWarning(Warning):
|
||||
"""Warning for experimental features."""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,3 @@
|
||||
from .client import FileClient
|
||||
|
||||
__all__ = ["FileClient"]
|
||||
@@ -0,0 +1,97 @@
|
||||
from io import BytesIO
|
||||
from typing import BinaryIO
|
||||
import os
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from llama_cloud.types import File, FileCreate
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class FileClient:
|
||||
"""
|
||||
Higher-level client for interacting with the LlamaCloud Files API.
|
||||
Uses presigned URLs for uploads by default.
|
||||
|
||||
Args:
|
||||
client: The LlamaCloud client to use.
|
||||
project_id: The project ID to use.
|
||||
organization_id: The organization ID to use.
|
||||
use_presigned_url: Whether to use presigned URLs for uploads (set to False when uploading to BYOC deployments).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: AsyncLlamaCloud,
|
||||
project_id: Optional[str] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
use_presigned_url: bool = True,
|
||||
):
|
||||
self.client = client
|
||||
self.project_id = project_id
|
||||
self.organization_id = organization_id
|
||||
self.use_presigned_url = use_presigned_url
|
||||
|
||||
async def get_file(self, file_id: str) -> File:
|
||||
return await self.client.files.get_file(
|
||||
file_id, project_id=self.project_id, organization_id=self.organization_id
|
||||
)
|
||||
|
||||
async def read_file_content(self, file_id: str) -> bytes:
|
||||
presigned_url = await self.client.files.read_file_content(
|
||||
file_id,
|
||||
project_id=self.project_id,
|
||||
organization_id=self.organization_id,
|
||||
)
|
||||
httpx_client = self.client._client_wrapper.httpx_client
|
||||
response = await httpx_client.get(presigned_url.url)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
|
||||
async def upload_file(
|
||||
self, file_path: str, external_file_id: Optional[str] = None
|
||||
) -> File:
|
||||
external_file_id = external_file_id or file_path
|
||||
file_size = os.path.getsize(file_path)
|
||||
with open(file_path, "rb") as file:
|
||||
return await self.upload_buffer(file, external_file_id, file_size)
|
||||
|
||||
async def upload_bytes(self, bytes: bytes, external_file_id: str) -> File:
|
||||
return await self.upload_buffer(BytesIO(bytes), external_file_id, len(bytes))
|
||||
|
||||
async def upload_buffer(
|
||||
self,
|
||||
buffer: BinaryIO,
|
||||
external_file_id: str,
|
||||
file_size: int,
|
||||
) -> File:
|
||||
if self.use_presigned_url:
|
||||
if getattr(buffer, "name", None):
|
||||
name = os.path.basename(str(getattr(buffer, "name", external_file_id)))
|
||||
else:
|
||||
name = external_file_id
|
||||
presigned_url = await self.client.files.generate_presigned_url(
|
||||
project_id=self.project_id,
|
||||
organization_id=self.organization_id,
|
||||
request=FileCreate(
|
||||
name=name,
|
||||
external_file_id=external_file_id,
|
||||
file_size=file_size,
|
||||
),
|
||||
)
|
||||
httpx_client = self.client._client_wrapper.httpx_client
|
||||
upload_response = await httpx_client.put(
|
||||
presigned_url.url,
|
||||
data=buffer.read(),
|
||||
)
|
||||
upload_response.raise_for_status()
|
||||
return await self.client.files.get_file(
|
||||
presigned_url.file_id,
|
||||
project_id=self.project_id,
|
||||
organization_id=self.organization_id,
|
||||
)
|
||||
else:
|
||||
return await self.client.files.upload_file(
|
||||
upload_file=buffer,
|
||||
external_file_id=external_file_id,
|
||||
project_id=self.project_id,
|
||||
organization_id=self.organization_id,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
from .base import LlamaCloudIndex
|
||||
from .retriever import LlamaCloudRetriever
|
||||
from .composite_retriever import (
|
||||
LlamaCloudCompositeRetriever,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LlamaCloudIndex",
|
||||
"LlamaCloudRetriever",
|
||||
"LlamaCloudCompositeRetriever",
|
||||
]
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import mimetypes
|
||||
import os
|
||||
import time
|
||||
import warnings
|
||||
from contextlib import asynccontextmanager
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
@@ -13,21 +14,30 @@ 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, check_for_updates
|
||||
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
|
||||
@@ -57,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."""
|
||||
|
||||
@@ -234,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.",
|
||||
@@ -281,6 +325,10 @@ class LlamaParse(BasePydanticReader):
|
||||
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)",
|
||||
@@ -333,6 +381,10 @@ class LlamaParse(BasePydanticReader):
|
||||
default=False,
|
||||
description="Preserve grid alignment across page in text mode.",
|
||||
)
|
||||
preserve_very_small_text: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set, the parser will try to preserve very small text lines. This can be useful for documents containing vector graphics with very small text lines that may not be recognized by OCR or a vision model (such as in CAD drawings).",
|
||||
)
|
||||
replace_failed_page_mode: Optional[FailedPageMode] = Field(
|
||||
default=None,
|
||||
description="The mode to use to replace the failed page, see FailedPageMode enum for possible value. If set, the parser will replace the failed page with the specified mode. If not set, the default mode (raw_text) will be used.",
|
||||
@@ -422,6 +474,34 @@ class LlamaParse(BasePydanticReader):
|
||||
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(
|
||||
@@ -461,6 +541,26 @@ class LlamaParse(BasePydanticReader):
|
||||
description="Whether to use the vendor multimodal API.",
|
||||
)
|
||||
|
||||
check_for_updates: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Automatically check for Python SDK updates.",
|
||||
)
|
||||
|
||||
@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:
|
||||
@@ -501,6 +601,16 @@ class LlamaParse(BasePydanticReader):
|
||||
|
||||
return self._aclient
|
||||
|
||||
_update_checked = False
|
||||
|
||||
async def _check_for_updates(self) -> None:
|
||||
if self.check_for_updates and not self._update_checked:
|
||||
try:
|
||||
await check_for_updates(self.aclient, quiet=False)
|
||||
self._update_checked = True
|
||||
except (ValueError, httpx.HTTPStatusError):
|
||||
pass
|
||||
|
||||
@asynccontextmanager
|
||||
async def client_context(self) -> AsyncGenerator[httpx.AsyncClient, None]:
|
||||
"""Create a context for the HTTPX client."""
|
||||
@@ -548,7 +658,9 @@ class LlamaParse(BasePydanticReader):
|
||||
file_input: FileInput,
|
||||
extra_info: Optional[dict] = None,
|
||||
fs: Optional[AbstractFileSystem] = None,
|
||||
partition_target_pages: Optional[str] = None,
|
||||
) -> str:
|
||||
await self._check_for_updates()
|
||||
files = None
|
||||
file_handle = None
|
||||
input_url = file_input if self._is_input_url(file_input) else None
|
||||
@@ -698,6 +810,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
|
||||
|
||||
@@ -769,6 +884,29 @@ class LlamaParse(BasePydanticReader):
|
||||
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:
|
||||
@@ -794,6 +932,9 @@ class LlamaParse(BasePydanticReader):
|
||||
"preserve_layout_alignment_across_pages"
|
||||
] = self.preserve_layout_alignment_across_pages
|
||||
|
||||
if self.preserve_very_small_text:
|
||||
data["preserve_very_small_text"] = self.preserve_very_small_text
|
||||
|
||||
if self.preset is not None:
|
||||
data["preset"] = self.preset
|
||||
|
||||
@@ -845,7 +986,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
|
||||
@@ -942,15 +1085,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,
|
||||
@@ -959,6 +1094,7 @@ class LlamaParse(BasePydanticReader):
|
||||
httpx.ReadTimeout,
|
||||
httpx.WriteTimeout,
|
||||
httpx.HTTPStatusError,
|
||||
httpx.RemoteProtocolError,
|
||||
) as err:
|
||||
error_count += 1
|
||||
end = time.time()
|
||||
@@ -979,9 +1115,39 @@ class LlamaParse(BasePydanticReader):
|
||||
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)
|
||||
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(
|
||||
@@ -989,21 +1155,105 @@ class LlamaParse(BasePydanticReader):
|
||||
)
|
||||
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, result = await self._parse_one(
|
||||
file_path, extra_info=extra_info, fs=fs
|
||||
)
|
||||
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 {},
|
||||
)
|
||||
]
|
||||
@@ -1026,7 +1276,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
|
||||
@@ -1038,6 +1292,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
|
||||
]
|
||||
@@ -1076,6 +1331,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],
|
||||
@@ -1094,7 +1377,7 @@ 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)):
|
||||
@@ -1106,22 +1389,10 @@ class LlamaParse(BasePydanticReader):
|
||||
file_name = extra_info["file_name"]
|
||||
else:
|
||||
file_name = str(file_path)
|
||||
|
||||
job_id, job_result = await self._parse_one(
|
||||
file_path,
|
||||
extra_info=extra_info,
|
||||
fs=fs,
|
||||
result_type=ResultType.JSON.value,
|
||||
)
|
||||
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):
|
||||
file_names = []
|
||||
@@ -1135,35 +1406,25 @@ class LlamaParse(BasePydanticReader):
|
||||
else:
|
||||
file_names.append(str(f))
|
||||
|
||||
job_results = []
|
||||
try:
|
||||
job_results = await run_jobs(
|
||||
for result in await run_jobs(
|
||||
[
|
||||
self._parse_one(
|
||||
self._aparse_one(
|
||||
f,
|
||||
file_names[i],
|
||||
extra_info=extra_info,
|
||||
fs=fs,
|
||||
result_type=ResultType.JSON.value,
|
||||
num_workers=1,
|
||||
)
|
||||
for f in file_path
|
||||
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
|
||||
return [
|
||||
JobResult(
|
||||
job_id=job_id,
|
||||
file_name=file_names[i],
|
||||
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 i, (job_id, job_result) in enumerate(job_results)
|
||||
]
|
||||
):
|
||||
job_results.extend(result)
|
||||
return job_results
|
||||
|
||||
except RuntimeError as e:
|
||||
if nest_asyncio_err in str(e):
|
||||
@@ -1204,21 +1465,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, result = await self._parse_one(
|
||||
job_results = await self._parse_one(
|
||||
file_path,
|
||||
extra_info=extra_info,
|
||||
result_type=ResultType.JSON.value,
|
||||
num_workers=num_workers,
|
||||
)
|
||||
result["job_id"] = job_id
|
||||
|
||||
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)
|
||||
@@ -1233,7 +1500,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]
|
||||
@@ -1254,7 +1521,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(
|
||||
@@ -1283,6 +1550,7 @@ class LlamaParse(BasePydanticReader):
|
||||
self, json_result: List[dict], download_path: str, asset_key: str
|
||||
) -> List[dict]:
|
||||
"""Download assets (images or charts) from the parsed result."""
|
||||
await self._check_for_updates()
|
||||
# Make the download path
|
||||
if not os.path.exists(download_path):
|
||||
os.makedirs(download_path)
|
||||
@@ -1377,10 +1645,21 @@ 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]:
|
||||
"""Download xlsx from the parsed result."""
|
||||
await self._check_for_updates()
|
||||
# make the download path
|
||||
if not os.path.exists(download_path):
|
||||
os.makedirs(download_path)
|
||||
@@ -1443,3 +1722,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,11 +14,14 @@ PAGE_REGEX = r"page[-_](\d+)\.jpg$"
|
||||
class JobMetadata(BaseModel):
|
||||
"""Metadata about the job."""
|
||||
|
||||
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_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_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):
|
||||
@@ -43,12 +46,16 @@ 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: Optional[BBox] = Field(
|
||||
default=None, description="The bounding box of the item."
|
||||
)
|
||||
html: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The HTML-formatted content of the item. Only applicable for table items when output_tables_as_HTML=True.",
|
||||
)
|
||||
|
||||
|
||||
class ImageItem(BaseModel):
|
||||
@@ -105,7 +112,7 @@ class ChartItem(BaseModel):
|
||||
class Page(BaseModel):
|
||||
"""A page of the document."""
|
||||
|
||||
page: int = Field(description="The page number.")
|
||||
page: int = Field(default=0, description="The page number.")
|
||||
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(
|
||||
@@ -124,32 +131,49 @@ 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: Optional[float] = Field(default=None, description="The width of the page.")
|
||||
height: Optional[float] = Field(default=None, description="The height of the page.")
|
||||
triggeredAutoMode: bool = Field(
|
||||
description="Whether the page triggered auto mode (thus increasing the cost)."
|
||||
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."
|
||||
)
|
||||
isAudioTranscript: bool = Field(
|
||||
default=False, description="Whether the page is an audio transcript."
|
||||
)
|
||||
durationInSeconds: Optional[float] = Field(
|
||||
default=None, description="The duration of the audio transcript in seconds."
|
||||
)
|
||||
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."
|
||||
@@ -281,13 +305,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__)
|
||||
|
||||
@@ -297,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
|
||||
@@ -0,0 +1,106 @@
|
||||
import os
|
||||
import importlib.metadata
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
import difflib
|
||||
from llama_cloud.types import StatusEnum
|
||||
import httpx
|
||||
import packaging.version
|
||||
from pydantic import BaseModel
|
||||
from typing import Any, Dict, List, Tuple, Type
|
||||
|
||||
# Asyncio error messages
|
||||
nest_asyncio_err = "cannot be called from a running event loop"
|
||||
nest_asyncio_msg = (
|
||||
"The event loop is already running. "
|
||||
"Add `import nest_asyncio; nest_asyncio.apply()` to your code to fix this issue."
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def is_terminal_status(status: StatusEnum) -> bool:
|
||||
"""
|
||||
Check if a status is terminal, i.e. the job is done and no more updates are expected.
|
||||
Note: this must be updated if the status enum is updated.
|
||||
|
||||
Args:
|
||||
status: The status to check
|
||||
|
||||
Returns:
|
||||
True if the status is terminal, False otherwise
|
||||
"""
|
||||
return status in {
|
||||
StatusEnum.SUCCESS,
|
||||
StatusEnum.ERROR,
|
||||
StatusEnum.CANCELLED,
|
||||
StatusEnum.PARTIAL_SUCCESS,
|
||||
}
|
||||
|
||||
|
||||
async def check_for_updates(client: httpx.AsyncClient, quiet: bool = True) -> bool:
|
||||
"""Check if an SDK update is available.
|
||||
|
||||
Args:
|
||||
client: HTTPX client to use.
|
||||
quiet: If False, update availability will also be printed to stdout.
|
||||
|
||||
Returns: True if an update is available.
|
||||
|
||||
Raises:
|
||||
ValueError: Failed to get a valid release version from PyPI.
|
||||
"""
|
||||
package_name = "llama-cloud-services"
|
||||
r = await client.get(f"https://pypi.org/pypi/{package_name}/json")
|
||||
version = r.json().get("info", {}).get("version", "")
|
||||
if not version:
|
||||
raise ValueError("Failed to fetch package info from PyPI")
|
||||
latest = packaging.version.parse(version)
|
||||
current = packaging.version.parse(importlib.metadata.version(package_name))
|
||||
if current < latest:
|
||||
if not quiet:
|
||||
msg = [
|
||||
f"\u26A0\uFE0F {package_name} is out of date",
|
||||
f"Current version: {current} | Latest: {latest}",
|
||||
"To upgrade: pip install -U --force-reinstall llama-cloud-services",
|
||||
]
|
||||
print(os.linesep.join(msg))
|
||||
return True
|
||||
elif not quiet:
|
||||
print(f"{package_name} is up to date")
|
||||
return False
|
||||
|
||||
|
||||
@contextmanager
|
||||
def augment_async_errors() -> Generator[None, None, None]:
|
||||
"""Context manager to add helpful information for errors due to nested event loops."""
|
||||
try:
|
||||
yield
|
||||
except RuntimeError as e:
|
||||
if nest_asyncio_err in str(e):
|
||||
raise RuntimeError(nest_asyncio_msg)
|
||||
raise
|
||||
@@ -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
|
||||
|
||||
@@ -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.58"
|
||||
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.58"]
|
||||
|
||||
[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"]
|
||||
Generated
+2844
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user