@botonic/plugin-flow-builder 0.21.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/lib/action.d.ts +15 -0
- package/lib/action.js +62 -0
- package/lib/action.js.map +1 -0
- package/lib/content-fields/button.d.ts +10 -0
- package/lib/content-fields/button.js +33 -0
- package/lib/content-fields/button.js.map +1 -0
- package/lib/content-fields/carousel.d.ts +10 -0
- package/lib/content-fields/carousel.js +37 -0
- package/lib/content-fields/carousel.js.map +1 -0
- package/lib/content-fields/content-base.d.ts +10 -0
- package/lib/content-fields/content-base.js +15 -0
- package/lib/content-fields/content-base.js.map +1 -0
- package/lib/content-fields/element.d.ts +11 -0
- package/lib/content-fields/element.js +25 -0
- package/lib/content-fields/element.js.map +1 -0
- package/lib/content-fields/image.d.ts +9 -0
- package/lib/content-fields/image.js +28 -0
- package/lib/content-fields/image.js.map +1 -0
- package/lib/content-fields/text.d.ts +12 -0
- package/lib/content-fields/text.js +35 -0
- package/lib/content-fields/text.js.map +1 -0
- package/lib/functions/conditional-provider.d.ts +4 -0
- package/lib/functions/conditional-provider.js +11 -0
- package/lib/functions/conditional-provider.js.map +1 -0
- package/lib/functions/conditional-queue-status.d.ts +3 -0
- package/lib/functions/conditional-queue-status.js +25 -0
- package/lib/functions/conditional-queue-status.js.map +1 -0
- package/lib/functions/index.d.ts +6 -0
- package/lib/functions/index.js +10 -0
- package/lib/functions/index.js.map +1 -0
- package/lib/handoff.d.ts +2 -0
- package/lib/handoff.js +39 -0
- package/lib/handoff.js.map +1 -0
- package/lib/hubtype-models.d.ts +152 -0
- package/lib/hubtype-models.js +50 -0
- package/lib/hubtype-models.js.map +1 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.js +171 -0
- package/lib/index.js.map +1 -0
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +10 -0
- package/lib/utils.js.map +1 -0
- package/package.json +53 -0
- package/src/action.tsx +62 -0
- package/src/content-fields/button.tsx +34 -0
- package/src/content-fields/carousel.tsx +42 -0
- package/src/content-fields/content-base.ts +15 -0
- package/src/content-fields/element.tsx +24 -0
- package/src/content-fields/image.tsx +22 -0
- package/src/content-fields/text.tsx +35 -0
- package/src/functions/conditional-provider.ts +5 -0
- package/src/functions/conditional-queue-status.ts +9 -0
- package/src/functions/index.ts +7 -0
- package/src/handoff.ts +27 -0
- package/src/hubtype-models.ts +190 -0
- package/src/index.ts +210 -0
- package/src/utils.ts +6 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export enum ButtonStyle {
|
|
2
|
+
BUTTON = 'button',
|
|
3
|
+
QUICK_REPLY = 'quick-reply',
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export enum MessageContentType {
|
|
7
|
+
CAROUSEL = 'carousel',
|
|
8
|
+
IMAGE = 'image',
|
|
9
|
+
TEXT = 'text',
|
|
10
|
+
KEYWORD = 'keyword',
|
|
11
|
+
HANDOFF = 'handoff',
|
|
12
|
+
FUNCTION = 'function',
|
|
13
|
+
INTENT = 'intent',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// TODO: refactor types correctly
|
|
17
|
+
export enum NonMessageContentType {
|
|
18
|
+
INTENT = 'intent',
|
|
19
|
+
PAYLOAD = 'payload',
|
|
20
|
+
QUEUE = 'queue',
|
|
21
|
+
URL = 'url',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export enum SubContentType {
|
|
25
|
+
BUTTON = 'button',
|
|
26
|
+
ELEMENT = 'element',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export enum MediaContentType {
|
|
30
|
+
ASSET = 'asset',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export enum StartFieldsType {
|
|
34
|
+
STARTUP = 'startUp',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export enum InputContentType {
|
|
38
|
+
INPUT = 'user-input',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export enum InputType {
|
|
42
|
+
INTENTS = 'intents',
|
|
43
|
+
KEYWORDS = 'keywords',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const NodeContentType = {
|
|
47
|
+
...MessageContentType,
|
|
48
|
+
...InputContentType,
|
|
49
|
+
}
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
51
|
+
export type NodeContentType = MessageContentType | InputContentType
|
|
52
|
+
|
|
53
|
+
export interface HtFlowBuilderData {
|
|
54
|
+
version: string
|
|
55
|
+
name: string
|
|
56
|
+
locales: string[]
|
|
57
|
+
nodes: HtNodeComponent[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface HtNodeLink {
|
|
61
|
+
id: string
|
|
62
|
+
type: NodeContentType
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface HtBaseNode {
|
|
66
|
+
id: string
|
|
67
|
+
code: string
|
|
68
|
+
meta: {
|
|
69
|
+
x: number
|
|
70
|
+
y: number
|
|
71
|
+
}
|
|
72
|
+
follow_up?: HtNodeLink
|
|
73
|
+
target?: HtNodeLink
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface HtTextLocale {
|
|
77
|
+
message: string
|
|
78
|
+
locale: string
|
|
79
|
+
}
|
|
80
|
+
export interface HtInputLocale {
|
|
81
|
+
values: string[]
|
|
82
|
+
locale: string
|
|
83
|
+
}
|
|
84
|
+
export interface HtMediaFileLocale {
|
|
85
|
+
id: string
|
|
86
|
+
file: string
|
|
87
|
+
locale: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface HtTextNode extends HtBaseNode {
|
|
91
|
+
type: MessageContentType.TEXT
|
|
92
|
+
content: {
|
|
93
|
+
text: HtTextLocale[]
|
|
94
|
+
buttons_style?: ButtonStyle
|
|
95
|
+
buttons: HtButton[]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface HtButton {
|
|
100
|
+
id: string
|
|
101
|
+
text: HtTextLocale[]
|
|
102
|
+
target?: HtNodeLink
|
|
103
|
+
hidden: string[]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface HtImageNode extends HtBaseNode {
|
|
107
|
+
type: MessageContentType.IMAGE
|
|
108
|
+
content: {
|
|
109
|
+
image: HtMediaFileLocale[]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface HtCarouselNode extends HtBaseNode {
|
|
114
|
+
type: MessageContentType.CAROUSEL
|
|
115
|
+
content: {
|
|
116
|
+
elements: HtElement[]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface HtElement {
|
|
121
|
+
id: string
|
|
122
|
+
title: HtTextLocale[]
|
|
123
|
+
subtitle: HtTextLocale[]
|
|
124
|
+
image: HtMediaFileLocale[]
|
|
125
|
+
button: HtButton
|
|
126
|
+
hidden: string[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface HtIntentNode extends HtBaseNode {
|
|
130
|
+
type: MessageContentType.INTENT
|
|
131
|
+
content: {
|
|
132
|
+
title: HtTextLocale[]
|
|
133
|
+
intents: HtInputLocale[]
|
|
134
|
+
confidence: number
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export interface HtKeywordNode extends HtBaseNode {
|
|
138
|
+
type: MessageContentType.KEYWORD
|
|
139
|
+
content: {
|
|
140
|
+
title: HtTextLocale[]
|
|
141
|
+
keywords: HtInputLocale[]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface HtHandoffNode extends HtBaseNode {
|
|
146
|
+
type: MessageContentType.HANDOFF
|
|
147
|
+
content: {
|
|
148
|
+
queue: HtQueueLocale[]
|
|
149
|
+
message: HtTextLocale[]
|
|
150
|
+
failMessage: HtTextLocale[]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface HtQueueLocale {
|
|
155
|
+
id: string
|
|
156
|
+
name: string
|
|
157
|
+
locale: string
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface HtFunctionNode extends HtBaseNode {
|
|
161
|
+
type: MessageContentType.FUNCTION
|
|
162
|
+
content: {
|
|
163
|
+
subtype: string
|
|
164
|
+
action: string
|
|
165
|
+
arguments: Array<any>
|
|
166
|
+
result_mapping: Array<any>
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/*
|
|
171
|
+
subtype: "conditional-queue-status",
|
|
172
|
+
action: "check-queue-status",
|
|
173
|
+
arguments: [{
|
|
174
|
+
locale: "es-ES",
|
|
175
|
+
values: [{type: "string", value: "056236e2-bcb5-452e-9020-e7e44e5769fc"}]
|
|
176
|
+
}],
|
|
177
|
+
result_mapping: [
|
|
178
|
+
{ result: "open", target: { id: "0fab7257-6773-4ae9-9deb-cb0b6a864d08", type: "carousel" }},
|
|
179
|
+
{ result: "closed", target: { id: "4fb1b430-f719-47d2-8e51-dcbc6d3cfa77", type: "image" }}
|
|
180
|
+
]
|
|
181
|
+
*/
|
|
182
|
+
|
|
183
|
+
export type HtNodeComponent =
|
|
184
|
+
| HtTextNode
|
|
185
|
+
| HtImageNode
|
|
186
|
+
| HtCarouselNode
|
|
187
|
+
| HtIntentNode
|
|
188
|
+
| HtKeywordNode
|
|
189
|
+
| HtHandoffNode
|
|
190
|
+
| HtFunctionNode
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Input,
|
|
3
|
+
Plugin,
|
|
4
|
+
PluginPostRequest,
|
|
5
|
+
PluginPreRequest,
|
|
6
|
+
} from '@botonic/core'
|
|
7
|
+
import axios from 'axios'
|
|
8
|
+
|
|
9
|
+
import { FlowCarousel } from './content-fields/carousel'
|
|
10
|
+
import { FlowContent } from './content-fields/content-base'
|
|
11
|
+
import { FlowImage } from './content-fields/image'
|
|
12
|
+
import { FlowText } from './content-fields/text'
|
|
13
|
+
import { DEFAULT_FUNCTIONS } from './functions'
|
|
14
|
+
import {
|
|
15
|
+
HtBaseNode,
|
|
16
|
+
HtFlowBuilderData,
|
|
17
|
+
HtFunctionNode,
|
|
18
|
+
HtHandoffNode,
|
|
19
|
+
HtIntentNode,
|
|
20
|
+
HtKeywordNode,
|
|
21
|
+
HtNodeComponent,
|
|
22
|
+
MessageContentType,
|
|
23
|
+
} from './hubtype-models'
|
|
24
|
+
|
|
25
|
+
type BotonicPluginFlowBuilderOptions = {
|
|
26
|
+
flowUrl: string
|
|
27
|
+
flow: any
|
|
28
|
+
customFunctions: Record<any, any>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default class BotonicPluginFlowBuilder implements Plugin {
|
|
32
|
+
private flowUrl: string
|
|
33
|
+
private flow: Promise<HtFlowBuilderData>
|
|
34
|
+
private functions: Record<any, any>
|
|
35
|
+
private currentRequest: PluginPreRequest
|
|
36
|
+
|
|
37
|
+
constructor(readonly options: BotonicPluginFlowBuilderOptions) {
|
|
38
|
+
this.flowUrl = options.flowUrl
|
|
39
|
+
this.flow = options.flow || this.readFlowContent()
|
|
40
|
+
const customFunctions = options.customFunctions || {}
|
|
41
|
+
this.functions = { ...DEFAULT_FUNCTIONS, ...customFunctions }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async readFlowContent() {
|
|
45
|
+
const response = await axios.get(this.flowUrl)
|
|
46
|
+
const data = await response.data
|
|
47
|
+
//@ts-ignore
|
|
48
|
+
return Promise.resolve(data as HtFlowBuilderData)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async pre(request: PluginPreRequest): Promise<void> {
|
|
52
|
+
this.currentRequest = request
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async post(_request: PluginPostRequest): Promise<void> {}
|
|
56
|
+
|
|
57
|
+
async getContent(id: string): Promise<HtNodeComponent> {
|
|
58
|
+
const flow = await this.flow
|
|
59
|
+
const content = flow.nodes.find((c: HtBaseNode) => c.id === id)
|
|
60
|
+
if (!content) throw Error(`text with id: '${id}' not found`)
|
|
61
|
+
return content
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getHandoffContent(): Promise<HtHandoffNode> {
|
|
65
|
+
const flow = await this.flow
|
|
66
|
+
const content = flow.nodes.find(
|
|
67
|
+
(c: HtNodeComponent) => c.type === 'handoff'
|
|
68
|
+
) as HtHandoffNode
|
|
69
|
+
if (!content) throw Error(`Handoff node not found`)
|
|
70
|
+
return content
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getFlowContent(
|
|
74
|
+
hubtypeContent: HtNodeComponent,
|
|
75
|
+
locale: string
|
|
76
|
+
): FlowContent | undefined {
|
|
77
|
+
switch (hubtypeContent.type) {
|
|
78
|
+
case MessageContentType.TEXT:
|
|
79
|
+
return FlowText.fromHubtypeCMS(hubtypeContent, locale)
|
|
80
|
+
case MessageContentType.IMAGE:
|
|
81
|
+
return FlowImage.fromHubtypeCMS(hubtypeContent, locale)
|
|
82
|
+
case MessageContentType.CAROUSEL:
|
|
83
|
+
return FlowCarousel.fromHubtypeCMS(hubtypeContent, locale)
|
|
84
|
+
default:
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getContents(
|
|
90
|
+
id: string,
|
|
91
|
+
locale: string,
|
|
92
|
+
prevContents?: FlowContent[]
|
|
93
|
+
): Promise<FlowContent[]> {
|
|
94
|
+
const contents = prevContents || []
|
|
95
|
+
const hubtypeContent = await this.getContent(id)
|
|
96
|
+
const content = await this.getFlowContent(hubtypeContent, locale)
|
|
97
|
+
if (hubtypeContent.type === MessageContentType.FUNCTION) {
|
|
98
|
+
const targetId = await this.callFunction(
|
|
99
|
+
hubtypeContent as HtFunctionNode,
|
|
100
|
+
locale
|
|
101
|
+
)
|
|
102
|
+
return this.getContents(targetId, locale, contents)
|
|
103
|
+
} else {
|
|
104
|
+
if (content) contents.push(content)
|
|
105
|
+
// TODO: prevent infinite recursive calls
|
|
106
|
+
if (hubtypeContent.follow_up)
|
|
107
|
+
return this.getContents(hubtypeContent.follow_up.id, locale, contents)
|
|
108
|
+
}
|
|
109
|
+
// execute function
|
|
110
|
+
// return this.getContents(function result_mapping target, locale, contents)
|
|
111
|
+
return contents
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async getPayloadByInput(
|
|
115
|
+
input: Input,
|
|
116
|
+
locale: string
|
|
117
|
+
): Promise<string | undefined> {
|
|
118
|
+
try {
|
|
119
|
+
const flow = await this.flow
|
|
120
|
+
const intents = flow.nodes.filter(
|
|
121
|
+
node => node.type == MessageContentType.INTENT
|
|
122
|
+
) as HtIntentNode[]
|
|
123
|
+
if (input.intent) {
|
|
124
|
+
const matchedIntents = intents.filter(node =>
|
|
125
|
+
//@ts-ignore
|
|
126
|
+
this.hasIntent(node, input.intent, locale)
|
|
127
|
+
)
|
|
128
|
+
if (matchedIntents.length > 0) {
|
|
129
|
+
return matchedIntents[0].target?.id
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Error getting payload by input: ', error)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
hasIntent(node: HtIntentNode, intent: string, locale: string) {
|
|
140
|
+
const result = node.content.intents.find(
|
|
141
|
+
i => i.locale === locale && i.values.includes(intent)
|
|
142
|
+
)
|
|
143
|
+
return Boolean(result)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getPayloadByKeyword(
|
|
147
|
+
input: Input,
|
|
148
|
+
locale: string
|
|
149
|
+
): Promise<string | undefined> {
|
|
150
|
+
try {
|
|
151
|
+
const flow = await this.flow
|
|
152
|
+
const keywordNodes = flow.nodes.filter(
|
|
153
|
+
node => node.type == MessageContentType.KEYWORD
|
|
154
|
+
) as HtKeywordNode[]
|
|
155
|
+
const matchedKeywordNodes = keywordNodes.filter(node =>
|
|
156
|
+
//@ts-ignore
|
|
157
|
+
this.matchKeywords(node, input.data, locale)
|
|
158
|
+
)
|
|
159
|
+
if (matchedKeywordNodes.length > 0) {
|
|
160
|
+
return matchedKeywordNodes[0].target?.id
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('Error getting payload by input: ', error)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return undefined
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
matchKeywords(node: HtKeywordNode, input: string, locale: string) {
|
|
170
|
+
const result = node.content.keywords.find(
|
|
171
|
+
i => i.locale === locale && this.containsAnyKeywords(input, i.values)
|
|
172
|
+
)
|
|
173
|
+
return Boolean(result)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
containsAnyKeywords(input: string, keywords: string[]) {
|
|
177
|
+
for (let i = 0; i < keywords.length; i++) {
|
|
178
|
+
if (input.includes(keywords[i])) {
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async callFunction(
|
|
186
|
+
functionNode: HtFunctionNode,
|
|
187
|
+
locale: string
|
|
188
|
+
): Promise<string> {
|
|
189
|
+
// Check if target is missing or missing arguments
|
|
190
|
+
// TODO: get arguments by locale
|
|
191
|
+
const nameValues = functionNode.content.arguments
|
|
192
|
+
.find(arg => arg.locale === locale)
|
|
193
|
+
.values.map(value => ({ [value.name]: value.value }))
|
|
194
|
+
const args = Object.assign(
|
|
195
|
+
{
|
|
196
|
+
session: this.currentRequest.session,
|
|
197
|
+
results: [functionNode.content.result_mapping.map(r => r.result)],
|
|
198
|
+
},
|
|
199
|
+
...nameValues
|
|
200
|
+
)
|
|
201
|
+
const functionResult = await this.functions[functionNode.content.subtype](
|
|
202
|
+
args
|
|
203
|
+
)
|
|
204
|
+
// TODO define result_mapping per locale??
|
|
205
|
+
const result = functionNode.content.result_mapping.find(
|
|
206
|
+
r => r.result === functionResult
|
|
207
|
+
)
|
|
208
|
+
return result.target.id
|
|
209
|
+
}
|
|
210
|
+
}
|