@scout9/app 1.0.0-alpha.0.5.3 → 1.0.0-alpha.0.5.5

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.
@@ -2,7 +2,7 @@ import { Configuration, Scout9Api } from '@scout9/admin';
2
2
  import { grey, italic, bgWhite, black } from 'kleur/colors';
3
3
  import { createMockConversation, createMockWorkflowEvent } from './mocks.js';
4
4
  import { loadConfig } from '../core/config/index.js';
5
- import { requireProjectFile } from '../utils/index.js';
5
+ import { requireProjectFile, simplifyError } from '../utils/index.js';
6
6
  import { globSync } from 'glob';
7
7
 
8
8
  import { Spirits } from './spirits.js';
@@ -14,394 +14,400 @@ import { WorkflowResponseSchema } from '../runtime/index.js';
14
14
  */
15
15
  export class Scout9Test {
16
16
 
17
- /**
18
- * @type {import('@scout9/app').Customer}
19
- */
20
- customer;
21
-
22
- /**
23
- * @type {import('@scout9/app').Persona}
24
- */
25
- persona;
26
-
27
- /**
28
- * @type {import('@scout9/app').Conversation}
29
- */
30
- conversation;
31
-
32
- /**
33
- * @type {import('@scout9/app').Message[]}
34
- */
35
- messages;
36
-
37
- /**
38
- * @type {import('@scout9/app').ConversationContext}
39
- */
40
- context;
17
+ /**
18
+ * @type {import('@scout9/app').Customer}
19
+ */
20
+ customer;
21
+
22
+ /**
23
+ * @type {import('@scout9/app').Persona}
24
+ */
25
+ persona;
26
+
27
+ /**
28
+ * @type {import('@scout9/app').Conversation}
29
+ */
30
+ conversation;
31
+
32
+ /**
33
+ * @type {import('@scout9/app').Message[]}
34
+ */
35
+ messages;
36
+
37
+ /**
38
+ * @type {import('@scout9/app').ConversationContext}
39
+ */
40
+ context;
41
+
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;
117
+ }
118
+
119
+ /**
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>}
123
+ */
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();
129
+ }
41
130
 
42
- #project = null;
43
- #app = null;
44
- #command = null;
45
- #api = null;
46
- #cwd;
47
- #src;
48
- #mode;
49
- #loaded;
50
- #personaId;
51
- #defaultLog;
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
+ }
52
135
 
136
+ if (override || !this.#api) {
137
+ this.#api = new Scout9Api(new Configuration({apiKey: process.env.SCOUT9_API_KEY}));
138
+ }
53
139
 
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;
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;
153
+ }
154
+
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;
163
+ }
164
+
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();
117
175
  }
118
176
 
119
- /**
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>}
123
- */
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();
129
- }
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;
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});
153
185
  }
154
186
 
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;
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
+ }
163
204
  }
164
205
 
