Files
2026-07-13 13:34:48 +08:00

194 lines
6.0 KiB
TypeScript

import { Client } from '@botpress/client'
import { z } from '@bpinternal/zui'
import { ObjectInstance, Tool } from 'llmz'
const sanitizePath = (path: string) => {
// Remove leading/trailing slashes and replace multiple slashes with a single slash
path = path.replace(/(^\/+|\/+$)/g, '').replace(/\/{2,}/g, '/')
// Allow only alphanumeric, underscore, hyphen, and slashes
path = path.replace(/[^a-zA-Z0-9_\-\\/\\.]/g, '')
// Ensure the path does not start with a slash
path = path.startsWith('/') ? path : `/${path}`
// Ensure the path does not end with a slash
path = path.endsWith('/') ? path.slice(0, -1) : path
// Ensure the path does not contain consecutive slashes
path = path.replace(/\/{2,}/g, '/')
// Ensure the path does not contain a leading dot
path = path.replace(/^\.\//, '')
// Ensure the path does not contain a trailing dot
path = path.replace(/\/\.$/, '')
return path.toLowerCase().trim()
}
const getPathFolder = (path: string) => {
const parts = path.split('/')
parts.pop() // Remove the last part (file name)
return parts.join('/') || '/'
}
const parseFilePath = (path: string) => {
if (!path || typeof path !== 'string') {
throw new Error(`Invalid file path: "${path}". Path must be a non-empty string.`)
}
if (path.length > 255) {
throw new Error(`Invalid file path: "${path}". Path length exceeds 255 characters.`)
}
const sanitizedPath = sanitizePath(path)
if (!sanitizedPath.length) {
throw new Error(`Invalid file path: "${path}". Path cannot be empty.`)
}
const folder = getPathFolder(sanitizedPath)
const fileName = sanitizedPath.split('/').pop()?.trim() || ''
if (!fileName.length) {
throw new Error(`Invalid file path: "${path}". File name cannot be empty.`)
}
if (fileName.length && !fileName.includes('.')) {
throw new Error(`Invalid file path: "${fileName}". File name must contain an extension.`)
}
return {
path: sanitizedPath,
folder,
file: fileName,
}
}
export const makeFileSystem = (client: Client) =>
new ObjectInstance({
name: 'fs',
description: 'File system operations',
tools: [
new Tool({
name: 'readFile',
description: 'Read a file from the file system',
input: z.string().describe('File path to read'),
output: z.string().describe('File content'),
handler: async (path) => {
const { path: key } = parseFilePath(path)
const { file } = await client.getFile({
id: key,
})
return await fetch(file.url).then((res) => res.text())
},
}),
new Tool({
name: 'writeFile',
description: 'Write a file to the file system',
input: z.object({
path: z.string().describe('File path to write'),
content: z.string().describe('File content to write'),
}),
handler: async (input) => {
const { path, file, folder } = parseFilePath(input.path)
await client.uploadFile({
key: path,
content: input.content,
accessPolicies: ['public_content'],
publicContentImmediatelyAccessible: true,
tags: {
purpose: 'file-system',
folder: folder || '/',
name: file,
},
})
},
}),
new Tool({
name: 'listFiles',
description: 'Write a file to the file system',
input: z.string().describe('Folder path to list files from').default('/'),
output: z
.array(
z.object({
folder: z.string().describe('Folder path'),
file: z.string().describe('File name'),
createdAt: z.string().describe('File creation date'),
updatedAt: z.string().describe('File last update date'),
size: z.number().describe('File size in bytes'),
url: z.string().url().describe('File URL'),
contentType: z.string().describe('File content type'),
})
)
.describe('List of files in the folder'),
handler: async (path) => {
const folder = sanitizePath(path || '/')
const files = await client.list
.files({
sortDirection: 'desc',
sortField: 'updatedAt',
tags: {
purpose: 'file-system',
folder,
},
})
.collect({ limit: 1000 })
return files.map((file) => ({
folder: file.tags?.folder || '/',
file: file.tags?.name || file.key,
createdAt: file.createdAt,
updatedAt: file.updatedAt,
size: file.size,
url: file.url,
contentType: file.contentType,
}))
},
}),
new Tool({
name: 'deleteFile',
description: 'Delete a file from the file system',
input: z.string().describe('File path to delete'),
output: z.boolean().describe('True if file was deleted successfully'),
handler: async (path) => {
const { path: key } = parseFilePath(path)
await client.deleteFile({ id: key })
return true
},
}),
new Tool({
name: 'moveFile',
description: 'Rename a file in the file system',
input: z.object({
before: z.string().describe('Current file path'),
after: z.string().describe('New file path'),
}),
handler: async ({ before, after }) => {
const { path: oldKey } = parseFilePath(before)
const { path: newKey, file, folder } = parseFilePath(after)
await client.copyFile({
idOrKey: oldKey,
destinationKey: newKey,
overwrite: true,
})
await client.deleteFile({ id: oldKey })
await client.updateFileMetadata({
id: newKey,
tags: {
purpose: 'file-system',
folder: folder || '/',
name: file,
},
})
},
}),
],
})