@scout9/app 1.0.0-alpha.0.4.9 → 1.0.0-alpha.0.5.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.
@@ -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
- * @type {import('@scout9/app').Customer}
17
- */
18
- customer;
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
- // Load app configuration (if not already loaded or override true)
167
- if (override || !this._project) {
168
- this._project = await loadConfig({cwd: this._cwd, src: this._src, mode: this._mode});
169
- }
22
+ /**
23
+ * @type {import('@scout9/app').Persona}
24
+ */
25
+ persona;
170
26
 
171
- if (override || !this._api) {
172
- this._api = new Scout9Api(new Configuration({apiKey: process.env.SCOUT9_API_KEY}));
173
- }
27
+ /**
28
+ * @type {import('@scout9/app').Conversation}
29
+ */
30
+ conversation;
174
31
 
175
- if (!this._personaId) {
176
- this._personaId = (this._project.persona || this._project.agents)?.[0]?.id;
177
- if (!this._personaId) {
178
- throw new Error(`No persona found in config, please specify a persona id`);
179
- }
180
- }
181
- this.conversation.$agent = this._personaId;
182
- this.persona = (this._project.persona || this._project.agents).find(p => p.id === this._personaId);
183
- if (!this.persona) {
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
- const defaultProgressLogger = (message, level = 'info', type = '') => {
212
- const typeStdout = type ? italic(bgWhite(' ' + black(type) + ' ')) : '';
213
- const messageStdout = grey(message);
214
- (console.hasOwnProperty(level) ? console[level] : console.log)(`\t${typeStdout ? typeStdout + ' ' : ''}${messageStdout}`);
215
- };
216
-
217
- // If custom logger provided, use it, otherwise use default logger
218
- let progressInput = typeof progress === 'function' ? progress : defaultProgressLogger;
219
-
220
- // If progress turned off, use a no-op function
221
- if (typeof progress === 'boolean') {
222
- if (!!progress) {
223
- progressInput = defaultProgressLogger; // use default logger
224
- } else {
225
- progressInput = () => {
226
- }; // use no-op
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
+ #commandId = null;
53
+
54
+
55
+ /**
56
+ * Mimics a customer message to your app (useful for testing)
57
+ * @param props - the Scout9Test properties
58
+ * @param {import('@scout9/app').Customer | undefined} [props.customer] - customer to use
59
+ * @param {any | undefined} [props.context] - prior conversation context
60
+ * @param {string | undefined} [props.persona] id to use
61
+ * @param {import('@scout9/app').Conversation | undefined} [props.conversation] - existing conversation
62
+ * @param {string | undefined} [props.cwd]
63
+ * @param {string | undefined} [props.src]
64
+ * @param {string | undefined} [props.mode]
65
+ * @param {string | null | undefined} [props.command] - if provided it will use the command runtime instead of the app runtime
66
+ * @param {import('@scout9/admin').Scout9Api} [props.api]
67
+ * @param {import('@scout9/app').WorkflowFunction} [props.app]
68
+ * @param {import('@scout9/app').Scout9ProjectBuildConfig} [props.project]
69
+ */
70
+ constructor(
71
+ {
72
+ persona,
73
+ customer,
74
+ context,
75
+ conversation = createMockConversation(),
76
+ cwd = process.cwd(),
77
+ src = 'src',
78
+ mode = 'production',
79
+ api,
80
+ app,
81
+ command = null,
82
+ project,
83
+ log = false
84
+ } = {
85
+ cwd: process.cwd(),
86
+ src: 'src',
87
+ mode: 'production'
88
+ }
89
+ ) {
90
+ this.messages = [];
91
+ this.#cwd = cwd;
92
+ this.#src = src;
93
+ this.#mode = mode;
94
+ this.context = {...(context || {}), __no_cache: true};
95
+ this.conversation = conversation;
96
+ if (api) {
97
+ this.#api = api;
98
+ }
99
+ if (app) {
100
+ this.#app = app;
101
+ }
102
+ if (project) {
103
+ this.#project = project;
104
+ }
105
+ if (command) {
106
+ this.#commandId = command;
107
+ }
108
+ if (!customer) {
109
+ customer = {
110
+ id: 'mock_customer_' + Math.random().toString(36).slice(2, 11),
111
+ name: 'Mock Customer',
112
+ firstName: 'Mock',
113
+ lastName: 'Customer'
114
+ };
115
+ this.conversation.$customer = customer.id;
116
+ } else {
117
+ this.conversation.$customer = customer.id;
118
+ }
119
+ this.customer = customer;
120
+ this.context.customer = customer;
121
+ this.#personaId = persona;
122
+ this.#defaultLog = !!log;
228
123
  }
229
124
 
230
125
  /**
231
- * @type {import('@scout9/app').Message}
126
+ * Loads the test environment
127
+ * @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
128
+ * @returns {Promise<void>}
232
129
  */
233
- const _message = {
234
- id: 'user_mock_' + Math.random().toString(36).slice(2, 11),
235
- role: 'customer',
236
- content: message,
237
- time: new Date().toISOString()
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
130
+ async load(override = false) {
131
+
132
+ // Load app (if not already loaded or override true)
133
+ if (override || !this.#app) {
134
+ this.#app = await this.#loadApp();
135
+ }
136
+
137
+ // Load app configuration (if not already loaded or override true)
138
+ if (override || !this.#project) {
139
+ this.#project = await loadConfig({cwd: this.#cwd, src: this.#src, mode: this.#mode});
256
140
  }
257
- return this._app(event);
258
- },
259
- generator: (request) => {
260
- return this._api.generate(request).then((_res => _res.data));
261
- },
262
- idGenerator: (prefix) => prefix + '_' + Math.random().toString(36).slice(2, 11),
263
- progress: progressInput,
264
- message: _message,
265
- context: this.context,
266
- messages: this.messages,
267
- conversation: this.conversation
268
- });
269
-
270
- this.context = result.context.after;
271
- this.messages = result.messages.after;
272
- this.conversation = result.conversation.after;
273
-
274
- if (!!result.conversation.forward) {
275
- // @TODO migrate this
276
- if (typeof result.conversation.forward === 'string') {
277
- this.conversation.forwardedTo = result.conversation.forward;
278
- } else if (result.conversation.forward === true) {
279
- this.conversation.forwardedTo = this.persona.forwardPhone || this.persona.forwardEmail || 'No Forward';
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);
141
+
142
+ if (this.#commandId) {
143
+ if (override || !this.#commandId) {
144
+ this.#command = await this.#loadCommand(this.#commandId);
145
+ }
146
+ }
147
+
148
+ if (override || !this.#api) {
149
+ this.#api = new Scout9Api(new Configuration({apiKey: process.env.SCOUT9_API_KEY}));
150
+ }
151
+
152
+ if (!this.#personaId) {
153
+ this.#personaId = (this.#project.persona || this.#project.agents)?.[0]?.id;
154
+ if (!this.#personaId) {
155
+ throw new Error(`No persona found in config, please specify a persona id`);
156
+ }
157
+ }
158
+ this.conversation.$agent = this.#personaId;
159
+ this.persona = (this.#project.persona || this.#project.agents).find(p => p.id === this.#personaId);
160
+ if (!this.persona) {
161
+ 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`);
162
+ }
163
+ this.context.agent = this.persona;
164
+ this.#loaded = true;
290
165
  }
291
166
 
292
- if (!result.messages.after.find(m => m.id === result.message.after.id)) {
293
- console.error(`Message not found in result.messages.after`, result.message.after.id);
167
+ /**
168
+ * Teardown the test environment
169
+ */
170
+ teardown() {
171
+ this.#loaded = false;
172
+ this.#api = null;
173
+ this.#project = null;
174
+ this.#app = null;
294
175
  }
295
176
 
296
- return result;
297
- }
298
-
299
- /**
300
- * Parse user message
301
- * @param {string} message - message string to parse
302
- * @param {string} [language] - language to parse in, defaults to "en" for english
303
- * @returns {Promise<import('@scout9/admin').ParseResponse>}
304
- */
305
- async parse(message, language = 'en') {
306
- if (!this._project) {
307
- throw new Error(`Config is not defined`);
177
+ /**
178
+ * Send a message as a customer to your app
179
+ * @param {string} message - message to send
180
+ * @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.
181
+ * @returns {Promise<import('@scout9/app/spirits').ConversationEvent>}
182
+ */
183
+ async send(message, progress = this.#defaultLog) {
184
+ if (!this.#loaded) {
185
+ await this.load();
186
+ }
187
+
188
+ const defaultProgressLogger = (message, level = 'info', type = '') => {
189
+ const typeStdout = type ? italic(bgWhite(' ' + black(type) + ' ')) : '';
190
+ const messageStdout = grey(message);
191
+ (console.hasOwnProperty(level) ? console[level] : console.log)(`\t${typeStdout ? typeStdout + ' ' : ''}${messageStdout}`);
192
+ };
193
+
194
+ // If custom logger provided, use it, otherwise use default logger
195
+ let progressInput = typeof progress === 'function' ? progress : defaultProgressLogger;
196
+
197
+ // If progress turned off, use a no-op function
198
+ if (typeof progress === 'boolean') {
199
+ if (!!progress) {
200
+ progressInput = defaultProgressLogger; // use default logger
201
+ } else {
202
+ progressInput = () => {
203
+ }; // use no-op
204
+ }
205
+ }
206
+
207
+ /**
208
+ * @type {import('@scout9/app').Message}
209
+ */
210
+ const _message = {
211
+ id: 'user_mock_' + Math.random().toString(36).slice(2, 11),
212
+ role: 'customer',
213
+ content: 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
- return this._api.parse({
310
- message,
311
- language,
312
- entities: this._project.entities
313
- }).then((_res => _res.data));
314
- }
315
-
316
- /**
317
- * Runs your local app workflow
318
- * @param {string} message - the message to run through the workflow
319
- * @param {Omit<Partial<import('@scout9/app').WorkflowEvent>, 'message'> | undefined} [event] - additional event data
320
- * @returns {Promise<import('@scout9/app').WorkflowResponse>}
321
- */
322
- async workflow(message, event = {}) {
323
- if (!this._app) {
324
- throw new Error(`Workflow function is not loaded or found - make sure to run ".load()" before calling ".workflow()"`);
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
- if (event.hasOwnProperty('message')) {
327
- console.warn(`WARNING: inserting a "event.message" will overwrite your "message" argument`);
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
- return this._app({
330
- ...createMockWorkflowEvent(message),
331
- ...event
332
- });
333
- }
334
-
335
- /**
336
- * Generate a response to the user from the given or registered persona's voice in relation to the current conversation's context.
337
- * @param {Object} [input] - Generation input, defaults to test registered data such as existing messages, context, and persona information.
338
- * @param {string} [input.personaId] - Persona ID to use, defaults to test registered persona id.
339
- * @param {Partial<import('@scout9/admin').ConversationCreateRequest>} [input.conversation] - Conversation overrides, defaults to test registered conversation data.
340
- * @param {import('@scout9/app').Message[]} [input.messages] - Message overrides, defaults to test registered message data.
341
- * @param {any} [input.context] - Context overrides, defaults to test registered context data.
342
- * @returns {Promise<import('@scout9/admin').GenerateResponse>}
343
- */
344
- async generate({personaId = this._personaId, conversation = {}, messages = this.messages, context = this.context}) {
345
- if (!this._api) {
346
- throw new Error(`Scout9 API is not loaded or found - make sure to run ".load()" before calling ".generate()"`);
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
- if (!this._project) {
349
- throw new Error(`Config is not defined - make sure to run ".load()" before calling ".generate()"`);
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
- const persona = (this._project.persona || this._project.agents).find(p => p.id === personaId);
352
- if (!persona) {
353
- throw new Error(`Could not find persona with id: ${personaId}, ensure your project is sync'd by running "scout9 sync"`);
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
- return this._api.generate({
356
- convo: {
357
- $customer: this.customer.id,
358
- environment: this.conversation.environment,
359
- initialContexts: this.conversation.initialContexts || [],
360
- ...conversation,
361
- $agent: persona
362
- },
363
- messages,
364
- context,
365
- persona,
366
- llm: this._project.llm,
367
- pmt: this._project.pmt
368
- }).then((_res => _res.data));
369
- }
370
-
371
- /**
372
- * @param {Partial<import('@scout9/app').ConversationContext>} ctx
373
- */
374
- set context(ctx) {
375
- this.context = {
376
- ...this.context,
377
- ...ctx
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
- const [appFilePath] = paths;
392
- return requireProjectFile(appFilePath)
393
- .then(mod => mod.default);
394
- }
405
+
395
406
 
396
407
  }