@botonic/plugin-flow-builder
Version:
Use Flow Builder to show your contents
275 lines (240 loc) • 8.83 kB
text/typescript
import {
INPUT,
Plugin,
PluginPreRequest,
PROVIDER,
ResolvedPlugins,
Session,
} from '@botonic/core'
import { ActionRequest } from '@botonic/react'
import { v7 as uuidv7 } from 'uuid'
import { FlowBuilderApi } from './api'
import {
EMPTY_PAYLOAD,
FLOW_BUILDER_API_URL_PROD,
SEPARATOR,
SOURCE_INFO_SEPARATOR,
} from './constants'
import { FlowContent } from './content-fields'
import {
HtBotActionNode,
HtFlowBuilderData,
HtNodeWithContent,
HtNodeWithContentType,
} from './content-fields/hubtype-fields'
import { FlowFactory } from './flow-factory'
import { CustomFunction, DEFAULT_FUNCTION_NAMES } from './functions'
import {
AiAgentFunction,
BotonicPluginFlowBuilderOptions,
ContentFilter,
FlowBuilderJSONVersion,
InShadowingConfig,
KnowledgeBaseFunction,
PayloadParamsBase,
RatingSubmittedInfo,
TrackEventFunction,
} from './types'
import { getNodeByUserInput } from './user-input'
import { SmartIntentsInferenceConfig } from './user-input/smart-intent'
import { inputHasTextData, resolveGetAccessToken } from './utils'
// TODO: Create a proper service to wrap all calls and allow api versioning
export default class BotonicPluginFlowBuilder implements Plugin {
public cmsApi: FlowBuilderApi
private flow?: HtFlowBuilderData
private functions: Record<any, any>
private currentRequest: PluginPreRequest
public getAccessToken: (session: Session) => string
public trackEvent?: TrackEventFunction
public getKnowledgeBaseResponse?: KnowledgeBaseFunction
public getAiAgentResponse?: AiAgentFunction
public smartIntentsConfig: SmartIntentsInferenceConfig
public inShadowing: InShadowingConfig
public contentFilters: ContentFilter[]
// TODO: Rethink how we construct FlowBuilderApi to be simpler
public jsonVersion: FlowBuilderJSONVersion
public apiUrl: string
public customRatingMessageEnabled: boolean
constructor(options: BotonicPluginFlowBuilderOptions<ResolvedPlugins, any>) {
this.apiUrl = options.apiUrl || FLOW_BUILDER_API_URL_PROD
this.jsonVersion = options.jsonVersion || FlowBuilderJSONVersion.LATEST
this.flow = options.flow
this.getAccessToken = resolveGetAccessToken(options.getAccessToken)
this.trackEvent = options.trackEvent
this.getKnowledgeBaseResponse = options.getKnowledgeBaseResponse
this.getAiAgentResponse = options.getAiAgentResponse
this.smartIntentsConfig = {
...options?.smartIntentsConfig,
useLatest: this.jsonVersion === FlowBuilderJSONVersion.LATEST,
}
const customFunctions = options.customFunctions || {}
this.functions = customFunctions
this.inShadowing = {
allowKeywords: options.inShadowing?.allowKeywords || false,
allowSmartIntents: options.inShadowing?.allowSmartIntents || false,
allowKnowledgeBases: options.inShadowing?.allowKnowledgeBases || false,
}
this.contentFilters = options.contentFilters || []
this.customRatingMessageEnabled =
options.customRatingMessageEnabled || false
}
resolveFlowUrl(request: PluginPreRequest): string {
if (request.session.is_test_integration) {
return `${this.apiUrl}/v1/bot_flows/{bot_id}/versions/${FlowBuilderJSONVersion.DRAFT}/`
}
return `${this.apiUrl}/v1/bot_flows/{bot_id}/versions/${this.jsonVersion}/`
}
async pre(request: PluginPreRequest): Promise<void> {
// When AI Agent is executed in Whatsapp, button payloads come as referral and must be converted to text being processed by the agent.
this.convertWhatsappAiAgentEmptyPayloads(request)
this.currentRequest = request
this.cmsApi = await FlowBuilderApi.create({
flowUrl: this.resolveFlowUrl(request),
url: this.apiUrl,
flow: this.flow,
accessToken: this.getAccessToken(request.session),
request: this.currentRequest,
})
const checkUserTextInput =
inputHasTextData(request.input) && !request.input.payload
if (checkUserTextInput) {
const resolvedLocale = this.cmsApi.getResolvedLocale()
const nodeByUserInput = await getNodeByUserInput(
this.cmsApi,
resolvedLocale,
request as unknown as ActionRequest,
this.smartIntentsConfig
)
request.input.payload = this.cmsApi.getPayload(nodeByUserInput?.target)
}
this.updateRequestBeforeRoutes(request)
}
private convertWhatsappAiAgentEmptyPayloads(request: PluginPreRequest): void {
if (request.session.user.provider === PROVIDER.WHATSAPP) {
const shouldUseReferral =
request.input.referral &&
request.input.payload?.startsWith(EMPTY_PAYLOAD)
if (shouldUseReferral) {
request.input.type = INPUT.TEXT
request.input.data = request.input.referral
}
}
}
private updateRequestBeforeRoutes(request: PluginPreRequest): void {
if (request.input.payload) {
request.input.payload = this.removeSourceSuffix(request.input.payload)
if (this.cmsApi.isBotAction(request.input.payload)) {
const cmsBotAction = this.cmsApi.getNodeById<HtBotActionNode>(
request.input.payload
)
request.input.payload =
this.cmsApi.createPayloadWithParams(cmsBotAction)
}
}
}
private removeSourceSuffix(payload: string): string {
return payload.split(SOURCE_INFO_SEPARATOR)[0]
}
post(request: PluginPreRequest): void {
request.input.nluResolution = undefined
}
async getContentsByContentID(
contentID: string,
prevContents?: FlowContent[]
): Promise<FlowContent[]> {
const node = this.cmsApi.getNodeByContentID(contentID) as HtNodeWithContent
return await this.getContentsByNode(node, prevContents)
}
getUUIDByContentID(contentID: string): string {
const node = this.cmsApi.getNodeByContentID(contentID)
return node.id
}
private async getContentsById(
id: string,
prevContents?: FlowContent[]
): Promise<FlowContent[]> {
const node = this.cmsApi.getNodeById(id) as HtNodeWithContent
return await this.getContentsByNode(node, prevContents)
}
async getStartContents(): Promise<FlowContent[]> {
const startNode = this.cmsApi.getStartNode()
this.currentRequest.session.flow_thread_id = uuidv7()
return await this.getContentsByNode(startNode)
}
async getContentsByNode(
node: HtNodeWithContent,
prevContents?: FlowContent[]
): Promise<FlowContent[]> {
const contents = prevContents || []
const resolvedLocale = this.cmsApi.getResolvedLocale()
if (
node.type === HtNodeWithContentType.FUNCTION &&
!DEFAULT_FUNCTION_NAMES.includes(node.content.action)
) {
const customFunctionResolver = new CustomFunction(
this.functions,
this.currentRequest,
resolvedLocale
)
const targetId = await customFunctionResolver.call(node)
return this.getContentsById(targetId, contents)
}
const flowFactory = new FlowFactory(
this.currentRequest,
this.cmsApi,
resolvedLocale
)
const content = await flowFactory.getFlowContent(node)
if (content) {
contents.push(content)
}
// If node is BOT_ACTION not add more contents to render, next nodes render after execute action
if (node.type === HtNodeWithContentType.BOT_ACTION) {
return contents
}
// TODO: prevent infinite recursive calls
if (content && content.followUp) {
return this.getContentsById(content.followUp.id, contents)
} else if (node.follow_up) {
console.log('FOLLOWUP FROM NODE-------> OLD SYSTEM')
return this.getContentsById(node.follow_up.id, contents)
}
return contents
}
getPayloadParams<T extends PayloadParamsBase>(payload: string): T {
const payloadParams = JSON.parse(payload.split(SEPARATOR)[1] || '{}')
return payloadParams
}
getFlowName(flowId: string): string {
return this.cmsApi.getFlowName(flowId)
}
getRatingSubmittedInfo(payload: string): RatingSubmittedInfo {
const buttonId = payload?.split(SEPARATOR)[1]
const ratingNode = this.cmsApi.getRatingNodeByButtonId(buttonId)
const ratingButton = this.cmsApi.getRatingButtonById(ratingNode, buttonId)
const possibleOptions = ratingNode.content.buttons.map(
button => button.text
)
const possibleValues = ratingNode.content.buttons.map(
button => button.value
)
return {
...ratingButton,
possibleOptions,
possibleValues,
}
}
}
export * from './action'
export { AGENT_RATING_PAYLOAD } from './constants'
export * from './content-fields'
export { HtBotActionNode } from './content-fields/hubtype-fields'
export { trackFlowContent } from './tracking'
export {
BotonicPluginFlowBuilderOptions,
ContentFilter,
FlowBuilderJSONVersion,
PayloadParamsBase,
RatingSubmittedInfo,
} from './types'
export * from './webview'