@scout9/app 1.0.0-alpha.0.4.9 → 1.0.0-alpha.0.5.1
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/dist/{dev-51604173.cjs → dev-65b97c9d.cjs} +320 -156
- package/dist/{index-2aa6f869.cjs → index-8a17e150.cjs} +7 -7
- package/dist/index.cjs +5 -4
- package/dist/{macros-c9b4654d.cjs → macros-2f21c706.cjs} +7 -0
- package/dist/{multipart-parser-24b2e9b9.cjs → multipart-parser-67e4f4d6.cjs} +4 -4
- package/dist/schemas.cjs +3 -1
- package/dist/{spirits-8225c9fd.cjs → spirits-2ab4d673.cjs} +12 -0
- package/dist/spirits.cjs +1 -1
- package/dist/testing-tools.cjs +3 -3
- package/package.json +4 -3
- package/src/core/config/commands.js +41 -0
- package/src/core/config/index.js +4 -1
- package/src/core/templates/app.js +483 -78
- package/src/exports.js +1 -0
- package/src/public.d.ts +5 -2
- package/src/runtime/schemas/workflow.js +7 -0
- package/src/testing-tools/dev.js +371 -360
- package/types/index.d.ts +25 -20
- package/types/index.d.ts.map +4 -1
package/src/testing-tools/dev.js
CHANGED
|
@@ -6,391 +6,402 @@ import { requireProjectFile } from '../utils/index.js';
|
|
|
6
6
|
import { globSync } from 'glob';
|
|
7
7
|
|
|
8
8
|
import { Spirits } from './spirits.js';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import { WorkflowResponseSchema } from '../runtime/index.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Testing tool kit, used to handle Scout9 operations such as parsing, workflow, and generating responses
|
|
12
14
|
*/
|
|
13
15
|
export class Scout9Test {
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @type {import('@scout9/app').Persona}
|
|
22
|
-
*/
|
|
23
|
-
persona;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @type {import('@scout9/app').Conversation}
|
|
27
|
-
*/
|
|
28
|
-
conversation;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @type {import('@scout9/app').Message[]}
|
|
32
|
-
*/
|
|
33
|
-
messages;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @type {import('@scout9/app').ConversationContext}
|
|
37
|
-
*/
|
|
38
|
-
context;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @private
|
|
42
|
-
* @type {import('@scout9/app').Scout9ProjectBuildConfig | null}
|
|
43
|
-
*/
|
|
44
|
-
_project = null;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* @private
|
|
48
|
-
* @type {import('@scout9/app').WorkflowFunction | null}
|
|
49
|
-
*/
|
|
50
|
-
_app = null;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @private
|
|
54
|
-
* @type {import('@scout9/admin').Scout9Api | null}
|
|
55
|
-
*/
|
|
56
|
-
_api = null;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @private
|
|
60
|
-
*/
|
|
61
|
-
_cwd;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* @private
|
|
65
|
-
*/
|
|
66
|
-
_src;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @private
|
|
70
|
-
*/
|
|
71
|
-
_mode;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @private
|
|
75
|
-
*/
|
|
76
|
-
_loaded;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @private
|
|
80
|
-
*/
|
|
81
|
-
_personaId;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* @private
|
|
85
|
-
*/
|
|
86
|
-
#defaultLog;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Mimics a customer message to your app (useful for testing)
|
|
91
|
-
* @param props - the Scout9Test properties
|
|
92
|
-
* @param {import('@scout9/app').Customer | undefined} [props.customer] - customer to use
|
|
93
|
-
* @param {any | undefined} [props.context] - prior conversation context
|
|
94
|
-
* @param {string | undefined} [props.persona] id to use
|
|
95
|
-
* @param {import('@scout9/app').Conversation | undefined} [props.conversation] - existing conversation
|
|
96
|
-
* @param {string | undefined} [props.cwd]
|
|
97
|
-
* @param {string | undefined} [props.src]
|
|
98
|
-
* @param {string | undefined} [props.mode]
|
|
99
|
-
* @param {import('@scout9/admin').Scout9Api} [props.api]
|
|
100
|
-
* @param {import('@scout9/app').WorkflowFunction} [props.app]
|
|
101
|
-
* @param {import('@scout9/app').Scout9ProjectBuildConfig} [props.project]
|
|
102
|
-
*/
|
|
103
|
-
constructor(
|
|
104
|
-
{
|
|
105
|
-
persona,
|
|
106
|
-
customer,
|
|
107
|
-
context,
|
|
108
|
-
conversation = createMockConversation(),
|
|
109
|
-
cwd = process.cwd(),
|
|
110
|
-
src = 'src',
|
|
111
|
-
mode = 'production',
|
|
112
|
-
api,
|
|
113
|
-
app,
|
|
114
|
-
project,
|
|
115
|
-
log = false
|
|
116
|
-
} = {
|
|
117
|
-
cwd: process.cwd(),
|
|
118
|
-
src: 'src',
|
|
119
|
-
mode: 'production'
|
|
120
|
-
}
|
|
121
|
-
) {
|
|
122
|
-
this.messages = [];
|
|
123
|
-
this._cwd = cwd;
|
|
124
|
-
this._src = src;
|
|
125
|
-
this._mode = mode;
|
|
126
|
-
this.context = {...(context || {}), __no_cache: true};
|
|
127
|
-
this.conversation = conversation;
|
|
128
|
-
if (api) {
|
|
129
|
-
this._api = api;
|
|
130
|
-
}
|
|
131
|
-
if (app) {
|
|
132
|
-
this._app = app;
|
|
133
|
-
}
|
|
134
|
-
if (project) {
|
|
135
|
-
this._project = project;
|
|
136
|
-
}
|
|
137
|
-
if (!customer) {
|
|
138
|
-
customer = {
|
|
139
|
-
id: 'mock_customer_' + Math.random().toString(36).slice(2, 11),
|
|
140
|
-
name: 'Mock Customer',
|
|
141
|
-
firstName: 'Mock',
|
|
142
|
-
lastName: 'Customer'
|
|
143
|
-
};
|
|
144
|
-
this.conversation.$customer = customer.id;
|
|
145
|
-
} else {
|
|
146
|
-
this.conversation.$customer = customer.id;
|
|
147
|
-
}
|
|
148
|
-
this.customer = customer;
|
|
149
|
-
this.context.customer = customer;
|
|
150
|
-
this._personaId = persona;
|
|
151
|
-
this.#defaultLog = !!log;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Loads the test environment
|
|
156
|
-
* @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
|
|
157
|
-
* @returns {Promise<void>}
|
|
158
|
-
*/
|
|
159
|
-
async load(override = false) {
|
|
160
|
-
|
|
161
|
-
// Load app (if not already loaded or override true)
|
|
162
|
-
if (override || !this._app) {
|
|
163
|
-
this._app = await this._loadApp();
|
|
164
|
-
}
|
|
17
|
+
/**
|
|
18
|
+
* @type {import('@scout9/app').Customer}
|
|
19
|
+
*/
|
|
20
|
+
customer;
|
|
165
21
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
22
|
+
/**
|
|
23
|
+
* @type {import('@scout9/app').Persona}
|
|
24
|
+
*/
|
|
25
|
+
persona;
|
|
170
26
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
27
|
+
/**
|
|
28
|
+
* @type {import('@scout9/app').Conversation}
|
|
29
|
+
*/
|
|
30
|
+
conversation;
|
|
174
31
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
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`);
|
|
185
|
-
}
|
|
186
|
-
this.context.agent = this.persona;
|
|
187
|
-
this._loaded = true;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Teardown the test environment
|
|
192
|
-
*/
|
|
193
|
-
teardown() {
|
|
194
|
-
this._loaded = false;
|
|
195
|
-
this._api = null;
|
|
196
|
-
this._project = null;
|
|
197
|
-
this._app = null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Send a message as a customer to your app
|
|
202
|
-
* @param {string} message - message to send
|
|
203
|
-
* @param {import('@scout9/app/spirits').StatusCallback | boolean} [progress] - progress callback, if true, will log progress, can override with your own callback. If not provided, no logs will be added.
|
|
204
|
-
* @returns {Promise<import('@scout9/app/spirits').ConversationEvent>}
|
|
205
|
-
*/
|
|
206
|
-
async send(message, progress = this.#defaultLog) {
|
|
207
|
-
if (!this._loaded) {
|
|
208
|
-
await this.load();
|
|
209
|
-
}
|
|
32
|
+
/**
|
|
33
|
+
* @type {import('@scout9/app').Message[]}
|
|
34
|
+
*/
|
|
35
|
+
messages;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @type {import('@scout9/app').ConversationContext}
|
|
39
|
+
*/
|
|
40
|
+
context;
|
|
210
41
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
42
|
+
#project = null;
|
|
43
|
+
#app = null;
|
|
44
|
+
#command = null;
|
|
45
|
+
#api = null;
|
|
46
|
+
#cwd;
|
|
47
|
+
#src;
|
|
48
|
+
#mode;
|
|
49
|
+
#loaded;
|
|
50
|
+
#personaId;
|
|
51
|
+
#defaultLog;
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Mimics a customer message to your app (useful for testing)
|
|
56
|
+
* @param props - the Scout9Test properties
|
|
57
|
+
* @param {import('@scout9/app').Customer | undefined} [props.customer] - customer to use
|
|
58
|
+
* @param {any | undefined} [props.context] - prior conversation context
|
|
59
|
+
* @param {string | undefined} [props.persona] id to use
|
|
60
|
+
* @param {import('@scout9/app').Conversation | undefined} [props.conversation] - existing conversation
|
|
61
|
+
* @param {string | undefined} [props.cwd]
|
|
62
|
+
* @param {string | undefined} [props.src]
|
|
63
|
+
* @param {string | undefined} [props.mode]
|
|
64
|
+
* @param {import('@scout9/admin').Scout9Api} [props.api]
|
|
65
|
+
* @param {import('@scout9/app').WorkflowFunction} [props.app]
|
|
66
|
+
* @param {import('@scout9/app').Scout9ProjectBuildConfig} [props.project]
|
|
67
|
+
*/
|
|
68
|
+
constructor(
|
|
69
|
+
{
|
|
70
|
+
persona,
|
|
71
|
+
customer,
|
|
72
|
+
context,
|
|
73
|
+
conversation = createMockConversation(),
|
|
74
|
+
cwd = process.cwd(),
|
|
75
|
+
src = 'src',
|
|
76
|
+
mode = 'production',
|
|
77
|
+
api,
|
|
78
|
+
app,
|
|
79
|
+
project,
|
|
80
|
+
log = false
|
|
81
|
+
} = {
|
|
82
|
+
cwd: process.cwd(),
|
|
83
|
+
src: 'src',
|
|
84
|
+
mode: 'production'
|
|
85
|
+
}
|
|
86
|
+
) {
|
|
87
|
+
this.messages = [];
|
|
88
|
+
this.#cwd = cwd;
|
|
89
|
+
this.#src = src;
|
|
90
|
+
this.#mode = mode;
|
|
91
|
+
this.context = {...(context || {}), __no_cache: true};
|
|
92
|
+
this.conversation = conversation;
|
|
93
|
+
if (api) {
|
|
94
|
+
this.#api = api;
|
|
95
|
+
}
|
|
96
|
+
if (app) {
|
|
97
|
+
this.#app = app;
|
|
98
|
+
}
|
|
99
|
+
if (project) {
|
|
100
|
+
this.#project = project;
|
|
101
|
+
}
|
|
102
|
+
if (!customer) {
|
|
103
|
+
customer = {
|
|
104
|
+
id: 'mock_customer_' + Math.random().toString(36).slice(2, 11),
|
|
105
|
+
name: 'Mock Customer',
|
|
106
|
+
firstName: 'Mock',
|
|
107
|
+
lastName: 'Customer'
|
|
108
|
+
};
|
|
109
|
+
this.conversation.$customer = customer.id;
|
|
110
|
+
} else {
|
|
111
|
+
this.conversation.$customer = customer.id;
|
|
112
|
+
}
|
|
113
|
+
this.customer = customer;
|
|
114
|
+
this.context.customer = customer;
|
|
115
|
+
this.#personaId = persona;
|
|
116
|
+
this.#defaultLog = !!log;
|
|
228
117
|
}
|
|
229
118
|
|
|
230
119
|
/**
|
|
231
|
-
*
|
|
120
|
+
* Loads the test environment
|
|
121
|
+
* @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
|
|
122
|
+
* @returns {Promise<void>}
|
|
232
123
|
*/
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
};
|
|
239
|
-
this.messages.push(_message);
|
|
240
|
-
const result = await Spirits.customer({
|
|
241
|
-
customer: this.customer,
|
|
242
|
-
config: this._project,
|
|
243
|
-
parser: async (_msg, _lng) => {
|
|
244
|
-
// @TODO can't do this for HUGE data sets
|
|
245
|
-
const detectableEntities = this._project.entities.filter(e => e.training?.length > 0 && e.definitions?.length > 0);
|
|
246
|
-
return this._api.parse({
|
|
247
|
-
message: _msg,
|
|
248
|
-
language: _lng,
|
|
249
|
-
entities: detectableEntities
|
|
250
|
-
}).then((_res => _res.data));
|
|
251
|
-
},
|
|
252
|
-
workflow: async (event) => {
|
|
253
|
-
globalThis.SCOUT9 = {
|
|
254
|
-
...event,
|
|
255
|
-
$convo: this.conversation.$id
|
|
124
|
+
async load(override = false) {
|
|
125
|
+
|
|
126
|
+
// Load app (if not already loaded or override true)
|
|
127
|
+
if (override || !this.#app) {
|
|
128
|
+
this.#app = await this.#loadApp();
|
|
256
129
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
this
|
|
280
|
-
} else if (!!result.conversation.forward?.to) {
|
|
281
|
-
this.conversation.forwardedTo = result.conversation.forward.to;
|
|
282
|
-
} else {
|
|
283
|
-
console.error(`Invalid forward result`, result.conversation.forward);
|
|
284
|
-
this.conversation.forwardedTo = 'Invalid Forward';
|
|
285
|
-
}
|
|
286
|
-
this.conversation.forwarded = new Date().toISOString();
|
|
287
|
-
this.conversation.forwardNote = result.conversation.forwardNote || '';
|
|
288
|
-
this.conversation.locked = true;
|
|
289
|
-
this.conversation.lockedReason = result.conversation.forwardNote ?? ('Forwarded to ' + this.conversation.forwardedTo);
|
|
130
|
+
|
|
131
|
+
// Load app configuration (if not already loaded or override true)
|
|
132
|
+
if (override || !this.#project) {
|
|
133
|
+
this.#project = await loadConfig({cwd: this.#cwd, src: this.#src, mode: this.#mode});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (override || !this.#api) {
|
|
137
|
+
this.#api = new Scout9Api(new Configuration({apiKey: process.env.SCOUT9_API_KEY}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!this.#personaId) {
|
|
141
|
+
this.#personaId = (this.#project.persona || this.#project.agents)?.[0]?.id;
|
|
142
|
+
if (!this.#personaId) {
|
|
143
|
+
throw new Error(`No persona found in config, please specify a persona id`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
this.conversation.$agent = this.#personaId;
|
|
147
|
+
this.persona = (this.#project.persona || this.#project.agents).find(p => p.id === this.#personaId);
|
|
148
|
+
if (!this.persona) {
|
|
149
|
+
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`);
|
|
150
|
+
}
|
|
151
|
+
this.context.agent = this.persona;
|
|
152
|
+
this.#loaded = true;
|
|
290
153
|
}
|
|
291
154
|
|
|
292
|
-
|
|
293
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Teardown the test environment
|
|
157
|
+
*/
|
|
158
|
+
teardown() {
|
|
159
|
+
this.#loaded = false;
|
|
160
|
+
this.#api = null;
|
|
161
|
+
this.#project = null;
|
|
162
|
+
this.#app = null;
|
|
294
163
|
}
|
|
295
164
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Send a message as a customer to your app
|
|
167
|
+
* @param {string} message - message to send
|
|
168
|
+
* @param {import('@scout9/app/spirits').StatusCallback | boolean} [progress] - progress callback, if true, will log progress, can override with your own callback. If not provided, no logs will be added.
|
|
169
|
+
* @returns {Promise<import('@scout9/app/spirits').ConversationEvent>}
|
|
170
|
+
*/
|
|
171
|
+
async send(message, progress = this.#defaultLog) {
|
|
172
|
+
|
|
173
|
+
if (!this.#loaded) {
|
|
174
|
+
await this.load();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check if it's a command
|
|
178
|
+
const target = message.toLowerCase().trim();
|
|
179
|
+
const commandToSwitchTo = this.#project.commands.find(command => {
|
|
180
|
+
return command.entity === target;
|
|
181
|
+
});
|
|
182
|
+
if (commandToSwitchTo) {
|
|
183
|
+
this.#command = await this.#loadCommand(commandToSwitchTo.entity);
|
|
184
|
+
Object.assign(this.context, {__command: commandToSwitchTo.entity});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const defaultProgressLogger = (message, level = 'info', type = '') => {
|
|
188
|
+
const typeStdout = type ? italic(bgWhite(' ' + black(type) + ' ')) : '';
|
|
189
|
+
const messageStdout = grey(message);
|
|
190
|
+
(console.hasOwnProperty(level) ? console[level] : console.log)(`\t${typeStdout ? typeStdout + ' ' : ''}${messageStdout}`);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// If custom logger provided, use it, otherwise use default logger
|
|
194
|
+
let progressInput = typeof progress === 'function' ? progress : defaultProgressLogger;
|
|
195
|
+
|
|
196
|
+
// If progress turned off, use a no-op function
|
|
197
|
+
if (typeof progress === 'boolean') {
|
|
198
|
+
if (!!progress) {
|
|
199
|
+
progressInput = defaultProgressLogger; // use default logger
|
|
200
|
+
} else {
|
|
201
|
+
progressInput = () => {
|
|
202
|
+
}; // use no-op
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @type {import('@scout9/app').Message}
|
|
208
|
+
* If we are switching to a command instance, we would want the first message to be system invoke
|
|
209
|
+
*/
|
|
210
|
+
const _message = {
|
|
211
|
+
id: 'user_mock_' + Math.random().toString(36).slice(2, 11),
|
|
212
|
+
role: commandToSwitchTo ? 'system' : 'customer',
|
|
213
|
+
content: commandToSwitchTo ? `Start by gathering/asking the user for relevant "${commandToSwitchTo.entity}" parameters` : message,
|
|
214
|
+
time: new Date().toISOString()
|
|
215
|
+
};
|
|
216
|
+
this.messages.push(_message);
|
|
217
|
+
const result = await Spirits.customer({
|
|
218
|
+
customer: this.customer,
|
|
219
|
+
config: this.#project,
|
|
220
|
+
parser: async (_msg, _lng) => {
|
|
221
|
+
// @TODO can't do this for HUGE data sets
|
|
222
|
+
const detectableEntities = this.#project.entities.filter(e => e.training?.length > 0 && e.definitions?.length > 0);
|
|
223
|
+
return this.#api.parse({
|
|
224
|
+
message: _msg,
|
|
225
|
+
language: _lng,
|
|
226
|
+
entities: detectableEntities
|
|
227
|
+
}).then((_res => _res.data));
|
|
228
|
+
},
|
|
229
|
+
workflow: async (event) => {
|
|
230
|
+
globalThis.SCOUT9 = {
|
|
231
|
+
...event,
|
|
232
|
+
$convo: this.conversation.$id ?? 'test_convo'
|
|
233
|
+
};
|
|
234
|
+
return (this.#command ? this.#command : this.#app)(event)
|
|
235
|
+
.then((response) => {
|
|
236
|
+
if ('toJSON' in response) {
|
|
237
|
+
return response.toJSON();
|
|
238
|
+
} else {
|
|
239
|
+
return response;
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
.then(WorkflowResponseSchema.parse);
|
|
243
|
+
},
|
|
244
|
+
generator: (request) => {
|
|
245
|
+
return this.#api.generate(request).then((_res => _res.data));
|
|
246
|
+
},
|
|
247
|
+
idGenerator: (prefix) => prefix + '_' + Math.random().toString(36).slice(2, 11),
|
|
248
|
+
progress: progressInput,
|
|
249
|
+
message: _message,
|
|
250
|
+
context: this.context,
|
|
251
|
+
messages: this.messages,
|
|
252
|
+
conversation: this.conversation
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
this.context = result.context.after;
|
|
256
|
+
this.messages = result.messages.after;
|
|
257
|
+
this.conversation = result.conversation.after;
|
|
258
|
+
|
|
259
|
+
if (!!result.conversation.forward) {
|
|
260
|
+
// @TODO migrate this
|
|
261
|
+
if (typeof result.conversation.forward === 'string') {
|
|
262
|
+
this.conversation.forwardedTo = result.conversation.forward;
|
|
263
|
+
} else if (result.conversation.forward === true) {
|
|
264
|
+
this.conversation.forwardedTo = this.persona.forwardPhone || this.persona.forwardEmail || 'No Forward';
|
|
265
|
+
} else if (!!result.conversation.forward?.to) {
|
|
266
|
+
this.conversation.forwardedTo = result.conversation.forward.to;
|
|
267
|
+
} else {
|
|
268
|
+
console.error(`Invalid forward result`, result.conversation.forward);
|
|
269
|
+
this.conversation.forwardedTo = 'Invalid Forward';
|
|
270
|
+
}
|
|
271
|
+
this.conversation.forwarded = new Date().toISOString();
|
|
272
|
+
this.conversation.forwardNote = result.conversation.forwardNote || '';
|
|
273
|
+
this.conversation.locked = true;
|
|
274
|
+
this.conversation.lockedReason = result.conversation.forwardNote ?? ('Forwarded to ' + this.conversation.forwardedTo);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!result.messages.after.find(m => m.id === result.message.after.id)) {
|
|
278
|
+
console.error(`Message not found in result.messages.after`, result.message.after.id);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return result;
|
|
308
282
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Parse user message
|
|
286
|
+
* @param {string} message - message string to parse
|
|
287
|
+
* @param {string} [language] - language to parse in, defaults to "en" for english
|
|
288
|
+
* @returns {Promise<import('@scout9/admin').ParseResponse>}
|
|
289
|
+
*/
|
|
290
|
+
async parse(message, language = 'en') {
|
|
291
|
+
if (!this.#project) {
|
|
292
|
+
throw new Error(`Config is not defined`);
|
|
293
|
+
}
|
|
294
|
+
return this.#api.parse({
|
|
295
|
+
message,
|
|
296
|
+
language,
|
|
297
|
+
entities: this.#project.entities
|
|
298
|
+
}).then((_res => _res.data));
|
|
325
299
|
}
|
|
326
|
-
|
|
327
|
-
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Runs your local app workflow
|
|
303
|
+
* @param {string} message - the message to run through the workflow
|
|
304
|
+
* @param {Omit<Partial<import('@scout9/app').WorkflowEvent>, 'message'> | undefined} [event] - additional event data
|
|
305
|
+
* @returns {Promise<import('@scout9/app').WorkflowResponse>}
|
|
306
|
+
*/
|
|
307
|
+
async workflow(message, event = {}) {
|
|
308
|
+
if (!this.#app) {
|
|
309
|
+
throw new Error(`Workflow function is not loaded or found - make sure to run ".load()" before calling ".workflow()"`);
|
|
310
|
+
}
|
|
311
|
+
if (event.hasOwnProperty('message')) {
|
|
312
|
+
console.warn(`WARNING: inserting a "event.message" will overwrite your "message" argument`);
|
|
313
|
+
}
|
|
314
|
+
return this.#app({
|
|
315
|
+
...createMockWorkflowEvent(message),
|
|
316
|
+
...event
|
|
317
|
+
});
|
|
328
318
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Generate a response to the user from the given or registered persona's voice in relation to the current conversation's context.
|
|
322
|
+
* @param {Object} [input] - Generation input, defaults to test registered data such as existing messages, context, and persona information.
|
|
323
|
+
* @param {string} [input.personaId] - Persona ID to use, defaults to test registered persona id.
|
|
324
|
+
* @param {Partial<import('@scout9/admin').ConversationCreateRequest>} [input.conversation] - Conversation overrides, defaults to test registered conversation data.
|
|
325
|
+
* @param {import('@scout9/app').Message[]} [input.messages] - Message overrides, defaults to test registered message data.
|
|
326
|
+
* @param {any} [input.context] - Context overrides, defaults to test registered context data.
|
|
327
|
+
* @returns {Promise<import('@scout9/admin').GenerateResponse>}
|
|
328
|
+
*/
|
|
329
|
+
async generate({personaId = this.#personaId, conversation = {}, messages = this.messages, context = this.context}) {
|
|
330
|
+
if (!this.#api) {
|
|
331
|
+
throw new Error(`Scout9 API is not loaded or found - make sure to run ".load()" before calling ".generate()"`);
|
|
332
|
+
}
|
|
333
|
+
if (!this.#project) {
|
|
334
|
+
throw new Error(`Config is not defined - make sure to run ".load()" before calling ".generate()"`);
|
|
335
|
+
}
|
|
336
|
+
const persona = (this.#project.persona || this.#project.agents).find(p => p.id === personaId);
|
|
337
|
+
if (!persona) {
|
|
338
|
+
throw new Error(`Could not find persona with id: ${personaId}, ensure your project is sync'd by running "scout9 sync"`);
|
|
339
|
+
}
|
|
340
|
+
return this.#api.generate({
|
|
341
|
+
convo: {
|
|
342
|
+
$customer: this.customer.id,
|
|
343
|
+
environment: this.conversation.environment,
|
|
344
|
+
initialContexts: this.conversation.initialContexts || [],
|
|
345
|
+
...conversation,
|
|
346
|
+
$agent: persona
|
|
347
|
+
},
|
|
348
|
+
messages,
|
|
349
|
+
context,
|
|
350
|
+
persona,
|
|
351
|
+
llm: this.#project.llm,
|
|
352
|
+
pmt: this.#project.pmt
|
|
353
|
+
}).then((_res => _res.data));
|
|
347
354
|
}
|
|
348
|
-
|
|
349
|
-
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @param {Partial<import('@scout9/app').ConversationContext>} ctx
|
|
358
|
+
*/
|
|
359
|
+
set context(ctx) {
|
|
360
|
+
this.context = {
|
|
361
|
+
...this.context,
|
|
362
|
+
...ctx
|
|
363
|
+
};
|
|
350
364
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
365
|
+
|
|
366
|
+
async #loadApp() {
|
|
367
|
+
const paths = globSync(`${this.#src}/app.{ts,cjs,mjs,js}`, {cwd: this.#cwd, absolute: true});
|
|
368
|
+
if (paths.length === 0) {
|
|
369
|
+
throw new Error(`Missing main project entry file ${this.#src}/app.{js|ts|cjs|mjs}`);
|
|
370
|
+
} else if (paths.length > 1) {
|
|
371
|
+
throw new Error(`Multiple main project entry files found ${this.#src}/app.{js|ts|cjs|mjs}`);
|
|
372
|
+
}
|
|
373
|
+
const [appFilePath] = paths;
|
|
374
|
+
return requireProjectFile(appFilePath)
|
|
375
|
+
.then(mod => mod.default);
|
|
354
376
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
* @private
|
|
383
|
-
*/
|
|
384
|
-
async _loadApp() {
|
|
385
|
-
const paths = globSync(`${this._src}/app.{ts,cjs,mjs,js}`, {cwd: this._cwd, absolute: true});
|
|
386
|
-
if (paths.length === 0) {
|
|
387
|
-
throw new Error(`Missing main project entry file ${this._src}/app.{js|ts|cjs|mjs}`);
|
|
388
|
-
} else if (paths.length > 1) {
|
|
389
|
-
throw new Error(`Multiple main project entry files found ${this._src}/app.{js|ts|cjs|mjs}`);
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* @param {string} command
|
|
380
|
+
* @returns {Promise<void>}
|
|
381
|
+
*/
|
|
382
|
+
async #loadCommand(command) {
|
|
383
|
+
if (!this.#project) {
|
|
384
|
+
throw new Error(`Must load #project before running #loadCommand`);
|
|
385
|
+
}
|
|
386
|
+
const commandsDir = resolve(this.#src, `./commands`);
|
|
387
|
+
const commandConfig = this.#project.commands.find(command => command.entity === command || command.path === command);
|
|
388
|
+
if (!commandConfig) {
|
|
389
|
+
throw new Error(`Unable to find command "${command}"`);
|
|
390
|
+
}
|
|
391
|
+
const commandFilePath = resolve(commandsDir, commandConfig.path);
|
|
392
|
+
let mod;
|
|
393
|
+
try {
|
|
394
|
+
mod = await import(commandFilePath);
|
|
395
|
+
} catch (e) {
|
|
396
|
+
throw new Error(`Unable to resolve command ${command} at ${commandFilePath}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!mod || !mod.default) {
|
|
400
|
+
throw new Error(`Unable to run command ${command} at ${commandFilePath} - must return a default function that returns a WorkflowEvent payload`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return mod.default;
|
|
390
404
|
}
|
|
391
|
-
|
|
392
|
-
return requireProjectFile(appFilePath)
|
|
393
|
-
.then(mod => mod.default);
|
|
394
|
-
}
|
|
405
|
+
|
|
395
406
|
|
|
396
407
|
}
|