Compare commits

..

4 Commits

Author SHA1 Message Date
Jerry Liu 68ea59b623 cr 2025-10-18 11:27:39 -07:00
Jerry Liu 41050ae084 cr 2025-10-17 23:54:35 -07:00
Jerry Liu 02009cb249 cr 2025-10-17 23:40:30 -07:00
Jerry Liu 117af53323 cr 2025-10-17 23:12:36 -07:00
51 changed files with 262 additions and 1849 deletions
-21
View File
@@ -1,21 +0,0 @@
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
-4
View File
@@ -1,4 +0,0 @@
**/build
**/public
pnpm-lock.yaml
routeTree.gen.ts
-88
View File
@@ -1,88 +0,0 @@
# 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
-34
View File
@@ -1,34 +0,0 @@
{
"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": "^0.3.10",
"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"
}
}
-5
View File
@@ -1,5 +0,0 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

@@ -1,19 +0,0 @@
{
"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"
}
@@ -1,53 +0,0 @@
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>
)
}
@@ -1,25 +0,0 @@
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>
)
}
-225
View File
@@ -1,225 +0,0 @@
/* 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>>
}
}
-15
View File
@@ -1,15 +0,0 @@
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
}
-128
View File
@@ -1,128 +0,0 @@
/// <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>
)
}
@@ -1,45 +0,0 @@
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,
[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
}
-99
View File
@@ -1,99 +0,0 @@
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>
)
}
@@ -1,23 +0,0 @@
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,
}
-33
View File
@@ -1,33 +0,0 @@
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
}
-22
View File
@@ -1,22 +0,0 @@
{
"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
}
}
-19
View File
@@ -1,19 +0,0 @@
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(),
],
})
-6
View File
@@ -1,11 +1,5 @@
# llama-cloud-services-py
## 0.6.77
### Patch Changes
- 407292b: Now return partial results on job failure
## 0.6.76
### Patch Changes
+1 -3
View File
@@ -1,6 +1,5 @@
from llama_cloud_services.parse import LlamaParse
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent
from llama_cloud_services.utils import SourceText, FileInput
from llama_cloud_services.extract import LlamaExtract, ExtractionAgent, SourceText
from llama_cloud_services.constants import EU_BASE_URL
from llama_cloud_services.index import (
LlamaCloudCompositeRetriever,
@@ -13,7 +12,6 @@ __all__ = [
"LlamaExtract",
"ExtractionAgent",
"SourceText",
"FileInput",
"EU_BASE_URL",
"LlamaCloudIndex",
"LlamaCloudRetriever",
@@ -1,10 +0,0 @@
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",
]
+27 -145
View File
@@ -1,7 +1,6 @@
import asyncio
import time
import warnings
from typing import Optional, List, Union
from typing import Optional
from pydantic import BaseModel
from llama_cloud.client import AsyncLlamaCloud
from llama_cloud.types import (
@@ -15,11 +14,7 @@ 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,
FileInput,
)
from llama_cloud_services.utils import is_terminal_status, augment_async_errors
from llama_index.core.async_utils import DEFAULT_NUM_WORKERS, run_jobs
from llama_cloud_services.beta.classifier.types import (
ClassifyJobResultsWithFiles,
@@ -171,98 +166,6 @@ 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],
@@ -270,17 +173,11 @@ class ClassifyClient:
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
raise_on_error: bool = True,
) -> ClassifyJobResultsWithFiles:
"""
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
file = await self.file_client.upload_file(file_input_path)
results = await self.aclassify_file_ids(
rules, [file.id], parsing_configuration, raise_on_error
)
return ClassifyJobResultsWithFiles.from_classify_job_results(results, [file])
def classify_file_path(
self,
@@ -289,17 +186,12 @@ class ClassifyClient:
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
raise_on_error: bool = True,
) -> ClassifyJobResultsWithFiles:
"""
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
)
with augment_async_errors():
return asyncio.run(
self.aclassify_file_path(
rules, file_input_path, parsing_configuration, raise_on_error
)
)
async def aclassify_file_paths(
self,
@@ -310,22 +202,17 @@ class ClassifyClient:
workers: int = DEFAULT_NUM_WORKERS,
show_progress: bool = False,
) -> ClassifyJobResultsWithFiles:
"""
Deprecated: Use aclassify() instead.
"""
warnings.warn(
"aclassify_file_paths is deprecated, use aclassify() instead",
DeprecationWarning,
stacklevel=2,
coroutines = [self.file_client.upload_file(path) for path in file_input_paths]
files: list[File] = await run_jobs(
coroutines,
show_progress=show_progress,
workers=workers,
desc="Uploading files for classification",
)
return await self.aclassify(
rules,
file_input_paths,
parsing_configuration,
raise_on_error,
workers,
show_progress,
results = await self.aclassify_file_ids(
rules, [file.id for file in files], parsing_configuration, raise_on_error
)
return ClassifyJobResultsWithFiles.from_classify_job_results(results, files)
def classify_file_paths(
self,
@@ -334,17 +221,12 @@ class ClassifyClient:
parsing_configuration: Optional[ClassifyParsingConfiguration] = None,
raise_on_error: bool = True,
) -> ClassifyJobResultsWithFiles:
"""
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
)
with augment_async_errors():
return asyncio.run(
self.aclassify_file_paths(
rules, file_input_paths, parsing_configuration, raise_on_error
)
)
async def wait_for_job_completion(self, job_id: str) -> ClassifyJob:
"""
+1 -2
View File
@@ -2,16 +2,15 @@ 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",
+100 -7
View File
@@ -2,9 +2,10 @@ import asyncio
import base64
import os
import time
from io import BufferedIOBase, TextIOWrapper
from io import BufferedIOBase, BufferedReader, BytesIO, 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
@@ -32,8 +33,7 @@ from llama_cloud_services.extract.utils import (
JSONObjectType,
ExperimentalWarning,
)
from llama_cloud_services.utils import augment_async_errors, SourceText, FileInput
from llama_cloud_services.files.client import FileClient
from llama_cloud_services.utils import augment_async_errors
from llama_index.core.schema import BaseComponent
from llama_index.core.async_utils import run_jobs
from llama_index.core.bridge.pydantic import Field, PrivateAttr
@@ -188,6 +188,46 @@ 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,
@@ -280,7 +320,6 @@ 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:
@@ -330,11 +369,65 @@ class ExtractionAgent:
ValueError: If filename is not provided for bytes input or for file-like objects
without a name attribute.
"""
return await self._file_client.upload_content(file_input)
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()
async def _upload_file(self, file_input: FileInput) -> File:
"""Upload a file from various input types using FileClient."""
return await self._file_client.upload_content(file_input)
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)
async def _wait_for_job_result(self, job_id: str) -> Optional[ExtractRun]:
"""Wait for and return the results of an extraction job."""
-82
View File
@@ -1,11 +1,9 @@
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:
@@ -97,83 +95,3 @@ 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."
)
+3 -33
View File
@@ -1146,25 +1146,6 @@ 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,
@@ -1206,7 +1187,7 @@ class LlamaParse(BasePydanticReader):
)
if self.verbose:
print("Started parsing the file under job_id %s" % job_id)
result = await self._get_job_result_with_error_handling(
result = await self._get_job_result(
job_id, result_type or self.result_type.value, verbose=self.verbose
)
return job_id, result
@@ -1269,15 +1250,6 @@ 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
@@ -1803,7 +1775,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_with_error_handling(
result = await self._get_job_result(
job_id, ResultType.JSON.value, verbose=self.verbose
)
return JobResult(
@@ -1818,9 +1790,7 @@ class LlamaParse(BasePydanticReader):
elif isinstance(job_id, list):
results = []
jobs = [
self._get_job_result_with_error_handling(
id_, ResultType.JSON.value, verbose=self.verbose
)
self._get_job_result(id_, ResultType.JSON.value, verbose=self.verbose)
for id_ in job_id
]
results = await run_jobs(
-7
View File
@@ -269,13 +269,6 @@ class JobResult(SafeBaseModel):
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,
+2 -102
View File
@@ -3,14 +3,11 @@ import importlib.metadata
from contextlib import contextmanager
from typing import Generator
import difflib
from llama_cloud.types import StatusEnum, File
from llama_cloud.types import StatusEnum
import httpx
import packaging.version
from pydantic import BaseModel
from typing import Any, Dict, List, Tuple, Type, Union, Optional
from io import BufferedIOBase, TextIOWrapper
from pathlib import Path
import secrets
from typing import Any, Dict, List, Tuple, Type
# Asyncio error messages
nest_asyncio_err = "cannot be called from a running event loop"
@@ -107,100 +104,3 @@ 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]
-7
View File
@@ -1,12 +1,5 @@
# llama_parse
## 0.6.77
### Patch Changes
- Updated dependencies [407292b]
- llama-cloud-services-py@0.6.77
## 0.6.76
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama_parse",
"version": "0.6.77",
"version": "0.6.76",
"description": "",
"main": "index.js",
"private": false,
+2 -2
View File
@@ -11,13 +11,13 @@ dev = [
[project]
name = "llama-parse"
version = "0.6.77"
version = "0.6.76"
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.77"]
dependencies = ["llama-cloud-services>=0.6.76"]
[project.scripts]
llama-parse = "llama_parse.cli.main:parse"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "llama-cloud-services-py",
"version": "0.6.77",
"version": "0.6.76",
"private": false,
"license": "MIT",
"scripts": {},
+1 -1
View File
@@ -19,7 +19,7 @@ dev = [
[project]
name = "llama-cloud-services"
version = "0.6.77"
version = "0.6.76"
description = "Tailored SDK clients for LlamaCloud services."
authors = [{name = "Logan Markewich", email = "logan@runllama.ai"}]
requires-python = ">=3.9,<4.0"
Generated
+2 -2
View File
@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.9, <4.0"
resolution-markers = [
"python_full_version >= '3.14'",
@@ -1596,7 +1596,7 @@ wheels = [
[[package]]
name = "llama-cloud-services"
version = "0.6.76"
version = "0.6.73"
source = { editable = "." }
dependencies = [
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
-6
View File
@@ -1,11 +1,5 @@
# llama-cloud-services
## 0.3.10
### Patch Changes
- fee516d: Adding LlamaClassify among the available LlamaCloud services
## 0.3.9
### Patch Changes
@@ -1,8 +0,0 @@
{
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": "./dist/index.js",
"private": true
}
+2 -14
View File
@@ -1,6 +1,6 @@
{
"name": "llama-cloud-services",
"version": "0.3.10",
"version": "0.3.9",
"type": "module",
"license": "MIT",
"scripts": {
@@ -24,8 +24,7 @@
"./reader",
"./parse",
"./beta/agent",
"./extract",
"./classify"
"./extract"
],
"exports": {
"./openapi.json": "./openapi.json",
@@ -84,17 +83,6 @@
},
"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",
@@ -1,69 +0,0 @@
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[],
parsingConfiguration: ClassifyParsingConfiguration,
fileContents:
| Buffer<ArrayBufferLike>[]
| File[]
| Uint8Array<ArrayBuffer>[]
| string[]
| undefined = undefined,
filePaths: string[] | undefined = undefined,
projectId: string | null = null,
organizationId: string | null = null,
pollingInterval: number = 1,
maxPollingIterations: number = 1800,
maxRetriesOnError: number = 10,
retryInterval: number = 0.5,
): Promise<ClassifyJobResults> {
const result = await classify(
rules,
parsingConfiguration,
fileContents,
filePaths,
projectId,
organizationId,
this.client,
pollingInterval,
maxPollingIterations,
maxRetriesOnError,
retryInterval,
);
return result;
}
}
+19 -1
View File
@@ -4,7 +4,25 @@ import * as extract from "./extract";
import type { ExtractAgent, ExtractConfig } from "./extract";
import { getEnv } from "@llamaindex/env";
import type { ExtractResult } from "./type";
import { getUrl } from "./utils";
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}`);
}
}
export class LlamaExtractAgent {
private agent: ExtractAgent;
-289
View File
@@ -1,289 +0,0 @@
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: string[],
rules: ClassifierRule[],
parsingConfiguration: ClassifyParsingConfiguration,
organizationId: null | string,
projectId: null | string,
client: Client | undefined,
maxRetriesOnError: number = 10,
retryInterval: number = 0.5,
): Promise<string> {
const rawData = {
file_ids: fileIds,
rules: rules,
parsing_configuration: parsingConfiguration,
} as ClassifyJobCreate;
const data = {
body: rawData,
query: {
project_id: projectId,
organization_id: organizationId,
},
} 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: string,
interval: number = 1,
maxIterations: number = 1800,
client: Client | undefined = 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: string,
client: Client | undefined = undefined,
projectId: string | null = null,
organizationId: string | null = null,
maxRetriesOnError: number = 10,
retryInterval: number = 0.5,
): Promise<ClassifyJobResults> {
const jobData = {
path: { classify_job_id: jobId },
query: { organization_id: organizationId, 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:
| Buffer<ArrayBufferLike>[]
| File[]
| Uint8Array<ArrayBuffer>[]
| string[]
| undefined = undefined,
filePaths: string[] | undefined = undefined,
projectId: string | null = null,
organizationId: string | null = null,
client: Client | undefined = undefined,
pollingInterval: number = 1,
maxPollingIterations: number = 1800,
maxRetriesOnError: number = 10,
retryInterval: number = 0.5,
): 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(
name,
undefined,
undefined,
projectId,
organizationId,
client,
maxRetriesOnError,
retryInterval,
);
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(
undefined,
content,
undefined,
projectId,
organizationId,
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,
organizationId,
projectId,
client,
maxRetriesOnError,
retryInterval,
);
const success = await pollForJobCompletion(
jobId,
pollingInterval,
maxPollingIterations,
client,
);
if (!success) {
throw new Error("Your job is taking longer than 10 minutes, timing out...");
} else {
return (await getJobResult(
jobId,
client,
projectId,
organizationId,
maxRetriesOnError,
retryInterval,
)) as ClassifyJobResults;
}
}
export {
type ClassifierRule,
type ClassifyJobResults,
type ClassifyParsingConfiguration,
};
+100 -1
View File
@@ -1,5 +1,9 @@
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,
@@ -15,6 +19,7 @@ import {
type GetJobApiV1ExtractionJobsJobIdGetData,
type GetJobResultApiV1ExtractionJobsJobIdResultGetData,
StatusEnum,
type UploadFileApiV1FilesPostData,
type StatelessExtractionRequest,
type ExtractStatelessApiV1ExtractionRunPostData,
type DeleteExtractionAgentApiV1ExtractionExtractionAgentsExtractionAgentIdDeleteData,
@@ -24,12 +29,17 @@ import {
runJobApiV1ExtractionJobsPost,
getJobApiV1ExtractionJobsJobIdGet,
getJobResultApiV1ExtractionJobsJobIdResultGet,
uploadFileApiV1FilesPost,
extractStatelessApiV1ExtractionRunPost,
deleteExtractionAgentApiV1ExtractionExtractionAgentsExtractionAgentIdDelete,
} from "./api";
import type { Client } from "@hey-api/client-fetch";
import { sleep } from "./utils";
import { uploadFile } from "./fileUpload";
import { fileTypeFromBuffer } from "file-type";
type BodyUploadFileApiV1FilesPost = {
upload_file: Blob | File;
};
export async function createAgent(
name: string,
@@ -211,6 +221,95 @@ 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>
-109
View File
@@ -1,109 +0,0 @@
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: 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 (
uploadResponse.response.ok &&
typeof uploadResponse.data != "undefined"
) {
fileId = uploadResponse.data.id as string;
return fileId;
}
}
}
-6
View File
@@ -8,9 +8,3 @@ 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";
-22
View File
@@ -117,25 +117,3 @@ 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}`);
}
}
@@ -2,8 +2,6 @@ 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";
@@ -491,59 +489,6 @@ 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,
undefined,
["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,
[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",