165
206
  /**
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>}
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
170
209
  */
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}`);
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'
191
233
  };
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
234
+ return (this.#command ? this.#command : this.#app)(event)
235
+ .then((response) => {
236
+ if ('toJSON' in response) {
237
+ return response.toJSON();
200
238
  } else {
201
- progressInput = () => {
202
- }; // use no-op
239
+ return response;
203
240
  }
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';
241
+ })
242
+ .then((event) => {
243
+ try {
244
+ return WorkflowResponseSchema.parse(event);
245
+ } catch (e) {
246
+ throw simplifyError(e, 'PMT runtime error');
270
247
  }
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;
248
+ });
249
+ },
250
+ generator: (request) => {
251
+ return this.#api.generate(request).then((_res => _res.data));
252
+ },
253
+ idGenerator: (prefix) => prefix + '_' + Math.random().toString(36).slice(2, 11),
254
+ progress: progressInput,
255
+ message: _message,
256
+ context: this.context,
257
+ messages: this.messages,
258
+ conversation: this.conversation
259
+ });
260
+
261
+ this.context = result.context.after;
262
+ this.messages = result.messages.after;
263
+ this.conversation = result.conversation.after;
264
+
265
+ if (!!result.conversation.forward) {
266
+ // @TODO migrate this
267
+ if (typeof result.conversation.forward === 'string') {
268
+ this.conversation.forwardedTo = result.conversation.forward;
269
+ } else if (result.conversation.forward === true) {
270
+ this.conversation.forwardedTo = this.persona.forwardPhone || this.persona.forwardEmail || 'No Forward';
271
+ } else if (!!result.conversation.forward?.to) {
272
+ this.conversation.forwardedTo = result.conversation.forward.to;
273
+ } else {
274
+ console.error(`Invalid forward result`, result.conversation.forward);
275
+ this.conversation.forwardedTo = 'Invalid Forward';
276
+ }
277
+ this.conversation.forwarded = new Date().toISOString();
278
+ this.conversation.forwardNote = result.conversation.forwardNote || '';
279
+ this.conversation.locked = true;
280
+ this.conversation.lockedReason = result.conversation.forwardNote ?? ('Forwarded to ' + this.conversation.forwardedTo);
282
281
  }
283
282
 
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));
283
+ if (!result.messages.after.find(m => m.id === result.message.after.id)) {
284
+ console.error(`Message not found in result.messages.after`, result.message.after.id);
299
285
  }
300
286
 
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
- });
287
+ return result;
288
+ }
289
+
290
+ /**
291
+ * Parse user message
292
+ * @param {string} message - message string to parse
293
+ * @param {string} [language] - language to parse in, defaults to "en" for english
294
+ * @returns {Promise<import('@scout9/admin').ParseResponse>}
295
+ */
296
+ async parse(message, language = 'en') {
297
+ if (!this.#project) {
298
+ throw new Error(`Config is not defined`);
318
299
  }
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));
300
+ return this.#api.parse({
301
+ message,
302
+ language,
303
+ entities: this.#project.entities
304
+ }).then((_res => _res.data));
305
+ }
306
+
307
+ /**
308
+ * Runs your local app workflow
309
+ * @param {string} message - the message to run through the workflow
310
+ * @param {Omit<Partial<import('@scout9/app').WorkflowEvent>, 'message'> | undefined} [event] - additional event data
311
+ * @returns {Promise<import('@scout9/app').WorkflowResponse>}
312
+ */
313
+ async workflow(message, event = {}) {
314
+ if (!this.#app) {
315
+ throw new Error(`Workflow function is not loaded or found - make sure to run ".load()" before calling ".workflow()"`);
354
316
  }
355
-
356
- /**
357
- * @param {Partial<import('@scout9/app').ConversationContext>} ctx
358
- */
359
- set context(ctx) {
360
- this.context = {
361
- ...this.context,
362
- ...ctx
363
- };
317
+ if (event.hasOwnProperty('message')) {
318
+ console.warn(`WARNING: inserting a "event.message" will overwrite your "message" argument`);
364
319
  }
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);
320
+ return this.#app({
321
+ ...createMockWorkflowEvent(message),
322
+ ...event
323
+ });
324
+ }
325
+
326
+ /**
327
+ * Generate a response to the user from the given or registered persona's voice in relation to the current conversation's context.
328
+ * @param {Object} [input] - Generation input, defaults to test registered data such as existing messages, context, and persona information.
329
+ * @param {string} [input.personaId] - Persona ID to use, defaults to test registered persona id.
330
+ * @param {Partial<import('@scout9/admin').ConversationCreateRequest>} [input.conversation] - Conversation overrides, defaults to test registered conversation data.
331
+ * @param {import('@scout9/app').Message[]} [input.messages] - Message overrides, defaults to test registered message data.
332
+ * @param {any} [input.context] - Context overrides, defaults to test registered context data.
333
+ * @returns {Promise<import('@scout9/admin').GenerateResponse>}
334
+ */
335
+ async generate({personaId = this.#personaId, conversation = {}, messages = this.messages, context = this.context}) {
336
+ if (!this.#api) {
337
+ throw new Error(`Scout9 API is not loaded or found - make sure to run ".load()" before calling ".generate()"`);
338
+ }
339
+ if (!this.#project) {
340
+ throw new Error(`Config is not defined - make sure to run ".load()" before calling ".generate()"`);
341
+ }
342
+ const persona = (this.#project.persona || this.#project.agents).find(p => p.id === personaId);
343
+ if (!persona) {
344
+ throw new Error(`Could not find persona with id: ${personaId}, ensure your project is sync'd by running "scout9 sync"`);
345
+ }
346
+ return this.#api.generate({
347
+ convo: {
348
+ $customer: this.customer.id,
349
+ environment: this.conversation.environment,
350
+ initialContexts: this.conversation.initialContexts || [],
351
+ ...conversation,
352
+ $agent: persona
353
+ },
354
+ messages,
355
+ context,
356
+ persona,
357
+ llm: this.#project.llm,
358
+ pmt: this.#project.pmt
359
+ }).then((_res => _res.data));
360
+ }
361
+
362
+ /**
363
+ * @param {Partial<import('@scout9/app').ConversationContext>} ctx
364
+ */
365
+ set context(ctx) {
366
+ this.context = {
367
+ ...this.context,
368
+ ...ctx
369
+ };
370
+ }
371
+
372
+ async #loadApp() {
373
+ const paths = globSync(`${this.#src}/app.{ts,cjs,mjs,js}`, {cwd: this.#cwd, absolute: true});
374
+ if (paths.length === 0) {
375
+ throw new Error(`Missing main project entry file ${this.#src}/app.{js|ts|cjs|mjs}`);
376
+ } else if (paths.length > 1) {
377
+ throw new Error(`Multiple main project entry files found ${this.#src}/app.{js|ts|cjs|mjs}`);
378
+ }
379
+ const [appFilePath] = paths;
380
+ return requireProjectFile(appFilePath)
381
+ .then(mod => mod.default);
382
+ }
383
+
384
+ /**
385
+ * @param {string} command
386
+ * @returns {Promise<void>}
387
+ */
388
+ async #loadCommand(command) {
389
+ if (!this.#project) {
390
+ throw new Error(`Must load #project before running #loadCommand`);
391
+ }
392
+ const commandsDir = resolve(this.#src, `./commands`);
393
+ const commandConfig = this.#project.commands.find(command => command.entity === command || command.path === command);
394
+ if (!commandConfig) {
395
+ throw new Error(`Unable to find command "${command}"`);
396
+ }
397
+ const commandFilePath = resolve(commandsDir, commandConfig.path);
398
+ let mod;
399
+ try {
400
+ mod = await import(commandFilePath);
401
+ } catch (e) {
402
+ throw new Error(`Unable to resolve command ${command} at ${commandFilePath}`);
376
403
  }
377
404
 
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;
405
+ if (!mod || !mod.default) {
406
+ throw new Error(`Unable to run command ${command} at ${commandFilePath} - must return a default function that returns a WorkflowEvent payload`);
404
407
  }
405
408
 
409
+ return mod.default;
410
+ }
411
+
406
412
 
407
413
  }