UNPKG

@botonic/plugin-flow-builder

Version:

Use Flow Builder to show your contents

303 lines (258 loc) 8.96 kB
import { Input, PluginPreRequest } from '@botonic/core' import axios from 'axios' import { AI_AGENTS_FLOW_NAME, KNOWLEDGE_BASE_FLOW_NAME, SEPARATOR, UUID_REGEXP, } from './constants' import { HtBotActionNode, HtFallbackNode, HtFlowBuilderData, HtGoToFlow, HtKeywordNode, HtNodeComponent, HtNodeLink, HtNodeWithContent, HtNodeWithContentType, HtNodeWithoutContentType, HtPayloadNode, HtRatingButton, HtRatingNode, } from './content-fields/hubtype-fields' import { HtSmartIntentNode } from './content-fields/hubtype-fields/smart-intent' import { FlowBuilderApiOptions, ProcessEnvNodeEnvs } from './types' export class FlowBuilderApi { url: string flowUrl: string flow: HtFlowBuilderData request: PluginPreRequest private constructor() {} static async create(options: FlowBuilderApiOptions): Promise<FlowBuilderApi> { const newApi = new FlowBuilderApi() newApi.url = options.url newApi.request = options.request // TODO: Refactor later to combine logic from `FlowBuilderApi.create`, `resolveFlowUrl` and `getAccessToken` to be in one place if (process.env.NODE_ENV === ProcessEnvNodeEnvs.DEVELOPMENT) { await newApi.updateSessionWithUserInfo(options.accessToken) } const updatedRequest = newApi.request newApi.flowUrl = options.flowUrl.replace( '{bot_id}', updatedRequest.session.bot.id ) newApi.flow = options.flow ?? (await newApi.getFlow(options.accessToken)) return newApi } private async getFlow(token: string): Promise<HtFlowBuilderData> { const { data } = await axios.get(this.flowUrl, { headers: { Authorization: `Bearer ${token}` }, }) return data as HtFlowBuilderData } private async updateSessionWithUserInfo(token: string) { const url = `${this.url}/v1/flow_builder/user_info/` const response = await axios.get(url, { headers: { Authorization: `Bearer ${token}` }, }) this.request.session.organization_id = response.data.organization_id this.request.session.bot.id = response.data.bot_id } getNodeByFlowId(id: string): HtNodeWithContent { const subFlow = this.flow.flows.find(subFlow => subFlow.id === id) if (!subFlow) throw Error(`SubFlow with id: '${id}' not found`) return this.getNodeById<HtNodeWithContent>(subFlow.start_node_id) } getNodeById<T extends HtNodeComponent>(id: string): T { const node = this.flow.nodes.find(node => node.id === id) if (!node) console.error(`Node with id: '${id}' not found`) if (node?.type === HtNodeWithoutContentType.GO_TO_FLOW) { return this.getNodeByFlowId(node.content.flow_id) as T } return node as T } getRatingNodeByButtonId(id: string): HtRatingNode { const ratingNodes = this.flow.nodes.filter( node => node.type === HtNodeWithContentType.RATING ) as HtRatingNode[] const ratingNode = ratingNodes.find(node => node.content.buttons.some(button => button.id === id) ) as HtRatingNode | undefined if (!ratingNode) { throw Error(`Rating node with button id: '${id}' not found`) } return ratingNode } getRatingButtonById(ratingNode: HtRatingNode, id: string): HtRatingButton { const ratingButton = ratingNode.content.buttons.find( button => button.id === id ) as HtRatingButton | undefined if (!ratingButton) { throw Error(`Rating button with id: '${id}' not found`) } return ratingButton } getNodeByContentID(contentID: string): HtNodeComponent { const content = this.flow.nodes.find(node => 'code' in node ? node.code === contentID : false ) if (!content) throw Error(`Node with contentID: '${contentID}' not found`) return content } getStartNode(): HtNodeWithContent { const startNodeId = this.flow.start_node_id if (!startNodeId) throw new Error('Start node id must be defined') return this.getNodeById(startNodeId) } getFallbackNode(alternate: boolean): HtNodeWithContent { const fallbackNode = this.flow.nodes.find( node => node.type === HtNodeWithContentType.FALLBACK ) as HtFallbackNode | undefined if (!fallbackNode) { throw new Error('Fallback node must be defined') } const fallbackFirstMessage = fallbackNode.content.first_message if (!fallbackFirstMessage) { throw new Error('Fallback 1st message must be defined') } const fallbackSecondMessage = fallbackNode.content.second_message if (!fallbackSecondMessage) { return this.getNodeById(fallbackFirstMessage.id) } return alternate ? this.getNodeById(fallbackFirstMessage.id) : this.getNodeById(fallbackSecondMessage.id) } getKnowledgeBaseConfig(): | { followup?: HtNodeLink; isActive: boolean } | undefined { const fallbackNode = this.flow.nodes.find( node => node.type === HtNodeWithContentType.FALLBACK ) as HtFallbackNode | undefined return fallbackNode ? { followup: fallbackNode.content.knowledge_base_followup, isActive: fallbackNode.content.is_knowledge_base_active || false, } : undefined } getSmartIntentNodes(): HtSmartIntentNode[] { return this.flow.nodes.filter( node => node.type === HtNodeWithContentType.SMART_INTENT ) as HtSmartIntentNode[] } getKeywordNodes(): HtKeywordNode[] { return this.flow.nodes.filter( node => node.type === HtNodeWithContentType.KEYWORD ) as HtKeywordNode[] } getPayload(target?: HtNodeLink): string | undefined { if (!target) { return undefined } return target.id } isBotAction(id: string): boolean { if (!this.isUUID(id)) { return false } const node = this.getNodeById(id) return node?.type === HtNodeWithContentType.BOT_ACTION } private isUUID(str: string): boolean { return UUID_REGEXP.test(str) } createPayloadWithParams(botActionNode: HtBotActionNode): string { const payloadId = botActionNode.content.payload_id const payloadNode = this.getNodeById<HtPayloadNode>(payloadId) const customParams = JSON.parse( botActionNode.content.payload_params || '{}' ) const followUpContentID = this.getFollowUpContentID( botActionNode.follow_up?.id ) const payloadJson = JSON.stringify({ ...customParams, followUpContentID, }) return `${payloadNode.content.payload}${SEPARATOR}${payloadJson}` } private getFollowUpContentID(id?: string): string | undefined { const followUpNode = id ? this.getNodeById<HtNodeWithContent | HtGoToFlow>(id) : undefined if (followUpNode?.type === HtNodeWithoutContentType.GO_TO_FLOW) { return this.getNodeById<HtNodeWithContent>(followUpNode?.content.flow_id) .code } else { return followUpNode?.code } } getFlowName(flowId: string): string { const flow = this.flow.flows.find(flow => flow.id === flowId) return flow ? flow.name : '' } getStartNodeKnowledgeBaseFlow(): HtNodeWithContent | undefined { const knowledgeBaseFlow = this.flow.flows.find( flow => flow.name === KNOWLEDGE_BASE_FLOW_NAME ) if (!knowledgeBaseFlow) { return undefined } return this.getNodeById<HtNodeWithContent>(knowledgeBaseFlow.start_node_id) } getStartNodeAiAgentFlow(): HtNodeWithContent | undefined { const aiAgentFlow = this.flow.flows.find( flow => flow.name === AI_AGENTS_FLOW_NAME ) if (!aiAgentFlow) { return undefined } return this.getNodeById<HtNodeWithContent>(aiAgentFlow.start_node_id) } isKnowledgeBaseEnabled(): boolean { return this.flow.is_knowledge_base_active || false } isAiAgentEnabled(): boolean { return this.flow.is_ai_agent_active || false } getResolvedLocale(): string { const systemLocale = this.request.getSystemLocale() const locale = this.resolveAsLocale(systemLocale) if (locale) { return locale } const language = this.resolveAsLanguage(systemLocale) if (language) { this.request.setSystemLocale(language) return language } const defaultLocale = this.resolveAsDefaultLocale() this.request.setSystemLocale(defaultLocale) return defaultLocale } private resolveAsLocale(locale: string): string | undefined { if (this.flow.locales.find(flowLocale => flowLocale === locale)) { return locale } return undefined } private resolveAsLanguage(locale?: string): string | undefined { const language = locale?.split('-')[0] if ( language && this.flow.locales.find(flowLocale => flowLocale === language) ) { console.log(`locale: ${locale} has been resolved as ${language}`) return language } return undefined } private resolveAsDefaultLocale(): string { console.log( `Resolve locale with default locale: ${this.flow.default_locale_code}` ) return this.flow.default_locale_code || 'en' } }