Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions integrations/googledrivekb/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import rootConfig from '../../eslint.config.mjs'

export default [
...rootConfig,
{
languageOptions: {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
]
31 changes: 31 additions & 0 deletions integrations/googledrivekb/hub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Description

Enable your bot to sync Google Drive files into a Botpress knowledge base. This integration reads and lists all files in your Google Drive and transfers them to the Botpress files API for indexing.

# Configuration

This integration requires OAuth authorization to connect your Google Drive account to Botpress.

## Automatic configuration with OAuth

Click the authorization button and follow the on-screen instructions. A Botpress-managed Google Drive application with read-only access will be used to connect to your account.

Actions taken by the bot will be attributed to the user who authorized the connection. **We recommend using a service account** rather than a personal Google Drive account. Share the relevant folders with the service account to control what the knowledge base can access.

## Configuring the integration in Botpress

1. Authorize the Google Drive Knowledge Base integration by clicking the authorization button.
2. Follow the on-screen instructions to connect your Botpress chatbot to Google Drive.
3. Once the connection is established, save the configuration and enable the integration.

# Using the integration

Use this integration as a knowledge base source. It connects with the **Knowledge Connector** plugin to automatically sync files from Google Drive folders into a Botpress knowledge base.

Use the `syncChannels` action to maintain subscription channels on all available files and folders. These channels notify your bot when files are created, updated, or deleted. Channels are valid for up to one day — call this action once daily to prevent event loss.

# Limitations

Standard Google Drive API limitations apply. These include rate limits, file size restrictions, and other constraints imposed by the Google Drive platform.

More details are available in the [Google Drive API documentation](https://developers.google.com/drive/api/guides/about-sdk).
8 changes: 8 additions & 0 deletions integrations/googledrivekb/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
177 changes: 177 additions & 0 deletions integrations/googledrivekb/integration.definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import * as sdk from '@botpress/sdk'
import { sentry as sentryHelpers } from '@botpress/sdk-addons'
import filesReadonly from './bp_modules/files-readonly'
import {
fileSchema,
downloadFileDataArgSchema,
listFoldersOutputSchema,
listFilesOutputSchema,
readFileArgSchema,
listItemsInputSchema,
downloadFileDataOutputSchema,
fileDeletedEventSchema,
folderSchema,
folderDeletedEventSchema,
baseDiscriminatedFileSchema,
fileChannelSchema,
} from './src/schemas'

// TODO: use default options
const toJSONSchemaOptions: Partial<sdk.z.transforms.JSONSchemaGenerationOptions> = {
discriminatedUnionStrategy: 'anyOf',
discriminator: false,
}

export default new sdk.IntegrationDefinition({
name: 'googledrivekb',
title: 'Google Drive (Knowledge Base)',
description: 'Sync Google Drive files into a Botpress knowledge base using read-only access to all files.',
version: '0.1.0',
readme: 'hub.md',
icon: 'icon.svg',
attributes: {
category: 'File Management',
repo: 'botpress',
},
configuration: {
identifier: {
linkTemplateScript: 'linkTemplate.vrl',
},
schema: sdk.z.object({}),
},
actions: {
listFiles: {
title: 'List Files',
description: 'List files in Google Drive',
input: {
schema: listItemsInputSchema,
},
output: {
schema: listFilesOutputSchema,
},
},
listFolders: {
title: 'List folders',
description: 'List folders in Google Drive',
input: {
schema: listItemsInputSchema,
},
output: {
schema: listFoldersOutputSchema,
},
},
readFile: {
title: 'Read File',
description: "Read a file's metadata in a Google Drive",
input: {
schema: readFileArgSchema,
},
output: {
schema: fileSchema.describe('The file read from Google Drive'),
},
},
downloadFileData: {
title: 'Download file data',
description: 'Download data from a file in Google Drive',
input: {
schema: downloadFileDataArgSchema,
},
output: {
schema: downloadFileDataOutputSchema,
},
},
syncChannels: {
title: 'Sync Channels',
description: 'Sync channels for file change subscriptions',
input: {
schema: sdk.z.object({}),
},
output: {
schema: sdk.z.object({}),
},
},
},
events: {
fileCreated: {
title: 'File Created',
description: 'Triggered when a file is created in Google Drive',
schema: fileSchema,
},
fileDeleted: {
title: 'File Deleted',
description: 'Triggered when a file is deleted in Google Drive',
schema: fileDeletedEventSchema,
},
folderCreated: {
title: 'Folder Created',
description: 'Triggered when a folder is created in Google Drive',
schema: folderSchema,
},
folderDeleted: {
title: 'Folder Deleted',
description: 'Triggered when a folder is deleted in Google Drive',
schema: folderDeletedEventSchema,
},
},
states: {
configuration: {
type: 'integration',
schema: sdk.z.object({
refreshToken: sdk.z
.string()
.title('Refresh token')
.describe('The refresh token to use to authenticate with Google. It gets exchanged for a bearer token'),
}),
},
filesCache: {
type: 'integration',
schema: sdk.z.object({
filesCache: sdk.z
.record(sdk.z.string(), baseDiscriminatedFileSchema)
.title('Files cache')
.describe('Map of known files'),
}),
},
filesChannelsCache: {
type: 'integration',
schema: sdk.z.object({
filesChannelsCache: sdk.z
.record(sdk.z.string(), fileChannelSchema)
.title('Files change subscription channels')
.describe('Serialized set of channels for file change subscriptions'),
}),
},
},
secrets: {
...sentryHelpers.COMMON_SECRET_NAMES,
CLIENT_ID: {
description: 'The client ID in your Google Cloud Credentials',
},
CLIENT_SECRET: {
description: 'The client secret associated with your client ID',
},
WEBHOOK_SECRET: {
description: 'The secret used to sign webhook tokens. Should be a high-entropy string that only Botpress knows',
},
},
__advanced: { toJSONSchemaOptions },
}).extend(filesReadonly, ({}) => ({
entities: {},
actions: {
listItemsInFolder: {
name: 'filesReadonlyListItemsInFolder',
attributes: { ...sdk.WELL_KNOWN_ATTRIBUTES.HIDDEN_IN_STUDIO },
},
transferFileToBotpress: {
name: 'filesReadonlyTransferFileToBotpress',
attributes: { ...sdk.WELL_KNOWN_ATTRIBUTES.HIDDEN_IN_STUDIO },
},
},
events: {
fileCreated: { name: 'filesReadonlyFileCreated' },
fileUpdated: { name: 'filesReadonlyFileUpdated' },
fileDeleted: { name: 'filesReadonlyFileDeleted' },
folderDeletedRecursive: { name: 'filesReadonlyFolderDeletedRecursive' },
aggregateFileChanges: { name: 'filesReadonlyAggregateFileChanges' },
},
}))
4 changes: 4 additions & 0 deletions integrations/googledrivekb/linkTemplate.vrl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
webhookId = to_string!(.webhookId)
webhookUrl = to_string!(.webhookUrl)

"{{ webhookUrl }}/oauth/wizard/start?state={{ webhookId }}"
31 changes: 31 additions & 0 deletions integrations/googledrivekb/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@botpresshub/googledrivekb",
"scripts": {
"check:type": "tsc --noEmit",
"check:bplint": "bp lint",
"build": "bp add -y && bp build",
"test": "vitest --run"
},
"private": true,
"dependencies": {
"@botpress/client": "workspace:*",
"@botpress/common": "workspace:*",
"@botpress/sdk": "workspace:*",
"@botpress/sdk-addons": "workspace:*",
"axios": "^1.7.7",
"googleapis": "^144.0.0",
"jsonwebtoken": "^9.0.2",
"uuid": "^9.0.0"
},
"devDependencies": {
"@botpress/cli": "workspace:*",
"@botpress/sdk": "workspace:*",
"@sentry/cli": "^2.39.1",
"@types/jsonwebtoken": "^9.0.3",
"@types/uuid": "^9.0.1",
"preact": "^10.26.6"
},
"bpDependencies": {
"files-readonly": "../../interfaces/files-readonly"
}
}
100 changes: 100 additions & 0 deletions integrations/googledrivekb/src/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Client as DriveClient } from './client'
import { wrapWithTryCatch } from './error-handling'
import { FileChannelsCache } from './file-channels-cache'
import { FileEventHandler } from './file-event-handler'
import { downloadToBotpress } from './files-api-utils'
import { FilesCache } from './files-cache'
import { filesReadonlyActions } from './files-readonly/actions'
import * as bp from '.botpress'

type ActionPropsAndTools<T extends bp.AnyActionProps> = {
driveClient: DriveClient
filesCache: FilesCache
fileChannelsCache: FileChannelsCache
fileEventHandler: FileEventHandler
} & T

const createActionPropsAndTools = async <T extends bp.AnyActionProps>(props: T): Promise<ActionPropsAndTools<T>> => {
const { client, ctx, logger } = props
const driveClient = await DriveClient.create({ client, ctx, logger })
const filesCache = await FilesCache.load({ client, ctx })
const fileChannelsCache = await FileChannelsCache.load({ client, ctx })
driveClient.setCache(filesCache)
return {
driveClient,
filesCache,
fileChannelsCache,
fileEventHandler: new FileEventHandler(client, driveClient, filesCache, fileChannelsCache),
...props,
}
}

const saveAllCaches = async <T extends bp.AnyActionProps>(props: ActionPropsAndTools<T>) => {
await props.filesCache.save()
await props.fileChannelsCache.save()
}

const makeSaveAllCachesAndReturnResult =
<T extends bp.AnyActionProps>(props: ActionPropsAndTools<T>) =>
async <R>(actionOutput: R) => {
await saveAllCaches(props)
return actionOutput
}

const listFiles: bp.IntegrationProps['actions']['listFiles'] = wrapWithTryCatch(async (baseProps) => {
const props = await createActionPropsAndTools(baseProps)
const { driveClient, input } = props
const saveAllCachesAndReturnResult = makeSaveAllCachesAndReturnResult(props)
return await driveClient.listFiles(input).then(saveAllCachesAndReturnResult)
}, 'Error listing files')

const listFolders: bp.IntegrationProps['actions']['listFolders'] = wrapWithTryCatch(async (baseProps) => {
const props = await createActionPropsAndTools(baseProps)
const { driveClient, input } = props
const saveAllCachesAndReturnResult = makeSaveAllCachesAndReturnResult(props)
return await driveClient.listFolders(input).then(saveAllCachesAndReturnResult)
}, 'Error listing folders')

const readFile: bp.IntegrationProps['actions']['readFile'] = wrapWithTryCatch(async (baseProps) => {
const props = await createActionPropsAndTools(baseProps)
const { driveClient, input } = props
const saveAllCachesAndReturnResult = makeSaveAllCachesAndReturnResult(props)
return await driveClient.readFile(input.id).then(saveAllCachesAndReturnResult)
}, 'Error reading file')

const downloadFileData: bp.IntegrationProps['actions']['downloadFileData'] = wrapWithTryCatch(async (baseProps) => {
const props = await createActionPropsAndTools(baseProps)
const { driveClient, input } = props
const { id, index } = input

const { botpressFileId, botpressFileUrl } = await downloadToBotpress({
botpressFileKey: id,
googleDriveFileId: id,
client: props.client,
driveClient,
indexFile: index,
})

await saveAllCaches(props)
return { bpFileId: botpressFileId, url: botpressFileUrl }
}, 'Error downloading file')

const syncChannels: bp.IntegrationProps['actions']['syncChannels'] = wrapWithTryCatch(async (baseProps) => {
const props = await createActionPropsAndTools(baseProps)
const { driveClient, fileChannelsCache } = props
const { fileChannels: newChannels } = await driveClient.tryWatchAll()
const oldChannels = fileChannelsCache.setAll(newChannels)
await fileChannelsCache.save()
await driveClient.tryUnwatch(oldChannels)
return {}
}, 'Error syncing channels')

export default {
listFiles,
listFolders,
readFile,
downloadFileData,
syncChannels,

...filesReadonlyActions,
} as const satisfies bp.IntegrationProps['actions']
Loading
Loading