mirror of
https://github.com/run-llama/llama_cloud_services.git
synced 2026-07-21 12:05:23 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc7d54be4c |
@@ -0,0 +1,48 @@
|
||||
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"
|
||||
@@ -1,53 +0,0 @@
|
||||
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"
|
||||
@@ -1,36 +0,0 @@
|
||||
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
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Lint - Python
|
||||
name: Linting
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
UV_VERSION: "0.7.20"
|
||||
POETRY_VERSION: "1.6.1"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -21,15 +21,17 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Set up python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
version: ${{ env.UV_VERSION }}
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install ${{ matrix.python-version }}
|
||||
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
version: ${{ env.POETRY_VERSION }}
|
||||
- name: Install pre-commit
|
||||
shell: bash
|
||||
run: poetry run pip install pre-commit
|
||||
- name: Run linter
|
||||
shell: bash
|
||||
working-directory: py
|
||||
run: uv run -- pre-commit run -a
|
||||
run: poetry run make lint
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Lint - TypeScript
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "ts/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "ts/**"
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_REMOTE_ONLY: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: "ts/llama_cloud_services/.nvmrc"
|
||||
- name: Install dependencies
|
||||
working-directory: ts/llama_cloud_services/
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: Run lint
|
||||
working-directory: ts/llama_cloud_services/
|
||||
run: pnpm run lint
|
||||
- name: Run Prettier
|
||||
working-directory: ts/llama_cloud_services/
|
||||
run: pnpm run format
|
||||
@@ -0,0 +1,83 @@
|
||||
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 120
|
||||
|
||||
- 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
|
||||
@@ -1,66 +0,0 @@
|
||||
name: Publish Release - Python
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
UV_VERSION: "0.7.20"
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish to PyPI
|
||||
if: github.repository == 'run-llama/llama_cloud_services'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@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
|
||||
@@ -1,51 +0,0 @@
|
||||
name: Publish Release - TypeScript
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "llama-cloud-services@*"
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: "ts/llama_cloud_services/.nvmrc"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ts/llama_cloud_services
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Build tarball
|
||||
run: |
|
||||
pnpm pack
|
||||
working-directory: ts/llama_cloud_services
|
||||
|
||||
- name: Setup npm authentication
|
||||
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Release
|
||||
working-directory: ts/llama_cloud_services
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: pnpm publish --access public --no-git-checks
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "ts/llama_cloud_services/llama-cloud-services*.tgz"
|
||||
name: Release ${{ github.ref }} - LlamaCloud Services TS
|
||||
bodyFile: "ts/llama_cloud_services/CHANGELOG.md"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Test end-to-end - Python
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "py/**"
|
||||
|
||||
env:
|
||||
UV_VERSION: "0.7.20"
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
|
||||
jobs:
|
||||
test_e2e:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
# You can use PyPy versions in python-version.
|
||||
# For example, pypy-2.7 and pypy-3.8
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: ${{ env.UV_VERSION }}
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install ${{ matrix.python-version }} && uv python pin ${{ matrix.python-version }}
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: py
|
||||
run: uv run pytest unit_tests/ tests/ -v
|
||||
|
||||
- name: Remove virtual environment
|
||||
working-directory: py
|
||||
run: rm -rf .venv/
|
||||
@@ -1,42 +0,0 @@
|
||||
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/
|
||||
@@ -1,37 +0,0 @@
|
||||
name: Lint - TypeScript
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "ts/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- "ts/**"
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
TURBO_REMOTE_ONLY: true
|
||||
LLAMA_CLOUD_API_KEY: ${{ secrets.LLAMA_CLOUD_API_KEY }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test - TypeScript
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: "ts/llama_cloud_services/.nvmrc"
|
||||
- name: Install dependencies
|
||||
working-directory: ts/llama_cloud_services/
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
- name: Run tests
|
||||
working-directory: ts/llama_cloud_services/
|
||||
run: pnpm test
|
||||
@@ -0,0 +1,40 @@
|
||||
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,7 +5,3 @@ __pycache__/
|
||||
.idea
|
||||
.env*
|
||||
.ipynb_checkpoints*
|
||||
*_cache/
|
||||
node_modules/
|
||||
.turbo/
|
||||
dist/
|
||||
|
||||
@@ -21,19 +21,19 @@ repos:
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
exclude: ".*uv.lock"
|
||||
exclude: ".*poetry.lock"
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 23.10.1
|
||||
hooks:
|
||||
- id: black-jupyter
|
||||
name: black-src
|
||||
alias: black
|
||||
exclude: ".*uv.lock"
|
||||
exclude: ".*poetry.lock"
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.0.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: ^py/tests|^py/unit_tests
|
||||
exclude: ^tests/
|
||||
additional_dependencies:
|
||||
[
|
||||
"types-requests",
|
||||
@@ -63,13 +63,13 @@ repos:
|
||||
rev: v3.0.3
|
||||
hooks:
|
||||
- id: prettier
|
||||
exclude: ^(uv.lock|ts/llama_cloud_services/pnpm-lock.yaml)
|
||||
exclude: poetry.lock
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies: [tomli]
|
||||
exclude: ^(uv.lock|docs|ts|examples)
|
||||
exclude: ^(poetry.lock|examples)
|
||||
args:
|
||||
[
|
||||
"--ignore-words-list",
|
||||
@@ -84,6 +84,6 @@ repos:
|
||||
rev: v0.23.1
|
||||
hooks:
|
||||
- id: toml-sort-fix
|
||||
exclude: ".*uv.lock"
|
||||
exclude: ".*poetry.lock"
|
||||
|
||||
exclude: .github/ISSUE_TEMPLATE
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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
|
||||
|
||||
...
|
||||
@@ -11,7 +11,6 @@ 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
|
||||
|
||||
@@ -26,19 +25,11 @@ 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:
|
||||
@@ -46,7 +37,6 @@ 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 🇪🇺
|
||||
|
||||
@@ -65,12 +55,6 @@ 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
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# LlamaCloud Services Examples
|
||||
|
||||
In this folder you will find several python notebooks and two end-to-end typescript applications that contain examples regarding:
|
||||
|
||||
- [LlamaParse - Python](./parse/)
|
||||
- [LlamaParse - TypeScript](./parse-ts/)
|
||||
- [LlamaExtract - Python](./extract/)
|
||||
- [LlamaReport - Python](./report/)
|
||||
- [LlamaCloud Index - TypeScript](./index-ts/)
|
||||
|
||||
Follow the instructions of each notebook/application to get started!
|
||||
@@ -1,131 +0,0 @@
|
||||
# LlamaCloud Index Demo
|
||||
|
||||
A TypeScript demo application showcasing the power of **LlamaCloud Index** - a fully automated document ingestion and retrieval serviced offered within [LlamaCloud](https://cloud.llamaindex.ai). This demo allows you to ask questions, retrieve relevant contextual information and generate AI-powered responses using OpenAI's GPT models.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Start the Demo](#start-the-demo)
|
||||
- [Development Mode](#development-mode)
|
||||
- [Build the Project](#build-the-project)
|
||||
- [Code Quality](#code-quality)
|
||||
- [Quick Commands Reference](#quick-commands-reference)
|
||||
- [How It Works](#how-it-works)
|
||||
- [API Dependencies](#api-dependencies)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Common Issues](#common-issues)
|
||||
- [License](#license)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Features
|
||||
|
||||
- 🤖 **RAG**: Simple-yet-effective Retrieval Augmented Generation pipeline built on top of LlamaCloud Index and OpenAI
|
||||
- 🎨 **Beautiful CLI**: Styled console interface with colors and ASCII art
|
||||
- ⚡ **Fast Development**: Hot reload support with watch mode
|
||||
- 🛠️ **TypeScript**: Full TypeScript support with strict type checking
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (version 18 or higher)
|
||||
- pnpm package manager
|
||||
- OpenAI API key
|
||||
- LlamaCloud API key
|
||||
- An existing LlamaCloud Index pipeline
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd llamaparse-demo
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. Set up your environment variables:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="your-openai-api-key"
|
||||
export LLAMA_CLOUD_API_KEY="your-llamacloud-api-key"
|
||||
export PIPELINE_NAME="your-pipeline-name"
|
||||
```
|
||||
|
||||
4. Or write them into a `.env` file:
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY="your-openai-api-key"
|
||||
LLAMA_CLOUD_API_KEY="your-llamacloud-api-key"
|
||||
PIPELINE_NAME="your-pipeline-name"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Start the Demo
|
||||
|
||||
```bash
|
||||
pnpm run start
|
||||
```
|
||||
|
||||
The application will display a welcome screen and prompt you to start chatting!
|
||||
|
||||
### Development Mode
|
||||
|
||||
For development with hot reload:
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### Build the Project
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
Format code:
|
||||
|
||||
```bash
|
||||
pnpm run format
|
||||
```
|
||||
|
||||
Lint code:
|
||||
|
||||
```bash
|
||||
pnpm run lint
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Message Input**: Enter a message
|
||||
2. **Retrieval**: Several nodes are retrieved from the LlamaCloud index you specified
|
||||
3. **AI Response Generation**: The retrieved information is passed on to the AI model, along with its relevance score, and a reply to your original message is generated starting from that.
|
||||
4. **Results**: View the AI-generated summary in your terminal
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Module Resolution Errors**: Ensure you're using Node.js 18+ and have all dependencies installed
|
||||
2. **API Key Issues**: Verify your OpenAI and LlamaCloud API keys are correctly set
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see the [LICENSE](../../../LICENSE) file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Run `pnpm format` and `pnpm lint`
|
||||
5. Submit a pull request
|
||||
@@ -1,15 +0,0 @@
|
||||
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,
|
||||
]);
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"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
@@ -1,48 +0,0 @@
|
||||
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);
|
||||
@@ -1,8 +0,0 @@
|
||||
import { createConsola } from "consola";
|
||||
import type { ConsolaInstance } from "consola";
|
||||
|
||||
export const logger: ConsolaInstance = createConsola({
|
||||
formatOptions: {
|
||||
date: false,
|
||||
},
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
# LlamaParse Demo
|
||||
|
||||
A TypeScript demo application showcasing the power of **LlamaParse** - an intelligent document parsing service from [LlamaCloud](https://cloud.llamaindex.ai). This demo allows you to parse various document formats and generate AI-powered summaries using OpenAI's GPT models.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Start the Demo](#start-the-demo)
|
||||
- [Development Mode](#development-mode)
|
||||
- [Build the Project](#build-the-project)
|
||||
- [Code Quality](#code-quality)
|
||||
- [Quick Commands Reference](#quick-commands-reference)
|
||||
- [How It Works](#how-it-works)
|
||||
- [API Dependencies](#api-dependencies)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Common Issues](#common-issues)
|
||||
- [License](#license)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Features
|
||||
|
||||
- 📄 **Document Parsing**: Parse PDFs, Word docs, and other formats using LlamaParse
|
||||
- 🤖 **AI Summaries**: Generate intelligent summaries using OpenAI GPT-4
|
||||
- 🎨 **Beautiful CLI**: Styled console interface with colors and ASCII art
|
||||
- ⚡ **Fast Development**: Hot reload support with watch mode
|
||||
- 🛠️ **TypeScript**: Full TypeScript support with strict type checking
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (version 18 or higher)
|
||||
- pnpm package manager
|
||||
- OpenAI API key
|
||||
- LlamaCloud API key
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd llamaparse-demo
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. Set up your environment variables:
|
||||
|
||||
```bash
|
||||
# Add your API keys to your environment
|
||||
export OPENAI_API_KEY="your-openai-api-key"
|
||||
export LLAMA_CLOUD_API_KEY="your-llamacloud-api-key"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Start the Demo
|
||||
|
||||
```bash
|
||||
pnpm run start
|
||||
```
|
||||
|
||||
The application will display a welcome screen and prompt you to enter the path to a document you'd like to process.
|
||||
|
||||
### Development Mode
|
||||
|
||||
For development with hot reload:
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### Build the Project
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
Format code:
|
||||
|
||||
```bash
|
||||
pnpm run format
|
||||
```
|
||||
|
||||
Lint code:
|
||||
|
||||
```bash
|
||||
pnpm run lint
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Document Input**: Enter the path to your document when prompted
|
||||
2. **Parsing**: LlamaParse processes the document and extracts structured content
|
||||
3. **AI Summary**: The extracted content is sent to OpenAI GPT-4 for summarization
|
||||
4. **Results**: View the AI-generated summary in your terminal
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Module Resolution Errors**: Ensure you're using Node.js 18+ and have all dependencies installed
|
||||
2. **API Key Issues**: Verify your OpenAI and LlamaCloud API keys are correctly set
|
||||
3. **File Path Errors**: Use absolute paths or ensure relative paths are correct from the project root
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see the [LICENSE](../../../LICENSE) file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Run `pnpm format` and `pnpm lint`
|
||||
5. Submit a pull request
|
||||
Binary file not shown.
@@ -1,15 +0,0 @@
|
||||
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,
|
||||
]);
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"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
@@ -1,34 +0,0 @@
|
||||
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);
|
||||
@@ -1,8 +0,0 @@
|
||||
import { createConsola } from "consola";
|
||||
import type { ConsolaInstance } from "consola";
|
||||
|
||||
export const logger: ConsolaInstance = createConsola({
|
||||
formatOptions: {
|
||||
date: false,
|
||||
},
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
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 through items of the page\n",
|
||||
" # loop trough 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",
|
||||
|
||||
+1
-1
@@ -208,5 +208,5 @@ Another option (orthogonal to the above) is to break the document into smaller s
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Example Notebook](docs/examples-py/extract/resume_screening.ipynb) - Detailed walkthrough of resume parsing
|
||||
- [Example Notebook](examples/resume_screening.ipynb) - Detailed walkthrough of resume parsing
|
||||
- [Discord Community](https://discord.com/invite/eN6D2HQ4aX) - Get help and share feedback
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# 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.
|
||||
@@ -2,11 +2,6 @@ from llama_cloud_services.parse import LlamaParse
|
||||
from llama_cloud_services.report import ReportClient, LlamaReport
|
||||
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent
|
||||
from llama_cloud_services.constants import EU_BASE_URL
|
||||
from llama_cloud_services.index import (
|
||||
LlamaCloudCompositeRetriever,
|
||||
LlamaCloudIndex,
|
||||
LlamaCloudRetriever,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LlamaParse",
|
||||
@@ -15,7 +10,4 @@ __all__ = [
|
||||
"LlamaExtract",
|
||||
"ExtractionAgent",
|
||||
"EU_BASE_URL",
|
||||
"LlamaCloudIndex",
|
||||
"LlamaCloudRetriever",
|
||||
"LlamaCloudCompositeRetriever",
|
||||
]
|
||||
-10
@@ -6,11 +6,6 @@ from .schema import (
|
||||
ExtractedT,
|
||||
AgentDataT,
|
||||
ComparisonOperator,
|
||||
parse_extracted_field_metadata,
|
||||
calculate_overall_confidence,
|
||||
InvalidExtractionData,
|
||||
ExtractedFieldMetadata,
|
||||
ExtractedFieldMetaDataDict,
|
||||
)
|
||||
from .client import AsyncAgentDataClient
|
||||
|
||||
@@ -23,9 +18,4 @@ __all__ = [
|
||||
"ExtractedT",
|
||||
"AgentDataT",
|
||||
"ComparisonOperator",
|
||||
"parse_extracted_field_metadata",
|
||||
"calculate_overall_confidence",
|
||||
"InvalidExtractionData",
|
||||
"ExtractedFieldMetadata",
|
||||
"ExtractedFieldMetaDataDict",
|
||||
]
|
||||
+6
-2
@@ -42,7 +42,7 @@ def agent_data_retry(func: WrappedFn) -> WrappedFn:
|
||||
)(func)
|
||||
|
||||
|
||||
def get_default_agent_id() -> str:
|
||||
def get_default_agent_id() -> Optional[str]:
|
||||
"""
|
||||
Retrieve the default agent ID from environment variables.
|
||||
|
||||
@@ -55,7 +55,7 @@ def get_default_agent_id() -> str:
|
||||
via environment variables instead of passing it explicitly
|
||||
to each client instance.
|
||||
"""
|
||||
return os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME") or "_public"
|
||||
return os.getenv("LLAMA_DEPLOY_DEPLOYMENT_NAME")
|
||||
|
||||
|
||||
class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
@@ -144,6 +144,10 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
"""
|
||||
|
||||
self.agent_url_id = agent_url_id or get_default_agent_id()
|
||||
if not self.agent_url_id:
|
||||
raise ValueError(
|
||||
"Agent ID is required, or set the LLAMA_DEPLOY_DEPLOYMENT_NAME environment variable"
|
||||
)
|
||||
|
||||
self.collection = collection
|
||||
if not client:
|
||||
+6
-211
@@ -37,11 +37,9 @@ Example Usage:
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import numbers
|
||||
from llama_cloud import ExtractRun
|
||||
from llama_cloud.types.agent_data import AgentData
|
||||
from llama_cloud.types.aggregate_group import AggregateGroup
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import (
|
||||
Generic,
|
||||
List,
|
||||
@@ -176,102 +174,6 @@ class TypedAgentDataItems(BaseModel, Generic[AgentDataT]):
|
||||
)
|
||||
|
||||
|
||||
class ExtractedFieldMetadata(BaseModel):
|
||||
"""
|
||||
Metadata for an extracted data field, such as confidence, and citation information.
|
||||
"""
|
||||
|
||||
reasoning: Optional[str] = Field(
|
||||
None,
|
||||
description="symbol for how the citation/confidence was derived: 'INFERRED FROM TEXT', 'VERBATIM EXTRACTION'",
|
||||
)
|
||||
confidence: Optional[float] = Field(
|
||||
None,
|
||||
description="The confidence score for the field, combined with parsing confidence if applicable",
|
||||
)
|
||||
extraction_confidence: Optional[float] = Field(
|
||||
None,
|
||||
description="The confidence score for the field based on the extracted text only",
|
||||
)
|
||||
page_number: Optional[int] = Field(
|
||||
None, description="The page number that the field occurred on"
|
||||
)
|
||||
matching_text: Optional[str] = Field(
|
||||
None,
|
||||
description="The original text this field's value was derived from",
|
||||
)
|
||||
|
||||
|
||||
ExtractedFieldMetaDataDict = Dict[
|
||||
str, Union[ExtractedFieldMetadata, Dict[str, Any], list[Any]]
|
||||
]
|
||||
|
||||
|
||||
def parse_extracted_field_metadata(
|
||||
field_metadata: dict[str, Any],
|
||||
) -> ExtractedFieldMetaDataDict:
|
||||
return {
|
||||
k: _parse_extracted_field_metadata_recursive(v)
|
||||
for k, v in field_metadata.items()
|
||||
if k not in _METADATA_FIELDS_SIBLING_TO_LEAF
|
||||
}
|
||||
|
||||
|
||||
_METADATA_FIELDS_SIBLING_TO_LEAF = {"reasoning"}
|
||||
|
||||
|
||||
def _parse_extracted_field_metadata_recursive(
|
||||
field_value: Any,
|
||||
additional_fields: dict[str, Any] = {},
|
||||
) -> Union[ExtractedFieldMetadata, Dict[str, Any], list[Any]]:
|
||||
"""
|
||||
Parse the extracted field metadata into a dictionary of field names to field metadata.
|
||||
"""
|
||||
|
||||
if isinstance(field_value, ExtractedFieldMetadata):
|
||||
# support running this multiple times
|
||||
return field_value
|
||||
elif isinstance(field_value, dict):
|
||||
# reasoning explicitly excluded, as it is included next to subfields, for example
|
||||
# "dimensions.width" is a leaf, but there will still potentially be a "dimensions.reasoning"
|
||||
indicator_fields = {"confidence", "extraction_confidence", "citation"}
|
||||
if len(indicator_fields.intersection(field_value.keys())) > 0:
|
||||
try:
|
||||
merged = {**field_value, **additional_fields}
|
||||
validated = ExtractedFieldMetadata.model_validate(merged)
|
||||
|
||||
# grab the citation from the array. This is just an array for backwards compatibility.
|
||||
if "citation" in field_value and len(field_value["citation"]) > 0:
|
||||
first_citation = field_value["citation"][0]
|
||||
if "page" in first_citation and isinstance(
|
||||
first_citation["page"], numbers.Number
|
||||
):
|
||||
validated.page_number = int(first_citation["page"]) # type: ignore
|
||||
if "matching_text" in first_citation and isinstance(
|
||||
first_citation["matching_text"], str
|
||||
):
|
||||
validated.matching_text = first_citation["matching_text"]
|
||||
return validated
|
||||
except ValidationError:
|
||||
pass
|
||||
additional_fields = {
|
||||
k: v
|
||||
for k, v in field_value.items()
|
||||
if k in _METADATA_FIELDS_SIBLING_TO_LEAF
|
||||
}
|
||||
return {
|
||||
k: _parse_extracted_field_metadata_recursive(v, additional_fields)
|
||||
for k, v in field_value.items()
|
||||
if k not in _METADATA_FIELDS_SIBLING_TO_LEAF
|
||||
}
|
||||
elif isinstance(field_value, list):
|
||||
return [_parse_extracted_field_metadata_recursive(item) for item in field_value]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid field value: {field_value}. Expected ExtractedFieldMetadata, dict, or list"
|
||||
)
|
||||
|
||||
|
||||
class ExtractedData(BaseModel, Generic[ExtractedT]):
|
||||
"""
|
||||
Wrapper for extracted data with workflow status tracking.
|
||||
@@ -318,13 +220,9 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
|
||||
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(
|
||||
confidence: Dict[str, Any] = 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 ",
|
||||
description="Confidence scores, if any, for each primitive field in the original_data data",
|
||||
)
|
||||
file_id: Optional[str] = Field(
|
||||
None, description="The ID of the file that was used to extract the data"
|
||||
@@ -345,7 +243,7 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
|
||||
cls,
|
||||
data: ExtractedT,
|
||||
status: StatusType = "pending_review",
|
||||
field_metadata: ExtractedFieldMetaDataDict = {},
|
||||
confidence: Optional[Dict[str, Any]] = None,
|
||||
file_id: Optional[str] = None,
|
||||
file_name: Optional[str] = None,
|
||||
file_hash: Optional[str] = None,
|
||||
@@ -357,128 +255,25 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
|
||||
Args:
|
||||
extracted_data: The extracted data payload
|
||||
status: Initial workflow status
|
||||
field_metadata: Optional confidence scores, citations, and other metadata for fields
|
||||
confidence: Optional confidence scores 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),
|
||||
confidence=confidence or {},
|
||||
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]):
|
||||
"""
|
||||
@@ -381,10 +381,6 @@ 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.",
|
||||
@@ -916,9 +912,6 @@ 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
|
||||
|
||||
@@ -1704,79 +1697,3 @@ 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
|
||||
@@ -301,57 +301,29 @@ class JobResult(BaseModel):
|
||||
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:
|
||||
def get_result(self) -> str:
|
||||
"""
|
||||
Get the raw parsed markdown from the job, distinct from the markdown documents.
|
||||
Get the parsed result from the job, distinct from the result documents. Top-level json object.
|
||||
This does not include page separators, e.g. if merge_tables_across_pages_in_markdown is True
|
||||
"""
|
||||
return asyncio_run(self.aget_markdown())
|
||||
return asyncio_run(self.aget_result())
|
||||
|
||||
async def aget_markdown(self) -> str:
|
||||
async def aget_result(self) -> str:
|
||||
"""
|
||||
Get the raw parsed markdown from the job, distinct from the markdown documents.
|
||||
Get the parsed result from the job, distinct from the result 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")
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
|
||||
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()
|
||||
client = AsyncLlamaCloud(
|
||||
base_url=self._base_url,
|
||||
token=self._api_key,
|
||||
httpx_client=self._client,
|
||||
)
|
||||
result = await client.parsing.get_job_result(
|
||||
job_id=self.job_id,
|
||||
)
|
||||
return result
|
||||
|
||||
async def _get_image_document_with_bytes(
|
||||
self, image: ImageItem, page: Page
|
||||
@@ -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](/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)
|
||||
- [Getting Started](/examples/parse/demo_basic.ipynb)
|
||||
- [Advanced RAG Example](/examples/parse/demo_advanced.ipynb)
|
||||
- [Raw API Usage](/examples/parse/demo_api.ipynb)
|
||||
|
||||
## Documentation
|
||||
|
||||
Generated
+3089
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "llama-parse"
|
||||
version = "0.6.49"
|
||||
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.49"
|
||||
|
||||
[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](./docs/examples-py/parse/demo_json_tour.ipynb).
|
||||
See more details about the result object in the [example notebook](./examples/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](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)
|
||||
- [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)
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
Generated
-1107
File diff suppressed because it is too large
Load Diff
Generated
+4626
File diff suppressed because it is too large
Load Diff
-21
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
@@ -1,87 +0,0 @@
|
||||
[](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).
|
||||
@@ -1,11 +0,0 @@
|
||||
from .base import LlamaCloudIndex
|
||||
from .retriever import LlamaCloudRetriever
|
||||
from .composite_retriever import (
|
||||
LlamaCloudCompositeRetriever,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LlamaCloudIndex",
|
||||
"LlamaCloudRetriever",
|
||||
"LlamaCloudCompositeRetriever",
|
||||
]
|
||||
@@ -1,422 +0,0 @@
|
||||
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
@@ -1,291 +0,0 @@
|
||||
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
|
||||
)
|
||||
@@ -1,217 +0,0 @@
|
||||
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
|
||||
@@ -1,29 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.0.0,<9",
|
||||
"pytest-asyncio",
|
||||
"ipykernel>=6.29.0,<7"
|
||||
]
|
||||
|
||||
[project]
|
||||
name = "llama-parse"
|
||||
version = "0.6.54"
|
||||
description = "Parse files into RAG-Optimized formats."
|
||||
authors = [{name = "Logan Markewich", email = "logan@llamaindex.ai"}]
|
||||
requires-python = ">=3.9,<4.0"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
dependencies = ["llama-cloud-services>=0.6.54"]
|
||||
|
||||
[project.scripts]
|
||||
llama-parse = "llama_parse.cli.main:parse"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["llama_parse"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
include = ["llama_parse"]
|
||||
Generated
-2844
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.0.0,<9",
|
||||
"pytest-asyncio",
|
||||
"ipykernel>=6.29.0,<7",
|
||||
"pre-commit==3.2.0",
|
||||
"autoevals>=0.0.114,<0.0.115",
|
||||
"deepdiff>=8.1.1,<9",
|
||||
"ipython>=8.12.3,<9",
|
||||
"jupyter>=1.1.1,<2",
|
||||
"mypy>=1.14.1,<2"
|
||||
]
|
||||
|
||||
[project]
|
||||
name = "llama-cloud-services"
|
||||
version = "0.6.54"
|
||||
description = "Tailored SDK clients for LlamaCloud services."
|
||||
authors = [{name = "Logan Markewich", email = "logan@runllama.ai"}]
|
||||
requires-python = ">=3.9,<4.0"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
dependencies = [
|
||||
"llama-index-core>=0.12.0",
|
||||
"llama-cloud==0.1.35",
|
||||
"pydantic>=2.8,!=2.10",
|
||||
"click>=8.1.7,<9",
|
||||
"python-dotenv>=1.0.1,<2",
|
||||
"eval-type-backport>=0.2.0,<0.3 ; python_version < '3.10'",
|
||||
"platformdirs>=4.3.7,<5",
|
||||
"tenacity>=8.5.0, <10.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
llama-parse = "llama_cloud_services.parse.cli.main:parse"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["llama_cloud_services"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
include = ["llama_cloud_services"]
|
||||
|
||||
[tool.mypy]
|
||||
files = ["llama_cloud_services"]
|
||||
python_version = "3.10"
|
||||
@@ -1,532 +0,0 @@
|
||||
import os
|
||||
import pytest
|
||||
import tempfile
|
||||
from typing import Generator, Tuple
|
||||
from uuid import uuid4
|
||||
|
||||
from llama_cloud import (
|
||||
AutoTransformConfig,
|
||||
PipelineCreate,
|
||||
PipelineFileCreate,
|
||||
ProjectCreate,
|
||||
CompositeRetrievalMode,
|
||||
LlamaParseParameters,
|
||||
ReRankConfig,
|
||||
)
|
||||
from llama_cloud.client import LlamaCloud
|
||||
from llama_index.core.bridge.pydantic import BaseModel
|
||||
from llama_index.core.constants import DEFAULT_BASE_URL
|
||||
from llama_index.core.schema import Document, ImageNode
|
||||
from llama_cloud_services.index import (
|
||||
LlamaCloudIndex,
|
||||
LlamaCloudCompositeRetriever,
|
||||
)
|
||||
|
||||
base_url = os.environ.get("LLAMA_CLOUD_BASE_URL", DEFAULT_BASE_URL)
|
||||
api_key = os.environ.get("LLAMA_CLOUD_API_KEY", None)
|
||||
organization_id = os.environ.get("LLAMA_CLOUD_ORGANIZATION_ID", None)
|
||||
project_name = os.environ.get("LLAMA_CLOUD_PROJECT_NAME", "framework_integration_test")
|
||||
|
||||
print("api-key", api_key, "base-url", base_url)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def remote_file() -> Tuple[str, str]:
|
||||
test_file_url = "https://www.google.com/robots.txt"
|
||||
test_file_name = "google_robots.txt"
|
||||
return test_file_url, test_file_name
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def index_name() -> Generator[str, None, None]:
|
||||
name = f"test_index_{uuid4()}"
|
||||
try:
|
||||
yield name
|
||||
finally:
|
||||
client = LlamaCloud(token=api_key, base_url=base_url)
|
||||
pipeline = client.pipelines.search_pipelines(project_name=name)
|
||||
if pipeline:
|
||||
client.pipelines.delete(pipeline_id=pipeline[0].id)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def local_file() -> str:
|
||||
return "tests/test_files/index/Simple PDF Slides.pdf"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def local_figures_file() -> str:
|
||||
return "tests/test_files/index/image_figure_slides.pdf"
|
||||
|
||||
|
||||
def _setup_index_with_file(
|
||||
client: LlamaCloud, index_name: str, remote_file: Tuple[str, str]
|
||||
) -> LlamaCloudIndex:
|
||||
# create project if it doesn't exist
|
||||
project_create = ProjectCreate(name=project_name)
|
||||
project = client.projects.upsert_project(
|
||||
organization_id=organization_id, request=project_create
|
||||
)
|
||||
|
||||
# create pipeline
|
||||
pipeline_create = PipelineCreate(
|
||||
name=index_name,
|
||||
transform_config=AutoTransformConfig(),
|
||||
)
|
||||
pipeline = client.pipelines.upsert_pipeline(
|
||||
project_id=project.id, request=pipeline_create
|
||||
)
|
||||
|
||||
# upload file to pipeline
|
||||
test_file_url, test_file_name = remote_file
|
||||
file = client.files.upload_file_from_url(
|
||||
project_id=project.id, url=test_file_url, name=test_file_name
|
||||
)
|
||||
|
||||
# add file to pipeline
|
||||
pipeline_file_create = PipelineFileCreate(file_id=file.id)
|
||||
client.pipelines.add_files_to_pipeline_api(
|
||||
pipeline_id=pipeline.id, request=[pipeline_file_create]
|
||||
)
|
||||
|
||||
return pipeline
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
def test_resolve_index_with_id(remote_file: Tuple[str, str], index_name: str):
|
||||
"""Test that we can instantiate an index with a given id."""
|
||||
client = LlamaCloud(token=api_key, base_url=base_url)
|
||||
pipeline = _setup_index_with_file(client, index_name, remote_file)
|
||||
|
||||
index = LlamaCloudIndex(
|
||||
pipeline_id=pipeline.id,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
)
|
||||
assert index is not None
|
||||
|
||||
index.wait_for_completion()
|
||||
retriever = index.as_retriever()
|
||||
|
||||
nodes = retriever.retrieve("Hello world.")
|
||||
assert len(nodes) > 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
def test_resolve_index_with_name(remote_file: Tuple[str, str], index_name: str):
|
||||
"""Test that we can instantiate an index with a given name."""
|
||||
client = LlamaCloud(token=api_key, base_url=base_url)
|
||||
pipeline = _setup_index_with_file(client, index_name, remote_file)
|
||||
|
||||
index = LlamaCloudIndex(
|
||||
name=pipeline.name,
|
||||
project_name=project_name,
|
||||
organization_id=organization_id,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
)
|
||||
assert index is not None
|
||||
|
||||
index.wait_for_completion()
|
||||
retriever = index.as_retriever()
|
||||
|
||||
nodes = retriever.retrieve("Hello world.")
|
||||
assert len(nodes) > 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
def test_upload_file(index_name: str):
|
||||
index = LlamaCloudIndex.create_index(
|
||||
name=index_name,
|
||||
project_name=project_name,
|
||||
organization_id=organization_id,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
# Create a temporary file to upload
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as temp_file:
|
||||
temp_file.write(b"Sample content for testing upload.")
|
||||
temp_file_path = temp_file.name
|
||||
|
||||
try:
|
||||
# Upload the file
|
||||
file_id = index.upload_file(temp_file_path, verbose=True)
|
||||
assert file_id is not None
|
||||
|
||||
# Verify the file is part of the index
|
||||
docs = index.ref_doc_info
|
||||
temp_file_name = os.path.basename(temp_file_path)
|
||||
assert any(
|
||||
temp_file_name == doc.metadata.get("file_name") for doc in docs.values()
|
||||
)
|
||||
|
||||
finally:
|
||||
# Clean up the temporary file
|
||||
os.remove(temp_file_path)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
def test_upload_file_from_url(remote_file: Tuple[str, str], index_name: str):
|
||||
index = LlamaCloudIndex.create_index(
|
||||
name=index_name,
|
||||
project_name=project_name,
|
||||
organization_id=organization_id,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
# Define a URL to a file for testing
|
||||
test_file_url, test_file_name = remote_file
|
||||
|
||||
# Upload the file from the URL
|
||||
file_id = index.upload_file_from_url(
|
||||
file_name=test_file_name, url=test_file_url, verbose=True
|
||||
)
|
||||
assert file_id is not None
|
||||
|
||||
# Verify the file is part of the index
|
||||
docs = index.ref_doc_info
|
||||
assert any(test_file_name == doc.metadata.get("file_name") for doc in docs.values())
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
def test_index_from_documents(index_name: str):
|
||||
documents = [
|
||||
Document(text="Hello world.", doc_id="1", metadata={"source": "test"}),
|
||||
]
|
||||
index = LlamaCloudIndex.from_documents(
|
||||
documents=documents,
|
||||
name=index_name,
|
||||
project_name=project_name,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
organization_id=organization_id,
|
||||
verbose=True,
|
||||
)
|
||||
docs = index.ref_doc_info
|
||||
assert len(docs) == 1
|
||||
assert docs["1"].metadata["source"] == "test"
|
||||
nodes = index.as_retriever().retrieve("Hello world.")
|
||||
assert len(nodes) > 0
|
||||
assert all(n.node.ref_doc_id == "1" for n in nodes)
|
||||
assert all(n.node.metadata["source"] == "test" for n in nodes)
|
||||
|
||||
index.insert(
|
||||
Document(text="Hello world.", doc_id="2", metadata={"source": "inserted"}),
|
||||
verbose=True,
|
||||
)
|
||||
docs = index.ref_doc_info
|
||||
assert len(docs) == 2
|
||||
assert docs["2"].metadata["source"] == "inserted"
|
||||
nodes = index.as_retriever().retrieve("Hello world.")
|
||||
assert len(nodes) > 0
|
||||
assert all(n.node.ref_doc_id in ["1", "2"] for n in nodes)
|
||||
assert any(n.node.ref_doc_id == "1" for n in nodes)
|
||||
assert any(n.node.ref_doc_id == "2" for n in nodes)
|
||||
|
||||
index.update_ref_doc(
|
||||
Document(text="Hello world.", doc_id="2", metadata={"source": "updated"}),
|
||||
verbose=True,
|
||||
)
|
||||
docs = index.ref_doc_info
|
||||
assert len(docs) == 2
|
||||
assert docs["2"].metadata["source"] == "updated"
|
||||
|
||||
index.refresh_ref_docs(
|
||||
[
|
||||
Document(text="Hello world.", doc_id="1", metadata={"source": "refreshed"}),
|
||||
Document(text="Hello world.", doc_id="3", metadata={"source": "refreshed"}),
|
||||
]
|
||||
)
|
||||
docs = index.ref_doc_info
|
||||
assert len(docs) == 3
|
||||
assert docs["3"].metadata["source"] == "refreshed"
|
||||
assert docs["1"].metadata["source"] == "refreshed"
|
||||
|
||||
index.delete_ref_doc("3", verbose=True)
|
||||
docs = index.ref_doc_info
|
||||
assert len(docs) == 2
|
||||
assert "3" not in docs
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_page_screenshot_retrieval(index_name: str, local_file: str):
|
||||
index = await LlamaCloudIndex.acreate_index(
|
||||
name=index_name,
|
||||
project_name=project_name,
|
||||
organization_id=organization_id,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
llama_parse_parameters=LlamaParseParameters(
|
||||
take_screenshot=True,
|
||||
),
|
||||
)
|
||||
|
||||
file_id = index.upload_file(local_file, wait_for_ingestion=True)
|
||||
|
||||
retriever = index.as_retriever(retrieve_page_screenshot_nodes=True)
|
||||
nodes = retriever.retrieve("1")
|
||||
assert len(nodes) > 0
|
||||
|
||||
image_nodes = [n.node for n in nodes if isinstance(n.node, ImageNode)]
|
||||
assert len(image_nodes) > 0
|
||||
assert all(n.metadata["file_id"] == file_id for n in image_nodes)
|
||||
assert all(n.metadata["page_index"] >= 0 for n in image_nodes)
|
||||
# ensure metadata is added from the image node
|
||||
# local_figures_file has the full absolute path, so just check the file name is in that absolute path
|
||||
assert all(local_file.endswith(n.metadata["file_name"]) for n in image_nodes)
|
||||
|
||||
nodes = await retriever.aretrieve("1")
|
||||
assert len(nodes) > 0
|
||||
|
||||
image_nodes = [n.node for n in nodes if isinstance(n.node, ImageNode)]
|
||||
assert len(image_nodes) > 0
|
||||
assert all(n.metadata["file_id"] == file_id for n in image_nodes)
|
||||
assert all(n.metadata["page_index"] >= 0 for n in image_nodes)
|
||||
# ensure metadata is added from the image node
|
||||
# local_figures_file has the full absolute path, so just check the file name is in that absolute path
|
||||
assert all(local_file.endswith(n.metadata["file_name"]) for n in image_nodes)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_page_figure_retrieval(index_name: str, local_figures_file: str):
|
||||
index = await LlamaCloudIndex.acreate_index(
|
||||
name=index_name,
|
||||
project_name=project_name,
|
||||
organization_id=organization_id,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
llama_parse_parameters=LlamaParseParameters(
|
||||
take_screenshot=True,
|
||||
extract_layout=True,
|
||||
),
|
||||
)
|
||||
|
||||
file_id = index.upload_file(local_figures_file, wait_for_ingestion=True)
|
||||
|
||||
retriever = index.as_retriever(retrieve_page_figure_nodes=True)
|
||||
nodes = retriever.retrieve("1")
|
||||
assert len(nodes) > 0
|
||||
|
||||
image_nodes = [n.node for n in nodes if isinstance(n.node, ImageNode)]
|
||||
assert len(image_nodes) > 0
|
||||
assert all(n.metadata["file_id"] == file_id for n in image_nodes)
|
||||
assert all(n.metadata["page_index"] >= 0 for n in image_nodes)
|
||||
# ensure metadata is added from the image node
|
||||
# local_figures_file has the full absolute path, so just check the file name is in that absolute path
|
||||
assert all(
|
||||
local_figures_file.endswith(n.metadata["file_name"]) for n in image_nodes
|
||||
)
|
||||
|
||||
nodes = await retriever.aretrieve("1")
|
||||
assert len(nodes) > 0
|
||||
|
||||
image_nodes = [n.node for n in nodes if isinstance(n.node, ImageNode)]
|
||||
assert len(image_nodes) > 0
|
||||
assert all(n.metadata["file_id"] == file_id for n in image_nodes)
|
||||
assert all(n.metadata["page_index"] >= 0 for n in image_nodes)
|
||||
# ensure metadata is added from the image node
|
||||
# local_figures_file has the full absolute path, so just check the file name is in that absolute path
|
||||
assert all(
|
||||
local_figures_file.endswith(n.metadata["file_name"]) for n in image_nodes
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skip(
|
||||
reason="Consistently failing with 'Server disconnected without sending a response'"
|
||||
)
|
||||
async def test_composite_retriever(index_name: str):
|
||||
"""Test the LlamaCloudCompositeRetriever with multiple indices."""
|
||||
# Create first index with documents
|
||||
documents1 = [
|
||||
Document(
|
||||
text="Hello world from index 1.", doc_id="1", metadata={"source": "index1"}
|
||||
),
|
||||
]
|
||||
index1 = LlamaCloudIndex.from_documents(
|
||||
documents=documents1,
|
||||
name=index_name,
|
||||
project_name=project_name,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
organization_id=organization_id,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Create second index with documents
|
||||
documents2 = [
|
||||
Document(
|
||||
text="Hello world from index 2.", doc_id="2", metadata={"source": "index2"}
|
||||
),
|
||||
]
|
||||
index2 = LlamaCloudIndex.from_documents(
|
||||
documents=documents2,
|
||||
name=f"test pipeline 2 {uuid4()}",
|
||||
project_name=project_name,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
organization_id=organization_id,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Create a composite retriever
|
||||
retriever = LlamaCloudCompositeRetriever(
|
||||
name="composite_retriever_test",
|
||||
project_name=project_name,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
create_if_not_exists=True,
|
||||
mode=CompositeRetrievalMode.FULL,
|
||||
rerank_top_n=5,
|
||||
rerank_config=ReRankConfig(
|
||||
top_n=5,
|
||||
),
|
||||
)
|
||||
|
||||
# Attach indices to the composite retriever
|
||||
retriever.add_index(index1, description="Information from index 1.")
|
||||
retriever.add_index(index2, description="Information from index 2.")
|
||||
|
||||
# Retrieve nodes using the composite retriever
|
||||
nodes = retriever.retrieve("Hello world.")
|
||||
|
||||
# Assertions to verify the retrieval
|
||||
assert len(nodes) >= 2
|
||||
|
||||
# Retrieve nodes using the composite retriever
|
||||
nodes = await retriever.aretrieve("Hello world.")
|
||||
|
||||
# Assertions to verify the retrieval
|
||||
assert len(nodes) >= 2
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_index_from_documents(index_name: str):
|
||||
documents = [
|
||||
Document(text="Hello world.", doc_id="1", metadata={"source": "test"}),
|
||||
]
|
||||
index = await LlamaCloudIndex.acreate_index(
|
||||
name=index_name,
|
||||
project_name=project_name,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
organization_id=organization_id,
|
||||
verbose=True,
|
||||
)
|
||||
await index.ainsert(documents[0])
|
||||
await index.await_for_completion()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_upload_file_from_url(
|
||||
remote_file: Tuple[str, str], index_name: str
|
||||
):
|
||||
index = await LlamaCloudIndex.acreate_index(
|
||||
name=index_name,
|
||||
project_name=project_name,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
test_file_url, test_file_name = remote_file
|
||||
file_id = await index.aupload_file_from_url(
|
||||
file_name=test_file_name, url=test_file_url, verbose=True
|
||||
)
|
||||
assert file_id is not None
|
||||
|
||||
await index.await_for_completion()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_index_from_file(index_name: str, local_file: str):
|
||||
index = await LlamaCloudIndex.acreate_index(
|
||||
name=index_name,
|
||||
project_name=project_name,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
file_id = await index.aupload_file(file_path=local_file, verbose=True)
|
||||
assert file_id is not None
|
||||
|
||||
await index.await_for_completion()
|
||||
|
||||
|
||||
class DummySchema(BaseModel):
|
||||
source: str
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_filters_inference_schema(index_name: str):
|
||||
"""Test the use of search_filters_inference_schema in retrieval."""
|
||||
# Define a dummy schema
|
||||
schema = DummySchema(source="test")
|
||||
|
||||
# Create documents
|
||||
documents = [
|
||||
Document(text="Hello world.", doc_id="1", metadata={"source": "test"}),
|
||||
]
|
||||
|
||||
# Create an index with documents
|
||||
index = LlamaCloudIndex.from_documents(
|
||||
documents=documents,
|
||||
name=index_name,
|
||||
project_name=project_name,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
organization_id=organization_id,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
# Use the retriever with the schema
|
||||
retriever = index.as_retriever(search_filters_inference_schema=schema)
|
||||
nodes = retriever.retrieve(
|
||||
'Search for documents where the metadata has source="test"'
|
||||
)
|
||||
|
||||
# Verify that nodes are retrieved
|
||||
assert len(nodes) > 0
|
||||
assert all(n.node.ref_doc_id == "1" for n in nodes)
|
||||
assert all(n.node.metadata["source"] == "test" for n in nodes)
|
||||
|
||||
nodes = await retriever.aretrieve(
|
||||
'Search for documents where the metadata has source="test"'
|
||||
)
|
||||
|
||||
# Verify that nodes are retrieved
|
||||
assert len(nodes) > 0
|
||||
assert all(n.node.ref_doc_id == "1" for n in nodes)
|
||||
assert all(n.node.metadata["source"] == "test" for n in nodes)
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,525 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
from llama_cloud import ExtractRun, File
|
||||
from llama_cloud.types.agent_data import AgentData
|
||||
from llama_cloud.types.aggregate_group import AggregateGroup
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from llama_cloud_services.beta.agent_data.schema import (
|
||||
ExtractedData,
|
||||
ExtractedFieldMetadata,
|
||||
InvalidExtractionData,
|
||||
TypedAgentData,
|
||||
TypedAggregateGroup,
|
||||
calculate_overall_confidence,
|
||||
parse_extracted_field_metadata,
|
||||
)
|
||||
|
||||
|
||||
# Test data models
|
||||
class Person(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
email: str
|
||||
|
||||
|
||||
class Company(BaseModel):
|
||||
name: str
|
||||
industry: str
|
||||
employees: int
|
||||
|
||||
|
||||
def test_typed_agent_data_from_raw():
|
||||
"""Test TypedAgentData.from_raw class method."""
|
||||
raw_data = AgentData(
|
||||
id="456",
|
||||
agent_slug="extraction-agent",
|
||||
collection="employees",
|
||||
data={"name": "Jane Smith", "age": 25, "email": "jane@company.com"},
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
typed_data = TypedAgentData.from_raw(raw_data, Person)
|
||||
|
||||
assert typed_data.id == "456"
|
||||
assert typed_data.agent_url_id == "extraction-agent"
|
||||
assert typed_data.collection == "employees"
|
||||
assert typed_data.data.name == "Jane Smith"
|
||||
assert typed_data.data.age == 25
|
||||
assert typed_data.data.email == "jane@company.com"
|
||||
|
||||
|
||||
def test_typed_agent_data_from_raw_validation_error():
|
||||
"""Test TypedAgentData.from_raw with invalid data."""
|
||||
raw_data = AgentData(
|
||||
id="789",
|
||||
agent_slug="test-agent",
|
||||
collection="people",
|
||||
data={"name": "Invalid Person", "age": "not_a_number"}, # Invalid age
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
TypedAgentData.from_raw(raw_data, Person)
|
||||
|
||||
|
||||
def test_extracted_data_create_method():
|
||||
"""Test ExtractedData.create class method."""
|
||||
person = Person(name="Created Person", age=35, email="created@example.com")
|
||||
|
||||
# Test with defaults
|
||||
extracted = ExtractedData.create(person)
|
||||
assert extracted.original_data == person
|
||||
assert extracted.data == person
|
||||
assert extracted.status == "pending_review"
|
||||
assert extracted.field_metadata == {}
|
||||
assert extracted.overall_confidence is None
|
||||
|
||||
# Test with custom values using ExtractedFieldMetadata
|
||||
field_metadata = {
|
||||
"name": ExtractedFieldMetadata(confidence=0.99, page_number=1),
|
||||
"age": ExtractedFieldMetadata(confidence=0.85, page_number=1),
|
||||
}
|
||||
extracted_custom = ExtractedData.create(
|
||||
person, status="accepted", field_metadata=field_metadata
|
||||
)
|
||||
assert extracted_custom.status == "accepted"
|
||||
assert extracted_custom.field_metadata["name"].confidence == 0.99
|
||||
assert extracted_custom.field_metadata["age"].confidence == 0.85
|
||||
assert extracted_custom.overall_confidence == pytest.approx((0.99 + 0.85) / 2)
|
||||
|
||||
|
||||
def test_extracted_data_with_dict():
|
||||
"""Test ExtractedData with dict data instead of Pydantic model."""
|
||||
data_dict = {"name": "Dict Person", "age": 45, "email": "dict@example.com"}
|
||||
|
||||
extracted = ExtractedData[Dict[str, Any]](
|
||||
original_data=data_dict, data=data_dict, status="accepted", confidence={}
|
||||
)
|
||||
|
||||
assert extracted.original_data["name"] == "Dict Person"
|
||||
assert extracted.data["age"] == 45
|
||||
|
||||
|
||||
def test_typed_aggregate_group_from_raw():
|
||||
"""Test TypedAggregateGroup.from_raw class method."""
|
||||
raw_group = AggregateGroup(
|
||||
group_key={"industry": "Technology"},
|
||||
count=25,
|
||||
first_item={"name": "Tech Corp", "industry": "Technology", "employees": 500},
|
||||
)
|
||||
|
||||
typed_group = TypedAggregateGroup.from_raw(raw_group, Company)
|
||||
|
||||
assert typed_group.group_key["industry"] == "Technology"
|
||||
assert typed_group.count == 25
|
||||
assert typed_group.first_item.name == "Tech Corp"
|
||||
assert typed_group.first_item.employees == 500
|
||||
|
||||
|
||||
def test_calculate_overall_confidence_simple_flat():
|
||||
"""Test calculate_overall_confidence with simple flat dictionary of ExtractedFieldMetadata."""
|
||||
field_metadata = {
|
||||
"name": ExtractedFieldMetadata(confidence=0.9),
|
||||
"age": ExtractedFieldMetadata(confidence=0.8),
|
||||
"email": ExtractedFieldMetadata(confidence=0.95),
|
||||
}
|
||||
result = calculate_overall_confidence(field_metadata)
|
||||
expected = (0.9 + 0.8 + 0.95) / 3
|
||||
assert result == pytest.approx(expected, rel=1e-9)
|
||||
|
||||
|
||||
def test_calculate_overall_confidence_nested():
|
||||
"""Test calculate_overall_confidence with nested dictionary structure."""
|
||||
field_metadata = {
|
||||
"person": {
|
||||
"name": ExtractedFieldMetadata(confidence=0.9),
|
||||
"age": ExtractedFieldMetadata(confidence=0.8),
|
||||
},
|
||||
"contact": {
|
||||
"email": ExtractedFieldMetadata(confidence=0.95),
|
||||
"phone": ExtractedFieldMetadata(confidence=0.85),
|
||||
},
|
||||
"score": ExtractedFieldMetadata(confidence=0.7),
|
||||
}
|
||||
result = calculate_overall_confidence(field_metadata)
|
||||
# Should average all leaf values: (0.9 + 0.8 + 0.95 + 0.85 + 0.7) / 5
|
||||
expected = (0.9 + 0.8 + 0.95 + 0.85 + 0.7) / 5
|
||||
assert result == pytest.approx(expected, rel=1e-9)
|
||||
|
||||
|
||||
def test_calculate_overall_confidence_with_lists():
|
||||
"""Test calculate_overall_confidence with lists of ExtractedFieldMetadata and nested structures."""
|
||||
field_metadata = {
|
||||
"scores": [
|
||||
ExtractedFieldMetadata(confidence=0.9),
|
||||
ExtractedFieldMetadata(confidence=0.8),
|
||||
ExtractedFieldMetadata(confidence=0.95),
|
||||
],
|
||||
"nested_data": [
|
||||
{
|
||||
"field1": ExtractedFieldMetadata(confidence=0.7),
|
||||
"field2": ExtractedFieldMetadata(confidence=0.6),
|
||||
},
|
||||
{
|
||||
"field1": ExtractedFieldMetadata(confidence=0.8),
|
||||
"field2": ExtractedFieldMetadata(confidence=0.9),
|
||||
},
|
||||
],
|
||||
"single_value": ExtractedFieldMetadata(confidence=0.85),
|
||||
}
|
||||
result = calculate_overall_confidence(field_metadata)
|
||||
# Should count: [0.9, 0.8, 0.95] + [0.7, 0.6, 0.8, 0.9] + [0.85] = 8 values
|
||||
expected = (0.9 + 0.8 + 0.95 + 0.7 + 0.6 + 0.8 + 0.9 + 0.85) / 8
|
||||
assert result == pytest.approx(expected, rel=1e-9)
|
||||
|
||||
|
||||
def test_calculate_overall_confidence_invalid_types():
|
||||
"""Test calculate_overall_confidence with invalid/mixed types alongside valid ExtractedFieldMetadata."""
|
||||
field_metadata = {
|
||||
"valid_metadata": ExtractedFieldMetadata(confidence=0.8),
|
||||
"valid_metadata_no_confidence": ExtractedFieldMetadata(), # No confidence
|
||||
"valid_list": [
|
||||
ExtractedFieldMetadata(confidence=0.5),
|
||||
ExtractedFieldMetadata(confidence=0.6),
|
||||
],
|
||||
"invalid_string": "not_a_number",
|
||||
"invalid_list_mixed": [
|
||||
ExtractedFieldMetadata(confidence=0.7),
|
||||
"invalid",
|
||||
ExtractedFieldMetadata(confidence=0.8),
|
||||
],
|
||||
"invalid_none": None,
|
||||
"random_dict": {"a": 1, "b": 2},
|
||||
}
|
||||
result = calculate_overall_confidence(field_metadata)
|
||||
expected = (0.8 + 0.5 + 0.6 + 0.7 + 0.8) / 5
|
||||
assert result == pytest.approx(expected, rel=1e-9)
|
||||
|
||||
|
||||
def test_calculate_overall_confidence_empty():
|
||||
"""Test calculate_overall_confidence with empty inputs."""
|
||||
# Empty dict
|
||||
assert calculate_overall_confidence({}) is None
|
||||
|
||||
# Empty list
|
||||
assert calculate_overall_confidence([]) is None
|
||||
|
||||
# Dict with only invalid values
|
||||
field_metadata_invalid = {"invalid": "not_a_number", "also_invalid": None}
|
||||
assert calculate_overall_confidence(field_metadata_invalid) is None
|
||||
|
||||
# List with only invalid values
|
||||
field_metadata_invalid_list = ["invalid", None, {}]
|
||||
assert calculate_overall_confidence(field_metadata_invalid_list) is None
|
||||
|
||||
# Dict with ExtractedFieldMetadata but no confidence values
|
||||
field_metadata_no_confidence = {
|
||||
"field1": ExtractedFieldMetadata(),
|
||||
"field2": ExtractedFieldMetadata(),
|
||||
}
|
||||
assert calculate_overall_confidence(field_metadata_no_confidence) is None
|
||||
|
||||
|
||||
def test_parse_extracted_field_metadata():
|
||||
"""Test parse_extracted_field_metadata with legacy citation format."""
|
||||
raw_metadata = {
|
||||
"name": {
|
||||
"confidence": 0.95,
|
||||
"citation": [{"page": 1, "matching_text": "John Smith"}],
|
||||
},
|
||||
"age": {
|
||||
"confidence": 0.87,
|
||||
"citation": [
|
||||
{
|
||||
"page": 2.0, # Float page number
|
||||
"matching_text": "25 years old",
|
||||
}
|
||||
],
|
||||
},
|
||||
"email": {
|
||||
"confidence": 0.92,
|
||||
"citation": [], # Empty citations
|
||||
},
|
||||
}
|
||||
|
||||
result = parse_extracted_field_metadata(raw_metadata)
|
||||
result2 = parse_extracted_field_metadata(result)
|
||||
assert result2 == result
|
||||
|
||||
# name should have parsed citation data
|
||||
assert isinstance(result["name"], ExtractedFieldMetadata)
|
||||
assert result["name"].confidence == 0.95
|
||||
assert result["name"].page_number == 1
|
||||
assert result["name"].matching_text == "John Smith"
|
||||
|
||||
# age should handle float page number
|
||||
assert isinstance(result["age"], ExtractedFieldMetadata)
|
||||
assert result["age"].confidence == 0.87
|
||||
assert result["age"].page_number == 2 # Should be converted to int
|
||||
assert result["age"].matching_text == "25 years old"
|
||||
|
||||
# email should handle empty citations
|
||||
assert isinstance(result["email"], ExtractedFieldMetadata)
|
||||
assert result["email"].confidence == 0.92
|
||||
|
||||
|
||||
def test_parse_extracted_field_metadata_complex():
|
||||
"""Test parse_extracted_field_metadata with new citation format and reasoning field."""
|
||||
raw_metadata = {
|
||||
"title": {
|
||||
"reasoning": "Combined key parametrics and construction from the datasheet for a structured title.",
|
||||
"citation": [
|
||||
{
|
||||
"page": 1,
|
||||
"matching_text": "PHE844/F844, Film, Metallized Polypropylene, Safety, 0.47 uF",
|
||||
}
|
||||
],
|
||||
"extraction_confidence": 0.9470628580889779,
|
||||
"confidence": 0.9470628580889779,
|
||||
},
|
||||
"manufacturer": {
|
||||
"reasoning": "VERBATIM EXTRACTION",
|
||||
"citation": [{"page": 1, "matching_text": "YAGEO KEMET"}],
|
||||
"extraction_confidence": 0.9997446550976602,
|
||||
"confidence": 0.9997446550976602,
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"reasoning": "VERBATIM EXTRACTION",
|
||||
"citation": [
|
||||
{"page": 1, "matching_text": "Features</td><td>EMI Safety"}
|
||||
],
|
||||
"extraction_confidence": 0.9999308195540074,
|
||||
"confidence": 0.9999308195540074,
|
||||
},
|
||||
{
|
||||
"reasoning": "VERBATIM EXTRACTION",
|
||||
"citation": [
|
||||
{"page": 1, "matching_text": "THB Performance</td><td>Yes"}
|
||||
],
|
||||
"extraction_confidence": 0.8642493886452225,
|
||||
"confidence": 0.8642493886452225,
|
||||
},
|
||||
],
|
||||
"dimensions": {
|
||||
"length": {
|
||||
"citation": [{"page": 1, "matching_text": "L</td><td>41mm MAX"}],
|
||||
"extraction_confidence": 0.8986941382802304,
|
||||
"confidence": 0.8986941382802304,
|
||||
},
|
||||
"width": {
|
||||
"citation": [{"page": 1, "matching_text": "T</td><td>13mm MAX"}],
|
||||
"extraction_confidence": 0.9999377974447091,
|
||||
"confidence": 0.9999377974447091,
|
||||
},
|
||||
"reasoning": "VERBATIM EXTRACTION",
|
||||
},
|
||||
}
|
||||
|
||||
result = parse_extracted_field_metadata(raw_metadata)
|
||||
assert result == {
|
||||
"title": ExtractedFieldMetadata(
|
||||
reasoning="Combined key parametrics and construction from the datasheet for a structured title.",
|
||||
confidence=0.9470628580889779,
|
||||
extraction_confidence=0.9470628580889779,
|
||||
page_number=1,
|
||||
matching_text="PHE844/F844, Film, Metallized Polypropylene, Safety, 0.47 uF",
|
||||
),
|
||||
"manufacturer": ExtractedFieldMetadata(
|
||||
reasoning="VERBATIM EXTRACTION",
|
||||
confidence=0.9997446550976602,
|
||||
extraction_confidence=0.9997446550976602,
|
||||
page_number=1,
|
||||
matching_text="YAGEO KEMET",
|
||||
),
|
||||
"features": [
|
||||
ExtractedFieldMetadata(
|
||||
reasoning="VERBATIM EXTRACTION",
|
||||
confidence=0.9999308195540074,
|
||||
extraction_confidence=0.9999308195540074,
|
||||
page_number=1,
|
||||
matching_text="Features</td><td>EMI Safety",
|
||||
),
|
||||
ExtractedFieldMetadata(
|
||||
reasoning="VERBATIM EXTRACTION",
|
||||
confidence=0.8642493886452225,
|
||||
extraction_confidence=0.8642493886452225,
|
||||
page_number=1,
|
||||
matching_text="THB Performance</td><td>Yes",
|
||||
),
|
||||
],
|
||||
"dimensions": {
|
||||
"length": ExtractedFieldMetadata(
|
||||
reasoning="VERBATIM EXTRACTION",
|
||||
confidence=0.8986941382802304,
|
||||
extraction_confidence=0.8986941382802304,
|
||||
page_number=1,
|
||||
matching_text="L</td><td>41mm MAX",
|
||||
),
|
||||
"width": ExtractedFieldMetadata(
|
||||
reasoning="VERBATIM EXTRACTION",
|
||||
confidence=0.9999377974447091,
|
||||
extraction_confidence=0.9999377974447091,
|
||||
page_number=1,
|
||||
matching_text="T</td><td>13mm MAX",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_file(
|
||||
id: str = "file-456",
|
||||
name: str = "resume.pdf",
|
||||
external_file_id: str = "external-file-id",
|
||||
project_id: str = "project-123",
|
||||
) -> File:
|
||||
return File.parse_obj(
|
||||
{
|
||||
"id": id,
|
||||
"name": name,
|
||||
"external_file_id": external_file_id,
|
||||
"project_id": project_id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def create_extract_run(
|
||||
id: str = "extract-123",
|
||||
data: Dict[str, Any] = {"name": "John Doe", "age": 30, "email": "john@example.com"},
|
||||
extraction_metadata: Dict[str, Any] = {
|
||||
"name": {
|
||||
"confidence": 0.95,
|
||||
"citation": [{"page": 1, "matching_text": "John Doe"}],
|
||||
},
|
||||
"age": {"confidence": 0.87},
|
||||
"email": {
|
||||
"confidence": 0.92,
|
||||
"citation": [{"page": 1, "matching_text": "john@example.com"}],
|
||||
},
|
||||
},
|
||||
data_schema: Dict[str, Any] = {},
|
||||
file: File = create_file(),
|
||||
) -> ExtractRun:
|
||||
return ExtractRun.parse_obj(
|
||||
{
|
||||
"id": id,
|
||||
"data": data,
|
||||
"extraction_metadata": {
|
||||
"field_metadata": extraction_metadata,
|
||||
},
|
||||
"data_schema": data_schema,
|
||||
"file": file,
|
||||
"extraction_agent_id": "extraction-agent-123",
|
||||
"config": {},
|
||||
"status": "SUCCESS",
|
||||
"from_ui": False,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_extracted_data_from_extraction_result_success():
|
||||
"""Test ExtractedData.from_extraction_result with valid data."""
|
||||
# Create mock ExtractRun with valid data
|
||||
extract_run = create_extract_run(
|
||||
file=create_file(id="file-456", name="resume.pdf"),
|
||||
)
|
||||
|
||||
# Create with file object
|
||||
extracted: ExtractedData[Person] = ExtractedData.from_extraction_result(
|
||||
extract_run,
|
||||
Person,
|
||||
file_hash="abc123",
|
||||
status="accepted",
|
||||
)
|
||||
|
||||
# Verify the extracted data
|
||||
assert isinstance(extracted.data, Person)
|
||||
assert extracted.data.name == "John Doe"
|
||||
assert extracted.data.age == 30
|
||||
assert extracted.data.email == "john@example.com"
|
||||
assert extracted.status == "accepted"
|
||||
assert extracted.file_id == "file-456"
|
||||
assert extracted.file_name == "resume.pdf"
|
||||
assert extracted.file_hash == "abc123"
|
||||
|
||||
# Verify field metadata was parsed
|
||||
assert isinstance(extracted.field_metadata["name"], ExtractedFieldMetadata)
|
||||
assert extracted.field_metadata["name"].confidence == 0.95
|
||||
assert extracted.field_metadata["name"].page_number == 1
|
||||
assert extracted.field_metadata["name"].matching_text == "John Doe"
|
||||
|
||||
# Verify overall confidence was calculated
|
||||
expected_confidence = (0.95 + 0.87 + 0.92) / 3
|
||||
assert extracted.overall_confidence == pytest.approx(expected_confidence)
|
||||
|
||||
|
||||
def test_extracted_data_from_extraction_result_with_file_params():
|
||||
"""Test ExtractedData.from_extraction_result with explicit file parameters."""
|
||||
extract_run = create_extract_run(
|
||||
file=create_file(id="original-file", name="original.pdf"),
|
||||
)
|
||||
|
||||
# Override file parameters
|
||||
extracted: ExtractedData[Person] = ExtractedData.from_extraction_result(
|
||||
extract_run,
|
||||
Person,
|
||||
file_id="custom-file-id", # Should override file.id
|
||||
file_name="custom-name.pdf", # Should override file.name
|
||||
file_hash="custom-hash",
|
||||
metadata={"source": "api_test"},
|
||||
)
|
||||
|
||||
assert extracted.file_id == "custom-file-id" # Overridden
|
||||
assert extracted.file_name == "custom-name.pdf" # Overridden
|
||||
assert extracted.file_hash == "custom-hash"
|
||||
assert extracted.metadata["source"] == "api_test"
|
||||
|
||||
|
||||
def test_extracted_data_from_extraction_result_invalid_data():
|
||||
"""Test ExtractedData.from_extraction_result with invalid data raises custom exception."""
|
||||
# Create ExtractRun with data that doesn't match Person schema
|
||||
extract_run = create_extract_run(
|
||||
data={
|
||||
"name": "Valid Name",
|
||||
"age": "not_a_number",
|
||||
"missing_email": True,
|
||||
}, # Invalid age, missing email
|
||||
extraction_metadata={
|
||||
"name": {"confidence": 0.9},
|
||||
},
|
||||
data_schema={},
|
||||
file=create_file(id="error-file", name="bad_data.pdf"),
|
||||
)
|
||||
|
||||
# Should raise InvalidExtractionData with ExtractedData containing error info
|
||||
with pytest.raises(InvalidExtractionData) as exc_info:
|
||||
ExtractedData.from_extraction_result(
|
||||
extract_run, Person, metadata={"test": "metadata"}
|
||||
)
|
||||
|
||||
# Verify the exception contains the invalid ExtractedData
|
||||
invalid_data = exc_info.value.invalid_item
|
||||
assert isinstance(invalid_data, ExtractedData)
|
||||
assert invalid_data.status == "error"
|
||||
assert invalid_data.data == {
|
||||
"name": "Valid Name",
|
||||
"age": "not_a_number",
|
||||
"missing_email": True,
|
||||
}
|
||||
assert invalid_data.file_id == "error-file"
|
||||
assert invalid_data.file_name == "bad_data.pdf"
|
||||
|
||||
# Check error metadata was added
|
||||
assert "extraction_error" in invalid_data.metadata
|
||||
assert "test" in invalid_data.metadata # Original metadata preserved
|
||||
assert "2 validation errors" in invalid_data.metadata["extraction_error"]
|
||||
|
||||
# Verify field metadata was still parsed (before validation failed)
|
||||
assert isinstance(invalid_data.field_metadata["name"], ExtractedFieldMetadata)
|
||||
assert invalid_data.field_metadata["name"].confidence == 0.9
|
||||
assert invalid_data.overall_confidence == 0.9
|
||||
@@ -1,16 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from llama_index.core.indices.managed.base import BaseManagedIndex
|
||||
from llama_cloud_services.index import (
|
||||
LlamaCloudIndex,
|
||||
)
|
||||
|
||||
|
||||
def test_class():
|
||||
names_of_base_classes = [b.__name__ for b in LlamaCloudIndex.__mro__]
|
||||
assert BaseManagedIndex.__name__ in names_of_base_classes
|
||||
|
||||
|
||||
def test_conflicting_index_identifiers():
|
||||
with pytest.raises(ValueError):
|
||||
LlamaCloudIndex(name="test", pipeline_id="test", index_id="test")
|
||||
Generated
-4096
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.mypy]
|
||||
files = ["llama_cloud_services"]
|
||||
python_version = "3.10"
|
||||
|
||||
[tool.poetry]
|
||||
name = "llama-cloud-services"
|
||||
version = "0.6.49"
|
||||
description = "Tailored SDK clients for LlamaCloud services."
|
||||
authors = ["Logan Markewich <logan@runllama.ai>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
packages = [{include = "llama_cloud_services"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<4.0"
|
||||
llama-index-core = ">=0.12.0"
|
||||
llama-cloud = "==0.1.34"
|
||||
pydantic = ">=2.8,!=2.10"
|
||||
click = "^8.1.7"
|
||||
python-dotenv = "^1.0.1"
|
||||
eval-type-backport = {python = "<3.10", version = "^0.2.0"}
|
||||
platformdirs = "^4.3.7"
|
||||
tenacity = ">=8.5.0, <10.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.0.0"
|
||||
pytest-asyncio = "*"
|
||||
ipykernel = "^6.29.0"
|
||||
pre-commit = "3.2.0"
|
||||
autoevals = "^0.0.114"
|
||||
deepdiff = "^8.1.1"
|
||||
ipython = "^8.12.3"
|
||||
jupyter = "^1.1.1"
|
||||
mypy = "^1.14.1"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
llama-parse = "llama_cloud_services.parse.cli.main:parse"
|
||||
@@ -1,180 +0,0 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["click", "tomlkit"]
|
||||
# ///
|
||||
|
||||
import click
|
||||
import subprocess
|
||||
import sys
|
||||
import tomlkit
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_current_versions() -> tuple[str, str, str]:
|
||||
"""Get current versions from both pyproject.toml files."""
|
||||
# Read main pyproject.toml
|
||||
main_content = Path("py/pyproject.toml").read_text()
|
||||
main_doc = tomlkit.parse(main_content)
|
||||
main_version = main_doc["project"]["version"]
|
||||
|
||||
# Read llama_parse/pyproject.toml
|
||||
llama_parse_content = Path("py/llama_parse/pyproject.toml").read_text()
|
||||
llama_parse_doc = tomlkit.parse(llama_parse_content)
|
||||
llama_parse_version = llama_parse_doc["project"]["version"]
|
||||
# Find llama-cloud-services dependency in the dependencies list
|
||||
dependency_version = None
|
||||
for dep in llama_parse_doc["project"]["dependencies"]:
|
||||
if isinstance(dep, str) and dep.startswith("llama-cloud-services"):
|
||||
dependency_version = (
|
||||
dep.split("==")[1]
|
||||
if "==" in dep
|
||||
else dep.split(">=")[1]
|
||||
if ">=" in dep
|
||||
else None
|
||||
)
|
||||
break
|
||||
|
||||
return str(main_version), str(llama_parse_version), str(dependency_version)
|
||||
|
||||
|
||||
def validate_versions(
|
||||
main_version: str, llama_parse_version: str, dependency_version: str
|
||||
) -> list[str]:
|
||||
"""Validate that versions are consistent and return warnings."""
|
||||
warnings = []
|
||||
|
||||
if main_version != llama_parse_version:
|
||||
warnings.append(
|
||||
f"Version mismatch: main={main_version}, llama_parse={llama_parse_version}"
|
||||
)
|
||||
|
||||
# Extract version from dependency string (e.g., ">=0.6.51" -> "0.6.51")
|
||||
if dependency_version and dependency_version.startswith(">="):
|
||||
dep_ver = dependency_version[2:]
|
||||
if dep_ver != main_version:
|
||||
warnings.append(
|
||||
f"Dependency version mismatch: dependency={dep_ver}, main={main_version}"
|
||||
)
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
def set_version(version: str) -> None:
|
||||
"""Set version across all pyproject.toml files using tomlkit to preserve formatting."""
|
||||
# Update main pyproject.toml
|
||||
main_content = Path("py/pyproject.toml").read_text()
|
||||
main_doc = tomlkit.parse(main_content)
|
||||
main_doc["project"]["version"] = version
|
||||
Path("py/pyproject.toml").write_text(tomlkit.dumps(main_doc))
|
||||
|
||||
# Update llama_parse/pyproject.toml
|
||||
llama_parse_content = Path("py/llama_parse/pyproject.toml").read_text()
|
||||
llama_parse_doc = tomlkit.parse(llama_parse_content)
|
||||
llama_parse_doc["project"]["version"] = version
|
||||
for dep_index, dep in enumerate(llama_parse_doc["project"]["dependencies"]):
|
||||
if isinstance(dep, str) and dep.startswith("llama-cloud-services"):
|
||||
llama_parse_doc["project"]["dependencies"][
|
||||
dep_index
|
||||
] = f"llama-cloud-services>={version}"
|
||||
break
|
||||
Path("py/llama_parse/pyproject.toml").write_text(tomlkit.dumps(llama_parse_doc))
|
||||
|
||||
click.echo(f"Updated all versions to {version}")
|
||||
|
||||
|
||||
def get_current_branch() -> str:
|
||||
"""Get the current git branch."""
|
||||
result = subprocess.run(
|
||||
["git", "branch", "--show-current"], capture_output=True, text=True, check=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def create_if_not_exists(version: str) -> None:
|
||||
"""Create a git tag and push it."""
|
||||
current_branch = get_current_branch()
|
||||
if current_branch != "main":
|
||||
click.echo(
|
||||
f"Error: Not on main branch (currently on {current_branch})", err=True
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
tag_name = f"v{version}"
|
||||
if not tag_exists(version):
|
||||
# Create tag
|
||||
subprocess.run(["git", "tag", tag_name], check=True)
|
||||
click.echo(f"Created tag {tag_name}")
|
||||
else:
|
||||
click.echo(f"Tag {tag_name} already exists")
|
||||
|
||||
|
||||
def tag_exists(version: str) -> bool:
|
||||
"""Check if a git tag exists."""
|
||||
tag_name = f"v{version}"
|
||||
result = subprocess.run(
|
||||
["git", "tag", "-l", tag_name], capture_output=True, text=True, check=True
|
||||
)
|
||||
return tag_name in result.stdout.strip()
|
||||
|
||||
|
||||
def push_tag(version: str) -> None:
|
||||
"""Push a git tag."""
|
||||
tag_name = f"v{version}"
|
||||
subprocess.run(["git", "push", "origin", tag_name], check=True)
|
||||
click.echo(f"Pushed tag {tag_name}")
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli() -> None:
|
||||
"""Version management for llama-cloud-services."""
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
def get() -> None:
|
||||
"""Get current versions and show validation warnings."""
|
||||
main_version, llama_parse_version, dependency_version = get_current_versions()
|
||||
|
||||
click.echo("Current versions:")
|
||||
click.echo(f" llama-cloud-services: {main_version}")
|
||||
click.echo(f" llama-parse: {llama_parse_version}")
|
||||
click.echo(f" dependency reference: {dependency_version}")
|
||||
|
||||
warnings = validate_versions(main_version, llama_parse_version, dependency_version)
|
||||
if warnings:
|
||||
click.echo("\nValidation warnings:")
|
||||
for warning in warnings:
|
||||
click.echo(f" ⚠️ {warning}")
|
||||
else:
|
||||
click.echo("\n✅ All versions are consistent")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("version")
|
||||
def set(version: str) -> None:
|
||||
"""Set version across all pyproject.toml files."""
|
||||
set_version(version)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--version", help="Version to tag (uses current version if not specified)"
|
||||
)
|
||||
@click.option(
|
||||
"--push",
|
||||
is_flag=True,
|
||||
help="Push the tag to the remote repository",
|
||||
)
|
||||
def tag(version: str | None = None, push: bool = False) -> None:
|
||||
"""Create and push a git tag for the current version."""
|
||||
if not version:
|
||||
main_version, _, _ = get_current_versions()
|
||||
version = main_version
|
||||
|
||||
create_if_not_exists(version)
|
||||
if push:
|
||||
push_tag(version)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
+2
-2
@@ -48,8 +48,8 @@ class TestData(BaseModel):
|
||||
# Skip all tests if API key is not set
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(
|
||||
not LLAMA_CLOUD_API_KEY,
|
||||
reason="LLAMA_CLOUD_API_KEY not set",
|
||||
not LLAMA_CLOUD_API_KEY or not LLAMA_DEPLOY_DEPLOYMENT_NAME,
|
||||
reason="LLAMA_CLOUD_API_KEY or LLAMA_DEPLOY_DEPLOYMENT_NAME not set",
|
||||
)
|
||||
async def test_agent_data_crud_operations():
|
||||
"""Test basic CRUD operations for agent data with automatic cleanup"""
|
||||
@@ -0,0 +1,109 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
from llama_cloud.types.agent_data import AgentData
|
||||
from llama_cloud.types.aggregate_group import AggregateGroup
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from llama_cloud_services.beta.agent_data.schema import (
|
||||
ExtractedData,
|
||||
TypedAgentData,
|
||||
TypedAggregateGroup,
|
||||
)
|
||||
|
||||
|
||||
# Test data models
|
||||
class Person(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
email: str
|
||||
|
||||
|
||||
class Company(BaseModel):
|
||||
name: str
|
||||
industry: str
|
||||
employees: int
|
||||
|
||||
|
||||
def test_typed_agent_data_from_raw():
|
||||
"""Test TypedAgentData.from_raw class method."""
|
||||
raw_data = AgentData(
|
||||
id="456",
|
||||
agent_slug="extraction-agent",
|
||||
collection="employees",
|
||||
data={"name": "Jane Smith", "age": 25, "email": "jane@company.com"},
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
typed_data = TypedAgentData.from_raw(raw_data, Person)
|
||||
|
||||
assert typed_data.id == "456"
|
||||
assert typed_data.agent_url_id == "extraction-agent"
|
||||
assert typed_data.collection == "employees"
|
||||
assert typed_data.data.name == "Jane Smith"
|
||||
assert typed_data.data.age == 25
|
||||
assert typed_data.data.email == "jane@company.com"
|
||||
|
||||
|
||||
def test_typed_agent_data_from_raw_validation_error():
|
||||
"""Test TypedAgentData.from_raw with invalid data."""
|
||||
raw_data = AgentData(
|
||||
id="789",
|
||||
agent_slug="test-agent",
|
||||
collection="people",
|
||||
data={"name": "Invalid Person", "age": "not_a_number"}, # Invalid age
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
TypedAgentData.from_raw(raw_data, Person)
|
||||
|
||||
|
||||
def test_extracted_data_create_method():
|
||||
"""Test ExtractedData.create class method."""
|
||||
person = Person(name="Created Person", age=35, email="created@example.com")
|
||||
|
||||
# Test with defaults
|
||||
extracted = ExtractedData.create(person)
|
||||
assert extracted.original_data == person
|
||||
assert extracted.data == person
|
||||
assert extracted.status == "pending_review"
|
||||
assert extracted.confidence == {}
|
||||
|
||||
# Test with custom values
|
||||
extracted_custom = ExtractedData.create(
|
||||
person, status="accepted", confidence={"name": 0.99}
|
||||
)
|
||||
assert extracted_custom.status == "accepted"
|
||||
assert extracted_custom.confidence["name"] == 0.99
|
||||
|
||||
|
||||
def test_extracted_data_with_dict():
|
||||
"""Test ExtractedData with dict data instead of Pydantic model."""
|
||||
data_dict = {"name": "Dict Person", "age": 45, "email": "dict@example.com"}
|
||||
|
||||
extracted = ExtractedData[Dict[str, Any]](
|
||||
original_data=data_dict, data=data_dict, status="accepted", confidence={}
|
||||
)
|
||||
|
||||
assert extracted.original_data["name"] == "Dict Person"
|
||||
assert extracted.data["age"] == 45
|
||||
|
||||
|
||||
def test_typed_aggregate_group_from_raw():
|
||||
"""Test TypedAggregateGroup.from_raw class method."""
|
||||
raw_group = AggregateGroup(
|
||||
group_key={"industry": "Technology"},
|
||||
count=25,
|
||||
first_item={"name": "Tech Corp", "industry": "Technology", "employees": 500},
|
||||
)
|
||||
|
||||
typed_group = TypedAggregateGroup.from_raw(raw_group, Company)
|
||||
|
||||
assert typed_group.group_key["industry"] == "Technology"
|
||||
assert typed_group.count == 25
|
||||
assert typed_group.first_item.name == "Tech Corp"
|
||||
assert typed_group.first_item.employees == 500
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user