@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
|
@@ -2,21 +2,21 @@ import polka from 'polka';
|
|
|
2
2
|
import sirv from 'sirv';
|
|
3
3
|
import compression from 'compression';
|
|
4
4
|
import bodyParser from 'body-parser';
|
|
5
|
-
import colors from 'kleur';
|
|
6
5
|
import { config as dotenv } from 'dotenv';
|
|
7
6
|
import { Configuration, Scout9Api } from '@scout9/admin';
|
|
8
|
-
import { EventResponse } from '@scout9/app';
|
|
7
|
+
import { EventResponse, ProgressLogger } from '@scout9/app';
|
|
9
8
|
import { WorkflowEventSchema, WorkflowResponseSchema } from '@scout9/app/schemas';
|
|
9
|
+
import { Spirits } from '@scout9/app/spirits';
|
|
10
10
|
import path, { resolve } from 'node:path';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import https from 'node:https';
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
|
-
import projectApp from './src/app.js';
|
|
15
|
-
import config from './config.js';
|
|
16
14
|
import { readdir } from 'fs/promises';
|
|
17
15
|
import { ZodError } from 'zod';
|
|
18
16
|
import { fromError } from 'zod-validation-error';
|
|
19
|
-
|
|
17
|
+
import { bgBlack, blue, bold, cyan, green, grey, magenta, red, white } from 'kleur/colors';
|
|
18
|
+
import projectApp from './src/app.js';
|
|
19
|
+
import config from './config.js';
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -80,7 +80,7 @@ const simplifyZodError = (error, tag = undefined) => {
|
|
|
80
80
|
validationError.message = validationError.message.replace('Validation error', tag);
|
|
81
81
|
}
|
|
82
82
|
return validationError;
|
|
83
|
-
}
|
|
83
|
+
};
|
|
84
84
|
|
|
85
85
|
const handleError = (e, res = undefined, tag = undefined, body = undefined) => {
|
|
86
86
|
let name = e?.name || 'Runtime Error';
|
|
@@ -105,7 +105,7 @@ const handleError = (e, res = undefined, tag = undefined, body = undefined) => {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
if (body) {
|
|
108
|
-
console.log(
|
|
108
|
+
console.log(grey(JSON.stringify(body, null, dev ? 2 : undefined)));
|
|
109
109
|
}
|
|
110
110
|
if (tag && typeof tag === 'string') {
|
|
111
111
|
message = `${tag}: ${message}`;
|
|
@@ -113,12 +113,12 @@ const handleError = (e, res = undefined, tag = undefined, body = undefined) => {
|
|
|
113
113
|
if (typeof e?.constructor?.name === 'string') {
|
|
114
114
|
message = `(${e?.constructor?.name}) ${message}`;
|
|
115
115
|
}
|
|
116
|
-
console.log(
|
|
116
|
+
console.log(red(`${bold(`${code} Error`)}: ${message}`));
|
|
117
117
|
if ('stack' in e) {
|
|
118
|
-
console.log('STACK:',
|
|
118
|
+
console.log('STACK:', grey(e.stack));
|
|
119
119
|
}
|
|
120
120
|
if (body) {
|
|
121
|
-
console.log('INPUT:',
|
|
121
|
+
console.log('INPUT:', grey(JSON.stringify(body, null, dev ? 2 : undefined)));
|
|
122
122
|
}
|
|
123
123
|
if (res) {
|
|
124
124
|
res.writeHead(code, {'Content-Type': 'application/json'});
|
|
@@ -130,7 +130,16 @@ const handleError = (e, res = undefined, tag = undefined, body = undefined) => {
|
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
-
const handleZodError = ({
|
|
133
|
+
const handleZodError = ({
|
|
134
|
+
error,
|
|
135
|
+
res = undefined,
|
|
136
|
+
code = 500,
|
|
137
|
+
status,
|
|
138
|
+
name,
|
|
139
|
+
bodyLabel = 'Provided Input',
|
|
140
|
+
body = undefined,
|
|
141
|
+
action = ''
|
|
142
|
+
}) => {
|
|
134
143
|
res?.writeHead?.(code, {'Content-Type': 'application/json'});
|
|
135
144
|
if (error instanceof ZodError) {
|
|
136
145
|
const formattedError = simplifyZodError(error);
|
|
@@ -139,12 +148,12 @@ const handleZodError = ({error, res = undefined, code = 500, status, name, bodyL
|
|
|
139
148
|
error: formattedError.message,
|
|
140
149
|
errors: [formattedError.message]
|
|
141
150
|
}));
|
|
142
|
-
console.log(
|
|
151
|
+
console.log(red(`${bold(`${name}`)}:`));
|
|
143
152
|
if (body) {
|
|
144
|
-
console.log(
|
|
145
|
-
console.log(
|
|
153
|
+
console.log(grey(`${bodyLabel}:`));
|
|
154
|
+
console.log(grey(JSON.stringify(body, null, dev ? 2 : undefined)));
|
|
146
155
|
}
|
|
147
|
-
console.log(
|
|
156
|
+
console.log(red(`${action}${formattedError}`));
|
|
148
157
|
} else {
|
|
149
158
|
console.error(error);
|
|
150
159
|
error.message = `${name}: ` + error.message;
|
|
@@ -214,8 +223,7 @@ if (dev) {
|
|
|
214
223
|
app.use(sirv(path.resolve(__dirname, 'public'), {dev: true}));
|
|
215
224
|
}
|
|
216
225
|
|
|
217
|
-
|
|
218
|
-
app.post(dev ? '/dev/workflow' : '/', async (req, res) => {
|
|
226
|
+
function parseWorkflowEvent(req, res) {
|
|
219
227
|
let workflowEvent;
|
|
220
228
|
|
|
221
229
|
try {
|
|
@@ -243,7 +251,18 @@ app.post(dev ? '/dev/workflow' : '/', async (req, res) => {
|
|
|
243
251
|
|
|
244
252
|
if (!workflowEvent) {
|
|
245
253
|
handleError(new Error('No workflowEvent defined'), res, req.body.event, 'Workflow Template Event No Event');
|
|
254
|
+
} else {
|
|
255
|
+
return workflowEvent;
|
|
246
256
|
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Root application POST endpoint will run the scout9 app
|
|
260
|
+
app.post(dev ? '/dev/workflow' : '/', async (req, res) => {
|
|
261
|
+
const workflowEvent = parseWorkflowEvent(req, res);
|
|
262
|
+
if (!workflowEvent) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
247
266
|
let response;
|
|
248
267
|
try {
|
|
249
268
|
response = await projectApp(workflowEvent)
|
|
@@ -253,7 +272,7 @@ app.post(dev ? '/dev/workflow' : '/', async (req, res) => {
|
|
|
253
272
|
} else {
|
|
254
273
|
return response;
|
|
255
274
|
}
|
|
256
|
-
})
|
|
275
|
+
});
|
|
257
276
|
} catch (error) {
|
|
258
277
|
if (error instanceof ZodError) {
|
|
259
278
|
handleZodError({
|
|
@@ -278,8 +297,8 @@ app.post(dev ? '/dev/workflow' : '/', async (req, res) => {
|
|
|
278
297
|
try {
|
|
279
298
|
const formattedResponse = WorkflowResponseSchema.parse(response);
|
|
280
299
|
if (dev) {
|
|
281
|
-
console.log(
|
|
282
|
-
console.log(
|
|
300
|
+
console.log(green(`Workflow Sending Response:`));
|
|
301
|
+
console.log(grey(JSON.stringify(formattedResponse, null, 2)));
|
|
283
302
|
}
|
|
284
303
|
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
285
304
|
res.end(JSON.stringify(formattedResponse));
|
|
@@ -402,7 +421,7 @@ async function runEntityApi(req, res) {
|
|
|
402
421
|
res.writeHead(response.status || 200, {'Content-Type': 'application/json'});
|
|
403
422
|
res.end(JSON.stringify(data));
|
|
404
423
|
console.log(`${req.method} EntityApi.${lastSegment}:`);
|
|
405
|
-
console.log(
|
|
424
|
+
console.log(grey(JSON.stringify(data)));
|
|
406
425
|
} else {
|
|
407
426
|
throw new Error(`Invalid response: not an EventResponse`);
|
|
408
427
|
}
|
|
@@ -429,12 +448,13 @@ async function getFilesRecursive(dir) {
|
|
|
429
448
|
return results;
|
|
430
449
|
}
|
|
431
450
|
|
|
451
|
+
|
|
452
|
+
const commandsDir = resolve(__dirname, `./src/commands`);
|
|
453
|
+
|
|
432
454
|
async function runCommandApi(req, res) {
|
|
433
455
|
let file;
|
|
434
456
|
const {body, url} = req;
|
|
435
457
|
const params = url.split('/').slice(2).filter(Boolean);
|
|
436
|
-
const commandsDir = resolve(__dirname, `./src/commands`);
|
|
437
|
-
|
|
438
458
|
try {
|
|
439
459
|
const files = await getFilesRecursive(commandsDir).then(files => files.map(file => file.replace(commandsDir, '.'))
|
|
440
460
|
.filter(file => params.every(p => file.includes(p))));
|
|
@@ -478,10 +498,22 @@ async function runCommandApi(req, res) {
|
|
|
478
498
|
return;
|
|
479
499
|
}
|
|
480
500
|
|
|
501
|
+
const workflowEvent = parseWorkflowEvent(req, res);
|
|
502
|
+
if (!workflowEvent) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
481
506
|
let result;
|
|
482
507
|
|
|
483
508
|
try {
|
|
484
|
-
result = await mod.default(
|
|
509
|
+
result = await mod.default(workflowEvent)
|
|
510
|
+
.then((response) => {
|
|
511
|
+
if ('toJSON' in response) {
|
|
512
|
+
return response.toJSON();
|
|
513
|
+
} else {
|
|
514
|
+
return response;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
485
517
|
} catch (e) {
|
|
486
518
|
console.error('Failed to run command', e);
|
|
487
519
|
res.writeHead(500, {'Content-Type': 'application/json'});
|
|
@@ -523,6 +555,7 @@ app.post('/entity/:entity/*', runEntityApi);
|
|
|
523
555
|
app.delete('/entity/:entity/*', runEntityApi);
|
|
524
556
|
|
|
525
557
|
// For local development: parse a message
|
|
558
|
+
let devProgram;
|
|
526
559
|
if (dev) {
|
|
527
560
|
|
|
528
561
|
app.get('/dev/config', async (req, res, next) => {
|
|
@@ -543,14 +576,14 @@ if (dev) {
|
|
|
543
576
|
if (!cache.isTested()) {
|
|
544
577
|
const testableEntities = config.entities.filter(e => e?.definitions?.length > 0 || e?.training?.length > 0);
|
|
545
578
|
if (dev && testableEntities.length > 0) {
|
|
546
|
-
console.log(`${
|
|
579
|
+
console.log(`${grey(`${cyan('>')} Testing ${bold(white(testableEntities.length))} Entities...`)}`);
|
|
547
580
|
const _res = await scout9.parse({
|
|
548
581
|
message: 'Dummy message to parse',
|
|
549
582
|
language: 'en',
|
|
550
583
|
entities: testableEntities
|
|
551
584
|
});
|
|
552
585
|
cache.setTested();
|
|
553
|
-
console.log(`\t${
|
|
586
|
+
console.log(`\t${green(`+ ${testableEntities.length} Entities passed`)}`);
|
|
554
587
|
}
|
|
555
588
|
}
|
|
556
589
|
} catch (e) {
|
|
@@ -558,25 +591,30 @@ if (dev) {
|
|
|
558
591
|
}
|
|
559
592
|
});
|
|
560
593
|
|
|
594
|
+
const devParse = async (message, language = 'en') => {
|
|
595
|
+
if (typeof message !== 'string') {
|
|
596
|
+
throw new Error('Invalid message - expected to be a string');
|
|
597
|
+
}
|
|
598
|
+
console.log(`${grey(`${cyan('>')} Parsing "${bold(white(message))}`)}"`);
|
|
599
|
+
const payload = await scout9.parse({
|
|
600
|
+
message,
|
|
601
|
+
language,
|
|
602
|
+
entities: config.entities
|
|
603
|
+
}).then((_res => _res.data));
|
|
604
|
+
let fields = '';
|
|
605
|
+
for (const [key, value] of Object.entries(payload.context)) {
|
|
606
|
+
fields += `\n\t\t${bold(white(key))}: ${grey(JSON.stringify(value))}`;
|
|
607
|
+
}
|
|
608
|
+
console.log(`\tParsed in ${payload.ms}ms:${grey(`${fields}`)}:`);
|
|
609
|
+
console.log(grey(JSON.stringify(payload)));
|
|
610
|
+
return payload;
|
|
611
|
+
};
|
|
612
|
+
|
|
561
613
|
app.post('/dev/parse', async (req, res, next) => {
|
|
562
614
|
try {
|
|
563
615
|
// req.body: {message: string}
|
|
564
|
-
const {message, language} = req.body;
|
|
565
|
-
|
|
566
|
-
throw new Error('Invalid message - expected to be a string');
|
|
567
|
-
}
|
|
568
|
-
console.log(`${colors.grey(`${colors.cyan('>')} Parsing "${colors.bold(colors.white(message))}`)}"`);
|
|
569
|
-
const payload = await scout9.parse({
|
|
570
|
-
message,
|
|
571
|
-
language: 'en',
|
|
572
|
-
entities: config.entities
|
|
573
|
-
}).then((_res => _res.data));
|
|
574
|
-
let fields = '';
|
|
575
|
-
for (const [key, value] of Object.entries(payload.context)) {
|
|
576
|
-
fields += `\n\t\t${colors.bold(colors.white(key))}: ${colors.grey(JSON.stringify(value))}`;
|
|
577
|
-
}
|
|
578
|
-
console.log(`\tParsed in ${payload.ms}ms:${colors.grey(`${fields}`)}:`);
|
|
579
|
-
console.log(colors.grey(JSON.stringify(payload)));
|
|
616
|
+
const {message, language = 'en'} = req.body;
|
|
617
|
+
const payload = await devParse(message, language);
|
|
580
618
|
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
581
619
|
res.end(JSON.stringify(payload));
|
|
582
620
|
} catch (e) {
|
|
@@ -584,13 +622,18 @@ if (dev) {
|
|
|
584
622
|
}
|
|
585
623
|
});
|
|
586
624
|
|
|
625
|
+
const devForward = async (convo) => {
|
|
626
|
+
console.log(`${grey(`${cyan('>')} Forwarding...`)}`);
|
|
627
|
+
const payload = await scout9.forward({convo}).then((_res => _res.data));
|
|
628
|
+
console.log(`\tForwarded in ${payload?.ms}ms`);
|
|
629
|
+
return payload;
|
|
630
|
+
};
|
|
631
|
+
|
|
587
632
|
app.post('/dev/forward', async (req, res, next) => {
|
|
588
633
|
try {
|
|
589
634
|
// req.body: {message: string}
|
|
590
635
|
const {convo} = req.body;
|
|
591
|
-
|
|
592
|
-
const payload = await scout9.forward({convo}).then((_res => _res.data));
|
|
593
|
-
console.log(`\tForwarded in ${payload?.ms}ms`);
|
|
636
|
+
const payload = await devForward(convo);
|
|
594
637
|
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
595
638
|
res.end(JSON.stringify(payload));
|
|
596
639
|
} catch (e) {
|
|
@@ -598,40 +641,399 @@ if (dev) {
|
|
|
598
641
|
}
|
|
599
642
|
});
|
|
600
643
|
|
|
644
|
+
const devGenerate = async (messages, personaId) => {
|
|
645
|
+
if (typeof messages !== 'object' || !Array.isArray(messages)) {
|
|
646
|
+
throw new Error('Invalid messages array - expected to be an array of objects');
|
|
647
|
+
}
|
|
648
|
+
if (typeof personaId !== 'string') {
|
|
649
|
+
throw new Error('Invalid persona - expected to be a string');
|
|
650
|
+
}
|
|
651
|
+
const persona = (config.persona || config.agents).find(p => p.id === personaId);
|
|
652
|
+
if (!persona) {
|
|
653
|
+
throw new Error(`Could not find persona with id: ${personaId}, ensure your project is sync'd by running "scout9 sync"`);
|
|
654
|
+
}
|
|
655
|
+
console.log(`${grey(`${cyan('>')} Determining ${bold(white(persona.firstName))}'s`)} response`);
|
|
656
|
+
const payload = await scout9.generate({
|
|
657
|
+
messages,
|
|
658
|
+
persona,
|
|
659
|
+
llm: config.llm,
|
|
660
|
+
pmt: config.pmt
|
|
661
|
+
}).then((_res => _res.data));
|
|
662
|
+
console.log(`\t${grey(`Response: ${green('"')}${bold(white(payload.message))}`)}${green(
|
|
663
|
+
'"')} (elapsed ${payload.ms}ms)`);
|
|
664
|
+
|
|
665
|
+
return payload;
|
|
666
|
+
};
|
|
667
|
+
|
|
601
668
|
app.post('/dev/generate', async (req, res, next) => {
|
|
602
669
|
try {
|
|
603
670
|
// req.body: {conversation: {}, messages: []}
|
|
604
671
|
const {messages, persona: personaId} = req.body;
|
|
605
|
-
|
|
606
|
-
throw new Error('Invalid messages array - expected to be an array of objects');
|
|
607
|
-
}
|
|
608
|
-
if (typeof personaId !== 'string') {
|
|
609
|
-
throw new Error('Invalid persona - expected to be a string');
|
|
610
|
-
}
|
|
611
|
-
const persona = (config.persona || config.agents).find(p => p.id === personaId);
|
|
612
|
-
if (!persona) {
|
|
613
|
-
throw new Error(`Could not find persona with id: ${personaId}, ensure your project is sync'd by running "scout9 sync"`);
|
|
614
|
-
}
|
|
615
|
-
console.log(`${colors.grey(`${colors.cyan('>')} Generating ${colors.bold(colors.white(persona.firstName))}'s`)} ${colors.bold(
|
|
616
|
-
colors.red(colors.bgBlack('auto-reply')))}`);
|
|
617
|
-
const payload = await scout9.generate({
|
|
618
|
-
messages,
|
|
619
|
-
persona,
|
|
620
|
-
llm: config.llm,
|
|
621
|
-
pmt: config.pmt
|
|
622
|
-
}).then((_res => _res.data));
|
|
623
|
-
console.log(`\t${colors.grey(`Response: ${colors.green('"')}${colors.bold(colors.white(payload.message))}`)}${colors.green(
|
|
624
|
-
'"')} (elapsed ${payload.ms}ms)`);
|
|
672
|
+
const payload = await devGenerate(messages, personaId);
|
|
625
673
|
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
626
674
|
res.end(JSON.stringify(payload));
|
|
627
675
|
} catch (e) {
|
|
628
676
|
handleError(e, res);
|
|
629
677
|
}
|
|
630
678
|
});
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
// NOTE: This does not sync with localhost app, that uses its own state
|
|
682
|
+
devProgram = async () => {
|
|
683
|
+
// Start program where use can test via command console
|
|
684
|
+
const {createInterface} = await import('node:readline');
|
|
685
|
+
const rl = createInterface({
|
|
686
|
+
input: process.stdin,
|
|
687
|
+
output: process.stdout
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const persona = (config.persona || config.agents)?.[0];
|
|
691
|
+
if (!persona) {
|
|
692
|
+
throw new Error(`A persona is required before processing`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* @returns {Omit<WorkflowEvent, 'message'> & {command?: CommandConfiguration}}
|
|
697
|
+
*/
|
|
698
|
+
const createState = () => ({
|
|
699
|
+
messages: [],
|
|
700
|
+
conversation: {
|
|
701
|
+
$id: 'dev_console_input',
|
|
702
|
+
$agent: persona.id,
|
|
703
|
+
$customer: 'temp',
|
|
704
|
+
environment: 'web'
|
|
705
|
+
},
|
|
706
|
+
context: {},
|
|
707
|
+
agent: persona,
|
|
708
|
+
customer: {
|
|
709
|
+
firstName: 'test',
|
|
710
|
+
name: 'test'
|
|
711
|
+
},
|
|
712
|
+
intent: {current: null, flow: [], initial: null},
|
|
713
|
+
stagnationCount: 0
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
/** @type {Omit<WorkflowEvent, 'message'> & {command?: CommandConfiguration}} */
|
|
717
|
+
let state = createState();
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
async function processCustomerMessage(message, callback) {
|
|
721
|
+
const messagePayload = {
|
|
722
|
+
id: `user_test_${Date.now()}`,
|
|
723
|
+
role: 'customer',
|
|
724
|
+
content: message,
|
|
725
|
+
time: new Date().toISOString()
|
|
726
|
+
};
|
|
727
|
+
const logger = new ProgressLogger('Processing...');
|
|
728
|
+
if (state.conversation.locked) {
|
|
729
|
+
logger.error(`Conversation locked - ${state.conversation.lockedReason ?? 'Unknown reason'}`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const addMessage = (payload) => {
|
|
733
|
+
state.messages.push(payload);
|
|
734
|
+
switch (payload.role) {
|
|
735
|
+
case 'system':
|
|
736
|
+
logger.write(magenta('system: ' + payload.content));
|
|
737
|
+
break;
|
|
738
|
+
case 'user':
|
|
739
|
+
case 'customer':
|
|
740
|
+
logger.write(green(`> ${state.agent.firstName ? state.agent.firstName + ': ' : ''}` + payload.content));
|
|
741
|
+
break;
|
|
742
|
+
case 'agent':
|
|
743
|
+
case 'assistant':
|
|
744
|
+
logger.write(blue(`> ${state.customer.name ? state.customer.name + ': ' : ''}` + payload.content));
|
|
745
|
+
break;
|
|
746
|
+
default:
|
|
747
|
+
logger.write(red(`UNKNOWN (${payload.role}) + ${payload.content}`));
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
const updateMessage = (payload) => {
|
|
752
|
+
const index = state.messages.findIndex(m => m.id === payload.id);
|
|
753
|
+
if (index < 0) {
|
|
754
|
+
throw new Error(`Cannot find message ${payload.id}`);
|
|
755
|
+
}
|
|
756
|
+
state.messages[index] = payload;
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const removeMessage = (payload) => {
|
|
760
|
+
if (typeof payload !== 'string') {
|
|
761
|
+
throw new Error(`Invalid payload`);
|
|
762
|
+
}
|
|
763
|
+
const index = state.messages.findIndex(m => m.id === payload.id);
|
|
764
|
+
if (index < 0) {
|
|
765
|
+
throw new Error(`Cannot find message ${payload.id}`);
|
|
766
|
+
}
|
|
767
|
+
state.messages.splice(index, 1);
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const updateConversation = (payload) => {
|
|
771
|
+
Object.assign(state.conversation, payload);
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const updateContext = (payload) => {
|
|
775
|
+
Object.assign(state.context, payload);
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
addMessage(messagePayload);
|
|
779
|
+
const result = await Spirits.customer({
|
|
780
|
+
customer: state.customer,
|
|
781
|
+
config,
|
|
782
|
+
parser: async (_msg, _lng) => {
|
|
783
|
+
logger.log(`Parsing...`);
|
|
784
|
+
return devParse(_msg, _lng);
|
|
785
|
+
},
|
|
786
|
+
workflow: async (workflowEvent) => {
|
|
787
|
+
// Set the global variables for the workflows/commands to run Scout9 Macros
|
|
788
|
+
globalThis.SCOUT9 = {
|
|
789
|
+
...workflowEvent,
|
|
790
|
+
$convo: state.conversation.$id ?? state.conversation.id
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
logger.log(`Gathering ${state.command ? 'Command ' + state.command.entity + ' ' : ''}instructions...`);
|
|
794
|
+
if (state.command) {
|
|
795
|
+
const commandFilePath = resolve(commandsDir, state.command.path);
|
|
796
|
+
let mod;
|
|
797
|
+
try {
|
|
798
|
+
mod = await import(commandFilePath);
|
|
799
|
+
} catch (e) {
|
|
800
|
+
logger.error(`Unable to resolve command ${state.command.entity} at ${commandFilePath}`);
|
|
801
|
+
throw new Error('Failed to gather command instructions');
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (!mod || !mod.default) {
|
|
805
|
+
logger.error(`Unable to run command ${state.command.entity} at ${commandFilePath} - must return a default function that returns a WorkflowEvent payload`);
|
|
806
|
+
throw new Error('Failed to run command instructions');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
|
|
811
|
+
return mod.default(workflowEvent)
|
|
812
|
+
.then((response) => {
|
|
813
|
+
if ('toJSON' in response) {
|
|
814
|
+
return response.toJSON();
|
|
815
|
+
} else {
|
|
816
|
+
return response;
|
|
817
|
+
}
|
|
818
|
+
})
|
|
819
|
+
.then(WorkflowResponseSchema.parse);
|
|
820
|
+
} catch (e) {
|
|
821
|
+
logger.error(`Failed to run command - ${e.message}`);
|
|
822
|
+
throw e;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
} else {
|
|
826
|
+
return projectApp(workflowEvent)
|
|
827
|
+
.then((response) => {
|
|
828
|
+
if ('toJSON' in response) {
|
|
829
|
+
return response.toJSON();
|
|
830
|
+
} else {
|
|
831
|
+
return response;
|
|
832
|
+
}
|
|
833
|
+
})
|
|
834
|
+
.then(WorkflowResponseSchema.parse);
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
generator: async (request) => {
|
|
838
|
+
logger.log(`Determining response...`);
|
|
839
|
+
const personaId = typeof request.persona === 'string' ? request.persona : request.persona.id;
|
|
840
|
+
return devGenerate(request.messages, personaId);
|
|
841
|
+
},
|
|
842
|
+
idGenerator: (prefix) => `${prefix}_test_${Date.now()}`,
|
|
843
|
+
progress: (
|
|
844
|
+
message,
|
|
845
|
+
level,
|
|
846
|
+
type,
|
|
847
|
+
payload
|
|
848
|
+
) => {
|
|
849
|
+
callback(message, level);
|
|
850
|
+
if (type) {
|
|
851
|
+
switch (type) {
|
|
852
|
+
case 'ADD_MESSAGE':
|
|
853
|
+
addMessage(payload);
|
|
854
|
+
break;
|
|
855
|
+
case 'UPDATE_MESSAGE':
|
|
856
|
+
updateMessage(payload);
|
|
857
|
+
break;
|
|
858
|
+
case 'REMOVE_MESSAGE':
|
|
859
|
+
removeMessage(payload);
|
|
860
|
+
break;
|
|
861
|
+
case 'UPDATE_CONVERSATION':
|
|
862
|
+
updateConversation(payload);
|
|
863
|
+
break;
|
|
864
|
+
case 'UPDATE_CONTEXT':
|
|
865
|
+
updateContext(payload);
|
|
866
|
+
break;
|
|
867
|
+
case 'SET_PROCESSING':
|
|
868
|
+
break;
|
|
869
|
+
default:
|
|
870
|
+
throw new Error(`Unknown progress type: ${type}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
message: messagePayload,
|
|
875
|
+
context: state.context,
|
|
876
|
+
messages: state.messages,
|
|
877
|
+
conversation: state.conversation
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// If a forward happens (due to a lock or other reason)
|
|
881
|
+
if (!!result.conversation.forward) {
|
|
882
|
+
if (!state.conversation.locked) {
|
|
883
|
+
// Only forward if conversation is not already locked
|
|
884
|
+
await devForward(state.conversation.$id);
|
|
885
|
+
}
|
|
886
|
+
updateConversation({locked: true});
|
|
887
|
+
logger.error(`Conversation locked`);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Process changes as a success
|
|
892
|
+
|
|
893
|
+
// Update conversation (assuming it's changed)
|
|
894
|
+
if (result.conversation.after) {
|
|
895
|
+
updateConversation(result.conversation.after);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Update conversation context (assuming it's changed)
|
|
899
|
+
if (result.context) {
|
|
900
|
+
updateContext(result.context.after);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (!result.messages.after.find(m => m.id === result.message.after.id)) {
|
|
904
|
+
console.error(`Message not found in result.messages.after`, result.message.after.id);
|
|
905
|
+
result.messages.after.push(result.message.after);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Sync messages state update/add/delete
|
|
909
|
+
for (const message of result.messages.after) {
|
|
910
|
+
// Did this exist?
|
|
911
|
+
const existed = !!result.messages.before.find(m => m.id === message.id);
|
|
912
|
+
if (existed) {
|
|
913
|
+
updateMessage(message);
|
|
914
|
+
} else {
|
|
915
|
+
addMessage(message);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
for (const message of result.messages.before) {
|
|
919
|
+
const exists = !!result.messages.after.find(m => m.id === message.id);
|
|
920
|
+
if (!exists) {
|
|
921
|
+
removeMessage(message.id);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
logger.done();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* @param {CommandConfiguration} command
|
|
930
|
+
* @param {string} message
|
|
931
|
+
* @param callback
|
|
932
|
+
* @returns {Promise<void>}
|
|
933
|
+
*/
|
|
934
|
+
async function processCommand(command, message, callback) {
|
|
935
|
+
console.log(magenta(`> command <${command.entity}>`));
|
|
936
|
+
state = createState();
|
|
937
|
+
state.command = command;
|
|
938
|
+
return processCustomerMessage(`Assist me in this ${command.entity} flow`, callback);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async function devProgramProcessInput(message, callback) {
|
|
942
|
+
// Check if internal command
|
|
943
|
+
switch (message.toLowerCase().trim()) {
|
|
944
|
+
case 'context':
|
|
945
|
+
console.log(white('> Current Conversation Context:'));
|
|
946
|
+
console.log(grey(JSON.stringify(state.context)));
|
|
947
|
+
return;
|
|
948
|
+
case 'conversation':
|
|
949
|
+
case 'convo':
|
|
950
|
+
console.log(white('> Current Conversation State:'));
|
|
951
|
+
console.log(grey(JSON.stringify(state.conversation)));
|
|
952
|
+
return;
|
|
953
|
+
case 'messages':
|
|
954
|
+
state.messages.forEach((msg) => {
|
|
955
|
+
switch (msg.role) {
|
|
956
|
+
case 'system':
|
|
957
|
+
console.log(magenta('\t - ' + msg.content));
|
|
958
|
+
break;
|
|
959
|
+
case 'user':
|
|
960
|
+
case 'customer':
|
|
961
|
+
console.log(green('> ' + msg.content));
|
|
962
|
+
break;
|
|
963
|
+
case 'agent':
|
|
964
|
+
case 'assistant':
|
|
965
|
+
console.log(blue('> ' + msg.content));
|
|
966
|
+
break;
|
|
967
|
+
default:
|
|
968
|
+
console.log(red(`UNKNOWN (${msg.role}) + ${msg.content}`));
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Check if it's a command
|
|
975
|
+
const target = message.toLowerCase().trim();
|
|
976
|
+
const command = config.commands.find(command => {
|
|
977
|
+
return command.entity === target;
|
|
978
|
+
});
|
|
979
|
+
// Run the command
|
|
980
|
+
if (command) {
|
|
981
|
+
return processCommand(command, message, callback);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Otherwise default to processing customer message
|
|
985
|
+
return processCustomerMessage(message, callback);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Function to ask for input, perform the task, and then ask again
|
|
989
|
+
function promptUser() {
|
|
990
|
+
rl.question('> ', async (input) => {
|
|
991
|
+
if (input.toLowerCase() === 'exit') {
|
|
992
|
+
rl.close();
|
|
993
|
+
} else {
|
|
994
|
+
if (input) {
|
|
995
|
+
await devProgramProcessInput(input, () => {
|
|
996
|
+
//
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
promptUser();
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
console.log(grey(`\nThe following ${bold('commands')} are available...`));
|
|
1007
|
+
[['context', 'logs the state context inserted into the conversation'], ['conversation', 'logs conversation details'], ['messages', 'logs all message history']].forEach(([command, description]) => {
|
|
1008
|
+
console.log(`\t - ${magenta(command)} ${grey(description)}`);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
if (config.commands.length) {
|
|
1012
|
+
console.log(grey(`\nThe following ${bold('custom commands')} are available...`));
|
|
1013
|
+
}
|
|
1014
|
+
config.commands.forEach((command) => {
|
|
1015
|
+
console.log(magenta(`\t - ${command.entity}`));
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// Start the first prompt
|
|
1019
|
+
|
|
1020
|
+
console.log(white(`\nType and hit enter to test your PMT responses...\n`));
|
|
1021
|
+
promptUser();
|
|
1022
|
+
|
|
1023
|
+
// Handle Ctrl+C (SIGINT) signal to exit gracefully
|
|
1024
|
+
rl.on('SIGINT', () => {
|
|
1025
|
+
rl.close();
|
|
1026
|
+
process.exit(0);
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
|
|
631
1033
|
}
|
|
632
1034
|
|
|
633
1035
|
|
|
634
|
-
app.listen(process.env.PORT || 8080, err => {
|
|
1036
|
+
app.listen(process.env.PORT || 8080, async (err) => {
|
|
635
1037
|
if (err) throw err;
|
|
636
1038
|
|
|
637
1039
|
const art_scout9 = `
|
|
@@ -647,6 +1049,8 @@ app.listen(process.env.PORT || 8080, err => {
|
|
|
647
1049
|
\\|_________|
|
|
648
1050
|
`;
|
|
649
1051
|
const art_pmt = `
|
|
1052
|
+
|
|
1053
|
+
|
|
650
1054
|
_______ __ __ ________
|
|
651
1055
|
| \ | \ / \| \
|
|
652
1056
|
| $$$$$$$\| $$\ / $$ \$$$$$$$$
|
|
@@ -657,34 +1061,35 @@ app.listen(process.env.PORT || 8080, err => {
|
|
|
657
1061
|
| $$ | $$ \$ | $$ | $$
|
|
658
1062
|
\$$ \$$ \$$ \$$
|
|
659
1063
|
|
|
660
|
-
|
|
661
|
-
|
|
662
1064
|
`;
|
|
663
1065
|
const protocol = process.env.PROTOCOL || 'http';
|
|
664
1066
|
const host = process.env.HOST || 'localhost';
|
|
665
1067
|
const port = process.env.PORT || 8080;
|
|
666
1068
|
const fullUrl = `${protocol}://${host}:${port}`;
|
|
667
1069
|
if (dev) {
|
|
668
|
-
console.log(
|
|
669
|
-
console.log(
|
|
670
|
-
console.log(`${
|
|
671
|
-
colors.red(colors.bgBlack('auto-reply')))} ${colors.grey('dev environment on')} ${fullUrl}`);
|
|
1070
|
+
console.log(bold(green(art_scout9)));
|
|
1071
|
+
console.log(bold(cyan(art_pmt)));
|
|
1072
|
+
console.log(`${grey(`${cyan('>')} Running ${bold(white('Scout9'))}`)} ${grey('dev environment on')} ${fullUrl}`);
|
|
672
1073
|
} else {
|
|
673
|
-
console.log(`Running Scout9
|
|
1074
|
+
console.log(`Running Scout9 app on ${fullUrl}`);
|
|
674
1075
|
}
|
|
675
1076
|
// Run checks
|
|
676
1077
|
if (!fs.existsSync(configFilePath)) {
|
|
677
|
-
console.log(
|
|
1078
|
+
console.log(red('Missing .env file, your PMT application may not work without it.'));
|
|
678
1079
|
}
|
|
679
1080
|
|
|
680
1081
|
if (dev && !process.env.SCOUT9_API_KEY) {
|
|
681
|
-
console.log(
|
|
682
|
-
'Missing SCOUT9_API_KEY environment variable, your
|
|
1082
|
+
console.log(red(
|
|
1083
|
+
'Missing SCOUT9_API_KEY environment variable, your PMT application may not work without it.'));
|
|
683
1084
|
}
|
|
684
1085
|
|
|
685
1086
|
if (process.env.SCOUT9_API_KEY === '<insert-scout9-api-key>') {
|
|
686
|
-
console.log(`${
|
|
687
|
-
'You can find your API key in the Scout9 dashboard.')} ${
|
|
1087
|
+
console.log(`${red('SCOUT9_API_KEY has not been set in your .env file.')} ${grey(
|
|
1088
|
+
'You can find your API key in the Scout9 dashboard.')} ${bold(cyan('https://scout9.com'))}`);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (dev) {
|
|
1092
|
+
devProgram();
|
|
688
1093
|
}
|
|
689
1094
|
|
|
690
1095
|
});
|