@scout9/app 1.0.0-alpha.0.1.9 → 1.0.0-alpha.0.1.91
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 +33 -0
- package/dist/{index-92deaa5f.cjs → exports-212ef6be.cjs} +46636 -4591
- package/dist/index.cjs +58 -15
- package/dist/{multipart-parser-090f08a9.cjs → multipart-parser-54a3ab5f.cjs} +13 -7
- package/dist/spirits-3b603262.cjs +1218 -0
- package/dist/spirits.cjs +9 -0
- package/dist/testing-tools.cjs +48 -0
- package/package.json +37 -8
- package/src/cli.js +162 -69
- package/src/core/config/agents.js +300 -7
- package/src/core/config/entities.js +58 -28
- package/src/core/config/index.js +37 -15
- package/src/core/config/project.js +160 -6
- package/src/core/config/workflow.js +13 -12
- package/src/core/data.js +27 -0
- package/src/core/index.js +386 -137
- package/src/core/sync.js +71 -0
- package/src/core/templates/Dockerfile +22 -0
- package/src/core/templates/app.js +453 -0
- package/src/core/templates/project-files.js +36 -0
- package/src/core/templates/template-package.json +13 -0
- package/src/exports.js +21 -17
- package/src/platform.js +189 -33
- package/src/public.d.ts.text +330 -0
- package/src/report.js +117 -0
- package/src/runtime/client/api.js +56 -159
- package/src/runtime/client/config.js +60 -11
- package/src/runtime/client/entity.js +19 -6
- package/src/runtime/client/index.js +5 -3
- package/src/runtime/client/message.js +13 -3
- package/src/runtime/client/platform.js +86 -0
- package/src/runtime/client/{agent.js → users.js} +35 -3
- package/src/runtime/client/utils.js +10 -9
- package/src/runtime/client/workflow.js +132 -9
- package/src/runtime/entry.js +2 -2
- package/src/testing-tools/dev.js +373 -0
- package/src/testing-tools/index.js +1 -0
- package/src/testing-tools/mocks.js +37 -5
- package/src/testing-tools/spirits.js +530 -0
- package/src/utils/audio-buffer.js +16 -0
- package/src/utils/audio-type.js +27 -0
- package/src/utils/configs/agents.js +68 -0
- package/src/utils/configs/entities.js +145 -0
- package/src/utils/configs/project.js +23 -0
- package/src/utils/configs/workflow.js +47 -0
- package/src/utils/file-type.js +569 -0
- package/src/utils/file.js +164 -0
- package/src/utils/glob.js +30 -0
- package/src/utils/image-buffer.js +23 -0
- package/src/utils/image-type.js +39 -0
- package/src/utils/index.js +1 -0
- package/src/utils/is-svg.js +37 -0
- package/src/utils/logger.js +111 -0
- package/src/utils/module.js +14 -25
- package/src/utils/project-templates.js +191 -0
- package/src/utils/project.js +387 -0
- package/src/utils/video-type.js +29 -0
- package/types/index.d.ts +7588 -206
- package/types/index.d.ts.map +97 -22
- package/dist/index-1b8d7dd2.cjs +0 -49555
- package/dist/index-2ccb115e.cjs +0 -49514
- package/dist/index-66b06a30.cjs +0 -49549
- package/dist/index-bc029a1d.cjs +0 -49528
- package/dist/index-d9a93523.cjs +0 -49527
- package/dist/multipart-parser-1508046a.cjs +0 -413
- package/dist/multipart-parser-7007403a.cjs +0 -413
- package/dist/multipart-parser-70c32c1d.cjs +0 -413
- package/dist/multipart-parser-71dec101.cjs +0 -413
- package/dist/multipart-parser-f15bf2e0.cjs +0 -414
- package/src/public.d.ts +0 -209
|
@@ -2,8 +2,14 @@ import { z } from 'zod';
|
|
|
2
2
|
import { zId } from './utils.js';
|
|
3
3
|
import { MessageSchema } from './message.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('zod').infer<typeof customerValueSchema>} ICustomerValue
|
|
7
|
+
*/
|
|
5
8
|
export const customerValueSchema = z.union([z.boolean(), z.number(), z.string()]);
|
|
6
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('zod').infer<typeof customerSchema>} ICustomer
|
|
12
|
+
*/
|
|
7
13
|
export const customerSchema = z.object({
|
|
8
14
|
firstName: z.string().optional(),
|
|
9
15
|
lastName: z.string().optional(),
|
|
@@ -24,8 +30,16 @@ export const customerSchema = z.object({
|
|
|
24
30
|
stripeDev: z.string().nullable().optional()
|
|
25
31
|
}).catchall(customerValueSchema);
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {import('zod').infer<typeof agentBaseConfigurationSchema>} IAgentBase
|
|
35
|
+
*/
|
|
36
|
+
export const agentBaseConfigurationSchema = z.object({
|
|
37
|
+
deployed: z.object({
|
|
38
|
+
web: z.string({description: 'Web URL for agent'}).optional(),
|
|
39
|
+
phone: z.string({description: 'Phone number for agent'}).optional(),
|
|
40
|
+
email: z.string({description: 'Email address for agent'}).optional()
|
|
41
|
+
}).optional(),
|
|
42
|
+
img: z.string().nullable().optional(),
|
|
29
43
|
firstName: z.string({description: 'Agent first name'}).optional(),
|
|
30
44
|
lastName: z.string({description: 'Agent last name'}).optional(),
|
|
31
45
|
inactive: z.boolean({description: 'Agent is inactive'}).optional(),
|
|
@@ -40,6 +54,24 @@ export const agentConfigurationSchema = z.object({
|
|
|
40
54
|
excludedLocations: z.array(z.string({description: 'Locations the agent is excluded from'})).optional(),
|
|
41
55
|
model: z.enum(['Scout9', 'bard', 'openai']).optional().default('openai'),
|
|
42
56
|
transcripts: z.array(z.array(MessageSchema)).optional(),
|
|
43
|
-
|
|
57
|
+
audios: z.array(z.any()).optional()
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {import('zod').infer<typeof agentBaseConfigurationSchema>} IAgent
|
|
62
|
+
* @typedef {import('zod').infer<typeof agentBaseConfigurationSchema>} IPersona
|
|
63
|
+
*/
|
|
64
|
+
export const agentConfigurationSchema = agentBaseConfigurationSchema.extend({
|
|
65
|
+
id: zId('Agent ID', {description: 'Unique ID for agent'}),
|
|
44
66
|
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @typedef {import('zod').infer<typeof agentsConfigurationSchema>} IAgentsConfiguration
|
|
70
|
+
*/
|
|
45
71
|
export const agentsConfigurationSchema = z.array(agentConfigurationSchema);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @typedef {import('zod').infer<typeof agentsBaseConfigurationSchema>} IAgentsBaseConfiguration
|
|
75
|
+
*/
|
|
76
|
+
export const agentsBaseConfigurationSchema = z.array(agentBaseConfigurationSchema);
|
|
77
|
+
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} name
|
|
5
|
+
* @param {Object} [props]
|
|
6
|
+
* @returns {import('zod').ZodString}
|
|
7
|
+
*/
|
|
8
|
+
export function zId(name, props = {}) {
|
|
9
|
+
return z.string(props).regex(/^[A-Za-z0-9\-_\[\]]+$/, {
|
|
10
|
+
message: `Invalid ${name} ID: ID must not contain spaces and should only contain alphanumeric characters, dashes, or underscores.`
|
|
11
|
+
});
|
|
11
12
|
}
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { zId } from './utils.js';
|
|
5
|
-
import { agentConfigurationSchema, customerSchema } from './
|
|
5
|
+
import { agentConfigurationSchema, customerSchema } from './users.js';
|
|
6
6
|
import { MessageSchema } from './message.js';
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('zod').infer<typeof WorkflowConfigurationSchema>} IWorkflowConfiguration
|
|
12
|
+
*/
|
|
10
13
|
export const WorkflowConfigurationSchema = z.object({
|
|
11
14
|
entities: z.array(zId('Workflow Folder', z.string()), {description: 'Workflow id association, used to handle route params'})
|
|
12
15
|
.min(1, 'Must have at least 1 entity')
|
|
@@ -14,8 +17,15 @@ export const WorkflowConfigurationSchema = z.object({
|
|
|
14
17
|
entity: zId('Workflow Folder', z.string()),
|
|
15
18
|
});
|
|
16
19
|
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {import('zod').infer<typeof WorkflowsConfigurationSchema>} IWorkflowsConfiguration
|
|
22
|
+
*/
|
|
17
23
|
export const WorkflowsConfigurationSchema = z.array(WorkflowConfigurationSchema);
|
|
18
24
|
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {import('zod').infer<typeof ConversationSchema>} IConversation
|
|
28
|
+
*/
|
|
19
29
|
export const ConversationSchema = z.object({
|
|
20
30
|
$agent: zId('Conversation Agent ID', z.string({description: 'Default agent assigned to the conversation(s)'})),
|
|
21
31
|
$customer: zId('Conversation Customer ID', z.string({description: 'Customer this conversation is with'})),
|
|
@@ -25,22 +35,54 @@ export const ConversationSchema = z.object({
|
|
|
25
35
|
subject: z.string({description: 'HTML Subject of the conversation'}).optional(),
|
|
26
36
|
platformEmailThreadId: z.string({description: 'Used to sync email messages with the conversation'}).optional(),
|
|
27
37
|
}).optional(),
|
|
38
|
+
locked: z.boolean({description: 'Whether the conversation is locked or not'}).optional().nullable(),
|
|
39
|
+
lockedReason: z.string({description: 'Why this conversation was locked'}).optional().nullable(),
|
|
40
|
+
lockAttempts: z.number({description: 'Number attempts made until conversation is locked'}).optional().nullable(),
|
|
41
|
+
forwardedTo: z.string({description: 'What personaId/phone/email was forwarded'}).optional().nullable(),
|
|
42
|
+
forwarded: z.string({description: 'Datetime ISO 8601 timestamp when persona was forwarded'}).optional().nullable(),
|
|
43
|
+
forwardNote: z.string().optional().nullable(),
|
|
44
|
+
intent: z.string({description: 'Detected intent of conversation'}).optional().nullable(),
|
|
45
|
+
intentScore: z.number({description: 'Confidence score of the assigned intent'}).optional().nullable(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {import('zod').infer<typeof IntentWorkflowEventSchema>} IIntentWorkflowEvent
|
|
50
|
+
*/
|
|
51
|
+
export const IntentWorkflowEventSchema = z.object({
|
|
52
|
+
current: z.string().nullable(),
|
|
53
|
+
flow: z.array(z.string()),
|
|
54
|
+
initial: z.string().nullable()
|
|
28
55
|
});
|
|
29
56
|
|
|
30
|
-
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {import('zod').infer<typeof WorkflowEventSchema>} IWorkflowEvent
|
|
59
|
+
*/
|
|
60
|
+
export const WorkflowEventSchema = z.object({
|
|
31
61
|
messages: z.array(MessageSchema),
|
|
32
62
|
conversation: ConversationSchema,
|
|
33
63
|
context: z.any(),
|
|
34
64
|
message: MessageSchema,
|
|
35
|
-
agent: agentConfigurationSchema
|
|
65
|
+
agent: agentConfigurationSchema.omit({
|
|
66
|
+
transcripts: true,
|
|
67
|
+
audios: true,
|
|
68
|
+
includedLocations: true,
|
|
69
|
+
excludedLocations: true,
|
|
70
|
+
model: true,
|
|
71
|
+
context: true
|
|
72
|
+
}),
|
|
36
73
|
customer: customerSchema,
|
|
37
|
-
intent:
|
|
38
|
-
|
|
74
|
+
intent: IntentWorkflowEventSchema,
|
|
75
|
+
stagnationCount: z.number(),
|
|
76
|
+
note: z.string({description: 'Any developer notes to provide'}).optional()
|
|
77
|
+
})
|
|
39
78
|
|
|
40
79
|
const Primitive = z.union([z.string(), z.number(), z.boolean()]);
|
|
41
80
|
// Assuming ConversationContext is already defined as a Zod schema
|
|
42
81
|
|
|
43
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Lazy is used to handle recursive types.
|
|
84
|
+
* @typedef {import('zod').infer<typeof ConversationContext>} IConversation
|
|
85
|
+
*/
|
|
44
86
|
export const ConversationContext = z.lazy(() =>
|
|
45
87
|
z.record(
|
|
46
88
|
Primitive.or(ConversationContext)
|
|
@@ -49,32 +91,113 @@ export const ConversationContext = z.lazy(() =>
|
|
|
49
91
|
|
|
50
92
|
const ContextSchema = z.record(Primitive.or(ConversationContext));
|
|
51
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Forward input information of a conversation
|
|
96
|
+
* @typedef {import('zod').infer<typeof ForwardSchema>} IForward
|
|
97
|
+
*/
|
|
52
98
|
export const ForwardSchema = z.union([
|
|
53
99
|
z.boolean(),
|
|
54
100
|
z.string(),
|
|
55
101
|
z.object({
|
|
56
102
|
to: z.string().optional(),
|
|
57
103
|
mode: z.enum(['after-reply', 'immediately']).optional(),
|
|
104
|
+
note: z.string({description: 'Note to provide to the agent'}).optional()
|
|
58
105
|
}),
|
|
59
|
-
]);
|
|
106
|
+
], {description: 'Forward input information of a conversation'});
|
|
107
|
+
|
|
60
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Instruction object schema used to send context to guide conversations
|
|
111
|
+
* @typedef {import('zod').infer<typeof InstructionSchema>} IInstruction
|
|
112
|
+
*/
|
|
61
113
|
export const InstructionSchema = z.object({
|
|
62
|
-
id: zId('Instruction ID'
|
|
114
|
+
id: zId('Instruction ID').describe('Unique ID for the instruction, this is used to remove the instruction later'),
|
|
63
115
|
content: z.string(),
|
|
64
116
|
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @typedef {import('zod').infer<typeof WorkflowResponseMessageApiRequest>} IWorkflowResponseMessageApiRequest
|
|
120
|
+
*/
|
|
121
|
+
export const WorkflowResponseMessageApiRequest = z.object({
|
|
122
|
+
uri: z.string(),
|
|
123
|
+
data: z.any().optional(),
|
|
124
|
+
headers: z.object({
|
|
125
|
+
[z.string()]: z.string(),
|
|
126
|
+
}).optional(),
|
|
127
|
+
method: z.enum(["GET", "POST", "PUT"]).optional()
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* If its a string, it will be sent as a static string.
|
|
132
|
+
* If it's a object or WorkflowResponseMessageAPI - it will use
|
|
133
|
+
* @typedef {import('zod').infer<typeof WorkflowResponseMessage>} IWorkflowResponseMessage
|
|
134
|
+
*/
|
|
135
|
+
export const WorkflowResponseMessage = z.union(
|
|
136
|
+
z.string(),
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* An api call that should be called later, must return a string or {message: string}
|
|
140
|
+
*/
|
|
141
|
+
WorkflowResponseMessageApiRequest
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The intended response provided by the WorkflowResponseMessageApiRequest
|
|
147
|
+
* @typedef {import('zod').infer<typeof WorkflowResponseMessageApiResponse>} IWorkflowResponseMessageApiResponse
|
|
148
|
+
*/
|
|
149
|
+
export const WorkflowResponseMessageApiResponse = z.union([
|
|
150
|
+
z.string(),
|
|
151
|
+
z.object({
|
|
152
|
+
message: z.string()
|
|
153
|
+
}),
|
|
154
|
+
z.object({
|
|
155
|
+
text: z.string()
|
|
156
|
+
}),
|
|
157
|
+
z.object({
|
|
158
|
+
data: z.object({
|
|
159
|
+
message: z.string()
|
|
160
|
+
})
|
|
161
|
+
}),
|
|
162
|
+
z.object({
|
|
163
|
+
data: z.object({
|
|
164
|
+
text: z.string()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The workflow response object slot
|
|
171
|
+
* @typedef {import('zod').infer<typeof WorkflowResponseSlotSchema>} IWorkflowResponseSlot
|
|
172
|
+
*/
|
|
65
173
|
export const WorkflowResponseSlotSchema = z.object({
|
|
66
174
|
forward: ForwardSchema.optional(),
|
|
175
|
+
forwardNote: z.string({description: 'Note to provide to the agent, recommend using forward object api instead'}).optional(),
|
|
67
176
|
instructions: z.union([z.string(), InstructionSchema, z.array(z.string()), z.array(InstructionSchema)]).optional(),
|
|
68
177
|
removeInstructions: z.array(z.string()).optional(),
|
|
69
178
|
message: z.string().optional(),
|
|
179
|
+
// message: WorkflowResponseMessage.optional(),
|
|
70
180
|
secondsDelay: z.number().optional(),
|
|
71
181
|
scheduled: z.number().optional(),
|
|
72
182
|
contextUpsert: ConversationContext.optional(),
|
|
73
183
|
resetIntent: z.boolean().optional(),
|
|
74
184
|
});
|
|
75
185
|
|
|
76
|
-
|
|
186
|
+
/**
|
|
187
|
+
* The workflow response to send in any given workflow
|
|
188
|
+
* @typedef {import('zod').infer<typeof WorkflowResponseSchema>} IWorkflowResponse
|
|
189
|
+
*/
|
|
77
190
|
export const WorkflowResponseSchema = z.union([
|
|
78
191
|
WorkflowResponseSlotSchema,
|
|
79
192
|
z.array(WorkflowResponseSlotSchema)
|
|
80
193
|
]);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @typedef {import('zod').infer<typeof WorkflowFunctionSchema>} IWorkflowFunction
|
|
197
|
+
*/
|
|
198
|
+
export const WorkflowFunctionSchema = z.function()
|
|
199
|
+
.args(WorkflowEventSchema)
|
|
200
|
+
.returns(z.union([
|
|
201
|
+
z.promise(WorkflowResponseSchema),
|
|
202
|
+
WorkflowResponseSchema,
|
|
203
|
+
]));
|
package/src/runtime/entry.js
CHANGED
|
@@ -63,12 +63,12 @@ function customRequire(folder, moduleName) {
|
|
|
63
63
|
|
|
64
64
|
export async function runInVM(
|
|
65
65
|
event,
|
|
66
|
-
{
|
|
66
|
+
{src, filePath, fileName}
|
|
67
67
|
) {
|
|
68
68
|
// Prepare the script context
|
|
69
69
|
const scriptExports = {};
|
|
70
70
|
const context = vm.createContext({
|
|
71
|
-
require: (moduleName) => customRequire(
|
|
71
|
+
require: (moduleName) => customRequire(src, moduleName),
|
|
72
72
|
console,
|
|
73
73
|
module: { exports: scriptExports },
|
|
74
74
|
exports: scriptExports,
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { Configuration, Scout9Api } from '@scout9/admin';
|
|
2
|
+
import {grey, italic, bgWhite, black} from 'kleur/colors';
|
|
3
|
+
import { createMockConversation, createMockWorkflowEvent } from './mocks.js';
|
|
4
|
+
import { loadConfig } from '../core/config/index.js';
|
|
5
|
+
import { requireProjectFile } from '../utils/index.js';
|
|
6
|
+
import { globSync } from 'glob';
|
|
7
|
+
|
|
8
|
+
export * from './spirits.js';
|
|
9
|
+
|
|
10
|
+
import { Spirits } from './spirits.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Testing tool kit, used to handle Scout9 operations such as parsing, workflow, and generating responses
|
|
14
|
+
*/
|
|
15
|
+
export class Scout9Test {
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @type {import('../runtime/client/users.js').ICustomer}
|
|
19
|
+
*/
|
|
20
|
+
customer;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @type {import('../runtime/client/users.js').IPersona}
|
|
24
|
+
*/
|
|
25
|
+
persona;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @type {import('../runtime/client/workflow.js').IConversation}
|
|
29
|
+
*/
|
|
30
|
+
conversation;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @type {import('../runtime/client/message.js').IMessage[]}
|
|
34
|
+
*/
|
|
35
|
+
messages;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @type {any}
|
|
39
|
+
*/
|
|
40
|
+
context;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @private
|
|
44
|
+
* @type {import('../runtime/client/config.js').IScout9ProjectBuildConfig | null}
|
|
45
|
+
*/
|
|
46
|
+
_project = null;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @private
|
|
50
|
+
* @type {import('../runtime/client/workflow.js').IWorkflowFunction | null}
|
|
51
|
+
*/
|
|
52
|
+
_app = null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @private
|
|
56
|
+
* @type {import('@scout9/admin').Scout9Api | null}
|
|
57
|
+
*/
|
|
58
|
+
_api = null;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
_cwd;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
_src;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
_mode;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
_loaded;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
_personaId;
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Mimics a customer message to your app (useful for testing)
|
|
88
|
+
* @param props - the Scout9Test properties
|
|
89
|
+
* @param {import('../runtime/client/users.js').ICustomer | undefined} [props.customer] - customer to use
|
|
90
|
+
* @param {any | undefined} [props.context] - prior conversation context
|
|
91
|
+
* @param {string | undefined} [props.persona] id to use
|
|
92
|
+
* @param {import('../runtime/client/workflow.js').IConversation | undefined} [props.conversation] - existing conversation
|
|
93
|
+
* @param {string | undefined} [props.cwd]
|
|
94
|
+
* @param {string | undefined} [props.src]
|
|
95
|
+
* @param {string | undefined} [props.mode]
|
|
96
|
+
* @param {import('@scout9/admin').Scout9Api} [props.api]
|
|
97
|
+
* @param {import('../runtime/client/workflow.js').IWorkflowFunction} [props.app]
|
|
98
|
+
* @param {import('../runtime/client/config.js').IScout9ProjectBuildConfig} [props.project]
|
|
99
|
+
*/
|
|
100
|
+
constructor(
|
|
101
|
+
{
|
|
102
|
+
persona,
|
|
103
|
+
customer,
|
|
104
|
+
context,
|
|
105
|
+
conversation = createMockConversation(),
|
|
106
|
+
cwd = process.cwd(),
|
|
107
|
+
src = 'src',
|
|
108
|
+
mode = 'production',
|
|
109
|
+
api,
|
|
110
|
+
app,
|
|
111
|
+
project
|
|
112
|
+
} = {
|
|
113
|
+
cwd: process.cwd(),
|
|
114
|
+
src: 'src',
|
|
115
|
+
mode: 'production'
|
|
116
|
+
}
|
|
117
|
+
) {
|
|
118
|
+
this.messages = [];
|
|
119
|
+
this._cwd = cwd;
|
|
120
|
+
this._src = src;
|
|
121
|
+
this._mode = mode;
|
|
122
|
+
this.context = context || {};
|
|
123
|
+
this.conversation = conversation;
|
|
124
|
+
if (api) {
|
|
125
|
+
this._api = api;
|
|
126
|
+
}
|
|
127
|
+
if (app) {
|
|
128
|
+
this._app = app;
|
|
129
|
+
}
|
|
130
|
+
if (project) {
|
|
131
|
+
this._project = project;
|
|
132
|
+
}
|
|
133
|
+
if (!customer) {
|
|
134
|
+
customer = {
|
|
135
|
+
id: 'mock_customer_' + Math.random().toString(36).slice(2, 11),
|
|
136
|
+
name: 'Mock Customer',
|
|
137
|
+
firstName: 'Mock',
|
|
138
|
+
lastName: 'Customer'
|
|
139
|
+
};
|
|
140
|
+
this.conversation.$customer = customer.id;
|
|
141
|
+
} else {
|
|
142
|
+
this.conversation.$customer = customer.id;
|
|
143
|
+
}
|
|
144
|
+
this.customer = customer;
|
|
145
|
+
this.context.customer = customer;
|
|
146
|
+
this._personaId = persona;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Loads the test environment
|
|
151
|
+
* @param {boolean} [override] - defaults to false, if true, it will override the current loaded state such as the scout9 api, workflow function, and project config
|
|
152
|
+
* @returns {Promise<void>}
|
|
153
|
+
*/
|
|
154
|
+
async load(override = false) {
|
|
155
|
+
|
|
156
|
+
// Load app (if not already loaded or override true)
|
|
157
|
+
if (override || !this._app) {
|
|
158
|
+
this._app = await this._loadApp();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Load app configuration (if not already loaded or override true)
|
|
162
|
+
if (override || !this._project) {
|
|
163
|
+
this._project = await loadConfig({cwd: this._cwd, src: this._src, mode: this._mode})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (override || !this._api) {
|
|
167
|
+
this._api = new Scout9Api(new Configuration({apiKey: process.env.SCOUT9_API_KEY}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!this._personaId) {
|
|
171
|
+
this._personaId = (this._project.persona || this._project.agents)?.[0]?.id;
|
|
172
|
+
if (!this._personaId) {
|
|
173
|
+
throw new Error(`No persona found in config, please specify a persona id`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
this.conversation.$agent = this._personaId;
|
|
177
|
+
this.persona = (this._project.persona || this._project.agents).find(p => p.id === this._personaId);
|
|
178
|
+
if (!this.persona) {
|
|
179
|
+
throw new Error(`Could not find persona with id: ${this._personaId}, ensure your project is sync'd by running "scout9 sync" or you are using the correct persona id`);
|
|
180
|
+
}
|
|
181
|
+
this.context.agent = this.persona;
|
|
182
|
+
this._loaded = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Teardown the test environment
|
|
187
|
+
*/
|
|
188
|
+
teardown() {
|
|
189
|
+
this._loaded = false;
|
|
190
|
+
this._api = null;
|
|
191
|
+
this._project = null;
|
|
192
|
+
this._app = null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Send a message as a customer to your app
|
|
197
|
+
* @param {string} message - message to send
|
|
198
|
+
* @param {import('@scout9/app/testing-tools').StatusCallback | boolean} [progress] - progress callback, if true, will log progress, can override with your own callback. If not provided, no logs will be added.
|
|
199
|
+
* @returns {Promise<ConversationEvent>}
|
|
200
|
+
*/
|
|
201
|
+
async send(message, progress = false) {
|
|
202
|
+
if (!this._loaded) {
|
|
203
|
+
await this.load();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const defaultProgressLogger = (message, level = 'info', type = '') => {
|
|
207
|
+
const typeStdout = type ? italic(bgWhite(' ' + black(type) + ' ')) : '';
|
|
208
|
+
const messageStdout = grey(message);
|
|
209
|
+
(console.hasOwnProperty(level) ? console[level] : console.log)(`\t${typeStdout ? typeStdout + ' ' : ''}${messageStdout}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// If custom logger provided, use it, otherwise use default logger
|
|
213
|
+
let progressInput = typeof progress === 'function' ? progress : defaultProgressLogger;
|
|
214
|
+
|
|
215
|
+
// If progress turned off, use a no-op function
|
|
216
|
+
if (typeof progress === 'boolean') {
|
|
217
|
+
if (!!progress) {
|
|
218
|
+
progressInput = defaultProgressLogger; // use default logger
|
|
219
|
+
} else {
|
|
220
|
+
progressInput = () => {}; // use no-op
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const _message = {
|
|
225
|
+
id: 'user_mock_' + Math.random().toString(36).slice(2, 11),
|
|
226
|
+
role: 'customer',
|
|
227
|
+
content: message,
|
|
228
|
+
time: new Date().toISOString()
|
|
229
|
+
};
|
|
230
|
+
this.messages.push(_message);
|
|
231
|
+
const result = await Spirits.customer({
|
|
232
|
+
customer: this.customer,
|
|
233
|
+
config: this._project,
|
|
234
|
+
parser: async (_msg, _lng) => {
|
|
235
|
+
// @TODO can't do this for HUGE data sets
|
|
236
|
+
const detectableEntities = this._project.entities.filter(e => e.training?.length > 0 && e.definitions?.length > 0);
|
|
237
|
+
return this._api.parse({
|
|
238
|
+
message: _msg,
|
|
239
|
+
language: _lng,
|
|
240
|
+
entities: detectableEntities
|
|
241
|
+
}).then((_res => _res.data));
|
|
242
|
+
},
|
|
243
|
+
workflow: async (event) => {
|
|
244
|
+
return this._app(event);
|
|
245
|
+
},
|
|
246
|
+
generator: (request) => {
|
|
247
|
+
return this._api.generate(request).then((_res => _res.data));
|
|
248
|
+
},
|
|
249
|
+
idGenerator: (prefix) => prefix + '_' + Math.random().toString(36).slice(2, 11),
|
|
250
|
+
progress: progressInput,
|
|
251
|
+
message: _message,
|
|
252
|
+
context: this.context,
|
|
253
|
+
messages: this.messages,
|
|
254
|
+
conversation: this.conversation
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
this.context = result.context.after;
|
|
258
|
+
this.messages = result.messages.after;
|
|
259
|
+
this.conversation = result.conversation.after;
|
|
260
|
+
|
|
261
|
+
if (!!result.conversation.forward) {
|
|
262
|
+
// @TODO migrate this
|
|
263
|
+
if (typeof result.conversation.forward === 'string') {
|
|
264
|
+
this.conversation.forwardedTo = result.conversation.forward;
|
|
265
|
+
} else if (result.conversation.forward === true) {
|
|
266
|
+
this.conversation.forwardedTo = this.persona.forwardPhone || this.persona.forwardEmail || 'No Forward';
|
|
267
|
+
} else if (!!result.conversation.forward?.to) {
|
|
268
|
+
this.conversation.forwardedTo = result.conversation.forward.to;
|
|
269
|
+
} else {
|
|
270
|
+
console.error(`Invalid forward result`, result.conversation.forward);
|
|
271
|
+
this.conversation.forwardedTo = 'Invalid Forward';
|
|
272
|
+
}
|
|
273
|
+
this.conversation.forwarded = new Date().toString();
|
|
274
|
+
this.conversation.forwardNote = result.conversation.forwardNote || '';
|
|
275
|
+
this.conversation.locked = true;
|
|
276
|
+
this.conversation.lockedReason = result.conversation.forwardNote ?? ('Forwarded to ' + this.conversation.forwardedTo);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!result.messages.after.find(m => m.id === result.message.after.id)) {
|
|
280
|
+
console.error(`Message not found in result.messages.after`, result.message.after.id);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Parse user message
|
|
288
|
+
* @param {string} message - message string to parse
|
|
289
|
+
* @param {string} [language] - language to parse in, defaults to "en" for english
|
|
290
|
+
* @returns {Promise<import('@scout9/admin').ParseResponse>}
|
|
291
|
+
*/
|
|
292
|
+
async parse(message, language = 'en') {
|
|
293
|
+
if (!this._project) {
|
|
294
|
+
throw new Error(`Config is not defined`);
|
|
295
|
+
}
|
|
296
|
+
return this._api.parse({
|
|
297
|
+
message,
|
|
298
|
+
language,
|
|
299
|
+
entities: this._project.entities
|
|
300
|
+
}).then((_res => _res.data));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Runs your local app workflow
|
|
305
|
+
* @param {string} message - the message to run through the workflow
|
|
306
|
+
* @param {Omit<Partial<import('../runtime/client/workflow.js').IWorkflowEvent>, 'message'> | undefined} [event] - additional event data
|
|
307
|
+
* @returns {Promise<import('../runtime/client/workflow.js').IWorkflowResponse>}
|
|
308
|
+
*/
|
|
309
|
+
async workflow(message, event = {}) {
|
|
310
|
+
if (!this._app) {
|
|
311
|
+
throw new Error(`Workflow function is not loaded or found - make sure to run ".load()" before calling ".workflow()"`);
|
|
312
|
+
}
|
|
313
|
+
if (event.hasOwnProperty('message')) {
|
|
314
|
+
console.warn(`WARNING: inserting a "event.message" will overwrite your "message" argument`);
|
|
315
|
+
}
|
|
316
|
+
return this._app({
|
|
317
|
+
...createMockWorkflowEvent(message),
|
|
318
|
+
...event
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Generate a response to the user from the given or registered persona's voice in relation to the current conversation's context.
|
|
324
|
+
* @param {Object} [input] - Generation input, defaults to test registered data such as existing messages, context, and persona information.
|
|
325
|
+
* @param {string} [input.personaId] - Persona ID to use, defaults to test registered persona id.
|
|
326
|
+
* @param {Partial<import('@scout9/admin').ConversationCreateRequest>} [input.conversation] - Conversation overrides, defaults to test registered conversation data.
|
|
327
|
+
* @param {import('../runtime/client/message.js').IMessage[]} [input.messages] - Message overrides, defaults to test registered message data.
|
|
328
|
+
* @param {any} [input.context] - Context overrides, defaults to test registered context data.
|
|
329
|
+
* @returns {Promise<import('@scout9/admin').GenerateResponse>}
|
|
330
|
+
*/
|
|
331
|
+
async generate({personaId = this._personaId, conversation = {}, messages = this.messages, context = this.context}) {
|
|
332
|
+
if (!this._api) {
|
|
333
|
+
throw new Error(`Scout9 API is not loaded or found - make sure to run ".load()" before calling ".generate()"`);
|
|
334
|
+
}
|
|
335
|
+
if (!this._project) {
|
|
336
|
+
throw new Error(`Config is not defined - make sure to run ".load()" before calling ".generate()"`);
|
|
337
|
+
}
|
|
338
|
+
const persona = (this._project.persona || this._project.agents).find(p => p.id === personaId);
|
|
339
|
+
if (!persona) {
|
|
340
|
+
throw new Error(`Could not find persona with id: ${personaId}, ensure your project is sync'd by running "scout9 sync"`);
|
|
341
|
+
}
|
|
342
|
+
return this._api.generate({
|
|
343
|
+
convo: {
|
|
344
|
+
$customer: this.customer.id,
|
|
345
|
+
environment: this.conversation.environment,
|
|
346
|
+
initialContexts: this.conversation.initialContexts || [],
|
|
347
|
+
...conversation,
|
|
348
|
+
$agent: persona
|
|
349
|
+
},
|
|
350
|
+
messages,
|
|
351
|
+
context,
|
|
352
|
+
persona,
|
|
353
|
+
llm: this._project.llm,
|
|
354
|
+
pmt: this._project.pmt
|
|
355
|
+
}).then((_res => _res.data));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @private
|
|
360
|
+
*/
|
|
361
|
+
async _loadApp() {
|
|
362
|
+
const paths = globSync(`${this._src}/app.{ts,cjs,mjs,js}`, {cwd: this._cwd, absolute: true});
|
|
363
|
+
if (paths.length === 0) {
|
|
364
|
+
throw new Error(`Missing main project entry file ${this._src}/app.{js|ts|cjs|mjs}`);
|
|
365
|
+
} else if (paths.length > 1) {
|
|
366
|
+
throw new Error(`Multiple main project entry files found ${this._src}/app.{js|ts|cjs|mjs}`);
|
|
367
|
+
}
|
|
368
|
+
const [appFilePath] = paths;
|
|
369
|
+
return requireProjectFile(appFilePath)
|
|
370
|
+
.then(mod => mod.default);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
}
|