chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { type Client } from '@botpress/client'
|
||||
import { z } from '@bpinternal/zui'
|
||||
import { Tool } from 'llmz'
|
||||
|
||||
/** Extract the full content & the metadata of the specified pages as markdown. */
|
||||
export const browsePages = (client: Client) =>
|
||||
new Tool({
|
||||
name: 'browser_browsePages',
|
||||
description: 'Extract the full content & the metadata of the specified pages as markdown.',
|
||||
input: z
|
||||
.object({
|
||||
urls: z.array(z.string()),
|
||||
waitFor: z
|
||||
.default(
|
||||
z
|
||||
.number()
|
||||
.describe(
|
||||
'Time to wait before extracting the content (in milliseconds). Set this value higher for dynamic pages.'
|
||||
),
|
||||
350
|
||||
)
|
||||
.describe(
|
||||
'Time to wait before extracting the content (in milliseconds). Set this value higher for dynamic pages.'
|
||||
),
|
||||
})
|
||||
.catchall(z.never()),
|
||||
output: z
|
||||
.object({
|
||||
results: z.array(
|
||||
z
|
||||
.object({
|
||||
url: z.string(),
|
||||
content: z.string(),
|
||||
favicon: z.optional(z.string()),
|
||||
title: z.optional(z.string()),
|
||||
description: z.optional(z.string()),
|
||||
})
|
||||
.catchall(z.never())
|
||||
),
|
||||
})
|
||||
.catchall(z.never()),
|
||||
handler: async (input) => {
|
||||
return client.callAction({ type: 'browser:browsePages', input }).then(({ output }) => output) as any
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { type Client } from '@botpress/client'
|
||||
import { z } from '@bpinternal/zui'
|
||||
import { Tool } from 'llmz'
|
||||
|
||||
/** Capture a screenshot of the specified page. */
|
||||
export const captureScreenshot = (client: Client) =>
|
||||
new Tool({
|
||||
name: 'browser_captureScreenshot',
|
||||
description: 'Capture a screenshot of the specified page.',
|
||||
input: z
|
||||
.object({
|
||||
url: z.string(),
|
||||
})
|
||||
.catchall(z.never()),
|
||||
output: z
|
||||
.object({
|
||||
imageUrl: z.string(),
|
||||
})
|
||||
.catchall(z.never()),
|
||||
handler: async (input) => {
|
||||
return client.callAction({ type: 'browser:captureScreenshot', input }).then(({ output }) => output) as any
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './browsePages'
|
||||
export * from './captureScreenshot'
|
||||
export * from './webSearch'
|
||||
@@ -0,0 +1,102 @@
|
||||
import { type Client } from '@botpress/client'
|
||||
import { z } from '@bpinternal/zui'
|
||||
import { Tool } from 'llmz'
|
||||
|
||||
/** Search information on the web. You need to browse to that page to get the full content of the page. */
|
||||
export const webSearch = (client: Client) =>
|
||||
new Tool({
|
||||
name: 'browser_webSearch',
|
||||
description: 'Search information on the web. You need to browse to that page to get the full content of the page.',
|
||||
input: z
|
||||
.object({
|
||||
query: z.string().min(1, undefined).max(1000, undefined).describe('What are we searching for?'),
|
||||
includeSites: z
|
||||
.optional(
|
||||
z
|
||||
.array(
|
||||
z
|
||||
.string()
|
||||
.regex(/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/, undefined)
|
||||
.min(3, undefined)
|
||||
.max(50, undefined)
|
||||
)
|
||||
.max(20, undefined)
|
||||
.describe('Include only these domains in the search (max 20)')
|
||||
)
|
||||
.describe('Include only these domains in the search (max 20)'),
|
||||
excludeSites: z
|
||||
.optional(
|
||||
z
|
||||
.array(
|
||||
z
|
||||
.string()
|
||||
.regex(/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/, undefined)
|
||||
.min(3, undefined)
|
||||
.max(50, undefined)
|
||||
)
|
||||
.max(20, undefined)
|
||||
.describe('Exclude these domains from the search (max 20)')
|
||||
)
|
||||
.describe('Exclude these domains from the search (max 20)'),
|
||||
count: z
|
||||
.default(
|
||||
z
|
||||
.number()
|
||||
.min(1, undefined)
|
||||
.max(20, undefined)
|
||||
.describe('Number of search results to return (default: 10)'),
|
||||
10
|
||||
)
|
||||
.describe('Number of search results to return (default: 10)'),
|
||||
freshness: z
|
||||
.optional(z.enum(['Day', 'Week', 'Month']).describe('Only consider results from the last day, week or month'))
|
||||
.describe('Only consider results from the last day, week or month'),
|
||||
browsePages: z
|
||||
|
||||
.boolean()
|
||||
// .default(false)
|
||||
.describe('Whether to browse to the pages to get the full content'),
|
||||
})
|
||||
.catchall(z.never()),
|
||||
output: z
|
||||
.object({
|
||||
results: z.array(
|
||||
z
|
||||
.object({
|
||||
name: z.string().describe('Title of the page'),
|
||||
url: z.string().describe('URL of the page'),
|
||||
snippet: z.string().describe('A short summary of the page'),
|
||||
links: z
|
||||
.optional(
|
||||
z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
.catchall(z.never())
|
||||
)
|
||||
.describe('Useful links on the page')
|
||||
)
|
||||
.describe('Useful links on the page'),
|
||||
page: z.optional(
|
||||
z
|
||||
.object({
|
||||
url: z.string(),
|
||||
content: z.string(),
|
||||
favicon: z.optional(z.string()),
|
||||
title: z.optional(z.string()),
|
||||
description: z.optional(z.string()),
|
||||
})
|
||||
.catchall(z.never())
|
||||
),
|
||||
})
|
||||
.catchall(z.never())
|
||||
),
|
||||
})
|
||||
.catchall(z.never()),
|
||||
handler: async (input) => {
|
||||
return client.callAction({ type: 'browser:webSearch', input }).then(({ output }) => output) as any
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,193 @@
|
||||
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,
|
||||
},
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user