@scout9/app 1.0.0-alpha.0.5.3 → 1.0.0-alpha.0.5.4
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-be9ba80f.cjs → dev-7e3c6095.cjs} +99 -57
- package/dist/{index-11e312c6.cjs → index-4dfed9d3.cjs} +54 -44
- package/dist/index.cjs +3 -3
- package/dist/{macros-57f9b28b.cjs → macros-c8392690.cjs} +6 -2
- package/dist/{multipart-parser-8a217889.cjs → multipart-parser-65dc2a0b.cjs} +3 -3
- package/dist/schemas.cjs +1 -1
- package/dist/testing-tools.cjs +2 -2
- package/package.json +1 -1
- package/src/core/config/agents.js +2 -3
- package/src/core/config/commands.js +38 -28
- package/src/core/config/entities.js +17 -9
- package/src/core/config/index.js +3 -2
- package/src/core/config/workflow.js +11 -5
- package/src/core/index.js +2 -3
- package/src/report.js +8 -2
- package/src/runtime/schemas/conversation.js +41 -36
- package/src/testing-tools/dev.js +370 -364
- package/src/utils/configs/agents.js +2 -1
- package/src/utils/configs/entities.js +14 -4
- package/src/utils/configs/workflow.js +42 -35
- package/src/utils/project.js +2 -1
- package/types/index.d.ts +79 -2
- package/types/index.d.ts.map +1 -1
package/src/testing-tools/dev.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
*
|
|
167
|
-
*
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
202
|
-
}; // use no-op
|
|
239
|
+
return response;
|
|
203
240
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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
|
}
|