@scout9/app 1.0.0-alpha.0.5.8 → 1.0.0-alpha.0.5.9
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-ccf05f24.cjs → dev-8ed1e379.cjs} +155 -9
- package/dist/{index-b329d3e5.cjs → index-4f698e67.cjs} +7 -7
- package/dist/index.cjs +4 -4
- package/dist/{macros-f855de63.cjs → macros-4bff92c7.cjs} +12 -3
- package/dist/{multipart-parser-5819fcfd.cjs → multipart-parser-8f894f70.cjs} +4 -4
- package/dist/schemas.cjs +1 -1
- package/dist/{spirits-2ab4d673.cjs → spirits-82575cd5.cjs} +160 -78
- package/dist/spirits.cjs +1 -1
- package/dist/testing-tools.cjs +3 -3
- package/package.json +2 -1
- package/src/core/templates/app.js +277 -178
- package/src/public.d.ts +8 -2
- package/src/runtime/schemas/users.js +6 -1
- package/src/runtime/schemas/workflow.js +6 -2
- package/src/testing-tools/spirits.js +46 -0
- package/types/index.d.ts +1057 -161
- package/types/index.d.ts.map +1 -1
|
@@ -5,16 +5,18 @@ import bodyParser from 'body-parser';
|
|
|
5
5
|
import { config as dotenv } from 'dotenv';
|
|
6
6
|
import { Configuration, Scout9Api } from '@scout9/admin';
|
|
7
7
|
import { EventResponse, ProgressLogger } from '@scout9/app';
|
|
8
|
-
import { WorkflowEventSchema, WorkflowResponseSchema } from '@scout9/app/schemas';
|
|
8
|
+
import { WorkflowEventSchema, WorkflowResponseSchema, MessageSchema } from '@scout9/app/schemas';
|
|
9
9
|
import { Spirits } from '@scout9/app/spirits';
|
|
10
10
|
import path, { resolve } from 'node:path';
|
|
11
|
+
import { createServer } from 'node:http';
|
|
11
12
|
import fs from 'node:fs';
|
|
12
13
|
import https from 'node:https';
|
|
13
14
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
14
15
|
import { readdir } from 'fs/promises';
|
|
15
16
|
import { ZodError } from 'zod';
|
|
16
17
|
import { fromError } from 'zod-validation-error';
|
|
17
|
-
import { bgBlack, blue, bold, cyan, green, grey, magenta, red, white } from 'kleur/colors';
|
|
18
|
+
import { bgBlack, blue, bold, cyan, green, grey, magenta, red, white, yellow } from 'kleur/colors';
|
|
19
|
+
|
|
18
20
|
import projectApp from './src/app.js';
|
|
19
21
|
import config from './config.js';
|
|
20
22
|
|
|
@@ -86,10 +88,10 @@ const handleError = (e, res = undefined, tag = undefined, body = undefined) => {
|
|
|
86
88
|
let name = e?.name || 'Runtime Error';
|
|
87
89
|
let message = e?.message || 'Unknown error';
|
|
88
90
|
let code = typeof e?.code === 'number'
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
? e.code
|
|
92
|
+
: typeof e?.status === 'number'
|
|
93
|
+
? e.status
|
|
94
|
+
: 500;
|
|
93
95
|
if ('response' in e) {
|
|
94
96
|
const response = e.response;
|
|
95
97
|
if (response?.status) {
|
|
@@ -128,18 +130,19 @@ const handleError = (e, res = undefined, tag = undefined, body = undefined) => {
|
|
|
128
130
|
code
|
|
129
131
|
}));
|
|
130
132
|
}
|
|
133
|
+
return {name, error: message, code};
|
|
131
134
|
};
|
|
132
135
|
|
|
133
136
|
const handleZodError = ({
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
error,
|
|
138
|
+
res = undefined,
|
|
139
|
+
code = 500,
|
|
140
|
+
status,
|
|
141
|
+
name,
|
|
142
|
+
bodyLabel = 'Provided Input',
|
|
143
|
+
body = undefined,
|
|
144
|
+
action = ''
|
|
145
|
+
}) => {
|
|
143
146
|
res?.writeHead?.(code, {'Content-Type': 'application/json'});
|
|
144
147
|
if (error instanceof ZodError) {
|
|
145
148
|
const formattedError = simplifyZodError(error);
|
|
@@ -170,13 +173,13 @@ const handleWorkflowResponse = async ({fun, workflowEvent, tag, expressRes: res,
|
|
|
170
173
|
let response;
|
|
171
174
|
try {
|
|
172
175
|
response = await fun(workflowEvent)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
.then((slots) => {
|
|
177
|
+
if ('toJSON' in slots) {
|
|
178
|
+
return slots.toJSON();
|
|
179
|
+
} else {
|
|
180
|
+
return slots;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
180
183
|
} catch (error) {
|
|
181
184
|
if (error instanceof ZodError) {
|
|
182
185
|
handleZodError({
|
|
@@ -222,7 +225,7 @@ const handleWorkflowResponse = async ({fun, workflowEvent, tag, expressRes: res,
|
|
|
222
225
|
}
|
|
223
226
|
}
|
|
224
227
|
|
|
225
|
-
}
|
|
228
|
+
};
|
|
226
229
|
|
|
227
230
|
const makeRequest = async (options, maxRedirects = 10) => {
|
|
228
231
|
return new Promise((resolve, reject) => {
|
|
@@ -279,6 +282,12 @@ app.use(bodyParser.json());
|
|
|
279
282
|
if (dev) {
|
|
280
283
|
app.use(compression());
|
|
281
284
|
app.use(sirv(path.resolve(__dirname, 'public'), {dev: true}));
|
|
285
|
+
app.use((req, res, next) => {
|
|
286
|
+
res.setHeader('Access-Control-Allow-Origin', '*'); // Adjust origin as needed
|
|
287
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, POST, OPTIONS');
|
|
288
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
289
|
+
next();
|
|
290
|
+
});
|
|
282
291
|
}
|
|
283
292
|
|
|
284
293
|
function parseWorkflowEvent(req, res) {
|
|
@@ -323,9 +332,9 @@ app.post(dev ? '/dev/workflow' : '/', async (req, res) => {
|
|
|
323
332
|
await handleWorkflowResponse({
|
|
324
333
|
fun: projectApp,
|
|
325
334
|
workflowEvent,
|
|
326
|
-
tag:
|
|
335
|
+
tag: 'PMTFlow',
|
|
327
336
|
expressRes: res,
|
|
328
|
-
expressReq: req
|
|
337
|
+
expressReq: req
|
|
329
338
|
});
|
|
330
339
|
return;
|
|
331
340
|
});
|
|
@@ -361,17 +370,17 @@ async function resolveEntityApi(entity, method) {
|
|
|
361
370
|
}
|
|
362
371
|
const {api, entities} = resolveEntity(entity, method);
|
|
363
372
|
const mod = await import(pathToFileURL(path.resolve(__dirname, `./src/entities/${entities.join('/')}/api.js`)).href)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
373
|
+
.catch((e) => {
|
|
374
|
+
switch (e.code) {
|
|
375
|
+
case 'ERR_MODULE_NOT_FOUND':
|
|
376
|
+
case 'MODULE_NOT_FOUND':
|
|
377
|
+
console.error(e);
|
|
378
|
+
throw new Error(`Invalid entity: no API method`);
|
|
379
|
+
default:
|
|
380
|
+
console.error(e);
|
|
381
|
+
throw new Error(`Invalid entity: Internal system error`);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
375
384
|
if (mod[method]) {
|
|
376
385
|
return mod[method];
|
|
377
386
|
}
|
|
@@ -384,7 +393,6 @@ async function resolveEntityApi(entity, method) {
|
|
|
384
393
|
|
|
385
394
|
}
|
|
386
395
|
|
|
387
|
-
|
|
388
396
|
function extractParamsFromPath(path) {
|
|
389
397
|
const segments = path.split('/').filter(Boolean); // Split and remove empty segments
|
|
390
398
|
let params = {};
|
|
@@ -468,7 +476,7 @@ async function runCommandApi(req, res) {
|
|
|
468
476
|
const params = url.split('/').slice(2).filter(Boolean);
|
|
469
477
|
try {
|
|
470
478
|
const files = await getFilesRecursive(commandsDir).then(files => files.map(file => file.replace(commandsDir, '.'))
|
|
471
|
-
|
|
479
|
+
.filter(file => params.every(p => file.includes(p))));
|
|
472
480
|
file = files?.[0];
|
|
473
481
|
} catch (e) {
|
|
474
482
|
console.log('No commands found', e.message);
|
|
@@ -516,7 +524,13 @@ async function runCommandApi(req, res) {
|
|
|
516
524
|
return;
|
|
517
525
|
}
|
|
518
526
|
|
|
519
|
-
await handleWorkflowResponse({
|
|
527
|
+
await handleWorkflowResponse({
|
|
528
|
+
fun: mod.default,
|
|
529
|
+
workflowEvent,
|
|
530
|
+
tag: `${params.join('_').toUpperCase()} Command`,
|
|
531
|
+
expressReq: req,
|
|
532
|
+
expressRes: res
|
|
533
|
+
});
|
|
520
534
|
}
|
|
521
535
|
|
|
522
536
|
app.post('/commands/:command', runCommandApi);
|
|
@@ -533,9 +547,11 @@ app.post('/entity/:entity/*', runEntityApi);
|
|
|
533
547
|
app.delete('/entity/:entity/*', runEntityApi);
|
|
534
548
|
|
|
535
549
|
// For local development: parse a message
|
|
536
|
-
let
|
|
550
|
+
let devReadlineProgram;
|
|
551
|
+
let devServer;
|
|
537
552
|
if (dev) {
|
|
538
553
|
|
|
554
|
+
|
|
539
555
|
app.get('/dev/config', async (req, res, next) => {
|
|
540
556
|
|
|
541
557
|
// Retrieve auth token
|
|
@@ -638,7 +654,7 @@ if (dev) {
|
|
|
638
654
|
pmt: config.pmt
|
|
639
655
|
}).then((_res => _res.data));
|
|
640
656
|
console.log(`\t${grey(`Response: ${green('"')}${bold(white(payload.message))}`)}${green(
|
|
641
|
-
|
|
657
|
+
'"')} (elapsed ${payload.ms}ms)`);
|
|
642
658
|
|
|
643
659
|
return payload;
|
|
644
660
|
};
|
|
@@ -655,33 +671,35 @@ if (dev) {
|
|
|
655
671
|
}
|
|
656
672
|
});
|
|
657
673
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const rl = createInterface({
|
|
664
|
-
input: process.stdin,
|
|
665
|
-
output: process.stdout
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
const persona = (config.persona || config.agents)?.[0];
|
|
674
|
+
const devPersona = (config.persona || config.agents)?.[0];
|
|
675
|
+
/**
|
|
676
|
+
* @returns {Omit<WorkflowEvent, 'message'> & {command?: CommandConfiguration}}
|
|
677
|
+
*/
|
|
678
|
+
const devCreateState = (persona = (config.persona || config.agents)?.[0]) => {
|
|
669
679
|
if (!persona) {
|
|
670
680
|
throw new Error(`A persona is required before processing`);
|
|
671
681
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
682
|
+
return {
|
|
683
|
+
messages: config.initialContext?.map((_context, index) => ({
|
|
684
|
+
id: `ctx_${index}`,
|
|
685
|
+
time: new Date().toISOString(),
|
|
686
|
+
content: _context,
|
|
687
|
+
role: 'system'
|
|
688
|
+
})),
|
|
678
689
|
conversation: {
|
|
679
690
|
$id: 'dev_console_input',
|
|
680
691
|
$agent: persona.id,
|
|
681
692
|
$customer: 'temp',
|
|
682
693
|
environment: 'web'
|
|
683
694
|
},
|
|
684
|
-
context: {
|
|
695
|
+
context: {
|
|
696
|
+
agent: persona,
|
|
697
|
+
customer: {
|
|
698
|
+
firstName: 'test',
|
|
699
|
+
name: 'test'
|
|
700
|
+
},
|
|
701
|
+
organization: config.organization
|
|
702
|
+
},
|
|
685
703
|
agent: persona,
|
|
686
704
|
customer: {
|
|
687
705
|
firstName: 'test',
|
|
@@ -689,37 +707,38 @@ if (dev) {
|
|
|
689
707
|
},
|
|
690
708
|
intent: {current: null, flow: [], initial: null},
|
|
691
709
|
stagnationCount: 0
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/** @type {Omit<WorkflowEvent, 'message'> & {command?: CommandConfiguration}} */
|
|
695
|
-
let state = createState();
|
|
710
|
+
};
|
|
711
|
+
};
|
|
696
712
|
|
|
713
|
+
/** @type {Omit<WorkflowEvent, 'message'> & {command?: CommandConfiguration}} */
|
|
714
|
+
let devState = devCreateState(devPersona);
|
|
697
715
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
716
|
+
async function devProcessCustomerMessage(message, callback, roleOverride = null) {
|
|
717
|
+
const messagePayload = {
|
|
718
|
+
id: `user_test_${Date.now()}`,
|
|
719
|
+
role: roleOverride ?? 'customer',
|
|
720
|
+
content: message,
|
|
721
|
+
time: new Date().toISOString()
|
|
722
|
+
};
|
|
723
|
+
const logger = new ProgressLogger('Processing...');
|
|
724
|
+
try {
|
|
725
|
+
if (devState.conversation.locked) {
|
|
726
|
+
logger.error(`Conversation locked - ${devState.conversation.lockedReason ?? 'Unknown reason'}`);
|
|
708
727
|
return;
|
|
709
728
|
}
|
|
710
729
|
const addMessage = (payload) => {
|
|
711
|
-
|
|
730
|
+
devState.messages.push(payload);
|
|
712
731
|
switch (payload.role) {
|
|
713
732
|
case 'system':
|
|
714
733
|
logger.write(magenta('system: ' + payload.content));
|
|
715
734
|
break;
|
|
716
735
|
case 'user':
|
|
717
736
|
case 'customer':
|
|
718
|
-
logger.write(green(`> ${
|
|
737
|
+
logger.write(green(`> ${devState.agent.firstName ? devState.agent.firstName + ': ' : ''}` + payload.content));
|
|
719
738
|
break;
|
|
720
739
|
case 'agent':
|
|
721
740
|
case 'assistant':
|
|
722
|
-
logger.write(blue(`> ${
|
|
741
|
+
logger.write(blue(`> ${devState.customer.name ? devState.customer.name + ': ' : ''}` + payload.content));
|
|
723
742
|
break;
|
|
724
743
|
default:
|
|
725
744
|
logger.write(red(`UNKNOWN (${payload.role}) + ${payload.content}`));
|
|
@@ -727,35 +746,35 @@ if (dev) {
|
|
|
727
746
|
};
|
|
728
747
|
|
|
729
748
|
const updateMessage = (payload) => {
|
|
730
|
-
const index =
|
|
749
|
+
const index = devState.messages.findIndex(m => m.id === payload.id);
|
|
731
750
|
if (index < 0) {
|
|
732
751
|
throw new Error(`Cannot find message ${payload.id}`);
|
|
733
752
|
}
|
|
734
|
-
|
|
753
|
+
devState.messages[index] = payload;
|
|
735
754
|
};
|
|
736
755
|
|
|
737
756
|
const removeMessage = (payload) => {
|
|
738
757
|
if (typeof payload !== 'string') {
|
|
739
758
|
throw new Error(`Invalid payload`);
|
|
740
759
|
}
|
|
741
|
-
const index =
|
|
760
|
+
const index = devState.messages.findIndex(m => m.id === payload.id);
|
|
742
761
|
if (index < 0) {
|
|
743
762
|
throw new Error(`Cannot find message ${payload.id}`);
|
|
744
763
|
}
|
|
745
|
-
|
|
764
|
+
devState.messages.splice(index, 1);
|
|
746
765
|
};
|
|
747
766
|
|
|
748
767
|
const updateConversation = (payload) => {
|
|
749
|
-
Object.assign(
|
|
768
|
+
Object.assign(devState.conversation, payload);
|
|
750
769
|
};
|
|
751
770
|
|
|
752
771
|
const updateContext = (payload) => {
|
|
753
|
-
Object.assign(
|
|
772
|
+
Object.assign(devState.context, payload);
|
|
754
773
|
};
|
|
755
774
|
|
|
756
775
|
addMessage(messagePayload);
|
|
757
776
|
const result = await Spirits.customer({
|
|
758
|
-
customer:
|
|
777
|
+
customer: devState.customer,
|
|
759
778
|
config,
|
|
760
779
|
parser: async (_msg, _lng) => {
|
|
761
780
|
logger.log(`Parsing...`);
|
|
@@ -765,43 +784,28 @@ if (dev) {
|
|
|
765
784
|
// Set the global variables for the workflows/commands to run Scout9 Macros
|
|
766
785
|
globalThis.SCOUT9 = {
|
|
767
786
|
...workflowEvent,
|
|
768
|
-
$convo:
|
|
787
|
+
$convo: devState.conversation.$id ?? devState.conversation.id
|
|
769
788
|
};
|
|
770
789
|
|
|
771
|
-
logger.log(`Gathering ${
|
|
772
|
-
if (
|
|
773
|
-
const commandFilePath = resolve(commandsDir,
|
|
790
|
+
logger.log(`Gathering ${devState.command ? 'Command ' + devState.command.entity + ' ' : ''}instructions...`);
|
|
791
|
+
if (devState.command) {
|
|
792
|
+
const commandFilePath = resolve(commandsDir, devState.command.path);
|
|
774
793
|
let mod;
|
|
775
794
|
try {
|
|
776
795
|
mod = await import(commandFilePath);
|
|
777
796
|
} catch (e) {
|
|
778
|
-
logger.error(`Unable to resolve command ${
|
|
797
|
+
logger.error(`Unable to resolve command ${devState.command.entity} at ${commandFilePath}`);
|
|
779
798
|
throw new Error('Failed to gather command instructions');
|
|
780
799
|
}
|
|
781
800
|
|
|
782
801
|
if (!mod || !mod.default) {
|
|
783
|
-
logger.error(`Unable to run command ${
|
|
802
|
+
logger.error(`Unable to run command ${devState.command.entity} at ${commandFilePath} - must return a default function that returns a WorkflowEvent payload`);
|
|
784
803
|
throw new Error('Failed to run command instructions');
|
|
785
804
|
}
|
|
786
805
|
|
|
787
806
|
try {
|
|
788
807
|
|
|
789
808
|
return mod.default(workflowEvent)
|
|
790
|
-
.then((response) => {
|
|
791
|
-
if ('toJSON' in response) {
|
|
792
|
-
return response.toJSON();
|
|
793
|
-
} else {
|
|
794
|
-
return response;
|
|
795
|
-
}
|
|
796
|
-
})
|
|
797
|
-
.then(WorkflowResponseSchema.parse);
|
|
798
|
-
} catch (e) {
|
|
799
|
-
logger.error(`Failed to run command - ${e.message}`);
|
|
800
|
-
throw e;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
} else {
|
|
804
|
-
return projectApp(workflowEvent)
|
|
805
809
|
.then((response) => {
|
|
806
810
|
if ('toJSON' in response) {
|
|
807
811
|
return response.toJSON();
|
|
@@ -810,6 +814,21 @@ if (dev) {
|
|
|
810
814
|
}
|
|
811
815
|
})
|
|
812
816
|
.then(WorkflowResponseSchema.parse);
|
|
817
|
+
} catch (e) {
|
|
818
|
+
logger.error(`Failed to run command - ${e.message}`);
|
|
819
|
+
throw e;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
} else {
|
|
823
|
+
return projectApp(workflowEvent)
|
|
824
|
+
.then((response) => {
|
|
825
|
+
if ('toJSON' in response) {
|
|
826
|
+
return response.toJSON();
|
|
827
|
+
} else {
|
|
828
|
+
return response;
|
|
829
|
+
}
|
|
830
|
+
})
|
|
831
|
+
.then(WorkflowResponseSchema.parse);
|
|
813
832
|
}
|
|
814
833
|
},
|
|
815
834
|
generator: async (request) => {
|
|
@@ -819,10 +838,10 @@ if (dev) {
|
|
|
819
838
|
},
|
|
820
839
|
idGenerator: (prefix) => `${prefix}_test_${Date.now()}`,
|
|
821
840
|
progress: (
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
841
|
+
message,
|
|
842
|
+
level,
|
|
843
|
+
type,
|
|
844
|
+
payload
|
|
826
845
|
) => {
|
|
827
846
|
callback(message, level);
|
|
828
847
|
if (type) {
|
|
@@ -850,16 +869,16 @@ if (dev) {
|
|
|
850
869
|
}
|
|
851
870
|
},
|
|
852
871
|
message: messagePayload,
|
|
853
|
-
context:
|
|
854
|
-
messages:
|
|
855
|
-
conversation:
|
|
872
|
+
context: devState.context,
|
|
873
|
+
messages: devState.messages,
|
|
874
|
+
conversation: devState.conversation
|
|
856
875
|
});
|
|
857
876
|
|
|
858
877
|
// If a forward happens (due to a lock or other reason)
|
|
859
878
|
if (!!result.conversation.forward) {
|
|
860
|
-
if (!
|
|
879
|
+
if (!devState.conversation.locked) {
|
|
861
880
|
// Only forward if conversation is not already locked
|
|
862
|
-
await devForward(
|
|
881
|
+
await devForward(devState.conversation.$id);
|
|
863
882
|
}
|
|
864
883
|
updateConversation({locked: true});
|
|
865
884
|
logger.error(`Conversation locked`);
|
|
@@ -900,68 +919,82 @@ if (dev) {
|
|
|
900
919
|
}
|
|
901
920
|
}
|
|
902
921
|
|
|
922
|
+
} catch (e) {
|
|
923
|
+
handleError(e);
|
|
924
|
+
} finally {
|
|
903
925
|
logger.done();
|
|
904
926
|
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* @param {CommandConfiguration} command
|
|
931
|
+
* @param {string} message
|
|
932
|
+
* @param callback
|
|
933
|
+
* @returns {Promise<void>}
|
|
934
|
+
*/
|
|
935
|
+
async function devProcessCommand(command, message, callback) {
|
|
936
|
+
console.log(magenta(`> command <${command.entity}>`));
|
|
937
|
+
devState = devCreateState();
|
|
938
|
+
devState.command = command;
|
|
939
|
+
return devProcessCustomerMessage(`Assist me in this ${command.entity} flow`, callback);
|
|
940
|
+
}
|
|
905
941
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
942
|
+
async function devProgramProcessInput(message, callback, roleOverride = null) {
|
|
943
|
+
// Check if internal command
|
|
944
|
+
switch (message.toLowerCase().trim()) {
|
|
945
|
+
case 'context':
|
|
946
|
+
console.log(white('> Current Conversation Context:'));
|
|
947
|
+
console.log(grey(JSON.stringify(devState.context)));
|
|
948
|
+
return;
|
|
949
|
+
case 'conversation':
|
|
950
|
+
case 'convo':
|
|
951
|
+
console.log(white('> Current Conversation State:'));
|
|
952
|
+
console.log(grey(JSON.stringify(devState.conversation)));
|
|
953
|
+
return;
|
|
954
|
+
case 'messages':
|
|
955
|
+
devState.messages.forEach((msg) => {
|
|
956
|
+
switch (msg.role) {
|
|
957
|
+
case 'system':
|
|
958
|
+
console.log(magenta('\t - ' + msg.content));
|
|
959
|
+
break;
|
|
960
|
+
case 'user':
|
|
961
|
+
case 'customer':
|
|
962
|
+
console.log(green('> ' + msg.content));
|
|
963
|
+
break;
|
|
964
|
+
case 'agent':
|
|
965
|
+
case 'assistant':
|
|
966
|
+
console.log(blue('> ' + msg.content));
|
|
967
|
+
break;
|
|
968
|
+
default:
|
|
969
|
+
console.log(red(`UNKNOWN (${msg.role}) + ${msg.content}`));
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
return;
|
|
917
973
|
}
|
|
918
974
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
console.log(white('> Current Conversation State:'));
|
|
929
|
-
console.log(grey(JSON.stringify(state.conversation)));
|
|
930
|
-
return;
|
|
931
|
-
case 'messages':
|
|
932
|
-
state.messages.forEach((msg) => {
|
|
933
|
-
switch (msg.role) {
|
|
934
|
-
case 'system':
|
|
935
|
-
console.log(magenta('\t - ' + msg.content));
|
|
936
|
-
break;
|
|
937
|
-
case 'user':
|
|
938
|
-
case 'customer':
|
|
939
|
-
console.log(green('> ' + msg.content));
|
|
940
|
-
break;
|
|
941
|
-
case 'agent':
|
|
942
|
-
case 'assistant':
|
|
943
|
-
console.log(blue('> ' + msg.content));
|
|
944
|
-
break;
|
|
945
|
-
default:
|
|
946
|
-
console.log(red(`UNKNOWN (${msg.role}) + ${msg.content}`));
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
975
|
+
// Check if it's a command
|
|
976
|
+
const target = message.toLowerCase().trim();
|
|
977
|
+
const command = config?.commands?.find(command => {
|
|
978
|
+
return command.entity === target;
|
|
979
|
+
});
|
|
980
|
+
// Run the command
|
|
981
|
+
if (command) {
|
|
982
|
+
return devProcessCommand(command, message, callback);
|
|
983
|
+
}
|
|
951
984
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
985
|
+
// Otherwise default to processing customer message
|
|
986
|
+
return devProcessCustomerMessage(message, callback, roleOverride);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// This is used for handling cli dev
|
|
990
|
+
devReadlineProgram = async () => {
|
|
991
|
+
// Start program where use can test via command console
|
|
992
|
+
const {createInterface} = await import('node:readline');
|
|
993
|
+
const rl = createInterface({
|
|
994
|
+
input: process.stdin,
|
|
995
|
+
output: process.stdout
|
|
996
|
+
});
|
|
961
997
|
|
|
962
|
-
// Otherwise default to processing customer message
|
|
963
|
-
return processCustomerMessage(message, callback);
|
|
964
|
-
}
|
|
965
998
|
|
|
966
999
|
// Function to ask for input, perform the task, and then ask again
|
|
967
1000
|
function promptUser() {
|
|
@@ -980,16 +1013,16 @@ if (dev) {
|
|
|
980
1013
|
}
|
|
981
1014
|
|
|
982
1015
|
|
|
983
|
-
|
|
984
1016
|
console.log(grey(`\nThe following ${bold('commands')} are available...`));
|
|
985
|
-
[['context', 'logs the state context inserted into the conversation'], ['conversation', 'logs conversation details'], ['messages', 'logs all message history']].forEach(
|
|
986
|
-
|
|
987
|
-
|
|
1017
|
+
[['context', 'logs the state context inserted into the conversation'], ['conversation', 'logs conversation details'], ['messages', 'logs all message history']].forEach(
|
|
1018
|
+
([command, description]) => {
|
|
1019
|
+
console.log(`\t - ${magenta(command)} ${grey(description)}`);
|
|
1020
|
+
});
|
|
988
1021
|
|
|
989
|
-
if (config
|
|
1022
|
+
if (config?.commands?.length) {
|
|
990
1023
|
console.log(grey(`\nThe following ${bold('custom commands')} are available...`));
|
|
991
1024
|
}
|
|
992
|
-
config
|
|
1025
|
+
config?.commands?.forEach((command) => {
|
|
993
1026
|
console.log(magenta(`\t - ${command.entity}`));
|
|
994
1027
|
});
|
|
995
1028
|
|
|
@@ -1007,11 +1040,77 @@ if (dev) {
|
|
|
1007
1040
|
|
|
1008
1041
|
};
|
|
1009
1042
|
|
|
1043
|
+
// API routes for handling and receiving the message state within websocket
|
|
1044
|
+
const {WebSocketServer, WebSocket} = await import('ws');
|
|
1045
|
+
devServer = createServer();
|
|
1046
|
+
devServer.on('request', app.handler);
|
|
1047
|
+
const wss = new WebSocketServer({server: devServer});
|
|
1048
|
+
wss.on('connection', (ws) => {
|
|
1049
|
+
|
|
1050
|
+
const sendState = () => {
|
|
1051
|
+
const payload = JSON.stringify({state: devState});
|
|
1052
|
+
ws.send(payload);
|
|
1053
|
+
wss.clients.forEach((client) => {
|
|
1054
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1055
|
+
client.send(payload);
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
console.log(grey('WebSocket client connected'));
|
|
1061
|
+
sendState();
|
|
1062
|
+
|
|
1063
|
+
// Handle incoming WebSocket messages
|
|
1064
|
+
ws.on('message', async (msg) => {
|
|
1065
|
+
let parsedMessage;
|
|
1066
|
+
if (Buffer.isBuffer(msg)) {
|
|
1067
|
+
parsedMessage = JSON.parse(msg.toString());
|
|
1068
|
+
} else if (typeof msg === 'string') {
|
|
1069
|
+
parsedMessage = JSON.parse(msg);
|
|
1070
|
+
} else {
|
|
1071
|
+
console.error('Unexpected message type:', typeof msg);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const message = MessageSchema.parse(parsedMessage);
|
|
1075
|
+
console.log(`${grey('> ')} "${cyan(message.content)}"`);
|
|
1076
|
+
try {
|
|
1077
|
+
await devProgramProcessInput(message.content, () => sendState(), message.role);
|
|
1078
|
+
sendState();
|
|
1079
|
+
ws.send(JSON.stringify({message: {id: message.id}}));
|
|
1080
|
+
} catch (e) {
|
|
1081
|
+
ws.send(JSON.stringify(handleError(e)));
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
ws.on('close', () => {
|
|
1086
|
+
console.log(yellow('WebSocket client disconnected'));
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
// CRUD REST calls to manipulate state
|
|
1091
|
+
app.get('/dev', async (req, res, next) => {
|
|
1092
|
+
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
1093
|
+
res.end(JSON.stringify(devState));
|
|
1094
|
+
});
|
|
1095
|
+
app.put('/dev', async (req, res, next) => {
|
|
1096
|
+
Object.assign(devState, req.body ?? {});
|
|
1097
|
+
sendState();
|
|
1098
|
+
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
1099
|
+
res.end(JSON.stringify(devState));
|
|
1100
|
+
});
|
|
1101
|
+
app.post('/dev/reset', async (req, res, next) => {
|
|
1102
|
+
devState = devCreateState(req?.body?.persona);
|
|
1103
|
+
sendState();
|
|
1104
|
+
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
1105
|
+
res.end(JSON.stringify(devState));
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
});
|
|
1010
1109
|
|
|
1011
1110
|
}
|
|
1012
1111
|
|
|
1013
1112
|
|
|
1014
|
-
app.listen(process.env.PORT || 8080, async (err) => {
|
|
1113
|
+
(dev ? devServer : app).listen(process.env.PORT || 8080, async (err) => {
|
|
1015
1114
|
if (err) throw err;
|
|
1016
1115
|
|
|
1017
1116
|
const art_scout9 = `
|
|
@@ -1058,16 +1157,16 @@ app.listen(process.env.PORT || 8080, async (err) => {
|
|
|
1058
1157
|
|
|
1059
1158
|
if (dev && !process.env.SCOUT9_API_KEY) {
|
|
1060
1159
|
console.log(red(
|
|
1061
|
-
|
|
1160
|
+
'Missing SCOUT9_API_KEY environment variable, your PMT application may not work without it.'));
|
|
1062
1161
|
}
|
|
1063
1162
|
|
|
1064
1163
|
if (process.env.SCOUT9_API_KEY === '<insert-scout9-api-key>') {
|
|
1065
1164
|
console.log(`${red('SCOUT9_API_KEY has not been set in your .env file.')} ${grey(
|
|
1066
|
-
|
|
1165
|
+
'You can find your API key in the Scout9 dashboard.')} ${bold(cyan('https://scout9.com'))}`);
|
|
1067
1166
|
}
|
|
1068
1167
|
|
|
1069
1168
|
if (dev) {
|
|
1070
|
-
|
|
1169
|
+
devReadlineProgram();
|
|
1071
1170
|
}
|
|
1072
1171
|
|
|
1073
1172
|
});
|