Custom Modules
Module Templates
To help you get started, two templates are available: Module Templates.
- Copy the template of your choice.
- Paste it in
modules/
. - In your
botpress.config.json
, enable the module.
Module Structure
This is the basic structure that your module should have. Files or folders followed by a question mark are optional and discussed more thoroughly below.
module-directory
├── build.extras.js?
├── package.json
└── src
├── actions?
├── backend
│ └── index.ts
├── config.ts?
└── views
└── index.jsx
Module Builder
It's the only dependency you have to add in your dev-dependencies. It handles typescript compilation, webpack setup and compilation, packaging for distribution, etc... Here's how to get started:
- Add the
module-builder
as a dependency:
"devDependencies": {
"module-builder": "../../build/module-builder"
}
- Add those scripts commands:
"scripts": {
"build": "./node_modules/.bin/module-builder build",
"watch": "./node_modules/.bin/module-builder watch",
"package": "./node_modules/.bin/module-builder package"
}
Then you can build your module using yarn build
.
Local Development Tips
In order to have code changes automatically recompiled, you need to first run yarn cmd dev:modules
(run yarn cmd default
to get full documentation for other useful commands). Restart server to apply backend changes and refresh your browser for UI changes.
Then, you can type cd modules/your-module
and start a yarn watch
process in another terminal. This will process will recompile your module's code.
To regenerate the config.schema.json
file for your module, you need to run yarn build
from the module directory.
Once your module is ready to be deployed, from your module's directory, run yarn build && yarn package
. This will generate a .tgz
archive containing your compiled module.
Overriding Webpack Options
It is possible to override webpack parameters by adding a "webpack" property to the package.json
file of your module. When you override a property, you also remove the default settings that we've set, so we recommend adding them back when overriding. For example, if you want to add an additional external file:
Example:
"webpack": {
"externals": {
//These 3 are included by default
"react": "React",
"react-dom": "ReactDOM",
"react-bootstrap": "ReactBootstrap",
//Your new addition:
"botpress/content-picker": "BotpressContentPicker"
}
}
Copying Extra Files
When you package your modules, only the files in the dist
folder are included in the zip, plus production-optimized node_modules
.
If you want to add special files to that folder (for example, copy automatically some files not handled by webpack), you need to add a simple file named build.extras.js
at the root of your module (at the same level as package.json
).
Your file needs to export an array named copyFiles
containing the paths to move. It keeps the same folder structure, only changing src
for dist
.
Example:
build.extras.js
module.exports = {
copyFiles: ["src/backend/somefolder/myfile_*", "src/backend/binary/*"]
}
Module Entry Point
This is where you define how Botpress will interact with your module. Your index.ts
file must export an sdk.ModuleEntryPoint
object.
tip
Keep your index.ts
file small and split your module's logic in multiple files.
We will explore each property below.
Example:
const entryPoint: sdk.ModuleEntryPoint = {
onServerStarted,
onServerReady,
onBotMount,
onBotUnmount,
onFlowChanged,
skills,
botTemplates,
definition: {
name: "my-module",
menuIcon: "some-icon",
menuText: "",
fullName: "My Module",
homepage: "https://botpress.com",
noInterface: false,
plugins: []
}
}
export default entryPoint
onServerStarted
This method is called as soon as the bot is starting up. The server is not available at that time, and calls to other API will fail. This is usually used to set up database connection, which you can access via Knex (bp.database
).
Example:
const onServerStarted = async (bp: SDK) => {
await db(bp)
}
onServerReady
This is called once all modules are initialized and when the server is listening for incoming connections.
Usually you will setup your API endpoint here.
Example:
const onServerReady = async (bp: SDK) => {
await api(bp)
}
onBotMount && onBotUnmount
These methods are called every time a bot is started or stopped (either when starting Botpress or when creating or deleting a bot).
Example:
const botScopedStorage: Map<string, MyStorage> = new Map<string, MyStorage>()
const onBotMount = async (bp: SDK, botId: string) => {
const storage = new MyStorage(bp, botId)
botScopedStorage.set(botId, storage)
}
const onBotUnmount = async (botId: string) => {
botScopedStorage.delete(botId, storage)
}
onFlowChanged
This method is called whenever a node is renamed in a flow. This allows you to update your module's data so you are up-to-date with the new changes. For more information on how to implement this method, please refer yourself to our implementation in the QNA Module
Example:
const onFlowChanged = async (bp: SDK, botId: string, flow: Flow) => {
...
}
skills
When you create new skills, they need a way to generate the custom flow that will be used by the dialog engine. Skills defined here will be displayed in the flow editor in the dropdown menu.
Example:
const skillsToRegister: sdk.Skill[] = [
{
id: "Choice", // This must be the name of the component exported in your module full view
name: "Choice", // This is the value displayed to the user
flowGenerator: choice.generateFlow
}
]
botTemplates
Templates allow you to create a new bot without starting from scratch. They can include about anything, like content elements, flows, NLU intents, QNAs, etc.
Example:
const botTemplates: sdk.BotTemplate[] = [
{
id: "welcome-bot",
name: "Welcome Bot",
desc: "This is a demonstration bot to showcase some capabilities"
}
]
Definition
The definition is used by Botpress to setup your module.
Please refer to the SDK Reference for information on the possible options.
The only way to communicate with modules (or between them) is by using the API endpoint.
All modules are isolated and receive their own instance of bp
.
API Endpoint
The only way to communicate with modules (or between them) is by using the API endpoint.
All modules are isolated and receive their own instance of bp
.
Consuming API Externally or From Another Module
The Botpress SDK exposes a method to get the axios headers for a request. It will automatically set the base URL for the request and the required headers to communicate with the specific bot. This method is bp.http.getAxiosConfigForBot('bot123'): Promise<AxiosRequestConfig>
.
The method also accepts a second parameter with additional options. Right now, the only available option is localUrl
. When set to true, the module will communicate with the local URL instead of the external one.
Example:
bp.http.getAxiosConfigForBot('bot123', { localUrl: true })
Once you have this, you simply have to call the axios method of your choice, and add the config as the last parameter.
Example:
extractNluContent: async () => {
const axiosConfig = await bp.http.getAxiosConfigForBot(event.botId)
const text = event.payload.text
const data = await axios.post(`/mod/nlu/extract`, { text }, axiosConfig)
}
Consuming API From Your Module's Views
When a user is using your module's interface, a bot is already selected so you just need to call bp.axios
. It is always passed down to your react components as a property.
Example:
const result = await this.props.bp.axios.get('/mod/my-module/query')
Creating an API Endpoint
Modules are global, as is the API, so they must be able to manage multiple bots.
tip
Set up the API route in the onServerReady
method of your entry point.
The bot ID targeted by the request is always available via req.params.botId
.
Setting up an API is very easy:
const router = bp.http.createRouterForBot("dialog-sessions")
router.get("/count", async (req, res) => {
const botId = req.params.botId
const { dialogSessions } = await knex("dialog_sessions")
.count("id as dialogSessions")
.where({ botId })
.first()
res.send({ dialogSessions })
})
In the example above, we added a route handler that will be available via /mod/dialog-sessions/count
which fetches data from the database and returns the data as json.
Configuration
Module configuration is handled automatically by Botpress (saving and loading). All you need to do is add a file named config.ts
in your bot src
folder. This file should be written in typescript (so your variables are correctly typed).
Since an example is worth a thousand words, check out existing modules configuration files to get a better idea.
Database
Botpress officially supports two databases: SQLite or Postgres. Your module can access the bot's database to create and update required tables.
Initialization and Table creation
Tables initialization should be done in the onServerStarted
block of your src/backend/index.ts
file.
index.ts
import Database from "./db"
let db = undefined
const onServerStarted = async (bp: SDK) => {
db = new Database(bp)
await db.initialize()
}
db.ts
export default class Database {
knex: any
constructor(private bp: SDK) {
this.knex = bp.database
}
initialize() {
if (!this.knex) {
throw new Error('You must initialize the database before')
}
this.knex.createTableIfNotExists('my_module_db', ...)
}
}
Migration
Database migration isn't available at the moment, it should be added in a future iteration.
Knex Extension
We extended Knex functionality with common features that makes development easier, by handling internally differences between different databases. When accessing bp.database
, you have access to all the usual Knex commands, plus the following ones:
Check if Using SQlite
The method bp.database.isLite
returns true if the database is SQLite.
Table Creation
Here is a simple example to create your module's table if it is missing:
Usage: bp.database.createTableIfNotExists(table_name, data_callback)
bp.database
.createTableIfNotExists("my_module_table", function(table) {
table.increments("id").primary()
table.string("type")
table.string("text", 640)
table.jsonb("raw_message")
table.timestamp("ts")
})
.then(async () => {
// You may chain table creation
})
Insert and Retrieve
Inserts the row in the database and returns the inserted row.
If you omit returnColumn
or idColumn
, it will use id
as the default.
Usage: bp.database.insertAndRetrieve(table_name, data, returnColumn?, idColumn?)
const someObject = (await bp.database.insertAndRetrieve)(
"my_module_table",
{
botId: session.botId,
important_data: bp.database.json.set(data || {}),
created_on: bp.database.date.now()
},
["botId", "important_data", "created_on"]
)
Date Helper
Views
There are two different type of views (or bundles) that your module can offer. A view can consist of multiple components. These components can be used by other modules, and your own module can also consume components of other modules.
Check out the Complete Module Example on GitHub for more details on how you can implement views.
Full View
This view includes heavy dependencies, like react-bootstrap
. When you want to add an interface for your module, your full view need to export a default
component.
The main view of the module is found in the src/views/full/index.jsx
file by default.
Skill components must be exported by this view (more on this below).
Lite View
The lite view doesn't include any heavy dependency. Common use case is to add a custom, lightweight component on the web chat. This type of view was added to keep the size of the webchat bundle small so it loads faster, especially on mobile phones.
Sharing Components Between Modules
It is now a lot easier to expose components for other modules or to use other module's components. Here is a quick example on how the webchat is able to display a component from a specific module:
Display a Custom Component Dynamically
When components are loaded this way, they are loaded and displayed immediately where the tag is placed.
Example:
// Fetch the module injector. It is available on any of your module's view.
const InjectedModuleView = this.props.bp.getModuleInjector()
// Use is very straightforward: specify the module name, the name of the component, and if it is available on the lite or full view.
<InjectedModuleView moduleName={moduleName} componentName={componentName} lite={true} extraProps={props} />
Load a Module's Components in Memory
By loading components this way, they aren't displayed immediately on the page.
They are accessible by using window.botpress['moduleName']['componentName']
.
Example:
// The first parameter is the module name, the second specifies if it should load the lite or full view
this.props.bp.loadModuleView(moduleName, true)
Skill Creation
There are a couple of steps required to create a new skill. Basically, a skill consist of a GUI to input values and a flow generator to create the interactions.
Step 1 - Create Your Visual Component
The first step is to create the GUI that will be displayed to the user. You can create your component in the file views/full/index.jsx
, or you can create it in a separate file, just make sure to export your skill component.
The name of your component (in the below example, MyCustomSkill) needs to be the same used in step 3 below.
Example:
import React from "react"
export class MyCustomSkill extends React.Component {
render() {
return null
}
}
Step 2 - Creating the Flow Generator
The flow generator will create all the transitions and conditions based on the data that was feeded by the GUI. That method will be called by the Studio when the user has finished inputting all his data. Your method will receive a data
object. and must return a partial flow.
Example:
const generateFlow = async (
data: any,
metadata: sdk.FlowGeneratorMetadata
): Promise<sdk.FlowGenerationResult> => {
const nodes: sdk.SkillFlowNode[] = [
{
name: "entry",
onEnter: [],
next: [{ condition: "true", node: "..." }]
}
]
return {
transitions: createTransitions(data),
flow: {
nodes: nodes,
catchAll: {
next: []
}
}
}
}
const createTransitions = data => {
const transitions: sdk.NodeTransition[] = []
return transitions
}
export default { generateFlow }
Step 3 - Connecting Those Components
Once your view and the flow generator is ready, you need to inform Botpress about your skill.
This is how you would register it:
// Note the array, you can register multiple skills that way
const skillsToRegister: sdk.Skill[] = [
{
id: "MyCustomSkill", // Name of your exported component
name: "My Magic Custom Skill", // Only used to display the skill in the list
flowGenerator: generateFlow
}
]
const entryPoint: sdk.ModuleEntryPoint = {
...,
skills: skillsToRegister
}
Register Actions
Modules can register new actions that will be available on the flow editor.
Those actions must be deployed to the data/global/actions
folder to be recognized by Botpress. Here is how to do that:
- Create a folder named
actions
insrc
. - Add your JavaScript files in the folder.
- When you build your module, your files will be copied in the
dist
folder. - At every startup, action files are copied in
data/global/actions/$MY_MODULE/
.
They are then accessible by the name $MY_MODULE/$MY_ACTION
in any node or skill.
If your action requires external dependencies, you must add them on your module's package.json
as dependencies. When the VM is initialized, we redirect require
requests to the node_modules
of its parent module.
note
Many dependencies are already included with Botpress and do not need to be added to your package (such as lodash, axios, etc.).
Module-Builder Docker Image
We provide a Docker image that can be used to compile your custom module. This is useful in CI/CD situations, where your pipeline will checkout your Custom Module's source code, and the Docker container will spit out a compiled .tgz
file.
Instructions
docker run -it --rm -v ${PWD}/modules/:/botpress/modules/ botpress/module-builder:v0_0_2 sh -c 'cd modules/YOUR_CUSTOM_MODULE && yarn && yarn build && yarn package'
cd modules/YOUR_CUSTOM_MODULE
The compiled module will be available in the directory you mounted as a YOUR_CUSTOM_MODULE.tgz
file.