UNPKG

@botpress/adk-cli

Version:

Command-line interface for the Botpress Agent Development Kit (ADK)

919 lines (731 loc) 20.1 kB
# Files API The Files API provides structured file storage with automatic indexing, semantic search capabilities, and CDN-backed content delivery. ## Accessing the Files API All file operations use the Botpress client. Get the client using the Context API: ```typescript import { context } from '@botpress/runtime' const client = context.get('client') ``` This pattern is used in all examples below. See **[Context API](./context-api.md)** for more details. ## Basic Concepts ### What is the Files API? - **Persistent storage**: Upload and manage files for your bot - **Semantic search**: Optional vector indexing for searchable content - **CDN delivery**: Fast file access through CloudFront - **Access control**: Fine-grained permissions and policies - **Metadata & tags**: Organize files with custom attributes ### File Properties Files have the following core properties: - **id**: Unique file identifier - **key**: User-defined unique key per bot - **url**: CDN URL for file content access - **size**: File size in bytes - **contentType**: MIME type (e.g., "application/pdf") - **tags**: Key-value pairs for organization - **metadata**: Custom JSON metadata - **accessPolicies**: Array of access control policies - **status**: Upload/indexing status - **createdAt/updatedAt**: Timestamps ## Creating and Uploading Files ### Basic File Upload ```typescript const { file } = await client.uploadFile({ key: 'documents/report-2024.pdf', // Unique key content: fileBuffer, // File content (Buffer/ArrayBuffer/Blob/string) contentType: 'application/pdf', // MIME type tags: { category: 'reports', year: '2024', }, index: true, // Enable semantic search accessPolicies: ['public_content'], // Make publicly accessible }) console.log('File ID:', file.id) console.log('File URL:', file.url) ``` ### Upload Options ```typescript await client.uploadFile({ key: 'unique-file-key', // Required: Unique identifier content: fileBuffer, // Required: File content contentType: 'text/plain', // Optional: MIME type // Indexing options index: true, // Enable semantic search indexing: { configuration: { vision: { transcribePages: true, // OCR for images/PDFs indexPages: 'all', // Index all pages }, }, }, // Access control accessPolicies: [ 'public_content', // Public URL access 'integrations', // Integration access ], // Organization tags: { category: 'documents', department: 'sales', }, // Custom metadata metadata: { author: 'John Doe', version: '1.0', }, // Expiry (max 90 days) expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // Immediate public access (requires public_content policy) publicContentImmediatelyAccessible: true, }) ``` ### Uploading Text Content For simple text/JSON content: ```typescript const { file } = await client.uploadFile({ key: 'config/settings.json', content: jsonContent, // String content contentType: 'application/json', index: false, }) ``` ### Uploading from URL To upload from an external URL: ```typescript const { file } = await client.uploadFile({ key: 'imported/image.jpg', url: 'https://example.com/image.jpg', // Fetch from URL contentType: 'image/jpeg', index: false, }) ``` ## Retrieving Files ### Get File Metadata ```typescript // Get by ID const { file } = await client.getFile({ id: 'file_abc123', }) // Get by key const { file } = await client.getFile({ id: 'documents/report-2024.pdf', // Can use key as ID }) console.log('File URL:', file.url) console.log('File size:', file.size) console.log('Status:', file.status) ``` ### Access File Content Files are accessed via URL. The URL type depends on access policies: ```typescript const { file } = await client.getFile({ id: fileId }) // For public files: permanent CDN URL // For private files: temporary presigned S3 URL const response = await fetch(file.url) const content = await response.text() // For JSON files const data = await response.json() // For binary files const buffer = await response.arrayBuffer() ``` ### File Status States ```typescript type FileStatus = | 'upload_pending' // Awaiting content upload | 'upload_completed' // Upload successful | 'upload_failed' // Upload failed | 'indexing_pending' // Queued for indexing | 'indexing_completed' // Indexed and searchable | 'indexing_failed' // Indexing failed // Check file status const { file } = await client.getFile({ id: fileId }) if (file.status === 'indexing_completed') { console.log('File is indexed and searchable') } ``` ## Listing Files ### Basic Listing ```typescript const { files, meta } = await client.listFiles({ limit: 20, offset: 0, }) files.forEach((file) => { console.log(file.key, file.size, file.status) }) // Pagination if (meta.nextToken) { const nextPage = await client.listFiles({ nextToken: meta.nextToken, }) } ``` ### Filtering by Tags ```typescript // Single tag filter const { files } = await client.listFiles({ tags: { category: 'reports' }, }) // Multiple values (OR condition) const { files } = await client.listFiles({ tags: { category: ['reports', 'documents'], }, }) // Exclude values const { files } = await client.listFiles({ tags: { category: { not: 'archived', }, }, }) ``` ### Filtering by IDs ```typescript const { files } = await client.listFiles({ ids: ['file_123', 'file_456', 'file_789'], }) ``` ### Sorting ```typescript const { files } = await client.listFiles({ sortBy: 'createdAt', sortOrder: 'desc', limit: 10, }) // Available sort fields: // - key // - size // - createdAt // - updatedAt // - status ``` ## Semantic Search ### Searching File Content ```typescript const { passages } = await client.searchFiles({ query: 'machine learning algorithms', limit: 5, }) passages.forEach((passage) => { console.log('File:', passage.file.key) console.log('Content:', passage.content) console.log('Similarity:', passage.score) console.log('Page:', passage.meta.pageNumber) }) ``` ### Search Options ```typescript const { passages } = await client.searchFiles({ query: 'customer feedback', // Filtering tags: { category: 'support' }, // Context contextDepth: 2, // Include 2 surrounding passages includeBreadcrumb: true, // Include document structure withContext: true, // Return detailed context // Consolidation consolidate: true, // Group by file, sort by position // Pagination limit: 10, }) ``` ### Search Result Structure ```typescript { passages: [ { content: 'The matched text passage...', score: 0.89, // Similarity score (0-1) meta: { type: 'paragraph', // passage | title | subtitle | etc. subtype: 'text', pageNumber: 5, position: 12, sourceUrl: 'https://...', }, file: { id: 'file_abc123', key: 'documents/report.pdf', contentType: 'application/pdf', tags: { category: 'reports' }, createdAt: '2024-01-15T10:30:00Z', updatedAt: '2024-01-15T10:30:00Z', }, context: [ // Only if withContext: true { type: 'preceding', content: 'Previous passage...', }, { type: 'current', content: 'The matched text passage...', }, { type: 'subsequent', content: 'Next passage...', }, ], }, ] } ``` ## Updating Files ### Update File Metadata ```typescript // Update without changing content const { file } = await client.updateFileMetadata({ id: fileId, metadata: { version: '2.0', updatedBy: 'Jane Doe', }, tags: { status: 'reviewed', newTag: 'value', }, accessPolicies: ['public_content'], expiresAt: new Date('2024-12-31').toISOString(), }) ``` ### Metadata Update Behavior ```typescript // Partial updates - existing keys preserved await client.updateFileMetadata({ id: fileId, metadata: { newKey: 'newValue', // Other metadata keys remain unchanged }, }) // Delete metadata key await client.updateFileMetadata({ id: fileId, metadata: { oldKey: null, // Removes this key }, }) // Delete tag await client.updateFileMetadata({ id: fileId, tags: { oldTag: null, // Removes this tag }, }) ``` ### Replace File Content ```typescript // Upload new content with same key (replaces old content) const { file } = await client.uploadFile({ key: existingFileKey, content: newContent, contentType: 'text/plain', }) ``` ## Managing File Passages ### List File Passages ```typescript const { passages, meta } = await client.listFilePassages({ id: fileId, limit: 20, }) passages.forEach((passage) => { console.log('Passage ID:', passage.id) console.log('Content:', passage.content) console.log('Type:', passage.meta.type) console.log('Page:', passage.meta.pageNumber) }) ``` ### Set Custom Passages ```typescript // Replace all passages with custom content await client.setFilePassages({ id: fileId, passages: [ { content: '# Introduction\n\nThis is the first section...', type: 'title', }, { content: 'Main content paragraph here.', type: 'paragraph', pageNumber: 1, }, { content: '## Subsection\n\nMore details...', type: 'subtitle', }, ], }) // File status becomes "indexing_pending" // Check status later to confirm completion ``` ## Deleting Files ### Delete Single File ```typescript // Delete by ID await client.deleteFile({ id: 'file_abc123' }) // Delete by key await client.deleteFile({ id: 'documents/report.pdf' }) ``` ### Cleanup When a file is deleted: - File metadata removed from database - File content removed from S3 - Vector embeddings removed (if indexed) - All passages deleted ## Knowledge Bases For high-level RAG implementation with automatic indexing and search integration, see **[Knowledge Bases](./knowledge-bases.md)**. The Files API provides low-level file management. Knowledge Bases use the Files API internally but provide a simpler interface for document indexing and semantic search in AI responses. ## Copying Files ### Copy Within Bot ```typescript const { file } = await client.copyFile({ idOrKey: sourceFileId, destinationKey: 'backups/report-copy.pdf', overwrite: false, // Fail if destination exists }) ``` ### Copy Across Bots ```typescript const { file } = await client.copyFile({ idOrKey: sourceFileId, destinationKey: 'imported/report.pdf', destinationBotId: targetBotId, overwrite: true, }) ``` ## Supported File Types ### Indexable Formats These file types support semantic search indexing in knowledge bases: ```typescript const INDEXABLE_TYPES = { // Documents pdf: 'application/pdf', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // Text txt: 'text/plain', md: 'text/markdown', mdx: 'text/markdown', html: 'text/html', htm: 'text/html', // Images (with vision processing) jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', } ``` The Dev Console knowledge connector picker uses file extensions as its only signal. It accepts `.pdf`, `.html`, `.htm`, `.txt`, `.doc`, `.docx`, `.md`, `.mdx`, plus extensionless provider files such as native Google Docs. It rejects other extensions before sync and lets the backend decide on extensionless files. ### Vision Processing For images and PDFs with vision enabled: ```typescript await client.uploadFile({ key: 'documents/invoice.pdf', content: pdfBuffer, contentType: 'application/pdf', index: true, indexing: { configuration: { vision: { transcribePages: true, // OCR text extraction indexPages: 'all', // or [1, 2, 3] for specific pages }, }, }, }) ``` ## Usage in Actions/Workflows ### In Actions ```typescript import { Action, z, context } from '@botpress/runtime' export const uploadDocument = new Action({ name: 'uploadDocument', input: z.object({ content: z.string(), filename: z.string(), }), output: z.object({ fileId: z.string(), fileUrl: z.string(), }), async handler({ input }) { const client = context.get('client') // Upload file const { file } = await client.uploadFile({ key: `uploads/${input.filename}`, content: input.content, contentType: 'text/plain', index: true, tags: { source: 'action', type: 'user-upload', }, }) return { fileId: file.id, fileUrl: file.url, } }, }) ``` ### In Workflows ```typescript import { Workflow, z, context } from '@botpress/runtime' export const ProcessDocumentWorkflow = new Workflow({ name: 'processDocument', input: z.object({ documentKey: z.string() }), async handler({ input, step }) { const client = context.get('client') // Get file const file = await step('get-file', async () => { const { file } = await client.getFile({ id: input.documentKey, }) return file }) // Wait for indexing await step('wait-indexing', async () => { let status = file.status while (status === 'INDEXING_PENDING') { await new Promise((resolve) => setTimeout(resolve, 1000)) const { file: updated } = await client.getFile({ id: file.id, }) status = updated.status } return status }) // Search content const results = await step('search', async () => { const { passages } = await client.searchFiles({ query: 'important findings', tags: { key: input.documentKey }, }) return passages }) return { processed: true, findings: results.length, } }, }) ``` ## Best Practices ### 1. Use Meaningful Keys ```typescript // ✅ Good - Clear, hierarchical structure await client.uploadFile({ key: 'documents/2024/reports/q1-sales.pdf', }) // ❌ Bad - Unclear, no structure await client.uploadFile({ key: 'file123.pdf', }) ``` ### 2. Tag Appropriately ```typescript // ✅ Good - Useful, queryable tags tags: { category: "reports", year: "2024", quarter: "Q1", department: "sales" } // ❌ Bad - Too specific or redundant tags: { filename: "q1-sales.pdf", // Already in key timestamp: "1234567890" // Use createdAt instead } ``` ### 3. Handle File Status ```typescript // ✅ Good - Check and handle status const { file } = await client.getFile({ id: fileId }) switch (file.status) { case 'UPLOAD_PENDING': console.log('Waiting for upload...') break case 'INDEXING_PENDING': console.log('Indexing in progress...') break case 'INDEXING_COMPLETED': // File is searchable break case 'INDEXING_FAILED': console.error('Indexing failed:', file.failedStatusReason) break } ``` ### 4. Set Appropriate Expiry ```typescript // ✅ Good - Reasonable expiry for temporary files await client.uploadFile({ key: 'temp/cache-data.json', expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 1 day }) // For permanent files, omit expiresAt await client.uploadFile({ key: 'documents/permanent-record.pdf', // No expiresAt = never expires }) ``` ### 5. Optimize Indexing ```typescript // ✅ Good - Index only searchable content await client.uploadFile({ key: 'documents/report.pdf', content: pdfBuffer, index: true, // Searchable document }) await client.uploadFile({ key: 'images/logo.png', content: imageBuffer, index: false, // Not searchable, save quota }) // ✅ Good - Use vision only when needed await client.uploadFile({ key: 'documents/scanned-invoice.pdf', content: pdfBuffer, index: true, indexing: { configuration: { vision: { transcribePages: true, // OCR needed }, }, }, }) ``` ## Common Patterns ### File Upload with Validation ```typescript export const uploadWithValidation = async (fileContent: Buffer, filename: string) => { const client = context.get('client') // Validate file size (max 100MB) const MAX_SIZE = 100 * 1024 * 1024 if (fileContent.length > MAX_SIZE) { throw new Error('File too large') } // Validate file type const contentType = getContentType(filename) if (!ALLOWED_TYPES.includes(contentType)) { throw new Error('File type not allowed') } // Upload const { file } = await client.uploadFile({ key: `uploads/${Date.now()}-${filename}`, content: fileContent, contentType, index: true, tags: { originalName: filename, uploadedAt: new Date().toISOString(), }, }) return file } ``` ### Search with Context ```typescript export const searchWithContext = async (query: string, category?: string) => { const client = context.get('client') const { passages } = await client.searchFiles({ query, tags: category ? { category } : undefined, contextDepth: 2, includeBreadcrumb: true, limit: 5, }) return passages.map((p) => ({ content: p.content, source: p.file.key, relevance: p.score, location: `Page ${p.meta.pageNumber || 'N/A'}`, })) } ``` ### Bulk File Operations ```typescript export const bulkUpload = async (files: FileData[]) => { const client = context.get('client') const results = await Promise.allSettled( files.map(async (fileData) => { const { file } = await client.uploadFile({ key: fileData.key, content: fileData.content, contentType: fileData.contentType, index: true, }) return file }) ) const successful = results.filter((r) => r.status === 'fulfilled').map((r) => r.value) const failed = results.filter((r) => r.status === 'rejected').map((r) => r.reason) return { successful, failed } } ``` ## Troubleshooting ### Common Issues 1. **Upload fails with 403 error** - Ensure you have proper access policies set - Check that the bot token has file upload permissions - Verify the file size is within limits 2. **File not searchable** - Check `file.status` is `INDEXING_COMPLETED` - Verify `index: true` was set during upload - Ensure file type is in supported indexable formats - Check billing plan supports indexing features 3. **Search returns no results** - Verify files are indexed (`status: "INDEXING_COMPLETED"`) - Check tag filters aren't too restrictive - Try broader search queries - Ensure files have searchable content 4. **File access denied** - Verify access policies are correctly set - Check workspace member permissions - For integration files, ensure proper namespace 5. **Quota exceeded** - Check storage usage in workspace settings - Delete unused files or increase quota - Consider file expiry policies for temporary files ## Limits and Quotas - **Maximum file size**: 100 MB - **Maximum expiry**: 90 days from upload - **Maximum tags per file**: 50 - **Maximum indexable file size**: varies by file type - **Search result limit**: Configurable, recommended max 100 ## Migration Notes ### Current API (ADK 1.13+) The current Files API uses direct content upload: ```typescript // ✅ CURRENT - Direct content parameter await client.uploadFile({ key: 'unique-key', content: fileBuffer, // Direct upload contentType: 'application/pdf', tags: { category: 'docs' }, }) // Alternative: Upload from URL await client.uploadFile({ key: 'unique-key', url: 'https://example.com/file.pdf', // Fetch from URL tags: { category: 'docs' }, }) ``` ### Retrieving File Content Files are accessed via their URL: ```typescript const { file } = await client.getFile({ id: fileId }) // Fetch file content const response = await fetch(file.url) const content = await response.arrayBuffer() // For text/JSON const text = await response.text() const json = await response.json() ``` **URL Types:** - **Public files** (`public_content` policy): Permanent CDN URL - **Private files**: Temporary presigned URL (use shortly after retrieval)