Compare commits

..

2 Commits

Author SHA1 Message Date
Adrian Lyjak 9ce2044995 bump to 0.6.44 2025-07-08 17:40:39 -04:00
Adrian Lyjak 90d1608a71 Add nicer hand-written agent data interface 2025-07-08 21:12:42 +00:00
166 changed files with 8529 additions and 106658 deletions
+48
View File
@@ -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"
-50
View File
@@ -1,50 +0,0 @@
name: Build Package - Python
# Build package on its own without additional pip install
on:
push:
branches:
- main
pull_request:
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"
-28
View File
@@ -1,28 +0,0 @@
name: Build Package - TypeScript
on: [pull_request]
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
-36
View File
@@ -1,36 +0,0 @@
name: Lint - TypeScript
on:
push:
branches:
- main
pull_request:
branches:
- main
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
+83
View File
@@ -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
-66
View File
@@ -1,66 +0,0 @@
name: Publish Release - Python
on:
push:
tags:
- "v*"
workflow_dispatch:
env:
UV_VERSION: "0.7.20"
jobs:
build-n-publish:
name: Build and publish to PyPI
if: github.repository == 'run-llama/llama_cloud_services'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: ${{ env.UV_VERSION }}
- name: Set up Python
run: uv python install
- name: Display Python version
run: python --version
- name: Build
working-directory: py
run: uv build
- name: Test installing built package
shell: bash
working-directory: py
run: |
uv venv
uv pip install dist/*.whl
- name: Publish package
shell: bash
working-directory: py
run: uv publish --token ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
- name: Build and publish llama-parse
working-directory: py/llama_parse/
run: |
uv build
uv publish --token ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }} - LlamaCloud Services PY
artifacts: "py/**/dist/*"
generateReleaseNotes: true
draft: false
prerelease: false
-51
View File
@@ -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 }}
-39
View File
@@ -1,39 +0,0 @@
name: Test - Python
on:
push:
branches:
- main
pull_request:
env:
UV_VERSION: "0.7.20"
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: 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 tests/**/test_*.py
- name: Remove virtual environment
working-directory: py
run: rm -rf .venv/
-34
View File
@@ -1,34 +0,0 @@
name: Lint - TypeScript
on:
push:
branches:
- main
pull_request:
branches:
- main
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:
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 tests
working-directory: ts/llama_cloud_services/
run: pnpm test --run
+40
View File
@@ -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
-4
View File
@@ -5,7 +5,3 @@ __pycache__/
.idea
.env*
.ipynb_checkpoints*
*_cache/
node_modules/
.turbo/
dist/
+6 -6
View File
@@ -21,19 +21,19 @@ repos:
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
exclude: ".*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/
exclude: ^tests/
additional_dependencies:
[
"types-requests",
@@ -63,13 +63,13 @@ repos:
rev: v3.0.3
hooks:
- id: prettier
exclude: uv.lock
exclude: poetry.lock
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
additional_dependencies: [tomli]
exclude: ^(uv.lock|examples|ts)
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
-33
View File
@@ -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
...
View File
+1 -17
View File
@@ -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
-11
View File
@@ -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!
-131
View File
@@ -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
-15
View File
@@ -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,
]);
-48
View File
@@ -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"
}
}
-1770
View File
File diff suppressed because it is too large Load Diff
-48
View File
@@ -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);
-8
View File
@@ -1,8 +0,0 @@
import { createConsola } from "consola";
import type { ConsolaInstance } from "consola";
export const logger: ConsolaInstance = createConsola({
formatOptions: {
date: false,
},
});
-56
View File
@@ -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;
}
-22
View File
@@ -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"]
}
-124
View File
@@ -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.
-15
View File
@@ -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,
]);
-47
View File
@@ -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"
}
}
-1758
View File
File diff suppressed because it is too large Load Diff
-34
View File
@@ -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);
-8
View File
@@ -1,8 +0,0 @@
import { createConsola } from "consola";
import type { ConsolaInstance } from "consola";
export const logger: ConsolaInstance = createConsola({
formatOptions: {
date: false,
},
});
-51
View File
@@ -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;
}
-22
View File
@@ -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"]
}
-516
View File
@@ -1,516 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Table Extraction with LlamaParse\n",
"\n",
"This notebook will show you how to extract tables and save them as CSV files thanks to LlamaParse advanced parsing capabilities."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**1. Install needed dependencies**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"! pip install llama-cloud-services pandas"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**2. Set you LLAMA_CLOUD_API_KEY as env variable**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"LLAMA_CLOUD_API_KEY: ··········\n"
]
}
],
"source": [
"import os\n",
"from getpass import getpass\n",
"\n",
"os.environ[\"LLAMA_CLOUD_API_KEY\"] = getpass(\"LLAMA_CLOUD_API_KEY: \")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**3. Initialiaze the parser**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from llama_cloud_services import LlamaParse\n",
"\n",
"parser = LlamaParse(result_type=\"markdown\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**4. Get data**\n",
"\n",
"This is a PDF with _lots_ of tables!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"--2025-07-16 16:20:41-- https://assets.accessible-digital-documents.com/uploads/2017/01/sample-tables.pdf\n",
"Resolving assets.accessible-digital-documents.com (assets.accessible-digital-documents.com)... 3.166.135.2, 3.166.135.62, 3.166.135.51, ...\n",
"Connecting to assets.accessible-digital-documents.com (assets.accessible-digital-documents.com)|3.166.135.2|:443... connected.\n",
"HTTP request sent, awaiting response... 200 OK\n",
"Length: 145494 (142K) [application/pdf]\n",
"Saving to: sample-tables.pdf\n",
"\n",
"sample-tables.pdf 100%[===================>] 142.08K --.-KB/s in 0.04s \n",
"\n",
"2025-07-16 16:20:41 (3.72 MB/s) - sample-tables.pdf saved [145494/145494]\n",
"\n"
]
}
],
"source": [
"! wget https://assets.accessible-digital-documents.com/uploads/2017/01/sample-tables.pdf"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**5. Parse document**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Started parsing the file under job_id b53949f7-9017-4b6a-b30c-be6227271ed2\n"
]
}
],
"source": [
"json_result = parser.get_json_result(\"sample-tables.pdf\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**6. Get tables!**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"tables = parser.get_tables(json_result, \"tables/\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**7. Load tables**\n",
"\n",
"Let's show one example table!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.google.colaboratory.intrinsic+json": {
"summary": "{\n \"name\": \"display(df\",\n \"rows\": 8,\n \"fields\": [\n {\n \"column\": \"Rainfall\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Average\",\n \"\",\n \"24 hour high\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Americas\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 908,\n \"min\": 9,\n \"max\": 2010,\n \"num_unique_values\": 8,\n \"samples\": [\n 104,\n 133,\n 2010\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Asia\",\n \"properties\": {\n \"dtype\": \"object\",\n \"num_unique_values\": 7,\n \"samples\": [\n \"\",\n 201.0,\n 28.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Europe\",\n \"properties\": {\n \"dtype\": \"object\",\n \"num_unique_values\": 7,\n \"samples\": [\n \"\",\n 193.0,\n 29.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Africa\",\n \"properties\": {\n \"dtype\": \"object\",\n \"num_unique_values\": 7,\n \"samples\": [\n \"\",\n 144.0,\n 20.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}",
"type": "dataframe"
},
"text/html": [
"\n",
" <div id=\"df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb\" class=\"colab-df-container\">\n",
" <div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Rainfall</th>\n",
" <th>Americas</th>\n",
" <th>Asia</th>\n",
" <th>Europe</th>\n",
" <th>Africa</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>(inches)</td>\n",
" <td>2010</td>\n",
" <td></td>\n",
" <td></td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>Average</td>\n",
" <td>104</td>\n",
" <td>201.0</td>\n",
" <td>193.0</td>\n",
" <td>144.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>24 hour high</td>\n",
" <td>15</td>\n",
" <td>26.0</td>\n",
" <td>27.0</td>\n",
" <td>18.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>12 hour high</td>\n",
" <td>9</td>\n",
" <td>10.0</td>\n",
" <td>11.0</td>\n",
" <td>12.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td></td>\n",
" <td>2009</td>\n",
" <td></td>\n",
" <td></td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>Average</td>\n",
" <td>133</td>\n",
" <td>244.0</td>\n",
" <td>155.0</td>\n",
" <td>166.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>24 hour high</td>\n",
" <td>27</td>\n",
" <td>28.0</td>\n",
" <td>29.0</td>\n",
" <td>20.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>12 hour high</td>\n",
" <td>11</td>\n",
" <td>12.0</td>\n",
" <td>13.0</td>\n",
" <td>16.0</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>\n",
" <div class=\"colab-df-buttons\">\n",
"\n",
" <div class=\"colab-df-container\">\n",
" <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb')\"\n",
" title=\"Convert this dataframe to an interactive table.\"\n",
" style=\"display:none;\">\n",
"\n",
" <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
" <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
" </svg>\n",
" </button>\n",
"\n",
" <style>\n",
" .colab-df-container {\n",
" display:flex;\n",
" gap: 12px;\n",
" }\n",
"\n",
" .colab-df-convert {\n",
" background-color: #E8F0FE;\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: #1967D2;\n",
" height: 32px;\n",
" padding: 0 0 0 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-convert:hover {\n",
" background-color: #E2EBFA;\n",
" box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: #174EA6;\n",
" }\n",
"\n",
" .colab-df-buttons div {\n",
" margin-bottom: 4px;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert {\n",
" background-color: #3B4455;\n",
" fill: #D2E3FC;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert:hover {\n",
" background-color: #434B5C;\n",
" box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
" filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
" fill: #FFFFFF;\n",
" }\n",
" </style>\n",
"\n",
" <script>\n",
" const buttonEl =\n",
" document.querySelector('#df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb button.colab-df-convert');\n",
" buttonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
"\n",
" async function convertToInteractive(key) {\n",
" const element = document.querySelector('#df-94a74c8f-1062-4a80-8d3f-32f0fbadf7bb');\n",
" const dataTable =\n",
" await google.colab.kernel.invokeFunction('convertToInteractive',\n",
" [key], {});\n",
" if (!dataTable) return;\n",
"\n",
" const docLinkHtml = 'Like what you see? Visit the ' +\n",
" '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
" + ' to learn more about interactive tables.';\n",
" element.innerHTML = '';\n",
" dataTable['output_type'] = 'display_data';\n",
" await google.colab.output.renderOutput(dataTable, element);\n",
" const docLink = document.createElement('div');\n",
" docLink.innerHTML = docLinkHtml;\n",
" element.appendChild(docLink);\n",
" }\n",
" </script>\n",
" </div>\n",
"\n",
"\n",
" <div id=\"df-54b2aa43-838b-47d3-9209-2fb18153cf87\">\n",
" <button class=\"colab-df-quickchart\" onclick=\"quickchart('df-54b2aa43-838b-47d3-9209-2fb18153cf87')\"\n",
" title=\"Suggest charts\"\n",
" style=\"display:none;\">\n",
"\n",
"<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
" width=\"24px\">\n",
" <g>\n",
" <path d=\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z\"/>\n",
" </g>\n",
"</svg>\n",
" </button>\n",
"\n",
"<style>\n",
" .colab-df-quickchart {\n",
" --bg-color: #E8F0FE;\n",
" --fill-color: #1967D2;\n",
" --hover-bg-color: #E2EBFA;\n",
" --hover-fill-color: #174EA6;\n",
" --disabled-fill-color: #AAA;\n",
" --disabled-bg-color: #DDD;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-quickchart {\n",
" --bg-color: #3B4455;\n",
" --fill-color: #D2E3FC;\n",
" --hover-bg-color: #434B5C;\n",
" --hover-fill-color: #FFFFFF;\n",
" --disabled-bg-color: #3B4455;\n",
" --disabled-fill-color: #666;\n",
" }\n",
"\n",
" .colab-df-quickchart {\n",
" background-color: var(--bg-color);\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: var(--fill-color);\n",
" height: 32px;\n",
" padding: 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-quickchart:hover {\n",
" background-color: var(--hover-bg-color);\n",
" box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: var(--button-hover-fill-color);\n",
" }\n",
"\n",
" .colab-df-quickchart-complete:disabled,\n",
" .colab-df-quickchart-complete:disabled:hover {\n",
" background-color: var(--disabled-bg-color);\n",
" fill: var(--disabled-fill-color);\n",
" box-shadow: none;\n",
" }\n",
"\n",
" .colab-df-spinner {\n",
" border: 2px solid var(--fill-color);\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" animation:\n",
" spin 1s steps(1) infinite;\n",
" }\n",
"\n",
" @keyframes spin {\n",
" 0% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" border-left-color: var(--fill-color);\n",
" }\n",
" 20% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 30% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 40% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 60% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 80% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" 90% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" }\n",
"</style>\n",
"\n",
" <script>\n",
" async function quickchart(key) {\n",
" const quickchartButtonEl =\n",
" document.querySelector('#' + key + ' button');\n",
" quickchartButtonEl.disabled = true; // To prevent multiple clicks.\n",
" quickchartButtonEl.classList.add('colab-df-spinner');\n",
" try {\n",
" const charts = await google.colab.kernel.invokeFunction(\n",
" 'suggestCharts', [key], {});\n",
" } catch (error) {\n",
" console.error('Error during call to suggestCharts:', error);\n",
" }\n",
" quickchartButtonEl.classList.remove('colab-df-spinner');\n",
" quickchartButtonEl.classList.add('colab-df-quickchart-complete');\n",
" }\n",
" (() => {\n",
" let quickchartButtonEl =\n",
" document.querySelector('#df-54b2aa43-838b-47d3-9209-2fb18153cf87 button');\n",
" quickchartButtonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
" })();\n",
" </script>\n",
" </div>\n",
"\n",
" </div>\n",
" </div>\n"
],
"text/plain": [
" Rainfall Americas Asia Europe Africa\n",
"0 (inches) 2010 \n",
"1 Average 104 201.0 193.0 144.0\n",
"2 24 hour high 15 26.0 27.0 18.0\n",
"3 12 hour high 9 10.0 11.0 12.0\n",
"4 2009 \n",
"5 Average 133 244.0 155.0 166.0\n",
"6 24 hour high 27 28.0 29.0 20.0\n",
"7 12 hour high 11 12.0 13.0 16.0"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import pandas as pd\n",
"from IPython.display import display\n",
"\n",
"df = pd.read_csv(\n",
" \"/content/tables/table_2025_16_07_16_30_01_569.csv\",\n",
")\n",
"display(df.fillna(\"\"))"
]
}
],
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -208,5 +208,5 @@ Another option (orthogonal to the above) is to break the document into smaller s
## Additional Resources
- [Example Notebook](examples/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
-86
View File
@@ -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 youre interested in centralizing your data pipelines and spending more time working on your actual RAG use cases, come [talk to us.](https://www.llamaindex.ai/contact)
If you have access to LlamaCloud, you can visit [LlamaCloud](https://cloud.llamaindex.ai) to sign in and get an API key.
## Setup
First, make sure you have the latest LlamaIndex version installed.
```
pip uninstall llama-index # run this if upgrading from v0.9.x or older
pip install -U llama-index --upgrade --no-cache-dir --force-reinstall
```
The `llama-index-indices-managed-llama-cloud` package is included with the above install, but you can also install directly
```
pip install -U llama-index-indices-managed-llama-cloud
```
## Usage
You can create an index on LlamaCloud using the following code. By default, new indexes use managed embeddings (OpenAI text-embedding-3-small, 1536 dimensions, 1 credit/page):
```python
import os
os.environ[
"LLAMA_CLOUD_API_KEY"
] = "llx-..." # can provide API-key in env or in the constructor later on
from llama_index.core import SimpleDirectoryReader
from llama_cloud_services import LlamaCloudIndex
# create a new index (uses managed embeddings by default)
index = LlamaCloudIndex.from_documents(
documents,
"my_first_index",
project_name="default",
api_key="llx-...",
verbose=True,
)
# connect to an existing index
index = LlamaCloudIndex("my_first_index", project_name="default")
```
You can also configure a retriever for managed retrieval:
```python
# from the existing index
index.as_retriever()
# from scratch
from llama_index.indices.managed.llama_cloud import LlamaCloudRetriever
retriever = LlamaCloudRetriever("my_first_index", project_name="default")
```
And of course, you can use other index shortcuts to get use out of your new managed index:
```python
query_engine = index.as_query_engine(llm=llm)
chat_engine = index.as_chat_engine(llm=llm)
```
## Retriever Settings
A full list of retriever settings/kwargs is below:
- `dense_similarity_top_k`: Optional[int] -- If greater than 0, retrieve `k` nodes using dense retrieval
- `sparse_similarity_top_k`: Optional[int] -- If greater than 0, retrieve `k` nodes using sparse retrieval
- `enable_reranking`: Optional[bool] -- Whether to enable reranking or not. Sacrifices some speed for accuracy
- `rerank_top_n`: Optional[int] -- The number of nodes to return after reranking initial retrieval results
- `alpha` Optional[float] -- The weighting between dense and sparse retrieval. 1 = Full dense retrieval, 0 = Full sparse retrieval.
@@ -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",
]
@@ -0,0 +1,19 @@
from .schema import (
TypedAgentData,
ExtractedData,
TypedAgentDataItems,
StatusType,
ExtractedT,
AgentDataT,
)
from .client import AsyncAgentDataClient
__all__ = [
"TypedAgentData",
"AsyncAgentDataClient",
"ExtractedData",
"TypedAgentDataItems",
"StatusType",
"ExtractedT",
"AgentDataT",
]
@@ -1,6 +1,7 @@
import os
from typing import Any, Dict, Generic, List, Optional, Type
from typing import Dict, Generic, List, Optional, Type
from llama_cloud import FilterOperation
from llama_cloud.client import AsyncLlamaCloud
from tenacity import (
WrappedFn,
@@ -13,7 +14,6 @@ import httpx
from .schema import (
AgentDataT,
ComparisonOperator,
TypedAgentData,
TypedAgentDataItems,
TypedAggregateGroup,
@@ -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]):
@@ -85,7 +85,7 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
agent_client = AsyncAgentDataClient(
client=llama_client,
type=ExtractedPerson,
collection="extracted_people",
collection_name="extracted_people",
agent_url_id="person-extraction-agent"
)
@@ -94,8 +94,8 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
result = await agent_client.create_agent_data(person)
# Search data
results = await agent_client.search(
filter={"age": {"gt": 25}},
results = await agent_client.search_agent_data(
filter={"age": FilterOperation(gt=25)},
order_by="data.name",
page_size=20
)
@@ -107,20 +107,19 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
def __init__(
self,
client: AsyncLlamaCloud,
type: Type[AgentDataT],
collection: str = "default",
collection_name: str = "default",
agent_url_id: Optional[str] = None,
client: Optional[AsyncLlamaCloud] = None,
token: Optional[str] = None,
base_url: Optional[str] = None,
):
"""
Initialize the AsyncAgentDataClient.
Args:
client: AsyncLlamaCloud client instance for API communication
type: Pydantic BaseModel class that defines the data structure.
All agent data will be validated against this type.
collection: Named collection within the agent for organizing data.
collection_name: Named collection within the agent for organizing data.
Defaults to "default". Collections allow logical separation of
different data types or workflows within the same agent.
agent_url_id: Unique identifier for the agent. This normally appears in the
@@ -128,11 +127,6 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
will attempt to use the LLAMA_DEPLOY_DEPLOYMENT_NAME environment
variable. Data can only be added to an already existing agent in the
platform.
client: AsyncLlamaCloud client instance for API communication. If not provided, will
construct one from the provided api token and base url
token: Llama Cloud API token. Reads from LLAMA_CLOUD_API_KEY if not provided
base_url: Llama Cloud API token. Reads from LLAMA_CLOUD_BASE_URL if not provided, and
defaults to https://api.cloud.llamaindex.ai
Raises:
ValueError: If agent_url_id is not provided and the
@@ -144,34 +138,33 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
"""
self.agent_url_id = agent_url_id or get_default_agent_id()
self.collection = collection
if not client:
client = AsyncLlamaCloud(
token=token or os.getenv("LLAMA_CLOUD_API_KEY"),
base_url=base_url or os.getenv("LLAMA_CLOUD_BASE_URL"),
if not self.agent_url_id:
raise ValueError(
"Agent ID is required, or set the LLAMA_DEPLOY_DEPLOYMENT_NAME environment variable"
)
self.collection_name = collection_name
self.client = client
self.type = type
@agent_data_retry
async def get_item(self, item_id: str) -> TypedAgentData[AgentDataT]:
async def get_agent_data(self, item_id: str) -> TypedAgentData[AgentDataT]:
raw_data = await self.client.beta.get_agent_data(
item_id=item_id,
)
return TypedAgentData.from_raw(raw_data, validator=self.type)
@agent_data_retry
async def create_item(self, data: AgentDataT) -> TypedAgentData[AgentDataT]:
async def create_agent_data(self, data: AgentDataT) -> TypedAgentData[AgentDataT]:
raw_data = await self.client.beta.create_agent_data(
agent_slug=self.agent_url_id,
collection=self.collection,
collection=self.collection_name,
data=data.model_dump(),
)
return TypedAgentData.from_raw(raw_data, validator=self.type)
@agent_data_retry
async def update_item(
async def update_agent_data(
self, item_id: str, data: AgentDataT
) -> TypedAgentData[AgentDataT]:
raw_data = await self.client.beta.update_agent_data(
@@ -181,13 +174,13 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
return TypedAgentData.from_raw(raw_data, validator=self.type)
@agent_data_retry
async def delete_item(self, item_id: str) -> None:
async def delete_agent_data(self, item_id: str) -> None:
await self.client.beta.delete_agent_data(item_id=item_id)
@agent_data_retry
async def search(
async def search_agent_data(
self,
filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None,
filter: Optional[Dict[str, Optional[FilterOperation]]] = None,
order_by: Optional[str] = None,
offset: Optional[int] = None,
page_size: Optional[int] = None,
@@ -198,11 +191,11 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
Args:
filter: Filter conditions to apply to the search. Dict mapping field names to FilterOperation objects. Filters only by data fields
Examples:
- {"age": {"gt": 18}} - age greater than 18
- {"status": {"eq": "active"}} - status equals "active"
- {"tags": {"includes": ["python", "ml"]}} - tags include "python" or "ml"
- {"created_at": {"gte": "2024-01-01"}} - created after date
- {"score": {"lt": 100, "gte": 50}} - score between 50 and 100
- {"age": FilterOperation(gt=18)} - age greater than 18
- {"status": FilterOperation(eq="active")} - status equals "active"
- {"tags": FilterOperation(includes=["python", "ml"])} - tags include "python" or "ml"
- {"created_at": FilterOperation(gte="2024-01-01")} - created after date
- {"score": FilterOperation(lt=100, gte=50)} - score between 50 and 100
order_by: Comma delimited list of fields to sort results by. Can order by standard agent fields like created_at, or by data fields. Data fields must be prefixed with "data.". If ordering desceding, use a " desc" suffix.
Examples:
- "data.name desc, created_at" - sort by name in descending order, and then by creation date
@@ -212,7 +205,7 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
"""
raw = await self.client.beta.search_agent_data_api_v_1_beta_agent_data_search_post(
agent_slug=self.agent_url_id,
collection=self.collection,
collection=self.collection_name,
filter=filter,
order_by=order_by,
offset=offset,
@@ -228,9 +221,9 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
)
@agent_data_retry
async def aggregate(
async def aggregate_agent_data(
self,
filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None,
filter: Optional[Dict[str, Optional[FilterOperation]]] = None,
group_by: Optional[List[str]] = None,
count: Optional[bool] = None,
first: Optional[bool] = None,
@@ -242,20 +235,20 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
Aggregate agent data into groups according to the group_by fields.
Args:
filter: Filter conditions to apply to the search. Dict mapping field names to FilterOperation objects. Filters only by data fields
See search for more details on filtering.
See search_agent_data for more details on filtering.
group_by: List of fields to group by. Groups strictly by equality. Can only group by data fields.
Examples:
- ["name"] - group by name
- ["name", "age"] - group by name and age
count: Whether to include the count of items in each group.
first: Whether to include the first item in each group.
order_by: Comma delimited list of fields to sort results by. See search for more details on ordering.
order_by: Comma delimited list of fields to sort results by. See search_agent_data for more details on ordering.
offset: Number of groups to skip from the beginning. Defaults to 0.
page_size: Maximum number of groups to return per page.
"""
raw = await self.client.beta.aggregate_agent_data_api_v_1_beta_agent_data_aggregate_post(
agent_slug=self.agent_url_id,
collection=self.collection,
collection=self.collection_name,
page_size=page_size,
filter=filter,
order_by=order_by,
@@ -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,
@@ -62,11 +60,7 @@ AgentDataT = TypeVar("AgentDataT", bound=BaseModel)
ExtractedT = TypeVar("ExtractedT", bound=Union[BaseModel, dict])
# Status types for extracted data workflow
StatusType = Union[Literal["error", "accepted", "rejected", "pending_review"], str]
ComparisonOperator = Dict[
str, Dict[Literal["gt", "gte", "lt", "lte", "eq", "includes"], Any]
]
StatusType = Union[Literal["error", "accepted", "rejected", "in_review"], str]
class TypedAgentData(BaseModel, Generic[AgentDataT]):
@@ -148,7 +142,7 @@ class TypedAgentDataItems(BaseModel, Generic[AgentDataT]):
Example:
```python
# Search with pagination
results = await client.search(
results = await client.search_agent_data(
page_size=10,
include_total=True
)
@@ -158,7 +152,7 @@ class TypedAgentDataItems(BaseModel, Generic[AgentDataT]):
if results.has_more:
# Load next page
next_page = await client.search(
next_page = await client.search_agent_data(
page_size=10,
offset=10
)
@@ -176,74 +170,6 @@ class TypedAgentDataItems(BaseModel, Generic[AgentDataT]):
)
class ExtractedFieldMetadata(BaseModel):
"""
Metadata for an extracted data field, such as confidence, and citation information.
"""
confidence: Optional[float] = Field(
None,
description="The confidence score for the field, combined with parsing confidence if applicable",
)
extracted_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:
"""
Parse the extracted field metadata into a dictionary of field names to field metadata.
"""
result: ExtractedFieldMetaDataDict = {}
for field_name, field_value in field_metadata.items():
if isinstance(field_value, ExtractedFieldMetadata):
# support running this multiple times
result[field_name] = field_value
elif isinstance(field_value, dict):
if "confidence" in field_value or "citations" in field_value:
try:
validated = ExtractedFieldMetadata.model_validate(field_value)
# grab the citation from the array. This is just an array for backwards compatibility.
if "citations" in field_value and len(field_value["citations"]) > 0:
first_citation = field_value["citations"][0]
if "page_number" in first_citation and isinstance(
first_citation["page_number"], numbers.Number
):
validated.page_number = int(first_citation["page_number"]) # type: ignore
if "matching_text" in first_citation and isinstance(
first_citation["matching_text"], str
):
validated.matching_text = first_citation["matching_text"]
result[field_name] = validated
continue
except ValidationError:
pass
result[field_name] = parse_extracted_field_metadata(field_value)
elif isinstance(field_value, list):
result[field_name] = [
parse_extracted_field_metadata(item) for item in field_value
]
else:
result[field_name] = field_value
return result
class ExtractedData(BaseModel, Generic[ExtractedT]):
"""
Wrapper for extracted data with workflow status tracking.
@@ -257,12 +183,9 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
data: The current state of the data (may differ from original after edits)
status: Current workflow status (in_review, accepted, rejected, error)
confidence: Confidence scores for individual fields (if available)
file_id: The llamacloud file ID of the file that was used to extract the data
file_name: The name of the file that was used to extract the data
file_hash: A content hash of the file that was used to extract the data, for de-duplication
Status Workflow:
- "pending_review": Initial state, awaiting human review
- "in_review": Initial state, awaiting human review
- "accepted": Data approved and ready for use
- "rejected": Data rejected, needs re-extraction or manual fix
- "error": Processing error occurred
@@ -271,8 +194,8 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
```python
# Create extracted data for review
extracted = ExtractedData.create(
data=person_data,
status="pending_review",
extracted_data=person_data,
status="in_review",
confidence={"name": 0.95, "age": 0.87}
)
@@ -289,39 +212,20 @@ class ExtractedData(BaseModel, Generic[ExtractedT]):
data: ExtractedT = Field(
description="The latest state of the data. Will differ if data has been updated"
)
status: StatusType = Field(description="The status of the extracted data")
overall_confidence: Optional[float] = Field(
None,
description="The overall confidence score for the extracted data",
status: Union[Literal["error", "accepted", "rejected", "in_review"], str] = Field(
description="The status of the extracted data"
)
field_metadata: ExtractedFieldMetaDataDict = Field(
confidence: Dict[str, Union[float, Dict]] = Field(
default_factory=dict,
description="Page links, and perhaps eventually bounding boxes, for individual fields in the extracted data. Structure is expected to have a ",
)
file_id: Optional[str] = Field(
None, description="The ID of the file that was used to extract the data"
)
file_name: Optional[str] = Field(
None, description="The name of the file that was used to extract the data"
)
file_hash: Optional[str] = Field(
None, description="The hash of the file that was used to extract the data"
)
metadata: Optional[Dict[str, Any]] = Field(
default_factory=dict,
description="Additional metadata about the extracted data, such as errors, tokens, etc.",
description="Confidence scores, if any, for each primitive field in the original_data data",
)
@classmethod
def create(
cls,
data: ExtractedT,
status: StatusType = "pending_review",
field_metadata: ExtractedFieldMetaDataDict = {},
file_id: Optional[str] = None,
file_name: Optional[str] = None,
file_hash: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
extracted_data: ExtractedT,
status: StatusType = "in_review",
confidence: Optional[Dict[str, Union[float, Dict]]] = None,
) -> "ExtractedData[ExtractedT]":
"""
Create a new ExtractedData instance with sensible defaults.
@@ -329,128 +233,18 @@ 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
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
confidence: Optional confidence scores for fields
Returns:
New ExtractedData instance ready for storage
"""
normalized_field_metadata = parse_extracted_field_metadata(field_metadata)
return cls(
original_data=data,
data=data,
original_data=extracted_data,
data=extracted_data,
status=status,
field_metadata=normalized_field_metadata,
overall_confidence=calculate_overall_confidence(normalized_field_metadata),
file_id=file_id,
file_name=file_name,
file_hash=file_hash,
metadata=metadata or {},
confidence=confidence 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]):
"""
@@ -137,9 +137,15 @@ def run_in_thread(
def _extraction_config_warning(config: ExtractConfig) -> None:
if config.cite_sources or config.confidence_scores:
if config.use_reasoning:
warnings.warn(
"`cite_sources`/`confidence_scores` could greatly increase the "
"`use_reasoning` is an experimental feature. Results will be available in "
"the `extraction_metadata` field for the extraction run.",
ExperimentalWarning,
)
if config.cite_sources:
warnings.warn(
"`cite_sources` is an experimental feature. This may greatly increase the "
"size of the response, and slow down the extraction. Results will be "
"available in the `extraction_metadata` field for the extraction run.",
ExperimentalWarning,
@@ -37,7 +37,6 @@ from llama_cloud_services.parse.utils import (
nest_asyncio_msg,
make_api_request,
partition_pages,
extract_tables_from_json_results,
)
# can put in a path to the file or the file bytes itself
@@ -325,10 +324,6 @@ class LlamaParse(BasePydanticReader):
default=None,
description="The maximum number of pages to extract text from documents. If set to 0 or not set, all pages will be that should be extracted will be extracted (can work in combination with targetPages).",
)
merge_tables_across_pages_in_markdown: Optional[bool] = Field(
default=False,
description="If set to true, the parser will merge tables across pages in the markdown output. This is useful for documents with tables that span across multiple pages.",
)
output_pdf_of_document: Optional[bool] = Field(
default=False,
description="If set to true, the parser will also output a PDF of the document. (except for spreadsheets)",
@@ -381,10 +376,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.",
@@ -868,11 +859,6 @@ class LlamaParse(BasePydanticReader):
if self.page_prefix is not None:
data["page_prefix"] = self.page_prefix
if self.merge_tables_across_pages_in_markdown:
data[
"merge_tables_across_pages_in_markdown"
] = self.merge_tables_across_pages_in_markdown
if self.hide_headers:
data["hide_headers"] = self.hide_headers
@@ -916,9 +902,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
@@ -1628,16 +1611,6 @@ class LlamaParse(BasePydanticReader):
else:
raise e
def get_tables(self, json_results: List[dict], download_path: str) -> List[str]:
if not os.path.exists(download_path):
os.makedirs(download_path)
return extract_tables_from_json_results(json_results, download_path)
async def aget_tables(
self, json_result: List[dict], download_path: str
) -> List[str]:
return await asyncio.to_thread(self.get_tables, json_result, download_path)
async def aget_xlsx(
self, json_result: List[dict], download_path: str
) -> List[dict]:
@@ -1704,79 +1677,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
@@ -295,64 +295,13 @@ class JobResult(BaseModel):
async def aget_markdown_nodes(self, split_by_page: bool = False) -> List[TextNode]:
"""
Get the markdown nodes from the job.
Args:
split_by_page: Whether to split the pages into separate documents
"""
documents = await self.aget_markdown_documents(split_by_page)
return [TextNode(text=doc.text, metadata=doc.metadata) for doc in documents]
def get_markdown(self) -> str:
"""
Get the raw parsed markdown from the job, distinct from the markdown documents.
This does not include page separators, e.g. if merge_tables_across_pages_in_markdown is True
"""
return asyncio_run(self.aget_markdown())
async def aget_markdown(self) -> str:
"""
Get the raw parsed markdown from the job, distinct from the markdown documents.
This does not include page separators, e.g. if merge_tables_across_pages_in_markdown is True
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/raw/markdown"
response = await make_api_request(self._client, "GET", url)
return response.content.decode("utf-8")
def get_text(self) -> str:
"""
Get the raw parsed text from the job.
"""
return asyncio_run(self.aget_text())
async def aget_text(self) -> str:
"""
Get the raw parsed text from the job.
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/raw/text"
response = await make_api_request(self._client, "GET", url)
return response.content.decode("utf-8")
def get_json(self) -> Dict[str, Any]:
"""
Get the full parsed JSON result from the job.
Note:
This is not the same as JobResult.json(), which is a
JSON serialized version of the JobResult Page Documents.
"""
return asyncio_run(self.aget_json())
async def aget_json(self) -> Dict[str, Any]:
"""
Get the full parsed JSON result from the job.
Note:
This is not the same as JobResult.json(), which is a
JSON serialized version of the JobResult Page Documents.
"""
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/json"
response = await make_api_request(self._client, "GET", url)
return response.json()
async def _get_image_document_with_bytes(
self, image: ImageItem, page: Page
) -> ImageDocument:
@@ -1,10 +1,7 @@
import httpx
import itertools
import logging
import os
from enum import Enum
from pathlib import Path
from datetime import datetime
from tenacity import (
retry,
stop_after_attempt,
@@ -12,7 +9,7 @@ from tenacity import (
retry_if_exception,
before_sleep_log,
)
from typing import Any, Iterable, Iterator, Optional, List, cast
from typing import Any, Iterable, Iterator, Optional
logger = logging.getLogger(__name__)
@@ -354,30 +351,3 @@ def partition_pages(
yield ",".join(targets)
else:
return
def extract_tables_from_json_results(
json_results: List[dict], download_path: str
) -> List[str]:
tables = []
for json_result in json_results:
pages = json_result["pages"]
for page in pages:
page = cast(dict, page)
items = page.get("items", [])
if items:
for i, item in enumerate(items):
item = cast(dict, item)
if item.get("type", "") == "table" and item.get("csv", ""):
savepath = os.path.join(
download_path,
f"table_{datetime.now().strftime('%Y_%d_%m_%H_%M_%S_%f')[:-3]}.csv",
)
if Path(savepath).exists():
savepath = (
savepath.replace(".csv", "_")[0] + str(i) + ".csv"
)
with open(savepath, "w") as f:
f.write(item["csv"])
tables.append(savepath)
return tables
@@ -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
+3088
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "llama-parse"
version = "0.6.44"
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.44"
[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"
+4 -4
View File
@@ -153,10 +153,10 @@ Full documentation for `SimpleDirectoryReader` can be found on the [LlamaIndex D
Several end-to-end indexing examples can be found in the examples folder
- [Getting Started](./examples/parse/demo_basic.ipynb)
- [Advanced RAG Example](./examples/parse/demo_advanced.ipynb)
- [Raw API Usage](./examples/parse/demo_api.ipynb)
- [Result Object Tour](./examples/parse/demo_json_tour.ipynb)
- [Getting Started](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
-1107
View File
File diff suppressed because it is too large Load Diff
Generated
+4626
View File
File diff suppressed because it is too large Load Diff
-21
View File
@@ -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.
-87
View File
@@ -1,87 +0,0 @@
[![PyPI - Downloads](https://img.shields.io/pypi/dm/llama-cloud-services)](https://pypi.org/project/llama-cloud-services/)
[![GitHub contributors](https://img.shields.io/github/contributors/run-llama/llama_cloud_services)](https://github.com/run-llama/llama_cloud_services/graphs/contributors)
[![Discord](https://img.shields.io/discord/1059199217496772688)](https://discord.gg/dGcwcsnxhU)
# Llama Cloud Services
This repository contains the code for hand-written SDKs and clients for interacting with LlamaCloud.
This includes:
- [LlamaParse](../parse.md) - A GenAI-native document parser that can parse complex document data for any downstream LLM use case (Agents, RAG, data processing, etc.).
- [LlamaReport (beta/invite-only)](../report.md) - A prebuilt agentic report builder that can be used to build reports from a variety of data sources.
- [LlamaExtract](../extract.md) - A prebuilt agentic data extractor that can be used to transform data into a structured JSON representation.
- [LlamaCloud Index](../index.md) - A widely customizable and fully automated document ingestion pipeline that also serves retrieval purposes.
## Getting Started
Install the package:
```bash
pip install llama-cloud-services
```
Then, get your API key from [LlamaCloud](https://cloud.llamaindex.ai/).
Then, you can use the services in your code:
```python
from llama_cloud_services import (
LlamaParse,
LlamaReport,
LlamaExtract,
LlamaCloudIndex,
)
from llama_cloud_services import LlamaParse, LlamaReport, LlamaExtract
parser = LlamaParse(api_key="YOUR_API_KEY")
report = LlamaReport(api_key="YOUR_API_KEY")
extract = LlamaExtract(api_key="YOUR_API_KEY")
index = LlamaCloudIndex(
"my_first_index", project_name="default", api_key="YOUR_API_KEY"
)
```
See the quickstart guides for each service for more information:
- [LlamaParse](../parse.md)
- [LlamaReport (beta/invite-only)](../report.md)
- [LlamaExtract](../extract.md)
- [LlamaCloud Index](../index.md)
## Switch to EU SaaS 🇪🇺
If you are interested in using LlamaCloud services in the EU, you can adjust your base URL to `https://api.cloud.eu.llamaindex.ai`.
You can also create your API key in the EU region [here](https://cloud.eu.llamaindex.ai).
```python
from llama_cloud_services import (
LlamaParse,
LlamaReport,
LlamaExtract,
EU_BASE_URL,
)
parser = LlamaParse(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
report = LlamaReport(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
extract = LlamaExtract(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
index = LlamaCloudIndex(
"my_first_index",
project_name="default",
api_key="YOUR_API_KEY",
base_url=EU_BASE_URL,
)
```
## Documentation
You can see complete SDK and API documentation for each service on [our official docs](https://docs.cloud.llamaindex.ai/).
## Terms of Service
See the [Terms of Service Here](../TOS.pdf).
## Get in Touch (LlamaCloud)
You can get in touch with us by following our [contact link](https://www.llamaindex.ai/contact).
@@ -1,31 +0,0 @@
from .schema import (
TypedAgentData,
ExtractedData,
TypedAgentDataItems,
StatusType,
ExtractedT,
AgentDataT,
ComparisonOperator,
parse_extracted_field_metadata,
calculate_overall_confidence,
InvalidExtractionData,
ExtractedFieldMetadata,
ExtractedFieldMetaDataDict,
)
from .client import AsyncAgentDataClient
__all__ = [
"TypedAgentData",
"AsyncAgentDataClient",
"ExtractedData",
"TypedAgentDataItems",
"StatusType",
"ExtractedT",
"AgentDataT",
"ComparisonOperator",
"parse_extracted_field_metadata",
"calculate_overall_confidence",
"InvalidExtractionData",
"ExtractedFieldMetadata",
"ExtractedFieldMetaDataDict",
]
-11
View File
@@ -1,11 +0,0 @@
from .base import LlamaCloudIndex
from .retriever import LlamaCloudRetriever
from .composite_retriever import (
LlamaCloudCompositeRetriever,
)
__all__ = [
"LlamaCloudIndex",
"LlamaCloudRetriever",
"LlamaCloudCompositeRetriever",
]
-422
View File
@@ -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
)
-217
View File
@@ -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
-29
View File
@@ -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.53"
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.53"]
[project.scripts]
llama-parse = "llama_parse.cli.main:parse"
[tool.hatch.build.targets.sdist]
include = ["llama_parse"]
[tool.hatch.build.targets.wheel]
include = ["llama_parse"]
-2844
View File
File diff suppressed because it is too large Load Diff
-48
View File
@@ -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.53"
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,421 +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,
"citations": [{"page_number": 1, "matching_text": "John Smith"}],
},
"age": {
"confidence": 0.87,
"citations": [
{
"page_number": 2.0, # Float page number
"matching_text": "25 years old",
}
],
},
"email": {
"confidence": 0.92,
"citations": [], # 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 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,
"citations": [{"page_number": 1, "matching_text": "John Doe"}],
},
"age": {"confidence": 0.87},
"email": {
"confidence": 0.92,
"citations": [{"page_number": 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
-538
View File
@@ -1,538 +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.indices.managed.base import BaseManagedIndex
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")
@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
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")
@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
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)
-62
View File
@@ -1,62 +0,0 @@
import pytest
from pathlib import Path
from llama_cloud_services.parse.utils import (
expand_target_pages,
partition_pages,
extract_tables_from_json_results,
)
from typing import List
@pytest.fixture()
def pseudo_json_results() -> List[dict]:
return [
{
"pages": [
{
"items": [
{
"type": "table",
"csv": "Name,Age,Height (cm)\nAnna,12,140\nBob,22,175\nClaire,33,173\nDenis,44,185\n",
}
]
}
]
}
]
def test_expand_target_pages() -> None:
with pytest.raises(ValueError):
list(expand_target_pages("x"))
with pytest.raises(ValueError):
list(expand_target_pages("1-2-3"))
with pytest.raises(ValueError):
list(expand_target_pages("2-1"))
result = list(expand_target_pages("0,2-3,5,8-10"))
assert result == [0, 2, 3, 5, 8, 9, 10]
def test_partion_pages() -> None:
pages = [0, 2, 3, 5, 8, 9, 10]
with pytest.raises(ValueError):
list(partition_pages(pages, 0))
result = list(partition_pages(pages, 3))
assert result == ["0,2-3", "5,8-9", "10"]
with pytest.raises(ValueError):
list(partition_pages(pages, 3, 0))
result = list(partition_pages(pages, 3, max_pages=5))
assert result == ["0,2-3", "5,8"]
result = list(partition_pages(pages, 3, max_pages=10))
assert result == ["0,2-3", "5,8-9", "10"]
def test_table_extraction(pseudo_json_results: List[dict], tmpdir: Path) -> None:
tables = extract_tables_from_json_results(pseudo_json_results, tmpdir)
assert len(tables) == 1
for table in tables:
assert Path(table).exists()
with open(table) as t:
assert t.read() == pseudo_json_results[0]["pages"][0]["items"][0]["csv"]
Binary file not shown.
Binary file not shown.
Generated
-4096
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -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.44"
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.33"
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"
-147
View File
@@ -1,147 +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("pyproject.toml").read_text()
main_doc = tomlkit.parse(main_content)
main_version = main_doc["tool"]["poetry"]["version"]
# Read llama_parse/pyproject.toml
llama_parse_content = Path("llama_parse/pyproject.toml").read_text()
llama_parse_doc = tomlkit.parse(llama_parse_content)
llama_parse_version = llama_parse_doc["tool"]["poetry"]["version"]
dependency_version = llama_parse_doc["tool"]["poetry"]["dependencies"][
"llama-cloud-services"
]
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("pyproject.toml").read_text()
main_doc = tomlkit.parse(main_content)
main_doc["tool"]["poetry"]["version"] = version
Path("pyproject.toml").write_text(tomlkit.dumps(main_doc))
# Update llama_parse/pyproject.toml
llama_parse_content = Path("llama_parse/pyproject.toml").read_text()
llama_parse_doc = tomlkit.parse(llama_parse_content)
llama_parse_doc["tool"]["poetry"]["version"] = version
llama_parse_doc["tool"]["poetry"]["dependencies"][
"llama-cloud-services"
] = f">={version}"
Path("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_and_push_tag(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}"
# Create tag
subprocess.run(["git", "tag", tag_name], check=True)
click.echo(f"Created tag {tag_name}")
# Push tag
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)"
)
def tag(version: str | None = None) -> None:
"""Create and push a git tag for the current version."""
if not version:
main_version, _, _ = get_current_versions()
version = main_version
create_and_push_tag(version)
if __name__ == "__main__":
cli()
@@ -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"""
@@ -67,7 +67,7 @@ async def test_agent_data_crud_operations():
agent_data_client = AsyncAgentDataClient(
client=client,
type=TestData,
collection=f"test-collection-{test_id[:8]}",
collection_name=f"test-collection-{test_id[:8]}",
agent_url_id=LLAMA_DEPLOY_DEPLOYMENT_NAME,
)
@@ -77,21 +77,21 @@ async def test_agent_data_crud_operations():
created_item = None
try:
# Test CREATE
created_item = await agent_data_client.create_item(test_data)
created_item = await agent_data_client.create_agent_data(test_data)
assert created_item.data.name == "test-item"
assert created_item.data.test_id == test_id
assert created_item.data.value == 42
assert created_item.id is not None
# Test READ
retrieved_item = await agent_data_client.get_item(created_item.id)
retrieved_item = await agent_data_client.get_agent_data(created_item.id)
assert retrieved_item.id == created_item.id
assert retrieved_item.data.name == "test-item"
assert retrieved_item.data.test_id == test_id
assert retrieved_item.data.value == 42
# Test SEARCH
search_results = await agent_data_client.search(
search_results = await agent_data_client.search_agent_data(
filter={"test_id": {"eq": test_id}}, page_size=10, include_total=True
)
assert len(search_results.items) == 1
@@ -99,7 +99,7 @@ async def test_agent_data_crud_operations():
assert search_results.total == 1
# Test AGGREGATE
aggregate_results = await agent_data_client.aggregate(
aggregate_results = await agent_data_client.aggregate_agent_data(
group_by=["test_id"], count=True
)
assert len(aggregate_results.items) == 1
@@ -108,7 +108,7 @@ async def test_agent_data_crud_operations():
# Test UPDATE
updated_data = TestData(name="updated-item", test_id=test_id, value=84)
updated_item = await agent_data_client.update_item(
updated_item = await agent_data_client.update_agent_data(
created_item.id, updated_data
)
assert updated_item.data.name == "updated-item"
@@ -116,7 +116,7 @@ async def test_agent_data_crud_operations():
assert updated_item.id == created_item.id
# Verify update persisted
verified_item = await agent_data_client.get_item(created_item.id)
verified_item = await agent_data_client.get_agent_data(created_item.id)
assert verified_item.data.name == "updated-item"
assert verified_item.data.value == 84
@@ -124,6 +124,6 @@ async def test_agent_data_crud_operations():
# Clean up test data
if created_item is not None:
try:
await agent_data_client.delete_item(created_item.id)
await agent_data_client.delete_agent_data(created_item.id)
except Exception as e:
print(f"Warning: Failed to cleanup test data {created_item.id}: {e}")
@@ -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 == "in_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