Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2d970a442 | |||
| 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 |
@@ -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": []
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"llama-cloud-services-py": patch
|
||||
---
|
||||
|
||||
Remove unneeded organization_id param from beta classifier client
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"llama-cloud-services": minor
|
||||
---
|
||||
|
||||
Switch to keyword arguments rather than positional args
|
||||
@@ -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 }}
|
||||
@@ -22,7 +22,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
|
||||
|
||||
@@ -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,31 +4,19 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Complete Parse → Classify → Extract Workflow with LlamaCloud Services\n",
|
||||
"# Document Classification + Extraction Workflow with LlamaCloud + LlamaIndex Workflows\n",
|
||||
"\n",
|
||||
"This notebook demonstrates the complete workflow for processing documents using LlamaCloud services:\n",
|
||||
"1. **Parse** - Extract and convert documents to markdown\n",
|
||||
"<a href=\"https://colab.research.google.com/github/run-llama/llama_cloud_services/blob/main/examples/misc/parse_classify_extract_workflow.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
|
||||
"\n",
|
||||
"This notebook shows a multi-step agentic document workflow that uses the **parsing**, **classification** and **extraction** modules in LlamaCloud, orchestrated through **LlamaIndex Workflows**. The workflow can take in a complex input document, parse it into clean markdown, classify it according to its subtype, and extract data according to a specified schema for that subtype. This allows you to automate document extraction of various types within the same workflow instead of having to manually separate the data beforehand. \n",
|
||||
"\n",
|
||||
"This notebook uses the following modules:\n",
|
||||
"1. **Parse (LlamaParse)** - Extract and convert documents to markdown\n",
|
||||
"2. **Classify** - Categorize documents based on their content\n",
|
||||
"3. **Extract** - Extract structured data using the markdown as input via SourceText\n",
|
||||
"3. **Extract (LlamaExtract)** - Extract structured data using the markdown as input via SourceText\n",
|
||||
"4. **LlamaIndex Workflows** - Event-driven orchestration of the parse, classify and extract steps\n",
|
||||
"\n",
|
||||
"## Overview of the Workflow\n",
|
||||
"\n",
|
||||
"### 1. Parse Phase\n",
|
||||
"- Use `LlamaParse` to convert documents (PDFs, Word docs, etc.) into structured formats\n",
|
||||
"- Extract markdown content that preserves document structure\n",
|
||||
"- Get both raw text and markdown representations\n",
|
||||
"\n",
|
||||
"### 2. Classify Phase\n",
|
||||
"- Use `ClassifyClient` to categorize documents based on content\n",
|
||||
"- Apply classification rules to route documents appropriately\n",
|
||||
"- Handle different document types with specific processing logic\n",
|
||||
"\n",
|
||||
"### 3. Extract Phase\n",
|
||||
"- Use `LlamaExtract` with `SourceText` to extract structured data\n",
|
||||
"- Pass the markdown content as input for more accurate extraction\n",
|
||||
"- Define custom schemas for structured data extraction\n",
|
||||
"\n",
|
||||
"Let's walk through each step with practical examples."
|
||||
"The workflow is implemented as a proper LlamaIndex Workflow with separate steps for parsing, classification, and extraction, connected by typed events. This provides modularity, observability, and type safety."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -45,8 +33,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Install required packages\n",
|
||||
"!pip install llama-cloud-services\n",
|
||||
"!pip install python-dotenv"
|
||||
"%pip install llama-cloud-services\n",
|
||||
"%pip install python-dotenv"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -73,7 +61,7 @@
|
||||
"nest_asyncio.apply()\n",
|
||||
"\n",
|
||||
"# Set up API key\n",
|
||||
"os.environ[\"LLAMA_CLOUD_API_KEY\"] = \"\" # edit it\n",
|
||||
"# os.environ[\"LLAMA_CLOUD_API_KEY\"] = \"\" # edit it\n",
|
||||
"\n",
|
||||
"# Setup Base URL\n",
|
||||
"# os.envrion[\"LLAMA_CLOUD_BASE_URL\"] = \"https://api.cloud.eu.llamaindex.ai/\" # update if necessay\n",
|
||||
@@ -99,7 +87,8 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"📁 financial_report.pdf already exists\n",
|
||||
"Downloading financial_report.pdf...\n",
|
||||
"✅ Downloaded financial_report.pdf\n",
|
||||
"📁 technical_spec.pdf already exists\n",
|
||||
"\n",
|
||||
"📂 Sample documents ready!\n"
|
||||
@@ -115,7 +104,7 @@
|
||||
"\n",
|
||||
"# Download sample documents\n",
|
||||
"docs_to_download = {\n",
|
||||
" \"financial_report.pdf\": \"https://raw.githubusercontent.com/run-llama/llama_index/main/docs/docs/examples/data/10k/uber_2021.pdf\",\n",
|
||||
" \"financial_report.pdf\": \"https://raw.githubusercontent.com/run-llama/llama_index/main/docs/examples/data/10k/uber_2021.pdf\",\n",
|
||||
" \"technical_spec.pdf\": \"https://www.ti.com/lit/ds/symlink/lm317.pdf\",\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
@@ -155,10 +144,10 @@
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🔄 Parsing documents...\n",
|
||||
"Started parsing the file under job_id 8a8c76f9-354d-4275-91d8-312ff1adc762\n",
|
||||
"...✅ Parsed financial report (Job ID: 8a8c76f9-354d-4275-91d8-312ff1adc762)\n",
|
||||
"Started parsing the file under job_id 7e603448-ed80-4d18-948b-6801ed51c41b\n",
|
||||
"✅ Parsed technical spec (Job ID: 7e603448-ed80-4d18-948b-6801ed51c41b)\n",
|
||||
"Started parsing the file under job_id 530c187a-bd2d-4eea-b38d-9e5738eab465\n",
|
||||
".✅ Parsed financial report (Job ID: 530c187a-bd2d-4eea-b38d-9e5738eab465)\n",
|
||||
"Started parsing the file under job_id a6e27710-776b-4445-8b94-8d75959ff5db\n",
|
||||
"✅ Parsed technical spec (Job ID: a6e27710-776b-4445-8b94-8d75959ff5db)\n",
|
||||
"\n",
|
||||
"📄 Parsing complete!\n"
|
||||
]
|
||||
@@ -246,23 +235,23 @@
|
||||
"\n",
|
||||
"## 1 Features\n",
|
||||
"\n",
|
||||
"• Output voltage range:\n",
|
||||
"- Output voltage range:\n",
|
||||
" – Adjustable: 1.25V to 37V\n",
|
||||
"• Output current: 1.5A\n",
|
||||
"• Line regulation: 0.01%/V (typ)\n",
|
||||
"• Load regulation: 0.1% (typ)\n",
|
||||
"• Internal short-circuit current limiting\n",
|
||||
"• Thermal overload protection\n",
|
||||
"• Output safe-area compensation (new chip)\n",
|
||||
"• PSRR: 80dB at 120Hz for CADJ = 10μF (new chip)\n",
|
||||
"• Packages:\n",
|
||||
"- Output current: 1.5A\n",
|
||||
"- Line regulation: 0.01%/V (typ)\n",
|
||||
"- Load regulation: 0.1% (typ)\n",
|
||||
"- Internal short-circuit current limiting\n",
|
||||
"- Thermal overload protection\n",
|
||||
"- Output safe-area compensation (new chip)\n",
|
||||
"- PSRR: 80dB at 120Hz for CADJ = 10μF (new chip)\n",
|
||||
"- Packages:\n",
|
||||
" – 4-pin, SOT-223 (DCY)\n",
|
||||
" – 3-pin, TO-263 (KTT)\n",
|
||||
" – 3-pin, TO-220 (KCS, KCT),\n",
|
||||
"...\n",
|
||||
"\n",
|
||||
"📏 Financial report markdown length: 1348671 characters\n",
|
||||
"📏 Technical spec markdown length: 90971 characters\n"
|
||||
"📏 Financial report markdown length: 1338499 characters\n",
|
||||
"📏 Technical spec markdown length: 92483 characters\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -339,6 +328,72 @@
|
||||
"print(f\"📝 Created {len(classification_rules)} classification rules\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Try Classification Independently\n",
|
||||
"\n",
|
||||
"Let's test the classification on one of our parsed documents to see how it works:\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🔍 Classifying financial document...\n",
|
||||
" Document length: 1,338,499 characters\n",
|
||||
"\n",
|
||||
"✅ Classification Result:\n",
|
||||
" Type: financial_document\n",
|
||||
" Confidence: 100.00%\n",
|
||||
" Reasoning: This document is a Form 10-K, which is an annual report required by the U.S. Securities and Exchange Commission (SEC) for publicly traded companies. It contains financial data, information about the c...\n",
|
||||
"\n",
|
||||
"======================================================================\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Let's classify the financial document\n",
|
||||
"print(\"🔍 Classifying financial document...\")\n",
|
||||
"print(f\" Document length: {len(financial_markdown):,} characters\\n\")\n",
|
||||
"\n",
|
||||
"# Write to temp file for classification\n",
|
||||
"import tempfile\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"with tempfile.NamedTemporaryFile(\n",
|
||||
" mode=\"w\", suffix=\".md\", delete=False, encoding=\"utf-8\"\n",
|
||||
") as tmp:\n",
|
||||
" tmp.write(financial_markdown)\n",
|
||||
" temp_financial_path = Path(tmp.name)\n",
|
||||
"\n",
|
||||
"# Classify the document\n",
|
||||
"financial_classification = await classify_client.aclassify_file_path(\n",
|
||||
" rules=classification_rules, file_input_path=str(temp_financial_path)\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"doc_type = financial_classification.items[0].result.type\n",
|
||||
"confidence = financial_classification.items[0].result.confidence\n",
|
||||
"reasoning = financial_classification.items[0].result.reasoning\n",
|
||||
"\n",
|
||||
"print(f\"✅ Classification Result:\")\n",
|
||||
"print(f\" Type: {doc_type}\")\n",
|
||||
"print(f\" Confidence: {confidence:.2%}\")\n",
|
||||
"print(\n",
|
||||
" f\" Reasoning: {reasoning[:200]}...\"\n",
|
||||
" if reasoning and len(reasoning) > 200\n",
|
||||
" else f\" Reasoning: {reasoning}\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"\\n\" + \"=\" * 70)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
@@ -444,9 +499,31 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Complete Workflow Summary\n",
|
||||
"## Building the Complete Workflow\n",
|
||||
"\n",
|
||||
"Let's create a function that demonstrates the complete workflow:"
|
||||
"Now that we've seen how parsing works, let's build a complete 3-step workflow (Parse → Classify → Extract) using LlamaIndex Workflows. We'll define the workflow structure here, and you can see it in action below where we also demonstrate the classification and extraction modules independently.\n",
|
||||
"\n",
|
||||
"### Install Workflows Package\n",
|
||||
"\n",
|
||||
"First, let's install the LlamaIndex workflows package:\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install llama-index-workflows llama-index-utils-workflow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define the Workflow\n",
|
||||
"\n",
|
||||
"Let's restructure the document processing into a proper LlamaIndex Workflow with separate classification and extraction steps:\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -458,7 +535,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🔧 Workflow function defined!\n"
|
||||
"🔧 Workflow defined!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -466,81 +543,286 @@
|
||||
"import tempfile\n",
|
||||
"from pathlib import Path\n",
|
||||
"from llama_cloud import ExtractConfig\n",
|
||||
"from workflows import Workflow, step, Context\n",
|
||||
"from workflows.events import Event, StartEvent, StopEvent\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def complete_document_workflow(markdown_content: str):\n",
|
||||
"# Define workflow events\n",
|
||||
"class ParseEvent(Event):\n",
|
||||
" \"\"\"Event emitted after parsing\"\"\"\n",
|
||||
"\n",
|
||||
" file_path: str\n",
|
||||
" markdown_content: str\n",
|
||||
" job_id: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class ClassifyEvent(Event):\n",
|
||||
" \"\"\"Event emitted after classification\"\"\"\n",
|
||||
"\n",
|
||||
" markdown_content: str\n",
|
||||
" temp_path: str\n",
|
||||
" doc_type: str\n",
|
||||
" confidence: float\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class ExtractEvent(Event):\n",
|
||||
" \"\"\"Event emitted after extraction\"\"\"\n",
|
||||
"\n",
|
||||
" doc_type: str\n",
|
||||
" confidence: float\n",
|
||||
" extracted_data: dict\n",
|
||||
" markdown_length: int\n",
|
||||
" temp_path: str\n",
|
||||
" markdown_sample: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class DocumentWorkflow(Workflow):\n",
|
||||
" \"\"\"\n",
|
||||
" Complete workflow: Parse → Classify → Extract\n",
|
||||
" Complete document processing workflow: Parse → Classify → Extract\n",
|
||||
" \"\"\"\n",
|
||||
" print(f\"🚀 Starting complete workflow\")\n",
|
||||
" print(\"=\" * 60)\n",
|
||||
"\n",
|
||||
" # Step 1: Classify\n",
|
||||
" print(\"🏷️ Step 2: Classifying document...\")\n",
|
||||
" def __init__(\n",
|
||||
" self,\n",
|
||||
" parser,\n",
|
||||
" classify_client,\n",
|
||||
" classification_rules,\n",
|
||||
" llama_extract,\n",
|
||||
" financial_schema,\n",
|
||||
" technical_schema,\n",
|
||||
" **kwargs,\n",
|
||||
" ):\n",
|
||||
" super().__init__(**kwargs)\n",
|
||||
" self.parser = parser\n",
|
||||
" self.classify_client = classify_client\n",
|
||||
" self.classification_rules = classification_rules\n",
|
||||
" self.llama_extract = llama_extract\n",
|
||||
" self.financial_schema = financial_schema\n",
|
||||
" self.technical_schema = technical_schema\n",
|
||||
"\n",
|
||||
" with tempfile.NamedTemporaryFile(\n",
|
||||
" mode=\"w\", suffix=\".md\", delete=False, encoding=\"utf-8\"\n",
|
||||
" ) as tmp:\n",
|
||||
" tmp.write(markdown_content)\n",
|
||||
" temp_path = Path(tmp.name)\n",
|
||||
" @step\n",
|
||||
" async def parse_document(self, ctx: Context, ev: StartEvent) -> ParseEvent:\n",
|
||||
" \"\"\"\n",
|
||||
" Step 1: Parse the document to extract markdown\n",
|
||||
" \"\"\"\n",
|
||||
" file_path = ev.file_path\n",
|
||||
" print(f\"📄 Step 1: Parsing document: {file_path}...\")\n",
|
||||
"\n",
|
||||
" print(temp_path)\n",
|
||||
" # Parse the document\n",
|
||||
" parse_result = await self.parser.aparse(file_path)\n",
|
||||
" markdown_content = await parse_result.aget_markdown()\n",
|
||||
" job_id = parse_result.job_id\n",
|
||||
"\n",
|
||||
" classification = await classify_client.aclassify_file_path(\n",
|
||||
" rules=classification_rules, file_input_path=str(temp_path)\n",
|
||||
" )\n",
|
||||
" doc_type = classification.items[0].result.type\n",
|
||||
" confidence = classification.items[0].result.confidence\n",
|
||||
" print(f\" ✅ Classified as: {doc_type} (confidence: {confidence:.2f})\")\n",
|
||||
" print(f\" ✅ Parsed successfully (Job ID: {job_id})\")\n",
|
||||
" print(f\" 📝 Extracted {len(markdown_content):,} characters\")\n",
|
||||
"\n",
|
||||
" # Step 2: Extract based on classification\n",
|
||||
" print(\"🔍 Step 3: Extracting structured data using SourceText...\")\n",
|
||||
" source_text = SourceText(\n",
|
||||
" text_content=markdown_content,\n",
|
||||
" filename=f\"{os.path.basename(temp_path)}_markdown.md\",\n",
|
||||
" )\n",
|
||||
" # Write event to stream for monitoring\n",
|
||||
" parse_event = ParseEvent(\n",
|
||||
" file_path=file_path,\n",
|
||||
" markdown_content=markdown_content,\n",
|
||||
" job_id=job_id,\n",
|
||||
" )\n",
|
||||
" ctx.write_event_to_stream(parse_event)\n",
|
||||
"\n",
|
||||
" # Choose schema based on classification\n",
|
||||
" if \"financial\" in doc_type.lower():\n",
|
||||
" schema = FinancialMetrics\n",
|
||||
" print(\" 📊 Using FinancialMetrics schema\")\n",
|
||||
" elif \"technical\" in doc_type.lower():\n",
|
||||
" schema = TechnicalSpec\n",
|
||||
" print(\" 🔧 Using TechnicalSpec schema\")\n",
|
||||
" else:\n",
|
||||
" schema = FinancialMetrics # Default fallback\n",
|
||||
" print(\" 📊 Using default FinancialMetrics schema\")\n",
|
||||
" return parse_event\n",
|
||||
"\n",
|
||||
" extract_config = ExtractConfig(\n",
|
||||
" extraction_mode=\"BALANCED\",\n",
|
||||
" )\n",
|
||||
" @step\n",
|
||||
" async def classify_document(self, ctx: Context, ev: ParseEvent) -> ClassifyEvent:\n",
|
||||
" \"\"\"\n",
|
||||
" Step 2: Classify the document based on its content\n",
|
||||
" \"\"\"\n",
|
||||
" markdown_content = ev.markdown_content\n",
|
||||
" print(\"🏷️ Step 2: Classifying document...\")\n",
|
||||
"\n",
|
||||
" extraction_result = llama_extract.extract(\n",
|
||||
" data_schema=schema, config=extract_config, files=source_text\n",
|
||||
" )\n",
|
||||
" # Write markdown to temp file for classification\n",
|
||||
" with tempfile.NamedTemporaryFile(\n",
|
||||
" mode=\"w\", suffix=\".md\", delete=False, encoding=\"utf-8\"\n",
|
||||
" ) as tmp:\n",
|
||||
" tmp.write(markdown_content)\n",
|
||||
" temp_path = Path(tmp.name)\n",
|
||||
"\n",
|
||||
" print(\" ✅ Extraction complete!\")\n",
|
||||
" # Classify the document\n",
|
||||
" classification = await self.classify_client.aclassify_file_path(\n",
|
||||
" rules=self.classification_rules, file_input_path=str(temp_path)\n",
|
||||
" )\n",
|
||||
" doc_type = classification.items[0].result.type\n",
|
||||
" confidence = classification.items[0].result.confidence\n",
|
||||
"\n",
|
||||
" return {\n",
|
||||
" \"file_path\": temp_path,\n",
|
||||
" \"markdown_length\": len(markdown_content),\n",
|
||||
" \"classification\": doc_type,\n",
|
||||
" \"confidence\": confidence,\n",
|
||||
" \"extracted_data\": extraction_result.data,\n",
|
||||
" \"markdown_sample\": markdown_content[:200] + \"...\"\n",
|
||||
" if len(markdown_content) > 200\n",
|
||||
" else markdown_content,\n",
|
||||
" }\n",
|
||||
" print(f\" ✅ Classified as: {doc_type} (confidence: {confidence:.2f})\")\n",
|
||||
"\n",
|
||||
" # Write event to stream for monitoring\n",
|
||||
" classify_event = ClassifyEvent(\n",
|
||||
" markdown_content=markdown_content,\n",
|
||||
" temp_path=str(temp_path),\n",
|
||||
" doc_type=doc_type,\n",
|
||||
" confidence=confidence,\n",
|
||||
" )\n",
|
||||
" ctx.write_event_to_stream(classify_event)\n",
|
||||
"\n",
|
||||
" return classify_event\n",
|
||||
"\n",
|
||||
" @step\n",
|
||||
" async def extract_data(self, ctx: Context, ev: ClassifyEvent) -> ExtractEvent:\n",
|
||||
" \"\"\"\n",
|
||||
" Step 3: Extract structured data based on classification\n",
|
||||
" \"\"\"\n",
|
||||
" print(\"🔍 Step 3: Extracting structured data using SourceText...\")\n",
|
||||
"\n",
|
||||
" # Choose schema based on classification\n",
|
||||
" if \"financial\" in ev.doc_type.lower():\n",
|
||||
" schema = self.financial_schema\n",
|
||||
" print(\" 📊 Using FinancialMetrics schema\")\n",
|
||||
" elif \"technical\" in ev.doc_type.lower():\n",
|
||||
" schema = self.technical_schema\n",
|
||||
" print(\" 🔧 Using TechnicalSpec schema\")\n",
|
||||
" else:\n",
|
||||
" schema = self.financial_schema # Default fallback\n",
|
||||
" print(\" 📊 Using default FinancialMetrics schema\")\n",
|
||||
"\n",
|
||||
" # Create SourceText from markdown content\n",
|
||||
" source_text = SourceText(\n",
|
||||
" text_content=ev.markdown_content,\n",
|
||||
" filename=f\"{os.path.basename(ev.temp_path)}_markdown.md\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" # Configure extraction\n",
|
||||
" extract_config = ExtractConfig(\n",
|
||||
" extraction_mode=\"BALANCED\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" # Perform extraction\n",
|
||||
" extraction_result = self.llama_extract.extract(\n",
|
||||
" data_schema=schema, config=extract_config, files=source_text\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" print(\" ✅ Extraction complete!\")\n",
|
||||
"\n",
|
||||
" # Create markdown sample\n",
|
||||
" markdown_sample = (\n",
|
||||
" ev.markdown_content[:200] + \"...\"\n",
|
||||
" if len(ev.markdown_content) > 200\n",
|
||||
" else ev.markdown_content\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" extract_event = ExtractEvent(\n",
|
||||
" doc_type=ev.doc_type,\n",
|
||||
" confidence=ev.confidence,\n",
|
||||
" extracted_data=extraction_result.data,\n",
|
||||
" markdown_length=len(ev.markdown_content),\n",
|
||||
" temp_path=ev.temp_path,\n",
|
||||
" markdown_sample=markdown_sample,\n",
|
||||
" )\n",
|
||||
" ctx.write_event_to_stream(extract_event)\n",
|
||||
"\n",
|
||||
" return extract_event\n",
|
||||
"\n",
|
||||
" @step\n",
|
||||
" async def finalize_results(self, ctx: Context, ev: ExtractEvent) -> StopEvent:\n",
|
||||
" \"\"\"\n",
|
||||
" Step 4: Finalize and return results\n",
|
||||
" \"\"\"\n",
|
||||
" result = {\n",
|
||||
" \"file_path\": ev.temp_path,\n",
|
||||
" \"markdown_length\": ev.markdown_length,\n",
|
||||
" \"classification\": ev.doc_type,\n",
|
||||
" \"confidence\": ev.confidence,\n",
|
||||
" \"extracted_data\": ev.extracted_data,\n",
|
||||
" \"markdown_sample\": ev.markdown_sample,\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" return StopEvent(result=result)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(\"🔧 Workflow function defined!\")"
|
||||
"print(\"🔧 Workflow defined!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Run Complete Workflow on Both Documents"
|
||||
"### Workflow Structure\n",
|
||||
"\n",
|
||||
"The workflow consists of four steps connected by typed events:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"┌─────────────┐\n",
|
||||
"│ StartEvent │ (file_path)\n",
|
||||
"└──────┬──────┘\n",
|
||||
" │\n",
|
||||
" ▼\n",
|
||||
"┌──────────────────┐\n",
|
||||
"│ parse_document │ Step 1: Parse PDF to markdown\n",
|
||||
"└──────┬───────────┘\n",
|
||||
" │\n",
|
||||
" ▼\n",
|
||||
"┌─────────────┐\n",
|
||||
"│ ParseEvent │ (markdown_content, job_id)\n",
|
||||
"└──────┬──────┘\n",
|
||||
" │\n",
|
||||
" ▼\n",
|
||||
"┌─────────────────────┐\n",
|
||||
"│ classify_document │ Step 2: Classification\n",
|
||||
"└──────┬──────────────┘\n",
|
||||
" │\n",
|
||||
" ▼\n",
|
||||
"┌──────────────┐\n",
|
||||
"│ ClassifyEvent│ (doc_type, confidence, markdown_content)\n",
|
||||
"└──────┬───────┘\n",
|
||||
" │\n",
|
||||
" ▼\n",
|
||||
"┌──────────────┐\n",
|
||||
"│ extract_data │ Step 3: Extraction with schema selection\n",
|
||||
"└──────┬───────┘\n",
|
||||
" │\n",
|
||||
" ▼\n",
|
||||
"┌──────────────┐\n",
|
||||
"│ ExtractEvent │ (extracted_data, doc_type, confidence)\n",
|
||||
"└──────┬───────┘\n",
|
||||
" │\n",
|
||||
" ▼\n",
|
||||
"┌──────────────────┐\n",
|
||||
"│ finalize_results │ Step 4: Format and return results\n",
|
||||
"└──────┬───────────┘\n",
|
||||
" │\n",
|
||||
" ▼\n",
|
||||
"┌─────────────┐\n",
|
||||
"│ StopEvent │ (final result dictionary)\n",
|
||||
"└─────────────┘\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"**Key Features:**\n",
|
||||
"- **Step 1 (parse_document)**: Takes a file path and parses the document into clean markdown\n",
|
||||
"- **Step 2 (classify_document)**: Takes markdown content and classifies it into document types\n",
|
||||
"- **Step 3 (extract_data)**: Selects appropriate schema based on classification and extracts structured data\n",
|
||||
"- **Step 4 (finalize_results)**: Packages all results into final output format\n",
|
||||
"- Events are written to the stream for real-time monitoring\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Visualize the Workflow\n",
|
||||
"\n",
|
||||
"Let's visualize the workflow structure to see the flow of events:\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Initialize the workflow\n",
|
||||
"workflow = DocumentWorkflow(\n",
|
||||
" parser=parser,\n",
|
||||
" classify_client=classify_client,\n",
|
||||
" classification_rules=classification_rules,\n",
|
||||
" llama_extract=llama_extract,\n",
|
||||
" financial_schema=FinancialMetrics,\n",
|
||||
" technical_schema=TechnicalSpec,\n",
|
||||
" timeout=300,\n",
|
||||
" verbose=True,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -552,53 +834,173 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🚀 Starting complete workflow\n",
|
||||
"============================================================\n",
|
||||
"🏷️ Step 2: Classifying document...\n",
|
||||
"/var/folders/g6/4b5lpp5974gcpr890ybhbw4r0000gn/T/tmpos3b62tm.md\n",
|
||||
" ✅ Classified as: financial_document (confidence: 1.00)\n",
|
||||
"🔍 Step 3: Extracting structured data using SourceText...\n",
|
||||
" 📊 Using FinancialMetrics schema\n",
|
||||
".. ✅ Extraction complete!\n",
|
||||
"\n",
|
||||
"============================================================\n",
|
||||
"\n",
|
||||
"🚀 Starting complete workflow\n",
|
||||
"============================================================\n",
|
||||
"🏷️ Step 2: Classifying document...\n",
|
||||
"/var/folders/g6/4b5lpp5974gcpr890ybhbw4r0000gn/T/tmpppz9ub_m.md\n",
|
||||
" ✅ Classified as: technical_specification (confidence: 1.00)\n",
|
||||
"🔍 Step 3: Extracting structured data using SourceText...\n",
|
||||
" 🔧 Using TechnicalSpec schema\n",
|
||||
" ✅ Extraction complete!\n",
|
||||
"\n",
|
||||
"============================================================\n",
|
||||
"\n",
|
||||
"📋 Processed 2 documents successfully!\n"
|
||||
"document_workflow.html\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Process both documents through the complete workflow\n",
|
||||
"results = []\n",
|
||||
"# Draw the workflow visualization\n",
|
||||
"from llama_index.utils.workflow import draw_all_possible_flows\n",
|
||||
"\n",
|
||||
"for doc_text in document_texts:\n",
|
||||
" try:\n",
|
||||
" result = await complete_document_workflow(doc_text)\n",
|
||||
" results.append(result)\n",
|
||||
" print(\"\\n\" + \"=\" * 60 + \"\\n\")\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"❌ Error processing {doc_path}: {str(e)}\")\n",
|
||||
" print(\"\\n\" + \"=\" * 60 + \"\\n\")\n",
|
||||
"\n",
|
||||
"print(f\"📋 Processed {len(results)} documents successfully!\")"
|
||||
"draw_all_possible_flows(\n",
|
||||
" workflow,\n",
|
||||
" filename=\"document_workflow.html\",\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Final Results Summary"
|
||||
"The workflow has been visualized and saved to `document_workflow.html`. You can open this file in a browser to see the interactive workflow diagram.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The workflow visualization shows:\n",
|
||||
"1. **StartEvent** → **parse_document** step\n",
|
||||
"2. **ParseEvent** → **classify_document** step\n",
|
||||
"3. **ClassifyEvent** → **extract_data** step \n",
|
||||
"4. **ExtractEvent** → **finalize_results** step\n",
|
||||
"5. **StopEvent** (final output)\n",
|
||||
"\n",
|
||||
"Each step is connected by typed events, allowing for clean separation of concerns and easy monitoring of the workflow execution.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Run the Workflow on Both Documents\n",
|
||||
"\n",
|
||||
"Now let's run the workflow on both documents and monitor the events:\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"======================================================================\n",
|
||||
"🚀 Processing Document 1: sample_docs/financial_report.pdf\n",
|
||||
"======================================================================\n",
|
||||
"\n",
|
||||
"Running step parse_document\n",
|
||||
"📄 Step 1: Parsing document: sample_docs/financial_report.pdf...\n",
|
||||
"Started parsing the file under job_id bb53c6bf-79cc-4f63-9c97-16983d59f29d\n",
|
||||
". ✅ Parsed successfully (Job ID: bb53c6bf-79cc-4f63-9c97-16983d59f29d)\n",
|
||||
" 📝 Extracted 1,338,499 characters\n",
|
||||
"Step parse_document produced event ParseEvent\n",
|
||||
"📄 Parse Event: Extracted 1,338,499 characters\n",
|
||||
"Running step classify_document\n",
|
||||
"🏷️ Step 2: Classifying document...\n",
|
||||
" ✅ Classified as: financial_document (confidence: 1.00)\n",
|
||||
"Step classify_document produced event ClassifyEvent\n",
|
||||
"📊 Classification Event: financial_document (1.00)\n",
|
||||
"Running step extract_data\n",
|
||||
"🔍 Step 3: Extracting structured data using SourceText...\n",
|
||||
" 📊 Using FinancialMetrics schema\n",
|
||||
".. ✅ Extraction complete!\n",
|
||||
"Step extract_data produced event ExtractEvent\n",
|
||||
"Running step finalize_results\n",
|
||||
"Step finalize_results produced event StopEvent\n",
|
||||
"✅ Extraction Event: 7 fields extracted\n",
|
||||
"\n",
|
||||
"✅ Document 1 processed successfully!\n",
|
||||
"\n",
|
||||
"======================================================================\n",
|
||||
"🚀 Processing Document 2: sample_docs/technical_spec.pdf\n",
|
||||
"======================================================================\n",
|
||||
"\n",
|
||||
"Running step parse_document\n",
|
||||
"📄 Step 1: Parsing document: sample_docs/technical_spec.pdf...\n",
|
||||
"Started parsing the file under job_id 944905c1-3c49-431a-ad86-4436d16f3d1c\n",
|
||||
" ✅ Parsed successfully (Job ID: 944905c1-3c49-431a-ad86-4436d16f3d1c)\n",
|
||||
" 📝 Extracted 92,483 characters\n",
|
||||
"Step parse_document produced event ParseEvent\n",
|
||||
"📄 Parse Event: Extracted 92,483 characters\n",
|
||||
"Running step classify_document\n",
|
||||
"🏷️ Step 2: Classifying document...\n",
|
||||
" ✅ Classified as: technical_specification (confidence: 1.00)\n",
|
||||
"Step classify_document produced event ClassifyEvent\n",
|
||||
"📊 Classification Event: technical_specification (1.00)\n",
|
||||
"Running step extract_data\n",
|
||||
"🔍 Step 3: Extracting structured data using SourceText...\n",
|
||||
" 🔧 Using TechnicalSpec schema\n",
|
||||
" ✅ Extraction complete!\n",
|
||||
"Step extract_data produced event ExtractEvent\n",
|
||||
"Running step finalize_results\n",
|
||||
"Step finalize_results produced event StopEvent\n",
|
||||
"✅ Extraction Event: 8 fields extracted\n",
|
||||
"\n",
|
||||
"✅ Document 2 processed successfully!\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"📋 Processed 2 documents successfully!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Process both documents through the workflow\n",
|
||||
"results = []\n",
|
||||
"\n",
|
||||
"# Define the document files to process\n",
|
||||
"document_files = [\n",
|
||||
" \"sample_docs/financial_report.pdf\",\n",
|
||||
" \"sample_docs/technical_spec.pdf\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for i, file_path in enumerate(document_files, 1):\n",
|
||||
" print(f\"\\n{'='*70}\")\n",
|
||||
" print(f\"🚀 Processing Document {i}: {file_path}\")\n",
|
||||
" print(f\"{'='*70}\\n\")\n",
|
||||
"\n",
|
||||
" try:\n",
|
||||
" # Run the workflow\n",
|
||||
" handler = workflow.run(file_path=file_path)\n",
|
||||
"\n",
|
||||
" # Monitor events as they are emitted\n",
|
||||
" async for event in handler.stream_events():\n",
|
||||
" if isinstance(event, ParseEvent):\n",
|
||||
" print(\n",
|
||||
" f\"📄 Parse Event: Extracted {len(event.markdown_content):,} characters\"\n",
|
||||
" )\n",
|
||||
" elif isinstance(event, ClassifyEvent):\n",
|
||||
" print(\n",
|
||||
" f\"📊 Classification Event: {event.doc_type} ({event.confidence:.2f})\"\n",
|
||||
" )\n",
|
||||
" elif isinstance(event, ExtractEvent):\n",
|
||||
" print(\n",
|
||||
" f\"✅ Extraction Event: {len(event.extracted_data)} fields extracted\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" # Get final result\n",
|
||||
" result = await handler\n",
|
||||
" results.append(result)\n",
|
||||
"\n",
|
||||
" print(f\"\\n✅ Document {i} processed successfully!\")\n",
|
||||
"\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"❌ Error processing document {i}: {str(e)}\")\n",
|
||||
" import traceback\n",
|
||||
"\n",
|
||||
" traceback.print_exc()\n",
|
||||
"\n",
|
||||
"print(f\"\\n\\n📋 Processed {len(results)} documents successfully!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Final Results Summary\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -613,9 +1015,9 @@
|
||||
"📈 COMPLETE WORKFLOW RESULTS SUMMARY\n",
|
||||
"======================================================================\n",
|
||||
"\n",
|
||||
"📄 Document 1: tmpos3b62tm.md\n",
|
||||
"📄 Document 1: tmpuyxzpd3x.md\n",
|
||||
" 📊 Classification: financial_document (confidence: 1.00)\n",
|
||||
" 📝 Markdown length: 1,348,671 characters\n",
|
||||
" 📝 Markdown length: 1,338,499 characters\n",
|
||||
" 📋 Markdown sample: \n",
|
||||
"\n",
|
||||
"# UNITED STATES\n",
|
||||
@@ -629,14 +1031,14 @@
|
||||
" • company_name: Uber Technologies, Inc.\n",
|
||||
" • document_type: Annual Report on Form 10-K\n",
|
||||
" • fiscal_year: 2021\n",
|
||||
" • revenue_2021: $21,764\n",
|
||||
" • net_income_2021: $(496)\n",
|
||||
" • key_business_segments: ['Mobility', 'Delivery', 'Freight', 'All Other (including former New Mobility, e-bikes, e-scooters, Advanced Technologies Group and other technology programs)']\n",
|
||||
" • risk_factors: [\"The company faces numerous risk factors across its business operations and environment. The COVID-19 pandemic and related mitigation measures have adversely affected parts of the business, including reduced demand for Mobility offerings and creating ongoing uncertainties. The company's operational and financial performance is influenced by competitive pressure in the mobility, delivery, and logistics industries, characterized by well-established alternatives, low barriers to entry, and low switching costs. Driver classification risks exist if Drivers are deemed employees, workers, or quasi-employees rather than independent contractors, exposing the company to legal actions and financial liabilities globally. Competition challenges require the company to sometimes lower fares, offer incentives, and promotions, which impacts profitability. There are significant operating losses historically with substantial future operating expense increases anticipated, and the ability to achieve or maintain profitability is uncertain. Network value depends on maintaining critical mass among Drivers, consumers, merchants, shippers, and carriers, and failures to do so diminish platform attractiveness. Brand and reputation maintenance is critical, with exposure to negative publicity, media coverage, and risks from associated companies' brands or licensed brands in joint ventures.\\n\\nOperational risks include historical workplace culture and compliance challenges, management complexity due to rapid growth, technological infrastructure issues potentially causing disruptions or poor user experience, and security or data privacy breaches that could impact revenue and reputation. Platform users may engage in or be subjected to criminal, violent, or dangerous activity leading to safety incidents and legal actions. New offerings and technologies investments are inherently risky without guaranteed benefits. Economic conditions, inflation, and increased costs (fuel, food, labor, energy) may negatively impact results. Regulatory risks are extensive and global, involving payment and financial services compliance, licensing, anti-money laundering laws, data privacy (GDPR, CCPA, LGPD), and labor laws. Legal and regulatory investigations and inquiries, including antitrust, FCPA, labor classification, data protection, and intellectual property matters, pose risks of fines, penalties, operational changes, and increased costs.\\n\\nGeopolitical and jurisdictional risks include operating limitations or bans in some locations, currency exchange risk, and complex evolving regulations with the potential for fines and loss of licenses or permits. Insurance risks include potential inadequacy of reserves, liability exposure from accidents or impersonation, and insurer insolvency. Driver qualification requirements and background checks may increase costs or fail to expose all relevant information, with associated insurance cost risks and potential for courtroom or regulatory challenges to pricing models.\\n\\nFinancial risks comprise significant accumulated deficits, requirement for additional capital with uncertain availability, debt obligations, tax exposure including uncertain positions and observed changes in tax laws, and volatility in common stock price with no expected cash dividends. Accounting judgments and estimates involve critical assumptions affecting reported financial metrics related to goodwill, revenue recognition, incentive accruals, and stock-based compensation. Cybersecurity risks include exposures to malware, ransomware, phishing, and other cyberattacks. Climate change presents physical and transitional risks that may impact operations and costs, and failure to meet climate commitments may have operational and reputational consequences.\\n\\nOther risks include potential liability under anti-corruption and anti-terrorism laws, adverse effects from defaults under debt agreements, limitations in takeover actions due to corporate governance provisions, and the impact of non-GAAP financial measure limitations. Overall, these diverse and interconnected risk factors contribute to significant uncertainty regarding the company's future business prospects, operating results, and financial condition.\"]\n",
|
||||
" • revenue_2021: $17,455 and $21,764\n",
|
||||
" • net_income_2021: $(496) to (700)\n",
|
||||
" • key_business_segments: ['Borrower and the Restricted Subsidiaries', 'Holdings', 'Guarantors', 'Material Domestic Subsidiaries', 'Material Foreign Subsidiaries']\n",
|
||||
" • risk_factors: ['Indemnification obligations of the borrower for losses, claims, damages, liabilities, and out-of-pocket expenses incurred by agents, lenders, arrangers, and related parties in connection with the agreement or loans, except in certain cases such as gross negligence, bad faith, willful misconduct, or material breach by the indemnitee.', \"Borrower not required to indemnify any indemnitee for settlements entered into without the borrower's consent.\", 'Limitation of liability for special, indirect, consequential, or punitive damages, and for damages from unauthorized use of information, except for direct damages resulting from gross negligence, bad faith, or willful misconduct.', 'Obligation of the borrower to indemnify the administrative agent for liabilities arising from performance of duties, except in cases of gross negligence, bad faith, or willful misconduct.', 'Limitations and conditions on assignments and participations of lender rights, including restrictions on assignments to disqualified institutions, loan parties, affiliates of loan parties, defaulting lenders, and natural persons.', 'Setoff rights for lenders and issuing banks after an event of default, allowing them to apply borrower deposits toward obligations under the agreement.', 'Potential for increased obligations under the agreement as a result of changes in law affecting payment terms.', 'Requirement for the borrower and guarantors to provide information to comply with anti-money laundering rules and the USA PATRIOT Act.']\n",
|
||||
"\n",
|
||||
"📄 Document 2: tmpppz9ub_m.md\n",
|
||||
"📄 Document 2: tmp7ower2xm.md\n",
|
||||
" 📊 Classification: technical_specification (confidence: 1.00)\n",
|
||||
" 📝 Markdown length: 90,971 characters\n",
|
||||
" 📝 Markdown length: 92,483 characters\n",
|
||||
" 📋 Markdown sample: \n",
|
||||
"\n",
|
||||
"LM317\n",
|
||||
@@ -648,20 +1050,14 @@
|
||||
" 🎯 Extracted fields: 8 fields\n",
|
||||
" • component_name: LM317\n",
|
||||
" • manufacturer: Texas Instruments\n",
|
||||
" • part_number: LM317\n",
|
||||
" • description: The LM317 is an adjustable three-pin, positive-voltage regulator capable of supplying up to 1.5A over an output voltage range of 1.25V to 37V. It features line and load regulation, internal current limiting, thermal overload protection, and safe operating area compensation.\n",
|
||||
" • part_number: LM317, SLVS044Z\n",
|
||||
" • description: The LM317 is an adjustable three-pin, positive-voltage regulator capable of supplying more than 1.5A (typically up to 1.5A) over an output voltage range of 1.25V to 37V. The device requires only two external resistors to set the output voltage. It features a typical line regulation of 0.01% and typical load regulation of 0.1%. The LM317 includes current limiting, thermal overload protection, and safe operating area protection. Overload protection remains functional even if the ADJUST pin is disconnected. The regulator is used in applications such as constant-current battery-charger circuits, slow turn-on 15V regulator circuits, AC voltage-regulator circuits, current-limited charger circuits, and high-current and adjustable regulator circuits. It is available in packages including SOT-223 (DCY), TO-220 (KCS), and TO-263 (KTT).\n",
|
||||
" • operating_voltage: {'min_voltage': 1.25, 'max_voltage': 37.0, 'unit': 'V'}\n",
|
||||
" • maximum_current: 1.5\n",
|
||||
" • key_features: ['Adjustable output voltage: 1.25V to 37V', 'Output current up to 1.5A', 'Line regulation: 0.01%/V (typical)', 'Load regulation: 0.1% (typical)', 'Internal short-circuit current limiting', 'Thermal overload protection', 'Output safe-area compensation', 'High power supply rejection ratio (PSRR): 80dB at 120Hz (new chip)', 'Available in SOT-223, TO-263, and TO-220 packages']\n",
|
||||
" • applications: ['Multifunction printers', 'AC drive power stage modules', 'Electricity meters', 'Servo drive control modules', 'Merchant network and server power supply units']\n",
|
||||
" • maximum_current: 4.0\n",
|
||||
" • key_features: ['Adjustable output voltage range: 1.25V to 37V', 'Output current up to 1.5A (up to 4A with external pass elements)', 'Line regulation: typically 0.01%/V', 'Load regulation: typically 0.1%', 'Internal short-circuit current limiting / Current limiting', 'Thermal overload protection / Thermal shutdown', 'Output safe-area compensation / Safe operating area protection', 'PSRR: 80dB at 120Hz for CADJ = 10μF (new chip)', 'NPN Darlington output drive', 'Programmable feedback', 'Multiple package options (SOT-223, TO-220, TO-263)', 'Can be used in constant-current, battery-charging, and regulator applications']\n",
|
||||
" • applications: ['Multifunction printers, AC drive power stage modules, Electricity meters, Servo drive control modules, Merchant network and server PSU, Adjustable voltage regulator, 0V to 30V regulator circuit, Regulator circuit with improved ripple rejection, Precision current-limiter, Tracking preregulator, 1.25V to 20V regulator, Battery charger circuit, Constant-current battery charger circuits, Slow turn-on regulator, AC voltage-regulator, Current-limited charger circuits, High-current adjustable regulator circuits, General-purpose adjustable power supply']\n",
|
||||
"\n",
|
||||
"✨ Workflow completed successfully!\n",
|
||||
"\n",
|
||||
"📚 Key Learnings:\n",
|
||||
" • Parse: Converted documents to clean markdown format\n",
|
||||
" • Classify: Automatically categorized document types\n",
|
||||
" • Extract: Used SourceText with markdown for structured data extraction\n",
|
||||
" • The markdown content provides much better context for extraction than raw PDFs\n"
|
||||
"✨ Workflow completed successfully!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -683,14 +1079,7 @@
|
||||
" for key, value in extracted.items():\n",
|
||||
" print(f\" • {key}: {value}\")\n",
|
||||
"\n",
|
||||
"print(\"\\n✨ Workflow completed successfully!\")\n",
|
||||
"print(\"\\n📚 Key Learnings:\")\n",
|
||||
"print(\" • Parse: Converted documents to clean markdown format\")\n",
|
||||
"print(\" • Classify: Automatically categorized document types\")\n",
|
||||
"print(\" • Extract: Used SourceText with markdown for structured data extraction\")\n",
|
||||
"print(\n",
|
||||
" \" • The markdown content provides much better context for extraction than raw PDFs\"\n",
|
||||
")"
|
||||
"print(\"\\n✨ Workflow completed successfully!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -699,9 +1088,9 @@
|
||||
"source": [
|
||||
"## Conclusion\n",
|
||||
"\n",
|
||||
"This notebook demonstrated the complete **Parse → Classify → Extract** workflow using LlamaCloud services:\n",
|
||||
"The notebook shows you how to build an e2e document **Classify → Extract** workflow using LlamaCloud. This uses some of our core building blocks around **classification** interleaved with **document extraction**.\n",
|
||||
"\n",
|
||||
"### Key Components:\n",
|
||||
"### Main Components:\n",
|
||||
"\n",
|
||||
"1. **LlamaParse** (`llama_cloud_services.parse.base.LlamaParse`):\n",
|
||||
" - Converts documents to clean, structured markdown\n",
|
||||
@@ -715,38 +1104,17 @@
|
||||
"\n",
|
||||
"3. **LlamaExtract with SourceText** (`llama_cloud_services.extract.extract.LlamaExtract`, `SourceText`):\n",
|
||||
" - Extracts structured data using custom Pydantic schemas\n",
|
||||
" - **SourceText** allows using markdown content as input instead of raw files\n",
|
||||
" - Provides much better extraction accuracy when using processed markdown\n",
|
||||
" - You can either feed in the file directly (in which case parsing will happen under the hood), or the parsed text through the **SourceText** object (which is the case in this example) \n",
|
||||
"\n",
|
||||
"### Workflow Benefits:\n",
|
||||
"\n",
|
||||
"- **Better Accuracy**: Using markdown from parsing provides cleaner, more structured input for extraction\n",
|
||||
"- **Automatic Routing**: Classification allows different processing logic for different document types\n",
|
||||
"- **Structured Output**: Custom schemas ensure consistent, structured data extraction\n",
|
||||
"- **Flexible Input**: SourceText supports text content, file paths, and bytes\n",
|
||||
"\n",
|
||||
"### Key Insights:\n",
|
||||
"\n",
|
||||
"1. **SourceText is the bridge**: It allows you to pass the clean markdown content from parsing directly to extraction\n",
|
||||
"2. **Markdown improves extraction**: Pre-processed markdown provides much better context than raw PDFs\n",
|
||||
"3. **Classification enables smart routing**: Different document types can use different extraction schemas\n",
|
||||
"4. **End-to-end automation**: The entire workflow can be automated for production use\n",
|
||||
"\n",
|
||||
"This approach is ideal for production document processing pipelines where you need to:\n",
|
||||
"- Process various document types automatically\n",
|
||||
"- Extract structured data consistently\n",
|
||||
"- Maintain high accuracy and reliability\n",
|
||||
"- Handle documents at scale\n",
|
||||
"\n",
|
||||
"The combination of these three services provides a powerful, flexible document processing pipeline that can handle complex, real-world document processing requirements."
|
||||
"**Benefits of an e2e workflow**: The main benefit of doing Classify -> Extract, instead of only Extract, is the fact that you can handle documents of different types/different expected schemas within the same workflow, without having to separate out the data before and running separate extractions on each data subset. "
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"display_name": "llama_parse",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
"name": "llama_parse"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
packages:
|
||||
- "ts/**"
|
||||
- "ts/*"
|
||||
- "py"
|
||||
- "py/*"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# llama-cloud-services-py
|
||||
|
||||
## 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
|
||||
@@ -1,5 +1,6 @@
|
||||
from llama_cloud_services.parse import LlamaParse
|
||||
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,
|
||||
@@ -12,6 +13,7 @@ __all__ = [
|
||||
"LlamaExtract",
|
||||
"ExtractionAgent",
|
||||
"SourceText",
|
||||
"FileInput",
|
||||
"EU_BASE_URL",
|
||||
"LlamaCloudIndex",
|
||||
"LlamaCloudRetriever",
|
||||
|
||||
@@ -194,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,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from llama_cloud_services.beta.classifier.client import ClassifyClient
|
||||
from llama_cloud_services.beta.classifier.types import ClassifyJobResultsWithFiles
|
||||
from llama_cloud_services.utils import SourceText, FileInput
|
||||
|
||||
__all__ = [
|
||||
"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 (
|
||||
@@ -14,7 +15,11 @@ from llama_cloud.types import (
|
||||
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,
|
||||
@@ -34,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.
|
||||
"""
|
||||
@@ -43,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
|
||||
@@ -59,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":
|
||||
"""
|
||||
@@ -69,7 +70,6 @@ class ClassifyClient:
|
||||
return cls(
|
||||
client,
|
||||
project_id,
|
||||
organization_id,
|
||||
)
|
||||
|
||||
async def acreate_classify_job(
|
||||
@@ -96,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(
|
||||
@@ -147,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
|
||||
@@ -166,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],
|
||||
@@ -173,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,
|
||||
@@ -186,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,
|
||||
@@ -202,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,
|
||||
@@ -221,12 +327,17 @@ 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) -> ClassifyJob:
|
||||
"""
|
||||
@@ -241,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):
|
||||
@@ -252,6 +363,6 @@ 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
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -333,7 +333,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 +420,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",
|
||||
@@ -919,7 +919,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
|
||||
# Add file to pipeline
|
||||
pipeline_file_create = PipelineFileCreate(file_id=file.id)
|
||||
self._client.pipelines.add_files_to_pipeline_api(
|
||||
self._client.pipeline_files.add_files_to_pipeline_api(
|
||||
pipeline_id=self.pipeline.id, request=[pipeline_file_create]
|
||||
)
|
||||
|
||||
@@ -946,7 +946,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
|
||||
# Add file to pipeline
|
||||
pipeline_file_create = PipelineFileCreate(file_id=file.id)
|
||||
await self._aclient.pipelines.add_files_to_pipeline_api(
|
||||
await self._aclient.pipeline_files.add_files_to_pipeline_api(
|
||||
pipeline_id=self.pipeline.id, request=[pipeline_file_create]
|
||||
)
|
||||
|
||||
@@ -984,7 +984,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
|
||||
# Add file to pipeline
|
||||
pipeline_file_create = PipelineFileCreate(file_id=file.id)
|
||||
self._client.pipelines.add_files_to_pipeline_api(
|
||||
self._client.pipeline_files.add_files_to_pipeline_api(
|
||||
pipeline_id=self.pipeline.id, request=[pipeline_file_create]
|
||||
)
|
||||
|
||||
@@ -1021,7 +1021,7 @@ class LlamaCloudIndex(BaseManagedIndex):
|
||||
|
||||
# Add file to pipeline
|
||||
pipeline_file_create = PipelineFileCreate(file_id=file.id)
|
||||
await self._aclient.pipelines.add_files_to_pipeline_api(
|
||||
await self._aclient.pipeline_files.add_files_to_pipeline_api(
|
||||
pipeline_id=self.pipeline.id, request=[pipeline_file_create]
|
||||
)
|
||||
|
||||
|
||||
@@ -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.",
|
||||
@@ -713,6 +717,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
|
||||
|
||||
@@ -1139,6 +1146,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,
|
||||
@@ -1180,7 +1206,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
|
||||
@@ -1243,6 +1269,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
|
||||
@@ -1768,7 +1803,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(
|
||||
@@ -1783,7 +1818,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.")
|
||||
@@ -164,7 +252,7 @@ class Page(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class JobResult(BaseModel):
|
||||
class JobResult(SafeBaseModel):
|
||||
"""The raw JSON result from the LlamaParse API."""
|
||||
|
||||
pages: List[Page] = Field(
|
||||
@@ -181,6 +269,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 +353,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 +386,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 +451,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]:
|
||||
|
||||
@@ -3,11 +3,14 @@ import importlib.metadata
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
import difflib
|
||||
from llama_cloud.types import StatusEnum
|
||||
from llama_cloud.types import StatusEnum, File
|
||||
import httpx
|
||||
import packaging.version
|
||||
from pydantic import BaseModel
|
||||
from typing import Any, Dict, List, Tuple, Type
|
||||
from typing import Any, Dict, List, Tuple, Type, Union, Optional
|
||||
from io import BufferedIOBase, TextIOWrapper
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
|
||||
# Asyncio error messages
|
||||
nest_asyncio_err = "cannot be called from a running event loop"
|
||||
@@ -104,3 +107,100 @@ def augment_async_errors() -> Generator[None, None, None]:
|
||||
if nest_asyncio_err in str(e):
|
||||
raise RuntimeError(nest_asyncio_msg)
|
||||
raise
|
||||
|
||||
|
||||
class SourceText:
|
||||
"""
|
||||
A wrapper class for providing text or file input with optional filename specification.
|
||||
|
||||
This class allows you to provide input in multiple ways:
|
||||
- Direct text content via text_content parameter
|
||||
- File paths as strings or Path objects
|
||||
- Raw bytes
|
||||
- File-like objects (BufferedIOBase, TextIOWrapper)
|
||||
- Already-uploaded file ID via file_id parameter
|
||||
|
||||
Args:
|
||||
file: The file input (bytes, file-like object, str path, or Path).
|
||||
Mutually exclusive with text_content and file_id.
|
||||
text_content: Raw text content to process. Mutually exclusive with file and file_id.
|
||||
file_id: ID of an already-uploaded file. Mutually exclusive with file and text_content.
|
||||
filename: Optional filename. Required for bytes/file-like objects without names.
|
||||
If not provided, will be auto-generated for text_content or inferred from paths.
|
||||
|
||||
Examples:
|
||||
# Direct text input
|
||||
source = SourceText(text_content="Hello world")
|
||||
|
||||
# File path
|
||||
source = SourceText(file="document.pdf")
|
||||
|
||||
# Bytes with filename
|
||||
source = SourceText(file=b"...", filename="document.pdf")
|
||||
|
||||
# File-like object (will read from current position)
|
||||
with open("document.pdf", "rb") as f:
|
||||
source = SourceText(file=f)
|
||||
|
||||
# Already-uploaded file
|
||||
source = SourceText(file_id="file_abc123")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
file: Union[bytes, BufferedIOBase, TextIOWrapper, str, Path, None] = None,
|
||||
text_content: Optional[str] = None,
|
||||
file_id: Optional[str] = None,
|
||||
filename: Optional[str] = None,
|
||||
):
|
||||
self.file = file
|
||||
self.filename = filename
|
||||
self.text_content = text_content
|
||||
self.file_id = file_id
|
||||
self._validate()
|
||||
|
||||
def _validate(self) -> None:
|
||||
"""Ensure filename is provided when needed."""
|
||||
# Check that exactly one of file, text_content, or file_id is provided
|
||||
provided = sum(
|
||||
[
|
||||
self.file is not None,
|
||||
self.text_content is not None,
|
||||
self.file_id is not None,
|
||||
]
|
||||
)
|
||||
|
||||
if provided == 0:
|
||||
raise ValueError("One of file, text_content, or file_id must be provided.")
|
||||
elif provided > 1:
|
||||
raise ValueError(
|
||||
"Only one of file, text_content, or file_id can be provided."
|
||||
)
|
||||
|
||||
# If file_id is provided, we don't need filename validation
|
||||
if self.file_id is not None:
|
||||
return
|
||||
|
||||
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 self.filename is None and not hasattr(self.file, "name"):
|
||||
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)}")
|
||||
|
||||
|
||||
# Type alias for file input that can be used across services
|
||||
FileInput = Union[str, Path, BufferedIOBase, SourceText, File]
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# llama_parse
|
||||
|
||||
## 0.6.78
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9f1ef4e: Fix extract
|
||||
- Updated dependencies [9f1ef4e]
|
||||
- llama-cloud-services-py@0.6.78
|
||||
|
||||
## 0.6.77
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [407292b]
|
||||
- llama-cloud-services-py@0.6.77
|
||||
|
||||
## 0.6.76
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [4f24f53]
|
||||
- llama-cloud-services-py@0.6.76
|
||||
|
||||
## 0.6.75
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [f81532e]
|
||||
- llama-cloud-services-py@0.6.75
|
||||
|
||||
## 0.6.74
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [1bf5223]
|
||||
- Updated dependencies [24166dc]
|
||||
- llama-cloud-services-py@0.6.74
|
||||
|
||||
## 0.6.73
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [e6a7939]
|
||||
- llama-cloud-services-py@0.6.73
|
||||
|
||||
## 0.6.72
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ad6734b]
|
||||
- llama-cloud-services-py@0.6.72
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "llama_parse",
|
||||
"version": "0.6.78",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"llama-cloud-services-py": "workspace:*"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.11.1",
|
||||
"devDependencies": {
|
||||
"changesets": "^1.0.2"
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,13 @@ dev = [
|
||||
|
||||
[project]
|
||||
name = "llama-parse"
|
||||
version = "0.6.69"
|
||||
version = "0.6.78"
|
||||
description = "Parse files into RAG-Optimized formats."
|
||||
authors = [{name = "Logan Markewich", email = "logan@llamaindex.ai"}]
|
||||
requires-python = ">=3.9,<4.0"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
dependencies = ["llama-cloud-services>=0.6.69"]
|
||||
dependencies = ["llama-cloud-services>=0.6.78"]
|
||||
|
||||
[project.scripts]
|
||||
llama-parse = "llama_parse.cli.main:parse"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "llama-cloud-services-py",
|
||||
"version": "0.6.78",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
"scripts": {},
|
||||
"devDependencies": {
|
||||
"changesets": "^1.0.2"
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ dev = [
|
||||
|
||||
[project]
|
||||
name = "llama-cloud-services"
|
||||
version = "0.6.69"
|
||||
version = "0.6.78"
|
||||
description = "Tailored SDK clients for LlamaCloud services."
|
||||
authors = [{name = "Logan Markewich", email = "logan@runllama.ai"}]
|
||||
requires-python = ">=3.9,<4.0"
|
||||
@@ -27,14 +27,14 @@ readme = "README.md"
|
||||
license = "MIT"
|
||||
dependencies = [
|
||||
"llama-index-core>=0.12.0",
|
||||
"llama-cloud==0.1.42",
|
||||
"llama-cloud==0.1.44",
|
||||
"pydantic>=2.8,!=2.10",
|
||||
"click>=8.1.7,<9",
|
||||
"python-dotenv>=1.0.1,<2",
|
||||
"eval-type-backport>=0.2.0,<0.3 ; python_version < '3.10'",
|
||||
"platformdirs>=4.3.7,<5",
|
||||
"tenacity>=8.5.0, <10.0",
|
||||
"packaging>=25.0"
|
||||
"packaging>=23.0"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -44,7 +44,6 @@ def classify_client(
|
||||
return ClassifyClient(
|
||||
async_llama_cloud_client,
|
||||
project_id=project.id,
|
||||
organization_id=project.organization_id,
|
||||
polling_interval=1,
|
||||
)
|
||||
|
||||
@@ -56,7 +55,6 @@ def file_client(
|
||||
return FileClient(
|
||||
async_llama_cloud_client,
|
||||
project_id=project.id,
|
||||
organization_id=project.organization_id,
|
||||
use_presigned_url=False,
|
||||
)
|
||||
|
||||
@@ -148,7 +146,6 @@ async def test_classify_file_ids_from_api_key(
|
||||
api_key=e2e_test_settings.LLAMA_CLOUD_API_KEY.get_secret_value(),
|
||||
base_url=e2e_test_settings.LLAMA_CLOUD_BASE_URL,
|
||||
project_id=pdf_file.project_id,
|
||||
organization_id=e2e_test_settings.LLAMA_CLOUD_ORGANIZATION_ID,
|
||||
)
|
||||
|
||||
# Classify the uploaded files
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent
|
||||
from time import perf_counter
|
||||
from llama_cloud_services.extract import LlamaExtract
|
||||
from collections import namedtuple
|
||||
import json
|
||||
import uuid
|
||||
from llama_cloud.types import (
|
||||
ExtractConfig,
|
||||
ExtractMode,
|
||||
LlamaParseParameters,
|
||||
LlamaExtractSettings,
|
||||
)
|
||||
from tests.extract.util import load_test_dotenv
|
||||
|
||||
@@ -122,27 +119,3 @@ def extraction_agent(test_case: BenchmarkTestCase, extractor: LlamaExtract):
|
||||
# Create new agent
|
||||
agent = extractor.create_agent(agent_name, schema, config=test_case.config)
|
||||
yield agent
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"CI" in os.environ or not LLAMA_CLOUD_API_KEY,
|
||||
reason="LLAMA_CLOUD_API_KEY not set or CI environment not suitable for benchmarking",
|
||||
)
|
||||
@pytest.mark.parametrize("test_case", get_test_cases(), ids=lambda x: x.name)
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_extraction(
|
||||
test_case: BenchmarkTestCase, extraction_agent: ExtractionAgent
|
||||
) -> None:
|
||||
start = perf_counter()
|
||||
result = await extraction_agent._run_extraction_test(
|
||||
test_case.input_file,
|
||||
extract_settings=LlamaExtractSettings(
|
||||
llama_parse_params=LlamaParseParameters(
|
||||
invalidate_cache=True,
|
||||
do_not_cache=True,
|
||||
)
|
||||
),
|
||||
)
|
||||
end = perf_counter()
|
||||
print(f"Time taken: {end - start} seconds")
|
||||
print(result)
|
||||
|
||||
@@ -58,6 +58,8 @@ def get_test_cases():
|
||||
settings = [
|
||||
ExtractConfig(extraction_mode=ExtractMode.FAST),
|
||||
ExtractConfig(extraction_mode=ExtractMode.BALANCED),
|
||||
ExtractConfig(extraction_mode=ExtractMode.MULTIMODAL),
|
||||
ExtractConfig(extraction_mode=ExtractMode.PREMIUM),
|
||||
]
|
||||
|
||||
for input_file in sorted(input_files):
|
||||
|
||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
def load_test_dotenv():
|
||||
load_dotenv(Path(__file__).parent.parent.parent / ".env.dev", override=True)
|
||||
load_dotenv(Path(__file__).parent.parent.parent.parent / ".env.dev", override=True)
|
||||
|
||||
|
||||
def json_subset_match_score(expected: Any, actual: Any) -> float:
|
||||
|
||||
@@ -44,7 +44,7 @@ def index_name() -> Generator[str, None, None]:
|
||||
client = LlamaCloud(token=api_key, base_url=base_url)
|
||||
pipeline = client.pipelines.search_pipelines(project_name=name)
|
||||
if pipeline:
|
||||
client.pipelines.delete(pipeline_id=pipeline[0].id)
|
||||
client.pipelines.delete_pipeline(pipeline_id=pipeline[0].id)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -83,7 +83,7 @@ def _setup_index_with_file(
|
||||
|
||||
# add file to pipeline
|
||||
pipeline_file_create = PipelineFileCreate(file_id=file.id)
|
||||
client.pipelines.add_files_to_pipeline_api(
|
||||
client.pipeline_files.add_files_to_pipeline_api(
|
||||
pipeline_id=pipeline.id, request=[pipeline_file_create]
|
||||
)
|
||||
|
||||
@@ -304,6 +304,9 @@ async def test_page_screenshot_retrieval(index_name: str, local_file: str):
|
||||
not base_url or not api_key, reason="No platform base url or api key set"
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skip(
|
||||
reason="Consistently failing with FAILED tests/index/test_index.py::test_page_figure_retrieval - assert 0 > 0 + where 0 = len([])"
|
||||
)
|
||||
async def test_page_figure_retrieval(index_name: str, local_figures_file: str):
|
||||
index = await LlamaCloudIndex.acreate_index(
|
||||
name=index_name,
|
||||
|
||||
@@ -6,6 +6,40 @@ from llama_cloud_services import LlamaParse
|
||||
from llama_cloud_services.parse.types import JobResult
|
||||
|
||||
|
||||
def test_format_parse_result_markdown_for_notebook():
|
||||
"""Test the _format_markdown_for_notebook function.
|
||||
Right now, the only work it does is escape single dollar signs."""
|
||||
result = JobResult(job_id="test", file_name="test.pdf", job_result={})
|
||||
|
||||
# Test None input
|
||||
assert result._format_markdown_for_notebook(None) is None
|
||||
|
||||
# Test single dollar sign gets escaped
|
||||
assert result._format_markdown_for_notebook("This costs $5") == "This costs \\$5"
|
||||
|
||||
# Test double dollar signs are preserved (LaTeX equations)
|
||||
assert (
|
||||
result._format_markdown_for_notebook("$$x^2 + y^2 = z^2$$")
|
||||
== "$$x^2 + y^2 = z^2$$"
|
||||
)
|
||||
|
||||
# Test mixed single and double dollar signs
|
||||
text = "This costs $5, but $$E = mc^2$$ is priceless"
|
||||
expected = "This costs \\$5, but $$E = mc^2$$ is priceless"
|
||||
assert result._format_markdown_for_notebook(text) == expected
|
||||
|
||||
# Test multiple single dollar signs
|
||||
assert result._format_markdown_for_notebook("$10 and $20") == "\\$10 and \\$20"
|
||||
|
||||
# Test three or more consecutive dollar signs (preserve them)
|
||||
assert result._format_markdown_for_notebook("$$$") == "$$$"
|
||||
|
||||
# Test adjacent dollar signs with text in between
|
||||
text = "$$inline$$ and $separate"
|
||||
expected = "$$inline$$ and \\$separate"
|
||||
assert result._format_markdown_for_notebook(text) == expected
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def file_path() -> str:
|
||||
return "tests/test_files/attention_is_all_you_need.pdf"
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from llama_cloud import ExtractRun, File
|
||||
@@ -434,6 +435,7 @@ def create_extract_run(
|
||||
"extraction_agent_id": "extraction-agent-123",
|
||||
"config": {},
|
||||
"status": "SUCCESS",
|
||||
"project_id": str(uuid.uuid4()),
|
||||
"from_ui": False,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -112,5 +112,6 @@
|
||||
"num_output_tokens": 3440
|
||||
}
|
||||
},
|
||||
"project_id": "77bdc79f-fb69-49ae-a783-fcc573eec7ce",
|
||||
"from_ui": false
|
||||
}
|
||||
|
||||
@@ -118,10 +118,8 @@ async def test_extraction_agent_aextract_accepts_llama_file(
|
||||
dummy_llama_extract_iface = SimpleNamespace()
|
||||
|
||||
async def fake_run_job(**kwargs):
|
||||
# Ensure we are receiving a request with the right file_id
|
||||
request = kwargs.get("request")
|
||||
assert hasattr(request, "file_id")
|
||||
assert request.file_id == llama_file.id
|
||||
file_id = kwargs.get("file_id")
|
||||
assert file_id == llama_file.id
|
||||
return SimpleNamespace(id="job_42")
|
||||
|
||||
dummy_llama_extract_iface.run_job = fake_run_job
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.9, <4.0"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
@@ -1582,21 +1582,21 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "llama-cloud"
|
||||
version = "0.1.42"
|
||||
version = "0.1.44"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/04/ae0694b582d6aab4d6e7957febb7bff048897ac231ad80ba1bd71547d944/llama_cloud-0.1.42.tar.gz", hash = "sha256:485aa0e364ea648e3aaa3b2c54af7bcb6f2242c50b4f86ec022e137413fff464", size = 112480, upload-time = "2025-09-16T20:25:42.631Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/eb/16e31fb0fc4df91b08fa19cc3f28ac6e3c7d4df0bcbb71dd2bf596e9586f/llama_cloud-0.1.44.tar.gz", hash = "sha256:276a2b4f94463da037431ca3063331b3b6be398bbfb003113ee76b7c2a873b53", size = 120502, upload-time = "2025-11-04T00:51:58.578Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/61/85d115699a59d03f0783e119aaf6d534fca95dbe1a4531a8056e6a4774ed/llama_cloud-0.1.42-py3-none-any.whl", hash = "sha256:4ed3edde4a277ff52eeb831188c8476eb079b5e4605ad3142157a0f054b27d96", size = 311857, upload-time = "2025-09-16T20:25:41.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/0a/fabe54c21d5927d626550cb9560a20e51e42468355f5f0fb300f84806e28/llama_cloud-0.1.44-py3-none-any.whl", hash = "sha256:dfdcc4932353711fc8639f14261cbb54a88139b7790ebdd3ed4fde29bbbc0b88", size = 332779, upload-time = "2025-11-04T00:51:57.371Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "llama-cloud-services"
|
||||
version = "0.6.68"
|
||||
version = "0.6.77"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
@@ -1631,9 +1631,9 @@ dev = [
|
||||
requires-dist = [
|
||||
{ name = "click", specifier = ">=8.1.7,<9" },
|
||||
{ name = "eval-type-backport", marker = "python_full_version < '3.10'", specifier = ">=0.2.0,<0.3" },
|
||||
{ name = "llama-cloud", specifier = "==0.1.42" },
|
||||
{ name = "llama-cloud", specifier = "==0.1.44" },
|
||||
{ name = "llama-index-core", specifier = ">=0.12.0" },
|
||||
{ name = "packaging", specifier = ">=25.0" },
|
||||
{ name = "packaging", specifier = ">=23.0" },
|
||||
{ name = "platformdirs", specifier = ">=4.3.7,<5" },
|
||||
{ name = "pydantic", specifier = ">=2.8,!=2.10" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.1,<2" },
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["click", "tomlkit", "packaging"]
|
||||
# ///
|
||||
|
||||
"""
|
||||
This is a script called by the changeset bot. Normally changeset can do the following things, but this is a mixed ts and python repo, so we need to do some extra things.
|
||||
|
||||
There's 2 things this does:
|
||||
- Versioning: Makes changes that may be committed with the newest version.
|
||||
- Releasing/Tagging: After versions are changed, we check each package to see if its released, and if not, we release it and tag it.
|
||||
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, List, cast
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import re
|
||||
|
||||
import click
|
||||
import tomlkit
|
||||
from packaging.version import Version
|
||||
|
||||
|
||||
def _run_command(
|
||||
cmd: List[str], cwd: Path | None = None, env: dict[str, str] | None = None
|
||||
) -> None:
|
||||
"""Run a command, streaming output to the console, and raise on failure."""
|
||||
subprocess.run(cmd, check=True, text=True, cwd=cwd or Path.cwd(), env=env)
|
||||
|
||||
|
||||
def _run_and_capture(
|
||||
cmd: List[str], cwd: Path | None = None, env: dict[str, str] | None = None
|
||||
) -> str:
|
||||
"""Run a command and return stdout as text, raising on failure."""
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
check=True,
|
||||
text=True,
|
||||
cwd=cwd or Path.cwd(),
|
||||
env=env,
|
||||
capture_output=True,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
@dataclass
|
||||
class Package:
|
||||
name: str
|
||||
version: str
|
||||
path: Path
|
||||
|
||||
def python_package_name(self) -> str | None:
|
||||
if "/py/" in str(self.path) or str(self.path).endswith("/py"):
|
||||
return self.name.removesuffix("-py")
|
||||
return None
|
||||
|
||||
|
||||
def _get_pnpm_workspace_packages() -> list[Package]:
|
||||
"""Return directories for all workspace packages from pnpm list JSON output."""
|
||||
output = _run_and_capture(["pnpm", "list", "-r", "--depth=-1", "--json"])
|
||||
|
||||
data = cast(list[dict[str, Any]], json.loads(output))
|
||||
packages: list[Package] = [
|
||||
Package(name=data["name"], version=data["version"], path=Path(data["path"]))
|
||||
for data in data
|
||||
]
|
||||
return packages
|
||||
|
||||
|
||||
def _sync_package_version_with_pyproject(
|
||||
package_dir: Path, packages: dict[str, Package], js_package_name: str
|
||||
) -> None:
|
||||
"""Sync version from package.json to pyproject.toml.
|
||||
|
||||
Returns True if pyproject was changed, else False.
|
||||
"""
|
||||
pyproject_path = package_dir / "pyproject.toml"
|
||||
if not pyproject_path.exists():
|
||||
return
|
||||
|
||||
package_version = packages[js_package_name].version
|
||||
py_doc = tomlkit.parse(pyproject_path.read_text())
|
||||
|
||||
by_python_name = {
|
||||
pkg.python_package_name(): pkg
|
||||
for pkg in packages.values()
|
||||
if pkg.python_package_name()
|
||||
}
|
||||
|
||||
current_version = py_doc["project"]["version"]
|
||||
assert isinstance(current_version, str)
|
||||
|
||||
# update workspace dependency strings by replacing the first version after == or >=
|
||||
deps = py_doc["project"]["dependencies"] or []
|
||||
changed = False
|
||||
for i, dep in enumerate(deps):
|
||||
if not isinstance(dep, str):
|
||||
continue
|
||||
pkg = (cast(str, dep).split("==")[0]).split(">=")[0]
|
||||
if pkg not in by_python_name:
|
||||
continue
|
||||
target_version = by_python_name[pkg].version
|
||||
new_dep = re.sub(
|
||||
r"(==|>=)\s*([0-9A-Za-z_.+-]+)",
|
||||
lambda m: m.group(1) + target_version,
|
||||
dep,
|
||||
count=1,
|
||||
)
|
||||
if new_dep != dep:
|
||||
deps[i] = new_dep
|
||||
changed = True
|
||||
|
||||
if current_version != package_version:
|
||||
py_doc["project"]["version"] = package_version
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
pyproject_path.write_text(tomlkit.dumps(py_doc))
|
||||
click.echo(
|
||||
f"Updated {pyproject_path} version to {package_version} and synced dependency specs"
|
||||
)
|
||||
|
||||
|
||||
def lock_python_dependencies() -> None:
|
||||
"""Lock Python dependencies."""
|
||||
try:
|
||||
_run_command(["uv", "lock"])
|
||||
click.echo("Locked Python dependencies")
|
||||
except subprocess.CalledProcessError as e:
|
||||
click.echo(f"Warning: Failed to lock Python dependencies: {e}", err=True)
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli() -> None:
|
||||
"""Changeset-based version management for llama-cloud-services."""
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
def version() -> None:
|
||||
"""Apply changeset versions, then sync versions for co-located JS/Py packages.
|
||||
|
||||
- Runs changesets to bump package.json versions.
|
||||
- Discovers all workspace packages via pnpm.
|
||||
- For any directory containing both package.json and pyproject.toml, and with
|
||||
package.json private: false, set pyproject [project].version to match the JS version.
|
||||
- If a pyproject is updated, run `uv sync` in that directory to update its lock file.
|
||||
"""
|
||||
# Ensure we're at the repo root
|
||||
os.chdir(Path(__file__).parent.parent)
|
||||
|
||||
# First, run changeset version to update all package.json files
|
||||
_run_command(["npx", "@changesets/cli", "version"])
|
||||
|
||||
# Enumerate workspace packages and perform syncs
|
||||
packages = _get_pnpm_workspace_packages()
|
||||
version_map = {pkg.name: pkg for pkg in packages}
|
||||
for pkg in packages:
|
||||
_sync_package_version_with_pyproject(pkg.path, version_map, pkg.name)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--tag", is_flag=True, help="Tag the packages after publishing")
|
||||
@click.option("--dry-run", is_flag=True, help="Dry run the publish")
|
||||
@click.option("--js/--no-js", default=True, help="Publish the js package")
|
||||
@click.option("--py/--no-py", default=True, help="Publish the py package")
|
||||
def publish(tag: bool, dry_run: bool, js: bool, py: bool) -> None:
|
||||
"""Publish all packages."""
|
||||
# move to the root
|
||||
os.chdir(Path(__file__).parent.parent)
|
||||
|
||||
if js:
|
||||
if not os.getenv("NPM_TOKEN"):
|
||||
click.echo("NPM_TOKEN is not set, skipping publish", err=True)
|
||||
raise click.Abort("No token set")
|
||||
if py:
|
||||
if not os.getenv("LLAMA_PARSE_PYPI_TOKEN"):
|
||||
click.echo("LLAMA_PARSE_PYPI_TOKEN is not set, skipping publish", err=True)
|
||||
raise click.Abort("No token set")
|
||||
|
||||
# not general script. Just checks each of the 2 packages to see if they need to be published.
|
||||
if js:
|
||||
maybe_publish_npm(dry_run)
|
||||
if py:
|
||||
maybe_publish_pypi(dry_run)
|
||||
|
||||
if tag:
|
||||
if dry_run:
|
||||
click.echo("Dry run, skipping tag. Would run:")
|
||||
click.echo(" npx @changesets/cli tag")
|
||||
click.echo(" git push --tags")
|
||||
else:
|
||||
# Let changesets create JS-related tags as usual
|
||||
_run_command(["npx", "@changesets/cli", "tag"])
|
||||
_run_command(["git", "push", "--tags"])
|
||||
|
||||
|
||||
def maybe_publish_npm(dry_run: bool) -> None:
|
||||
"""Publish the ts package if it needs to be published."""
|
||||
target_dir = Path("ts/llama_cloud_services")
|
||||
ts_path_package = target_dir / "package.json"
|
||||
package_json = json.loads(ts_path_package.read_text())
|
||||
version = package_json["version"]
|
||||
|
||||
# Check if this version is already published on npm
|
||||
result = subprocess.run(
|
||||
["npm", "view", "llama-cloud-services", "versions", "--json"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=target_dir,
|
||||
)
|
||||
|
||||
published_versions = json.loads(result.stdout)
|
||||
if version in published_versions:
|
||||
click.echo(
|
||||
f"npm package llama-cloud-services@{version} already published, skipping"
|
||||
)
|
||||
return
|
||||
click.echo(f"Publishing npm package llama-cloud-services@{version}")
|
||||
# defer to the package.json publish script
|
||||
if dry_run:
|
||||
click.echo("Dry run, skipping publish. Would run:")
|
||||
click.echo(" pnpm run publish")
|
||||
return
|
||||
else:
|
||||
_run_command(["pnpm", "run", "build"], cwd=target_dir)
|
||||
_run_command(["pnpm", "publish"], cwd=target_dir)
|
||||
|
||||
|
||||
def maybe_publish_pypi(dry_run: bool) -> None:
|
||||
"""Publish the py packages if they need to be published."""
|
||||
for pyproject in list(Path("py").glob("*/pyproject.toml")) + [
|
||||
Path("py/pyproject.toml")
|
||||
]:
|
||||
name, version = current_version(pyproject)
|
||||
if is_published(name, version):
|
||||
click.echo(f"PyPI package {name}@{version} already published, skipping")
|
||||
continue
|
||||
click.echo(f"Publishing PyPI package {name}@{version}")
|
||||
|
||||
# Use different tokens for different packages
|
||||
env = os.environ.copy()
|
||||
token = os.environ["LLAMA_PARSE_PYPI_TOKEN"]
|
||||
env["UV_PUBLISH_TOKEN"] = token
|
||||
if dry_run:
|
||||
summary = (token[:3] + "***") if len(token) <= 6 else token[:6] + "****"
|
||||
click.echo(
|
||||
f"Dry run, skipping publish. Would run with publish token {summary}:"
|
||||
)
|
||||
click.echo(" uv build")
|
||||
click.echo(" uv publish")
|
||||
else:
|
||||
_run_command(["uv", "build"], cwd=pyproject.parent)
|
||||
_run_command(["uv", "publish"], cwd=pyproject.parent, env=env)
|
||||
|
||||
|
||||
def current_version(pyproject: Path) -> tuple[str, str]:
|
||||
"""Return (package_name, version_str) taken from the given pyproject.toml."""
|
||||
doc = tomlkit.parse(pyproject.read_text())
|
||||
name = doc["project"]["name"]
|
||||
version = str(Version(doc["project"]["version"])) # normalise
|
||||
return name, version
|
||||
|
||||
|
||||
def is_published(
|
||||
name: str, version: str, index_url: str = "https://pypi.org/pypi"
|
||||
) -> bool:
|
||||
"""
|
||||
True → `<name>==<version>` exists on the given index
|
||||
False → package missing *or* version missing
|
||||
"""
|
||||
url = f"{index_url.rstrip('/')}/{name}/json"
|
||||
try:
|
||||
data = json.load(urllib.request.urlopen(url))
|
||||
except urllib.error.HTTPError as e: # 404 → package not published at all
|
||||
if e.code == 404:
|
||||
return False
|
||||
raise # any other error should surface
|
||||
return version in data["releases"] # keys are version strings
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -1,226 +0,0 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = ["click", "tomlkit"]
|
||||
# ///
|
||||
|
||||
import click
|
||||
import subprocess
|
||||
import sys
|
||||
import tomlkit
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
|
||||
def get_current_versions() -> tuple[str, str, str, str | None]:
|
||||
"""Get current versions from both pyproject.toml files and TS package.json."""
|
||||
# Read main pyproject.toml
|
||||
main_content = Path("py/pyproject.toml").read_text()
|
||||
main_doc = tomlkit.parse(main_content)
|
||||
main_version = main_doc["project"]["version"]
|
||||
|
||||
# Read llama_parse/pyproject.toml
|
||||
llama_parse_content = Path("py/llama_parse/pyproject.toml").read_text()
|
||||
llama_parse_doc = tomlkit.parse(llama_parse_content)
|
||||
llama_parse_version = llama_parse_doc["project"]["version"]
|
||||
# Find llama-cloud-services dependency in the dependencies list
|
||||
dependency_version = None
|
||||
for dep in llama_parse_doc["project"]["dependencies"]:
|
||||
if isinstance(dep, str) and dep.startswith("llama-cloud-services"):
|
||||
dependency_version = (
|
||||
dep.split("==")[1]
|
||||
if "==" in dep
|
||||
else dep.split(">=")[1]
|
||||
if ">=" in dep
|
||||
else None
|
||||
)
|
||||
break
|
||||
|
||||
# Read TypeScript package.json version via helper
|
||||
ts_version: str = get_ts_version()
|
||||
|
||||
return (
|
||||
str(main_version),
|
||||
str(llama_parse_version),
|
||||
str(dependency_version),
|
||||
str(ts_version) if ts_version is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def validate_versions(
|
||||
main_version: str,
|
||||
llama_parse_version: str,
|
||||
dependency_version: str,
|
||||
) -> list[str]:
|
||||
"""Validate that versions are consistent and return warnings."""
|
||||
warnings = []
|
||||
|
||||
if main_version != llama_parse_version:
|
||||
warnings.append(
|
||||
f"Version mismatch: main={main_version}, llama_parse={llama_parse_version}"
|
||||
)
|
||||
|
||||
# Extract version from dependency string (e.g., ">=0.6.51" -> "0.6.51")
|
||||
if dependency_version and dependency_version.startswith(">="):
|
||||
dep_ver = dependency_version[2:]
|
||||
if dep_ver != main_version:
|
||||
warnings.append(
|
||||
f"Dependency version mismatch: dependency={dep_ver}, main={main_version}"
|
||||
)
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
def set_version(version: str) -> None:
|
||||
"""Set version across Python projects (no TS change)."""
|
||||
# Update main pyproject.toml
|
||||
main_content = Path("py/pyproject.toml").read_text()
|
||||
main_doc = tomlkit.parse(main_content)
|
||||
main_doc["project"]["version"] = version
|
||||
Path("py/pyproject.toml").write_text(tomlkit.dumps(main_doc))
|
||||
|
||||
# Update llama_parse/pyproject.toml
|
||||
llama_parse_content = Path("py/llama_parse/pyproject.toml").read_text()
|
||||
llama_parse_doc = tomlkit.parse(llama_parse_content)
|
||||
llama_parse_doc["project"]["version"] = version
|
||||
for dep_index, dep in enumerate(llama_parse_doc["project"]["dependencies"]):
|
||||
if isinstance(dep, str) and dep.startswith("llama-cloud-services"):
|
||||
llama_parse_doc["project"]["dependencies"][
|
||||
dep_index
|
||||
] = f"llama-cloud-services>={version}"
|
||||
break
|
||||
Path("py/llama_parse/pyproject.toml").write_text(tomlkit.dumps(llama_parse_doc))
|
||||
|
||||
click.echo(f"Updated Python versions to {version}")
|
||||
|
||||
|
||||
def get_ts_version() -> str:
|
||||
"""Read TypeScript package.json version (if present)."""
|
||||
ts_package_path = Path("ts/llama_cloud_services/package.json")
|
||||
package_data = json.loads(ts_package_path.read_text())
|
||||
data = package_data.get("version")
|
||||
if data is None:
|
||||
raise RuntimeError("TypeScript package.json version not found")
|
||||
return data
|
||||
|
||||
|
||||
def set_ts_version(version: str) -> None:
|
||||
"""Set TypeScript package.json version only."""
|
||||
ts_package_path = Path("ts/llama_cloud_services/package.json")
|
||||
package_data = json.loads(ts_package_path.read_text())
|
||||
package_data["version"] = version
|
||||
ts_package_path.write_text(json.dumps(package_data, indent=2) + "\n")
|
||||
click.echo(f"Updated TypeScript package.json version to {version}")
|
||||
|
||||
|
||||
def get_current_branch() -> str:
|
||||
"""Get the current git branch."""
|
||||
result = subprocess.run(
|
||||
["git", "branch", "--show-current"], capture_output=True, text=True, check=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def create_if_not_exists(version: str) -> str:
|
||||
"""Create a git tag and push it."""
|
||||
current_branch = get_current_branch()
|
||||
if current_branch != "main":
|
||||
click.echo(
|
||||
f"Error: Not on main branch (currently on {current_branch})", err=True
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
tag_name = f"v{version}" if version[0].isdigit() else version
|
||||
if not tag_exists(tag_name):
|
||||
# Create tag
|
||||
subprocess.run(["git", "tag", tag_name], check=True)
|
||||
click.echo(f"Created tag {tag_name}")
|
||||
else:
|
||||
click.echo(f"Tag {tag_name} already exists")
|
||||
return tag_name
|
||||
|
||||
|
||||
def tag_exists(tag_name: str) -> bool:
|
||||
"""Check if a git tag exists."""
|
||||
result = subprocess.run(
|
||||
["git", "tag", "-l", tag_name], capture_output=True, text=True, check=True
|
||||
)
|
||||
return tag_name in result.stdout.strip()
|
||||
|
||||
|
||||
def push_tag(tag_name: str) -> None:
|
||||
"""Push a git tag."""
|
||||
subprocess.run(["git", "push", "origin", tag_name], check=True)
|
||||
click.echo(f"Pushed tag {tag_name}")
|
||||
|
||||
|
||||
@click.group()
|
||||
def cli() -> None:
|
||||
"""Version management for llama-cloud-services."""
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
def get() -> None:
|
||||
"""Get current versions and show validation warnings."""
|
||||
(
|
||||
main_version,
|
||||
llama_parse_version,
|
||||
dependency_version,
|
||||
ts_version,
|
||||
) = get_current_versions()
|
||||
|
||||
click.echo("Current versions:")
|
||||
click.echo(f" llama-cloud-services: {main_version}")
|
||||
click.echo(f" llama-parse: {llama_parse_version}")
|
||||
click.echo(f" dependency reference: {dependency_version}")
|
||||
click.echo(f" typescript package: {ts_version}")
|
||||
|
||||
warnings = validate_versions(main_version, llama_parse_version, dependency_version)
|
||||
if warnings:
|
||||
click.echo("\nValidation warnings:")
|
||||
for warning in warnings:
|
||||
click.echo(f" ⚠️ {warning}")
|
||||
else:
|
||||
click.echo("\n✅ All versions are consistent")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("version")
|
||||
@click.option("--js", is_flag=True, help="Update TypeScript package.json only")
|
||||
def set(version: str, js: bool) -> None:
|
||||
"""Set version for Python, TypeScript, or both (default: Python only)."""
|
||||
|
||||
if js:
|
||||
set_ts_version(version)
|
||||
return
|
||||
else:
|
||||
set_version(version)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option(
|
||||
"--version", help="Version to tag (uses current version if not specified)"
|
||||
)
|
||||
@click.option(
|
||||
"--push",
|
||||
is_flag=True,
|
||||
help="Push the tag to the remote repository",
|
||||
)
|
||||
@click.option(
|
||||
"--js",
|
||||
is_flag=True,
|
||||
help="tag TypeScript package.json only",
|
||||
)
|
||||
def tag(version: str | None = None, push: bool = False, js: bool = False) -> None:
|
||||
"""Create and push a git tag for the current version."""
|
||||
if not version:
|
||||
main_version, _, _, js_version = get_current_versions()
|
||||
version = f"llama-cloud-services@{js_version}" if js else main_version
|
||||
|
||||
tag_name = create_if_not_exists(version)
|
||||
if push:
|
||||
push_tag(tag_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -9,10 +9,12 @@ test("LlamaIndex module resolution test", async (t) => {
|
||||
const index = new LlamaCloudIndex({
|
||||
name: "test-index",
|
||||
projectName: "Default",
|
||||
apiKey: process.env.LLAMA_CLOUD_API_KEY || "test-key",
|
||||
});
|
||||
const reader = new LlamaParseReader({
|
||||
resultType: "markdown",
|
||||
verbose: false,
|
||||
apiKey: process.env.LLAMA_CLOUD_API_KEY || "test-key",
|
||||
});
|
||||
ok(index !== undefined);
|
||||
ok(reader !== undefined);
|
||||
@@ -24,6 +26,7 @@ test("LlamaIndex module resolution test", async (t) => {
|
||||
const index = new mod.LlamaCloudIndex({
|
||||
name: "test-index",
|
||||
projectName: "Default",
|
||||
apiKey: process.env.LLAMA_CLOUD_API_KEY || "test-key",
|
||||
});
|
||||
ok(index !== undefined);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# llama-cloud-services
|
||||
|
||||
## 0.3.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fee516d: Adding LlamaClassify among the available LlamaCloud services
|
||||
|
||||
## 0.3.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5d4cabd: Add ImageNode support in TypeScript
|
||||
|
||||
## 0.3.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6e0f2f4: Agent data extraction citations can be undefined
|
||||
|
||||
## 0.3.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d028397: Update llama-cloud api version, and integrate with agent data deletion
|
||||
|
||||
## v0.1.0
|
||||
|
||||
First release for `llama-cloud-services`.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": "./dist/index.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"name": "llama-cloud-services",
|
||||
"version": "0.3.6",
|
||||
"version": "0.3.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"get-openapi": "node ./scripts/get-openapi.js",
|
||||
"generate": "./node_modules/.bin/openapi-ts",
|
||||
"build": "pnpm run generate && bunchee",
|
||||
"dev": "bunchee --watch",
|
||||
"lint": "eslint src/ --ignore-pattern client/*.ts --no-warn-ignored",
|
||||
"format": "prettier --write ./src/",
|
||||
"format:check": "prettier --check ./src/",
|
||||
"format": "prettier --write ./src/ tests/",
|
||||
"format:check": "prettier --check ./src/ tests/",
|
||||
"test": "vitest run --testTimeout=60000",
|
||||
"test:watch": "vitest --watch",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage"
|
||||
"test:coverage": "vitest --coverage",
|
||||
"release": "pnpm run build && pnpm publish"
|
||||
},
|
||||
"files": [
|
||||
"openapi.json",
|
||||
@@ -22,7 +24,8 @@
|
||||
"./reader",
|
||||
"./parse",
|
||||
"./beta/agent",
|
||||
"./extract"
|
||||
"./extract",
|
||||
"./classify"
|
||||
],
|
||||
"exports": {
|
||||
"./openapi.json": "./openapi.json",
|
||||
@@ -81,6 +84,17 @@
|
||||
},
|
||||
"default": "./extract/dist/index.js"
|
||||
},
|
||||
"./classify": {
|
||||
"require": {
|
||||
"types": "./classify/dist/index.d.cts",
|
||||
"default": "./classify/dist/index.cjs"
|
||||
},
|
||||
"import": {
|
||||
"types": "./classify/dist/index.d.ts",
|
||||
"default": "./classify/dist/index.js"
|
||||
},
|
||||
"default": "./classify/dist/index.js"
|
||||
},
|
||||
".": {
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import fs from 'fs';
|
||||
|
||||
async function downloadOpenApiSpec() {
|
||||
try {
|
||||
const response = await fetch('https://api.cloud.llamaindex.ai/api/openapi.json');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
fs.writeFileSync('openapi.json', JSON.stringify(data, null, 2));
|
||||
console.log('Successfully downloaded openapi.json');
|
||||
} catch (error) {
|
||||
console.error('Error downloading OpenAPI spec:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
downloadOpenApiSpec();
|
||||
@@ -0,0 +1,75 @@
|
||||
import { createClient, createConfig, type Client } from "@hey-api/client-fetch";
|
||||
import {
|
||||
classify,
|
||||
type ClassifyParsingConfiguration,
|
||||
type ClassifierRule,
|
||||
type ClassifyJobResults,
|
||||
} from "./classify";
|
||||
import { getUrl } from "./utils";
|
||||
import { getEnv } from "@llamaindex/env";
|
||||
import { File } from "buffer";
|
||||
|
||||
export class LlamaClassify {
|
||||
private client: Client;
|
||||
|
||||
constructor(
|
||||
apiKey: string | undefined = undefined,
|
||||
baseUrl: string | undefined = undefined,
|
||||
region: string | undefined = undefined,
|
||||
) {
|
||||
const key = apiKey ?? getEnv("LLAMA_CLOUD_API_KEY");
|
||||
if (typeof key === "undefined") {
|
||||
throw new Error(
|
||||
"No API key provided and no API key found in environment. Please pass the API key or set `LLAMA_CLOUD_API_KEY` as an environment variable.",
|
||||
);
|
||||
}
|
||||
const url = getUrl(baseUrl, region);
|
||||
this.client = createClient(
|
||||
createConfig({
|
||||
baseUrl: url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async classify(
|
||||
rules: ClassifierRule[],
|
||||
configuration: ClassifyParsingConfiguration,
|
||||
{
|
||||
fileContents,
|
||||
filePaths,
|
||||
projectId,
|
||||
pollingInterval = 1,
|
||||
maxPollingIterations = 1800,
|
||||
maxRetriesOnError = 10,
|
||||
retryInterval = 0.5,
|
||||
}: {
|
||||
fileContents?:
|
||||
| Buffer<ArrayBufferLike>[]
|
||||
| File[]
|
||||
| Uint8Array<ArrayBuffer>[]
|
||||
| string[]
|
||||
| undefined;
|
||||
filePaths?: string[] | undefined;
|
||||
projectId?: string;
|
||||
pollingInterval?: number;
|
||||
maxPollingIterations?: number;
|
||||
maxRetriesOnError?: number;
|
||||
retryInterval?: number;
|
||||
},
|
||||
): Promise<ClassifyJobResults> {
|
||||
const result = await classify(rules, configuration, {
|
||||
fileContents,
|
||||
filePaths,
|
||||
projectId: projectId ?? undefined,
|
||||
client: this.client,
|
||||
pollingInterval,
|
||||
maxPollingIterations,
|
||||
maxRetriesOnError,
|
||||
retryInterval,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,16 @@ import { DEFAULT_PROJECT_NAME } from "@llamaindex/core/global";
|
||||
import type { QueryBundle } from "@llamaindex/core/query-engine";
|
||||
import { BaseRetriever } from "@llamaindex/core/retriever";
|
||||
import type { NodeWithScore } from "@llamaindex/core/schema";
|
||||
import { jsonToNode, ObjectType } from "@llamaindex/core/schema";
|
||||
import { jsonToNode, ObjectType, ImageNode } from "@llamaindex/core/schema";
|
||||
import { extractText } from "@llamaindex/core/utils";
|
||||
import type { ClientParams, CloudConstructorParams } from "./type.js";
|
||||
import { getPipelineId, initService } from "./utils.js";
|
||||
import { getPipelineId, getProjectId, initService } from "./utils.js";
|
||||
import {
|
||||
type PageScreenshotNodeWithScore,
|
||||
type PageFigureNodeWithScore,
|
||||
generateFilePageScreenshotPresignedUrlApiV1FilesIdPageScreenshotsPageIndexPresignedUrlPost,
|
||||
generateFilePageFigurePresignedUrlApiV1FilesIdPageFiguresPageIndexFigureNamePresignedUrlPost,
|
||||
} from "./api";
|
||||
|
||||
export type CloudRetrieveParams = Omit<
|
||||
RetrievalParams,
|
||||
@@ -43,6 +49,95 @@ export class LlamaCloudRetriever extends BaseRetriever {
|
||||
});
|
||||
}
|
||||
|
||||
private async fetchBase64FromPresignedUrl(url: string): Promise<string> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch media from presigned URL: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
return buffer.toString("base64");
|
||||
}
|
||||
|
||||
private async pageScreenshotNodesToNodeWithScore(
|
||||
nodes: PageScreenshotNodeWithScore[] | undefined,
|
||||
projectId: string,
|
||||
): Promise<NodeWithScore[]> {
|
||||
if (!nodes || nodes.length === 0) return [];
|
||||
|
||||
const results = await Promise.all(
|
||||
nodes.map(async (n) => {
|
||||
const { data: presigned } =
|
||||
await generateFilePageScreenshotPresignedUrlApiV1FilesIdPageScreenshotsPageIndexPresignedUrlPost(
|
||||
{
|
||||
throwOnError: true,
|
||||
path: {
|
||||
id: n.node.file_id,
|
||||
page_index: n.node.page_index,
|
||||
},
|
||||
query: {
|
||||
project_id: projectId,
|
||||
organization_id: this.organizationId ?? null,
|
||||
},
|
||||
},
|
||||
);
|
||||
const base64 = await this.fetchBase64FromPresignedUrl(presigned.url);
|
||||
const imageNode = new ImageNode({
|
||||
image: base64,
|
||||
metadata: {
|
||||
...(n.node.metadata ?? {}),
|
||||
file_id: n.node.file_id,
|
||||
page_index: n.node.page_index,
|
||||
},
|
||||
});
|
||||
return { node: imageNode, score: n.score } satisfies NodeWithScore;
|
||||
}),
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async pageFigureNodesToNodeWithScore(
|
||||
nodes: PageFigureNodeWithScore[] | undefined,
|
||||
projectId: string,
|
||||
): Promise<NodeWithScore[]> {
|
||||
if (!nodes || nodes.length === 0) return [];
|
||||
|
||||
const results = await Promise.all(
|
||||
nodes.map(async (n) => {
|
||||
const { data: presigned } =
|
||||
await generateFilePageFigurePresignedUrlApiV1FilesIdPageFiguresPageIndexFigureNamePresignedUrlPost(
|
||||
{
|
||||
throwOnError: true,
|
||||
path: {
|
||||
id: n.node.file_id,
|
||||
page_index: n.node.page_index,
|
||||
figure_name: n.node.figure_name,
|
||||
},
|
||||
query: {
|
||||
project_id: projectId,
|
||||
organization_id: this.organizationId ?? null,
|
||||
},
|
||||
},
|
||||
);
|
||||
const base64 = await this.fetchBase64FromPresignedUrl(presigned.url);
|
||||
const imageNode = new ImageNode({
|
||||
image: base64,
|
||||
metadata: {
|
||||
...(n.node.metadata ?? {}),
|
||||
file_id: n.node.file_id,
|
||||
page_index: n.node.page_index,
|
||||
figure_name: n.node.figure_name,
|
||||
},
|
||||
});
|
||||
return { node: imageNode, score: n.score } satisfies NodeWithScore;
|
||||
}),
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// LlamaCloud expects null values for filters, but LlamaIndexTS uses undefined for empty values
|
||||
// This function converts the undefined values to null
|
||||
private convertFilter(filters?: MetadataFilters): MetadataFilters | null {
|
||||
@@ -76,6 +171,35 @@ export class LlamaCloudRetriever extends BaseRetriever {
|
||||
}
|
||||
|
||||
async _retrieve(query: QueryBundle): Promise<NodeWithScore[]> {
|
||||
// Handle deprecated image retrieval flag
|
||||
const retrieveImageNodes = (this.retrieveParams as RetrievalParams)
|
||||
.retrieve_image_nodes;
|
||||
if (typeof retrieveImageNodes !== "undefined") {
|
||||
console.warn(
|
||||
"The `retrieve_image_nodes` parameter is deprecated. Use `retrieve_page_screenshot_nodes` and `retrieve_page_figure_nodes` instead.",
|
||||
);
|
||||
}
|
||||
|
||||
const retrievePageScreenshotNodes = (this.retrieveParams as RetrievalParams)
|
||||
.retrieve_page_screenshot_nodes;
|
||||
const retrievePageFigureNodes = (this.retrieveParams as RetrievalParams)
|
||||
.retrieve_page_figure_nodes;
|
||||
|
||||
if (retrieveImageNodes) {
|
||||
if (
|
||||
retrievePageScreenshotNodes === false ||
|
||||
retrievePageFigureNodes === false
|
||||
) {
|
||||
throw new Error(
|
||||
"If `retrieve_image_nodes` is set to true, both `retrieve_page_screenshot_nodes` and `retrieve_page_figure_nodes` must also be set to true or omitted.",
|
||||
);
|
||||
}
|
||||
(this.retrieveParams as RetrievalParams).retrieve_page_screenshot_nodes =
|
||||
true;
|
||||
(this.retrieveParams as RetrievalParams).retrieve_page_figure_nodes =
|
||||
true;
|
||||
}
|
||||
|
||||
const pipelineId = await getPipelineId(
|
||||
this.pipelineName,
|
||||
this.projectName,
|
||||
@@ -98,6 +222,34 @@ export class LlamaCloudRetriever extends BaseRetriever {
|
||||
},
|
||||
});
|
||||
|
||||
return this.resultNodesToNodeWithScore(results.retrieval_nodes);
|
||||
const textNodes = this.resultNodesToNodeWithScore(results.retrieval_nodes);
|
||||
|
||||
const needScreenshots = (this.retrieveParams as RetrievalParams)
|
||||
.retrieve_page_screenshot_nodes;
|
||||
const needFigures = (this.retrieveParams as RetrievalParams)
|
||||
.retrieve_page_figure_nodes;
|
||||
|
||||
if (!needScreenshots && !needFigures) {
|
||||
return textNodes;
|
||||
}
|
||||
|
||||
const projectId = await getProjectId(this.projectName, this.organizationId);
|
||||
|
||||
const [screenshotNodes, figureNodes] = await Promise.all([
|
||||
needScreenshots
|
||||
? this.pageScreenshotNodesToNodeWithScore(
|
||||
results.image_nodes,
|
||||
projectId,
|
||||
)
|
||||
: Promise.resolve([] as NodeWithScore[]),
|
||||
needFigures
|
||||
? this.pageFigureNodesToNodeWithScore(
|
||||
results.page_figure_nodes,
|
||||
projectId,
|
||||
)
|
||||
: Promise.resolve([] as NodeWithScore[]),
|
||||
]);
|
||||
|
||||
return [...textNodes, ...screenshotNodes, ...figureNodes];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,25 +4,7 @@ import * as extract from "./extract";
|
||||
import type { ExtractAgent, ExtractConfig } from "./extract";
|
||||
import { getEnv } from "@llamaindex/env";
|
||||
import type { ExtractResult } from "./type";
|
||||
|
||||
const URLS = {
|
||||
us: "https://api.cloud.llamaindex.ai",
|
||||
eu: "https://api.cloud.eu.llamaindex.ai",
|
||||
"us-staging": "https://api.staging.llamaindex.ai",
|
||||
} as const;
|
||||
|
||||
function getUrl(baseUrl: string | undefined, region: string | undefined) {
|
||||
if (typeof baseUrl != "undefined") {
|
||||
return baseUrl;
|
||||
}
|
||||
if (typeof region === "undefined") {
|
||||
return URLS["us"];
|
||||
} else if (region === "us" || region === "eu" || region === "us-staging") {
|
||||
return URLS[region];
|
||||
} else {
|
||||
throw new Error(`Unsupported region: ${region}`);
|
||||
}
|
||||
}
|
||||
import { getUrl } from "./utils";
|
||||
|
||||
export class LlamaExtractAgent {
|
||||
private agent: ExtractAgent;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
aggregateAgentDataApiV1BetaAgentDataAggregatePost,
|
||||
createAgentDataApiV1BetaAgentDataPost,
|
||||
deleteAgentDataApiV1BetaAgentDataItemIdDelete,
|
||||
deleteAgentDataByQueryApiV1BetaAgentDataDeletePost,
|
||||
getAgentDataApiV1BetaAgentDataItemIdGet,
|
||||
searchAgentDataApiV1BetaAgentDataSearchPost,
|
||||
updateAgentDataApiV1BetaAgentDataItemIdPut,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
} from "../../client";
|
||||
import type {
|
||||
AggregateAgentDataOptions,
|
||||
DeleteAgentDataOptions,
|
||||
SearchAgentDataOptions,
|
||||
TypedAgentData,
|
||||
TypedAgentDataItems,
|
||||
@@ -112,6 +114,24 @@ export class AgentClient<T = unknown> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all matching agent data, returns the total number of deleted items
|
||||
*/
|
||||
async delete(options: DeleteAgentDataOptions): Promise<number> {
|
||||
const response = await deleteAgentDataByQueryApiV1BetaAgentDataDeletePost({
|
||||
throwOnError: true,
|
||||
body: {
|
||||
deployment_name: this.deploymentName,
|
||||
...(this.collection !== undefined && {
|
||||
collection: this.collection,
|
||||
}),
|
||||
...(options.filter !== undefined && { filter: options.filter }),
|
||||
},
|
||||
client: this.client,
|
||||
});
|
||||
return response.data.deleted_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search agent data
|
||||
*/
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface ExtractedFieldMetadata {
|
||||
confidence?: number;
|
||||
/** The confidence score for the field based on the extracted text only */
|
||||
extraction_confidence?: number;
|
||||
citation: FieldCitation[];
|
||||
citation?: FieldCitation[];
|
||||
}
|
||||
|
||||
export interface FieldCitation {
|
||||
@@ -127,6 +127,14 @@ export interface SearchAgentDataOptions {
|
||||
includeTotal?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for deleting agent data
|
||||
*/
|
||||
export interface DeleteAgentDataOptions {
|
||||
/** Filter options for the deletion. */
|
||||
filter?: Record<string, FilterOperation>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for aggregating agent data
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
import type {
|
||||
Options,
|
||||
CreateClassifyJobApiV1ClassifierJobsPostData,
|
||||
ClassifyJobCreate,
|
||||
ClassifierRule,
|
||||
ClassifyParsingConfiguration,
|
||||
GetClassifyJobApiV1ClassifierJobsClassifyJobIdGetData,
|
||||
GetClassificationJobResultsApiV1ClassifierJobsClassifyJobIdResultsGetData,
|
||||
ClassifyJobResults,
|
||||
} from "./api";
|
||||
import {
|
||||
StatusEnum,
|
||||
createClassifyJobApiV1ClassifierJobsPost,
|
||||
getClassifyJobApiV1ClassifierJobsClassifyJobIdGet,
|
||||
getClassificationJobResultsApiV1ClassifierJobsClassifyJobIdResultsGet,
|
||||
} from "./api";
|
||||
import type { Client } from "@hey-api/client-fetch";
|
||||
import { sleep } from "./utils";
|
||||
import { uploadFile } from "./fileUpload";
|
||||
import { File } from "buffer";
|
||||
|
||||
async function createClassifyJob({
|
||||
fileIds,
|
||||
rules,
|
||||
parsingConfiguration,
|
||||
projectId,
|
||||
client,
|
||||
maxRetriesOnError = 10,
|
||||
retryInterval = 0.5,
|
||||
}: {
|
||||
fileIds: string[];
|
||||
rules: ClassifierRule[];
|
||||
parsingConfiguration: ClassifyParsingConfiguration;
|
||||
projectId?: string | undefined;
|
||||
client?: Client | undefined;
|
||||
maxRetriesOnError?: number;
|
||||
retryInterval?: number;
|
||||
}): Promise<string> {
|
||||
const rawData = {
|
||||
file_ids: fileIds,
|
||||
rules: rules,
|
||||
parsing_configuration: parsingConfiguration,
|
||||
} as ClassifyJobCreate;
|
||||
const data = {
|
||||
body: rawData,
|
||||
query: {
|
||||
project_id: projectId,
|
||||
},
|
||||
} as CreateClassifyJobApiV1ClassifierJobsPostData;
|
||||
const options = data as Options<CreateClassifyJobApiV1ClassifierJobsPostData>;
|
||||
if (typeof client != "undefined") {
|
||||
options.client = client;
|
||||
}
|
||||
let retries = 0;
|
||||
while (true) {
|
||||
if (retries > maxRetriesOnError) {
|
||||
throw new Error(
|
||||
"Error while creating the classify job: Exceeded maximum number of retries, the API keeps returning errors.",
|
||||
);
|
||||
}
|
||||
const response = await createClassifyJobApiV1ClassifierJobsPost(options);
|
||||
if (!response.response.ok) {
|
||||
if ("error" in response) {
|
||||
console.log(
|
||||
`An error occurred while creating the classification job.\nDetails:\n\n${JSON.stringify(
|
||||
response.error,
|
||||
)}\n\nRetrying...`,
|
||||
);
|
||||
}
|
||||
retries++;
|
||||
await sleep(retryInterval * 1000);
|
||||
} else {
|
||||
if (typeof response.data != "undefined") {
|
||||
return response.data.id;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Error while creating the classify job: the job creation succeeded but no data where returned",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function pollForJobCompletion({
|
||||
jobId,
|
||||
interval = 1,
|
||||
maxIterations = 1800,
|
||||
client,
|
||||
}: {
|
||||
jobId: string;
|
||||
interval?: number;
|
||||
maxIterations?: number;
|
||||
client?: Client | undefined;
|
||||
}): Promise<boolean> {
|
||||
let status: StatusEnum | undefined = undefined;
|
||||
const jobData = {
|
||||
path: { classify_job_id: jobId },
|
||||
} as GetClassifyJobApiV1ClassifierJobsClassifyJobIdGetData;
|
||||
const jobOptions =
|
||||
jobData as Options<GetClassifyJobApiV1ClassifierJobsClassifyJobIdGetData>;
|
||||
if (typeof client != "undefined") {
|
||||
jobOptions.client = client;
|
||||
}
|
||||
let numIterations: number = 0;
|
||||
while (true) {
|
||||
if (numIterations > maxIterations) {
|
||||
return false;
|
||||
}
|
||||
const response =
|
||||
await getClassifyJobApiV1ClassifierJobsClassifyJobIdGet(jobOptions);
|
||||
if (!response.response.ok) {
|
||||
numIterations++;
|
||||
}
|
||||
if (typeof response.data != "undefined") {
|
||||
status = response.data.status as StatusEnum;
|
||||
if (status == StatusEnum.CANCELLED || status == StatusEnum.ERROR) {
|
||||
throw new Error("There was an error during the classification job.");
|
||||
} else if (status == StatusEnum.SUCCESS) {
|
||||
return true;
|
||||
} else {
|
||||
numIterations++;
|
||||
await sleep(interval * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getJobResult({
|
||||
jobId,
|
||||
client,
|
||||
projectId,
|
||||
maxRetriesOnError = 10,
|
||||
retryInterval = 0.5,
|
||||
}: {
|
||||
jobId: string;
|
||||
client?: Client | undefined;
|
||||
projectId?: string | undefined;
|
||||
maxRetriesOnError?: number;
|
||||
retryInterval?: number;
|
||||
}): Promise<ClassifyJobResults> {
|
||||
const jobData = {
|
||||
path: { classify_job_id: jobId },
|
||||
query: { project_id: projectId },
|
||||
} as GetClassificationJobResultsApiV1ClassifierJobsClassifyJobIdResultsGetData;
|
||||
const jobOptions =
|
||||
jobData as Options<GetClassificationJobResultsApiV1ClassifierJobsClassifyJobIdResultsGetData>;
|
||||
if (typeof client != "undefined") {
|
||||
jobOptions.client = client;
|
||||
}
|
||||
let retries: number = 0;
|
||||
while (true) {
|
||||
if (retries > maxRetriesOnError) {
|
||||
throw new Error(
|
||||
"Error while getting the result of the classification job: Exceeded maximum number of retries, the API keeps returning errors.",
|
||||
);
|
||||
}
|
||||
const response =
|
||||
await getClassificationJobResultsApiV1ClassifierJobsClassifyJobIdResultsGet(
|
||||
jobOptions,
|
||||
);
|
||||
if (!response.response.ok) {
|
||||
if ("error" in response) {
|
||||
console.log(
|
||||
"An error occurred: ",
|
||||
JSON.stringify(response.error),
|
||||
"\nRetrying...",
|
||||
);
|
||||
}
|
||||
retries++;
|
||||
await sleep(retryInterval * 1000);
|
||||
}
|
||||
if (typeof response.data != "undefined") {
|
||||
return response.data as ClassifyJobResults;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Error while retrieving results for the classify job: the result was successfully obtained but no data were returned",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function classify(
|
||||
rules: ClassifierRule[],
|
||||
parsingConfiguration: ClassifyParsingConfiguration,
|
||||
{
|
||||
fileContents,
|
||||
filePaths,
|
||||
projectId,
|
||||
client,
|
||||
pollingInterval = 1,
|
||||
maxPollingIterations = 1800,
|
||||
maxRetriesOnError = 10,
|
||||
retryInterval = 0.5,
|
||||
}: {
|
||||
fileContents?:
|
||||
| Buffer<ArrayBufferLike>[]
|
||||
| File[]
|
||||
| Uint8Array<ArrayBuffer>[]
|
||||
| string[]
|
||||
| undefined;
|
||||
filePaths?: string[] | undefined;
|
||||
projectId?: string | undefined;
|
||||
client?: Client | undefined;
|
||||
pollingInterval?: number;
|
||||
maxPollingIterations?: number;
|
||||
maxRetriesOnError?: number;
|
||||
retryInterval?: number;
|
||||
},
|
||||
): Promise<ClassifyJobResults> {
|
||||
const fileIds: string[] = [];
|
||||
if (!filePaths && !fileContents) {
|
||||
throw new Error(
|
||||
"One between filePath and fileContent needs to be provided",
|
||||
);
|
||||
}
|
||||
|
||||
if (filePaths) {
|
||||
const uploadPromises = filePaths.map(async (name) => {
|
||||
try {
|
||||
const fileId = await uploadFile({
|
||||
filePath: name,
|
||||
maxRetriesOnError,
|
||||
retryInterval: retryInterval,
|
||||
project_id: projectId,
|
||||
client: client,
|
||||
});
|
||||
if (fileId) {
|
||||
return fileId;
|
||||
} else {
|
||||
console.error(`Unable to upload ${name}, skipping...`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error uploading ${name}:`, error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(uploadPromises);
|
||||
fileIds.push(...results.filter((id) => id !== null));
|
||||
}
|
||||
|
||||
if (fileContents) {
|
||||
const uploadPromises = fileContents.map(async (content) => {
|
||||
try {
|
||||
const fileId = await uploadFile({
|
||||
fileContent: content,
|
||||
...(projectId ? { project_id: projectId } : {}),
|
||||
...(client ? { client: client } : {}),
|
||||
maxRetriesOnError,
|
||||
retryInterval,
|
||||
});
|
||||
if (fileId) {
|
||||
return fileId;
|
||||
} else {
|
||||
console.error(`Unable to upload file (content), skipping...`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error uploading file (content):`, error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(uploadPromises);
|
||||
fileIds.push(...results.filter((id) => id !== null));
|
||||
}
|
||||
|
||||
if (fileIds.length == 0) {
|
||||
throw new Error(
|
||||
"None of the provided files was successfully uploaded, it is not possible to create a classification job.",
|
||||
);
|
||||
}
|
||||
|
||||
const jobId = await createClassifyJob({
|
||||
fileIds,
|
||||
rules,
|
||||
parsingConfiguration,
|
||||
...(projectId ? { projectId: projectId } : {}),
|
||||
...(client ? { client: client } : {}),
|
||||
maxRetriesOnError,
|
||||
retryInterval,
|
||||
});
|
||||
const success = await pollForJobCompletion({
|
||||
jobId,
|
||||
interval: pollingInterval,
|
||||
maxIterations: maxPollingIterations,
|
||||
client,
|
||||
});
|
||||
if (!success) {
|
||||
throw new Error("Your job is taking longer than 10 minutes, timing out...");
|
||||
} else {
|
||||
return (await getJobResult({
|
||||
jobId,
|
||||
client,
|
||||
projectId,
|
||||
maxRetriesOnError,
|
||||
retryInterval,
|
||||
})) as ClassifyJobResults;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
type ClassifierRule,
|
||||
type ClassifyJobResults,
|
||||
type ClassifyParsingConfiguration,
|
||||
};
|
||||
@@ -1530,27 +1530,6 @@ export const Body_run_job_on_file_api_v1_extraction_jobs_file_postSchema = {
|
||||
title: "Body_run_job_on_file_api_v1_extraction_jobs_file_post",
|
||||
} as const;
|
||||
|
||||
export const Body_run_job_test_user_api_v1_extraction_jobs_test_postSchema = {
|
||||
properties: {
|
||||
job_create: {
|
||||
$ref: "#/components/schemas/ExtractJobCreate",
|
||||
},
|
||||
extract_settings: {
|
||||
anyOf: [
|
||||
{
|
||||
$ref: "#/components/schemas/LlamaExtractSettings",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: ["job_create"],
|
||||
title: "Body_run_job_test_user_api_v1_extraction_jobs_test_post",
|
||||
} as const;
|
||||
|
||||
export const Body_screenshot_api_parsing_screenshot_postSchema = {
|
||||
properties: {
|
||||
file: {
|
||||
@@ -2796,30 +2775,6 @@ export const Body_upload_file_api_v1_parsing_upload_postSchema = {
|
||||
title: "Body_upload_file_api_v1_parsing_upload_post",
|
||||
} as const;
|
||||
|
||||
export const Body_upload_file_v2_api_v2alpha1_parse_upload_postSchema = {
|
||||
properties: {
|
||||
configuration: {
|
||||
type: "string",
|
||||
title: "Configuration",
|
||||
},
|
||||
file: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
title: "File",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: ["configuration"],
|
||||
title: "Body_upload_file_v2_api_v2alpha1_parse_upload_post",
|
||||
} as const;
|
||||
|
||||
export const BoxAuthMechanismSchema = {
|
||||
type: "string",
|
||||
enum: ["developer_token", "ccg"],
|
||||
@@ -3180,12 +3135,6 @@ export const ChatMessageSchema = {
|
||||
title: "ChatMessage",
|
||||
} as const;
|
||||
|
||||
export const ChunkModeSchema = {
|
||||
type: "string",
|
||||
enum: ["PAGE", "DOCUMENT", "SECTION", "GROUPED_PAGES"],
|
||||
title: "ChunkMode",
|
||||
} as const;
|
||||
|
||||
export const ClassificationResultSchema = {
|
||||
properties: {
|
||||
reasoning: {
|
||||
@@ -5486,6 +5435,13 @@ export const CustomClaimsSchema = {
|
||||
description: "Whether the user is allowed to delete organizations.",
|
||||
default: false,
|
||||
},
|
||||
allowed_spreadsheet: {
|
||||
type: "boolean",
|
||||
title: "Allowed Spreadsheet",
|
||||
description:
|
||||
"Whether the user is allowed to access the spreadsheet feature.",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
title: "CustomClaims",
|
||||
@@ -6213,6 +6169,54 @@ export const DeleteParamsSchema = {
|
||||
description: "Schema for the parameters of a delete job.",
|
||||
} as const;
|
||||
|
||||
export const DeleteRequestSchema = {
|
||||
properties: {
|
||||
deployment_name: {
|
||||
type: "string",
|
||||
title: "Deployment Name",
|
||||
description: "The agent deployment's name to delete data for",
|
||||
},
|
||||
collection: {
|
||||
type: "string",
|
||||
title: "Collection",
|
||||
description: "The logical agent data collection to delete from",
|
||||
default: "default",
|
||||
},
|
||||
filter: {
|
||||
anyOf: [
|
||||
{
|
||||
additionalProperties: {
|
||||
$ref: "#/components/schemas/FilterOperation",
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
title: "Filter",
|
||||
description: "Optional filters to select which items to delete",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: ["deployment_name"],
|
||||
title: "DeleteRequest",
|
||||
description: "API request body for bulk deleting agent data by query",
|
||||
} as const;
|
||||
|
||||
export const DeleteResponseSchema = {
|
||||
properties: {
|
||||
deleted_count: {
|
||||
type: "integer",
|
||||
title: "Deleted Count",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: ["deleted_count"],
|
||||
title: "DeleteResponse",
|
||||
description: "API response for bulk delete operation",
|
||||
} as const;
|
||||
|
||||
export const DirectRetrievalParamsSchema = {
|
||||
properties: {
|
||||
mode: {
|
||||
@@ -6946,6 +6950,20 @@ export const ExtractConfigSchema = {
|
||||
description: "Whether to invalidate the cache for the extraction.",
|
||||
default: false,
|
||||
},
|
||||
num_pages_context: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
title: "Num Pages Context",
|
||||
description:
|
||||
"Number of pages to pass as context on long document extraction.",
|
||||
},
|
||||
page_range: {
|
||||
anyOf: [
|
||||
{
|
||||
@@ -7202,6 +7220,7 @@ export const ExtractModelsSchema = {
|
||||
"openai-gpt-5-mini",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-flash-lite",
|
||||
"gemini-2.5-pro",
|
||||
"openai-gpt-4o",
|
||||
"openai-gpt-4o-mini",
|
||||
@@ -7849,6 +7868,52 @@ export const ExtractTargetSchema = {
|
||||
title: "ExtractTarget",
|
||||
} as const;
|
||||
|
||||
export const ExtractedTableSchema = {
|
||||
properties: {
|
||||
table_id: {
|
||||
type: "integer",
|
||||
title: "Table Id",
|
||||
description: "Unique identifier for this table within the file",
|
||||
},
|
||||
sheet_name: {
|
||||
type: "string",
|
||||
title: "Sheet Name",
|
||||
description: "Worksheet name where table was found",
|
||||
},
|
||||
row_span: {
|
||||
type: "integer",
|
||||
title: "Row Span",
|
||||
description: "Number of rows in the table",
|
||||
},
|
||||
col_span: {
|
||||
type: "integer",
|
||||
title: "Col Span",
|
||||
description: "Number of columns in the table",
|
||||
},
|
||||
has_headers: {
|
||||
type: "boolean",
|
||||
title: "Has Headers",
|
||||
description: "Whether the table has header rows",
|
||||
},
|
||||
metadata_json: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
title: "Metadata Json",
|
||||
description: "JSON metadata with detailed table information",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: ["table_id", "sheet_name", "row_span", "col_span", "has_headers"],
|
||||
title: "ExtractedTable",
|
||||
description: "A single extracted table from a spreadsheet",
|
||||
} as const;
|
||||
|
||||
export const FailPageModeSchema = {
|
||||
type: "string",
|
||||
enum: ["raw_text", "blank_page", "error_message"],
|
||||
@@ -10828,140 +10893,6 @@ export const LegacyParseJobConfigSchema = {
|
||||
description: "Configuration for llamaparse job",
|
||||
} as const;
|
||||
|
||||
export const LlamaExtractSettingsSchema = {
|
||||
properties: {
|
||||
max_file_size: {
|
||||
type: "integer",
|
||||
title: "Max File Size",
|
||||
description: "The maximum file size (in bytes) allowed for the document.",
|
||||
default: 104857600,
|
||||
},
|
||||
max_file_size_ui: {
|
||||
type: "integer",
|
||||
title: "Max File Size Ui",
|
||||
description: "The maximum file size (in bytes) allowed for the document.",
|
||||
default: 31457280,
|
||||
},
|
||||
max_pages: {
|
||||
type: "integer",
|
||||
title: "Max Pages",
|
||||
description: "The maximum number of pages allowed for the document.",
|
||||
default: 500,
|
||||
},
|
||||
chunk_mode: {
|
||||
$ref: "#/components/schemas/ChunkMode",
|
||||
description: "The mode to use for chunking the document.",
|
||||
default: "SECTION",
|
||||
},
|
||||
max_chunk_size: {
|
||||
type: "integer",
|
||||
title: "Max Chunk Size",
|
||||
description:
|
||||
"The maximum size of the chunks (in tokens) to use for chunking the document.",
|
||||
default: 10000,
|
||||
},
|
||||
extraction_agent_config: {
|
||||
additionalProperties: {
|
||||
$ref: "#/components/schemas/StructParseConf",
|
||||
},
|
||||
type: "object",
|
||||
title: "Extraction Agent Config",
|
||||
description: "The configuration for the extraction agent.",
|
||||
},
|
||||
use_multimodal_parsing: {
|
||||
type: "boolean",
|
||||
title: "Use Multimodal Parsing",
|
||||
description: "Whether to use experimental multimodal parsing.",
|
||||
default: false,
|
||||
},
|
||||
use_pixel_extraction: {
|
||||
type: "boolean",
|
||||
title: "Use Pixel Extraction",
|
||||
description:
|
||||
"DEPRECATED: Whether to use extraction over pixels for multimodal mode.",
|
||||
default: false,
|
||||
},
|
||||
llama_parse_params: {
|
||||
$ref: "#/components/schemas/LlamaParseParameters",
|
||||
description: "LlamaParse related settings.",
|
||||
default: {
|
||||
languages: ["en"],
|
||||
parsing_instruction: "",
|
||||
disable_ocr: false,
|
||||
annotate_links: true,
|
||||
adaptive_long_table: true,
|
||||
compact_markdown_table: false,
|
||||
disable_reconstruction: false,
|
||||
disable_image_extraction: false,
|
||||
invalidate_cache: false,
|
||||
outlined_table_extraction: true,
|
||||
merge_tables_across_pages_in_markdown: false,
|
||||
output_pdf_of_document: false,
|
||||
do_not_cache: false,
|
||||
fast_mode: false,
|
||||
skip_diagonal_text: false,
|
||||
preserve_layout_alignment_across_pages: false,
|
||||
preserve_very_small_text: false,
|
||||
gpt4o_mode: false,
|
||||
do_not_unroll_columns: false,
|
||||
extract_layout: false,
|
||||
high_res_ocr: false,
|
||||
html_make_all_elements_visible: false,
|
||||
layout_aware: false,
|
||||
specialized_chart_parsing_agentic: false,
|
||||
specialized_chart_parsing_plus: false,
|
||||
specialized_chart_parsing_efficient: false,
|
||||
specialized_image_parsing: false,
|
||||
precise_bounding_box: false,
|
||||
html_remove_navigation_elements: false,
|
||||
html_remove_fixed_elements: false,
|
||||
guess_xlsx_sheet_name: false,
|
||||
use_vendor_multimodal_model: false,
|
||||
page_prefix: `<<<PAGE:{pageNumber}>>>
|
||||
|
||||
`,
|
||||
page_suffix: `
|
||||
|
||||
<<<END_PAGE>>>`,
|
||||
take_screenshot: false,
|
||||
is_formatting_instruction: true,
|
||||
premium_mode: false,
|
||||
continuous_mode: false,
|
||||
auto_mode: false,
|
||||
auto_mode_trigger_on_table_in_page: false,
|
||||
auto_mode_trigger_on_image_in_page: false,
|
||||
structured_output: false,
|
||||
extract_charts: false,
|
||||
spreadsheet_extract_sub_tables: false,
|
||||
spreadsheet_force_formula_computation: false,
|
||||
inline_images_in_markdown: false,
|
||||
strict_mode_image_extraction: false,
|
||||
strict_mode_image_ocr: false,
|
||||
strict_mode_reconstruction: false,
|
||||
strict_mode_buggy_font: false,
|
||||
save_images: true,
|
||||
hide_headers: false,
|
||||
hide_footers: false,
|
||||
ignore_document_elements_for_layout_detection: false,
|
||||
output_tables_as_HTML: false,
|
||||
internal_is_screenshot_job: false,
|
||||
parse_mode: "parse_page_with_llm",
|
||||
page_error_tolerance: 0.05,
|
||||
replace_failed_page_mode: "raw_text",
|
||||
},
|
||||
},
|
||||
multimodal_parse_resolution: {
|
||||
$ref: "#/components/schemas/MultimodalParseResolution",
|
||||
description: "The resolution to use for multimodal parsing.",
|
||||
default: "medium",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
title: "LlamaExtractSettings",
|
||||
description: `All settings for the extraction agent. Only the settings in ExtractConfig
|
||||
are exposed to the user.`,
|
||||
} as const;
|
||||
|
||||
export const LlamaParseParametersSchema = {
|
||||
properties: {
|
||||
webhook_configurations: {
|
||||
@@ -12602,12 +12533,6 @@ export const MetronomeDashboardTypeSchema = {
|
||||
title: "MetronomeDashboardType",
|
||||
} as const;
|
||||
|
||||
export const MultimodalParseResolutionSchema = {
|
||||
type: "string",
|
||||
enum: ["medium", "high"],
|
||||
title: "MultimodalParseResolution",
|
||||
} as const;
|
||||
|
||||
export const NodeRelationshipSchema = {
|
||||
type: "string",
|
||||
enum: ["1", "2", "3", "4", "5"],
|
||||
@@ -13430,6 +13355,48 @@ export const PaginatedResponse_QuotaConfiguration_Schema = {
|
||||
title: "PaginatedResponse[QuotaConfiguration]",
|
||||
} as const;
|
||||
|
||||
export const PaginatedResponse_SpreadsheetJob_Schema = {
|
||||
properties: {
|
||||
items: {
|
||||
items: {
|
||||
$ref: "#/components/schemas/SpreadsheetJob",
|
||||
},
|
||||
type: "array",
|
||||
title: "Items",
|
||||
description: "The list of items.",
|
||||
},
|
||||
next_page_token: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
title: "Next Page Token",
|
||||
description:
|
||||
"A token, which can be sent as page_token to retrieve the next page. If this field is omitted, there are no subsequent pages.",
|
||||
},
|
||||
total_size: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "integer",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
title: "Total Size",
|
||||
description:
|
||||
"The total number of items available. This is only populated when specifically requested. The value may be an estimate and can be used for display purposes only.",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: ["items"],
|
||||
title: "PaginatedResponse[SpreadsheetJob]",
|
||||
} as const;
|
||||
|
||||
export const ParseConfigurationSchema = {
|
||||
properties: {
|
||||
id: {
|
||||
@@ -17841,69 +17808,6 @@ export const ProjectUpdateSchema = {
|
||||
description: "Schema for updating a project.",
|
||||
} as const;
|
||||
|
||||
export const PromptConfSchema = {
|
||||
properties: {
|
||||
system_prompt: {
|
||||
type: "string",
|
||||
title: "System Prompt",
|
||||
description: "The system prompt to use for the extraction.",
|
||||
default:
|
||||
"Given a JSON schema, extract the data from the provided SOURCE TEXT according to the schema. Only output information that is explicitly stated or can be inferred from the SOURCE TEXT.",
|
||||
},
|
||||
extraction_prompt: {
|
||||
type: "string",
|
||||
title: "Extraction Prompt",
|
||||
description: "The prompt to use for the extraction.",
|
||||
default: "The extracted data using the given JSON schema.",
|
||||
},
|
||||
error_handling_prompt: {
|
||||
type: "string",
|
||||
title: "Error Handling Prompt",
|
||||
description: "The prompt to use for error handling.",
|
||||
default:
|
||||
"If the source text does not contain enough information to extract the value, explain the reason very briefly. Else, output null and fill out the value__ field.",
|
||||
},
|
||||
reasoning_prompt: {
|
||||
type: "string",
|
||||
title: "Reasoning Prompt",
|
||||
description: "The prompt to use for reasoning.",
|
||||
default: `
|
||||
Provide a brief explanation for how you arrived at the extracted value based on the source text provided.
|
||||
- For inferred values, explain the reasoning behind the extraction briefly.
|
||||
- For simple verbatim extraction, output 'VERBATIM EXTRACTION'.
|
||||
- When supporting data is not present in the source text, output 'INSUFFICIENT DATA' and emit blank or null values for the value__ field.
|
||||
`,
|
||||
},
|
||||
cite_sources_prompt: {
|
||||
additionalProperties: {
|
||||
type: "string",
|
||||
},
|
||||
type: "object",
|
||||
title: "Cite Sources Prompt",
|
||||
description: "The prompt to use for citing sources.",
|
||||
default: {
|
||||
description: `
|
||||
### Citation Rules (read carefully):
|
||||
- You must ANNOTATE every value with the MOST RELEVANT short EXACT substring from the source text that supports it.
|
||||
- For inferred values, cite the text used to infer it in the matching_text field or output 'INFERRED FROM TEXT'
|
||||
- If no support exists, output 'INSUFFICIENT DATA' and leave value__ null or '', 0.0, False etc depending on the type of the field.
|
||||
`,
|
||||
page: "Cite the page number of the source text that the extracted value is from. The page number is the integer that appears right after <<<PAGE:. If no page number is present in this format, use the default value of 1.",
|
||||
matching_text:
|
||||
'Cite the **MOST RELEVANT EXACT TEXT from the SOURCE TEXT** that supports the extracted value within 80 characters. If the exact substring is >80 chars, truncate with ellipsis "...". Provide only the single most relevant citation.',
|
||||
},
|
||||
},
|
||||
scratchpad_prompt: {
|
||||
type: "string",
|
||||
title: "Scratchpad Prompt",
|
||||
description: "The prompt to use for scratchpad.",
|
||||
default: "Use for intermediate step-by-step reasoning. Be concise.",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
title: "PromptConf",
|
||||
} as const;
|
||||
|
||||
export const PublicModelNameSchema = {
|
||||
type: "string",
|
||||
enum: [
|
||||
@@ -17926,6 +17830,7 @@ export const PublicModelNameSchema = {
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.0-flash-lite",
|
||||
"gemini-2.5-flash-lite",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-pro",
|
||||
],
|
||||
@@ -18752,12 +18657,6 @@ export const RoleSchema = {
|
||||
description: "Schema for a role.",
|
||||
} as const;
|
||||
|
||||
export const SchemaRelaxModeSchema = {
|
||||
type: "string",
|
||||
enum: ["FULL", "TOP_LEVEL", "LEAF"],
|
||||
title: "SchemaRelaxMode",
|
||||
} as const;
|
||||
|
||||
export const SearchRequestSchema = {
|
||||
properties: {
|
||||
page_size: {
|
||||
@@ -18950,6 +18849,135 @@ BM25: Uses Qdrant's FastEmbed BM25 model for sparse embeddings
|
||||
AUTO: Automatically selects based on deployment mode (BYOC uses term frequency, Cloud uses Splade)`,
|
||||
} as const;
|
||||
|
||||
export const SpreadsheetJobSchema = {
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
title: "Id",
|
||||
description: "The ID of the job",
|
||||
},
|
||||
user_id: {
|
||||
type: "string",
|
||||
title: "User Id",
|
||||
description: "The ID of the user",
|
||||
},
|
||||
project_id: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
title: "Project Id",
|
||||
description: "The ID of the project",
|
||||
},
|
||||
file_id: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
title: "File Id",
|
||||
description: "The ID of the file to parse",
|
||||
},
|
||||
config: {
|
||||
$ref: "#/components/schemas/SpreadsheetParsingConfig",
|
||||
description: "Configuration for the parsing job",
|
||||
},
|
||||
status: {
|
||||
$ref: "#/components/schemas/StatusEnum",
|
||||
description: "The status of the parsing job",
|
||||
},
|
||||
created_at: {
|
||||
type: "string",
|
||||
title: "Created At",
|
||||
description: "When the job was created",
|
||||
},
|
||||
updated_at: {
|
||||
type: "string",
|
||||
title: "Updated At",
|
||||
description: "When the job was last updated",
|
||||
},
|
||||
success: {
|
||||
anyOf: [
|
||||
{
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
title: "Success",
|
||||
description: "Whether the job completed successfully",
|
||||
},
|
||||
tables: {
|
||||
items: {
|
||||
$ref: "#/components/schemas/ExtractedTable",
|
||||
},
|
||||
type: "array",
|
||||
title: "Tables",
|
||||
description: "All extracted tables (populated when job is complete)",
|
||||
},
|
||||
errors: {
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
type: "array",
|
||||
title: "Errors",
|
||||
description: "Any errors encountered",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: [
|
||||
"id",
|
||||
"user_id",
|
||||
"project_id",
|
||||
"file_id",
|
||||
"config",
|
||||
"status",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
],
|
||||
title: "SpreadsheetJob",
|
||||
description: "A spreadsheet parsing job",
|
||||
} as const;
|
||||
|
||||
export const SpreadsheetJobCreateSchema = {
|
||||
properties: {
|
||||
file_id: {
|
||||
type: "string",
|
||||
format: "uuid",
|
||||
title: "File Id",
|
||||
description: "The ID of the file to parse",
|
||||
},
|
||||
config: {
|
||||
$ref: "#/components/schemas/SpreadsheetParsingConfig",
|
||||
description: "Configuration for the parsing job",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
required: ["file_id"],
|
||||
title: "SpreadsheetJobCreate",
|
||||
description: "Request to create a spreadsheet parsing job",
|
||||
} as const;
|
||||
|
||||
export const SpreadsheetParsingConfigSchema = {
|
||||
properties: {
|
||||
sheet_names: {
|
||||
anyOf: [
|
||||
{
|
||||
items: {
|
||||
type: "string",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
{
|
||||
type: "null",
|
||||
},
|
||||
],
|
||||
title: "Sheet Names",
|
||||
description:
|
||||
"The names of the sheets to parse. If empty, all sheets will be parsed.",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
title: "SpreadsheetParsingConfig",
|
||||
description: "Configuration for spreadsheet parsing",
|
||||
} as const;
|
||||
|
||||
export const StatusEnumSchema = {
|
||||
type: "string",
|
||||
enum: ["PENDING", "SUCCESS", "ERROR", "PARTIAL_SUCCESS", "CANCELLED"],
|
||||
@@ -18957,101 +18985,6 @@ export const StatusEnumSchema = {
|
||||
description: "Enum for representing the status of a job",
|
||||
} as const;
|
||||
|
||||
export const StructModeSchema = {
|
||||
type: "string",
|
||||
enum: [
|
||||
"STRUCT_PARSE",
|
||||
"JSON_MODE",
|
||||
"FUNC_CALL",
|
||||
"STRUCT_RELAXED",
|
||||
"UNSTRUCTURED",
|
||||
],
|
||||
title: "StructMode",
|
||||
} as const;
|
||||
|
||||
export const StructParseConfSchema = {
|
||||
properties: {
|
||||
model: {
|
||||
$ref: "#/components/schemas/ExtractModels",
|
||||
description: "The model to use for the structured parsing.",
|
||||
default: "openai-gpt-4-1",
|
||||
},
|
||||
temperature: {
|
||||
type: "number",
|
||||
title: "Temperature",
|
||||
description: "The temperature to use for the structured parsing.",
|
||||
default: 0,
|
||||
},
|
||||
relaxation_mode: {
|
||||
$ref: "#/components/schemas/SchemaRelaxMode",
|
||||
description: "The relaxation mode to use for the structured parsing.",
|
||||
default: "LEAF",
|
||||
},
|
||||
struct_mode: {
|
||||
$ref: "#/components/schemas/StructMode",
|
||||
description: "The struct mode to use for the structured parsing.",
|
||||
default: "STRUCT_PARSE",
|
||||
},
|
||||
fetch_logprobs: {
|
||||
type: "boolean",
|
||||
title: "Fetch Logprobs",
|
||||
description: "Whether to fetch logprobs for the structured parsing.",
|
||||
default: false,
|
||||
},
|
||||
handle_missing: {
|
||||
type: "boolean",
|
||||
title: "Handle Missing",
|
||||
description: "Whether to handle missing fields in the schema.",
|
||||
default: false,
|
||||
},
|
||||
use_reasoning: {
|
||||
type: "boolean",
|
||||
title: "Use Reasoning",
|
||||
description: "Whether to use reasoning for the structured extraction.",
|
||||
default: false,
|
||||
},
|
||||
cite_sources: {
|
||||
type: "boolean",
|
||||
title: "Cite Sources",
|
||||
description: "Whether to cite sources for the structured extraction.",
|
||||
default: false,
|
||||
},
|
||||
prompt_conf: {
|
||||
$ref: "#/components/schemas/PromptConf",
|
||||
description: "The prompt configuration for the structured parsing.",
|
||||
default: {
|
||||
system_prompt:
|
||||
"Given a JSON schema, extract the data from the provided SOURCE TEXT according to the schema. Only output information that is explicitly stated or can be inferred from the SOURCE TEXT.",
|
||||
extraction_prompt: "The extracted data using the given JSON schema.",
|
||||
error_handling_prompt:
|
||||
"If the source text does not contain enough information to extract the value, explain the reason very briefly. Else, output null and fill out the value__ field.",
|
||||
reasoning_prompt: `
|
||||
Provide a brief explanation for how you arrived at the extracted value based on the source text provided.
|
||||
- For inferred values, explain the reasoning behind the extraction briefly.
|
||||
- For simple verbatim extraction, output 'VERBATIM EXTRACTION'.
|
||||
- When supporting data is not present in the source text, output 'INSUFFICIENT DATA' and emit blank or null values for the value__ field.
|
||||
`,
|
||||
cite_sources_prompt: {
|
||||
description: `
|
||||
### Citation Rules (read carefully):
|
||||
- You must ANNOTATE every value with the MOST RELEVANT short EXACT substring from the source text that supports it.
|
||||
- For inferred values, cite the text used to infer it in the matching_text field or output 'INFERRED FROM TEXT'
|
||||
- If no support exists, output 'INSUFFICIENT DATA' and leave value__ null or '', 0.0, False etc depending on the type of the field.
|
||||
`,
|
||||
matching_text:
|
||||
'Cite the **MOST RELEVANT EXACT TEXT from the SOURCE TEXT** that supports the extracted value within 80 characters. If the exact substring is >80 chars, truncate with ellipsis "...". Provide only the single most relevant citation.',
|
||||
page: "Cite the page number of the source text that the extracted value is from. The page number is the integer that appears right after <<<PAGE:. If no page number is present in this format, use the default value of 1.",
|
||||
},
|
||||
scratchpad_prompt:
|
||||
"Use for intermediate step-by-step reasoning. Be concise.",
|
||||
},
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
title: "StructParseConf",
|
||||
description: "Configuration for the structured parsing agent.",
|
||||
} as const;
|
||||
|
||||
export const SupportedLLMModelSchema = {
|
||||
properties: {
|
||||
name: {
|
||||
|
||||
@@ -691,115 +691,6 @@ export const zBodyRunJobOnFileApiV1ExtractionJobsFilePost = z.object({
|
||||
config_override: z.union([z.string(), z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zExtractTarget = z.enum(["PER_DOC", "PER_PAGE"]);
|
||||
|
||||
export const zExtractMode = z.enum([
|
||||
"FAST",
|
||||
"BALANCED",
|
||||
"PREMIUM",
|
||||
"MULTIMODAL",
|
||||
]);
|
||||
|
||||
export const zPublicModelName = z.enum([
|
||||
"openai-gpt-4o",
|
||||
"openai-gpt-4o-mini",
|
||||
"openai-gpt-4-1",
|
||||
"openai-gpt-4-1-mini",
|
||||
"openai-gpt-4-1-nano",
|
||||
"openai-gpt-5",
|
||||
"openai-gpt-5-mini",
|
||||
"openai-gpt-5-nano",
|
||||
"openai-text-embedding-3-small",
|
||||
"openai-text-embedding-3-large",
|
||||
"openai-whisper-1",
|
||||
"anthropic-sonnet-3.5",
|
||||
"anthropic-sonnet-3.5-v2",
|
||||
"anthropic-sonnet-3.7",
|
||||
"anthropic-sonnet-4.0",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.0-flash-lite",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-pro",
|
||||
]);
|
||||
|
||||
export const zExtractModels = z.enum([
|
||||
"openai-gpt-4-1",
|
||||
"openai-gpt-4-1-mini",
|
||||
"openai-gpt-4-1-nano",
|
||||
"openai-gpt-5",
|
||||
"openai-gpt-5-mini",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-pro",
|
||||
"openai-gpt-4o",
|
||||
"openai-gpt-4o-mini",
|
||||
]);
|
||||
|
||||
export const zDocumentChunkMode = z.enum(["PAGE", "SECTION"]);
|
||||
|
||||
export const zExtractConfig = z.object({
|
||||
priority: z
|
||||
.union([z.enum(["low", "medium", "high", "critical"]), z.null()])
|
||||
.optional(),
|
||||
extraction_target: zExtractTarget.optional(),
|
||||
extraction_mode: zExtractMode.optional(),
|
||||
parse_model: z.union([zPublicModelName, z.null()]).optional(),
|
||||
extract_model: z.union([zExtractModels, z.null()]).optional(),
|
||||
multimodal_fast_mode: z.boolean().optional().default(false),
|
||||
system_prompt: z.union([z.string(), z.null()]).optional(),
|
||||
use_reasoning: z.boolean().optional().default(false),
|
||||
cite_sources: z.boolean().optional().default(false),
|
||||
confidence_scores: z.boolean().optional().default(false),
|
||||
chunk_mode: zDocumentChunkMode.optional(),
|
||||
high_resolution_mode: z.boolean().optional().default(false),
|
||||
invalidate_cache: z.boolean().optional().default(false),
|
||||
page_range: z.union([z.string(), z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zExtractJobCreate = z.object({
|
||||
priority: z
|
||||
.union([z.enum(["low", "medium", "high", "critical"]), z.null()])
|
||||
.optional(),
|
||||
webhook_configurations: z
|
||||
.union([z.array(zWebhookConfiguration), z.null()])
|
||||
.optional(),
|
||||
extraction_agent_id: z.string().uuid(),
|
||||
file_id: z.string().uuid(),
|
||||
data_schema_override: z
|
||||
.union([z.object({}), z.string(), z.null()])
|
||||
.optional(),
|
||||
config_override: z.union([zExtractConfig, z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zChunkMode = z.enum([
|
||||
"PAGE",
|
||||
"DOCUMENT",
|
||||
"SECTION",
|
||||
"GROUPED_PAGES",
|
||||
]);
|
||||
|
||||
export const zMultimodalParseResolution = z.enum(["medium", "high"]);
|
||||
|
||||
export const zLlamaExtractSettings = z.object({
|
||||
max_file_size: z.number().int().optional().default(104857600),
|
||||
max_file_size_ui: z.number().int().optional().default(31457280),
|
||||
max_pages: z.number().int().optional().default(500),
|
||||
chunk_mode: zChunkMode.optional(),
|
||||
max_chunk_size: z.number().int().optional().default(10000),
|
||||
extraction_agent_config: z.object({}).optional(),
|
||||
use_multimodal_parsing: z.boolean().optional().default(false),
|
||||
use_pixel_extraction: z.boolean().optional().default(false),
|
||||
llama_parse_params: zLlamaParseParameters.optional(),
|
||||
multimodal_parse_resolution: zMultimodalParseResolution.optional(),
|
||||
});
|
||||
|
||||
export const zBodyRunJobTestUserApiV1ExtractionJobsTestPost = z.object({
|
||||
job_create: zExtractJobCreate,
|
||||
extract_settings: z.union([zLlamaExtractSettings, z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zBodyScreenshotApiParsingScreenshotPost = z.object({
|
||||
file: z.union([z.string(), z.null()]).optional(),
|
||||
do_not_cache: z.boolean().optional().default(false),
|
||||
@@ -1072,11 +963,6 @@ export const zBodyUploadFileApiV1ParsingUploadPost = z.object({
|
||||
page_footer_suffix: z.string().optional(),
|
||||
});
|
||||
|
||||
export const zBodyUploadFileV2ApiV2Alpha1ParseUploadPost = z.object({
|
||||
configuration: z.string(),
|
||||
file: z.union([z.string(), z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zBoxAuthMechanism = z.enum(["developer_token", "ccg"]);
|
||||
|
||||
export const zSupportedLlmModelNames = z.enum([
|
||||
@@ -1700,6 +1586,7 @@ export const zCustomClaims = z.object({
|
||||
allowed_classify: z.boolean().optional().default(true),
|
||||
api_datasource_access: z.boolean().optional().default(false),
|
||||
allow_org_deletion: z.boolean().optional().default(false),
|
||||
allowed_spreadsheet: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export const zCustomerPortalSessionCreatePayload = z.object({
|
||||
@@ -1855,6 +1742,16 @@ export const zDefaultOrganizationUpdate = z.object({
|
||||
organization_id: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const zDeleteRequest = z.object({
|
||||
deployment_name: z.string(),
|
||||
collection: z.string().optional().default("default"),
|
||||
filter: z.union([z.object({}), z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zDeleteResponse = z.object({
|
||||
deleted_count: z.number().int(),
|
||||
});
|
||||
|
||||
export const zRetrieverPipeline = z.object({
|
||||
name: z.union([z.string().min(1).max(3000), z.null()]),
|
||||
description: z.union([z.string().max(15000), z.null()]),
|
||||
@@ -1870,6 +1767,8 @@ export const zDirectRetrievalParams = z.object({
|
||||
pipelines: z.array(zRetrieverPipeline).optional(),
|
||||
});
|
||||
|
||||
export const zDocumentChunkMode = z.enum(["PAGE", "SECTION"]);
|
||||
|
||||
export const zDocumentIngestionJobParams = z.object({
|
||||
custom_metadata: z.union([z.object({}), z.null()]).optional(),
|
||||
resource_info: z.union([z.object({}), z.null()]).optional(),
|
||||
@@ -2122,6 +2021,74 @@ Query: {query_str}
|
||||
Answer: `),
|
||||
});
|
||||
|
||||
export const zExtractTarget = z.enum(["PER_DOC", "PER_PAGE"]);
|
||||
|
||||
export const zExtractMode = z.enum([
|
||||
"FAST",
|
||||
"BALANCED",
|
||||
"PREMIUM",
|
||||
"MULTIMODAL",
|
||||
]);
|
||||
|
||||
export const zPublicModelName = z.enum([
|
||||
"openai-gpt-4o",
|
||||
"openai-gpt-4o-mini",
|
||||
"openai-gpt-4-1",
|
||||
"openai-gpt-4-1-mini",
|
||||
"openai-gpt-4-1-nano",
|
||||
"openai-gpt-5",
|
||||
"openai-gpt-5-mini",
|
||||
"openai-gpt-5-nano",
|
||||
"openai-text-embedding-3-small",
|
||||
"openai-text-embedding-3-large",
|
||||
"openai-whisper-1",
|
||||
"anthropic-sonnet-3.5",
|
||||
"anthropic-sonnet-3.5-v2",
|
||||
"anthropic-sonnet-3.7",
|
||||
"anthropic-sonnet-4.0",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.0-flash-lite",
|
||||
"gemini-2.5-flash-lite",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-pro",
|
||||
]);
|
||||
|
||||
export const zExtractModels = z.enum([
|
||||
"openai-gpt-4-1",
|
||||
"openai-gpt-4-1-mini",
|
||||
"openai-gpt-4-1-nano",
|
||||
"openai-gpt-5",
|
||||
"openai-gpt-5-mini",
|
||||
"gemini-2.0-flash",
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-flash-lite",
|
||||
"gemini-2.5-pro",
|
||||
"openai-gpt-4o",
|
||||
"openai-gpt-4o-mini",
|
||||
]);
|
||||
|
||||
export const zExtractConfig = z.object({
|
||||
priority: z
|
||||
.union([z.enum(["low", "medium", "high", "critical"]), z.null()])
|
||||
.optional(),
|
||||
extraction_target: zExtractTarget.optional(),
|
||||
extraction_mode: zExtractMode.optional(),
|
||||
parse_model: z.union([zPublicModelName, z.null()]).optional(),
|
||||
extract_model: z.union([zExtractModels, z.null()]).optional(),
|
||||
multimodal_fast_mode: z.boolean().optional().default(false),
|
||||
system_prompt: z.union([z.string(), z.null()]).optional(),
|
||||
use_reasoning: z.boolean().optional().default(false),
|
||||
cite_sources: z.boolean().optional().default(false),
|
||||
confidence_scores: z.boolean().optional().default(false),
|
||||
chunk_mode: zDocumentChunkMode.optional(),
|
||||
high_resolution_mode: z.boolean().optional().default(false),
|
||||
invalidate_cache: z.boolean().optional().default(false),
|
||||
num_pages_context: z.union([z.number().int().gte(1), z.null()]).optional(),
|
||||
page_range: z.union([z.string(), z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zExtractAgent = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
@@ -2167,6 +2134,21 @@ export const zExtractJob = z.object({
|
||||
file: zFile,
|
||||
});
|
||||
|
||||
export const zExtractJobCreate = z.object({
|
||||
priority: z
|
||||
.union([z.enum(["low", "medium", "high", "critical"]), z.null()])
|
||||
.optional(),
|
||||
webhook_configurations: z
|
||||
.union([z.array(zWebhookConfiguration), z.null()])
|
||||
.optional(),
|
||||
extraction_agent_id: z.string().uuid(),
|
||||
file_id: z.string().uuid(),
|
||||
data_schema_override: z
|
||||
.union([z.object({}), z.string(), z.null()])
|
||||
.optional(),
|
||||
config_override: z.union([zExtractConfig, z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zExtractJobCreateBatch = z.object({
|
||||
extraction_agent_id: z.string().uuid(),
|
||||
file_ids: z.array(z.string().uuid()).min(1),
|
||||
@@ -2234,6 +2216,15 @@ export const zExtractStatelessRequest = z.object({
|
||||
file: z.union([zFileData, z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zExtractedTable = z.object({
|
||||
table_id: z.number().int(),
|
||||
sheet_name: z.string(),
|
||||
row_span: z.number().int(),
|
||||
col_span: z.number().int(),
|
||||
has_headers: z.boolean(),
|
||||
metadata_json: z.union([z.string(), z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zFileCountByStatusResponse = z.object({
|
||||
counts: z.object({}),
|
||||
total_count: z.number().int(),
|
||||
@@ -2987,6 +2978,30 @@ export const zPaginatedResponseQuotaConfiguration = z.object({
|
||||
items: z.array(zQuotaConfiguration),
|
||||
});
|
||||
|
||||
export const zSpreadsheetParsingConfig = z.object({
|
||||
sheet_names: z.union([z.array(z.string()), z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zSpreadsheetJob = z.object({
|
||||
id: z.string(),
|
||||
user_id: z.string(),
|
||||
project_id: z.string().uuid(),
|
||||
file_id: z.string().uuid(),
|
||||
config: zSpreadsheetParsingConfig,
|
||||
status: zStatusEnum,
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
success: z.union([z.boolean(), z.null()]).optional(),
|
||||
tables: z.array(zExtractedTable).optional(),
|
||||
errors: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const zPaginatedResponseSpreadsheetJob = z.object({
|
||||
items: z.array(zSpreadsheetJob),
|
||||
next_page_token: z.union([z.string(), z.null()]).optional(),
|
||||
total_size: z.union([z.number().int(), z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zParseConfiguration = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
@@ -3400,49 +3415,6 @@ export const zProjectUpdate = z.object({
|
||||
name: z.string().min(1).max(3000),
|
||||
});
|
||||
|
||||
export const zPromptConf = z.object({
|
||||
system_prompt: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(
|
||||
"Given a JSON schema, extract the data from the provided SOURCE TEXT according to the schema. Only output information that is explicitly stated or can be inferred from the SOURCE TEXT.",
|
||||
),
|
||||
extraction_prompt: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("The extracted data using the given JSON schema."),
|
||||
error_handling_prompt: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(
|
||||
"If the source text does not contain enough information to extract the value, explain the reason very briefly. Else, output null and fill out the value__ field.",
|
||||
),
|
||||
reasoning_prompt: z.string().optional().default(`
|
||||
Provide a brief explanation for how you arrived at the extracted value based on the source text provided.
|
||||
- For inferred values, explain the reasoning behind the extraction briefly.
|
||||
- For simple verbatim extraction, output 'VERBATIM EXTRACTION'.
|
||||
- When supporting data is not present in the source text, output 'INSUFFICIENT DATA' and emit blank or null values for the value__ field.
|
||||
`),
|
||||
cite_sources_prompt: z
|
||||
.object({})
|
||||
.optional()
|
||||
.default({
|
||||
description: `
|
||||
### Citation Rules (read carefully):
|
||||
- You must ANNOTATE every value with the MOST RELEVANT short EXACT substring from the source text that supports it.
|
||||
- For inferred values, cite the text used to infer it in the matching_text field or output 'INFERRED FROM TEXT'
|
||||
- If no support exists, output 'INSUFFICIENT DATA' and leave value__ null or '', 0.0, False etc depending on the type of the field.
|
||||
`,
|
||||
page: "Cite the page number of the source text that the extracted value is from. The page number is the integer that appears right after <<<PAGE:. If no page number is present in this format, use the default value of 1.",
|
||||
matching_text:
|
||||
'Cite the **MOST RELEVANT EXACT TEXT from the SOURCE TEXT** that supports the extracted value within 80 characters. If the exact substring is >80 chars, truncate with ellipsis "...". Provide only the single most relevant citation.',
|
||||
}),
|
||||
scratchpad_prompt: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("Use for intermediate step-by-step reasoning. Be concise."),
|
||||
});
|
||||
|
||||
export const zRelatedNodeInfo = z.object({
|
||||
node_id: z.string(),
|
||||
node_type: z.union([zObjectType, z.string(), z.null()]).optional(),
|
||||
@@ -3545,8 +3517,6 @@ export const zRole = z.object({
|
||||
permissions: z.array(zPermission),
|
||||
});
|
||||
|
||||
export const zSchemaRelaxMode = z.enum(["FULL", "TOP_LEVEL", "LEAF"]);
|
||||
|
||||
export const zSearchRequest = z.object({
|
||||
page_size: z.union([z.number().int(), z.null()]).optional(),
|
||||
page_token: z.union([z.string(), z.null()]).optional(),
|
||||
@@ -3558,24 +3528,9 @@ export const zSearchRequest = z.object({
|
||||
offset: z.union([z.number().int().gte(0).lte(1000), z.null()]).optional(),
|
||||
});
|
||||
|
||||
export const zStructMode = z.enum([
|
||||
"STRUCT_PARSE",
|
||||
"JSON_MODE",
|
||||
"FUNC_CALL",
|
||||
"STRUCT_RELAXED",
|
||||
"UNSTRUCTURED",
|
||||
]);
|
||||
|
||||
export const zStructParseConf = z.object({
|
||||
model: zExtractModels.optional(),
|
||||
temperature: z.number().optional().default(0),
|
||||
relaxation_mode: zSchemaRelaxMode.optional(),
|
||||
struct_mode: zStructMode.optional(),
|
||||
fetch_logprobs: z.boolean().optional().default(false),
|
||||
handle_missing: z.boolean().optional().default(false),
|
||||
use_reasoning: z.boolean().optional().default(false),
|
||||
cite_sources: z.boolean().optional().default(false),
|
||||
prompt_conf: zPromptConf.optional(),
|
||||
export const zSpreadsheetJobCreate = z.object({
|
||||
file_id: z.string().uuid(),
|
||||
config: zSpreadsheetParsingConfig.optional(),
|
||||
});
|
||||
|
||||
export const zSupportedLlmModel = z.object({
|
||||
@@ -4017,6 +3972,33 @@ export const zCreateIntentAndCustomerSessionApiV1BillingCreateIntentAndCustomerS
|
||||
export const zGetMetronomeDashboardApiV1BillingMetronomeDashboardGetResponse =
|
||||
zMetronomeDashboardResponse;
|
||||
|
||||
export const zListJobsApiV1ExtractionJobsGetResponse = z.array(zExtractJob);
|
||||
|
||||
export const zRunJobApiV1ExtractionJobsPostResponse = zExtractJob;
|
||||
|
||||
export const zGetJobApiV1ExtractionJobsJobIdGetResponse = zExtractJob;
|
||||
|
||||
export const zRunJobOnFileApiV1ExtractionJobsFilePostResponse = zExtractJob;
|
||||
|
||||
export const zRunBatchJobsApiV1ExtractionJobsBatchPostResponse =
|
||||
z.array(zExtractJob);
|
||||
|
||||
export const zGetJobResultApiV1ExtractionJobsJobIdResultGetResponse =
|
||||
zExtractResultset;
|
||||
|
||||
export const zListExtractRunsApiV1ExtractionRunsGetResponse =
|
||||
zPaginatedExtractRunsResponse;
|
||||
|
||||
export const zGetLatestRunFromUiApiV1ExtractionRunsLatestFromUiGetResponse =
|
||||
z.union([zExtractRun, z.null()]);
|
||||
|
||||
export const zGetRunByJobIdApiV1ExtractionRunsByJobJobIdGetResponse =
|
||||
zExtractRun;
|
||||
|
||||
export const zGetRunApiV1ExtractionRunsRunIdGetResponse = zExtractRun;
|
||||
|
||||
export const zExtractStatelessApiV1ExtractionRunPostResponse = zExtractJob;
|
||||
|
||||
export const zListExtractionAgentsApiV1ExtractionExtractionAgentsGetResponse =
|
||||
z.array(zExtractAgent);
|
||||
|
||||
@@ -4041,35 +4023,6 @@ export const zGetExtractionAgentApiV1ExtractionExtractionAgentsExtractionAgentId
|
||||
export const zUpdateExtractionAgentApiV1ExtractionExtractionAgentsExtractionAgentIdPutResponse =
|
||||
zExtractAgent;
|
||||
|
||||
export const zListJobsApiV1ExtractionJobsGetResponse = z.array(zExtractJob);
|
||||
|
||||
export const zRunJobApiV1ExtractionJobsPostResponse = zExtractJob;
|
||||
|
||||
export const zGetJobApiV1ExtractionJobsJobIdGetResponse = zExtractJob;
|
||||
|
||||
export const zRunJobTestUserApiV1ExtractionJobsTestPostResponse = zExtractJob;
|
||||
|
||||
export const zRunJobOnFileApiV1ExtractionJobsFilePostResponse = zExtractJob;
|
||||
|
||||
export const zRunBatchJobsApiV1ExtractionJobsBatchPostResponse =
|
||||
z.array(zExtractJob);
|
||||
|
||||
export const zGetJobResultApiV1ExtractionJobsJobIdResultGetResponse =
|
||||
zExtractResultset;
|
||||
|
||||
export const zListExtractRunsApiV1ExtractionRunsGetResponse =
|
||||
zPaginatedExtractRunsResponse;
|
||||
|
||||
export const zGetLatestRunFromUiApiV1ExtractionRunsLatestFromUiGetResponse =
|
||||
z.union([zExtractRun, z.null()]);
|
||||
|
||||
export const zGetRunByJobIdApiV1ExtractionRunsByJobJobIdGetResponse =
|
||||
zExtractRun;
|
||||
|
||||
export const zGetRunApiV1ExtractionRunsRunIdGetResponse = zExtractRun;
|
||||
|
||||
export const zExtractStatelessApiV1ExtractionRunPostResponse = zExtractJob;
|
||||
|
||||
export const zListApiKeysApiV1BetaApiKeysGetResponse = zApiKeyQueryResponse;
|
||||
|
||||
export const zCreateApiKeyApiV1BetaApiKeysPostResponse = zApiKey;
|
||||
@@ -4100,6 +4053,9 @@ export const zSearchAgentDataApiV1BetaAgentDataSearchPostResponse =
|
||||
export const zAggregateAgentDataApiV1BetaAgentDataAggregatePostResponse =
|
||||
zPaginatedResponseAggregateGroup;
|
||||
|
||||
export const zDeleteAgentDataByQueryApiV1BetaAgentDataDeletePostResponse =
|
||||
zDeleteResponse;
|
||||
|
||||
export const zListQuotaConfigurationsApiV1BetaQuotaManagementGetResponse =
|
||||
zPaginatedResponseQuotaConfiguration;
|
||||
|
||||
@@ -4135,6 +4091,18 @@ export const zQueryParseConfigurationsApiV1BetaParseConfigurationsQueryPostRespo
|
||||
export const zGetLatestParseConfigurationApiV1BetaParseConfigurationsLatestGetResponse =
|
||||
z.union([zParseConfiguration, z.null()]);
|
||||
|
||||
export const zListSpreadsheetJobsApiV1BetaSpreadsheetJobsGetResponse =
|
||||
zPaginatedResponseSpreadsheetJob;
|
||||
|
||||
export const zCreateSpreadsheetJobApiV1BetaSpreadsheetJobsPostResponse =
|
||||
zSpreadsheetJob;
|
||||
|
||||
export const zGetSpreadsheetJobApiV1BetaSpreadsheetJobsSpreadsheetJobIdGetResponse =
|
||||
zSpreadsheetJob;
|
||||
|
||||
export const zGetTableDownloadPresignedUrlApiV1BetaSpreadsheetJobsSpreadsheetJobIdTablesTableIdResultGetResponse =
|
||||
zPresignedUrl;
|
||||
|
||||
export const zUploadFileV2ApiV2Alpha1ParseUploadPostResponse = zParsingJob;
|
||||
|
||||
export const zGetSupportedFileExtensionsApiParsingSupportedFileExtensionsGetResponse =
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { emitWarning } from "process";
|
||||
import fs from "fs/promises";
|
||||
import { Blob } from "buffer";
|
||||
import * as path from "path";
|
||||
import type { ExtractResult } from "./type";
|
||||
import { randomUUID } from "@llamaindex/env";
|
||||
import { File } from "buffer";
|
||||
import {
|
||||
type Options,
|
||||
@@ -19,7 +15,6 @@ import {
|
||||
type GetJobApiV1ExtractionJobsJobIdGetData,
|
||||
type GetJobResultApiV1ExtractionJobsJobIdResultGetData,
|
||||
StatusEnum,
|
||||
type UploadFileApiV1FilesPostData,
|
||||
type StatelessExtractionRequest,
|
||||
type ExtractStatelessApiV1ExtractionRunPostData,
|
||||
type DeleteExtractionAgentApiV1ExtractionExtractionAgentsExtractionAgentIdDeleteData,
|
||||
@@ -29,17 +24,12 @@ import {
|
||||
runJobApiV1ExtractionJobsPost,
|
||||
getJobApiV1ExtractionJobsJobIdGet,
|
||||
getJobResultApiV1ExtractionJobsJobIdResultGet,
|
||||
uploadFileApiV1FilesPost,
|
||||
extractStatelessApiV1ExtractionRunPost,
|
||||
deleteExtractionAgentApiV1ExtractionExtractionAgentsExtractionAgentIdDelete,
|
||||
} from "./api";
|
||||
import type { Client } from "@hey-api/client-fetch";
|
||||
import { sleep } from "./utils";
|
||||
import { fileTypeFromBuffer } from "file-type";
|
||||
|
||||
type BodyUploadFileApiV1FilesPost = {
|
||||
upload_file: Blob | File;
|
||||
};
|
||||
import { uploadFile } from "./fileUpload";
|
||||
|
||||
export async function createAgent(
|
||||
name: string,
|
||||
@@ -221,95 +211,6 @@ export async function getAgent(
|
||||
}
|
||||
}
|
||||
|
||||
function textToFile(text: string, fileName: string | null = null) {
|
||||
return new File(
|
||||
[text],
|
||||
fileName ?? "uploadedFile_" + randomUUID().replaceAll("-", "_") + ".txt",
|
||||
);
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
filePath: string | undefined = undefined,
|
||||
fileContent:
|
||||
| Buffer<ArrayBufferLike>
|
||||
| File
|
||||
| Uint8Array<ArrayBuffer>
|
||||
| string
|
||||
| undefined = undefined,
|
||||
fileName: string | undefined = undefined,
|
||||
project_id: string | null = null,
|
||||
organization_id: string | null = null,
|
||||
client: Client | undefined = undefined,
|
||||
maxRetriesOnError: number = 10,
|
||||
retryInterval: number = 0.5,
|
||||
): Promise<string | undefined> {
|
||||
let file: File | undefined = undefined;
|
||||
if (typeof filePath === "undefined" && typeof fileContent === "undefined") {
|
||||
throw new Error(
|
||||
"One between filePath and fileContent needs to be provided",
|
||||
);
|
||||
} else if (typeof filePath != "undefined") {
|
||||
const buffer = await fs.readFile(filePath);
|
||||
const actualFileName = fileName ?? path.basename(filePath);
|
||||
const uint8Array = new Uint8Array(buffer);
|
||||
file = new File([uint8Array], actualFileName);
|
||||
} else if (typeof fileContent != "undefined") {
|
||||
if (fileContent instanceof File) {
|
||||
file = fileContent;
|
||||
} else if (fileContent instanceof Buffer) {
|
||||
const fileType = await fileTypeFromBuffer(fileContent);
|
||||
const ext = fileType?.ext ?? "pdf";
|
||||
const uint8Array = new Uint8Array(fileContent);
|
||||
file = new File(
|
||||
[uint8Array],
|
||||
fileName ??
|
||||
"uploadedFile_" + randomUUID().replaceAll("-", "_") + "." + ext,
|
||||
);
|
||||
} else if (fileContent instanceof Uint8Array) {
|
||||
const fileType = await fileTypeFromBuffer(fileContent);
|
||||
const ext = fileType?.ext ?? "pdf";
|
||||
file = new File(
|
||||
[fileContent],
|
||||
fileName ??
|
||||
"uploadedFile_" + randomUUID().replaceAll("-", "_") + "." + ext,
|
||||
);
|
||||
} else if (typeof fileContent === "string") {
|
||||
file = textToFile(fileContent, fileName);
|
||||
} else {
|
||||
throw new Error("Unsupported fileContent type");
|
||||
}
|
||||
}
|
||||
const fileToUpload = {
|
||||
upload_file: file,
|
||||
} as BodyUploadFileApiV1FilesPost;
|
||||
const uploadData = {
|
||||
body: fileToUpload,
|
||||
query: { organization_id: organization_id, project_id: project_id },
|
||||
} as UploadFileApiV1FilesPostData;
|
||||
const uploadOptions = uploadData as Options<UploadFileApiV1FilesPostData>;
|
||||
if (typeof client != "undefined") {
|
||||
uploadOptions.client = client;
|
||||
}
|
||||
let retries: number = 0;
|
||||
while (true) {
|
||||
if (retries > maxRetriesOnError) {
|
||||
throw new Error(
|
||||
"Error while processing your file: Exceeded maximum number of retries, the API keeps returning errors.",
|
||||
);
|
||||
}
|
||||
const uploadResponse = await uploadFileApiV1FilesPost(uploadOptions);
|
||||
let fileId: string | undefined = undefined;
|
||||
if (!uploadResponse.response.ok) {
|
||||
retries++;
|
||||
await sleep(retryInterval * 1000);
|
||||
}
|
||||
if (typeof uploadResponse.data != "undefined") {
|
||||
fileId = uploadResponse.data.id as string;
|
||||
return fileId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createExtractJob(
|
||||
options:
|
||||
| Options<RunJobApiV1ExtractionJobsPostData>
|
||||
@@ -477,16 +378,16 @@ export async function extract(
|
||||
maxRetriesOnError: number = 10,
|
||||
retryInterval: number = 0.5,
|
||||
): Promise<ExtractResult | undefined> {
|
||||
const fileId = (await uploadFile(
|
||||
const fileId = (await uploadFile({
|
||||
filePath,
|
||||
fileContent,
|
||||
fileName,
|
||||
project_id,
|
||||
organization_id,
|
||||
project_id: project_id ?? undefined,
|
||||
organization_id: organization_id ?? undefined,
|
||||
client,
|
||||
maxRetriesOnError,
|
||||
retryInterval,
|
||||
)) as string;
|
||||
})) as string;
|
||||
const extractJobCreate = {
|
||||
extraction_agent_id: agentId,
|
||||
file_id: fileId,
|
||||
@@ -556,16 +457,16 @@ export async function extractStateless(
|
||||
maxRetriesOnError: number = 10,
|
||||
retryInterval: number = 0.5,
|
||||
): Promise<ExtractResult | undefined> {
|
||||
const fileId = (await uploadFile(
|
||||
const fileId = (await uploadFile({
|
||||
filePath,
|
||||
fileContent,
|
||||
fileName,
|
||||
project_id,
|
||||
organization_id,
|
||||
project_id: project_id ?? undefined,
|
||||
organization_id: organization_id ?? undefined,
|
||||
client,
|
||||
maxRetriesOnError,
|
||||
retryInterval,
|
||||
)) as string;
|
||||
})) as string;
|
||||
const extractStatetelessCreate = {
|
||||
data_schema: dataSchema,
|
||||
file_id: fileId,
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import fs from "fs/promises";
|
||||
import { Blob } from "buffer";
|
||||
import * as path from "path";
|
||||
import { randomUUID } from "@llamaindex/env";
|
||||
import { File } from "buffer";
|
||||
import {
|
||||
type Options,
|
||||
type UploadFileApiV1FilesPostData,
|
||||
uploadFileApiV1FilesPost,
|
||||
} from "./api";
|
||||
import type { Client } from "@hey-api/client-fetch";
|
||||
import { sleep } from "./utils";
|
||||
import { fileTypeFromBuffer } from "file-type";
|
||||
|
||||
type BodyUploadFileApiV1FilesPost = {
|
||||
upload_file: Blob | File;
|
||||
};
|
||||
|
||||
function textToFile(text: string, fileName: string | null = null) {
|
||||
return new File(
|
||||
[text],
|
||||
fileName ?? "uploadedFile_" + randomUUID().replaceAll("-", "_") + ".txt",
|
||||
);
|
||||
}
|
||||
|
||||
export async function uploadFile({
|
||||
filePath,
|
||||
fileContent,
|
||||
fileName,
|
||||
project_id,
|
||||
organization_id,
|
||||
client,
|
||||
maxRetriesOnError = 10,
|
||||
retryInterval = 0.5,
|
||||
}: {
|
||||
filePath?: string | undefined;
|
||||
fileContent?:
|
||||
| Buffer<ArrayBufferLike>
|
||||
| File
|
||||
| Uint8Array<ArrayBuffer>
|
||||
| string
|
||||
| undefined;
|
||||
fileName?: string | undefined;
|
||||
project_id?: string | undefined;
|
||||
organization_id?: string | undefined;
|
||||
client?: Client | undefined;
|
||||
maxRetriesOnError?: number;
|
||||
retryInterval?: number;
|
||||
}): Promise<string | undefined> {
|
||||
let file: File | undefined = undefined;
|
||||
if (typeof filePath === "undefined" && typeof fileContent === "undefined") {
|
||||
throw new Error(
|
||||
"One between filePath and fileContent needs to be provided",
|
||||
);
|
||||
} else if (typeof filePath != "undefined") {
|
||||
const buffer = await fs.readFile(filePath);
|
||||
const actualFileName = fileName ?? path.basename(filePath);
|
||||
const uint8Array = new Uint8Array(buffer);
|
||||
file = new File([uint8Array], actualFileName);
|
||||
} else if (typeof fileContent != "undefined") {
|
||||
if (fileContent instanceof File) {
|
||||
file = fileContent;
|
||||
} else if (fileContent instanceof Buffer) {
|
||||
const fileType = await fileTypeFromBuffer(fileContent);
|
||||
const ext = fileType?.ext ?? "pdf";
|
||||
const uint8Array = new Uint8Array(fileContent);
|
||||
file = new File(
|
||||
[uint8Array],
|
||||
fileName ??
|
||||
"uploadedFile_" + randomUUID().replaceAll("-", "_") + "." + ext,
|
||||
);
|
||||
} else if (fileContent instanceof Uint8Array) {
|
||||
const fileType = await fileTypeFromBuffer(fileContent);
|
||||
const ext = fileType?.ext ?? "pdf";
|
||||
file = new File(
|
||||
[fileContent],
|
||||
fileName ??
|
||||
"uploadedFile_" + randomUUID().replaceAll("-", "_") + "." + ext,
|
||||
);
|
||||
} else if (typeof fileContent === "string") {
|
||||
file = textToFile(fileContent, fileName);
|
||||
} else {
|
||||
throw new Error("Unsupported fileContent type");
|
||||
}
|
||||
}
|
||||
const fileToUpload = {
|
||||
upload_file: file,
|
||||
} as BodyUploadFileApiV1FilesPost;
|
||||
const uploadData = {
|
||||
body: fileToUpload,
|
||||
query: { project_id: project_id, organization_id: organization_id },
|
||||
} as UploadFileApiV1FilesPostData;
|
||||
const uploadOptions = uploadData as Options<UploadFileApiV1FilesPostData>;
|
||||
if (typeof client != "undefined") {
|
||||
uploadOptions.client = client;
|
||||
}
|
||||
let retries: number = 0;
|
||||
while (true) {
|
||||
if (retries > maxRetriesOnError) {
|
||||
throw new Error(
|
||||
"Error while processing your file: Exceeded maximum number of retries, the API keeps returning errors.",
|
||||
);
|
||||
}
|
||||
const uploadResponse = await uploadFileApiV1FilesPost(uploadOptions);
|
||||
let fileId: string | undefined = undefined;
|
||||
if (!uploadResponse.response.ok) {
|
||||
const error = await uploadResponse.response.text();
|
||||
console.error("Error while uploading file: ", error);
|
||||
retries++;
|
||||
await sleep(retryInterval * 1000);
|
||||
}
|
||||
if (
|
||||
uploadResponse.response.ok &&
|
||||
typeof uploadResponse.data != "undefined"
|
||||
) {
|
||||
fileId = uploadResponse.data.id as string;
|
||||
return fileId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,9 @@ export type { CloudConstructorParams } from "./type.js";
|
||||
export { LlamaParseReader } from "./reader.js";
|
||||
export { LlamaExtract, LlamaExtractAgent } from "./LlamaExtract.js";
|
||||
export type { ExtractConfig } from "./extract.js";
|
||||
export { LlamaClassify } from "./LlamaClassify.js";
|
||||
export type {
|
||||
ClassifierRule,
|
||||
ClassifyJobResults,
|
||||
ClassifyParsingConfiguration,
|
||||
} from "./classify.js";
|
||||
|
||||
@@ -117,3 +117,25 @@ export function getSavePath(downloadPath: string, i: number): string {
|
||||
|
||||
return savePath;
|
||||
}
|
||||
|
||||
const URLS = {
|
||||
us: "https://api.cloud.llamaindex.ai",
|
||||
eu: "https://api.cloud.eu.llamaindex.ai",
|
||||
"us-staging": "https://api.staging.llamaindex.ai",
|
||||
} as const;
|
||||
|
||||
export function getUrl(
|
||||
baseUrl: string | undefined,
|
||||
region: string | undefined,
|
||||
) {
|
||||
if (typeof baseUrl != "undefined") {
|
||||
return baseUrl;
|
||||
}
|
||||
if (typeof region === "undefined") {
|
||||
return URLS["us"];
|
||||
} else if (region === "us" || region === "eu" || region === "us-staging") {
|
||||
return URLS[region];
|
||||
} else {
|
||||
throw new Error(`Unsupported region: ${region}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { AgentClient, createAgentDataClient } from "../src/beta/agent/index.js";
|
||||
import * as sdk from "../src/client/index.js";
|
||||
|
||||
describe("AgentClient", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("createItem sends correct payload and returns typed data", async () => {
|
||||
const spy = vi
|
||||
.spyOn(sdk, "createAgentDataApiV1BetaAgentDataPost")
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
id: "1",
|
||||
deployment_name: "dep",
|
||||
collection: "col",
|
||||
data: { foo: "bar" },
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
},
|
||||
} as any);
|
||||
|
||||
const client = new AgentClient<{ foo: string }>({
|
||||
deploymentName: "dep",
|
||||
collection: "col",
|
||||
});
|
||||
const result = await client.createItem({ foo: "bar" });
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
const call = spy.mock.calls[0][0];
|
||||
expect(call.body.deployment_name).toBe("dep");
|
||||
expect(call.body.collection).toBe("col");
|
||||
expect(call.body.data).toEqual({ foo: "bar" });
|
||||
|
||||
expect(result.id).toBe("1");
|
||||
expect(result.deploymentName).toBe("dep");
|
||||
expect(result.collection).toBe("col");
|
||||
expect(result.data).toEqual({ foo: "bar" });
|
||||
expect(result.createdAt).toEqual(new Date("2024-01-01T00:00:00Z"));
|
||||
expect(result.updatedAt).toEqual(new Date("2024-01-01T00:00:00Z"));
|
||||
});
|
||||
|
||||
it("getItem returns null for 404 errors", async () => {
|
||||
const spy = vi
|
||||
.spyOn(sdk, "getAgentDataApiV1BetaAgentDataItemIdGet")
|
||||
.mockImplementation(async () => {
|
||||
const err: any = new Error("Not found");
|
||||
err.response = { status: 404 };
|
||||
throw err;
|
||||
});
|
||||
|
||||
const client = new AgentClient({ deploymentName: "dep" });
|
||||
const res = await client.getItem("missing-id");
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
|
||||
it("updateItem updates and returns typed data", async () => {
|
||||
const spy = vi
|
||||
.spyOn(sdk, "updateAgentDataApiV1BetaAgentDataItemIdPut")
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
id: "123",
|
||||
deployment_name: "dep",
|
||||
collection: "col",
|
||||
data: { foo: "baz" },
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-02T00:00:00Z",
|
||||
},
|
||||
} as any);
|
||||
|
||||
const client = new AgentClient<{ foo: string }>({
|
||||
deploymentName: "dep",
|
||||
collection: "col",
|
||||
});
|
||||
const res = await client.updateItem("123", { foo: "baz" });
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
const call = spy.mock.calls[0][0];
|
||||
expect(call.path.item_id).toBe("123");
|
||||
expect(call.body.data).toEqual({ foo: "baz" });
|
||||
|
||||
expect(res.id).toBe("123");
|
||||
expect(res.updatedAt).toEqual(new Date("2024-01-02T00:00:00Z"));
|
||||
});
|
||||
|
||||
it("deleteItem calls delete endpoint with correct path", async () => {
|
||||
const spy = vi
|
||||
.spyOn(sdk, "deleteAgentDataApiV1BetaAgentDataItemIdDelete")
|
||||
.mockResolvedValue({} as any);
|
||||
|
||||
const client = new AgentClient({ deploymentName: "dep" });
|
||||
await client.deleteItem("abc");
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
expect(spy.mock.calls[0][0].path.item_id).toBe("abc");
|
||||
});
|
||||
|
||||
it("delete by query returns deleted count", async () => {
|
||||
const spy = vi
|
||||
.spyOn(sdk, "deleteAgentDataByQueryApiV1BetaAgentDataDeletePost")
|
||||
.mockResolvedValue({ data: { deleted_count: 7 } } as any);
|
||||
|
||||
const client = new AgentClient({
|
||||
deploymentName: "dep",
|
||||
collection: "col",
|
||||
});
|
||||
const count = await client.delete({
|
||||
filter: { status: { op: "eq", value: "accepted" } as any },
|
||||
});
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
const body = spy.mock.calls[0][0].body;
|
||||
expect(body.deployment_name).toBe("dep");
|
||||
expect(body.collection).toBe("col");
|
||||
expect(count).toBe(7);
|
||||
});
|
||||
|
||||
it("search maps items and optional fields correctly", async () => {
|
||||
const now = "2024-01-01T00:00:00Z";
|
||||
const spy = vi
|
||||
.spyOn(sdk, "searchAgentDataApiV1BetaAgentDataSearchPost")
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
id: "1",
|
||||
deployment_name: "dep",
|
||||
collection: "col",
|
||||
data: { foo: "bar" },
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
],
|
||||
total_size: 1,
|
||||
next_page_token: "next",
|
||||
},
|
||||
} as any);
|
||||
|
||||
const client = new AgentClient<{ foo: string }>({
|
||||
deploymentName: "dep",
|
||||
collection: "col",
|
||||
});
|
||||
const result = await client.search({
|
||||
includeTotal: true,
|
||||
orderBy: "created_at desc",
|
||||
pageSize: 1,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
const body = spy.mock.calls[0][0].body;
|
||||
expect(body.deployment_name).toBe("dep");
|
||||
expect(body.collection).toBe("col");
|
||||
expect(body.include_total).toBe(true);
|
||||
expect(body.order_by).toBe("created_at desc");
|
||||
expect(body.page_size).toBe(1);
|
||||
expect(body.offset).toBe(0);
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.totalSize).toBe(1);
|
||||
expect(result.nextPageToken).toBe("next");
|
||||
expect(result.items[0].createdAt).toEqual(new Date(now));
|
||||
});
|
||||
|
||||
it("aggregate maps groups and optional fields correctly", async () => {
|
||||
const spy = vi
|
||||
.spyOn(sdk, "aggregateAgentDataApiV1BetaAgentDataAggregatePost")
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
group_key: { status: "accepted" },
|
||||
count: 3,
|
||||
first_item: { foo: "bar" },
|
||||
},
|
||||
],
|
||||
total_size: 1,
|
||||
next_page_token: "tok",
|
||||
},
|
||||
} as any);
|
||||
|
||||
const client = new AgentClient<{ foo: string }>({
|
||||
deploymentName: "dep",
|
||||
collection: "col",
|
||||
});
|
||||
const result = await client.aggregate({
|
||||
groupBy: ["status"],
|
||||
count: true,
|
||||
first: true,
|
||||
pageSize: 1,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
const body = spy.mock.calls[0][0].body;
|
||||
expect(body.deployment_name).toBe("dep");
|
||||
expect(body.collection).toBe("col");
|
||||
expect(body.group_by).toEqual(["status"]);
|
||||
expect(body.count).toBe(true);
|
||||
expect(body.first).toBe(true);
|
||||
expect(body.page_size).toBe(1);
|
||||
expect(body.offset).toBe(0);
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.totalSize).toBe(1);
|
||||
expect(result.nextPageToken).toBe("tok");
|
||||
expect(result.items[0].groupKey).toEqual({ status: "accepted" });
|
||||
expect(result.items[0].count).toBe(3);
|
||||
expect(result.items[0].firstItem).toEqual({ foo: "bar" });
|
||||
});
|
||||
|
||||
it("createAgentDataClient infers deployment name from env", async () => {
|
||||
const spy = vi
|
||||
.spyOn(sdk, "searchAgentDataApiV1BetaAgentDataSearchPost")
|
||||
.mockResolvedValue({
|
||||
data: { items: [], total_size: 0 },
|
||||
} as any);
|
||||
|
||||
const client = createAgentDataClient({
|
||||
env: { LLAMA_DEPLOY_DEPLOYMENT_NAME: "env-dep" },
|
||||
});
|
||||
await client.search({});
|
||||
|
||||
const body = spy.mock.calls[0][0].body;
|
||||
expect(body.deployment_name).toBe("env-dep");
|
||||
});
|
||||
|
||||
it("createAgentDataClient infers deployment name from windowUrl (non-local)", async () => {
|
||||
const spy = vi
|
||||
.spyOn(sdk, "deleteAgentDataByQueryApiV1BetaAgentDataDeletePost")
|
||||
.mockResolvedValue({
|
||||
data: { deleted_count: 0 },
|
||||
} as any);
|
||||
|
||||
const client = createAgentDataClient({
|
||||
windowUrl: "https://app.llamaindex.ai/deployments/abc/ui/",
|
||||
});
|
||||
await client.delete({});
|
||||
|
||||
const body = spy.mock.calls[0][0].body;
|
||||
expect(body.deployment_name).toBe("abc");
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,11 @@ import { describe, it, expect, beforeEach, beforeAll } from "vitest";
|
||||
import { LlamaParseReader } from "../src/reader.js";
|
||||
import { LlamaCloudIndex } from "../src/LlamaCloudIndex.js";
|
||||
import { LlamaExtract, LlamaExtractAgent } from "../src/LlamaExtract.js";
|
||||
import { LlamaClassify } from "../src/LlamaClassify.js";
|
||||
import {
|
||||
ClassifierRule,
|
||||
ClassifyParsingConfiguration,
|
||||
} from "../src/classify.js";
|
||||
import { Document } from "@llamaindex/core/schema";
|
||||
import { fs } from "@llamaindex/env";
|
||||
import { ExtractConfig } from "../src/api.js";
|
||||
@@ -489,6 +494,65 @@ describe("Integration Tests", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("LlamaClassify Integration", () => {
|
||||
it.skipIf(skipIfNoApiKey)(
|
||||
"should classify data correctly (file paths and file contents) ",
|
||||
async () => {
|
||||
const classifyClient = new LlamaClassify(
|
||||
process.env.LLAMA_CLOUD_API_KEY!,
|
||||
"https://api.cloud.llamaindex.ai",
|
||||
);
|
||||
const testContent = `A Fox one day spied a beautiful bunch of ripe grapes hanging from a vine trained along the branches of a tree. The grapes seemed ready to burst with juice, and the Fox's mouth watered as he gazed longingly at them. The bunch hung from a high branch, and the Fox had to jump for it. The first time he jumped he missed it by a long way. So he walked off a short distance and took a running leap at it, only to fall short once more. Again and again he tried, but in vain. Now he sat down and looked at the grapes in disgust. "What a fool I am," he said. "Here I am wearing myself out to get a bunch of sour grapes that are not worth gaping for." And off he walked very, very scornfully.There are many who pretend to despise and belittle that which is beyond their reach.`;
|
||||
const testFilePath = "the_fox_and_the_grapes.md";
|
||||
|
||||
await fs.writeFile(testFilePath, new TextEncoder().encode(testContent));
|
||||
|
||||
const rules: ClassifierRule[] = [
|
||||
{
|
||||
type: "fable",
|
||||
description:
|
||||
"A short story featuring animals whose aim is to teach the reader a lesson (the moral of the story)",
|
||||
},
|
||||
{
|
||||
type: "fairy_tale",
|
||||
description:
|
||||
"A mid-to-long story featuring humans, magic creatures and other characters, whose main aim is to entertain the readers.",
|
||||
},
|
||||
];
|
||||
|
||||
const parsingConfig: ClassifyParsingConfiguration = { lang: "en" };
|
||||
|
||||
const result = await classifyClient.classify(rules, parsingConfig, {
|
||||
filePaths: ["the_fox_and_the_grapes.md"],
|
||||
});
|
||||
expect("items" in result).toBeTruthy();
|
||||
expect(result.items.length).toBeGreaterThan(0);
|
||||
expect("result" in result.items[0]).toBeTruthy();
|
||||
expect(result.items[0].result!.type === "fable").toBeTruthy();
|
||||
|
||||
const buffer = await fs.readFile("the_fox_and_the_grapes.md");
|
||||
const resultBuffer = await classifyClient.classify(
|
||||
rules,
|
||||
parsingConfig,
|
||||
{ fileContents: [buffer] },
|
||||
);
|
||||
expect("items" in resultBuffer).toBeTruthy();
|
||||
expect(resultBuffer.items.length).toBeGreaterThan(0);
|
||||
expect("result" in resultBuffer.items[0]).toBeTruthy();
|
||||
expect(resultBuffer.items[0].result!.type === "fable").toBeTruthy();
|
||||
|
||||
try {
|
||||
await fs.unlink("the_fox_and_the_grapes.md");
|
||||
} catch (err) {
|
||||
console.log(
|
||||
`Unable to delete file the_fox_and_the_grapes.md because of ${err}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
60000,
|
||||
);
|
||||
});
|
||||
|
||||
describe("LlamaExtract Integration", () => {
|
||||
it.skipIf(skipIfNoApiKey)(
|
||||
"should create agents correctly",
|
||||
|
||||