@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.
@@ -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(colors.grey(JSON.stringify(body, null, dev ? 2 : undefined)));
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(colors.red(`${colors.bold(`${code} Error`)}: ${message}`));
116
+ console.log(red(`${bold(`${code} Error`)}: ${message}`));
117
117
  if ('stack' in e) {
118
- console.log('STACK:', colors.grey(e.stack));
118
+ console.log('STACK:', grey(e.stack));
119
119
  }
120
120
  if (body) {
121
- console.log('INPUT:', colors.grey(JSON.stringify(body, null, dev ? 2 : undefined)));
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 = ({error, res = undefined, code = 500, status, name, bodyLabel = 'Provided Input', body = undefined, action = ''}) => {
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(colors.red(`${colors.bold(`${name}`)}:`));
151
+ console.log(red(`${bold(`${name}`)}:`));
143
152
  if (body) {
144
- console.log(colors.grey(`${bodyLabel}:`));
145
- console.log(colors.grey(JSON.stringify(body, null, dev ? 2 : undefined)));
153
+ console.log(grey(`${bodyLabel}:`));
154
+ console.log(grey(JSON.stringify(body, null, dev ? 2 : undefined)));
146
155
  }
147
- console.log(colors.red(`${action}${formattedError}`));
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
- // Root application POST endpoint will run the scout9 app
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(colors.green(`Workflow Sending Response:`));
282
- console.log(colors.grey(JSON.stringify(formattedResponse, null, 2)));
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(colors.grey(JSON.stringify(data)));
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(body);
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(`${colors.grey(`${colors.cyan('>')} Testing ${colors.bold(colors.white(testableEntities.length))} Entities...`)}`);
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${colors.green(`+ ${testableEntities.length} Entities passed`)}`);
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
- if (typeof message !== 'string') {
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
- console.log(`${colors.grey(`${colors.cyan('>')} Forwarding...`)}`);
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
- if (typeof messages !== 'object' || !Array.isArray(messages)) {
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(colors.bold(colors.green(art_scout9)));
669
- console.log(colors.bold(colors.cyan(art_pmt)));
670
- console.log(`${colors.grey(`${colors.cyan('>')} Running ${colors.bold(colors.white('Scout9'))}`)} ${colors.bold(
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 auto-reply app on ${fullUrl}`);
1074
+ console.log(`Running Scout9 app on ${fullUrl}`);
674
1075
  }
675
1076
  // Run checks
676
1077
  if (!fs.existsSync(configFilePath)) {
677
- console.log(colors.red('Missing .env file, your auto reply application may not work without it.'));
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(colors.red(
682
- 'Missing SCOUT9_API_KEY environment variable, your auto reply application may not work without it.'));
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(`${colors.red('SCOUT9_API_KEY has not been set in your .env file.')} ${colors.grey(
687
- 'You can find your API key in the Scout9 dashboard.')} ${colors.bold(colors.cyan('https://scout9.com'))}`);
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
  });