botpress / botpress

The open-source hub to build & deploy GPT/LLM Agents ⚡️
https://botpress.com
MIT License
12.19k stars 1.69k forks source link

feat(sdk): add support for integration interfaces #13118

Closed franklevasseur closed 1 day ago

franklevasseur commented 1 month ago

feat(sdk): add support for integration interfaces

Why

Let's say you want to write reusable pieces of bots. These includes workflows, bot actions, hooks, agents, etc. We refer to any kind of reusable piece of a bot as a skill.

Some skills have a dependency on an integration;

What

We want to be able to define interfaces that integrations can implement. This way, we can write skills that depend on an interface and not on a specific integration.

In its simplest form; an interface is a bunch of action and event definitions with template arguments. Here are some examples:

interface HITL {
  startHITL(input: { upstreamConversationId: string }): Promise<{ downstreamConversationId: string }>
}
interface Listable<T extends { id: string }> {
  list(input: { nextToken?: string }): Promise<{ items: T[]; meta: { netToken?: string } }>
}
interface Creatable<T extends { id: string }> {
  create(input: Partial<T>): Promise<{ item: T }>
  on(event: 'created', handler: (item: T) => void): void // the "created" event
}

**These examples aren't actual integration definitions build using SDK's constructs. I use TypeScript in a more abstract way to illustrate the concept.**

In a attempt to reuse a concept that we created earlier this year, we decided to call template arguments entities. Entities are schemas that can be defined in an integration and used to resolve a template argument when implementing an interface.

Here's what it will be used for:

class ZendeskIntegration implements HITL {
  startHITL(input: { upstreamConversationId: string }): Promise<{ downstreamConversationId: string }> {
    // implementation
  }
}
class LinearIntegration implements Listable<Issue>, Listable<Project> {
  issueList(input: { nextToken?: string }): Promise<{ items: Issue[]; meta: { netToken?: string } }> {
    // implementation
  }

  projectList(input: { nextToken?: string }): Promise<{ items: Project[]; meta: { netToken?: string } }> {
    // implementation
  }
}

How

Defining an interface

SDK now allows defining interfaces using the InterfaceDeclaration class. An interface project is a simple directory with a interface.definition.ts file that default exports an interface declaration.

// interface.definition.ts
import { InterfaceDeclaration } from '@botpress/sdk'

export default new InterfaceDeclaration({
  name: 'hitl',
  version: '0.0.1',
  entities: {},
  events: {},
  actions: {
    startHITL: {
      input: {
        schema: () => z.object({ upstreamConversationId: z.string() }),
      },
      output: {
        schema: () => z.object({ downstreamConversationId: z.string() }),
      },
    },
  },
})

To upload this interface, run command bp deploy like you would do for an integration or a bot.

The SDK also provides a bunch of builtin interfaces to be implemented.

Implementing an interface

Let's say you want linear to implement the listable interface. In the integration.definition.ts file, you would do add:

import { IntegrationDefinition, interfaces } from '@botpress/sdk'

export default new IntegrationDefinition({
  name: 'linear',
  version: '0.4.3',
  title: 'Linear',
  // ...
}).extend(interfaces.listable, (self) => ({
  item: self.issue,
}))

The typings will complain if the provided schema for item is not an existing entity of your integration or does not implement the required type. Here, the item template argument expects to be of type { id: string }, so the provided entity issue needs to have an id field.

Using an interface

When developping a bot, you can dynamically check if an integration installed in the bot implements an interface, by fetching the integration in the API and checking the integration field.