Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3080d8f2b | |||
| 8fec891cbe | |||
| 99f4eba8d0 | |||
| 54561e2dd2 | |||
| bfaec79a8f | |||
| 3e0e522a6b | |||
| f70b6d87ec | |||
| 693b5b83b1 | |||
| ad38ef5cd7 | |||
| 4c4c6e6575 | |||
| 740b47d9dc | |||
| f3233deb2e | |||
| fd45127678 | |||
| 0506c88735 | |||
| 4bc9eb6c0d | |||
| 5a3dac655c | |||
| 519254efbe | |||
| 6ab56b79f3 | |||
| e020e3e2b1 | |||
| f293547910 | |||
| 662bc37462 | |||
| 9f1ef4ef1f | |||
| 1243573924 | |||
| 407292b177 | |||
| a7df7c0912 | |||
| c758144bfe | |||
| fee516dd19 | |||
| 032fbd5768 | |||
| 970e864514 | |||
| d0649ece6e | |||
| 5d4cabd843 | |||
| 9070a6ac16 | |||
| 4f24f537f6 | |||
| 8859a203e2 | |||
| b091364054 | |||
| 43b1a013ca | |||
| f81532e7f2 | |||
| 986d3987d3 | |||
| 1bf522311f | |||
| 24166dcfc8 | |||
| bfb7f3973f | |||
| 979f643c77 | |||
| aefd89cf1b | |||
| 8ea2b2c64e | |||
| 4a9a2a21d8 | |||
| e6a7939206 | |||
| 104a03e829 | |||
| 6e0f2f4ca0 | |||
| 0708d11f8a | |||
| be19185503 | |||
| 7571b0d6c4 | |||
| ad6734bf80 | |||
| 9ec2a8322e | |||
| 51011b9f30 | |||
| 09805f9e15 | |||
| 8ced6f6eab | |||
| 081ddeca34 | |||
| 2460908789 | |||
| c226d6a54c | |||
| 5d4c682eb2 | |||
| f72d3535c8 | |||
| 1ea09a366e | |||
| d4bbeb6389 | |||
| d028397603 | |||
| 35ea8476db | |||
| 3e5f7c4f1e | |||
| 9d9b816644 | |||
| 83555f76e6 | |||
| 5edf5f914a | |||
| 22e4975cb2 | |||
| bc2f04379b | |||
| f9f951d5d8 | |||
| 355129fea5 | |||
| d9aed80ded | |||
| c07d2d70a8 | |||
| ed6937a5a9 | |||
| 34c15932a3 | |||
| b18ea96d11 | |||
| 196ab827f5 |
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: ${{ env.UV_VERSION }}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: "ts/llama_cloud_services/.nvmrc"
|
||||
|
||||
|
||||
@@ -30,12 +30,12 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: python
|
||||
dependency-caching: true
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && 2 || 0 }}
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: ${{ env.UV_VERSION }}
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: "ts/llama_cloud_services/.nvmrc"
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
name: Publish Release - Python
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
UV_VERSION: "0.7.20"
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish to PyPI
|
||||
if: github.repository == 'run-llama/llama_cloud_services'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
version: ${{ env.UV_VERSION }}
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install
|
||||
|
||||
- name: Display Python version
|
||||
run: python --version
|
||||
|
||||
- name: Build
|
||||
working-directory: py
|
||||
run: uv build
|
||||
|
||||
- name: Test installing built package
|
||||
shell: bash
|
||||
working-directory: py
|
||||
run: |
|
||||
uv venv
|
||||
uv pip install dist/*.whl
|
||||
|
||||
- name: Publish package
|
||||
shell: bash
|
||||
working-directory: py
|
||||
run: uv publish --token ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
|
||||
|
||||
- name: Build and publish llama-parse
|
||||
working-directory: py/llama_parse/
|
||||
run: |
|
||||
uv build
|
||||
uv publish --token ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }} - LlamaCloud Services PY
|
||||
artifacts: "py/**/dist/*"
|
||||
generateReleaseNotes: true
|
||||
draft: false
|
||||
prerelease: false
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Publish Release - TypeScript
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "llama-cloud-services@*"
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: "ts/llama_cloud_services/.nvmrc"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Run Build
|
||||
working-directory: ts/llama_cloud_services/
|
||||
run: pnpm build
|
||||
|
||||
- name: Build tarball
|
||||
run: |
|
||||
pnpm pack
|
||||
working-directory: ts/llama_cloud_services
|
||||
|
||||
- name: Setup npm authentication
|
||||
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Release
|
||||
working-directory: ts/llama_cloud_services
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: pnpm publish --access public --no-git-checks
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "ts/llama_cloud_services/llama-cloud-services*.tgz"
|
||||
name: Release ${{ github.ref_name }} - LlamaCloud Services TS
|
||||
generateReleaseNotes: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -12,6 +12,7 @@ env:
|
||||
jobs:
|
||||
test_e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
# You can use PyPy versions in python-version.
|
||||
# For example, pypy-2.7 and pypy-3.8
|
||||
@@ -22,7 +23,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: ${{ env.UV_VERSION }}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: ${{ env.UV_VERSION }}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: "ts/llama_cloud_services/.nvmrc"
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Version Bump and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
# Only run on main branch pushes
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Add auth token to .npmrc file
|
||||
run: |
|
||||
cat << EOF >> ".npmrc"
|
||||
//registry.npmjs.org/:_authToken=$NPM_TOKEN
|
||||
EOF
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Create Release Pull Request or Publish packages
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
commit: "chore: version packages"
|
||||
title: "chore: version packages"
|
||||
# Custom version script
|
||||
version: pnpm -w run version
|
||||
# Custom publish script
|
||||
publish: pnpm -w run publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
LLAMA_PARSE_PYPI_TOKEN: ${{ secrets.LLAMA_PARSE_PYPI_TOKEN }}
|
||||
@@ -9,3 +9,4 @@ __pycache__/
|
||||
node_modules/
|
||||
.turbo/
|
||||
dist/
|
||||
.npmrc
|
||||
|
||||
@@ -29,12 +29,12 @@ repos:
|
||||
- id: black-jupyter
|
||||
name: black-src
|
||||
alias: black
|
||||
exclude: ".*uv.lock"
|
||||
exclude: ".*uv.lock|examples/extract/solar_panel_e2e_comparison.ipynb"
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.0.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: ^py/tests|^py/unit_tests
|
||||
exclude: ^py/tests|^py/unit_tests|^examples
|
||||
additional_dependencies:
|
||||
[
|
||||
"types-requests",
|
||||
|
||||
@@ -9,7 +9,6 @@ This repository contains the code for hand-written SDKs and clients for interact
|
||||
This includes:
|
||||
|
||||
- [LlamaParse](./parse.md) - A GenAI-native document parser that can parse complex document data for any downstream LLM use case (Agents, RAG, data processing, etc.).
|
||||
- [LlamaReport (beta/invite-only)](./report.md) - A prebuilt agentic report builder that can be used to build reports from a variety of data sources.
|
||||
- [LlamaExtract](./extract.md) - A prebuilt agentic data extractor that can be used to transform data into a structured JSON representation.
|
||||
- [LlamaCloud Index](./index.md) - A widely customizable and fully automated document ingestion pipeline that also serves retrieval purposes.
|
||||
|
||||
@@ -28,13 +27,11 @@ Then, you can use the services in your code:
|
||||
```python
|
||||
from llama_cloud_services import (
|
||||
LlamaParse,
|
||||
LlamaReport,
|
||||
LlamaExtract,
|
||||
LlamaCloudIndex,
|
||||
)
|
||||
|
||||
parser = LlamaParse(api_key="YOUR_API_KEY")
|
||||
report = LlamaReport(api_key="YOUR_API_KEY")
|
||||
extract = LlamaExtract(api_key="YOUR_API_KEY")
|
||||
index = LlamaCloudIndex(
|
||||
"my_first_index", project_name="default", api_key="YOUR_API_KEY"
|
||||
@@ -44,7 +41,6 @@ index = LlamaCloudIndex(
|
||||
See the quickstart guides for each service for more information:
|
||||
|
||||
- [LlamaParse](./parse.md)
|
||||
- [LlamaReport (beta/invite-only)](./report.md)
|
||||
- [LlamaExtract](./extract.md)
|
||||
- [LlamaCloud Index](./index.md)
|
||||
|
||||
@@ -57,13 +53,11 @@ You can also create your API key in the EU region [here](https://cloud.eu.llamai
|
||||
```python
|
||||
from llama_cloud_services import (
|
||||
LlamaParse,
|
||||
LlamaReport,
|
||||
LlamaExtract,
|
||||
EU_BASE_URL,
|
||||
)
|
||||
|
||||
parser = LlamaParse(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
|
||||
report = LlamaReport(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
|
||||
extract = LlamaExtract(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
|
||||
index = LlamaCloudIndex(
|
||||
"my_first_index",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
node_modules
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
.DS_Store
|
||||
.cache
|
||||
.env
|
||||
.vercel
|
||||
.output
|
||||
.nitro
|
||||
/build/
|
||||
/api/
|
||||
/server/build
|
||||
/public/build# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
.tanstack
|
||||
.vscode
|
||||
@@ -0,0 +1,4 @@
|
||||
**/build
|
||||
**/public
|
||||
pnpm-lock.yaml
|
||||
routeTree.gen.ts
|
||||
@@ -0,0 +1,88 @@
|
||||
# LlamaClassify Demo
|
||||
|
||||
A TypeScript demo application showcasing the power of **LlamaClassify** - an agentic documents classification service from [LlamaCloud](https://cloud.llamaindex.ai). This demo allows you to classify financial documents among three different types (Cash flow statement, Income Statement and Balance Sheet).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Start the Demo](#start-the-demo)
|
||||
- [How It Works](#how-it-works)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Common Issues](#common-issues)
|
||||
- [License](#license)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Features
|
||||
|
||||
- 📄 **Documemt Classification**: Classify files based on well-defined rules you can customized and play around with.
|
||||
- 🤖 **Reasoning-based Actionable Insights**: Get in-depth, reasoning based insights on the document classification, accompanied by confidence scores.
|
||||
- 🎨 **Beautiful UI**: [DaisyUI](https://daisyui.com)-based interface powered by [TanStack](https://tanstack.com)
|
||||
- ⚡ **Fast Development**: Hot reload support with development mode
|
||||
- 🛠️ **TypeScript**: Full TypeScript support with strict type checking
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (version 22 or higher)
|
||||
- pnpm package manager
|
||||
- LlamaCloud API key
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/run-llama/llama_cloud_services
|
||||
cd lama_cloud_services/examples-ts/classify/
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Set up your environment variables:
|
||||
|
||||
```bash
|
||||
# Add your API key to your environment
|
||||
export LLAMA_CLOUD_API_KEY="your-llamacloud-api-key"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Start the Demo
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The application will be up and running on http://localhost:3000
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Document Input**: Enter the path to your document when prompted
|
||||
2. **Parsing**: LlamaClassify, based on the rules you can find [here](./src/utils/classifier.ts), processes the document and classifies it
|
||||
3. **Results**: The classification outcome, as well as the reasoning behind it and the confidence score, are displayed in the UI.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Module Resolution Errors**: Ensure you're using Node.js 22+ and have all dependencies installed
|
||||
2. **API Key Issues**: Verify your LlamaCloud API key is correctly set
|
||||
3. **File Path Errors**: Use absolute paths or ensure relative paths are correct from the project root
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see the [LICENSE](../../LICENSE) file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Run `npm run format` and `npm run lint`
|
||||
5. Submit a pull request
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "tanstack-start-example-basic",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build && tsc --noEmit",
|
||||
"start": "node .output/server/index.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-router": "^1.133.22",
|
||||
"@tanstack/react-router-devtools": "^1.133.22",
|
||||
"@tanstack/react-start": "^1.133.22",
|
||||
"llama-cloud-services": "file:../../ts/llama_cloud_services",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.15",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"daisyui": "^5.3.7",
|
||||
"postcss": "^8.5.1",
|
||||
"tailwindcss": "^4.1.15",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^7.1.7",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
ErrorComponent,
|
||||
Link,
|
||||
rootRouteId,
|
||||
useMatch,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router'
|
||||
import type { ErrorComponentProps } from '@tanstack/react-router'
|
||||
|
||||
export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
||||
const router = useRouter()
|
||||
const isRoot = useMatch({
|
||||
strict: false,
|
||||
select: (state) => state.id === rootRouteId,
|
||||
})
|
||||
|
||||
console.error('DefaultCatchBoundary Error:', error)
|
||||
|
||||
return (
|
||||
<div className="min-w-0 flex-1 p-4 flex flex-col items-center justify-center gap-6">
|
||||
<ErrorComponent error={error} />
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
<button
|
||||
onClick={() => {
|
||||
router.invalidate()
|
||||
}}
|
||||
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
{isRoot ? (
|
||||
<Link
|
||||
to="/"
|
||||
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to="/"
|
||||
className={`px-2 py-1 bg-gray-600 dark:bg-gray-700 rounded-sm text-white uppercase font-extrabold`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
window.history.back()
|
||||
}}
|
||||
>
|
||||
Go Back
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
|
||||
export function NotFound({ children }: { children?: any }) {
|
||||
return (
|
||||
<div className="space-y-2 p-2">
|
||||
<div className="text-gray-600 dark:text-gray-400">
|
||||
{children || <p>The page you are looking for does not exist.</p>}
|
||||
</div>
|
||||
<p className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
className="bg-emerald-500 text-white px-2 py-1 rounded-sm uppercase font-black text-sm"
|
||||
>
|
||||
Go back
|
||||
</button>
|
||||
<Link
|
||||
to="/"
|
||||
className="bg-cyan-600 text-white px-2 py-1 rounded-sm uppercase font-black text-sm"
|
||||
>
|
||||
Start Over
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as UsersRouteImport } from './routes/users'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as UsersIndexRouteImport } from './routes/users.index'
|
||||
import { Route as PostsIndexRouteImport } from './routes/posts.index'
|
||||
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
|
||||
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
|
||||
import { Route as ApiClassifyRouteImport } from './routes/api/classify'
|
||||
import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
|
||||
|
||||
const UsersRoute = UsersRouteImport.update({
|
||||
id: '/users',
|
||||
path: '/users',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const UsersIndexRoute = UsersIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => UsersRoute,
|
||||
} as any)
|
||||
const PostsIndexRoute = PostsIndexRouteImport.update({
|
||||
id: '/posts/',
|
||||
path: '/posts/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const UsersUserIdRoute = UsersUserIdRouteImport.update({
|
||||
id: '/$userId',
|
||||
path: '/$userId',
|
||||
getParentRoute: () => UsersRoute,
|
||||
} as any)
|
||||
const PostsPostIdRoute = PostsPostIdRouteImport.update({
|
||||
id: '/posts/$postId',
|
||||
path: '/posts/$postId',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ApiClassifyRoute = ApiClassifyRouteImport.update({
|
||||
id: '/api/classify',
|
||||
path: '/api/classify',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PostsPostIdDeepRoute = PostsPostIdDeepRouteImport.update({
|
||||
id: '/posts_/$postId/deep',
|
||||
path: '/posts/$postId/deep',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/users': typeof UsersRouteWithChildren
|
||||
'/api/classify': typeof ApiClassifyRoute
|
||||
'/posts/$postId': typeof PostsPostIdRoute
|
||||
'/users/$userId': typeof UsersUserIdRoute
|
||||
'/posts': typeof PostsIndexRoute
|
||||
'/users/': typeof UsersIndexRoute
|
||||
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/api/classify': typeof ApiClassifyRoute
|
||||
'/posts/$postId': typeof PostsPostIdRoute
|
||||
'/users/$userId': typeof UsersUserIdRoute
|
||||
'/posts': typeof PostsIndexRoute
|
||||
'/users': typeof UsersIndexRoute
|
||||
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/users': typeof UsersRouteWithChildren
|
||||
'/api/classify': typeof ApiClassifyRoute
|
||||
'/posts/$postId': typeof PostsPostIdRoute
|
||||
'/users/$userId': typeof UsersUserIdRoute
|
||||
'/posts/': typeof PostsIndexRoute
|
||||
'/users/': typeof UsersIndexRoute
|
||||
'/posts_/$postId/deep': typeof PostsPostIdDeepRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/users'
|
||||
| '/api/classify'
|
||||
| '/posts/$postId'
|
||||
| '/users/$userId'
|
||||
| '/posts'
|
||||
| '/users/'
|
||||
| '/posts/$postId/deep'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/api/classify'
|
||||
| '/posts/$postId'
|
||||
| '/users/$userId'
|
||||
| '/posts'
|
||||
| '/users'
|
||||
| '/posts/$postId/deep'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/users'
|
||||
| '/api/classify'
|
||||
| '/posts/$postId'
|
||||
| '/users/$userId'
|
||||
| '/posts/'
|
||||
| '/users/'
|
||||
| '/posts_/$postId/deep'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
UsersRoute: typeof UsersRouteWithChildren
|
||||
ApiClassifyRoute: typeof ApiClassifyRoute
|
||||
PostsPostIdRoute: typeof PostsPostIdRoute
|
||||
PostsIndexRoute: typeof PostsIndexRoute
|
||||
PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/users': {
|
||||
id: '/users'
|
||||
path: '/users'
|
||||
fullPath: '/users'
|
||||
preLoaderRoute: typeof UsersRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/users/': {
|
||||
id: '/users/'
|
||||
path: '/'
|
||||
fullPath: '/users/'
|
||||
preLoaderRoute: typeof UsersIndexRouteImport
|
||||
parentRoute: typeof UsersRoute
|
||||
}
|
||||
'/posts/': {
|
||||
id: '/posts/'
|
||||
path: '/posts'
|
||||
fullPath: '/posts'
|
||||
preLoaderRoute: typeof PostsIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/users/$userId': {
|
||||
id: '/users/$userId'
|
||||
path: '/$userId'
|
||||
fullPath: '/users/$userId'
|
||||
preLoaderRoute: typeof UsersUserIdRouteImport
|
||||
parentRoute: typeof UsersRoute
|
||||
}
|
||||
'/posts/$postId': {
|
||||
id: '/posts/$postId'
|
||||
path: '/posts/$postId'
|
||||
fullPath: '/posts/$postId'
|
||||
preLoaderRoute: typeof PostsPostIdRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/api/classify': {
|
||||
id: '/api/classify'
|
||||
path: '/api/classify'
|
||||
fullPath: '/api/classify'
|
||||
preLoaderRoute: typeof ApiClassifyRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/posts_/$postId/deep': {
|
||||
id: '/posts_/$postId/deep'
|
||||
path: '/posts/$postId/deep'
|
||||
fullPath: '/posts/$postId/deep'
|
||||
preLoaderRoute: typeof PostsPostIdDeepRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface UsersRouteChildren {
|
||||
UsersUserIdRoute: typeof UsersUserIdRoute
|
||||
UsersIndexRoute: typeof UsersIndexRoute
|
||||
}
|
||||
|
||||
const UsersRouteChildren: UsersRouteChildren = {
|
||||
UsersUserIdRoute: UsersUserIdRoute,
|
||||
UsersIndexRoute: UsersIndexRoute,
|
||||
}
|
||||
|
||||
const UsersRouteWithChildren = UsersRoute._addFileChildren(UsersRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
UsersRoute: UsersRouteWithChildren,
|
||||
ApiClassifyRoute: ApiClassifyRoute,
|
||||
PostsPostIdRoute: PostsPostIdRoute,
|
||||
PostsIndexRoute: PostsIndexRoute,
|
||||
PostsPostIdDeepRoute: PostsPostIdDeepRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createRouter } from '@tanstack/react-router'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
|
||||
import { NotFound } from './components/NotFound'
|
||||
|
||||
export function getRouter() {
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
defaultPreload: 'intent',
|
||||
defaultErrorComponent: DefaultCatchBoundary,
|
||||
defaultNotFoundComponent: () => <NotFound />,
|
||||
scrollRestoration: true,
|
||||
})
|
||||
return router
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/// <reference types="vite/client" />
|
||||
import {
|
||||
HeadContent,
|
||||
Scripts,
|
||||
createRootRoute,
|
||||
} from '@tanstack/react-router'
|
||||
import * as React from 'react'
|
||||
import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary'
|
||||
import { NotFound } from '~/components/NotFound'
|
||||
import { seo } from '~/utils/seo'
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
charSet: 'utf-8',
|
||||
},
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1',
|
||||
},
|
||||
...seo({
|
||||
title:
|
||||
'Financial Documents Classification Agent',
|
||||
description: `Classify financial documents as balance sheets, income statements and cash flow statemets. `,
|
||||
}),
|
||||
],
|
||||
links: [
|
||||
{ rel: 'stylesheet', href: "https://cdn.jsdelivr.net/npm/daisyui@5" },
|
||||
{
|
||||
rel: 'apple-touch-icon',
|
||||
sizes: '180x180',
|
||||
href: '/apple-touch-icon.png',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '32x32',
|
||||
href: '/favicon-32x32.png',
|
||||
},
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
sizes: '16x16',
|
||||
href: '/favicon-16x16.png',
|
||||
},
|
||||
{ rel: 'manifest', href: '/site.webmanifest', color: '#fffff' },
|
||||
{ rel: 'icon', href: '/favicon.ico' },
|
||||
],
|
||||
scripts: [
|
||||
{
|
||||
src: '/customScript.js',
|
||||
type: 'text/javascript',
|
||||
},
|
||||
{
|
||||
src: "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4",
|
||||
type: "text/javascript",
|
||||
}
|
||||
],
|
||||
}),
|
||||
errorComponent: DefaultCatchBoundary,
|
||||
notFoundComponent: () => <NotFound />,
|
||||
shellComponent: RootDocument,
|
||||
})
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<div className="navbar bg-base-100 shadow-sm">
|
||||
<div className="navbar-start">
|
||||
<div className="dropdown">
|
||||
<div tabIndex={0} role="button" className="btn btn-ghost btn-circle">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M4 6h16M4 12h16M4 18h7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="menu menu-lg dropdown-content bg-base-100 rounded-box z-1 mt-3 w-80 p-2 shadow"
|
||||
>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="https://cloud.llamaindex.ai">Get Started with LlamaCloud</a></li>
|
||||
<li><a href="https://developers.llamaindex.ai/python/cloud/llamaclassify/getting_started/">LlamaClassify Docs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="navbar-center">
|
||||
<a className="btn btn-ghost text-xl" href="/">Financial Documents Classification Agent</a>
|
||||
</div>
|
||||
<div className="navbar-end">
|
||||
<a href="https://github.com/run-llama/llama_cloud_services/main/blob/examples-ts/classify">
|
||||
<button className="btn btn-ghost btn-circle">
|
||||
<div className="indicator">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-10 w-10"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 640 512"
|
||||
>
|
||||
<path d="M237.9 461.4C237.9 463.4 235.6 465 232.7 465C229.4 465.3 227.1 463.7 227.1 461.4C227.1 459.4 229.4 457.8 232.3 457.8C235.3 457.5 237.9 459.1 237.9 461.4zM206.8 456.9C206.1 458.9 208.1 461.2 211.1 461.8C213.7 462.8 216.7 461.8 217.3 459.8C217.9 457.8 216 455.5 213 454.6C210.4 453.9 207.5 454.9 206.8 456.9zM251 455.2C248.1 455.9 246.1 457.8 246.4 460.1C246.7 462.1 249.3 463.4 252.3 462.7C255.2 462 257.2 460.1 256.9 458.1C256.6 456.2 253.9 454.9 251 455.2zM316.8 72C178.1 72 72 177.3 72 316C72 426.9 141.8 521.8 241.5 555.2C254.3 557.5 258.8 549.6 258.8 543.1C258.8 536.9 258.5 502.7 258.5 481.7C258.5 481.7 188.5 496.7 173.8 451.9C173.8 451.9 162.4 422.8 146 415.3C146 415.3 123.1 399.6 147.6 399.9C147.6 399.9 172.5 401.9 186.2 425.7C208.1 464.3 244.8 453.2 259.1 446.6C261.4 430.6 267.9 419.5 275.1 412.9C219.2 406.7 162.8 398.6 162.8 302.4C162.8 274.9 170.4 261.1 186.4 243.5C183.8 237 175.3 210.2 189 175.6C209.9 169.1 258 202.6 258 202.6C278 197 299.5 194.1 320.8 194.1C342.1 194.1 363.6 197 383.6 202.6C383.6 202.6 431.7 169 452.6 175.6C466.3 210.3 457.8 237 455.2 243.5C471.2 261.2 481 275 481 302.4C481 398.9 422.1 406.6 366.2 412.9C375.4 420.8 383.2 435.8 383.2 459.3C383.2 493 382.9 534.7 382.9 542.9C382.9 549.4 387.5 557.3 400.2 555C500.2 521.8 568 426.9 568 316C568 177.3 455.5 72 316.8 72zM169.2 416.9C167.9 417.9 168.2 420.2 169.9 422.1C171.5 423.7 173.8 424.4 175.1 423.1C176.4 422.1 176.1 419.8 174.4 417.9C172.8 416.3 170.5 415.6 169.2 416.9zM158.4 408.8C157.7 410.1 158.7 411.7 160.7 412.7C162.3 413.7 164.3 413.4 165 412C165.7 410.7 164.7 409.1 162.7 408.1C160.7 407.5 159.1 407.8 158.4 408.8zM190.8 444.4C189.2 445.7 189.8 448.7 192.1 450.6C194.4 452.9 197.3 453.2 198.6 451.6C199.9 450.3 199.3 447.3 197.3 445.4C195.1 443.1 192.1 442.8 190.8 444.4zM179.4 429.7C177.8 430.7 177.8 433.3 179.4 435.6C181 437.9 183.7 438.9 185 437.9C186.6 436.6 186.6 434 185 431.7C183.6 429.4 181 428.4 179.4 429.7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { classifier, classificationRules, parsingConfig } from '~/utils/classifier'
|
||||
|
||||
export const Route = createFileRoute('/api/classify')({
|
||||
component: RouteComponent,
|
||||
server: {
|
||||
handlers: {
|
||||
POST: async ({ request }) => {
|
||||
const body = await request.formData()
|
||||
const fl = body.get("file") as File;
|
||||
if (!fl) {
|
||||
return new Response(JSON.stringify({"result": "you need to provide a file"}))
|
||||
}
|
||||
const buff = await fl.arrayBuffer()
|
||||
const rawRes = await classifier.classify(
|
||||
classificationRules,
|
||||
parsingConfig,
|
||||
{ fileContents: [new Uint8Array(buff)] },
|
||||
)
|
||||
const results = rawRes.items
|
||||
let classification = ""
|
||||
|
||||
for (const result of results) {
|
||||
if ("result" in result && result.result) {
|
||||
classification += `
|
||||
<div class="card bg-base-100 shadow-xl p-6 mb-4">
|
||||
<div class="space-y-3">
|
||||
<p><span class="font-semibold">📄 Document:</span> ${fl.name}</p>
|
||||
<p><span class="font-semibold">🏷️ Type:</span> <span class="badge badge-primary">${result.result.type}</span></p>
|
||||
<p><span class="font-semibold">📊 Confidence:</span> ${result.result.confidence*100}%</p>
|
||||
<p><span class="font-semibold">💭 Reasoning:</span> ${result.result.reasoning}</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
return new Response(JSON.stringify({"result": classification}))
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: Home,
|
||||
})
|
||||
|
||||
function Home() {
|
||||
const [file, setFile] = useState<null | File>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [reply, setReply] = useState<null | string>(null)
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = event.target.files?.[0]
|
||||
if (selectedFile) {
|
||||
setFile(selectedFile)
|
||||
}
|
||||
}
|
||||
const handleClearFile = () => {
|
||||
if (file) {
|
||||
setFile(null)
|
||||
}
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = ''
|
||||
}
|
||||
if (reply) {
|
||||
setReply(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClassify = async () => {
|
||||
if (!file) return
|
||||
|
||||
if (reply) {
|
||||
setReply(null)
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const res = await fetch('/api/classify', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
setReply(data.result)
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center gap-y-8">
|
||||
<br />
|
||||
<h1 className="text-xl font-bold text-gray-700">AI-Powered finacial document classification</h1>
|
||||
<h2 className="text-lg font-semibold text-gray-500">Need help sorting out the financial documents jungle? Let our classification agent handle it!</h2>
|
||||
<fieldset className="fieldset bg-base-100 border-base-300 rounded-box w-200 border p-4">
|
||||
<legend className="fieldset-legend text-lg">Upload your financial document here</legend>
|
||||
<label className="label flex justify-center">
|
||||
<input type="file" className="file-input" onChange={handleFileChange} accept='application/pdf' ref={fileInputRef} />
|
||||
</label>
|
||||
</fieldset>
|
||||
{file && (
|
||||
<div className="flex flex-col justify-center items-center gap-y-8">
|
||||
<p className="text-sm text-gray-600">Selected file: {file.name}</p>
|
||||
<div className='grid grid-cols-2 gap-x-6'>
|
||||
<button
|
||||
type="button"
|
||||
className='btn bg-gray-500 text-white shadow-lg hover:bg-gray-600 hover:shadow-xl rounded'
|
||||
onClick={handleClassify}
|
||||
>
|
||||
Classify
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClearFile}
|
||||
type="button"
|
||||
className="px-4 py-2 bg-red-300 text-black rounded hover:bg-red-400 hover:shadow-xl shadow-lg"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{loading && (
|
||||
<span className="loading loading-spinner text-primary"></span>
|
||||
)}
|
||||
{reply && (
|
||||
<div
|
||||
className="max-w-2xl w-full"
|
||||
dangerouslySetInnerHTML={{ __html: reply }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { LlamaClassify, ClassifierRule, ClassifyParsingConfiguration } from "llama-cloud-services"
|
||||
|
||||
export const classifier = new LlamaClassify(process.env.LLAMA_CLOUD_API_KEY);
|
||||
|
||||
export const classificationRules: ClassifierRule[] = [
|
||||
{
|
||||
description: "Shows a company's assets, liabilities, and shareholders' equity at a specific point in time, providing a snapshot of financial position.",
|
||||
type: "balance_sheet"
|
||||
},
|
||||
{
|
||||
description: "Reports cash inflows and outflows from operating, investing, and financing activities, highlighting liquidity and cash management.",
|
||||
type: "cash_flow_statement"
|
||||
},
|
||||
{
|
||||
description: "Summarizes revenues, expenses, and profits over a period, indicating financial performance and profitability.",
|
||||
type: "income_statement"
|
||||
},
|
||||
];
|
||||
|
||||
export const parsingConfig: ClassifyParsingConfiguration = {
|
||||
lang: "en",
|
||||
max_pages: 20,
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export const seo = ({
|
||||
title,
|
||||
description,
|
||||
keywords,
|
||||
image,
|
||||
}: {
|
||||
title: string
|
||||
description?: string
|
||||
image?: string
|
||||
keywords?: string
|
||||
}) => {
|
||||
const tags = [
|
||||
{ title },
|
||||
{ name: 'description', content: description },
|
||||
{ name: 'keywords', content: keywords },
|
||||
{ name: 'twitter:title', content: title },
|
||||
{ name: 'twitter:description', content: description },
|
||||
{ name: 'twitter:creator', content: '@tannerlinsley' },
|
||||
{ name: 'twitter:site', content: '@tannerlinsley' },
|
||||
{ name: 'og:type', content: 'website' },
|
||||
{ name: 'og:title', content: title },
|
||||
{ name: 'og:description', content: description },
|
||||
...(image
|
||||
? [
|
||||
{ name: 'twitter:image', content: image },
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'og:image', content: image },
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
return tags
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "ES2022",
|
||||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
},
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import tsConfigPaths from 'vite-tsconfig-paths'
|
||||
import viteReact from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
plugins: [
|
||||
tsConfigPaths({
|
||||
projects: ['./tsconfig.json'],
|
||||
}),
|
||||
tanstackStart({
|
||||
srcDirectory: 'src',
|
||||
}),
|
||||
viteReact(),
|
||||
],
|
||||
})
|
||||
@@ -4,7 +4,6 @@ In this folder you will find several python notebooks that contain examples rega
|
||||
|
||||
- [LlamaParse](./parse/)
|
||||
- [LlamaExtract](./extract/)
|
||||
- [LlamaReport](./report/)
|
||||
- [LlamaCloudIndex](./index/)
|
||||
|
||||
Follow the instructions in each notebook to get started!
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"source": [
|
||||
"# Extraction and Analysis over a Fidelity Multi-Fund Annual Report\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services-demo/blob/main/examples/extract/asset_manager_fund_analysis.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/extract/asset_manager_fund_analysis.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"In this notebook we show you how to create an agentic document workflow over a complex document that contains annual reports for multiple funds - each fund reports financials in a standardized reporting structure, and it's all consolidated in the same document.\n",
|
||||
"\n",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"source": [
|
||||
"# Automotive Equity Research: A Multi-Step Agentic Workflow\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services-demo/blob/main/examples/extract/automotive_sector_analysis.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/extract/automotive_sector_analysis.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"This notebook demonstrates an end‑to‑end agentic workflow using LlamaExtract and the LlamaIndex event‑driven workflow framework for automotive sector analysis.\n",
|
||||
"\n",
|
||||
|
||||
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 769 KiB |
|
After Width: | Height: | Size: 942 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,508 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a7oq3cfnync",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Extracting Repeating Entities from Documents\n",
|
||||
"\n",
|
||||
"This notebook demonstrates how to use the `PER_TABLE_ROW` extraction target to extract structured data from documents containing repeating entities like tables, lists, or catalogs.\n",
|
||||
"\n",
|
||||
"## Why Use the Tabular Extraction Target?\n",
|
||||
"\n",
|
||||
"`PER_DOC` (refer to the table below for a quick overview of the different extraction targets) is the default extraction target in LlamaExtract, which looks at the entire document's context when doing an extraction. When extracting lists of entities, LLM-based extraction has a critical failure mode — it often **only extracts the first few tens of entries** from a long list. This happens because LLMs have limited attention spans for repetitive data. Document-level extraction doesn't guarantee exhaustive coverage, and long lists lead to incomplete extractions.\n",
|
||||
"\n",
|
||||
"**The Solution**: `PER_TABLE_ROW` solves this by processing each entity individually or in smaller batches, ensuring **exhaustive extraction** of all entries regardless of list length.\n",
|
||||
"\n",
|
||||
"### Entity-Level Extraction\n",
|
||||
"\n",
|
||||
"When using `extraction_target=ExtractTarget.PER_TABLE_ROW`, you define a schema for a **single entity** (e.g., one hospital, one product, one invoice line item), not the full document. LlamaExtract automatically:\n",
|
||||
"- Detects the formatting patterns that distinguish individual entities (table rows, list items, section headers, etc.)\n",
|
||||
"- Applies your schema to each identified entity\n",
|
||||
"- Returns a `list[YourSchema]` with one object per entity\n",
|
||||
"\n",
|
||||
"This approach is ideal when each entity locally contains all the information needed for your schema.\n",
|
||||
"\n",
|
||||
"### Choosing the Right Extraction Target\n",
|
||||
"\n",
|
||||
"| Extraction Target | Best For | Returns |\n",
|
||||
"|-------------------|----------|---------|\n",
|
||||
"| `PER_DOC` | Single-entity documents, summaries, or short lists | One JSON object for entire document |\n",
|
||||
"| `PER_PAGE` | Multi-page documents where each page is independent | One JSON object per page |\n",
|
||||
"| `PER_TABLE_ROW` | **Long lists, tables, catalogs with repeating entities** | List of JSON objects (one per entity) |\n",
|
||||
"\n",
|
||||
"📖 For more details, see the [Extraction Target documentation](https://developers.llamaindex.ai/python/cloud/llamaextract/features/concepts/#extraction-target)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "9427d1de",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from dotenv import load_dotenv\n",
|
||||
"from llama_cloud_services import LlamaExtract\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Load environment variables (put LLAMA_CLOUD_API_KEY in your .env file)\n",
|
||||
"load_dotenv(override=True)\n",
|
||||
"\n",
|
||||
"# Optionally, add your project id/organization id\n",
|
||||
"llama_extract = LlamaExtract()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4426b360",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Table of Hospitals by County and Insurance Plans\n",
|
||||
"\n",
|
||||
"We have a PDF document with a list of hospitals by county and different insurance plans offered by Blue Shield of California. \n",
|
||||
"\n",
|
||||
"\n",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c86sjymhn1r",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We want to extract each hospital from this table along with a list of applicable insurance plans. \n",
|
||||
"\n",
|
||||
"### Example 1: Structured Table\n",
|
||||
"\n",
|
||||
"This is an ideal use case for `PER_TABLE_ROW` extraction:\n",
|
||||
"- **Clear structure**: The document has explicit table formatting with rows and columns\n",
|
||||
"- **Repeating entities**: Each row represents one hospital with consistent attributes\n",
|
||||
"- **Local information**: All data for each hospital (county, name, plans) is contained within its row\n",
|
||||
"\n",
|
||||
"Notice that our `Hospital` schema describes a **single hospital**, not the full document. LlamaExtract will return a `list[Hospital]` with one entry per table row."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "7c61a802",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Hospital(BaseModel):\n",
|
||||
" \"\"\"List of hospitals by county available for different BSC plans\"\"\"\n",
|
||||
"\n",
|
||||
" county: str = Field(description=\"County name\")\n",
|
||||
" hospital_name: str = Field(description=\"Name of the hospital\")\n",
|
||||
" plan_names: list[str] = Field(\n",
|
||||
" description=\"List of plans available at the hospital. One of: Trio HMO, SaveNet, Access+ HMO, BlueHPN PPO, Tandem PPO, PPO\"\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "b8a69b7a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_cloud_services.extract import ExtractConfig, ExtractMode, ExtractTarget\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"result = await llama_extract.aextract(\n",
|
||||
" data_schema=Hospital,\n",
|
||||
" files=\"./data/tables/BSC-Hospital-List-by-County.pdf\",\n",
|
||||
" config=ExtractConfig(\n",
|
||||
" extraction_mode=ExtractMode.PREMIUM,\n",
|
||||
" extraction_target=ExtractTarget.PER_TABLE_ROW,\n",
|
||||
" parse_model=\"anthropic-sonnet-4.5\",\n",
|
||||
" ),\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "43722cda",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Results"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "95b5aca6",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"380"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"len(result.data)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "1e355770",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{'county': 'Alameda',\n",
|
||||
" 'hospital_name': 'Alameda Hospital',\n",
|
||||
" 'plan_names': ['Trio HMO',\n",
|
||||
" 'SaveNet',\n",
|
||||
" 'Access+ HMO',\n",
|
||||
" 'BlueHPN PPO',\n",
|
||||
" 'Tandem PPO',\n",
|
||||
" 'PPO']},\n",
|
||||
" {'county': 'Alameda',\n",
|
||||
" 'hospital_name': 'Alta Bates Med Ctr Herrick Campus',\n",
|
||||
" 'plan_names': ['Trio HMO',\n",
|
||||
" 'Access+ HMO',\n",
|
||||
" 'BlueHPN PPO',\n",
|
||||
" 'Tandem PPO',\n",
|
||||
" 'PPO']},\n",
|
||||
" {'county': 'Alameda',\n",
|
||||
" 'hospital_name': 'Alta Bates Summit Med Ctr Alta Bates Campus',\n",
|
||||
" 'plan_names': ['Trio HMO',\n",
|
||||
" 'Access+ HMO',\n",
|
||||
" 'BlueHPN PPO',\n",
|
||||
" 'Tandem PPO',\n",
|
||||
" 'PPO']},\n",
|
||||
" {'county': 'Alameda',\n",
|
||||
" 'hospital_name': 'Alta Bates Summit Med Ctr Summit Campus',\n",
|
||||
" 'plan_names': ['Trio HMO',\n",
|
||||
" 'Access+ HMO',\n",
|
||||
" 'BlueHPN PPO',\n",
|
||||
" 'Tandem PPO',\n",
|
||||
" 'PPO']},\n",
|
||||
" {'county': 'Alameda',\n",
|
||||
" 'hospital_name': 'Alta Bates Summit Medical Center',\n",
|
||||
" 'plan_names': ['Trio HMO',\n",
|
||||
" 'Access+ HMO',\n",
|
||||
" 'BlueHPN PPO',\n",
|
||||
" 'Tandem PPO',\n",
|
||||
" 'PPO']},\n",
|
||||
" {'county': 'Alameda',\n",
|
||||
" 'hospital_name': 'BHC Fremont Hospital',\n",
|
||||
" 'plan_names': ['Trio HMO',\n",
|
||||
" 'SaveNet',\n",
|
||||
" 'Access+ HMO',\n",
|
||||
" 'BlueHPN PPO',\n",
|
||||
" 'Tandem PPO',\n",
|
||||
" 'PPO']},\n",
|
||||
" {'county': 'Alameda',\n",
|
||||
" 'hospital_name': 'Centre For Neuro Skills San Francisco',\n",
|
||||
" 'plan_names': ['Trio HMO',\n",
|
||||
" 'SaveNet',\n",
|
||||
" 'Access+ HMO',\n",
|
||||
" 'BlueHPN PPO',\n",
|
||||
" 'Tandem PPO',\n",
|
||||
" 'PPO']},\n",
|
||||
" {'county': 'Alameda',\n",
|
||||
" 'hospital_name': 'Eden Medical Center',\n",
|
||||
" 'plan_names': ['Trio HMO', 'Access+ HMO', 'PPO']},\n",
|
||||
" {'county': 'Alameda',\n",
|
||||
" 'hospital_name': 'Fairmont Hospital',\n",
|
||||
" 'plan_names': ['Trio HMO',\n",
|
||||
" 'SaveNet',\n",
|
||||
" 'Access+ HMO',\n",
|
||||
" 'BlueHPN PPO',\n",
|
||||
" 'Tandem PPO',\n",
|
||||
" 'PPO']},\n",
|
||||
" {'county': 'Alameda',\n",
|
||||
" 'hospital_name': 'Highland Hospital',\n",
|
||||
" 'plan_names': ['Trio HMO',\n",
|
||||
" 'SaveNet',\n",
|
||||
" 'Access+ HMO',\n",
|
||||
" 'BlueHPN PPO',\n",
|
||||
" 'Tandem PPO',\n",
|
||||
" 'PPO']}]"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"result.data[:10]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e28f0de8",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "di156pb7s6j",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"**Success!** We extracted all **380 hospitals** from the multi-page PDF. Each entity was correctly parsed with its county, hospital name, and applicable insurance plans. With `PER_DOC`, we would likely have only gotten the first 20-30 entries."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "gelvl6db268",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Extracting from a Toy Catalog\n",
|
||||
"\n",
|
||||
"### Example 2: Semi-Structured List\n",
|
||||
"\n",
|
||||
"The `PER_TABLE_ROW` extraction target also works well for documents that aren't explicit tables but have similar properties:\n",
|
||||
"- **Ordered listing**: The toys are listed sequentially with visual separation (section headers, spacing)\n",
|
||||
"- **Repeating pattern**: Each toy entry has a consistent structure (code, name, specs, description)\n",
|
||||
"- **Local information**: All attributes for each toy are grouped together in its entry\n",
|
||||
"\n",
|
||||
"Even though this isn't a traditional table format, each toy entity locally contains all the information needed for our schema. LlamaExtract detects the formatting patterns that distinguish each toy and extracts them as separate entities.\n",
|
||||
"\n",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8cf0b2db",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class ToyCatalog(BaseModel):\n",
|
||||
" \"\"\"Product information from a toy catalog.\"\"\"\n",
|
||||
"\n",
|
||||
" section_name: str = Field(\n",
|
||||
" description=\"The name of the toy section (e.g. Table Toys, Active Toys).\"\n",
|
||||
" )\n",
|
||||
" product_code: str = Field(\n",
|
||||
" description=\"The unique product code for the toy (e.g., GA457).\"\n",
|
||||
" )\n",
|
||||
" toy_name: str = Field(description=\"The name of the toy.\")\n",
|
||||
" age_range: str = Field(\n",
|
||||
" description=\"The recommended age range for the toy (e.g., 6 +, 4 +).\",\n",
|
||||
" )\n",
|
||||
" player_range: str = Field(\n",
|
||||
" description=\"The number of players the toy is designed for (e.g., 2, 2-4, 1-6).\",\n",
|
||||
" )\n",
|
||||
" material: str = Field(\n",
|
||||
" description=\"The primary material(s) the toy is made of (e.g., wood, cardboard).\",\n",
|
||||
" )\n",
|
||||
" description: str = Field(\n",
|
||||
" description=\"A brief description of the toy and its components and dimensions.\",\n",
|
||||
" )"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "mysu1i2qo9e",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Results\n",
|
||||
"\n",
|
||||
"Again, our schema represents a **single toy product**, not the entire catalog. The system will return a `list[ToyCatalog]` with one entry per toy."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "5b38b806",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"result = await llama_extract.aextract(\n",
|
||||
" data_schema=ToyCatalog,\n",
|
||||
" files=\"./data/tables/Click-BS-Toys-Catalogue-2024.pdf\",\n",
|
||||
" config=ExtractConfig(\n",
|
||||
" extraction_mode=ExtractMode.PREMIUM,\n",
|
||||
" extraction_target=ExtractTarget.PER_TABLE_ROW,\n",
|
||||
" parse_model=\"anthropic-sonnet-4.5\",\n",
|
||||
" ),\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "91aface0",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"153"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"len(result.data)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "51278736",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[{'section_name': 'Table Toys',\n",
|
||||
" 'product_code': 'GA457',\n",
|
||||
" 'toy_name': 'Dots and Boxes',\n",
|
||||
" 'age_range': '6+',\n",
|
||||
" 'player_range': '2',\n",
|
||||
" 'material': 'wood',\n",
|
||||
" 'description': 'base 17x17 cm\\n50 border pieces 4x1,2x0,3 cm\\n34 trees 2,6x1,4 cm'},\n",
|
||||
" {'section_name': 'Table Toys',\n",
|
||||
" 'product_code': 'GA456',\n",
|
||||
" 'toy_name': '3 In a Row',\n",
|
||||
" 'age_range': '8+',\n",
|
||||
" 'player_range': '2',\n",
|
||||
" 'material': 'wood, pine, cardboard',\n",
|
||||
" 'description': 'base 24x22,5x2,5 cm\\n30 cards 5,5x5 cm\\n6 chips'},\n",
|
||||
" {'section_name': 'Table Toys',\n",
|
||||
" 'product_code': 'GA467',\n",
|
||||
" 'toy_name': 'Which Cow am i?',\n",
|
||||
" 'age_range': '6+',\n",
|
||||
" 'player_range': '2',\n",
|
||||
" 'material': 'wood, beech',\n",
|
||||
" 'description': '2 cow bases 56x4x4,5 cm\\n16 cards 4x5 cm'},\n",
|
||||
" {'section_name': 'Table Toys',\n",
|
||||
" 'product_code': 'GA460',\n",
|
||||
" 'toy_name': 'Balance Bunnies',\n",
|
||||
" 'age_range': '4+',\n",
|
||||
" 'player_range': '2',\n",
|
||||
" 'material': 'wood',\n",
|
||||
" 'description': '1 base 35x12x25 cm\\n7 bunnies 7 foxes\\n1 dice 3 cm'},\n",
|
||||
" {'section_name': 'Table Toys',\n",
|
||||
" 'product_code': 'GA462',\n",
|
||||
" 'toy_name': 'Color Combination Race',\n",
|
||||
" 'age_range': '4+',\n",
|
||||
" 'player_range': '2-4',\n",
|
||||
" 'material': 'wood, cardboard',\n",
|
||||
" 'description': 'base 6,5x6,5x15 cm, rings 5,5x5,5x0,5 mm\\ncardholder 6x6x2 cm, cards 5,5x5,5 cm\\ncolor cards Ø 15,5 cm - Ø 7 cm'},\n",
|
||||
" {'section_name': 'Table Toys',\n",
|
||||
" 'product_code': 'GA465',\n",
|
||||
" 'toy_name': 'Plop It',\n",
|
||||
" 'age_range': '6+',\n",
|
||||
" 'player_range': '2-4',\n",
|
||||
" 'material': 'wood, elastic, cardboard',\n",
|
||||
" 'description': 'Catch the right balls and plop them in the net!\\n* 2 ploppers 8x5 cm\\n* 2 net holders Ø 5cm, length 55 cm\\n* 6 cards 1,5x2,5 cm, 30 balls Ø 2,5 cm\\n* 1 rope 120 cm'},\n",
|
||||
" {'section_name': 'Table Toys',\n",
|
||||
" 'product_code': 'GA466',\n",
|
||||
" 'toy_name': 'Whack a Shape',\n",
|
||||
" 'age_range': '4+',\n",
|
||||
" 'player_range': '2-4',\n",
|
||||
" 'material': 'wood',\n",
|
||||
" 'description': '* base 38,5x15,5 cm\\n* 2 stands 36 half balls, 4 hammers\\n* 1 dice 2,5 cm\\n* 4 cards'},\n",
|
||||
" {'section_name': 'Table Toys',\n",
|
||||
" 'product_code': 'GA458',\n",
|
||||
" 'toy_name': 'Sling Puck | Table Hockey',\n",
|
||||
" 'age_range': '6+',\n",
|
||||
" 'player_range': '2',\n",
|
||||
" 'material': 'wood',\n",
|
||||
" 'description': '* double sides base 39x21x3 cm\\n* 10 chips Ø 2,5 cm\\n* 2 pushers 4x4x3 cm'},\n",
|
||||
" {'section_name': 'Table Toys',\n",
|
||||
" 'product_code': 'GA039',\n",
|
||||
" 'toy_name': 'DIY Birdhouse',\n",
|
||||
" 'age_range': '3+',\n",
|
||||
" 'player_range': '1',\n",
|
||||
" 'material': 'wood',\n",
|
||||
" 'description': '* house 9x9x13 cm'},\n",
|
||||
" {'section_name': 'Table Toys',\n",
|
||||
" 'product_code': 'GA319',\n",
|
||||
" 'toy_name': 'Triangle Domino',\n",
|
||||
" 'age_range': '6+',\n",
|
||||
" 'player_range': '2-4',\n",
|
||||
" 'material': 'wood',\n",
|
||||
" 'description': '* 35 triangles 10x10 x10 cm'}]"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"result.data[:10]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d1810c0a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ezur9gnhmsb",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"**Success!** Despite the semi-structured format, we extracted all **152 toy products** from the catalog (there's an extra repeated extracted toy from the Appendix section). LlamaExtract automatically detected the visual patterns separating each toy entry and applied our schema to each one."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "aeyr3io29u",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Summary\n",
|
||||
"\n",
|
||||
"The `PER_TABLE_ROW` extraction target is powerful for extracting repeating structured entities from documents. Key takeaways:\n",
|
||||
"\n",
|
||||
"1. **Schema design**: Define your schema for a single entity, not the full document. The system returns `list[YourSchema]`.\n",
|
||||
"\n",
|
||||
"2. **Works with various formats**: Not just traditional tables—any document with distinguishable repeating entities (bullets, numbering, headers, visual separation, etc.). The common requirement is that each entity should contain all the necessary data for your schema within its local context.\n",
|
||||
"\n",
|
||||
"3. **Automatic pattern detection**: LlamaExtract identifies the formatting patterns that distinguish entities and applies your schema to each one."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -1035,7 +1035,7 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": ".venv",
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
@@ -1052,5 +1052,5 @@
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"source": [
|
||||
"# Dynamic Section Retrieval with LlamaParse\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services-demo/blob/main/examples/parse/advanced_rag/dynamic_section_retrieval.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/advanced_rag/dynamic_section_retrieval.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"This notebook showcases a concept called \"dynamic section retrieval\".\n",
|
||||
"\n",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"source": [
|
||||
"# Advanced RAG with LlamaParse\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_parse/blob/main/examples/demo_advanced.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_parse/blob/main/examples/parse/demo_advanced.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"This notebook is a complete walkthrough for using LlamaParse with advanced indexing/retrieval techniques in LlamaIndex over the Apple 10K Filing. \n",
|
||||
"\n",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"source": [
|
||||
"# RAG with Excel Spreadsheet using LlamaPrase\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/demo_excel.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/demo_excel.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"This notebook shows you using LlamaParse with Excel Spreadsheet.\n",
|
||||
"\n",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"source": [
|
||||
"# Download Charts\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/demo_get_charts.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/demo_get_charts.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"This notebook demonstrates how to download charts from a document using the result object.\n",
|
||||
"\n",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"source": [
|
||||
"# LlamaParse - Fast checking Insurance Contract for Coverage\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/demo_insurance.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/demo_insurance.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"In this notebook we will look at how LlamaParse can be used to extract structured coverage information from an insurance policy.\n",
|
||||
"\n",
|
||||
@@ -36,7 +36,7 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Download an insurance policy fron IRDAI\n",
|
||||
"## Download an insurance policy from IRDAI\n",
|
||||
"\n",
|
||||
"The Insurance Regulatory and Development Authority of India (IRDAI) maintains a great resource: https://policyholder.gov.in/web/guest/non-life-insurance-products where all insurance policies available in India are publicly available for download! Let's download a complex health insurance policy as an example."
|
||||
]
|
||||
@@ -228,11 +228,11 @@
|
||||
" result_type=\"markdown\",\n",
|
||||
" system_prompt_append=\"\"\"\n",
|
||||
"This document is an insurance policy.\n",
|
||||
"When a benefits/coverage/exlusion is describe in the document ammend to it add a text in the follwing benefits string format (where coverage could be an exclusion).\n",
|
||||
"When a benefits/coverage/exlusion is describe in the document amend to it add a text in the following benefits string format (where coverage could be an exclusion).\n",
|
||||
"\n",
|
||||
"For {nameofrisk} and in this condition {whenDoesThecoverageApply} the coverage is {coverageDescription}. \n",
|
||||
" \n",
|
||||
"If the document contain a benefits TABLE that describe coverage amounts, do not ouput it as a table, but instead as a list of benefits string.\n",
|
||||
"If the document contain a benefits TABLE that describe coverage amounts, do not output it as a table, but instead as a list of benefits string.\n",
|
||||
" \n",
|
||||
"\"\"\",\n",
|
||||
").aparse(\"./policy.pdf\")\n",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"source": [
|
||||
"# LlamaParse `JobResult` Tour\n",
|
||||
"\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/demo_json.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/demo_json.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"The `JobResult` object is the main object returned by the LlamaParse API. It contains all the information about the job, including the parsed data, metadata, and any errors.\n",
|
||||
"\n",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"\n",
|
||||
"LlamaParse supports users to specify a `language` parameter before uploading documents, giving users better OCR capabilities over non-English PDFs, parsing images into more accurate representations.\n",
|
||||
"\n",
|
||||
"You can specify 80+ different languages: see this file for a full list of supported languages: https://github.com/run-llama/llama_cloud_services/blob/main/llama_parse/base.py.\n",
|
||||
"You can specify 80+ different languages: see this file for a full list of supported languages: https://github.com/run-llama/llama_cloud_services/blob/main/py/llama_cloud_services/parse/base.py.\n",
|
||||
"\n",
|
||||
"This notebook shows a demo of this in action. \n",
|
||||
"\n",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/excel/o1_excel_rag.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/parse/excel/o1_excel_rag.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -740,7 +740,7 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"In this example, these pages aren't going to be that different when parsed, but we can verify which pages triggered auto-made by looking at the [JSON output](https://github.com/run-llama/llama_cloud_services/blob/main/examples/demo_json_tour.ipynb) of LlamaParse:"
|
||||
"In this example, these pages aren't going to be that different when parsed, but we can verify which pages triggered auto-made by looking at the [JSON output](https://github.com/run-llama/llama_cloud_services/blob/main/examples/parse/demo_json_tour.ipynb) of LlamaParse:"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,762 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Report Generation with LlamaReport\n",
|
||||
"\n",
|
||||
"In this notebook, we'll walk through the basic process of generating a report with LlamaReport, and highlight some of the key features of the library.\n",
|
||||
"\n",
|
||||
"TLDR:\n",
|
||||
"1. Download source data to use as knowledge base for the report\n",
|
||||
"2. Kick off report generation with a template\n",
|
||||
"3. Get the plan and review/accept/reject suggestions\n",
|
||||
"4. Get the final report\n",
|
||||
"5. Review/accept/reject suggestions to edit the final report\n",
|
||||
"6. Print the final report"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install llama-cloud-services"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Download Source Data\n",
|
||||
"\n",
|
||||
"Here, we download the `Attention is All You Need` paper as a PDF.\n",
|
||||
"\n",
|
||||
"LlamaReport currently supports up to 5 files as input, and essentially any file type that can be parsed by LlamaParse.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!wget \"https://arxiv.org/pdf/1706.03762.pdf\" -O \"./attention.pdf\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. Kick off Report Generation\n",
|
||||
"\n",
|
||||
"Here, we kick off report generation with a template.\n",
|
||||
"\n",
|
||||
"The template can either be a string or a file path, but here we'll use a string.\n",
|
||||
"\n",
|
||||
"In our experiments, anything works as a template, but some general guidelines:\n",
|
||||
"\n",
|
||||
"- Use markdown formatting + instructions in each section to guide the report generation\n",
|
||||
"- If using an existing file as a template, provide extra instructions to guide the report generation\n",
|
||||
"\n",
|
||||
"**NOTE:** Since we are in a notebook, we will use async functions and `await` throughout. Synchronous methods that work without `await` are available by just removing the `a` from the method name and removing the `await` keyword."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from llama_cloud_services import LlamaReport\n",
|
||||
"\n",
|
||||
"llama_report = LlamaReport(\n",
|
||||
" api_key=\"llx-...\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"report_client = await llama_report.acreate_report(\n",
|
||||
" name=\"my_cool_report_on_attention\",\n",
|
||||
" # can pass in file paths or bytes\n",
|
||||
" input_files=[\"./attention.pdf\"],\n",
|
||||
" template_text=\"\"\"\\\n",
|
||||
"# [Some title]\\n\\n\n",
|
||||
"## TLDR\\n\n",
|
||||
"A quick summary of the paper.\\n\\n\n",
|
||||
"## Details\\n\n",
|
||||
"More details about the paper, possibly more than one section here.\\n\n",
|
||||
"\"\"\",\n",
|
||||
" # optional additional instructions for the report generation\n",
|
||||
" # template_instructions=None,\n",
|
||||
" # optional file path to an existing template instead of template_text\n",
|
||||
" # template_file=None,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The returned `ReportClient` object is used to interact with the report generation process for this specific report."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Report(id=0a394b33-1a3e-463c-b5cb-7ff8ab827d0a, name=my_cool_report_on_attention)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(report_client)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Get the plan\n",
|
||||
"\n",
|
||||
"The first phases of report generation involve ingesting the source data and generating a plan.\n",
|
||||
"\n",
|
||||
"The plan is a list of instructions for the report generation, and can be reviewed/accepted/rejected by the user.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"plan = await report_client.await_for_plan(\n",
|
||||
" timeout=10000,\n",
|
||||
" poll_interval=10,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"# {title}\n",
|
||||
"[ReportQuery(field='title', prompt='Generate a clear and concise title for this paper about the Transformer model and attention mechanisms', context='The paper discusses the Transformer architecture for sequence transduction using attention mechanisms, focusing on machine translation applications')]\n",
|
||||
"==================\n",
|
||||
"## TLDR\n",
|
||||
"\n",
|
||||
"{tldr_content}\n",
|
||||
"[ReportQuery(field='tldr_content', prompt='Write a brief, clear summary of the key points about the Transformer model', context='Focus on the main innovations: attention mechanisms, efficiency improvements, and state-of-the-art results in machine translation')]\n",
|
||||
"==================\n",
|
||||
"## Details\n",
|
||||
"\n",
|
||||
"{details_content}\n",
|
||||
"[ReportQuery(field='details_content', prompt='Provide detailed information about the Transformer model architecture and its applications', context='Include information about:\\n- The attention mechanism implementation\\n- Advantages over recurrent and convolutional models\\n- Performance in machine translation tasks\\n- Training efficiency improvements')]\n",
|
||||
"==================\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for plan_block in plan.blocks:\n",
|
||||
" print(plan_block.block.template)\n",
|
||||
" print(plan_block.queries)\n",
|
||||
" print(\"==================\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"With the plan, we can either use it to kick off generation of the final report, or we can edit the plan and adjust it as needed.\n",
|
||||
"\n",
|
||||
"While we could manually edit the objects here and use `await report_client.aupdate_plan(action=\"edit\", updated_plan=plan)`, we can also use `LlamaReport` to agentically edit the plan."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"suggestions = await report_client.asuggest_edits(\n",
|
||||
" \"Can you split the details section into two sections?\"\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Justification for change: \n",
|
||||
"I'll help you break down the details section into two distinct parts - one focusing on the architecture and another on the practical applications and performance. This will make the content more organized and easier to follow. The original block at index 2 will be replaced with these two new sections.\n",
|
||||
"\n",
|
||||
"Proposed changes:\n",
|
||||
"\n",
|
||||
"## Architecture Details\n",
|
||||
"\n",
|
||||
"{architecture_content}\n",
|
||||
"\n",
|
||||
"[ReportQuery(field='architecture_content', prompt='Describe the technical details of the Transformer model architecture', context='Focus on:\\n- Core components of the Transformer architecture\\n- Self-attention mechanism implementation\\n- Multi-head attention details\\n- Position encoding approach\\n- Feed-forward network structure')]\n",
|
||||
"==================\n",
|
||||
"\n",
|
||||
"## Performance and Applications\n",
|
||||
"\n",
|
||||
"{applications_content}\n",
|
||||
"\n",
|
||||
"[ReportQuery(field='applications_content', prompt='Explain the practical applications and performance advantages of the Transformer model', context='Cover:\\n- Comparison with RNN and CNN models\\n- Machine translation results and benchmarks\\n- Training efficiency improvements\\n- Real-world applications and use cases\\n- Scalability benefits')]\n",
|
||||
"==================\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for suggestion in suggestions:\n",
|
||||
" print(\"Justification for change:\", suggestion.justification)\n",
|
||||
" print(\"Proposed changes:\")\n",
|
||||
" for plan_block in suggestion.blocks:\n",
|
||||
" print(plan_block.block.template)\n",
|
||||
" print(plan_block.queries)\n",
|
||||
" print(\"==================\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This looks pretty good! We can also use the client to automatically accept and apply, or reject, these suggestions.\n",
|
||||
"\n",
|
||||
"This will (locally) keep track of the history of changes, so that future suggestions can be based on the previous changes."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for suggestion in suggestions:\n",
|
||||
" await report_client.aaccept_edit(suggestion)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"What effect did that have on the tracked local history? Let's see!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[EditAction(block_idx=2, old_content='## Details\\n\\n{details_content}\\n\\nField: details_content, Prompt: Provide detailed information about the Transformer model architecture and its applications, Context: Include information about:\\n- The attention mechanism implementation\\n- Advantages over recurrent and convolutional models\\n- Performance in machine translation tasks\\n- Training efficiency improvements\\nDepends on: none', new_content='\\n## Architecture Details\\n\\n{architecture_content}\\n\\n\\nField: architecture_content, Prompt: Describe the technical details of the Transformer model architecture, Context: Focus on:\\n- Core components of the Transformer architecture\\n- Self-attention mechanism implementation\\n- Multi-head attention details\\n- Position encoding approach\\n- Feed-forward network structure\\nDepends on: none', action='approved', timestamp=datetime.datetime(2025, 2, 4, 20, 59, 55, 773558)),\n",
|
||||
" EditAction(block_idx=3, old_content='[No old content]', new_content='\\n## Performance and Applications\\n\\n{applications_content}\\n\\n\\nField: applications_content, Prompt: Explain the practical applications and performance advantages of the Transformer model, Context: Cover:\\n- Comparison with RNN and CNN models\\n- Machine translation results and benchmarks\\n- Training efficiency improvements\\n- Real-world applications and use cases\\n- Scalability benefits\\nDepends on: previous', action='approved', timestamp=datetime.datetime(2025, 2, 4, 20, 59, 55, 773687))]"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"report_client.edit_history"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[Message(role=<MessageRole.USER: 'user'>, content='Can you split the details section into two sections?', timestamp=datetime.datetime(2025, 2, 4, 20, 59, 47, 754848)),\n",
|
||||
" Message(role=<MessageRole.ASSISTANT: 'assistant'>, content=\"\\nI'll help you break down the details section into two distinct parts - one focusing on the architecture and another on the practical applications and performance. This will make the content more organized and easier to follow. The original block at index 2 will be replaced with these two new sections.\\n\", timestamp=datetime.datetime(2025, 2, 4, 20, 59, 55, 482070))]"
|
||||
]
|
||||
},
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"report_client.chat_history"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"These two items are used to provide context for future suggestions! You can always clear this, or provide your own history."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# report_client.suggest_edits(\"....\", chat_history=[{\"role\": \"user\", \"content\": \"...\"}, ...])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Get the final report\n",
|
||||
"\n",
|
||||
"Now that we have a plan, we can kick off generation of the final report."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# kicks off report generation\n",
|
||||
"await report_client.aupdate_plan(action=\"approve\")\n",
|
||||
"\n",
|
||||
"# waits for report generation to complete\n",
|
||||
"report = await report_client.await_completion(\n",
|
||||
" timeout=10000,\n",
|
||||
" poll_interval=10,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"# Attention Is All You Need: A Pure Attention-Based Architecture for Neural Machine Translation\n",
|
||||
"\n",
|
||||
"## TLDR\n",
|
||||
"\n",
|
||||
"The Transformer introduced a revolutionary architecture that relies entirely on attention mechanisms, eliminating the need for recurrence or convolution in sequence processing. Its key innovations include multi-head self-attention for parallel processing of input sequences, scaled dot-product attention for efficient computation, and positional encodings for sequence order awareness. The model achieved breakthrough results in machine translation (28.4 BLEU on English-to-German, 41.8 BLEU on English-to-French) while requiring significantly less training time than previous approaches, training in 3.5 days on 8 GPUs. This architecture demonstrated that attention mechanisms alone are sufficient for state-of-the-art sequence modeling, setting a new direction for natural language processing.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"## Architecture Details\n",
|
||||
"\n",
|
||||
"The Transformer architecture represents a groundbreaking approach to sequence processing, built entirely on attention mechanisms without recurrence or convolution. Here are its key technical details:\n",
|
||||
"\n",
|
||||
"Core Components:\n",
|
||||
"- Encoder-decoder architecture with stacked self-attention and point-wise feed-forward layers\n",
|
||||
"- Each layer contains two main sub-layers: multi-head self-attention mechanism and position-wise feed-forward network\n",
|
||||
"- Layer normalization and residual connections between sub-layers\n",
|
||||
"- No recurrent or convolutional elements, enabling parallel processing\n",
|
||||
"\n",
|
||||
"Self-Attention Mechanism:\n",
|
||||
"- Processes relationships between all positions in a sequence simultaneously\n",
|
||||
"- Computes attention weights using queries, keys, and values derived from input representations\n",
|
||||
"- Implements scaled dot-product attention to prevent gradient issues with large input dimensions\n",
|
||||
"- Allows direct modeling of dependencies regardless of positional distance\n",
|
||||
"- Uses masking in decoder to prevent leftward information flow and maintain auto-regressive property\n",
|
||||
"\n",
|
||||
"Multi-Head Attention:\n",
|
||||
"- Employs multiple attention heads operating in parallel\n",
|
||||
"- Each head processes information in different representation subspaces\n",
|
||||
"- Three types of attention applications:\n",
|
||||
" 1. Encoder self-attention (all positions attend to each other)\n",
|
||||
" 2. Decoder self-attention (each position attends to previous positions)\n",
|
||||
" 3. Encoder-decoder attention (decoder queries attend to encoder outputs)\n",
|
||||
"- Counteracts reduced resolution from attention averaging through parallel processing\n",
|
||||
"\n",
|
||||
"Position-wise Feed-Forward Network:\n",
|
||||
"- Applied identically to each position separately\n",
|
||||
"- Consists of two linear transformations with ReLU activation\n",
|
||||
"- Structure: FFN(x) = max(0, xW1 + b1)W2 + b2\n",
|
||||
"- Input and output dimensionality: dmodel = 512\n",
|
||||
"- Inner-layer dimensionality: dff = 2048\n",
|
||||
"- Parameters vary between layers but remain constant across positions\n",
|
||||
"\n",
|
||||
"Position Encoding:\n",
|
||||
"- Adds positional information to input embeddings\n",
|
||||
"- Enables the model to consider sequential order without recurrence\n",
|
||||
"- Implements sinusoidal position encodings to allow model to attend to relative positions\n",
|
||||
"- Maintains constant number of operations between any two positions, unlike convolutional approaches\n",
|
||||
"- Allows effective modeling of both local and long-range dependencies\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"## Performance and Applications\n",
|
||||
"\n",
|
||||
"The Transformer model demonstrates significant performance advantages and practical applications across multiple domains:\n",
|
||||
"\n",
|
||||
"Performance Advantages over RNN/CNN Models:\n",
|
||||
"- Eliminates sequential computation constraints present in RNNs, enabling superior parallelization\n",
|
||||
"- Reduces operations needed for relating distant positions to a constant number, compared to linear/logarithmic scaling in CNNs\n",
|
||||
"- Processes all input and output positions simultaneously through self-attention mechanisms\n",
|
||||
"- Achieves state-of-the-art results while requiring significantly less computational resources\n",
|
||||
"\n",
|
||||
"Machine Translation Benchmarks:\n",
|
||||
"- WMT 2014 English-to-German: 28.4 BLEU score, exceeding previous best results by over 2 BLEU points\n",
|
||||
"- WMT 2014 English-to-French: 41.8 BLEU score (single-model state-of-the-art)\n",
|
||||
"- Surpasses performance of existing model ensembles in translation tasks\n",
|
||||
"\n",
|
||||
"Training Efficiency:\n",
|
||||
"- Requires only 3.5 days of training on eight GPUs for state-of-the-art performance\n",
|
||||
"- Achieves superior results at \"a small fraction of the training costs\" compared to previous models\n",
|
||||
"- Enables significantly faster training through parallel processing of input/output sequences\n",
|
||||
"- Can reach production-quality performance in as little as twelve hours on modern GPU hardware\n",
|
||||
"\n",
|
||||
"Real-world Applications:\n",
|
||||
"- Machine translation systems\n",
|
||||
"- Natural language understanding tasks\n",
|
||||
"- Reading comprehension\n",
|
||||
"- Abstractive summarization\n",
|
||||
"- Text entailment analysis\n",
|
||||
"- Constituency parsing (achieving 92.7 F1 score in semi-supervised settings)\n",
|
||||
"- Adaptable to both large and limited training data scenarios\n",
|
||||
"\n",
|
||||
"Scalability Benefits:\n",
|
||||
"- Highly parallelizable architecture enables efficient scaling across multiple GPUs\n",
|
||||
"- Constant computational complexity for relating any input/output positions\n",
|
||||
"- Effective handling of long-range dependencies in sequences\n",
|
||||
"- Maintains performance quality while scaling to larger datasets and model sizes\n",
|
||||
"- Generalizes well across different tasks and domains without architectural changes\n",
|
||||
"- Supports efficient inference and deployment in production environments\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"report_text = \"\\n\\n\".join([block.template for block in report.blocks])\n",
|
||||
"print(report_text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Edit the final report\n",
|
||||
"\n",
|
||||
"Now that we have a report, we can edit it.\n",
|
||||
"\n",
|
||||
"We can use the `asuggest_edits` method to get suggestions for edits, and then use the `aaccept_edit`/`areject_edit` methods to apply them.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Justification for change: \n",
|
||||
"I'd suggest changing \"TLDR\" to \"Executive Summary\" which is more appropriate for a professional or academic report. This term is widely used in formal documents and better reflects the nature of this concise overview section while maintaining the same function of providing a quick summary of the key points.\n",
|
||||
"\n",
|
||||
"Proposed changes:\n",
|
||||
"## Executive Summary\n",
|
||||
"\n",
|
||||
"The Transformer introduced a revolutionary architecture that relies entirely on attention mechanisms, eliminating the need for recurrence or convolution in sequence processing. Its key innovations include multi-head self-attention for parallel processing of input sequences, scaled dot-product attention for efficient computation, and positional encodings for sequence order awareness. The model achieved breakthrough results in machine translation (28.4 BLEU on English-to-German, 41.8 BLEU on English-to-French) while requiring significantly less training time than previous approaches, training in 3.5 days on 8 GPUs. This architecture demonstrated that attention mechanisms alone are sufficient for state-of-the-art sequence modeling, setting a new direction for natural language processing.\n",
|
||||
"==================\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"suggestions = await report_client.asuggest_edits(\n",
|
||||
" \"Can you change the TLDR header to something more professional?\"\n",
|
||||
")\n",
|
||||
"for suggestion in suggestions:\n",
|
||||
" print(\"Justification for change:\", suggestion.justification)\n",
|
||||
" print(\"Proposed changes:\")\n",
|
||||
" for block in suggestion.blocks:\n",
|
||||
" print(block.template)\n",
|
||||
" print(\"==================\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Changing to \"Executive Summary\" sounds reasonable, lets accept that!\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for suggestion in suggestions:\n",
|
||||
" await report_client.aaccept_edit(suggestion)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. Print the final report\n",
|
||||
"\n",
|
||||
"Now that we have a report, we can print it."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"# Attention Is All You Need: A Pure Attention-Based Architecture for Neural Machine Translation\n",
|
||||
"\n",
|
||||
"## Executive Summary\n",
|
||||
"\n",
|
||||
"The Transformer introduced a revolutionary architecture that relies entirely on attention mechanisms, eliminating the need for recurrence or convolution in sequence processing. Its key innovations include multi-head self-attention for parallel processing of input sequences, scaled dot-product attention for efficient computation, and positional encodings for sequence order awareness. The model achieved breakthrough results in machine translation (28.4 BLEU on English-to-German, 41.8 BLEU on English-to-French) while requiring significantly less training time than previous approaches, training in 3.5 days on 8 GPUs. This architecture demonstrated that attention mechanisms alone are sufficient for state-of-the-art sequence modeling, setting a new direction for natural language processing.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"## Architecture Details\n",
|
||||
"\n",
|
||||
"The Transformer architecture represents a groundbreaking approach to sequence processing, built entirely on attention mechanisms without recurrence or convolution. Here are its key technical details:\n",
|
||||
"\n",
|
||||
"Core Components:\n",
|
||||
"- Encoder-decoder architecture with stacked self-attention and point-wise feed-forward layers\n",
|
||||
"- Each layer contains two main sub-layers: multi-head self-attention mechanism and position-wise feed-forward network\n",
|
||||
"- Layer normalization and residual connections between sub-layers\n",
|
||||
"- No recurrent or convolutional elements, enabling parallel processing\n",
|
||||
"\n",
|
||||
"Self-Attention Mechanism:\n",
|
||||
"- Processes relationships between all positions in a sequence simultaneously\n",
|
||||
"- Computes attention weights using queries, keys, and values derived from input representations\n",
|
||||
"- Implements scaled dot-product attention to prevent gradient issues with large input dimensions\n",
|
||||
"- Allows direct modeling of dependencies regardless of positional distance\n",
|
||||
"- Uses masking in decoder to prevent leftward information flow and maintain auto-regressive property\n",
|
||||
"\n",
|
||||
"Multi-Head Attention:\n",
|
||||
"- Employs multiple attention heads operating in parallel\n",
|
||||
"- Each head processes information in different representation subspaces\n",
|
||||
"- Three types of attention applications:\n",
|
||||
" 1. Encoder self-attention (all positions attend to each other)\n",
|
||||
" 2. Decoder self-attention (each position attends to previous positions)\n",
|
||||
" 3. Encoder-decoder attention (decoder queries attend to encoder outputs)\n",
|
||||
"- Counteracts reduced resolution from attention averaging through parallel processing\n",
|
||||
"\n",
|
||||
"Position-wise Feed-Forward Network:\n",
|
||||
"- Applied identically to each position separately\n",
|
||||
"- Consists of two linear transformations with ReLU activation\n",
|
||||
"- Structure: FFN(x) = max(0, xW1 + b1)W2 + b2\n",
|
||||
"- Input and output dimensionality: dmodel = 512\n",
|
||||
"- Inner-layer dimensionality: dff = 2048\n",
|
||||
"- Parameters vary between layers but remain constant across positions\n",
|
||||
"\n",
|
||||
"Position Encoding:\n",
|
||||
"- Adds positional information to input embeddings\n",
|
||||
"- Enables the model to consider sequential order without recurrence\n",
|
||||
"- Implements sinusoidal position encodings to allow model to attend to relative positions\n",
|
||||
"- Maintains constant number of operations between any two positions, unlike convolutional approaches\n",
|
||||
"- Allows effective modeling of both local and long-range dependencies\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"## Performance and Applications\n",
|
||||
"\n",
|
||||
"The Transformer model demonstrates significant performance advantages and practical applications across multiple domains:\n",
|
||||
"\n",
|
||||
"Performance Advantages over RNN/CNN Models:\n",
|
||||
"- Eliminates sequential computation constraints present in RNNs, enabling superior parallelization\n",
|
||||
"- Reduces operations needed for relating distant positions to a constant number, compared to linear/logarithmic scaling in CNNs\n",
|
||||
"- Processes all input and output positions simultaneously through self-attention mechanisms\n",
|
||||
"- Achieves state-of-the-art results while requiring significantly less computational resources\n",
|
||||
"\n",
|
||||
"Machine Translation Benchmarks:\n",
|
||||
"- WMT 2014 English-to-German: 28.4 BLEU score, exceeding previous best results by over 2 BLEU points\n",
|
||||
"- WMT 2014 English-to-French: 41.8 BLEU score (single-model state-of-the-art)\n",
|
||||
"- Surpasses performance of existing model ensembles in translation tasks\n",
|
||||
"\n",
|
||||
"Training Efficiency:\n",
|
||||
"- Requires only 3.5 days of training on eight GPUs for state-of-the-art performance\n",
|
||||
"- Achieves superior results at \"a small fraction of the training costs\" compared to previous models\n",
|
||||
"- Enables significantly faster training through parallel processing of input/output sequences\n",
|
||||
"- Can reach production-quality performance in as little as twelve hours on modern GPU hardware\n",
|
||||
"\n",
|
||||
"Real-world Applications:\n",
|
||||
"- Machine translation systems\n",
|
||||
"- Natural language understanding tasks\n",
|
||||
"- Reading comprehension\n",
|
||||
"- Abstractive summarization\n",
|
||||
"- Text entailment analysis\n",
|
||||
"- Constituency parsing (achieving 92.7 F1 score in semi-supervised settings)\n",
|
||||
"- Adaptable to both large and limited training data scenarios\n",
|
||||
"\n",
|
||||
"Scalability Benefits:\n",
|
||||
"- Highly parallelizable architecture enables efficient scaling across multiple GPUs\n",
|
||||
"- Constant computational complexity for relating any input/output positions\n",
|
||||
"- Effective handling of long-range dependencies in sequences\n",
|
||||
"- Maintains performance quality while scaling to larger datasets and model sizes\n",
|
||||
"- Generalizes well across different tasks and domains without architectural changes\n",
|
||||
"- Supports efficient inference and deployment in production environments\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"report_response = await report_client.aget()\n",
|
||||
"report_text = \"\\n\\n\".join([block.template for block in report_response.report.blocks])\n",
|
||||
"print(report_text)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can also see the sources for each block!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"0.99687636\n",
|
||||
"# Abstract\n",
|
||||
"\n",
|
||||
"The dominant sequence transduction models are based on complex recurrent or convolutiona\n",
|
||||
"==================\n",
|
||||
"0.99591404\n",
|
||||
"# 2 Background\n",
|
||||
"\n",
|
||||
"The goal of reducing sequential computation also forms the foundation of the Extende\n",
|
||||
"==================\n",
|
||||
"0.9951325\n",
|
||||
"# 1 Introduction\n",
|
||||
"\n",
|
||||
"Recurrent neural networks, long short-term memory [13] and gated recurrent [7] neu\n",
|
||||
"==================\n",
|
||||
"0.99442345\n",
|
||||
"# 7 Conclusion\n",
|
||||
"\n",
|
||||
"In this work, we presented the Transformer, the first sequence transduction model ba\n",
|
||||
"==================\n",
|
||||
"0.9967649\n",
|
||||
"# 3.2.3 Applications of Attention in our Model\n",
|
||||
"\n",
|
||||
"The Transformer uses multi-head attention in three d\n",
|
||||
"==================\n",
|
||||
"0.99533635\n",
|
||||
"# 2 Background\n",
|
||||
"\n",
|
||||
"The goal of reducing sequential computation also forms the foundation of the Extende\n",
|
||||
"==================\n",
|
||||
"0.9935868\n",
|
||||
"# Abstract\n",
|
||||
"\n",
|
||||
"The dominant sequence transduction models are based on complex recurrent or convolutiona\n",
|
||||
"==================\n",
|
||||
"0.98780584\n",
|
||||
"# Outputs\n",
|
||||
"\n",
|
||||
"(shifted right)\n",
|
||||
"\n",
|
||||
"Figure 1: The Transformer - model architecture.\n",
|
||||
"\n",
|
||||
"The Transformer follows\n",
|
||||
"==================\n",
|
||||
"0.9205043\n",
|
||||
"# 3.3 Position-wise Feed-Forward Networks\n",
|
||||
"\n",
|
||||
"In addition to attention sub-layers, each of the layers i\n",
|
||||
"==================\n",
|
||||
"0.79581684\n",
|
||||
"# 1 Introduction\n",
|
||||
"\n",
|
||||
"Recurrent neural networks, long short-term memory [13] and gated recurrent [7] neu\n",
|
||||
"==================\n",
|
||||
"0.9946774\n",
|
||||
"# Abstract\n",
|
||||
"\n",
|
||||
"The dominant sequence transduction models are based on complex recurrent or convolutiona\n",
|
||||
"==================\n",
|
||||
"0.97079873\n",
|
||||
"# 7 Conclusion\n",
|
||||
"\n",
|
||||
"In this work, we presented the Transformer, the first sequence transduction model ba\n",
|
||||
"==================\n",
|
||||
"0.9535353\n",
|
||||
"# 6.3 English Constituency Parsing\n",
|
||||
"\n",
|
||||
"To evaluate if the Transformer can generalize to other tasks we \n",
|
||||
"==================\n",
|
||||
"0.9514138\n",
|
||||
"# 2 Background\n",
|
||||
"\n",
|
||||
"The goal of reducing sequential computation also forms the foundation of the Extende\n",
|
||||
"==================\n",
|
||||
"0.9790758\n",
|
||||
"# 1 Introduction\n",
|
||||
"\n",
|
||||
"Recurrent neural networks, long short-term memory [13] and gated recurrent [7] neu\n",
|
||||
"==================\n",
|
||||
"0.92262185\n",
|
||||
"# Outputs\n",
|
||||
"\n",
|
||||
"(shifted right)\n",
|
||||
"\n",
|
||||
"Figure 1: The Transformer - model architecture.\n",
|
||||
"\n",
|
||||
"The Transformer follows\n",
|
||||
"==================\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for block in report_response.report.blocks:\n",
|
||||
" # Each block has a list of sources, which are the nodes that were used to generate the block\n",
|
||||
" for source in block.sources:\n",
|
||||
" print(source.score)\n",
|
||||
" print(source.node.text[:100])\n",
|
||||
" print(\"==================\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "llama-parse-aNC435Vv-py3.10",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
This project uses LlamaSheets to extract data from spreadsheets for analysis.
|
||||
|
||||
## Current Project Structure
|
||||
|
||||
- `data/` - Contains extracted parquet files from LlamaSheets
|
||||
- `{name}_region_{N}.parquet` - Table data files
|
||||
- `{name}_metadata_{N}.parquet` - Cell metadata files
|
||||
- `{name}_job_metadata.json` - Extraction job information
|
||||
- `scripts/` - Analysis and helper scripts
|
||||
- `reports/` - Your generated reports and outputs
|
||||
|
||||
## Working with LlamaSheets Data
|
||||
|
||||
### Understanding the Files
|
||||
|
||||
When a spreadsheet is extracted, you'll find:
|
||||
|
||||
1. **Table parquet files** (`region_*.parquet`): The actual table data
|
||||
- Columns correspond to spreadsheet columns
|
||||
- Data types are preserved (dates, numbers, strings, booleans)
|
||||
|
||||
2. **Metadata parquet files** (`metadata_*.parquet`): Rich cell-level metadata
|
||||
- Formatting: `font_bold`, `font_italic`, `font_size`, `background_color_rgb`
|
||||
- Position: `row_number`, `column_number`, `coordinate` (e.g., "A1")
|
||||
- Type detection: `data_type`, `is_date_like`, `is_percentage`, `is_currency`
|
||||
- Layout: `is_in_first_row`, `is_merged_cell`, `horizontal_alignment`
|
||||
- Content: `cell_value`, `raw_cell_value`
|
||||
|
||||
3. **Job metadata JSON** (`job_metadata.json`): Overall extraction results
|
||||
- `regions[]`: List of extracted regions with IDs, locations, and titles/descriptions
|
||||
- `worksheet_metadata[]`: Generated titles and descriptions
|
||||
- `status`: Success/failure status
|
||||
|
||||
### Key Principles
|
||||
|
||||
1. **Use metadata to understand structure**: Bold cells often indicate headers, colors indicate groupings
|
||||
2. **Validate before analysis**: Check data types, look for missing values
|
||||
3. **Preserve formatting context**: The metadata tells you what the spreadsheet author emphasized
|
||||
4. **Save intermediate results**: Store cleaned data as new parquet files
|
||||
|
||||
### Common Patterns
|
||||
|
||||
**Loading data:**
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
df = pd.read_parquet("data/region_1_Sheet1.parquet")
|
||||
meta_df = pd.read_parquet("data/metadata_1_Sheet1.parquet")
|
||||
```
|
||||
|
||||
**Finding headers:**
|
||||
```python
|
||||
headers = meta_df[meta_df["font_bold"] == True]["cell_value"].tolist()
|
||||
```
|
||||
|
||||
**Finding date columns:**
|
||||
```python
|
||||
date_cols = meta_df[meta_df["is_date_like"] == True]["column_number"].unique()
|
||||
```
|
||||
|
||||
## Tools Available
|
||||
|
||||
- **Python 3.11+**: For data analysis
|
||||
- **pandas**: DataFrame manipulation
|
||||
- **pyarrow**: Parquet file reading
|
||||
- **matplotlib**: Visualization (optional)
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Always read the job_metadata.json first to understand what was extracted
|
||||
- Check both table data and metadata before making assumptions
|
||||
- Write reusable functions for common operations
|
||||
- Document any data quality issues discovered
|
||||
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
Generate sample spreadsheets for LlamaSheets + Claude workflows.
|
||||
|
||||
This script creates example Excel files that demonstrate different use cases:
|
||||
1. Simple data table (for Workflow 1)
|
||||
2. Regional sales data (for Workflow 2)
|
||||
3. Complex budget with formatting (for Workflow 3)
|
||||
4. Weekly sales report (for Workflow 4)
|
||||
|
||||
Usage:
|
||||
python generate_sample_data.py
|
||||
"""
|
||||
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment
|
||||
|
||||
|
||||
def generate_workflow_1_data(output_dir: Path) -> None:
|
||||
"""Generate simple financial report for Workflow 1."""
|
||||
print("📊 Generating Workflow 1: financial_report_q1.xlsx")
|
||||
|
||||
# Create sample quarterly data
|
||||
months = ["January", "February", "March"]
|
||||
categories = ["Revenue", "Cost of Goods Sold", "Operating Expenses", "Net Income"]
|
||||
|
||||
data = []
|
||||
for category in categories:
|
||||
row: dict[str, str | int] = {"Category": category}
|
||||
for month in months:
|
||||
if category == "Revenue":
|
||||
value = random.randint(80000, 120000)
|
||||
elif category == "Cost of Goods Sold":
|
||||
value = random.randint(30000, 50000)
|
||||
elif category == "Operating Expenses":
|
||||
value = random.randint(20000, 35000)
|
||||
else: # Net Income
|
||||
value = int(
|
||||
int(row.get("January", 0))
|
||||
+ int(row.get("February", 0))
|
||||
+ int(row.get("March", 0))
|
||||
)
|
||||
value = random.randint(15000, 40000)
|
||||
row[month] = value
|
||||
data.append(row)
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Write to Excel
|
||||
output_file = output_dir / "financial_report_q1.xlsx"
|
||||
with pd.ExcelWriter(output_file, engine="openpyxl") as writer:
|
||||
df.to_excel(writer, sheet_name="Q1 Summary", index=False)
|
||||
|
||||
# Format it nicely
|
||||
worksheet = writer.sheets["Q1 Summary"]
|
||||
for cell in worksheet[1]: # Header row
|
||||
cell.font = Font(bold=True)
|
||||
cell.fill = PatternFill(
|
||||
start_color="4F81BD", end_color="4F81BD", fill_type="solid"
|
||||
)
|
||||
cell.font = Font(color="FFFFFF", bold=True)
|
||||
|
||||
print(f" ✅ Created {output_file}")
|
||||
|
||||
|
||||
def generate_workflow_2_data(output_dir: Path) -> None:
|
||||
"""Generate regional sales data for Workflow 2."""
|
||||
print("\n📊 Generating Workflow 2: Regional sales data")
|
||||
|
||||
regions = ["northeast", "southeast", "west"]
|
||||
products = ["Widget A", "Widget B", "Widget C", "Gadget X", "Gadget Y"]
|
||||
|
||||
for region in regions:
|
||||
data = []
|
||||
start_date = datetime(2024, 1, 1)
|
||||
|
||||
# Generate 90 days of sales data
|
||||
for day in range(90):
|
||||
date = start_date + timedelta(days=day)
|
||||
# Random number of sales per day (3-8)
|
||||
for _ in range(random.randint(3, 8)):
|
||||
product = random.choice(products)
|
||||
units_sold = random.randint(1, 20)
|
||||
price_per_unit = random.randint(50, 200)
|
||||
revenue = units_sold * price_per_unit
|
||||
|
||||
data.append(
|
||||
{
|
||||
"Date": date.strftime("%Y-%m-%d"),
|
||||
"Product": product,
|
||||
"Units_Sold": units_sold,
|
||||
"Revenue": revenue,
|
||||
}
|
||||
)
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Write to Excel
|
||||
output_file = output_dir / f"sales_{region}.xlsx"
|
||||
df.to_excel(output_file, sheet_name="Sales", index=False)
|
||||
print(f" ✅ Created {output_file} ({len(df)} rows)")
|
||||
|
||||
|
||||
def generate_workflow_3_data(output_dir: Path) -> None:
|
||||
"""Generate complex budget spreadsheet with formatting for Workflow 3."""
|
||||
print("\n📊 Generating Workflow 3: company_budget_2024.xlsx")
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Budget"
|
||||
|
||||
# Define departments with colors
|
||||
departments = {
|
||||
"Engineering": "C6E0B4",
|
||||
"Marketing": "FFD966",
|
||||
"Sales": "F4B084",
|
||||
"Operations": "B4C7E7",
|
||||
}
|
||||
|
||||
# Define categories
|
||||
categories = {
|
||||
"Personnel": ["Salaries", "Benefits", "Training"],
|
||||
"Infrastructure": ["Office Rent", "Equipment", "Software Licenses"],
|
||||
"Operations": ["Travel", "Supplies", "Miscellaneous"],
|
||||
}
|
||||
|
||||
# Styles
|
||||
header_font = Font(bold=True, size=12)
|
||||
category_font = Font(bold=True, size=11)
|
||||
|
||||
row = 1
|
||||
|
||||
# Title
|
||||
ws.merge_cells(f"A{row}:E{row}")
|
||||
ws[f"A{row}"] = "2024 Annual Budget"
|
||||
ws[f"A{row}"].font = Font(bold=True, size=14)
|
||||
ws[f"A{row}"].alignment = Alignment(horizontal="center")
|
||||
row += 2
|
||||
|
||||
# Headers
|
||||
ws[f"A{row}"] = "Category"
|
||||
ws[f"B{row}"] = "Item"
|
||||
for i, dept in enumerate(departments.keys()):
|
||||
ws.cell(row, 3 + i, dept)
|
||||
ws.cell(row, 3 + i).font = header_font
|
||||
|
||||
for cell in ws[row]:
|
||||
cell.font = header_font
|
||||
row += 1
|
||||
|
||||
# Data
|
||||
for category, items in categories.items():
|
||||
# Category header (bold)
|
||||
ws[f"A{row}"] = category
|
||||
ws[f"A{row}"].font = category_font
|
||||
row += 1
|
||||
|
||||
# Items with department budgets
|
||||
for item in items:
|
||||
ws[f"A{row}"] = ""
|
||||
ws[f"B{row}"] = item
|
||||
|
||||
# Add budget amounts for each department (with color)
|
||||
for i, (dept, color) in enumerate(departments.items()):
|
||||
amount = random.randint(5000, 50000)
|
||||
cell = ws.cell(row, 3 + i, amount)
|
||||
cell.fill = PatternFill(
|
||||
start_color=color, end_color=color, fill_type="solid"
|
||||
)
|
||||
cell.number_format = "$#,##0"
|
||||
|
||||
row += 1
|
||||
|
||||
row += 1 # Blank row between categories
|
||||
|
||||
# Adjust column widths
|
||||
ws.column_dimensions["A"].width = 20
|
||||
ws.column_dimensions["B"].width = 25
|
||||
for i in range(len(departments)):
|
||||
ws.column_dimensions[chr(67 + i)].width = 15 # C, D, E, F
|
||||
|
||||
output_file = output_dir / "company_budget_2024.xlsx"
|
||||
wb.save(output_file)
|
||||
print(f" ✅ Created {output_file}")
|
||||
print(" • Bold categories, colored departments, merged title cell")
|
||||
|
||||
|
||||
def generate_workflow_4_data(output_dir: Path) -> None:
|
||||
"""Generate weekly sales report for Workflow 4."""
|
||||
print("\n📊 Generating Workflow 4: sales_weekly.xlsx")
|
||||
|
||||
products = [
|
||||
"Product A",
|
||||
"Product B",
|
||||
"Product C",
|
||||
"Product D",
|
||||
"Product E",
|
||||
"Product F",
|
||||
"Product G",
|
||||
"Product H",
|
||||
]
|
||||
|
||||
# Generate one week of data
|
||||
data = []
|
||||
start_date = datetime(2024, 11, 4) # Monday
|
||||
|
||||
for day in range(7):
|
||||
date = start_date + timedelta(days=day)
|
||||
# Each product has 3-10 transactions per day
|
||||
for product in products:
|
||||
for _ in range(random.randint(3, 10)):
|
||||
units = random.randint(1, 15)
|
||||
price = random.randint(20, 150)
|
||||
revenue = units * price
|
||||
|
||||
data.append(
|
||||
{
|
||||
"Date": date.strftime("%Y-%m-%d"),
|
||||
"Product": product,
|
||||
"Units": units,
|
||||
"Revenue": revenue,
|
||||
}
|
||||
)
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Write to Excel with some formatting
|
||||
output_file = output_dir / "sales_weekly.xlsx"
|
||||
with pd.ExcelWriter(output_file, engine="openpyxl") as writer:
|
||||
df.to_excel(writer, sheet_name="Weekly Sales", index=False)
|
||||
|
||||
# Format header
|
||||
worksheet = writer.sheets["Weekly Sales"]
|
||||
for cell in worksheet[1]:
|
||||
cell.font = Font(bold=True)
|
||||
|
||||
print(f" ✅ Created {output_file} ({len(df)} rows)")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Generate all sample data files."""
|
||||
print("=" * 60)
|
||||
print("Generating Sample Data for LlamaSheets + Coding Agent Workflows")
|
||||
print("=" * 60)
|
||||
|
||||
# Create output directory
|
||||
output_dir = Path("input_data")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Generate data for each workflow
|
||||
generate_workflow_1_data(output_dir)
|
||||
generate_workflow_2_data(output_dir)
|
||||
generate_workflow_3_data(output_dir)
|
||||
generate_workflow_4_data(output_dir)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ All sample data generated!")
|
||||
print("=" * 60)
|
||||
print(f"\nFiles created in {output_dir.absolute()}:")
|
||||
print("\nWorkflow 1 (Understanding a New Spreadsheet):")
|
||||
print(" • financial_report_q1.xlsx")
|
||||
print("\nWorkflow 2 (Generating Analysis Scripts):")
|
||||
print(" • sales_northeast.xlsx")
|
||||
print(" • sales_southeast.xlsx")
|
||||
print(" • sales_west.xlsx")
|
||||
print("\nWorkflow 3 (Using Cell Metadata):")
|
||||
print(" • company_budget_2024.xlsx")
|
||||
print("\nWorkflow 4 (Complete Automation):")
|
||||
print(" • sales_weekly.xlsx")
|
||||
print("\nYou can now use these files with the workflows in the documentation!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,5 @@
|
||||
llama-cloud-services # LlamaSheets SDK
|
||||
pandas>=2.0.0
|
||||
pyarrow>=12.0.0
|
||||
openpyxl>=3.0.0 # For Excel file support
|
||||
matplotlib>=3.7.0 # For visualizations (optional)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Helper script to extract spreadsheets using LlamaSheets."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import dotenv
|
||||
from pathlib import Path
|
||||
|
||||
from llama_cloud_services.beta.sheets import LlamaSheets
|
||||
from llama_cloud_services.beta.sheets.types import (
|
||||
SpreadsheetParsingConfig,
|
||||
SpreadsheetResultType,
|
||||
)
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
async def extract_spreadsheet(
|
||||
file_path: str, output_dir: str = "data", generate_metadata: bool = True
|
||||
) -> dict:
|
||||
"""Extract a spreadsheet using LlamaSheets."""
|
||||
|
||||
client = LlamaSheets(
|
||||
base_url="https://api.cloud.llamaindex.ai",
|
||||
api_key=os.getenv("LLAMA_CLOUD_API_KEY"),
|
||||
)
|
||||
|
||||
print(f"Extracting {file_path}...")
|
||||
|
||||
# Extract regions
|
||||
config = SpreadsheetParsingConfig(
|
||||
sheet_names=None, # Extract all sheets
|
||||
generate_additional_metadata=generate_metadata,
|
||||
)
|
||||
|
||||
job_result = await client.aextract_regions(file_path, config=config)
|
||||
|
||||
print(f"Extracted {len(job_result.regions)} region(s)")
|
||||
|
||||
# Create output directory
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Get base name for files
|
||||
base_name = Path(file_path).stem
|
||||
|
||||
# Save job metadata
|
||||
job_metadata_path = output_path / f"{base_name}_job_metadata.json"
|
||||
with open(job_metadata_path, "w") as f:
|
||||
json.dump(job_result.model_dump(mode="json"), f, indent=2)
|
||||
print(f"Saved job metadata to {job_metadata_path}")
|
||||
|
||||
# Download each region
|
||||
for idx, region in enumerate(job_result.regions, 1):
|
||||
sheet_name = region.sheet_name.replace(" ", "_")
|
||||
|
||||
# Download region data
|
||||
region_bytes = await client.adownload_region_result(
|
||||
job_id=job_result.id,
|
||||
region_id=region.region_id,
|
||||
result_type=region.region_type,
|
||||
)
|
||||
|
||||
region_path = output_path / f"{base_name}_region_{idx}_{sheet_name}.parquet"
|
||||
with open(region_path, "wb") as f:
|
||||
f.write(region_bytes)
|
||||
print(f" Table {idx}: {region_path}")
|
||||
|
||||
# Download metadata
|
||||
metadata_bytes = await client.adownload_region_result(
|
||||
job_id=job_result.id,
|
||||
region_id=region.region_id,
|
||||
result_type=SpreadsheetResultType.CELL_METADATA,
|
||||
)
|
||||
|
||||
metadata_path = output_path / f"{base_name}_metadata_{idx}_{sheet_name}.parquet"
|
||||
with open(metadata_path, "wb") as f:
|
||||
f.write(metadata_bytes)
|
||||
print(f" Metadata {idx}: {metadata_path}")
|
||||
|
||||
print(f"\nAll files saved to {output_path}/")
|
||||
|
||||
return job_result.model_dump(mode="json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python scripts/extract.py <spreadsheet_file>")
|
||||
sys.exit(1)
|
||||
|
||||
file_path = sys.argv[1]
|
||||
|
||||
if not Path(file_path).exists():
|
||||
print(f"❌ File not found: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
result = asyncio.run(extract_spreadsheet(file_path))
|
||||
print(f"\n✅ Extraction complete! Job ID: {result['id']}")
|
||||
@@ -0,0 +1,278 @@
|
||||
"""
|
||||
Generate sample spreadsheets for LlamaSheets + LlamaIndex Agent workflows.
|
||||
|
||||
This script creates example Excel files that demonstrate different use cases:
|
||||
1. Simple data table (for Workflow 1)
|
||||
2. Regional sales data (for Workflow 2)
|
||||
3. Complex budget with formatting (for Workflow 3)
|
||||
4. Weekly sales report (for Workflow 4)
|
||||
|
||||
Usage:
|
||||
python generate_sample_data.py
|
||||
"""
|
||||
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, PatternFill, Alignment
|
||||
|
||||
|
||||
def generate_workflow_1_data(output_dir: Path) -> None:
|
||||
"""Generate simple financial report for Workflow 1."""
|
||||
print("📊 Generating Workflow 1: financial_report_q1.xlsx")
|
||||
|
||||
# Create sample quarterly data
|
||||
months = ["January", "February", "March"]
|
||||
categories = ["Revenue", "Cost of Goods Sold", "Operating Expenses", "Net Income"]
|
||||
|
||||
data = []
|
||||
for category in categories:
|
||||
row: dict[str, str | int] = {"Category": category}
|
||||
for month in months:
|
||||
if category == "Revenue":
|
||||
value = random.randint(80000, 120000)
|
||||
elif category == "Cost of Goods Sold":
|
||||
value = random.randint(30000, 50000)
|
||||
elif category == "Operating Expenses":
|
||||
value = random.randint(20000, 35000)
|
||||
else: # Net Income
|
||||
value = int(
|
||||
int(row.get("January", 0))
|
||||
+ int(row.get("February", 0))
|
||||
+ int(row.get("March", 0))
|
||||
)
|
||||
value = random.randint(15000, 40000)
|
||||
row[month] = value
|
||||
data.append(row)
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Write to Excel
|
||||
output_file = output_dir / "financial_report_q1.xlsx"
|
||||
with pd.ExcelWriter(output_file, engine="openpyxl") as writer:
|
||||
df.to_excel(writer, sheet_name="Q1 Summary", index=False)
|
||||
|
||||
# Format it nicely
|
||||
worksheet = writer.sheets["Q1 Summary"]
|
||||
for cell in worksheet[1]: # Header row
|
||||
cell.font = Font(bold=True)
|
||||
cell.fill = PatternFill(
|
||||
start_color="4F81BD", end_color="4F81BD", fill_type="solid"
|
||||
)
|
||||
cell.font = Font(color="FFFFFF", bold=True)
|
||||
|
||||
print(f" ✅ Created {output_file}")
|
||||
|
||||
|
||||
def generate_workflow_2_data(output_dir: Path) -> None:
|
||||
"""Generate regional sales data for Workflow 2."""
|
||||
print("\n📊 Generating Workflow 2: Regional sales data")
|
||||
|
||||
regions = ["northeast", "southeast", "west"]
|
||||
products = ["Widget A", "Widget B", "Widget C", "Gadget X", "Gadget Y"]
|
||||
|
||||
for region in regions:
|
||||
data = []
|
||||
start_date = datetime(2024, 1, 1)
|
||||
|
||||
# Generate 90 days of sales data
|
||||
for day in range(90):
|
||||
date = start_date + timedelta(days=day)
|
||||
# Random number of sales per day (3-8)
|
||||
for _ in range(random.randint(3, 8)):
|
||||
product = random.choice(products)
|
||||
units_sold = random.randint(1, 20)
|
||||
price_per_unit = random.randint(50, 200)
|
||||
revenue = units_sold * price_per_unit
|
||||
|
||||
data.append(
|
||||
{
|
||||
"Date": date.strftime("%Y-%m-%d"),
|
||||
"Product": product,
|
||||
"Units_Sold": units_sold,
|
||||
"Revenue": revenue,
|
||||
}
|
||||
)
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Write to Excel
|
||||
output_file = output_dir / f"sales_{region}.xlsx"
|
||||
df.to_excel(output_file, sheet_name="Sales", index=False)
|
||||
print(f" ✅ Created {output_file} ({len(df)} rows)")
|
||||
|
||||
|
||||
def generate_workflow_3_data(output_dir: Path) -> None:
|
||||
"""Generate complex budget spreadsheet with formatting for Workflow 3."""
|
||||
print("\n📊 Generating Workflow 3: company_budget_2024.xlsx")
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Budget"
|
||||
|
||||
# Define departments with colors
|
||||
departments = {
|
||||
"Engineering": "C6E0B4",
|
||||
"Marketing": "FFD966",
|
||||
"Sales": "F4B084",
|
||||
"Operations": "B4C7E7",
|
||||
}
|
||||
|
||||
# Define categories
|
||||
categories = {
|
||||
"Personnel": ["Salaries", "Benefits", "Training"],
|
||||
"Infrastructure": ["Office Rent", "Equipment", "Software Licenses"],
|
||||
"Operations": ["Travel", "Supplies", "Miscellaneous"],
|
||||
}
|
||||
|
||||
# Styles
|
||||
header_font = Font(bold=True, size=12)
|
||||
category_font = Font(bold=True, size=11)
|
||||
|
||||
row = 1
|
||||
|
||||
# Title
|
||||
ws.merge_cells(f"A{row}:E{row}")
|
||||
ws[f"A{row}"] = "2024 Annual Budget"
|
||||
ws[f"A{row}"].font = Font(bold=True, size=14)
|
||||
ws[f"A{row}"].alignment = Alignment(horizontal="center")
|
||||
row += 2
|
||||
|
||||
# Headers
|
||||
ws[f"A{row}"] = "Category"
|
||||
ws[f"B{row}"] = "Item"
|
||||
for i, dept in enumerate(departments.keys()):
|
||||
ws.cell(row, 3 + i, dept)
|
||||
ws.cell(row, 3 + i).font = header_font
|
||||
|
||||
for cell in ws[row]:
|
||||
cell.font = header_font
|
||||
row += 1
|
||||
|
||||
# Data
|
||||
for category, items in categories.items():
|
||||
# Category header (bold)
|
||||
ws[f"A{row}"] = category
|
||||
ws[f"A{row}"].font = category_font
|
||||
row += 1
|
||||
|
||||
# Items with department budgets
|
||||
for item in items:
|
||||
ws[f"A{row}"] = ""
|
||||
ws[f"B{row}"] = item
|
||||
|
||||
# Add budget amounts for each department (with color)
|
||||
for i, (dept, color) in enumerate(departments.items()):
|
||||
amount = random.randint(5000, 50000)
|
||||
cell = ws.cell(row, 3 + i, amount)
|
||||
cell.fill = PatternFill(
|
||||
start_color=color, end_color=color, fill_type="solid"
|
||||
)
|
||||
cell.number_format = "$#,##0"
|
||||
|
||||
row += 1
|
||||
|
||||
row += 1 # Blank row between categories
|
||||
|
||||
# Adjust column widths
|
||||
ws.column_dimensions["A"].width = 20
|
||||
ws.column_dimensions["B"].width = 25
|
||||
for i in range(len(departments)):
|
||||
ws.column_dimensions[chr(67 + i)].width = 15 # C, D, E, F
|
||||
|
||||
output_file = output_dir / "company_budget_2024.xlsx"
|
||||
wb.save(output_file)
|
||||
print(f" ✅ Created {output_file}")
|
||||
print(" • Bold categories, colored departments, merged title cell")
|
||||
|
||||
|
||||
def generate_workflow_4_data(output_dir: Path) -> None:
|
||||
"""Generate weekly sales report for Workflow 4."""
|
||||
print("\n📊 Generating Workflow 4: sales_weekly.xlsx")
|
||||
|
||||
products = [
|
||||
"Product A",
|
||||
"Product B",
|
||||
"Product C",
|
||||
"Product D",
|
||||
"Product E",
|
||||
"Product F",
|
||||
"Product G",
|
||||
"Product H",
|
||||
]
|
||||
|
||||
# Generate one week of data
|
||||
data = []
|
||||
start_date = datetime(2024, 11, 4) # Monday
|
||||
|
||||
for day in range(7):
|
||||
date = start_date + timedelta(days=day)
|
||||
# Each product has 3-10 transactions per day
|
||||
for product in products:
|
||||
for _ in range(random.randint(3, 10)):
|
||||
units = random.randint(1, 15)
|
||||
price = random.randint(20, 150)
|
||||
revenue = units * price
|
||||
|
||||
data.append(
|
||||
{
|
||||
"Date": date.strftime("%Y-%m-%d"),
|
||||
"Product": product,
|
||||
"Units": units,
|
||||
"Revenue": revenue,
|
||||
}
|
||||
)
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Write to Excel with some formatting
|
||||
output_file = output_dir / "sales_weekly.xlsx"
|
||||
with pd.ExcelWriter(output_file, engine="openpyxl") as writer:
|
||||
df.to_excel(writer, sheet_name="Weekly Sales", index=False)
|
||||
|
||||
# Format header
|
||||
worksheet = writer.sheets["Weekly Sales"]
|
||||
for cell in worksheet[1]:
|
||||
cell.font = Font(bold=True)
|
||||
|
||||
print(f" ✅ Created {output_file} ({len(df)} rows)")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Generate all sample data files."""
|
||||
print("=" * 60)
|
||||
print("Generating Sample Data for LlamaSheets + Coding Agent Workflows")
|
||||
print("=" * 60)
|
||||
|
||||
# Create output directory
|
||||
output_dir = Path("input_data")
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Generate data for each workflow
|
||||
generate_workflow_1_data(output_dir)
|
||||
generate_workflow_2_data(output_dir)
|
||||
generate_workflow_3_data(output_dir)
|
||||
generate_workflow_4_data(output_dir)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ All sample data generated!")
|
||||
print("=" * 60)
|
||||
print(f"\nFiles created in {output_dir.absolute()}:")
|
||||
print("\nWorkflow 1 (Understanding a New Spreadsheet):")
|
||||
print(" • financial_report_q1.xlsx")
|
||||
print("\nWorkflow 2 (Generating Analysis Scripts):")
|
||||
print(" • sales_northeast.xlsx")
|
||||
print(" • sales_southeast.xlsx")
|
||||
print(" • sales_west.xlsx")
|
||||
print("\nWorkflow 3 (Using Cell Metadata):")
|
||||
print(" • company_budget_2024.xlsx")
|
||||
print("\nWorkflow 4 (Complete Automation):")
|
||||
print(" • sales_weekly.xlsx")
|
||||
print("\nYou can now use these files with the workflows in the documentation!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
LlamaSheets Agent with LlamaIndex
|
||||
|
||||
This example shows how to build an agent that can work with spreadsheet data
|
||||
extracted by LlamaSheets using Python code execution.
|
||||
|
||||
The agent has minimal tools but maximum flexibility - it can execute arbitrary
|
||||
pandas code against the extracted data, similar to a coding agent.
|
||||
|
||||
NOTE: Code execution should be handled safely in a sandboxed environment for security.
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import dotenv
|
||||
import pandas as pd
|
||||
from llama_index.core.agent import FunctionAgent, ToolCall, ToolCallResult, AgentStream
|
||||
from llama_index.llms.openai import OpenAI
|
||||
from workflows import Context
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
# Global context for executed code
|
||||
_code_context: Dict[str, Any] = {}
|
||||
|
||||
|
||||
# Helper function for initial agent context
|
||||
def list_extracted_data(data_dir: str = "data") -> str:
|
||||
"""
|
||||
List all regions and metadata files extracted by LlamaSheets.
|
||||
|
||||
This helps discover what data is available to work with.
|
||||
|
||||
Args:
|
||||
data_dir: Directory containing extracted parquet files (default: "data")
|
||||
|
||||
Returns:
|
||||
JSON string with information about available files
|
||||
"""
|
||||
data_path = Path(data_dir)
|
||||
|
||||
if not data_path.exists():
|
||||
return json.dumps({"error": f"Data directory '{data_dir}' not found"})
|
||||
|
||||
# Find all parquet and metadata files
|
||||
region_files = list(data_path.glob("*_region_*.parquet"))
|
||||
job_metadata_files = list(data_path.glob("*_job_metadata.json"))
|
||||
|
||||
regions = []
|
||||
for region_file in region_files:
|
||||
# Quick peek at dimensions
|
||||
df = pd.read_parquet(region_file)
|
||||
|
||||
# Find corresponding metadata file
|
||||
base_name = region_file.stem.replace("_region_", "_metadata_")
|
||||
metadata_path = region_file.parent / f"{base_name}.parquet"
|
||||
|
||||
regions.append(
|
||||
{
|
||||
"region_file": str(region_file),
|
||||
"metadata_file": str(metadata_path) if metadata_path.exists() else None,
|
||||
"shape": {"rows": len(df), "columns": len(df.columns)},
|
||||
"columns": list(df.columns),
|
||||
}
|
||||
)
|
||||
|
||||
result = {
|
||||
"data_directory": str(data_path.absolute()),
|
||||
"num_regions": len(regions),
|
||||
"regions": regions,
|
||||
"job_metadata_files": [str(f) for f in job_metadata_files],
|
||||
}
|
||||
|
||||
return json.dumps(result, indent=2)
|
||||
|
||||
|
||||
# Agent tool for code execution against dataframes
|
||||
def execute_code(code: str) -> str:
|
||||
"""
|
||||
Execute Python pandas code against LlamaSheets extracted data.
|
||||
|
||||
This tool allows flexible data analysis by executing arbitrary pandas code.
|
||||
You can load parquet files, manipulate dataframes, and return results.
|
||||
|
||||
The code executes in a context where:
|
||||
- pandas is available as 'pd'
|
||||
- json is available for formatting output
|
||||
|
||||
Args:
|
||||
code: Python code to execute. Any print() statements or stdout/stderr
|
||||
will be captured and returned. Optionally set a 'result' variable
|
||||
for structured output.
|
||||
|
||||
Returns:
|
||||
String containing:
|
||||
- Any stdout/stderr output from the code execution
|
||||
- The 'result' variable if it was set (formatted appropriately)
|
||||
- Error message if execution failed
|
||||
|
||||
Example usage:
|
||||
code = '''
|
||||
# Load and inspect data
|
||||
df = pd.read_parquet("data/sales_region_1.parquet")
|
||||
print(f"Loaded {len(df)} rows")
|
||||
|
||||
result = {
|
||||
"shape": df.shape,
|
||||
"columns": list(df.columns),
|
||||
"sample": df.head(3).to_dict(orient="records")
|
||||
}
|
||||
'''
|
||||
"""
|
||||
global _code_context
|
||||
|
||||
# Capture stdout and stderr
|
||||
stdout_capture = io.StringIO()
|
||||
stderr_capture = io.StringIO()
|
||||
old_stdout = sys.stdout
|
||||
old_stderr = sys.stderr
|
||||
|
||||
try:
|
||||
# Redirect stdout/stderr
|
||||
sys.stdout = stdout_capture
|
||||
sys.stderr = stderr_capture
|
||||
|
||||
# Create execution context with pandas, json, and previously loaded dfs
|
||||
exec_context = {
|
||||
"pd": pd,
|
||||
"json": json,
|
||||
"Path": Path,
|
||||
**_code_context, # Include previously loaded dataframes
|
||||
}
|
||||
|
||||
# Execute the code
|
||||
exec(code, exec_context)
|
||||
|
||||
# Update global context with any new variables (excluding built-ins and modules)
|
||||
for key, value in exec_context.items():
|
||||
if not key.startswith("_") and key not in ["pd", "json", "Path"]:
|
||||
_code_context[key] = value
|
||||
|
||||
# Restore stdout/stderr
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
|
||||
# Collect output
|
||||
stdout_output = stdout_capture.getvalue()
|
||||
stderr_output = stderr_capture.getvalue()
|
||||
|
||||
output_parts = []
|
||||
|
||||
# Add stdout if any
|
||||
if stdout_output:
|
||||
output_parts.append(f"<stdout>{stdout_output}</stdout>")
|
||||
|
||||
# Add stderr if any
|
||||
if stderr_output:
|
||||
output_parts.append(f"<stderr>{stderr_output}</stderr>")
|
||||
|
||||
# Try to get a result (if code set a 'result' variable)
|
||||
if "result" in exec_context:
|
||||
result = exec_context["result"]
|
||||
result_str = None
|
||||
|
||||
if isinstance(result, pd.DataFrame):
|
||||
# Convert DataFrame to readable format
|
||||
result_str = result.to_string()
|
||||
elif isinstance(result, (dict, list)):
|
||||
result_str = json.dumps(result, indent=2, default=str)
|
||||
else:
|
||||
result_str = str(result)
|
||||
|
||||
if result_str:
|
||||
output_parts.append(f"<result_var>{result_str}</result_var>")
|
||||
|
||||
# Return combined output or success message
|
||||
if output_parts:
|
||||
return "\n\n".join(output_parts)
|
||||
else:
|
||||
return "Code executed successfully (no output or result)"
|
||||
|
||||
except Exception as e:
|
||||
# Restore stdout/stderr in case of error
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
|
||||
# Get any partial output
|
||||
stdout_output = stdout_capture.getvalue()
|
||||
stderr_output = stderr_capture.getvalue()
|
||||
|
||||
error_parts = []
|
||||
if stdout_output:
|
||||
error_parts.append(f"=== STDOUT (before error) ===\n{stdout_output}")
|
||||
if stderr_output:
|
||||
error_parts.append(f"=== STDERR (before error) ===\n{stderr_output}")
|
||||
|
||||
error_parts.append(f"=== ERROR ===\n{str(e)}")
|
||||
error_parts.append(f"\n=== CODE ===\n{code}")
|
||||
|
||||
return "\n\n".join(error_parts)
|
||||
|
||||
|
||||
def create_llamasheets_agent(
|
||||
llm_model: str = "gpt-4.1", api_key: Optional[str] = None
|
||||
) -> FunctionAgent:
|
||||
# Initialize LLM
|
||||
llm = OpenAI(model=llm_model, api_key=api_key)
|
||||
|
||||
# Create tools list
|
||||
tools = [execute_code]
|
||||
|
||||
# System prompt to guide the agent
|
||||
available_regions = list_extracted_data()
|
||||
system_prompt = f"""You are an AI assistant that helps analyze spreadsheet data extracted by LlamaSheets.
|
||||
|
||||
LlamaSheets extracts messy spreadsheets into clean parquet files with two types of outputs:
|
||||
1. Region files (*_region_*.parquet) - The actual data with columns and rows
|
||||
2. Metadata files (*_metadata_*.parquet) - Rich cell-level metadata including:
|
||||
- Formatting: font_bold, font_italic, font_size, background_color_rgb
|
||||
- Position: row_number, column_number, coordinate
|
||||
- Type detection: data_type, is_date_like, is_percentage, is_currency
|
||||
- Layout: is_in_first_row, is_merged_cell, horizontal_alignment
|
||||
|
||||
You have access to tools that allow you to execute Python pandas code against these files.
|
||||
Use these tools to load the parquet files, analyze the data, and return results.
|
||||
|
||||
Key tips:
|
||||
- Bold cells in metadata often indicate headers
|
||||
- Background colors often indicate groupings or departments
|
||||
- Load both region and metadata files for complete analysis
|
||||
- Write clear pandas code - you have full pandas functionality available
|
||||
- Store results in variables for reuse across multiple code executions
|
||||
|
||||
Existing Processed Regions:
|
||||
{available_regions}
|
||||
"""
|
||||
|
||||
# Configure agent
|
||||
return FunctionAgent(tools=tools, llm=llm, system_prompt=system_prompt)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Example of using the LlamaSheets agent."""
|
||||
|
||||
# Create the agent
|
||||
agent = create_llamasheets_agent()
|
||||
ctx = Context(agent)
|
||||
|
||||
# Example queries the agent can handle:
|
||||
queries = [
|
||||
# Discovery
|
||||
"What spreadsheet data is available?",
|
||||
# Simple analysis
|
||||
"Load the sales data and show me the first few rows with column info",
|
||||
# Using metadata
|
||||
"Find all bold cells in the metadata - these are likely headers",
|
||||
]
|
||||
|
||||
# Example: Run a query
|
||||
for query in queries:
|
||||
print(f"\n=== Query: {query} ===")
|
||||
handler = agent.run(query, ctx=ctx)
|
||||
async for ev in handler.stream_events():
|
||||
if isinstance(ev, ToolCall):
|
||||
tool_kwargs_str = (
|
||||
str(ev.tool_kwargs)[:500] + " ..."
|
||||
if len(str(ev.tool_kwargs)) > 500
|
||||
else str(ev.tool_kwargs)
|
||||
)
|
||||
print(f"\n[Tool Call] {ev.tool_name} with args:\n{tool_kwargs_str}\n\n")
|
||||
elif isinstance(ev, ToolCallResult):
|
||||
result_str = (
|
||||
str(ev.tool_output)[:500] + " ..."
|
||||
if len(str(ev.tool_output)) > 500
|
||||
else str(ev.tool_output)
|
||||
)
|
||||
print(f"\n[Tool Result] {ev.tool_name}:\n{result_str}\n\n")
|
||||
elif isinstance(ev, AgentStream):
|
||||
print(ev.delta, end="", flush=True)
|
||||
|
||||
_ = await handler
|
||||
print("\n=== End Query ===\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,7 @@
|
||||
llama-cloud-services # LlamaSheets SDK
|
||||
llama-index-core
|
||||
llama-index-llms-openai
|
||||
pandas>=2.0.0
|
||||
pyarrow>=12.0.0
|
||||
openpyxl>=3.0.0 # For Excel file support
|
||||
matplotlib>=3.7.0 # For visualizations (optional)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Helper script to extract spreadsheets using LlamaSheets."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import dotenv
|
||||
from pathlib import Path
|
||||
|
||||
from llama_cloud_services.beta.sheets import LlamaSheets
|
||||
from llama_cloud_services.beta.sheets.types import (
|
||||
SpreadsheetParsingConfig,
|
||||
SpreadsheetResultType,
|
||||
)
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
|
||||
async def extract_spreadsheet(
|
||||
file_path: str, output_dir: str = "data", generate_metadata: bool = True
|
||||
) -> dict:
|
||||
"""Extract a spreadsheet using LlamaSheets."""
|
||||
|
||||
client = LlamaSheets(
|
||||
base_url="https://api.cloud.llamaindex.ai",
|
||||
api_key=os.getenv("LLAMA_CLOUD_API_KEY"),
|
||||
)
|
||||
|
||||
print(f"Extracting {file_path}...")
|
||||
|
||||
# Extract regions
|
||||
config = SpreadsheetParsingConfig(
|
||||
sheet_names=None, # Extract all sheets
|
||||
generate_additional_metadata=generate_metadata,
|
||||
)
|
||||
|
||||
job_result = await client.aextract_regions(file_path, config=config)
|
||||
|
||||
print(f"Extracted {len(job_result.regions)} region(s)")
|
||||
|
||||
# Create output directory
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Get base name for files
|
||||
base_name = Path(file_path).stem
|
||||
|
||||
# Save job metadata
|
||||
job_metadata_path = output_path / f"{base_name}_job_metadata.json"
|
||||
with open(job_metadata_path, "w") as f:
|
||||
json.dump(job_result.model_dump(mode="json"), f, indent=2)
|
||||
print(f"Saved job metadata to {job_metadata_path}")
|
||||
|
||||
# Download each region
|
||||
for idx, region in enumerate(job_result.regions, 1):
|
||||
sheet_name = region.sheet_name.replace(" ", "_")
|
||||
|
||||
# Download region data
|
||||
region_bytes = await client.adownload_region_result(
|
||||
job_id=job_result.id,
|
||||
region_id=region.region_id,
|
||||
result_type=region.region_type,
|
||||
)
|
||||
|
||||
region_path = output_path / f"{base_name}_region_{idx}_{sheet_name}.parquet"
|
||||
with open(region_path, "wb") as f:
|
||||
f.write(region_bytes)
|
||||
print(f" Table {idx}: {region_path}")
|
||||
|
||||
# Download metadata
|
||||
metadata_bytes = await client.adownload_region_result(
|
||||
job_id=job_result.id,
|
||||
region_id=region.region_id,
|
||||
result_type=SpreadsheetResultType.CELL_METADATA,
|
||||
)
|
||||
|
||||
metadata_path = output_path / f"{base_name}_metadata_{idx}_{sheet_name}.parquet"
|
||||
with open(metadata_path, "wb") as f:
|
||||
f.write(metadata_bytes)
|
||||
print(f" Metadata {idx}: {metadata_path}")
|
||||
|
||||
print(f"\nAll files saved to {output_path}/")
|
||||
|
||||
return job_result.model_dump(mode="json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python scripts/extract.py <spreadsheet_file>")
|
||||
sys.exit(1)
|
||||
|
||||
file_path = sys.argv[1]
|
||||
|
||||
if not Path(file_path).exists():
|
||||
print(f"❌ File not found: {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
result = asyncio.run(extract_spreadsheet(file_path))
|
||||
print(f"\n✅ Extraction complete! Job ID: {result['id']}")
|
||||
@@ -5,14 +5,21 @@
|
||||
"private": true,
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"scripts": {
|
||||
"pre-commit-version": "pnpm changeset",
|
||||
"version": "./scripts/changeset-version.py version",
|
||||
"publish": "./scripts/changeset-version.py publish --tag"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.6.2",
|
||||
"lint-staged": "^15.4.2"
|
||||
"lint-staged": "^15.4.2",
|
||||
"@changesets/cli": "^2.29.5",
|
||||
"changesets": "^1.0.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"ts/llama_cloud_services/src/**/*.{ts,tsx,js,jsx}": [
|
||||
"pnpm --filter llama-cloud-services exec eslint --fix",
|
||||
"pnpm --filter llama-cloud-services exec prettier --write"
|
||||
"pnpm --filter llama-cloud-services exec prettier --write src/ tests/"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912"
|
||||
|
||||
@@ -147,7 +147,7 @@ documents = SimpleDirectoryReader(
|
||||
).load_data()
|
||||
```
|
||||
|
||||
Full documentation for `SimpleDirectoryReader` can be found on the [LlamaIndex Documentation](https://docs.llamaindex.ai/en/stable/module_guides/loading/simpledirectoryreader.html).
|
||||
Full documentation for `SimpleDirectoryReader` can be found on the [LlamaIndex Documentation](https://developers.llamaindex.ai/python/framework/module_guides/loading/simpledirectoryreader/).
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
packages:
|
||||
- "ts/**"
|
||||
- "ts/*"
|
||||
- "py"
|
||||
- "py/*"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# llama-cloud-services-py
|
||||
|
||||
## 0.6.82
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- bfaec79: Update for new page number params
|
||||
|
||||
## 0.6.81
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f3233de: Propagate retrieval metadata to retriever nodes
|
||||
|
||||
## 0.6.80
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0506c88: Moved ClassifyClient to LlamaClassify (backward compatible)
|
||||
|
||||
## 0.6.79
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e020e3e: Remove unneeded organization_id param from beta classifier client
|
||||
|
||||
## 0.6.78
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9f1ef4e: Fix extract
|
||||
|
||||
## 0.6.77
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 407292b: Now return partial results on job failure
|
||||
|
||||
## 0.6.76
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4f24f53: Add aggressive_table_extraction flag in python sdk
|
||||
|
||||
## 0.6.75
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- f81532e: Safest types possible for parse
|
||||
|
||||
## 0.6.74
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1bf5223: Fix default bbox values
|
||||
- 24166dc: Now only escape single dollar signs - preserve double for latex equations
|
||||
|
||||
## 0.6.73
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e6a7939: Loosen packaging dep requirement
|
||||
|
||||
## 0.6.72
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ad6734b: Fixup and test versioning
|
||||
|
||||
## 0.6.71
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 51011b9: Escape dollar signs in jupyter notebooks
|
||||
|
||||
## 0.6.70
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d028397: Update llama-cloud api version, and integrate with agent data deletion
|
||||
@@ -15,4 +15,4 @@ test: ## Run unit tests via pytest
|
||||
|
||||
.PHONY: e2e
|
||||
e2e: ## Run all tests. Run with high parallelism using xdist since tests are bottlenecked bound by the slow backend parsing
|
||||
uv run pytest -v -n 32 tests/
|
||||
uv run pytest -v -n 32 --timeout=300 --session-timeout=1740 tests/
|
||||
|
||||
@@ -9,7 +9,6 @@ This repository contains the code for hand-written SDKs and clients for interact
|
||||
This includes:
|
||||
|
||||
- [LlamaParse](../parse.md) - A GenAI-native document parser that can parse complex document data for any downstream LLM use case (Agents, RAG, data processing, etc.).
|
||||
- [LlamaReport (beta/invite-only)](../report.md) - A prebuilt agentic report builder that can be used to build reports from a variety of data sources.
|
||||
- [LlamaExtract](../extract.md) - A prebuilt agentic data extractor that can be used to transform data into a structured JSON representation.
|
||||
- [LlamaCloud Index](../index.md) - A widely customizable and fully automated document ingestion pipeline that also serves retrieval purposes.
|
||||
|
||||
@@ -28,14 +27,12 @@ Then, you can use the services in your code:
|
||||
```python
|
||||
from llama_cloud_services import (
|
||||
LlamaParse,
|
||||
LlamaReport,
|
||||
LlamaExtract,
|
||||
LlamaCloudIndex,
|
||||
)
|
||||
from llama_cloud_services import LlamaParse, LlamaReport, LlamaExtract
|
||||
from llama_cloud_services import LlamaParse, LlamaExtract
|
||||
|
||||
parser = LlamaParse(api_key="YOUR_API_KEY")
|
||||
report = LlamaReport(api_key="YOUR_API_KEY")
|
||||
extract = LlamaExtract(api_key="YOUR_API_KEY")
|
||||
index = LlamaCloudIndex(
|
||||
"my_first_index", project_name="default", api_key="YOUR_API_KEY"
|
||||
@@ -45,7 +42,6 @@ index = LlamaCloudIndex(
|
||||
See the quickstart guides for each service for more information:
|
||||
|
||||
- [LlamaParse](../parse.md)
|
||||
- [LlamaReport (beta/invite-only)](../report.md)
|
||||
- [LlamaExtract](../extract.md)
|
||||
- [LlamaCloud Index](../index.md)
|
||||
|
||||
@@ -58,13 +54,11 @@ You can also create your API key in the EU region [here](https://cloud.eu.llamai
|
||||
```python
|
||||
from llama_cloud_services import (
|
||||
LlamaParse,
|
||||
LlamaReport,
|
||||
LlamaExtract,
|
||||
EU_BASE_URL,
|
||||
)
|
||||
|
||||
parser = LlamaParse(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
|
||||
report = LlamaReport(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
|
||||
extract = LlamaExtract(api_key="YOUR_API_KEY", base_url=EU_BASE_URL)
|
||||
index = LlamaCloudIndex(
|
||||
"my_first_index",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from llama_cloud_services.parse import LlamaParse
|
||||
from llama_cloud_services.report import ReportClient, LlamaReport
|
||||
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent, SourceText
|
||||
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent
|
||||
from llama_cloud_services.utils import SourceText, FileInput
|
||||
from llama_cloud_services.constants import EU_BASE_URL
|
||||
from llama_cloud_services.index import (
|
||||
LlamaCloudCompositeRetriever,
|
||||
@@ -10,11 +10,10 @@ from llama_cloud_services.index import (
|
||||
|
||||
__all__ = [
|
||||
"LlamaParse",
|
||||
"ReportClient",
|
||||
"LlamaReport",
|
||||
"LlamaExtract",
|
||||
"ExtractionAgent",
|
||||
"SourceText",
|
||||
"FileInput",
|
||||
"EU_BASE_URL",
|
||||
"LlamaCloudIndex",
|
||||
"LlamaCloudRetriever",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import os
|
||||
from typing import Any, Dict, Generic, List, Optional, Type
|
||||
|
||||
from llama_cloud import (
|
||||
AgentData,
|
||||
PaginatedResponseAgentData,
|
||||
PaginatedResponseAggregateGroup,
|
||||
)
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from tenacity import (
|
||||
WrappedFn,
|
||||
@@ -86,7 +91,7 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
client=llama_client,
|
||||
type=ExtractedPerson,
|
||||
collection="extracted_people",
|
||||
agent_url_id="person-extraction-agent"
|
||||
deployment_name="person-extraction-agent"
|
||||
)
|
||||
|
||||
# Create data
|
||||
@@ -109,10 +114,12 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
self,
|
||||
type: Type[AgentDataT],
|
||||
collection: str = "default",
|
||||
agent_url_id: Optional[str] = None,
|
||||
deployment_name: Optional[str] = None,
|
||||
client: Optional[AsyncLlamaCloud] = None,
|
||||
token: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
# deprecated, use deployment_name instead
|
||||
agent_url_id: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initialize the AsyncAgentDataClient.
|
||||
@@ -123,11 +130,11 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
collection: Named collection within the agent for organizing data.
|
||||
Defaults to "default". Collections allow logical separation of
|
||||
different data types or workflows within the same agent.
|
||||
agent_url_id: Unique identifier for the agent. This normally appears in the
|
||||
url of an agent within the llama cloud platform. If not provided,
|
||||
will attempt to use the LLAMA_DEPLOY_DEPLOYMENT_NAME environment
|
||||
variable. Data can only be added to an already existing agent in the
|
||||
platform.
|
||||
deployment_name: Unique identifier for the agent deployment. This normally
|
||||
appears in the URL of an agent within the Llama Cloud platform. If not
|
||||
provided, will attempt to use the LLAMA_DEPLOY_DEPLOYMENT_NAME
|
||||
environment variable. Data can only be added to an already existing
|
||||
agent in the platform.
|
||||
client: AsyncLlamaCloud client instance for API communication. If not provided, will
|
||||
construct one from the provided api token and base url
|
||||
token: Llama Cloud API token. Reads from LLAMA_CLOUD_API_KEY if not provided
|
||||
@@ -135,15 +142,14 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
defaults to https://api.cloud.llamaindex.ai
|
||||
|
||||
Raises:
|
||||
ValueError: If agent_url_id is not provided and the
|
||||
ValueError: If deployment_name is not provided and the
|
||||
LLAMA_DEPLOY_DEPLOYMENT_NAME environment variable is not set
|
||||
|
||||
Note:
|
||||
The client automatically applies retry logic to all API calls with
|
||||
exponential backoff for timeout, connection, and HTTP status errors.
|
||||
"""
|
||||
|
||||
self.agent_url_id = agent_url_id or get_default_agent_id()
|
||||
self.deployment_name = deployment_name or agent_url_id or get_default_agent_id()
|
||||
|
||||
self.collection = collection
|
||||
if not client:
|
||||
@@ -156,15 +162,19 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
|
||||
@agent_data_retry
|
||||
async def get_item(self, item_id: str) -> TypedAgentData[AgentDataT]:
|
||||
raw_data = await self.client.beta.get_agent_data(
|
||||
raw_data = await self.untyped_get_item(item_id)
|
||||
return TypedAgentData.from_raw(raw_data, self.type)
|
||||
|
||||
@agent_data_retry
|
||||
async def untyped_get_item(self, item_id: str) -> AgentData:
|
||||
return await self.client.beta.get_agent_data(
|
||||
item_id=item_id,
|
||||
)
|
||||
return TypedAgentData.from_raw(raw_data, validator=self.type)
|
||||
|
||||
@agent_data_retry
|
||||
async def create_item(self, data: AgentDataT) -> TypedAgentData[AgentDataT]:
|
||||
raw_data = await self.client.beta.create_agent_data(
|
||||
agent_slug=self.agent_url_id,
|
||||
deployment_name=self.deployment_name,
|
||||
collection=self.collection,
|
||||
data=data.model_dump(),
|
||||
)
|
||||
@@ -184,6 +194,21 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
async def delete_item(self, item_id: str) -> None:
|
||||
await self.client.beta.delete_agent_data(item_id=item_id)
|
||||
|
||||
@agent_data_retry
|
||||
async def delete(
|
||||
self, filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None
|
||||
) -> int:
|
||||
"""
|
||||
Delete agent data by query, similar to search.
|
||||
Returns the number of deleted items.
|
||||
"""
|
||||
response = await self.client.beta.delete_agent_data_by_query_api_v_1_beta_agent_data_delete_post(
|
||||
deployment_name=self.deployment_name,
|
||||
collection=self.collection,
|
||||
filter=filter,
|
||||
)
|
||||
return response.deleted_count
|
||||
|
||||
@agent_data_retry
|
||||
async def search(
|
||||
self,
|
||||
@@ -210,9 +235,7 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
offset: Number of items to skip from the beginning. Defaults to 0.
|
||||
include_total: Whether to include the total count in the response. Defaults to False to improve performance. It's recommended to only request on the first page.
|
||||
"""
|
||||
raw = await self.client.beta.search_agent_data_api_v_1_beta_agent_data_search_post(
|
||||
agent_slug=self.agent_url_id,
|
||||
collection=self.collection,
|
||||
raw = await self.untyped_search(
|
||||
filter=filter,
|
||||
order_by=order_by,
|
||||
offset=offset,
|
||||
@@ -227,6 +250,25 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
total=raw.total_size,
|
||||
)
|
||||
|
||||
@agent_data_retry
|
||||
async def untyped_search(
|
||||
self,
|
||||
filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None,
|
||||
order_by: Optional[str] = None,
|
||||
offset: Optional[int] = None,
|
||||
page_size: Optional[int] = None,
|
||||
include_total: bool = False,
|
||||
) -> PaginatedResponseAgentData:
|
||||
return await self.client.beta.search_agent_data_api_v_1_beta_agent_data_search_post(
|
||||
deployment_name=self.deployment_name,
|
||||
collection=self.collection,
|
||||
filter=filter,
|
||||
order_by=order_by,
|
||||
offset=offset,
|
||||
page_size=page_size,
|
||||
include_total=include_total,
|
||||
)
|
||||
|
||||
@agent_data_retry
|
||||
async def aggregate(
|
||||
self,
|
||||
@@ -253,8 +295,38 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
offset: Number of groups to skip from the beginning. Defaults to 0.
|
||||
page_size: Maximum number of groups to return per page.
|
||||
"""
|
||||
raw = await self.client.beta.aggregate_agent_data_api_v_1_beta_agent_data_aggregate_post(
|
||||
agent_slug=self.agent_url_id,
|
||||
raw = await self.untyped_aggregate(
|
||||
filter=filter,
|
||||
group_by=group_by,
|
||||
count=count,
|
||||
first=first,
|
||||
order_by=order_by,
|
||||
offset=offset,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
return TypedAggregateGroupItems(
|
||||
items=[
|
||||
TypedAggregateGroup.from_raw(grp, validator=self.type)
|
||||
for grp in raw.items
|
||||
],
|
||||
has_more=raw.next_page_token is not None,
|
||||
total=raw.total_size,
|
||||
)
|
||||
|
||||
@agent_data_retry
|
||||
async def untyped_aggregate(
|
||||
self,
|
||||
filter: Optional[Dict[str, Dict[ComparisonOperator, Any]]] = None,
|
||||
group_by: Optional[List[str]] = None,
|
||||
count: Optional[bool] = None,
|
||||
first: Optional[bool] = None,
|
||||
order_by: Optional[str] = None,
|
||||
offset: Optional[int] = None,
|
||||
page_size: Optional[int] = None,
|
||||
) -> PaginatedResponseAggregateGroup:
|
||||
return await self.client.beta.aggregate_agent_data_api_v_1_beta_agent_data_aggregate_post(
|
||||
deployment_name=self.deployment_name,
|
||||
collection=self.collection,
|
||||
page_size=page_size,
|
||||
filter=filter,
|
||||
@@ -264,11 +336,3 @@ class AsyncAgentDataClient(Generic[AgentDataT]):
|
||||
first=first,
|
||||
offset=offset,
|
||||
)
|
||||
return TypedAggregateGroupItems(
|
||||
items=[
|
||||
TypedAggregateGroup.from_raw(item, validator=self.type)
|
||||
for item in raw.items
|
||||
],
|
||||
has_more=raw.next_page_token is not None,
|
||||
total=raw.total_size,
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ CRUD operations, search capabilities, filtering, and aggregation functionality
|
||||
for managing agent-generated data at scale.
|
||||
|
||||
Key Concepts:
|
||||
- Agent Slug: Unique identifier for an agent instance
|
||||
- Deployment Name: Unique identifier for an agent deployment
|
||||
- Collection: Named grouping of data within an agent (defaults to "default"). Data within a collection should be of the same type.
|
||||
- Agent Data: Individual structured data records with metadata and timestamps
|
||||
|
||||
@@ -26,7 +26,7 @@ Example Usage:
|
||||
client=async_llama_cloud,
|
||||
type=Person,
|
||||
collection="people",
|
||||
agent_url_id="my-extraction-agent-xyz"
|
||||
deployment_name="my-extraction-agent-xyz"
|
||||
)
|
||||
|
||||
# Create typed data
|
||||
@@ -56,7 +56,6 @@ from typing import (
|
||||
|
||||
# Type variable for user-defined data models
|
||||
AgentDataT = TypeVar("AgentDataT", bound=BaseModel)
|
||||
|
||||
# Type variable for extracted data (can be dict or Pydantic model)
|
||||
ExtractedT = TypeVar("ExtractedT", bound=Union[BaseModel, dict])
|
||||
|
||||
@@ -78,7 +77,7 @@ class TypedAgentData(BaseModel, Generic[AgentDataT]):
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier for this data record
|
||||
agent_url_id: Identifier of the agent that created this data
|
||||
deployment_name: Identifier of the agent deployment that created this data
|
||||
collection: Named collection within the agent (used for organization)
|
||||
data: The actual structured data payload (typed as AgentDataT)
|
||||
created_at: Timestamp when the record was first created
|
||||
@@ -94,8 +93,8 @@ class TypedAgentData(BaseModel, Generic[AgentDataT]):
|
||||
"""
|
||||
|
||||
id: Optional[str] = Field(description="Unique identifier for this data record")
|
||||
agent_url_id: str = Field(
|
||||
description="Identifier of the agent that created this data"
|
||||
deployment_name: str = Field(
|
||||
description="Identifier of the agent deployment that created this data"
|
||||
)
|
||||
collection: Optional[str] = Field(
|
||||
description="Named collection within the agent for data organization"
|
||||
@@ -116,15 +115,15 @@ class TypedAgentData(BaseModel, Generic[AgentDataT]):
|
||||
Args:
|
||||
raw_data: Raw agent data from the API
|
||||
validator: Pydantic model class to validate the data field
|
||||
|
||||
Returns:
|
||||
TypedAgentData instance with validated data
|
||||
"""
|
||||
|
||||
data: AgentDataT = validator.model_validate(raw_data.data)
|
||||
|
||||
return cls(
|
||||
id=raw_data.id,
|
||||
agent_url_id=raw_data.agent_slug,
|
||||
deployment_name=raw_data.deployment_name,
|
||||
collection=raw_data.collection,
|
||||
data=data,
|
||||
created_at=raw_data.created_at,
|
||||
@@ -222,12 +221,16 @@ def parse_extracted_field_metadata(
|
||||
return {
|
||||
k: _parse_extracted_field_metadata_recursive(v)
|
||||
for k, v in field_metadata.items()
|
||||
if k not in _METADATA_FIELDS_SIBLING_TO_LEAF
|
||||
and k not in _ADDITIONAL_ROOT_METADATA_FIELDS
|
||||
if not _is_reasoning_field(k, v) and k not in _ADDITIONAL_ROOT_METADATA_FIELDS
|
||||
}
|
||||
|
||||
|
||||
_METADATA_FIELDS_SIBLING_TO_LEAF = {"reasoning"}
|
||||
def _is_reasoning_field(field_name: str, field_value: Any) -> bool:
|
||||
# There can either be a user specified reasoning field (from the schema), or a reasoning metadata field for the
|
||||
# dict of values
|
||||
return field_name == "reasoning" and isinstance(field_value, str)
|
||||
|
||||
|
||||
_ADDITIONAL_ROOT_METADATA_FIELDS = {"error"}
|
||||
|
||||
|
||||
@@ -257,14 +260,12 @@ def _parse_extracted_field_metadata_recursive(
|
||||
except ValidationError:
|
||||
pass
|
||||
additional_fields = {
|
||||
k: v
|
||||
for k, v in field_value.items()
|
||||
if k in _METADATA_FIELDS_SIBLING_TO_LEAF
|
||||
k: v for k, v in field_value.items() if _is_reasoning_field(k, v)
|
||||
}
|
||||
return {
|
||||
k: _parse_extracted_field_metadata_recursive(v, additional_fields)
|
||||
for k, v in field_value.items()
|
||||
if k not in _METADATA_FIELDS_SIBLING_TO_LEAF
|
||||
if not _is_reasoning_field(k, v)
|
||||
}
|
||||
elif isinstance(field_value, list):
|
||||
return [_parse_extracted_field_metadata_recursive(item) for item in field_value]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from llama_cloud_services.beta.classifier.client import LlamaClassify, ClassifyClient
|
||||
from llama_cloud_services.beta.classifier.types import ClassifyJobResultsWithFiles
|
||||
from llama_cloud_services.utils import SourceText, FileInput
|
||||
|
||||
__all__ = [
|
||||
"LlamaClassify",
|
||||
"ClassifyClient",
|
||||
"ClassifyJobResultsWithFiles",
|
||||
"SourceText",
|
||||
"FileInput",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional
|
||||
import warnings
|
||||
from typing import Optional, List, Union
|
||||
from pydantic import BaseModel
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from llama_cloud.types import (
|
||||
@@ -9,13 +10,16 @@ from llama_cloud.types import (
|
||||
ClassifyJobResults,
|
||||
ClassifyParsingConfiguration,
|
||||
StatusEnum,
|
||||
ClassifyJobWithStatus,
|
||||
File,
|
||||
)
|
||||
from llama_cloud.resources.classifier.client import OMIT
|
||||
from llama_cloud_services.files.client import FileClient
|
||||
from llama_cloud_services.constants import POLLING_TIMEOUT_SECONDS
|
||||
from llama_cloud_services.utils import is_terminal_status, augment_async_errors
|
||||
from llama_cloud_services.utils import (
|
||||
is_terminal_status,
|
||||
augment_async_errors,
|
||||
FileInput,
|
||||
)
|
||||
from llama_index.core.async_utils import DEFAULT_NUM_WORKERS, run_jobs
|
||||
from llama_cloud_services.beta.classifier.types import (
|
||||
ClassifyJobResultsWithFiles,
|
||||
@@ -27,7 +31,7 @@ class ClassificationOutput(BaseModel):
|
||||
classification: str
|
||||
|
||||
|
||||
class ClassifyClient:
|
||||
class LlamaClassify:
|
||||
"""
|
||||
Experimental - Client for interacting with the LlamaCloud Classifier API.
|
||||
The Classification API is currently in beta and may change in the future without notice.
|
||||
@@ -35,7 +39,6 @@ class ClassifyClient:
|
||||
Args:
|
||||
client: The LlamaCloud client to use.
|
||||
project_id: The project ID to use.
|
||||
organization_id: The organization ID to use.
|
||||
polling_interval: The interval to poll for job completion in seconds.
|
||||
polling_timeout: The timeout for the job to complete in seconds.
|
||||
"""
|
||||
@@ -44,15 +47,13 @@ class ClassifyClient:
|
||||
self,
|
||||
client: AsyncLlamaCloud,
|
||||
project_id: Optional[str] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
polling_interval: float = 1.0,
|
||||
polling_timeout: float = POLLING_TIMEOUT_SECONDS,
|
||||
):
|
||||
self.client = client
|
||||
self.project_id = project_id
|
||||
self.organization_id = organization_id
|
||||
self.polling_interval = polling_interval
|
||||
self.file_client = FileClient(client, project_id, organization_id)
|
||||
self.file_client = FileClient(client, project_id)
|
||||
self.polling_timeout = polling_timeout
|
||||
|
||||
@classmethod
|
||||
@@ -60,7 +61,6 @@ class ClassifyClient:
|
||||
cls,
|
||||
api_key: str,
|
||||
project_id: Optional[str] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
) -> "ClassifyClient":
|
||||
"""
|
||||
@@ -70,7 +70,6 @@ class ClassifyClient:
|
||||
return cls(
|
||||
client,
|
||||
project_id,
|
||||
organization_id,
|
||||
)
|
||||
|
||||
async def acreate_classify_job(
|
||||
@@ -97,7 +96,6 @@ class ClassifyClient:
|
||||
file_ids=file_ids,
|
||||
parsing_configuration=parsing_configuration or OMIT,
|
||||
project_id=self.project_id,
|
||||
organization_id=self.organization_id,
|
||||
)
|
||||
|
||||
def create_classify_job(
|
||||
@@ -148,7 +146,6 @@ class ClassifyClient:
|
||||
results = await self.client.classifier.get_classification_job_results(
|
||||
classify_job_with_status.id,
|
||||
project_id=self.project_id,
|
||||
organization_id=self.organization_id,
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -167,6 +164,98 @@ class ClassifyClient:
|
||||
)
|
||||
)
|
||||
|
||||
async def aclassify(
|
||||
self,
|
||||
rules: list[ClassifierRule],
|
||||
files: Union[FileInput, List[FileInput]],
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
workers: int = DEFAULT_NUM_WORKERS,
|
||||
show_progress: bool = False,
|
||||
) -> ClassifyJobResultsWithFiles:
|
||||
"""
|
||||
Classify one or more files from various input types.
|
||||
|
||||
Args:
|
||||
rules: The rules to use for classification.
|
||||
files: The file(s) to classify. Can be a single file or list of files. Each can be:
|
||||
- str/Path: File path
|
||||
- SourceText: Text content or file with explicit filename
|
||||
- File: Already uploaded file
|
||||
- BufferedIOBase: File-like object
|
||||
parsing_configuration: The parsing configuration to use for classification.
|
||||
raise_on_error: Whether to raise an error if the classification job fails.
|
||||
workers: Number of parallel workers for uploading files.
|
||||
show_progress: Whether to show progress bars.
|
||||
|
||||
Returns:
|
||||
The results of the classification job with file metadata.
|
||||
"""
|
||||
# Normalize to list
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
|
||||
# Upload all files
|
||||
coroutines = [
|
||||
self.file_client.upload_content(file_input) for file_input in files
|
||||
]
|
||||
uploaded_files: List[File] = await run_jobs(
|
||||
coroutines,
|
||||
show_progress=show_progress,
|
||||
workers=workers,
|
||||
desc="Uploading files for classification",
|
||||
)
|
||||
|
||||
# Classify
|
||||
results = await self.aclassify_file_ids(
|
||||
rules,
|
||||
[file.id for file in uploaded_files],
|
||||
parsing_configuration,
|
||||
raise_on_error,
|
||||
)
|
||||
return ClassifyJobResultsWithFiles.from_classify_job_results(
|
||||
results, uploaded_files
|
||||
)
|
||||
|
||||
def classify(
|
||||
self,
|
||||
rules: list[ClassifierRule],
|
||||
files: Union[FileInput, List[FileInput]],
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
workers: int = DEFAULT_NUM_WORKERS,
|
||||
show_progress: bool = False,
|
||||
) -> ClassifyJobResultsWithFiles:
|
||||
"""
|
||||
Classify one or more files from various input types (synchronous version).
|
||||
|
||||
Args:
|
||||
rules: The rules to use for classification.
|
||||
files: The file(s) to classify. Can be a single file or list of files. Each can be:
|
||||
- str/Path: File path
|
||||
- SourceText: Text content or file with explicit filename
|
||||
- File: Already uploaded file
|
||||
- BufferedIOBase: File-like object
|
||||
parsing_configuration: The parsing configuration to use for classification.
|
||||
raise_on_error: Whether to raise an error if the classification job fails.
|
||||
workers: Number of parallel workers for uploading files.
|
||||
show_progress: Whether to show progress bars.
|
||||
|
||||
Returns:
|
||||
The results of the classification job with file metadata.
|
||||
"""
|
||||
with augment_async_errors():
|
||||
return asyncio.run(
|
||||
self.aclassify(
|
||||
rules,
|
||||
files,
|
||||
parsing_configuration,
|
||||
raise_on_error,
|
||||
workers,
|
||||
show_progress,
|
||||
)
|
||||
)
|
||||
|
||||
async def aclassify_file_path(
|
||||
self,
|
||||
rules: list[ClassifierRule],
|
||||
@@ -174,11 +263,17 @@ class ClassifyClient:
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
) -> ClassifyJobResultsWithFiles:
|
||||
file = await self.file_client.upload_file(file_input_path)
|
||||
results = await self.aclassify_file_ids(
|
||||
rules, [file.id], parsing_configuration, raise_on_error
|
||||
"""
|
||||
Deprecated: Use aclassify() instead.
|
||||
"""
|
||||
warnings.warn(
|
||||
"aclassify_file_path is deprecated, use aclassify() instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return await self.aclassify(
|
||||
rules, file_input_path, parsing_configuration, raise_on_error
|
||||
)
|
||||
return ClassifyJobResultsWithFiles.from_classify_job_results(results, [file])
|
||||
|
||||
def classify_file_path(
|
||||
self,
|
||||
@@ -187,12 +282,17 @@ class ClassifyClient:
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
) -> ClassifyJobResultsWithFiles:
|
||||
with augment_async_errors():
|
||||
return asyncio.run(
|
||||
self.aclassify_file_path(
|
||||
rules, file_input_path, parsing_configuration, raise_on_error
|
||||
)
|
||||
)
|
||||
"""
|
||||
Deprecated: Use classify() instead.
|
||||
"""
|
||||
warnings.warn(
|
||||
"classify_file_path is deprecated, use classify() instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.classify(
|
||||
rules, file_input_path, parsing_configuration, raise_on_error
|
||||
)
|
||||
|
||||
async def aclassify_file_paths(
|
||||
self,
|
||||
@@ -203,17 +303,22 @@ class ClassifyClient:
|
||||
workers: int = DEFAULT_NUM_WORKERS,
|
||||
show_progress: bool = False,
|
||||
) -> ClassifyJobResultsWithFiles:
|
||||
coroutines = [self.file_client.upload_file(path) for path in file_input_paths]
|
||||
files: list[File] = await run_jobs(
|
||||
coroutines,
|
||||
show_progress=show_progress,
|
||||
workers=workers,
|
||||
desc="Uploading files for classification",
|
||||
"""
|
||||
Deprecated: Use aclassify() instead.
|
||||
"""
|
||||
warnings.warn(
|
||||
"aclassify_file_paths is deprecated, use aclassify() instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
results = await self.aclassify_file_ids(
|
||||
rules, [file.id for file in files], parsing_configuration, raise_on_error
|
||||
return await self.aclassify(
|
||||
rules,
|
||||
file_input_paths,
|
||||
parsing_configuration,
|
||||
raise_on_error,
|
||||
workers,
|
||||
show_progress,
|
||||
)
|
||||
return ClassifyJobResultsWithFiles.from_classify_job_results(results, files)
|
||||
|
||||
def classify_file_paths(
|
||||
self,
|
||||
@@ -222,14 +327,19 @@ class ClassifyClient:
|
||||
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
|
||||
raise_on_error: bool = True,
|
||||
) -> ClassifyJobResultsWithFiles:
|
||||
with augment_async_errors():
|
||||
return asyncio.run(
|
||||
self.aclassify_file_paths(
|
||||
rules, file_input_paths, parsing_configuration, raise_on_error
|
||||
)
|
||||
)
|
||||
"""
|
||||
Deprecated: Use classify() instead.
|
||||
"""
|
||||
warnings.warn(
|
||||
"classify_file_paths is deprecated, use classify() instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.classify(
|
||||
rules, file_input_paths, parsing_configuration, raise_on_error
|
||||
)
|
||||
|
||||
async def wait_for_job_completion(self, job_id: str) -> ClassifyJobWithStatus:
|
||||
async def wait_for_job_completion(self, job_id: str) -> ClassifyJob:
|
||||
"""
|
||||
Wait for a classify job to complete.
|
||||
Meant to expose lower level access to classifier jobs for advanced use cases.
|
||||
@@ -242,7 +352,7 @@ class ClassifyClient:
|
||||
The classify job with status.
|
||||
"""
|
||||
job = await self.client.classifier.get_classify_job(
|
||||
job_id, project_id=self.project_id, organization_id=self.organization_id
|
||||
job_id, project_id=self.project_id
|
||||
)
|
||||
start_time = time.time()
|
||||
while not is_terminal_status(job.status):
|
||||
@@ -253,6 +363,9 @@ class ClassifyClient:
|
||||
)
|
||||
await asyncio.sleep(self.polling_interval)
|
||||
job = await self.client.classifier.get_classify_job(
|
||||
job_id, project_id=self.project_id, organization_id=self.organization_id
|
||||
job_id, project_id=self.project_id
|
||||
)
|
||||
return job
|
||||
|
||||
|
||||
ClassifyClient = LlamaClassify
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""LlamaCloud Spreadsheet API SDK
|
||||
|
||||
This module provides a Python SDK for the LlamaCloud Spreadsheet API.
|
||||
"""
|
||||
|
||||
from llama_cloud_services.beta.sheets.client import (
|
||||
LlamaSheets,
|
||||
SpreadsheetAPIError,
|
||||
SpreadsheetJobError,
|
||||
SpreadsheetTimeoutError,
|
||||
)
|
||||
from llama_cloud_services.beta.sheets.types import (
|
||||
ExtractedRegionSummary,
|
||||
FileUploadResponse,
|
||||
JobStatus,
|
||||
PresignedUrlResponse,
|
||||
SpreadsheetJob,
|
||||
SpreadsheetJobResult,
|
||||
SpreadsheetParseResult,
|
||||
SpreadsheetParsingConfig,
|
||||
SpreadsheetResultType,
|
||||
WorksheetMetadata,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Client
|
||||
"LlamaSheets",
|
||||
# Exceptions
|
||||
"SpreadsheetAPIError",
|
||||
"SpreadsheetJobError",
|
||||
"SpreadsheetTimeoutError",
|
||||
# Types
|
||||
"ExtractedRegionSummary",
|
||||
"FileUploadResponse",
|
||||
"JobStatus",
|
||||
"PresignedUrlResponse",
|
||||
"SpreadsheetJob",
|
||||
"SpreadsheetJobResult",
|
||||
"SpreadsheetParseResult",
|
||||
"SpreadsheetParsingConfig",
|
||||
"SpreadsheetResultType",
|
||||
"WorksheetMetadata",
|
||||
]
|
||||
@@ -0,0 +1,518 @@
|
||||
import asyncio
|
||||
import io
|
||||
import os
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from tenacity import (
|
||||
AsyncRetrying,
|
||||
retry_if_exception,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
)
|
||||
|
||||
from llama_cloud_services.beta.sheets.types import (
|
||||
FileUploadResponse,
|
||||
JobStatus,
|
||||
PresignedUrlResponse,
|
||||
SpreadsheetJob,
|
||||
SpreadsheetJobResult,
|
||||
SpreadsheetParsingConfig,
|
||||
SpreadsheetResultType,
|
||||
)
|
||||
from llama_cloud_services.constants import BASE_URL
|
||||
from llama_cloud_services.files.client import FileClient
|
||||
from llama_cloud_services.utils import (
|
||||
augment_async_errors,
|
||||
FileInput,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def _should_retry_exception(exception: BaseException) -> bool:
|
||||
"""Determine if an exception should be retried."""
|
||||
if isinstance(exception, httpx.HTTPStatusError):
|
||||
return exception.response.status_code in (429, 500, 502, 503, 504)
|
||||
return False
|
||||
|
||||
|
||||
class SpreadsheetAPIError(Exception):
|
||||
"""Base exception for spreadsheet API errors"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SpreadsheetJobError(SpreadsheetAPIError):
|
||||
"""Exception raised when a spreadsheet job fails"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class SpreadsheetTimeoutError(SpreadsheetAPIError):
|
||||
"""Exception raised when a job times out"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class LlamaSheets:
|
||||
"""Client for the LlamaCloud Spreadsheet API"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
max_timeout: int = 300,
|
||||
poll_interval: int = 5,
|
||||
max_retries: int = 3,
|
||||
async_httpx_client: httpx.AsyncClient | None = None,
|
||||
) -> None:
|
||||
"""Initialize the LlamaSheets client.
|
||||
|
||||
Args:
|
||||
api_key: API key for authentication. If not provided, will use LLAMA_CLOUD_API_KEY env var
|
||||
base_url: Base URL for the API
|
||||
max_timeout: Maximum time to wait for job completion in seconds
|
||||
poll_interval: Interval between status checks in seconds
|
||||
max_retries: Maximum number of retries for failed requests
|
||||
async_httpx_client: Optional custom async httpx client
|
||||
"""
|
||||
self.api_key = api_key or os.environ.get("LLAMA_CLOUD_API_KEY")
|
||||
if not self.api_key:
|
||||
raise ValueError(
|
||||
"An API key must be provided either as an argument or via the LLAMA_CLOUD_API_KEY environment variable."
|
||||
)
|
||||
|
||||
base_url = base_url or os.environ.get("LLAMA_CLOUD_BASE_URL", BASE_URL)
|
||||
self.base_url = str(base_url).rstrip("/")
|
||||
|
||||
self.max_timeout = max_timeout
|
||||
self.poll_interval = poll_interval
|
||||
self.max_retries = max_retries
|
||||
|
||||
self._async_client: httpx.AsyncClient | None = async_httpx_client
|
||||
self._files_client = FileClient(
|
||||
AsyncLlamaCloud(
|
||||
token=self.api_key,
|
||||
base_url=self.base_url,
|
||||
httpx_client=async_httpx_client,
|
||||
)
|
||||
)
|
||||
|
||||
def _get_async_client(self) -> httpx.AsyncClient:
|
||||
"""Get or create the async httpx client"""
|
||||
if self._async_client is None:
|
||||
self._async_client = httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(60.0),
|
||||
follow_redirects=True,
|
||||
)
|
||||
return self._async_client
|
||||
|
||||
def _get_headers(self) -> dict[str, str]:
|
||||
"""Get common headers for API requests"""
|
||||
return {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# Sync methods
|
||||
|
||||
def upload_file(
|
||||
self, file_obj: FileInput, file_name: str | None = None
|
||||
) -> FileUploadResponse:
|
||||
"""Upload a file to the Files API.
|
||||
|
||||
Args:
|
||||
file_obj: File to upload (path, bytes, or file-like object)
|
||||
file_name: Optional name for the uploaded filename
|
||||
|
||||
Returns:
|
||||
FileUploadResponse with the uploaded file ID
|
||||
"""
|
||||
with augment_async_errors():
|
||||
return asyncio.run(self.aupload_file(file_obj))
|
||||
|
||||
def create_job(
|
||||
self,
|
||||
file_id: str,
|
||||
config: dict | SpreadsheetParsingConfig | None = None,
|
||||
) -> SpreadsheetJob:
|
||||
"""Create a new spreadsheet parsing job.
|
||||
|
||||
Args:
|
||||
file_id: ID of the uploaded file
|
||||
config: Parsing configuration
|
||||
|
||||
Returns:
|
||||
SpreadsheetJob with job details
|
||||
"""
|
||||
with augment_async_errors():
|
||||
return asyncio.run(self.acreate_job(file_id, config))
|
||||
|
||||
def get_job(
|
||||
self, job_id: str, include_results_metadata: bool = True
|
||||
) -> SpreadsheetJobResult:
|
||||
"""Get the status of a spreadsheet parsing job.
|
||||
|
||||
Args:
|
||||
job_id: ID of the job
|
||||
include_results_metadata: Whether to include results metadata in the response
|
||||
|
||||
Returns:
|
||||
SpreadsheetJobResult with job status and optionally results
|
||||
"""
|
||||
with augment_async_errors():
|
||||
return asyncio.run(self.aget_job(job_id, include_results_metadata))
|
||||
|
||||
def wait_for_completion(self, job_id: str) -> SpreadsheetJobResult:
|
||||
"""Wait for a job to complete by polling.
|
||||
|
||||
Args:
|
||||
job_id: ID of the job to wait for
|
||||
|
||||
Returns:
|
||||
SpreadsheetJobResult when job is complete
|
||||
|
||||
Raises:
|
||||
SpreadsheetTimeoutError: If job doesn't complete within max_timeout
|
||||
SpreadsheetJobError: If job fails
|
||||
"""
|
||||
with augment_async_errors():
|
||||
return asyncio.run(self.await_for_completion(job_id))
|
||||
|
||||
def download_region_result(
|
||||
self,
|
||||
job_id: str,
|
||||
region_id: str,
|
||||
result_type: SpreadsheetResultType = SpreadsheetResultType.TABLE,
|
||||
) -> bytes:
|
||||
"""Download a region result (either region data or cell metadata).
|
||||
|
||||
Args:
|
||||
job_id: ID of the job
|
||||
region_id: ID of the region
|
||||
result_type: Type of result to download (region or cell_metadata)
|
||||
|
||||
Returns:
|
||||
Raw bytes of the parquet file
|
||||
"""
|
||||
with augment_async_errors():
|
||||
return asyncio.run(
|
||||
self.adownload_region_result(job_id, region_id, result_type)
|
||||
)
|
||||
|
||||
def download_region_as_dataframe(
|
||||
self,
|
||||
job_id: str,
|
||||
region_id: str,
|
||||
result_type: SpreadsheetResultType = SpreadsheetResultType.TABLE,
|
||||
) -> "pd.DataFrame":
|
||||
"""Download a region result as a pandas DataFrame.
|
||||
|
||||
Args:
|
||||
job_id: ID of the job
|
||||
region_id: ID of the region
|
||||
result_type: Type of result to download (region or cell_metadata)
|
||||
|
||||
Returns:
|
||||
pandas DataFrame
|
||||
"""
|
||||
with augment_async_errors():
|
||||
return asyncio.run(
|
||||
self.adownload_region_as_dataframe(job_id, region_id, result_type)
|
||||
)
|
||||
|
||||
def extract_regions(
|
||||
self,
|
||||
file_obj: FileInput,
|
||||
config: dict | SpreadsheetParsingConfig | None = None,
|
||||
) -> SpreadsheetJobResult:
|
||||
"""High-level method to parse a spreadsheet file.
|
||||
|
||||
This method handles the entire workflow:
|
||||
1. Upload the file
|
||||
2. Create a parsing job
|
||||
3. Wait for completion
|
||||
4. Return results
|
||||
|
||||
Args:
|
||||
file_obj: File to parse (path, bytes, or file-like object)
|
||||
config: Parsing configuration
|
||||
|
||||
Returns:
|
||||
SpreadsheetJobResult with parsing results
|
||||
"""
|
||||
with augment_async_errors():
|
||||
return asyncio.run(self.aextract_regions(file_obj, config))
|
||||
|
||||
# Async methods
|
||||
|
||||
async def aupload_file(
|
||||
self, file_obj: FileInput, file_name: str | None = None
|
||||
) -> FileUploadResponse:
|
||||
"""Upload a file to the Files API.
|
||||
|
||||
Args:
|
||||
file_obj: File to upload (path, bytes, or file-like object)
|
||||
file_name: Optional name for the uploaded filename
|
||||
|
||||
Returns:
|
||||
FileUploadResponse with the uploaded file ID
|
||||
"""
|
||||
try:
|
||||
async for attempt in AsyncRetrying(
|
||||
stop=stop_after_attempt(self.max_retries),
|
||||
wait=wait_exponential(multiplier=1, min=1, max=32),
|
||||
retry=retry_if_exception(_should_retry_exception),
|
||||
reraise=True,
|
||||
):
|
||||
with attempt:
|
||||
return await self._files_client.upload_content(
|
||||
file_obj, external_file_id=file_name
|
||||
)
|
||||
except Exception as e:
|
||||
raise SpreadsheetAPIError(f"Failed to upload file: {e}") from e
|
||||
raise RuntimeError("Tenacity did not execute")
|
||||
|
||||
async def acreate_job(
|
||||
self,
|
||||
file_id: str,
|
||||
config: dict | SpreadsheetParsingConfig | None = None,
|
||||
) -> SpreadsheetJob:
|
||||
"""Create a new spreadsheet parsing job.
|
||||
|
||||
Args:
|
||||
file_id: ID of the uploaded file
|
||||
config: Parsing configuration
|
||||
|
||||
Returns:
|
||||
SpreadsheetJob with job details
|
||||
"""
|
||||
if config is None:
|
||||
config = SpreadsheetParsingConfig()
|
||||
elif isinstance(config, dict):
|
||||
config = SpreadsheetParsingConfig.model_validate(config)
|
||||
|
||||
if not isinstance(config, SpreadsheetParsingConfig):
|
||||
raise ValueError(
|
||||
"config must be a dict or SpreadsheetParsingConfig instance"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"file_id": file_id,
|
||||
"config": config.model_dump(mode="json", exclude_none=True),
|
||||
}
|
||||
|
||||
try:
|
||||
async for attempt in AsyncRetrying(
|
||||
stop=stop_after_attempt(self.max_retries),
|
||||
wait=wait_exponential(multiplier=1, min=1, max=32),
|
||||
retry=retry_if_exception(_should_retry_exception),
|
||||
reraise=True,
|
||||
):
|
||||
with attempt:
|
||||
client = self._get_async_client()
|
||||
response = await client.post(
|
||||
f"{self.base_url}/api/v1/beta/sheets/jobs",
|
||||
headers=self._get_headers(),
|
||||
json=payload,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return SpreadsheetJob.model_validate(response.json())
|
||||
except Exception as e:
|
||||
raise SpreadsheetAPIError(f"Failed to create job: {e}") from e
|
||||
raise RuntimeError("Tenacity did not execute")
|
||||
|
||||
async def aget_job(
|
||||
self, job_id: str, include_results_metadata: bool = True
|
||||
) -> SpreadsheetJobResult:
|
||||
"""Get the status of a spreadsheet parsing job.
|
||||
|
||||
Args:
|
||||
job_id: ID of the job
|
||||
include_results_metadata: Whether to include results in the response
|
||||
|
||||
Returns:
|
||||
SpreadsheetJobResult with job status and optionally results
|
||||
"""
|
||||
try:
|
||||
async for attempt in AsyncRetrying(
|
||||
stop=stop_after_attempt(self.max_retries),
|
||||
wait=wait_exponential(multiplier=1, min=1, max=32),
|
||||
retry=retry_if_exception(_should_retry_exception),
|
||||
reraise=True,
|
||||
):
|
||||
with attempt:
|
||||
client = self._get_async_client()
|
||||
response = await client.get(
|
||||
f"{self.base_url}/api/v1/beta/sheets/jobs/{job_id}",
|
||||
headers=self._get_headers(),
|
||||
params={"include_results": include_results_metadata},
|
||||
)
|
||||
response.raise_for_status()
|
||||
return SpreadsheetJobResult.model_validate(response.json())
|
||||
except Exception as e:
|
||||
raise SpreadsheetAPIError(f"Failed to get job status: {e}") from e
|
||||
raise RuntimeError("Tenacity did not execute")
|
||||
|
||||
async def await_for_completion(self, job_id: str) -> SpreadsheetJobResult:
|
||||
"""Wait for a job to complete by polling.
|
||||
|
||||
Args:
|
||||
job_id: ID of the job to wait for
|
||||
|
||||
Returns:
|
||||
SpreadsheetJobResult when job is complete
|
||||
|
||||
Raises:
|
||||
SpreadsheetTimeoutError: If job doesn't complete within max_timeout
|
||||
SpreadsheetJobError: If job fails
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
while (time.time() - start_time) < self.max_timeout:
|
||||
job_result = await self.aget_job(job_id, include_results_metadata=True)
|
||||
|
||||
if job_result.status in (
|
||||
JobStatus.SUCCESS,
|
||||
JobStatus.PARTIAL_SUCCESS,
|
||||
JobStatus.ERROR,
|
||||
JobStatus.FAILURE,
|
||||
):
|
||||
if job_result.status in (JobStatus.SUCCESS, JobStatus.PARTIAL_SUCCESS):
|
||||
return job_result
|
||||
else:
|
||||
error_msg = f"Job failed with status: {job_result.status}"
|
||||
if job_result.errors:
|
||||
error_msg += f"\nErrors: {', '.join(job_result.errors)}"
|
||||
raise SpreadsheetJobError(error_msg)
|
||||
|
||||
await asyncio.sleep(self.poll_interval)
|
||||
|
||||
raise SpreadsheetTimeoutError(
|
||||
f"Job did not complete within {self.max_timeout} seconds"
|
||||
)
|
||||
|
||||
async def adownload_region_result(
|
||||
self,
|
||||
job_id: str,
|
||||
region_id: str,
|
||||
result_type: SpreadsheetResultType = SpreadsheetResultType.TABLE,
|
||||
) -> bytes:
|
||||
"""Download a region result (either region data or cell metadata).
|
||||
|
||||
Args:
|
||||
job_id: ID of the job
|
||||
region_id: ID of the region
|
||||
result_type: Type of result to download (region or cell_metadata)
|
||||
|
||||
Returns:
|
||||
Raw bytes of the parquet file
|
||||
"""
|
||||
# Get presigned URL
|
||||
presigned_response = None
|
||||
result_type_str = str(result_type)
|
||||
try:
|
||||
async for attempt in AsyncRetrying(
|
||||
stop=stop_after_attempt(self.max_retries),
|
||||
wait=wait_exponential(multiplier=1, min=1, max=32),
|
||||
retry=retry_if_exception(_should_retry_exception),
|
||||
reraise=True,
|
||||
):
|
||||
with attempt:
|
||||
client = self._get_async_client()
|
||||
response = await client.get(
|
||||
f"{self.base_url}/api/v1/beta/sheets/jobs/{job_id}/regions/{region_id}/result/{result_type_str}",
|
||||
headers=self._get_headers(),
|
||||
)
|
||||
response.raise_for_status()
|
||||
presigned_response = PresignedUrlResponse.model_validate(
|
||||
response.json()
|
||||
)
|
||||
except Exception as e:
|
||||
raise SpreadsheetAPIError(f"Failed to get presigned URL: {e}") from e
|
||||
|
||||
# Download using presigned URL
|
||||
if presigned_response is None:
|
||||
raise SpreadsheetAPIError("Failed to obtain presigned URL.")
|
||||
|
||||
try:
|
||||
async for attempt in AsyncRetrying(
|
||||
stop=stop_after_attempt(self.max_retries),
|
||||
wait=wait_exponential(multiplier=1, min=1, max=32),
|
||||
retry=retry_if_exception(_should_retry_exception),
|
||||
reraise=True,
|
||||
):
|
||||
with attempt:
|
||||
download_response = await client.get(presigned_response.url)
|
||||
download_response.raise_for_status()
|
||||
return download_response.content
|
||||
except Exception as e:
|
||||
raise SpreadsheetAPIError(f"Failed to download result: {e}") from e
|
||||
raise RuntimeError("Tenacity did not execute")
|
||||
|
||||
async def adownload_region_as_dataframe(
|
||||
self,
|
||||
job_id: str,
|
||||
region_id: str,
|
||||
result_type: SpreadsheetResultType = SpreadsheetResultType.TABLE,
|
||||
) -> "pd.DataFrame":
|
||||
"""Download a region result as a pandas DataFrame.
|
||||
|
||||
Args:
|
||||
job_id: ID of the job
|
||||
region_id: ID of the region
|
||||
result_type: Type of result to download (region or cell_metadata)
|
||||
|
||||
Returns:
|
||||
pandas DataFrame
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
parquet_bytes = await self.adownload_region_result(
|
||||
job_id, region_id, result_type
|
||||
)
|
||||
return pd.read_parquet(io.BytesIO(parquet_bytes))
|
||||
|
||||
async def aextract_regions(
|
||||
self,
|
||||
file_obj: FileInput,
|
||||
config: dict | SpreadsheetParsingConfig | None = None,
|
||||
) -> SpreadsheetJobResult:
|
||||
"""High-level method to parse a spreadsheet file.
|
||||
|
||||
This method handles the entire workflow:
|
||||
1. Upload the file
|
||||
2. Create a parsing job
|
||||
3. Wait for completion
|
||||
4. Return results
|
||||
|
||||
Args:
|
||||
file_obj: File to parse (path, bytes, or file-like object)
|
||||
config: Parsing configuration
|
||||
|
||||
Returns:
|
||||
SpreadsheetJobResult with parsing results
|
||||
"""
|
||||
# Upload file
|
||||
file_response = await self.aupload_file(file_obj)
|
||||
|
||||
# Create job
|
||||
job = await self.acreate_job(file_response.id, config)
|
||||
|
||||
# Wait for completion
|
||||
return await self.await_for_completion(job.id)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
"""Close all HTTP clients (async)"""
|
||||
if self._async_client:
|
||||
await self._async_client.aclose()
|
||||
|
||||
async def __aenter__(self) -> "LlamaSheets":
|
||||
return self
|
||||
|
||||
async def __aexit__(self, _exc_type, _exc_val, _exc_tb) -> None: # type: ignore
|
||||
await self.aclose()
|
||||
@@ -0,0 +1,156 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class SpreadsheetResultType(str, Enum):
|
||||
TABLE = "table"
|
||||
EXTRA = "extra"
|
||||
CELL_METADATA = "cell_metadata"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
class ExtractedRegionSummary(BaseModel):
|
||||
"""A summary of a single extracted region from a spreadsheet"""
|
||||
|
||||
region_id: str = Field(
|
||||
...,
|
||||
description="Unique identifier for this region within the file",
|
||||
)
|
||||
sheet_name: str = Field(..., description="Worksheet name where region was found")
|
||||
location: str = Field(..., description="Location of the region in the spreadsheet")
|
||||
title: str | None = Field(None, description="Generated title for the region")
|
||||
description: str | None = Field(
|
||||
None, description="Generated description of the region"
|
||||
)
|
||||
region_type: SpreadsheetResultType = Field(
|
||||
..., description="Type of the extracted region"
|
||||
)
|
||||
|
||||
|
||||
class WorksheetMetadata(BaseModel):
|
||||
"""Metadata about a worksheet in a spreadsheet"""
|
||||
|
||||
sheet_name: str = Field(..., description="Name of the worksheet")
|
||||
title: str | None = Field(None, description="Generated title for the worksheet")
|
||||
description: str | None = Field(
|
||||
None, description="Generated description of the worksheet"
|
||||
)
|
||||
|
||||
|
||||
class SpreadsheetParseResult(BaseModel):
|
||||
"""Result of parsing a single spreadsheet file"""
|
||||
|
||||
success: bool = Field(..., description="Whether parsing was successful")
|
||||
file_name: str = Field(..., description="Original filename")
|
||||
|
||||
regions: list[ExtractedRegionSummary] = Field(
|
||||
default_factory=list, description="All successfully extracted regions"
|
||||
)
|
||||
worksheet_metadata: list[WorksheetMetadata] = Field(
|
||||
default_factory=list, description="Metadata for each processed worksheet"
|
||||
)
|
||||
|
||||
# Error information
|
||||
errors: list[str] = Field(
|
||||
default_factory=list, description="Any errors encountered during parsing"
|
||||
)
|
||||
|
||||
|
||||
class SpreadsheetParsingConfig(BaseModel):
|
||||
"""Configuration for spreadsheet parsing and region extraction"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
sheet_names: list[str] | None = Field(
|
||||
default=None,
|
||||
description="The names of the sheets to extract regions from. If empty, the default sheet is extracted.",
|
||||
)
|
||||
include_hidden_cells: bool = Field(
|
||||
default=True,
|
||||
description="Whether to include hidden cells when extracting regions from the spreadsheet.",
|
||||
)
|
||||
extraction_range: str | None = Field(
|
||||
default=None,
|
||||
description="A1 notation of the range to extract a single region from. If None, the entire sheet is used.",
|
||||
)
|
||||
generate_additional_metadata: bool = Field(
|
||||
default=True,
|
||||
description="Whether to generate additional metadata (title, description) for each extracted region.",
|
||||
)
|
||||
use_experimental_processing: bool = Field(
|
||||
default=False,
|
||||
description="Enables experimental processing. Accuracy may be impacted.",
|
||||
)
|
||||
|
||||
|
||||
class SpreadsheetJob(BaseModel):
|
||||
"""A spreadsheet parsing job"""
|
||||
|
||||
id: str = Field(..., description="The ID of the job")
|
||||
user_id: str = Field(..., description="The ID of the user")
|
||||
project_id: str = Field(..., description="The ID of the project")
|
||||
file: dict = Field(..., description="The file object being parsed")
|
||||
config: SpreadsheetParsingConfig = Field(
|
||||
..., description="Configuration for the parsing job"
|
||||
)
|
||||
status: str = Field(..., description="The status of the parsing job")
|
||||
created_at: str = Field(..., description="When the job was created")
|
||||
updated_at: str = Field(..., description="When the job was last updated")
|
||||
|
||||
@field_validator("created_at", "updated_at", mode="before")
|
||||
def validate_dates(cls, v: str) -> str:
|
||||
"""Validate that the dates are in the correct format"""
|
||||
if isinstance(v, datetime):
|
||||
return v.isoformat()
|
||||
else:
|
||||
return v
|
||||
|
||||
|
||||
class SpreadsheetJobResult(SpreadsheetJob):
|
||||
"""A spreadsheet parsing job result."""
|
||||
|
||||
# Results are included when the job is complete
|
||||
success: bool | None = Field(
|
||||
None, description="Whether the job completed successfully"
|
||||
)
|
||||
regions: list[ExtractedRegionSummary] = Field(
|
||||
default_factory=list,
|
||||
description="All extracted regions (populated when job is complete)",
|
||||
)
|
||||
worksheet_metadata: list[WorksheetMetadata] = Field(
|
||||
default_factory=list,
|
||||
description="Metadata for each processed worksheet (populated when job is complete)",
|
||||
)
|
||||
errors: list[str] = Field(
|
||||
default_factory=list, description="Any errors encountered"
|
||||
)
|
||||
|
||||
|
||||
class JobStatus(str, Enum):
|
||||
"""Status of a spreadsheet parsing job"""
|
||||
|
||||
PENDING = "PENDING"
|
||||
IN_PROGRESS = "IN_PROGRESS"
|
||||
SUCCESS = "SUCCESS"
|
||||
PARTIAL_SUCCESS = "PARTIAL_SUCCESS"
|
||||
ERROR = "ERROR"
|
||||
FAILURE = "FAILURE"
|
||||
|
||||
|
||||
class PresignedUrlResponse(BaseModel):
|
||||
"""Response containing a presigned URL for downloading results"""
|
||||
|
||||
url: str = Field(..., description="The presigned URL for downloading")
|
||||
|
||||
|
||||
class FileUploadResponse(BaseModel):
|
||||
"""Response from uploading a file"""
|
||||
|
||||
id: str = Field(..., description="The ID of the uploaded file")
|
||||
name: str = Field(..., description="The name of the file")
|
||||
project_id: str = Field(..., description="The project ID")
|
||||
user_id: str = Field(..., description="The user ID")
|
||||
@@ -1,2 +1,3 @@
|
||||
BASE_URL = "https://api.cloud.llamaindex.ai"
|
||||
EU_BASE_URL = "https://api.cloud.eu.llamaindex.ai"
|
||||
POLLING_TIMEOUT_SECONDS = 300.0
|
||||
|
||||
@@ -2,15 +2,16 @@ from llama_cloud_services.extract.extract import (
|
||||
LlamaExtract,
|
||||
ExtractConfig,
|
||||
ExtractionAgent,
|
||||
SourceText,
|
||||
ExtractTarget,
|
||||
ExtractMode,
|
||||
)
|
||||
from llama_cloud_services.utils import SourceText, FileInput
|
||||
|
||||
__all__ = [
|
||||
"LlamaExtract",
|
||||
"ExtractionAgent",
|
||||
"SourceText",
|
||||
"FileInput",
|
||||
"ExtractConfig",
|
||||
"ExtractTarget",
|
||||
"ExtractMode",
|
||||
|
||||
@@ -2,10 +2,9 @@ import asyncio
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
from io import BufferedIOBase, BufferedReader, BytesIO, TextIOWrapper
|
||||
from io import BufferedIOBase, TextIOWrapper
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Type, Union, Coroutine, Any, TypeVar
|
||||
import secrets
|
||||
import warnings
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
@@ -19,14 +18,12 @@ from llama_cloud import (
|
||||
ExtractAgent as CloudExtractAgent,
|
||||
ExtractConfig,
|
||||
ExtractJob,
|
||||
ExtractJobCreate,
|
||||
ExtractRun,
|
||||
File,
|
||||
FileData,
|
||||
ExtractMode,
|
||||
StatusEnum,
|
||||
ExtractTarget,
|
||||
LlamaExtractSettings,
|
||||
PaginatedExtractRunsResponse,
|
||||
)
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
@@ -35,7 +32,8 @@ from llama_cloud_services.extract.utils import (
|
||||
JSONObjectType,
|
||||
ExperimentalWarning,
|
||||
)
|
||||
from llama_cloud_services.utils import augment_async_errors
|
||||
from llama_cloud_services.utils import augment_async_errors, SourceText, FileInput
|
||||
from llama_cloud_services.files.client import FileClient
|
||||
from llama_index.core.schema import BaseComponent
|
||||
from llama_index.core.async_utils import run_jobs
|
||||
from llama_index.core.bridge.pydantic import Field, PrivateAttr
|
||||
@@ -190,46 +188,6 @@ async def _wait_for_job_result(
|
||||
)
|
||||
|
||||
|
||||
class SourceText:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
file: Union[bytes, BufferedIOBase, TextIOWrapper, str, Path, None] = None,
|
||||
text_content: Optional[str] = None,
|
||||
filename: Optional[str] = None,
|
||||
):
|
||||
self.file = file
|
||||
self.filename = filename
|
||||
self.text_content = text_content
|
||||
self._validate()
|
||||
|
||||
def _validate(self) -> None:
|
||||
"""Ensure filename is provided when needed."""
|
||||
if not ((self.file is None) ^ (self.text_content is None)):
|
||||
raise ValueError("Either file or text_content must be provided.")
|
||||
if self.text_content is not None:
|
||||
if not self.filename:
|
||||
random_hex = secrets.token_hex(4)
|
||||
self.filename = f"text_input_{random_hex}.txt"
|
||||
return
|
||||
|
||||
if isinstance(self.file, (bytes, BufferedIOBase, TextIOWrapper)):
|
||||
if not self.filename and hasattr(self.file, "name"):
|
||||
self.filename = os.path.basename(str(self.file.name))
|
||||
elif not hasattr(self.file, "name") and self.filename is None:
|
||||
raise ValueError(
|
||||
"filename must be provided when file is bytes or a file-like object without a name"
|
||||
)
|
||||
elif isinstance(self.file, (str, Path)):
|
||||
if not self.filename:
|
||||
self.filename = os.path.basename(str(self.file))
|
||||
else:
|
||||
raise ValueError(f"Unsupported file type: {type(self.file)}")
|
||||
|
||||
|
||||
FileInput = Union[str, Path, BufferedIOBase, SourceText, File]
|
||||
|
||||
|
||||
def run_in_thread(
|
||||
coro: Coroutine[Any, Any, T],
|
||||
thread_pool: ThreadPoolExecutor,
|
||||
@@ -322,6 +280,7 @@ class ExtractionAgent:
|
||||
self._thread_pool = ThreadPoolExecutor(
|
||||
max_workers=min(10, (os.cpu_count() or 1) + 4)
|
||||
)
|
||||
self._file_client = FileClient(client, project_id, organization_id)
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
@@ -371,65 +330,11 @@ class ExtractionAgent:
|
||||
ValueError: If filename is not provided for bytes input or for file-like objects
|
||||
without a name attribute.
|
||||
"""
|
||||
file_contents: Optional[Union[BufferedIOBase, BytesIO]] = None
|
||||
try:
|
||||
if file_input.text_content is not None:
|
||||
# Handle direct text content
|
||||
file_contents = BytesIO(file_input.text_content.encode("utf-8"))
|
||||
elif isinstance(file_input.file, TextIOWrapper):
|
||||
# Handle text-based IO objects
|
||||
file_contents = BytesIO(file_input.file.read().encode("utf-8"))
|
||||
elif isinstance(file_input.file, (str, Path)):
|
||||
# Handle file paths
|
||||
file_contents = open(file_input.file, "rb")
|
||||
elif isinstance(file_input.file, bytes):
|
||||
# Handle bytes
|
||||
file_contents = BytesIO(file_input.file)
|
||||
elif isinstance(file_input.file, BufferedIOBase):
|
||||
# Handle binary IO objects
|
||||
file_contents = file_input.file
|
||||
else:
|
||||
raise ValueError(f"Unsupported file type: {type(file_input.file)}")
|
||||
|
||||
# Add name attribute to file object if needed
|
||||
if not hasattr(file_contents, "name"):
|
||||
file_contents.name = file_input.filename # type: ignore
|
||||
|
||||
return await self._client.files.upload_file(
|
||||
project_id=self._project_id, upload_file=file_contents
|
||||
)
|
||||
finally:
|
||||
if file_contents is not None and isinstance(
|
||||
file_contents, (BufferedReader, BytesIO)
|
||||
):
|
||||
file_contents.close()
|
||||
return await self._file_client.upload_content(file_input)
|
||||
|
||||
async def _upload_file(self, file_input: FileInput) -> File:
|
||||
source_text = None
|
||||
if isinstance(file_input, File):
|
||||
return file_input
|
||||
if isinstance(file_input, SourceText):
|
||||
source_text = file_input
|
||||
elif isinstance(file_input, (str, Path)):
|
||||
path = Path(file_input)
|
||||
source_text = SourceText(file=path, filename=path.name)
|
||||
else:
|
||||
# Try to get filename from the file object if not provided
|
||||
filename = None
|
||||
if hasattr(file_input, "name"):
|
||||
filename = os.path.basename(str(file_input.name))
|
||||
if filename is None:
|
||||
raise ValueError(
|
||||
"Use SourceText to provide filename when uploading bytes or file-like objects."
|
||||
)
|
||||
|
||||
warnings.warn(
|
||||
"Use SourceText instead of bytes or file-like objects",
|
||||
DeprecationWarning,
|
||||
)
|
||||
source_text = SourceText(file=file_input, filename=filename)
|
||||
|
||||
return await self.upload_file(source_text)
|
||||
"""Upload a file from various input types using FileClient."""
|
||||
return await self._file_client.upload_content(file_input)
|
||||
|
||||
async def _wait_for_job_result(self, job_id: str) -> Optional[ExtractRun]:
|
||||
"""Wait for and return the results of an extraction job."""
|
||||
@@ -463,56 +368,6 @@ class ExtractionAgent:
|
||||
)
|
||||
)
|
||||
|
||||
async def _run_extraction_test(
|
||||
self,
|
||||
files: Union[FileInput, List[FileInput]],
|
||||
extract_settings: LlamaExtractSettings,
|
||||
) -> Union[ExtractJob, List[ExtractJob]]:
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
single_file = True
|
||||
else:
|
||||
single_file = False
|
||||
|
||||
upload_tasks = [self._upload_file(file) for file in files]
|
||||
with augment_async_errors():
|
||||
uploaded_files = await run_jobs(
|
||||
upload_tasks,
|
||||
workers=self.num_workers,
|
||||
desc="Uploading files",
|
||||
show_progress=self.show_progress,
|
||||
)
|
||||
|
||||
async def run_job(file: File) -> ExtractRun:
|
||||
job_queued = await self._client.llama_extract.run_job_test_user(
|
||||
job_create=ExtractJobCreate(
|
||||
extraction_agent_id=self.id,
|
||||
file_id=file.id,
|
||||
data_schema_override=self.data_schema,
|
||||
config_override=self.config,
|
||||
),
|
||||
extract_settings=extract_settings,
|
||||
)
|
||||
return await self._wait_for_job_result(job_queued.id)
|
||||
|
||||
job_tasks = [run_job(file) for file in uploaded_files]
|
||||
with augment_async_errors():
|
||||
extract_results = await run_jobs(
|
||||
job_tasks,
|
||||
workers=self.num_workers,
|
||||
desc="Running extraction jobs",
|
||||
show_progress=self.show_progress,
|
||||
)
|
||||
|
||||
if self._verbose:
|
||||
for file, job in zip(files, extract_results):
|
||||
file_repr = (
|
||||
str(file) if isinstance(file, (str, Path)) else "<bytes/buffer>"
|
||||
)
|
||||
print(f"Running extraction for file {file_repr} under job_id {job.id}")
|
||||
|
||||
return extract_results[0] if single_file else extract_results
|
||||
|
||||
async def queue_extraction(
|
||||
self,
|
||||
files: Union[FileInput, List[FileInput]],
|
||||
@@ -544,12 +399,10 @@ class ExtractionAgent:
|
||||
|
||||
job_tasks = [
|
||||
self._client.llama_extract.run_job(
|
||||
request=ExtractJobCreate(
|
||||
extraction_agent_id=self.id,
|
||||
file_id=file.id,
|
||||
data_schema_override=self.data_schema,
|
||||
config_override=self.config,
|
||||
),
|
||||
extraction_agent_id=self.id,
|
||||
file_id=file.id,
|
||||
data_schema_override=self.data_schema,
|
||||
config_override=self.config,
|
||||
)
|
||||
for file in uploaded_files
|
||||
]
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from io import BytesIO
|
||||
from typing import BinaryIO
|
||||
import os
|
||||
from pathlib import Path
|
||||
from llama_cloud.client import AsyncLlamaCloud
|
||||
from llama_cloud.types import File, FileCreate
|
||||
from typing import Optional
|
||||
from llama_cloud_services.utils import SourceText, FileInput
|
||||
|
||||
|
||||
class FileClient:
|
||||
@@ -95,3 +97,83 @@ class FileClient:
|
||||
project_id=self.project_id,
|
||||
organization_id=self.organization_id,
|
||||
)
|
||||
|
||||
async def upload_content(
|
||||
self, file_input: FileInput, external_file_id: Optional[str] = None
|
||||
) -> File:
|
||||
"""
|
||||
Upload content from various input types or fetch an already-uploaded file.
|
||||
|
||||
Args:
|
||||
file_input: The content to upload. Can be:
|
||||
- File: Already uploaded file (returned as-is)
|
||||
- str/Path: Path to a file on disk
|
||||
- SourceText: Text content, file, or file_id with explicit filename
|
||||
- BufferedIOBase: File-like binary object
|
||||
external_file_id: Optional external identifier for the file
|
||||
|
||||
Returns:
|
||||
File: The uploaded (or fetched) file object
|
||||
|
||||
Raises:
|
||||
ValueError: If the input type is not supported or required info is missing
|
||||
"""
|
||||
# If already a File object, return it
|
||||
if isinstance(file_input, File):
|
||||
return file_input
|
||||
|
||||
# Handle SourceText
|
||||
if isinstance(file_input, SourceText):
|
||||
# If file_id is provided, fetch the file object
|
||||
if file_input.file_id is not None:
|
||||
return await self.get_file(file_input.file_id)
|
||||
elif file_input.text_content is not None:
|
||||
# Handle direct text content
|
||||
text_bytes = file_input.text_content.encode("utf-8")
|
||||
return await self.upload_bytes(
|
||||
text_bytes, external_file_id or file_input.filename or "file"
|
||||
)
|
||||
elif isinstance(file_input.file, (str, Path)):
|
||||
# Handle file paths using the existing upload_file method
|
||||
return await self.upload_file(
|
||||
str(file_input.file), external_file_id or file_input.filename
|
||||
)
|
||||
elif isinstance(file_input.file, bytes):
|
||||
# Handle bytes
|
||||
return await self.upload_bytes(
|
||||
file_input.file, external_file_id or file_input.filename or "file"
|
||||
)
|
||||
elif hasattr(file_input.file, "read"):
|
||||
# Handle any file-like object (TextIOWrapper, BytesIO, BufferedReader, BufferedIOBase, etc.)
|
||||
content = file_input.file.read() # type: ignore
|
||||
if isinstance(content, str):
|
||||
content = content.encode("utf-8")
|
||||
return await self.upload_bytes(
|
||||
content, external_file_id or file_input.filename or "file"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported file type: {type(file_input.file)}")
|
||||
|
||||
# Handle string/Path directly
|
||||
elif isinstance(file_input, (str, Path)):
|
||||
return await self.upload_file(str(file_input), external_file_id)
|
||||
|
||||
# Handle raw file-like objects
|
||||
elif hasattr(file_input, "read"):
|
||||
if hasattr(file_input, "name"):
|
||||
filename = os.path.basename(str(file_input.name))
|
||||
else:
|
||||
filename = external_file_id or "file"
|
||||
|
||||
# Read content to determine size
|
||||
content = file_input.read()
|
||||
if isinstance(content, str):
|
||||
content = content.encode("utf-8")
|
||||
|
||||
return await self.upload_bytes(content, external_file_id or filename)
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported file input type: {type(file_input)}. "
|
||||
f"Supported types: str, Path, SourceText, BufferedIOBase, or File."
|
||||
)
|
||||
|
||||
@@ -258,6 +258,7 @@ def page_screenshot_nodes_to_node_with_score(
|
||||
client: LlamaCloud,
|
||||
raw_image_nodes: Optional[List[PageScreenshotNodeWithScore]],
|
||||
project_id: str,
|
||||
metadata: Optional[dict] = None,
|
||||
) -> List[NodeWithScore]:
|
||||
if not raw_image_nodes:
|
||||
return []
|
||||
@@ -273,6 +274,7 @@ def page_screenshot_nodes_to_node_with_score(
|
||||
image_base64 = base64.b64encode(image_bytes).decode("utf-8")
|
||||
image_node_metadata: Dict[str, Any] = {
|
||||
**(raw_image_node.node.metadata or {}),
|
||||
**(metadata or {}),
|
||||
"file_id": raw_image_node.node.file_id,
|
||||
"page_index": raw_image_node.node.page_index,
|
||||
}
|
||||
@@ -289,6 +291,7 @@ def image_nodes_to_node_with_score(
|
||||
client: LlamaCloud,
|
||||
raw_image_nodes: Optional[List[PageScreenshotNodeWithScore]],
|
||||
project_id: str,
|
||||
metadata: Optional[dict] = None,
|
||||
) -> List[NodeWithScore]:
|
||||
"""
|
||||
Legacy method to alias page_screenshot_nodes_to_node_with_score.
|
||||
@@ -297,7 +300,10 @@ def image_nodes_to_node_with_score(
|
||||
return []
|
||||
|
||||
return page_screenshot_nodes_to_node_with_score(
|
||||
client=client, raw_image_nodes=raw_image_nodes, project_id=project_id
|
||||
client=client,
|
||||
raw_image_nodes=raw_image_nodes,
|
||||
project_id=project_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
||||
@@ -305,6 +311,7 @@ def page_figure_nodes_to_node_with_score(
|
||||
client: LlamaCloud,
|
||||
raw_figure_nodes: Optional[List[PageFigureNodeWithScore]],
|
||||
project_id: str,
|
||||
metadata: Optional[dict] = None,
|
||||
) -> List[NodeWithScore]:
|
||||
if not raw_figure_nodes:
|
||||
return []
|
||||
@@ -321,6 +328,7 @@ def page_figure_nodes_to_node_with_score(
|
||||
figure_base64 = base64.b64encode(figure_bytes).decode("utf-8")
|
||||
figure_node_metadata: Dict[str, Any] = {
|
||||
**(raw_figure_node.node.metadata or {}),
|
||||
**(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,
|
||||
@@ -337,6 +345,7 @@ async def apage_screenshot_nodes_to_node_with_score(
|
||||
client: AsyncLlamaCloud,
|
||||
raw_image_nodes: Optional[List[PageScreenshotNodeWithScore]],
|
||||
project_id: str,
|
||||
metadata: Optional[dict] = None,
|
||||
) -> List[NodeWithScore]:
|
||||
if not raw_image_nodes:
|
||||
return []
|
||||
@@ -357,6 +366,7 @@ async def apage_screenshot_nodes_to_node_with_score(
|
||||
image_base64 = base64.b64encode(image_bytes).decode("utf-8")
|
||||
image_node_metadata: Dict[str, Any] = {
|
||||
**(raw_image_node.node.metadata or {}),
|
||||
**(metadata or {}),
|
||||
"file_id": raw_image_node.node.file_id,
|
||||
"page_index": raw_image_node.node.page_index,
|
||||
}
|
||||
@@ -372,6 +382,7 @@ async def aimage_nodes_to_node_with_score(
|
||||
client: AsyncLlamaCloud,
|
||||
raw_image_nodes: Optional[List[PageScreenshotNodeWithScore]],
|
||||
project_id: str,
|
||||
metadata: Optional[dict] = None,
|
||||
) -> List[NodeWithScore]:
|
||||
"""
|
||||
Legacy method to alias apage_screenshot_nodes_to_node_with_score.
|
||||
@@ -380,7 +391,10 @@ async def aimage_nodes_to_node_with_score(
|
||||
return []
|
||||
|
||||
return await apage_screenshot_nodes_to_node_with_score(
|
||||
client=client, raw_image_nodes=raw_image_nodes, project_id=project_id
|
||||
client=client,
|
||||
raw_image_nodes=raw_image_nodes,
|
||||
project_id=project_id,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
||||
@@ -388,6 +402,7 @@ async def apage_figure_nodes_to_node_with_score(
|
||||
client: AsyncLlamaCloud,
|
||||
raw_figure_nodes: Optional[List[PageFigureNodeWithScore]],
|
||||
project_id: str,
|
||||
metadata: Optional[dict] = None,
|
||||
) -> List[NodeWithScore]:
|
||||
if not raw_figure_nodes:
|
||||
return []
|
||||
@@ -409,6 +424,7 @@ async def apage_figure_nodes_to_node_with_score(
|
||||
figure_base64 = base64.b64encode(figure_bytes).decode("utf-8")
|
||||
figure_node_metadata: Dict[str, Any] = {
|
||||
**(raw_figure_node.node.metadata or {}),
|
||||
**(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,
|
||||
|
||||
@@ -19,6 +19,7 @@ from llama_cloud import (
|
||||
PipelineCreate,
|
||||
PipelineCreateEmbeddingConfig,
|
||||
PipelineCreateTransformConfig,
|
||||
PipelineFileCreateCustomMetadataValue,
|
||||
PipelineType,
|
||||
ProjectCreate,
|
||||
ManagedIngestionStatus,
|
||||
@@ -333,7 +334,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
if file_ids:
|
||||
self._wait_for_resources(
|
||||
file_ids,
|
||||
lambda fid: self._client.pipelines.get_pipeline_file_status(
|
||||
lambda fid: self._client.pipeline_files.get_pipeline_file_status(
|
||||
pipeline_id=self.pipeline.id, file_id=fid
|
||||
),
|
||||
resource_name="file",
|
||||
@@ -420,7 +421,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
if file_ids:
|
||||
await self._await_for_resources(
|
||||
file_ids,
|
||||
lambda fid: self._aclient.pipelines.get_pipeline_file_status(
|
||||
lambda fid: self._aclient.pipeline_files.get_pipeline_file_status(
|
||||
pipeline_id=self.pipeline.id, file_id=fid
|
||||
),
|
||||
resource_name="file",
|
||||
@@ -489,6 +490,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
name: str,
|
||||
project_name: str = DEFAULT_PROJECT_NAME,
|
||||
organization_id: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
app_url: Optional[str] = None,
|
||||
@@ -504,15 +506,15 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
app_url = app_url or os.environ.get("LLAMA_CLOUD_APP_URL", DEFAULT_APP_URL)
|
||||
client = get_client(api_key, base_url, app_url, timeout)
|
||||
|
||||
# create project if it doesn't exist
|
||||
project = client.projects.upsert_project(
|
||||
organization_id=organization_id, request=ProjectCreate(name=project_name)
|
||||
)
|
||||
if project.id is None:
|
||||
raise ValueError(f"Failed to create/get project {project_name}")
|
||||
|
||||
if verbose:
|
||||
print(f"Created project {project.id} with name {project.name}")
|
||||
if project_id is None:
|
||||
# create project if it doesn't exist
|
||||
project = client.projects.upsert_project(
|
||||
organization_id=organization_id,
|
||||
request=ProjectCreate(name=project_name),
|
||||
)
|
||||
project_id = project.id
|
||||
if verbose:
|
||||
print(f"Created project {project_id} with name {project_name}")
|
||||
|
||||
# create pipeline
|
||||
pipeline_create = PipelineCreate(
|
||||
@@ -523,7 +525,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
llama_parse_parameters=llama_parse_parameters or LlamaParseParameters(),
|
||||
)
|
||||
pipeline = client.pipelines.upsert_pipeline(
|
||||
project_id=project.id, request=pipeline_create
|
||||
project_id=project_id, request=pipeline_create
|
||||
)
|
||||
if pipeline.id is None:
|
||||
raise ValueError(f"Failed to create/get pipeline {name}")
|
||||
@@ -532,8 +534,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
|
||||
return cls(
|
||||
name,
|
||||
project_name=project.name,
|
||||
organization_id=project.organization_id,
|
||||
project_id=project_id,
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
app_url=app_url,
|
||||
@@ -606,6 +607,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
name: str,
|
||||
project_name: str = DEFAULT_PROJECT_NAME,
|
||||
organization_id: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
app_url: Optional[str] = None,
|
||||
@@ -631,6 +633,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
verbose=verbose,
|
||||
embedding_config=embedding_config,
|
||||
transform_config=transform_config,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
app_url = app_url or os.environ.get("LLAMA_CLOUD_APP_URL", DEFAULT_APP_URL)
|
||||
@@ -903,6 +906,9 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
def upload_file(
|
||||
self,
|
||||
file_path: str,
|
||||
custom_metadata: Optional[
|
||||
dict[str, Optional[PipelineFileCreateCustomMetadataValue]]
|
||||
] = None,
|
||||
verbose: bool = False,
|
||||
wait_for_ingestion: bool = True,
|
||||
raise_on_error: bool = False,
|
||||
@@ -916,8 +922,10 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
print(f"Uploaded file {file.id} with name {file.name}")
|
||||
|
||||
# Add file to pipeline
|
||||
pipeline_file_create = PipelineFileCreate(file_id=file.id)
|
||||
self._client.pipelines.add_files_to_pipeline_api(
|
||||
pipeline_file_create = PipelineFileCreate(
|
||||
file_id=file.id, custom_metadata=custom_metadata
|
||||
)
|
||||
self._client.pipeline_files.add_files_to_pipeline_api(
|
||||
pipeline_id=self.pipeline.id, request=[pipeline_file_create]
|
||||
)
|
||||
|
||||
@@ -930,6 +938,9 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
async def aupload_file(
|
||||
self,
|
||||
file_path: str,
|
||||
custom_metadata: Optional[
|
||||
dict[str, Optional[PipelineFileCreateCustomMetadataValue]]
|
||||
] = None,
|
||||
verbose: bool = False,
|
||||
wait_for_ingestion: bool = True,
|
||||
raise_on_error: bool = False,
|
||||
@@ -943,8 +954,10 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
print(f"Uploaded file {file.id} with name {file.name}")
|
||||
|
||||
# Add file to pipeline
|
||||
pipeline_file_create = PipelineFileCreate(file_id=file.id)
|
||||
await self._aclient.pipelines.add_files_to_pipeline_api(
|
||||
pipeline_file_create = PipelineFileCreate(
|
||||
file_id=file.id, custom_metadata=custom_metadata
|
||||
)
|
||||
await self._aclient.pipeline_files.add_files_to_pipeline_api(
|
||||
pipeline_id=self.pipeline.id, request=[pipeline_file_create]
|
||||
)
|
||||
|
||||
@@ -959,6 +972,9 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
self,
|
||||
file_name: str,
|
||||
url: str,
|
||||
custom_metadata: Optional[
|
||||
dict[str, Optional[PipelineFileCreateCustomMetadataValue]]
|
||||
] = None,
|
||||
proxy_url: Optional[str] = None,
|
||||
request_headers: Optional[Dict[str, str]] = None,
|
||||
verify_ssl: bool = True,
|
||||
@@ -981,8 +997,10 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
print(f"Uploaded file {file.id} with ID {file.id}")
|
||||
|
||||
# Add file to pipeline
|
||||
pipeline_file_create = PipelineFileCreate(file_id=file.id)
|
||||
self._client.pipelines.add_files_to_pipeline_api(
|
||||
pipeline_file_create = PipelineFileCreate(
|
||||
file_id=file.id, custom_metadata=custom_metadata
|
||||
)
|
||||
self._client.pipeline_files.add_files_to_pipeline_api(
|
||||
pipeline_id=self.pipeline.id, request=[pipeline_file_create]
|
||||
)
|
||||
|
||||
@@ -996,6 +1014,9 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
self,
|
||||
file_name: str,
|
||||
url: str,
|
||||
custom_metadata: Optional[
|
||||
dict[str, Optional[PipelineFileCreateCustomMetadataValue]]
|
||||
] = None,
|
||||
proxy_url: Optional[str] = None,
|
||||
request_headers: Optional[Dict[str, str]] = None,
|
||||
verify_ssl: bool = True,
|
||||
@@ -1018,8 +1039,10 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
print(f"Uploaded file {file.id} with ID {file.id}")
|
||||
|
||||
# Add file to pipeline
|
||||
pipeline_file_create = PipelineFileCreate(file_id=file.id)
|
||||
await self._aclient.pipelines.add_files_to_pipeline_api(
|
||||
pipeline_file_create = PipelineFileCreate(
|
||||
file_id=file.id, custom_metadata=custom_metadata
|
||||
)
|
||||
await self._aclient.pipeline_files.add_files_to_pipeline_api(
|
||||
pipeline_id=self.pipeline.id, request=[pipeline_file_create]
|
||||
)
|
||||
|
||||
|
||||
@@ -129,11 +129,12 @@ class LlamaCloudRetriever(BaseRetriever):
|
||||
)
|
||||
|
||||
def _result_nodes_to_node_with_score(
|
||||
self, result_nodes: List[TextNodeWithScore]
|
||||
self, result_nodes: List[TextNodeWithScore], metadata: Optional[dict] = None
|
||||
) -> List[NodeWithScore]:
|
||||
nodes = []
|
||||
for res in result_nodes:
|
||||
text_node = TextNode.parse_obj(res.node.dict())
|
||||
text_node = TextNode.model_validate(res.node.dict())
|
||||
text_node.metadata.update(metadata or {})
|
||||
nodes.append(NodeWithScore(node=text_node, score=res.score))
|
||||
|
||||
return nodes
|
||||
@@ -161,17 +162,25 @@ class LlamaCloudRetriever(BaseRetriever):
|
||||
search_filters_inference_schema=search_filters_inference_schema,
|
||||
)
|
||||
|
||||
result_nodes = self._result_nodes_to_node_with_score(results.retrieval_nodes)
|
||||
result_nodes = self._result_nodes_to_node_with_score(
|
||||
results.retrieval_nodes, metadata=results.metadata
|
||||
)
|
||||
if self._retrieve_page_screenshot_nodes:
|
||||
result_nodes.extend(
|
||||
page_screenshot_nodes_to_node_with_score(
|
||||
self._client, results.image_nodes, self.project.id
|
||||
self._client,
|
||||
results.image_nodes,
|
||||
self.project.id,
|
||||
metadata=results.metadata,
|
||||
)
|
||||
)
|
||||
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
|
||||
self._client,
|
||||
results.page_figure_nodes,
|
||||
self.project.id,
|
||||
metadata=results.metadata,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -200,17 +209,25 @@ class LlamaCloudRetriever(BaseRetriever):
|
||||
search_filters_inference_schema=search_filters_inference_schema,
|
||||
)
|
||||
|
||||
result_nodes = self._result_nodes_to_node_with_score(results.retrieval_nodes)
|
||||
result_nodes = self._result_nodes_to_node_with_score(
|
||||
results.retrieval_nodes, metadata=results.metadata
|
||||
)
|
||||
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
|
||||
self._aclient,
|
||||
results.image_nodes,
|
||||
self.project.id,
|
||||
metadata=results.metadata,
|
||||
)
|
||||
)
|
||||
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
|
||||
self._aclient,
|
||||
results.page_figure_nodes,
|
||||
self.project.id,
|
||||
metadata=results.metadata,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -188,6 +188,10 @@ class LlamaParse(BasePydanticReader):
|
||||
default=False,
|
||||
description="If set to true, LlamaParse will try to detect long table and adapt the output.",
|
||||
)
|
||||
aggressive_table_extraction: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, LlamaParse will try to extract tables aggressively, may lead to false positives.",
|
||||
)
|
||||
annotate_links: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Annotate links found in the document to extract their URL.",
|
||||
@@ -281,7 +285,7 @@ class LlamaParse(BasePydanticReader):
|
||||
description="Note: Non compatible with gpt-4o. If set to true, the parser will use a faster mode to extract text from documents. This mode will skip OCR of images, and table/heading reconstruction.",
|
||||
)
|
||||
|
||||
guess_xlsx_sheet_names: Optional[bool] = Field(
|
||||
guess_xlsx_sheet_name: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="Whether to guess the sheet names of the xlsx file.",
|
||||
)
|
||||
@@ -309,6 +313,10 @@ class LlamaParse(BasePydanticReader):
|
||||
default=False,
|
||||
description="If set to true, the parser will ignore document elements for layout detection and only rely on a vision model.",
|
||||
)
|
||||
inline_images_in_markdown: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will inline images in the markdown output.",
|
||||
)
|
||||
input_s3_region: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The region of the input S3 bucket if input_s3_path is specified.",
|
||||
@@ -325,6 +333,10 @@ class LlamaParse(BasePydanticReader):
|
||||
default=None,
|
||||
description="The maximum timeout in seconds to wait for the parsing to finish. Override default timeout of 30 minutes. Minimum is 120 seconds.",
|
||||
)
|
||||
keep_page_separator_when_merging_tables: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will keep the page separator when merging tables across pages.",
|
||||
)
|
||||
language: Optional[str] = Field(
|
||||
default="en", description="The language of the text to parse."
|
||||
)
|
||||
@@ -396,6 +408,14 @@ class LlamaParse(BasePydanticReader):
|
||||
default=False,
|
||||
description="If set, the parser will try to preserve very small text lines. This can be useful for documents containing vector graphics with very small text lines that may not be recognized by OCR or a vision model (such as in CAD drawings).",
|
||||
)
|
||||
presentation_out_of_bounds_content: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will include out-of-bounds content in presentation files.",
|
||||
)
|
||||
precise_bounding_box: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will use a more precise bounding box to extract text from documents. This will increase the accuracy of the parsing job, but reduce the speed.",
|
||||
)
|
||||
replace_failed_page_mode: Optional[FailedPageMode] = Field(
|
||||
default=None,
|
||||
description="The mode to use to replace the failed page, see FailedPageMode enum for possible value. If set, the parser will replace the failed page with the specified mode. If not set, the default mode (raw_text) will be used.",
|
||||
@@ -408,6 +428,14 @@ class LlamaParse(BasePydanticReader):
|
||||
default=None,
|
||||
description="A suffix to add after error message in failed pages. If not set, no suffix will be used.",
|
||||
)
|
||||
remove_hidden_text: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will remove hidden text from the document.",
|
||||
)
|
||||
save_images: Optional[bool] = Field(
|
||||
default=True,
|
||||
description="If set to true, the parser will save images extracted from the document.",
|
||||
)
|
||||
skip_diagonal_text: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will ignore diagonal text (when the text rotation in degrees modulo 90 is not 0).",
|
||||
@@ -416,7 +444,26 @@ class LlamaParse(BasePydanticReader):
|
||||
default=False,
|
||||
description="If set to true, the parser will extract sub-tables from the spreadsheet when possible (more than one table per sheet).",
|
||||
)
|
||||
|
||||
spreadsheet_force_formula_computation: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will re-compute values for all spreadsheet cells containing formulas.",
|
||||
)
|
||||
specialized_chart_parsing_agentic: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will use a specialized agentic chart parsing model to extract data from charts. This model is able to understand the chart type and extract the data accordingly.",
|
||||
)
|
||||
specialized_chart_parsing_efficient: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will use a specialized efficient chart parsing model to extract data from charts. This model is faster and cheaper than the agentic model, but may be less accurate.",
|
||||
)
|
||||
specialized_chart_parsing_plus: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will use a specialized one-shot chart parsing model to extract data from charts. This model is able to understand the chart type and extract the data accordingly. It is more accurate than the efficient model, but also more expensive.",
|
||||
)
|
||||
specialized_image_parsing: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will use a specialized image parsing model to extract data from images.",
|
||||
)
|
||||
strict_mode_buggy_font: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="If set to true, the parser will fail if it can't extract text from a document because of a buggy font.",
|
||||
@@ -513,6 +560,10 @@ class LlamaParse(BasePydanticReader):
|
||||
default=None,
|
||||
description="A prefix to add to the page footer in the output markdown.",
|
||||
)
|
||||
extract_printed_page_number: Optional[bool] = Field(
|
||||
default=None,
|
||||
description="Whether to extract the printed page numbers from pages in the document.",
|
||||
)
|
||||
|
||||
# Deprecated
|
||||
bounding_box: Optional[str] = Field(
|
||||
@@ -557,6 +608,23 @@ class LlamaParse(BasePydanticReader):
|
||||
description="Automatically check for Python SDK updates.",
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def handle_deprecated_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
# Handle deprecated guess_xlsx_sheet_names -> guess_xlsx_sheet_name
|
||||
if "guess_xlsx_sheet_names" in data:
|
||||
warnings.warn(
|
||||
"The parameter 'guess_xlsx_sheet_names' is deprecated and will be removed in a future release. "
|
||||
"Use 'guess_xlsx_sheet_name' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
# Only set the new parameter if it's not already explicitly set
|
||||
if "guess_xlsx_sheet_name" not in data:
|
||||
data["guess_xlsx_sheet_name"] = data["guess_xlsx_sheet_names"]
|
||||
del data["guess_xlsx_sheet_names"]
|
||||
return data
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def warn_extra_params(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@@ -694,6 +762,9 @@ class LlamaParse(BasePydanticReader):
|
||||
if self.adaptive_long_table:
|
||||
data["adaptive_long_table"] = self.adaptive_long_table
|
||||
|
||||
if self.aggressive_table_extraction:
|
||||
data["aggressive_table_extraction"] = self.aggressive_table_extraction
|
||||
|
||||
if self.annotate_links:
|
||||
data["annotate_links"] = self.annotate_links
|
||||
|
||||
@@ -794,8 +865,8 @@ class LlamaParse(BasePydanticReader):
|
||||
)
|
||||
data["formatting_instruction"] = self.formatting_instruction
|
||||
|
||||
if self.guess_xlsx_sheet_names:
|
||||
data["guess_xlsx_sheet_names"] = self.guess_xlsx_sheet_names
|
||||
if self.guess_xlsx_sheet_name:
|
||||
data["guess_xlsx_sheet_name"] = self.guess_xlsx_sheet_name
|
||||
|
||||
if self.html_make_all_elements_visible:
|
||||
data["html_make_all_elements_visible"] = self.html_make_all_elements_visible
|
||||
@@ -819,6 +890,9 @@ class LlamaParse(BasePydanticReader):
|
||||
"ignore_document_elements_for_layout_detection"
|
||||
] = self.ignore_document_elements_for_layout_detection
|
||||
|
||||
if self.inline_images_in_markdown:
|
||||
data["inline_images_in_markdown"] = self.inline_images_in_markdown
|
||||
|
||||
if input_url is not None:
|
||||
files = None
|
||||
data["input_url"] = str(input_url)
|
||||
@@ -847,6 +921,11 @@ class LlamaParse(BasePydanticReader):
|
||||
if self.job_timeout_in_seconds is not None:
|
||||
data["job_timeout_in_seconds"] = self.job_timeout_in_seconds
|
||||
|
||||
if self.keep_page_separator_when_merging_tables:
|
||||
data[
|
||||
"keep_page_separator_when_merging_tables"
|
||||
] = self.keep_page_separator_when_merging_tables
|
||||
|
||||
if self.language:
|
||||
data["language"] = self.language
|
||||
|
||||
@@ -925,9 +1004,17 @@ class LlamaParse(BasePydanticReader):
|
||||
if self.preserve_very_small_text:
|
||||
data["preserve_very_small_text"] = self.preserve_very_small_text
|
||||
|
||||
if self.presentation_out_of_bounds_content:
|
||||
data[
|
||||
"presentation_out_of_bounds_content"
|
||||
] = self.presentation_out_of_bounds_content
|
||||
|
||||
if self.preset is not None:
|
||||
data["preset"] = self.preset
|
||||
|
||||
if self.precise_bounding_box:
|
||||
data["precise_bounding_box"] = self.precise_bounding_box
|
||||
|
||||
if self.replace_failed_page_mode is not None:
|
||||
data["replace_failed_page_mode"] = self.replace_failed_page_mode.value
|
||||
|
||||
@@ -941,12 +1028,38 @@ class LlamaParse(BasePydanticReader):
|
||||
"replace_failed_page_with_error_message_suffix"
|
||||
] = self.replace_failed_page_with_error_message_suffix
|
||||
|
||||
if self.remove_hidden_text:
|
||||
data["remove_hidden_text"] = self.remove_hidden_text
|
||||
|
||||
data["save_images"] = self.save_images
|
||||
|
||||
if self.skip_diagonal_text:
|
||||
data["skip_diagonal_text"] = self.skip_diagonal_text
|
||||
|
||||
if self.spreadsheet_extract_sub_tables:
|
||||
data["spreadsheet_extract_sub_tables"] = self.spreadsheet_extract_sub_tables
|
||||
|
||||
if self.spreadsheet_force_formula_computation:
|
||||
data[
|
||||
"spreadsheet_force_formula_computation"
|
||||
] = self.spreadsheet_force_formula_computation
|
||||
|
||||
if self.specialized_chart_parsing_agentic:
|
||||
data[
|
||||
"specialized_chart_parsing_agentic"
|
||||
] = self.specialized_chart_parsing_agentic
|
||||
|
||||
if self.specialized_chart_parsing_efficient:
|
||||
data[
|
||||
"specialized_chart_parsing_efficient"
|
||||
] = self.specialized_chart_parsing_efficient
|
||||
|
||||
if self.specialized_chart_parsing_plus:
|
||||
data["specialized_chart_parsing_plus"] = self.specialized_chart_parsing_plus
|
||||
|
||||
if self.specialized_image_parsing:
|
||||
data["specialized_image_parsing"] = self.specialized_image_parsing
|
||||
|
||||
if self.strict_mode_buggy_font:
|
||||
data["strict_mode_buggy_font"] = self.strict_mode_buggy_font
|
||||
|
||||
@@ -1002,6 +1115,9 @@ class LlamaParse(BasePydanticReader):
|
||||
"markdown_table_multiline_header_separator"
|
||||
] = self.markdown_table_multiline_header_separator
|
||||
|
||||
if self.extract_printed_page_number is not None:
|
||||
data["extract_printed_page_number"] = self.extract_printed_page_number
|
||||
|
||||
# Deprecated
|
||||
if self.bounding_box is not None:
|
||||
data["bounding_box"] = self.bounding_box
|
||||
@@ -1099,6 +1215,25 @@ class LlamaParse(BasePydanticReader):
|
||||
)
|
||||
current_interval = self._calculate_backoff(current_interval)
|
||||
|
||||
async def _get_job_result_with_error_handling(
|
||||
self, job_id: str, result_type: str, verbose: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Get job result with error handling based on ignore_errors setting."""
|
||||
try:
|
||||
return await self._get_job_result(job_id, result_type, verbose=verbose)
|
||||
except JobFailedException as e:
|
||||
if self.ignore_errors:
|
||||
# Return error information when ignore_errors is True
|
||||
return {
|
||||
"pages": [],
|
||||
"job_metadata": {},
|
||||
"error": f"{e.status}: {e.error_message or 'No error message'}",
|
||||
"error_code": e.error_code,
|
||||
"status": e.status,
|
||||
}
|
||||
else:
|
||||
raise e
|
||||
|
||||
async def _parse_one(
|
||||
self,
|
||||
file_path: FileInput,
|
||||
@@ -1140,7 +1275,7 @@ class LlamaParse(BasePydanticReader):
|
||||
)
|
||||
if self.verbose:
|
||||
print("Started parsing the file under job_id %s" % job_id)
|
||||
result = await self._get_job_result(
|
||||
result = await self._get_job_result_with_error_handling(
|
||||
job_id, result_type or self.result_type.value, verbose=self.verbose
|
||||
)
|
||||
return job_id, result
|
||||
@@ -1203,6 +1338,15 @@ class LlamaParse(BasePydanticReader):
|
||||
result_type=ResultType.JSON.value,
|
||||
partition_target_pages=f"{total}-{total + size - 1}",
|
||||
)
|
||||
# Check if the result is an error result (when ignore_errors=True)
|
||||
if json_result.get("error_code") == "NO_DATA_FOUND_IN_FILE":
|
||||
raise JobFailedException(
|
||||
job_id=job_id,
|
||||
status=json_result.get("status", "ERROR"),
|
||||
error_code=json_result.get("error_code"),
|
||||
error_message=json_result.get("error"),
|
||||
)
|
||||
|
||||
result_type = result_type or self.result_type.value
|
||||
if result_type == ResultType.JSON.value:
|
||||
job_result = json_result
|
||||
@@ -1728,7 +1872,7 @@ class LlamaParse(BasePydanticReader):
|
||||
JobResult object or list of JobResult objects if multiple job IDs were provided.
|
||||
"""
|
||||
if isinstance(job_id, str):
|
||||
result = await self._get_job_result(
|
||||
result = await self._get_job_result_with_error_handling(
|
||||
job_id, ResultType.JSON.value, verbose=self.verbose
|
||||
)
|
||||
return JobResult(
|
||||
@@ -1743,7 +1887,9 @@ class LlamaParse(BasePydanticReader):
|
||||
elif isinstance(job_id, list):
|
||||
results = []
|
||||
jobs = [
|
||||
self._get_job_result(id_, ResultType.JSON.value, verbose=self.verbose)
|
||||
self._get_job_result_with_error_handling(
|
||||
id_, ResultType.JSON.value, verbose=self.verbose
|
||||
)
|
||||
for id_ in job_id
|
||||
]
|
||||
results = await run_jobs(
|
||||
|
||||
@@ -1,17 +1,87 @@
|
||||
import httpx
|
||||
import os
|
||||
import re
|
||||
from pydantic import BaseModel, Field, SerializeAsAny
|
||||
from typing import Dict, Any, List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field, SerializeAsAny, model_validator
|
||||
from typing import Dict, Any, List, Optional, get_origin, get_args
|
||||
|
||||
from llama_cloud_services.parse.utils import make_api_request
|
||||
from llama_cloud_services.parse.utils import (
|
||||
make_api_request,
|
||||
is_jupyter,
|
||||
)
|
||||
from llama_index.core.async_utils import asyncio_run
|
||||
from llama_index.core.schema import Document, ImageDocument, ImageNode, TextNode
|
||||
|
||||
PAGE_REGEX = r"page[-_](\d+)\.jpg$"
|
||||
|
||||
SAFE_MODEL_CONFIGS = ConfigDict(
|
||||
extra="allow",
|
||||
validate_assignment=False,
|
||||
arbitrary_types_allowed=True,
|
||||
validate_default=False,
|
||||
)
|
||||
|
||||
class JobMetadata(BaseModel):
|
||||
|
||||
class SafeBaseModel(BaseModel):
|
||||
"""Base model that gracefully handles None values from unstable backend responses."""
|
||||
|
||||
model_config = SAFE_MODEL_CONFIGS
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def coerce_none_to_defaults(cls, data: Any) -> Any:
|
||||
"""
|
||||
Replace None values with appropriate defaults based on field type annotations.
|
||||
This prevents validation errors when the backend returns None for non-optional fields.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
# Process each field that has a None value
|
||||
result = {}
|
||||
for key, value in data.items():
|
||||
if value is not None or key not in cls.model_fields:
|
||||
result[key] = value
|
||||
continue
|
||||
|
||||
# Value is None and field exists in model
|
||||
field_info = cls.model_fields[key]
|
||||
|
||||
# If field has a default or default_factory, let Pydantic handle it
|
||||
from pydantic_core import PydanticUndefined
|
||||
|
||||
if (
|
||||
field_info.default is not PydanticUndefined
|
||||
or field_info.default_factory is not None
|
||||
):
|
||||
continue
|
||||
|
||||
# Otherwise, provide a sensible default based on the type annotation
|
||||
annotation = field_info.annotation
|
||||
origin = get_origin(annotation)
|
||||
|
||||
# Handle List types
|
||||
if origin is list:
|
||||
result[key] = []
|
||||
# Handle Dict types
|
||||
elif origin is dict:
|
||||
result[key] = {}
|
||||
# Handle basic types
|
||||
elif annotation == str or (origin and str in get_args(annotation)):
|
||||
result[key] = ""
|
||||
elif annotation == int or (origin and int in get_args(annotation)):
|
||||
result[key] = 0
|
||||
elif annotation == float or (origin and float in get_args(annotation)):
|
||||
result[key] = 0.0
|
||||
elif annotation == bool or (origin and bool in get_args(annotation)):
|
||||
result[key] = False
|
||||
# If we can't determine a safe default, skip (let Pydantic try)
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class JobMetadata(SafeBaseModel):
|
||||
"""Metadata about the job."""
|
||||
|
||||
job_pages: int = Field(default=0, description="The number of pages in the job.")
|
||||
@@ -24,19 +94,31 @@ class JobMetadata(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class BBox(BaseModel):
|
||||
class BBox(SafeBaseModel):
|
||||
"""A bounding box."""
|
||||
|
||||
x: float = Field(description="The x-coordinate of the bounding box.")
|
||||
y: float = Field(description="The y-coordinate of the bounding box.")
|
||||
w: float = Field(description="The width of the bounding box.")
|
||||
h: float = Field(description="The height of the bounding box.")
|
||||
x: Optional[float] = Field(
|
||||
default=None,
|
||||
description="The x-coordinate of the bounding box.",
|
||||
)
|
||||
y: Optional[float] = Field(
|
||||
default=None,
|
||||
description="The y-coordinate of the bounding box.",
|
||||
)
|
||||
w: Optional[float] = Field(
|
||||
default=None,
|
||||
description="The width of the bounding box.",
|
||||
)
|
||||
h: Optional[float] = Field(
|
||||
default=None,
|
||||
description="The height of the bounding box.",
|
||||
)
|
||||
|
||||
|
||||
class PageItem(BaseModel):
|
||||
class PageItem(SafeBaseModel):
|
||||
"""An item in a page."""
|
||||
|
||||
type: str = Field(description="The type of the item.")
|
||||
type: str = Field(default="", description="The type of the item.")
|
||||
lvl: Optional[int] = Field(
|
||||
default=None, description="The level of indentation of the item."
|
||||
)
|
||||
@@ -58,10 +140,10 @@ class PageItem(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class ImageItem(BaseModel):
|
||||
class ImageItem(SafeBaseModel):
|
||||
"""An image in a page."""
|
||||
|
||||
name: str = Field(description="The name of the image.")
|
||||
name: str = Field(default="", description="The name of the image.")
|
||||
height: Optional[float] = Field(
|
||||
default=None, description="The height of the image."
|
||||
)
|
||||
@@ -81,22 +163,28 @@ class ImageItem(BaseModel):
|
||||
type: Optional[str] = Field(default=None, description="The type of the image.")
|
||||
|
||||
|
||||
class LayoutItem(BaseModel):
|
||||
class LayoutItem(SafeBaseModel):
|
||||
"""The layout of a page."""
|
||||
|
||||
image: str = Field(description="The name of the image containing the layout item")
|
||||
confidence: float = Field(description="The confidence of the layout item.")
|
||||
label: str = Field(description="The label of the layout item.")
|
||||
image: str = Field(
|
||||
default="", description="The name of the image containing the layout item"
|
||||
)
|
||||
confidence: float = Field(
|
||||
default=0.0, description="The confidence of the layout item."
|
||||
)
|
||||
label: str = Field(default="", description="The label of the layout item.")
|
||||
bbox: Optional[BBox] = Field(
|
||||
default=None, description="The bounding box of the layout item."
|
||||
)
|
||||
isLikelyNoise: bool = Field(description="Whether the layout item is likely noise.")
|
||||
isLikelyNoise: bool = Field(
|
||||
default=False, description="Whether the layout item is likely noise."
|
||||
)
|
||||
|
||||
|
||||
class ChartItem(BaseModel):
|
||||
class ChartItem(SafeBaseModel):
|
||||
"""A chart in a page."""
|
||||
|
||||
name: str = Field(description="The name of the chart.")
|
||||
name: str = Field(default="", description="The name of the chart.")
|
||||
x: Optional[float] = Field(
|
||||
default=None, description="The x-coordinate of the chart."
|
||||
)
|
||||
@@ -109,7 +197,7 @@ class ChartItem(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class Page(BaseModel):
|
||||
class Page(SafeBaseModel):
|
||||
"""A page of the document."""
|
||||
|
||||
page: int = Field(default=0, description="The page number.")
|
||||
@@ -162,9 +250,22 @@ class Page(BaseModel):
|
||||
slideSpeakerNotes: Optional[str] = Field(
|
||||
default=None, description="The speaker notes for the slide."
|
||||
)
|
||||
confidence: Optional[float] = Field(
|
||||
default=None, description="The confidence of the page parsing."
|
||||
)
|
||||
printedPageNumber: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The printed page number on the page, if found and extractPrintedPageNumber is set to true.",
|
||||
)
|
||||
pageHeaderMarkdown: Optional[str] = Field(
|
||||
default=None, description="The page header in markdown format."
|
||||
)
|
||||
pageFooterMarkdown: Optional[str] = Field(
|
||||
default=None, description="The page footer in markdown format."
|
||||
)
|
||||
|
||||
|
||||
class JobResult(BaseModel):
|
||||
class JobResult(SafeBaseModel):
|
||||
"""The raw JSON result from the LlamaParse API."""
|
||||
|
||||
pages: List[Page] = Field(
|
||||
@@ -181,6 +282,13 @@ class JobResult(BaseModel):
|
||||
error: Optional[str] = Field(
|
||||
default=None, description="The error message if the job failed."
|
||||
)
|
||||
error_code: Optional[str] = Field(
|
||||
default=None, description="The error code if the job failed."
|
||||
)
|
||||
status: Optional[str] = Field(
|
||||
default=None,
|
||||
description="The job status (e.g., PENDING, SUCCESS, ERROR, CANCELED).",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -258,6 +366,29 @@ class JobResult(BaseModel):
|
||||
documents = await self.aget_text_documents(split_by_page)
|
||||
return [TextNode(text=doc.text, metadata=doc.metadata) for doc in documents]
|
||||
|
||||
def _format_markdown_for_notebook(self, text: Optional[str]) -> Optional[str]:
|
||||
"""Format markdown text for Jupyter notebook display by escaping dollar signs."""
|
||||
if text is None:
|
||||
return None
|
||||
|
||||
def escape_single_dollar_signs(text: str) -> str:
|
||||
"""Escape single dollar signs in text to prevent Jupyter from interpreting them as LaTeX.
|
||||
|
||||
Preserves all strings of dollar signs greater than length 1,
|
||||
especially preserving double dollar signs ($$) which denote LaTeX equations.
|
||||
|
||||
Args:
|
||||
text: The text to escape
|
||||
|
||||
Returns:
|
||||
Text with single dollar signs escaped
|
||||
"""
|
||||
# Replace single $ with \$, but preserve $$
|
||||
# Use negative lookahead and lookbehind to match $ not preceded or followed by $
|
||||
return re.sub(r"(?<!\$)\$(?!\$)", r"\$", text)
|
||||
|
||||
return escape_single_dollar_signs(text)
|
||||
|
||||
def get_markdown_documents(self, split_by_page: bool = False) -> List[Document]:
|
||||
"""
|
||||
Get the markdown documents from the job.
|
||||
@@ -268,17 +399,22 @@ class JobResult(BaseModel):
|
||||
if split_by_page:
|
||||
return [
|
||||
Document(
|
||||
text=page.md,
|
||||
text=self._format_markdown_for_notebook(page.md)
|
||||
if is_jupyter()
|
||||
else page.md,
|
||||
metadata={"page_number": page.page, "file_name": self.file_name},
|
||||
)
|
||||
for page in self.pages
|
||||
]
|
||||
else:
|
||||
text = self._page_separator.join(
|
||||
[page.md if page.md is not None else "" for page in self.pages]
|
||||
)
|
||||
return [
|
||||
Document(
|
||||
text=self._page_separator.join(
|
||||
[page.md if page.md is not None else "" for page in self.pages]
|
||||
),
|
||||
text=self._format_markdown_for_notebook(text)
|
||||
if is_jupyter()
|
||||
else text,
|
||||
metadata={"file_name": self.file_name},
|
||||
)
|
||||
]
|
||||
@@ -328,7 +464,10 @@ class JobResult(BaseModel):
|
||||
"""
|
||||
url = f"{self._base_url}/api/v1/parsing/job/{self.job_id}/result/raw/markdown"
|
||||
response = await make_api_request(self._client, "GET", url)
|
||||
return response.content.decode("utf-8")
|
||||
markdown = response.content.decode("utf-8")
|
||||
return (
|
||||
self._format_markdown_for_notebook(markdown) if is_jupyter() else markdown
|
||||
)
|
||||
|
||||
def get_text(self) -> str:
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import functools
|
||||
import httpx
|
||||
import itertools
|
||||
import logging
|
||||
@@ -356,6 +357,17 @@ def partition_pages(
|
||||
return
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def is_jupyter() -> bool:
|
||||
"""Check if we're running in a Jupyter environment."""
|
||||
try:
|
||||
from IPython import get_ipython
|
||||
|
||||
return get_ipython().__class__.__name__ == "ZMQInteractiveShell"
|
||||
except (ImportError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def extract_tables_from_json_results(
|
||||
json_results: List[dict], download_path: str
|
||||
) -> List[str]:
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from llama_cloud_services.report.report import ReportClient
|
||||
from llama_cloud_services.report.base import LlamaReport
|
||||
|
||||
__all__ = ["ReportClient", "LlamaReport"]
|
||||
@@ -1,269 +0,0 @@
|
||||
import asyncio
|
||||
import httpx
|
||||
import os
|
||||
import io
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Optional, List, Union, Any, Coroutine, TypeVar
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from llama_cloud.types import ReportMetadata
|
||||
from llama_cloud_services.report.report import ReportClient
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class LlamaReport:
|
||||
"""Client for managing reports and general report operations."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
async_httpx_client: Optional[httpx.AsyncClient] = None,
|
||||
):
|
||||
self.api_key = api_key or os.getenv("LLAMA_CLOUD_API_KEY", None)
|
||||
if not self.api_key:
|
||||
raise ValueError("No API key provided.")
|
||||
|
||||
self.base_url = base_url or os.getenv(
|
||||
"LLAMA_CLOUD_BASE_URL", "https://api.cloud.llamaindex.ai"
|
||||
)
|
||||
self.timeout = timeout or 60
|
||||
|
||||
# Initialize HTTP clients
|
||||
self._aclient = async_httpx_client or httpx.AsyncClient(timeout=self.timeout)
|
||||
|
||||
# Set auth headers
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
}
|
||||
|
||||
self.organization_id = organization_id
|
||||
self.project_id = project_id
|
||||
self._client_params = {
|
||||
"timeout": self._aclient.timeout,
|
||||
"headers": self._aclient.headers,
|
||||
"base_url": self._aclient.base_url,
|
||||
"auth": self._aclient.auth,
|
||||
"event_hooks": self._aclient.event_hooks,
|
||||
"cookies": self._aclient.cookies,
|
||||
"max_redirects": self._aclient.max_redirects,
|
||||
"params": self._aclient.params,
|
||||
"trust_env": self._aclient.trust_env,
|
||||
}
|
||||
self._thread_pool = ThreadPoolExecutor(
|
||||
max_workers=min(10, (os.cpu_count() or 1) + 4)
|
||||
)
|
||||
|
||||
@property
|
||||
def aclient(self) -> httpx.AsyncClient:
|
||||
if self._aclient is None:
|
||||
self._aclient = httpx.AsyncClient(**self._client_params)
|
||||
return self._aclient
|
||||
|
||||
def _run_sync(self, coro: Coroutine[Any, Any, T]) -> T:
|
||||
"""Run coroutine in a separate thread to avoid event loop issues"""
|
||||
|
||||
# force a new client for this thread/event loop
|
||||
original_client = self._aclient
|
||||
self._aclient = None
|
||||
|
||||
def run_coro() -> T:
|
||||
async def wrapped_coro() -> T:
|
||||
return await coro
|
||||
|
||||
return asyncio.run(wrapped_coro())
|
||||
|
||||
result = self._thread_pool.submit(run_coro).result()
|
||||
|
||||
# restore the original client
|
||||
self._aclient = original_client
|
||||
|
||||
return result
|
||||
|
||||
async def _get_default_project(self) -> str:
|
||||
response = await self.aclient.get(
|
||||
urljoin(str(self.base_url), "/api/v1/projects"), headers=self.headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
projects = response.json()
|
||||
default_project = [p for p in projects if p.get("is_default")]
|
||||
return default_project[0]["id"]
|
||||
|
||||
async def _build_url(
|
||||
self, endpoint: str, extra_params: Optional[List[str]] = None
|
||||
) -> str:
|
||||
"""Helper method to build URLs with common query parameters."""
|
||||
url = urljoin(str(self.base_url), endpoint)
|
||||
|
||||
if not self.project_id:
|
||||
self.project_id = await self._get_default_project()
|
||||
|
||||
query_params = []
|
||||
if self.organization_id:
|
||||
query_params.append(f"organization_id={self.organization_id}")
|
||||
if self.project_id:
|
||||
query_params.append(f"project_id={self.project_id}")
|
||||
if extra_params:
|
||||
query_params.extend([p for p in extra_params if p is not None])
|
||||
|
||||
if query_params:
|
||||
url += "?" + "&".join(query_params)
|
||||
|
||||
return url
|
||||
|
||||
async def acreate_report(
|
||||
self,
|
||||
name: str,
|
||||
template_instructions: Optional[str] = None,
|
||||
template_text: Optional[str] = None,
|
||||
template_file: Optional[Union[str, tuple[str, bytes]]] = None,
|
||||
input_files: Optional[List[Union[str, tuple[str, bytes]]]] = None,
|
||||
existing_retriever_id: Optional[str] = None,
|
||||
) -> ReportClient:
|
||||
"""Create a new report asynchronously."""
|
||||
url = await self._build_url("/api/v1/reports/")
|
||||
open_files: List[io.BufferedReader] = []
|
||||
|
||||
data = {"name": name}
|
||||
if template_instructions:
|
||||
data["template_instructions"] = template_instructions
|
||||
if template_text:
|
||||
data["template_text"] = template_text
|
||||
if existing_retriever_id:
|
||||
data["existing_retriever_id"] = str(existing_retriever_id)
|
||||
|
||||
files: List[tuple[str, io.BufferedReader | bytes]] = []
|
||||
if template_file:
|
||||
if isinstance(template_file, str):
|
||||
open_files.append(open(template_file, "rb"))
|
||||
files.append(("template_file", open_files[-1]))
|
||||
else:
|
||||
files.append(("template_file", template_file[1]))
|
||||
|
||||
if input_files:
|
||||
for f in input_files:
|
||||
if isinstance(f, str):
|
||||
open_files.append(open(f, "rb"))
|
||||
files.append(("files", open_files[-1]))
|
||||
else:
|
||||
files.append(("files", f[1]))
|
||||
|
||||
response = await self.aclient.post(
|
||||
url, headers=self.headers, data=data, files=files
|
||||
)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
report_id = response.json()["id"]
|
||||
return ReportClient(report_id, name, self)
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise ValueError(
|
||||
f"Failed to create report: {e.response.text}\nError Code: {e.response.status_code}"
|
||||
)
|
||||
finally:
|
||||
for open_file in open_files:
|
||||
open_file.close()
|
||||
|
||||
def create_report(
|
||||
self,
|
||||
name: str,
|
||||
template_instructions: Optional[str] = None,
|
||||
template_text: Optional[str] = None,
|
||||
template_file: Optional[Union[str, tuple[str, bytes]]] = None,
|
||||
input_files: Optional[List[Union[str, tuple[str, bytes]]]] = None,
|
||||
existing_retriever_id: Optional[str] = None,
|
||||
) -> ReportClient:
|
||||
"""Create a new report."""
|
||||
return self._run_sync(
|
||||
self.acreate_report(
|
||||
name=name,
|
||||
template_instructions=template_instructions,
|
||||
template_text=template_text,
|
||||
template_file=template_file,
|
||||
input_files=input_files,
|
||||
existing_retriever_id=existing_retriever_id,
|
||||
)
|
||||
)
|
||||
|
||||
async def alist_reports(
|
||||
self, state: Optional[str] = None, limit: int = 100, offset: int = 0
|
||||
) -> List[ReportClient]:
|
||||
"""List all reports asynchronously."""
|
||||
params = []
|
||||
if state:
|
||||
params.append(f"state={state}")
|
||||
if limit:
|
||||
params.append(f"limit={limit}")
|
||||
if offset:
|
||||
params.append(f"offset={offset}")
|
||||
|
||||
url = await self._build_url(
|
||||
"/api/v1/reports/list",
|
||||
extra_params=params,
|
||||
)
|
||||
|
||||
response = await self.aclient.get(url, headers=self.headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
return [
|
||||
ReportClient(r["report_id"], r["name"], self)
|
||||
for r in data["report_responses"]
|
||||
]
|
||||
|
||||
def list_reports(
|
||||
self, state: Optional[str] = None, limit: int = 100, offset: int = 0
|
||||
) -> List[ReportClient]:
|
||||
"""Synchronous wrapper for listing reports."""
|
||||
return self._run_sync(self.alist_reports(state, limit, offset))
|
||||
|
||||
async def aget_report(self, report_id: str) -> ReportClient:
|
||||
"""Get a Report instance for working with a specific report."""
|
||||
url = await self._build_url(f"/api/v1/reports/{report_id}")
|
||||
|
||||
response = await self.aclient.get(url, headers=self.headers)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
return ReportClient(data["report_id"], data["name"], self)
|
||||
|
||||
def get_report(self, report_id: str) -> ReportClient:
|
||||
"""Synchronous wrapper for getting a report."""
|
||||
return self._run_sync(self.aget_report(report_id))
|
||||
|
||||
async def aget_report_metadata(self, report_id: str) -> ReportMetadata:
|
||||
"""Get metadata for a specific report asynchronously.
|
||||
|
||||
Returns:
|
||||
dict containing:
|
||||
- id: Report ID
|
||||
- name: Report name
|
||||
- state: Current report state
|
||||
- report_metadata: Additional metadata
|
||||
- template_file: Name of template file if used
|
||||
- template_instructions: Template instructions if provided
|
||||
- input_files: List of input file names
|
||||
"""
|
||||
url = await self._build_url(f"/api/v1/reports/{report_id}/metadata")
|
||||
|
||||
response = await self.aclient.get(url, headers=self.headers)
|
||||
response.raise_for_status()
|
||||
return ReportMetadata(**response.json())
|
||||
|
||||
def get_report_metadata(self, report_id: str) -> ReportMetadata:
|
||||
"""Synchronous wrapper for getting report metadata."""
|
||||
return self._run_sync(self.aget_report_metadata(report_id))
|
||||
|
||||
async def adelete_report(self, report_id: str) -> None:
|
||||
"""Delete a specific report asynchronously."""
|
||||
url = await self._build_url(f"/api/v1/reports/{report_id}")
|
||||
|
||||
response = await self.aclient.delete(url, headers=self.headers)
|
||||
response.raise_for_status()
|
||||
|
||||
def delete_report(self, report_id: str) -> None:
|
||||
"""Synchronous wrapper for deleting a report."""
|
||||
return self._run_sync(self.adelete_report(report_id))
|
||||
@@ -1,527 +0,0 @@
|
||||
import asyncio
|
||||
import httpx
|
||||
import time
|
||||
from typing import Optional, List, Literal, Union, TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from llama_cloud.types import (
|
||||
ReportEventItemEventData_Progress,
|
||||
ReportMetadata,
|
||||
EditSuggestion,
|
||||
ReportResponse,
|
||||
ReportPlan,
|
||||
ReportBlock,
|
||||
ReportPlanBlock,
|
||||
Report,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from llama_cloud_services.report.base import LlamaReport
|
||||
|
||||
|
||||
class MessageRole(str, Enum):
|
||||
USER = "user"
|
||||
ASSISTANT = "assistant"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
role: MessageRole
|
||||
content: str
|
||||
timestamp: datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class EditAction:
|
||||
block_idx: int
|
||||
old_content: str
|
||||
new_content: Optional[str]
|
||||
action: Literal["approved", "rejected"]
|
||||
timestamp: datetime
|
||||
|
||||
|
||||
DEFAULT_POLL_INTERVAL = 5
|
||||
DEFAULT_TIMEOUT = 600
|
||||
|
||||
|
||||
class ReportClient:
|
||||
"""Client for operations on a specific report."""
|
||||
|
||||
def __init__(self, report_id: str, name: str, parent_client: "LlamaReport"):
|
||||
self.report_id = report_id
|
||||
self.name = name
|
||||
self._client = parent_client
|
||||
self._headers = parent_client.headers
|
||||
self._run_sync = parent_client._run_sync
|
||||
self._build_url = parent_client._build_url
|
||||
self.chat_history: List[Message] = []
|
||||
self.edit_history: List[EditAction] = []
|
||||
|
||||
@property
|
||||
def aclient(self) -> httpx.AsyncClient:
|
||||
return self._client.aclient
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Report(id={self.report_id}, name={self.name})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Report(id={self.report_id}, name={self.name})"
|
||||
|
||||
def _get_block_content(self, block: Union[ReportBlock, ReportPlanBlock]) -> str:
|
||||
if isinstance(block, ReportBlock):
|
||||
return block.template
|
||||
elif isinstance(block, ReportPlanBlock):
|
||||
return block.block.template
|
||||
else:
|
||||
raise ValueError(f"Invalid block type: {type(block)}")
|
||||
|
||||
def _get_block_idx(self, block: Union[ReportBlock, ReportPlanBlock]) -> int:
|
||||
if isinstance(block, ReportBlock):
|
||||
return block.idx
|
||||
elif isinstance(block, ReportPlanBlock):
|
||||
return block.block.idx
|
||||
else:
|
||||
raise ValueError(f"Invalid block type: {type(block)}")
|
||||
|
||||
async def aget(self, version: Optional[int] = None) -> ReportResponse:
|
||||
"""Get this report's details asynchronously."""
|
||||
extra_params = []
|
||||
if version is not None:
|
||||
extra_params.append(f"version={version}")
|
||||
|
||||
url = await self._build_url(f"/api/v1/reports/{self.report_id}", extra_params)
|
||||
|
||||
response = await self.aclient.get(url, headers=self._headers)
|
||||
response.raise_for_status()
|
||||
return ReportResponse(**response.json())
|
||||
|
||||
def get(self, version: Optional[int] = None) -> ReportResponse:
|
||||
"""Synchronous wrapper for getting this report's details."""
|
||||
return self._run_sync(self.aget(version))
|
||||
|
||||
async def aupdate_report(self, updated_report: Report) -> ReportResponse:
|
||||
"""Update this report's content asynchronously."""
|
||||
url = await self._build_url(f"/api/v1/reports/{self.report_id}")
|
||||
response = await self.aclient.patch(
|
||||
url, headers=self._headers, json={"content": updated_report.dict()}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return ReportResponse(**response.json())
|
||||
|
||||
def update_report(self, updated_report: Report) -> ReportResponse:
|
||||
"""Synchronous wrapper for updating this report's content."""
|
||||
return self._run_sync(self.aupdate_report(updated_report))
|
||||
|
||||
async def aupdate_plan(
|
||||
self,
|
||||
action: Literal["approve", "reject", "edit"],
|
||||
updated_plan: Optional[ReportPlan] = None,
|
||||
) -> ReportResponse:
|
||||
"""Update this report's plan asynchronously."""
|
||||
if action == "edit" and not updated_plan:
|
||||
raise ValueError("updated_plan is required when action is 'edit'")
|
||||
|
||||
url = await self._build_url(
|
||||
f"/api/v1/reports/{self.report_id}/plan", [f"action={action}"]
|
||||
)
|
||||
|
||||
data = None
|
||||
if updated_plan is not None:
|
||||
plan_dict = updated_plan.dict()
|
||||
plan_dict.pop("generated_at", None)
|
||||
data = plan_dict
|
||||
|
||||
if updated_plan is None and action == "edit":
|
||||
raise ValueError("updated_plan is required when action is 'edit'")
|
||||
|
||||
response = await self.aclient.patch(url, headers=self._headers, json=data)
|
||||
response.raise_for_status()
|
||||
return ReportResponse(**response.json())
|
||||
|
||||
def update_plan(
|
||||
self,
|
||||
action: Literal["approve", "reject", "edit"],
|
||||
updated_plan: Optional[ReportPlan] = None,
|
||||
) -> ReportResponse:
|
||||
"""Synchronous wrapper for updating this report's plan."""
|
||||
return self._run_sync(self.aupdate_plan(action, updated_plan))
|
||||
|
||||
async def asuggest_edits(
|
||||
self,
|
||||
user_query: str,
|
||||
auto_history: bool = True,
|
||||
chat_history: Optional[List[dict]] = None,
|
||||
) -> List[EditSuggestion]:
|
||||
"""Get AI suggestions for edits to this report asynchronously.
|
||||
|
||||
Args:
|
||||
user_query: The user's request/question about what to edit
|
||||
auto_history: Whether to automatically add the user's message to the chat history
|
||||
chat_history:
|
||||
A list of chat messages to include in the chat history.
|
||||
The format being a list of dictionaries with "role" and "content" keys.
|
||||
"""
|
||||
# Add user message to history
|
||||
self.chat_history.append(
|
||||
Message(role=MessageRole.USER, content=user_query, timestamp=datetime.now())
|
||||
)
|
||||
|
||||
# Format chat history with edit summaries
|
||||
chat_history_dicts = []
|
||||
for msg in self.chat_history[:-1]: # Exclude current message
|
||||
content = msg.content
|
||||
if msg.role == MessageRole.USER:
|
||||
# Add edit summary for user messages
|
||||
edit_summary = self._get_edit_summary_after_message(msg.timestamp)
|
||||
if edit_summary:
|
||||
content = f"{content}\n\nActions taken:\n{edit_summary}"
|
||||
|
||||
chat_history_dicts.append({"role": msg.role.value, "content": content})
|
||||
|
||||
# decide whether to include chat history or not
|
||||
if chat_history:
|
||||
chat_history_dicts = chat_history
|
||||
elif auto_history:
|
||||
chat_history_dicts = chat_history_dicts
|
||||
else:
|
||||
chat_history_dicts = []
|
||||
|
||||
# Make the API call
|
||||
url = await self._build_url(f"/api/v1/reports/{self.report_id}/suggest_edits")
|
||||
data = {"user_query": user_query, "chat_history": chat_history_dicts}
|
||||
|
||||
response = await self.aclient.post(url, headers=self._headers, json=data)
|
||||
response.raise_for_status()
|
||||
suggestions = response.json()
|
||||
suggestions = [EditSuggestion(**suggestion) for suggestion in suggestions]
|
||||
|
||||
# Add assistant response to history
|
||||
if suggestions:
|
||||
for suggestion in suggestions:
|
||||
self.chat_history.append(
|
||||
Message(
|
||||
role=MessageRole.ASSISTANT,
|
||||
content=suggestion.justification,
|
||||
timestamp=datetime.now(),
|
||||
)
|
||||
)
|
||||
|
||||
return suggestions
|
||||
|
||||
def suggest_edits(
|
||||
self,
|
||||
user_query: str,
|
||||
auto_history: bool = True,
|
||||
chat_history: Optional[List[dict]] = None,
|
||||
) -> List[EditSuggestion]:
|
||||
"""Synchronous wrapper for getting edit suggestions."""
|
||||
return self._run_sync(
|
||||
self.asuggest_edits(user_query, auto_history, chat_history)
|
||||
)
|
||||
|
||||
async def await_completion(
|
||||
self, timeout: int = DEFAULT_TIMEOUT, poll_interval: int = DEFAULT_POLL_INTERVAL
|
||||
) -> Report:
|
||||
"""Wait for this report to complete processing."""
|
||||
start_time = time.time()
|
||||
while True:
|
||||
report_response = await self.aget()
|
||||
status = report_response.status
|
||||
|
||||
if status == "completed":
|
||||
return report_response.report
|
||||
elif status == "error":
|
||||
events = await self.aget_events()
|
||||
raise ValueError(f"Report entered error state: {events[-1].msg}")
|
||||
elif time.time() - start_time > timeout:
|
||||
raise TimeoutError(f"Report did not complete within {timeout} seconds")
|
||||
|
||||
await asyncio.sleep(poll_interval)
|
||||
|
||||
def wait_for_completion(
|
||||
self, timeout: int = DEFAULT_TIMEOUT, poll_interval: int = DEFAULT_POLL_INTERVAL
|
||||
) -> Report:
|
||||
"""Synchronous wrapper for awaiting report completion."""
|
||||
return self._run_sync(self.await_completion(timeout, poll_interval))
|
||||
|
||||
async def await_for_plan(
|
||||
self, timeout: int = DEFAULT_TIMEOUT, poll_interval: int = DEFAULT_POLL_INTERVAL
|
||||
) -> ReportPlan:
|
||||
"""Wait for this report's plan to be ready for review."""
|
||||
start_time = time.time()
|
||||
while True:
|
||||
report_metadata = await self.aget_metadata()
|
||||
state = report_metadata.state
|
||||
|
||||
if state == "waiting_approval":
|
||||
report_response = await self.aget()
|
||||
return report_response.plan
|
||||
elif state == "error":
|
||||
events = await self.aget_events()
|
||||
raise ValueError(f"Report entered error state: {events[-1].msg}")
|
||||
elif time.time() - start_time > timeout:
|
||||
raise TimeoutError(f"Plan was not ready within {timeout} seconds")
|
||||
|
||||
await asyncio.sleep(poll_interval)
|
||||
|
||||
def wait_for_plan(
|
||||
self, timeout: int = DEFAULT_TIMEOUT, poll_interval: int = DEFAULT_POLL_INTERVAL
|
||||
) -> ReportPlan:
|
||||
"""Synchronous wrapper for awaiting plan readiness."""
|
||||
return self._run_sync(self.await_for_plan(timeout, poll_interval))
|
||||
|
||||
async def aget_metadata(self) -> ReportMetadata:
|
||||
"""Get this report's metadata asynchronously."""
|
||||
return await self._client.aget_report_metadata(self.report_id)
|
||||
|
||||
def get_metadata(self) -> ReportMetadata:
|
||||
"""Synchronous wrapper for getting this report's metadata."""
|
||||
return self._run_sync(self.aget_metadata())
|
||||
|
||||
async def adelete(self) -> None:
|
||||
"""Delete this report asynchronously."""
|
||||
return await self._client.adelete_report(self.report_id)
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Synchronous wrapper for deleting this report."""
|
||||
return self._run_sync(self.adelete())
|
||||
|
||||
async def aaccept_edit(self, suggestion: EditSuggestion) -> None:
|
||||
"""Accept a suggested edit.
|
||||
|
||||
Args:
|
||||
suggestion: The EditSuggestion to accept, typically from suggest_edits()
|
||||
"""
|
||||
if len(suggestion.blocks) == 0:
|
||||
return
|
||||
|
||||
# Determine if we're editing a plan or report based on first block type
|
||||
is_plan_edit = isinstance(suggestion.blocks[0], ReportPlanBlock)
|
||||
|
||||
# Get current content
|
||||
report_response = await self.aget()
|
||||
current_blocks = (
|
||||
report_response.plan.blocks
|
||||
if is_plan_edit
|
||||
else report_response.report.blocks
|
||||
)
|
||||
|
||||
# Track the edit
|
||||
new_blocks = []
|
||||
for edit_block in suggestion.blocks:
|
||||
# Find matching block in current content
|
||||
old_block = next(
|
||||
(
|
||||
b
|
||||
for b in current_blocks
|
||||
if self._get_block_idx(b) == self._get_block_idx(edit_block)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
old_content = (
|
||||
self._get_block_content(old_block) if old_block else "[No old content]"
|
||||
)
|
||||
new_content = self._get_block_content(edit_block)
|
||||
|
||||
if is_plan_edit:
|
||||
new_queries_str = "\n".join(
|
||||
[
|
||||
f"Field: {q.field}, Prompt: {q.prompt}, Context: {q.context}"
|
||||
for q in edit_block.queries
|
||||
]
|
||||
)
|
||||
new_dependency_str = (
|
||||
f"Depends on: {edit_block.dependency}"
|
||||
if edit_block.dependency
|
||||
else ""
|
||||
)
|
||||
new_content += f"\n\n{new_queries_str}\n{new_dependency_str}"
|
||||
|
||||
if old_block:
|
||||
old_queries_str = "\n".join(
|
||||
[
|
||||
f"Field: {q.field}, Prompt: {q.prompt}, Context: {q.context}"
|
||||
for q in old_block.queries
|
||||
]
|
||||
)
|
||||
old_dependency_str = (
|
||||
f"Depends on: {old_block.dependency}"
|
||||
if old_block.dependency
|
||||
else ""
|
||||
)
|
||||
old_content += f"\n\n{old_queries_str}\n{old_dependency_str}"
|
||||
|
||||
self.edit_history.append(
|
||||
EditAction(
|
||||
block_idx=self._get_block_idx(edit_block),
|
||||
old_content=old_content,
|
||||
new_content=new_content,
|
||||
action="approved",
|
||||
timestamp=datetime.now(),
|
||||
)
|
||||
)
|
||||
|
||||
# Create updated block
|
||||
if is_plan_edit:
|
||||
new_blocks.append(
|
||||
ReportPlanBlock(
|
||||
block=ReportBlock(
|
||||
idx=edit_block.block.idx,
|
||||
template=self._get_block_content(edit_block),
|
||||
sources=edit_block.block.sources,
|
||||
),
|
||||
queries=edit_block.queries,
|
||||
dependency=edit_block.dependency,
|
||||
)
|
||||
)
|
||||
else:
|
||||
new_blocks.append(
|
||||
ReportBlock(
|
||||
idx=edit_block.idx,
|
||||
template=self._get_block_content(edit_block),
|
||||
sources=edit_block.sources,
|
||||
)
|
||||
)
|
||||
|
||||
if new_blocks:
|
||||
if is_plan_edit:
|
||||
# Update plan in place
|
||||
plan = report_response.plan
|
||||
|
||||
# Replace edited blocks and add new ones
|
||||
for new_block in new_blocks:
|
||||
block_idx = self._get_block_idx(new_block)
|
||||
existing_block_idx = next(
|
||||
(
|
||||
i
|
||||
for i, b in enumerate(plan.blocks)
|
||||
if b.block.idx == block_idx
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if existing_block_idx is not None:
|
||||
# Replace existing block
|
||||
plan.blocks[existing_block_idx] = new_block
|
||||
else:
|
||||
# Add new block to end
|
||||
plan.blocks.append(new_block)
|
||||
|
||||
await self.aupdate_plan("edit", plan)
|
||||
else:
|
||||
# Update report in place
|
||||
report = report_response.report
|
||||
|
||||
# Replace edited blocks and add new ones
|
||||
for new_block in new_blocks:
|
||||
block_idx = self._get_block_idx(new_block)
|
||||
existing_block_idx = next(
|
||||
(i for i, b in enumerate(report.blocks) if b.idx == block_idx),
|
||||
None,
|
||||
)
|
||||
|
||||
if existing_block_idx is not None:
|
||||
# Replace existing block
|
||||
report.blocks[existing_block_idx] = new_block
|
||||
else:
|
||||
# Add new block to end
|
||||
report.blocks.append(new_block)
|
||||
|
||||
await self.aupdate_report(report)
|
||||
|
||||
def accept_edit(self, suggestion: EditSuggestion) -> None:
|
||||
"""Synchronous wrapper for accepting an edit."""
|
||||
return self._run_sync(self.aaccept_edit(suggestion))
|
||||
|
||||
async def areject_edit(self, suggestion: EditSuggestion) -> None:
|
||||
"""Reject a suggested edit.
|
||||
|
||||
Args:
|
||||
suggestion: The EditSuggestion to reject, typically from suggest_edits()
|
||||
"""
|
||||
# Track the rejections
|
||||
for edit_block in suggestion.blocks:
|
||||
self.edit_history.append(
|
||||
EditAction(
|
||||
block_idx=self._get_block_idx(edit_block),
|
||||
old_content=self._get_block_content(edit_block),
|
||||
new_content=None,
|
||||
action="rejected",
|
||||
timestamp=datetime.now(),
|
||||
)
|
||||
)
|
||||
|
||||
def reject_edit(self, suggestion: EditSuggestion) -> None:
|
||||
"""Synchronous wrapper for rejecting an edit."""
|
||||
return self._run_sync(self.areject_edit(suggestion))
|
||||
|
||||
def _get_edit_summary_after_message(
|
||||
self, message_timestamp: datetime
|
||||
) -> Optional[str]:
|
||||
"""Get a summary of edits that occurred after a specific message."""
|
||||
relevant_edits = [
|
||||
edit for edit in self.edit_history if edit.timestamp > message_timestamp
|
||||
]
|
||||
|
||||
if not relevant_edits:
|
||||
return None
|
||||
|
||||
approved = [edit for edit in relevant_edits if edit.action == "approved"]
|
||||
rejected = [edit for edit in relevant_edits if edit.action == "rejected"]
|
||||
|
||||
summary = []
|
||||
|
||||
if approved:
|
||||
summary.append("Approved edits:")
|
||||
for edit in approved:
|
||||
summary.append(
|
||||
f'Block {edit.block_idx}: "{edit.old_content}" -> "{edit.new_content}"'
|
||||
)
|
||||
|
||||
if rejected:
|
||||
if approved: # Add spacing if we had approved edits
|
||||
summary.append("")
|
||||
summary.append("Rejected edits:")
|
||||
for edit in rejected:
|
||||
summary.append(f'Block {edit.block_idx}: "{edit.old_content}"')
|
||||
|
||||
return "\n".join(summary)
|
||||
|
||||
async def aget_events(
|
||||
self, last_sequence: Optional[int] = None
|
||||
) -> List[ReportEventItemEventData_Progress]:
|
||||
"""Get all events for this report asynchronously.
|
||||
|
||||
Args:
|
||||
last_sequence: If provided, only get events after this sequence number
|
||||
|
||||
Returns:
|
||||
List of ReportEvent objects
|
||||
"""
|
||||
extra_params = []
|
||||
if last_sequence is not None:
|
||||
extra_params.append(f"last_sequence={last_sequence}")
|
||||
|
||||
url = await self._build_url(
|
||||
f"/api/v1/reports/{self.report_id}/events", extra_params
|
||||
)
|
||||
|
||||
response = await self.aclient.get(url, headers=self._headers)
|
||||
response.raise_for_status()
|
||||
progress_events = []
|
||||
for event in response.json():
|
||||
if event["event_type"] == "progress":
|
||||
progress_events.append(
|
||||
ReportEventItemEventData_Progress(**event["event_data"])
|
||||
)
|
||||
|
||||
return progress_events
|
||||
|
||||
def get_events(
|
||||
self, last_sequence: Optional[int] = None
|
||||
) -> List[ReportEventItemEventData_Progress]:
|
||||
"""Synchronous wrapper for getting report events."""
|
||||
return self._run_sync(self.aget_events(last_sequence))
|
||||