@fonoster/autopilot 0.5.0 → 0.5.2

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.
@@ -11,16 +11,32 @@ export declare class Cerebro {
11
11
  voiceRequest: VoiceRequest;
12
12
  status: CerebroStatus;
13
13
  activationTimeout: number;
14
+ maxIteractionsBeforeHangup: number;
15
+ failedInteractions: number;
14
16
  activeTimer: NodeJS.Timer;
17
+ interactionTimer: NodeJS.Timer;
15
18
  intentsEngine: IntentsEngine;
16
19
  stream: SGatherStream;
17
20
  config: CerebroConfig;
18
21
  lastIntent: any;
19
22
  effects: EffectsManager;
23
+ interactionsTimer: NodeJS.Timeout;
24
+ isCallHandover: boolean;
25
+ isDead: boolean;
20
26
  constructor(config: CerebroConfig);
21
27
  wake(): Promise<void>;
22
28
  sleep(): Promise<void>;
23
29
  startActiveTimer(): void;
24
30
  resetActiveTimer(): void;
31
+ /**
32
+ * Start the interactions timer
33
+ * If the user doesn't say anything we should play the welcome message
34
+ * If it gets played twice many times we should hangup
35
+ * If the user says something we should reset the timer
36
+ * If we just finish an effect we should reset the timer
37
+ */
38
+ startInteractionTimer(): void;
39
+ resetInteractionTimer(): void;
25
40
  stopPlayback(): Promise<void>;
41
+ cleanup(): Promise<void>;
26
42
  }
@@ -36,12 +36,18 @@ class Cerebro {
36
36
  voiceRequest;
37
37
  status;
38
38
  activationTimeout;
39
+ maxIteractionsBeforeHangup = 2;
40
+ failedInteractions = 0;
39
41
  activeTimer;
42
+ interactionTimer;
40
43
  intentsEngine;
41
44
  stream;
42
45
  config;
43
46
  lastIntent;
44
47
  effects;
48
+ interactionsTimer;
49
+ isCallHandover = false;
50
+ isDead = false;
45
51
  constructor(config) {
46
52
  this.voiceResponse = config.voiceResponse;
47
53
  this.voiceRequest = config.voiceRequest;
@@ -62,6 +68,10 @@ class Cerebro {
62
68
  // Subscribe to events
63
69
  async wake() {
64
70
  this.status = types_1.CerebroStatus.AWAKE_PASSIVE;
71
+ // This timer becomes active only if we don't have an activation intent
72
+ if (!this.config.activationIntentId) {
73
+ this.startInteractionTimer();
74
+ }
65
75
  this.voiceResponse.on("error", (error) => {
66
76
  this.cerebroEvents.emit("error", error);
67
77
  (0, logger_1.ulogger)({
@@ -81,16 +91,31 @@ class Cerebro {
81
91
  this.stream = await this.voiceResponse.sgather(speechConfig);
82
92
  this.stream.on("transcript", async (data) => {
83
93
  if (data.isFinal && data.transcript) {
94
+ logger.verbose("clear interactions timer", {
95
+ sessionId: this.voiceRequest.sessionId
96
+ });
97
+ clearTimeout(this.interactionsTimer);
84
98
  const intent = await this.intentsEngine.findIntent(data.transcript, {
85
99
  telephony: {
86
100
  caller_id: this.voiceRequest.callerNumber
87
101
  }
88
102
  });
89
103
  logger.verbose("cerebro received new transcription from user", {
104
+ sessionId: this.voiceRequest.sessionId,
90
105
  text: data.transcript,
91
106
  ref: intent.ref,
92
107
  confidence: intent.confidence
93
108
  });
109
+ if (intent.effects.find((e) => e.type === "hangup") ||
110
+ intent.effects.find((e) => e.type === "transfer")) {
111
+ logger.verbose("call hand over: stop all the timers and close the stream", {
112
+ sessionId: this.voiceRequest.sessionId
113
+ });
114
+ clearTimeout(this.activeTimer);
115
+ clearTimeout(this.interactionsTimer);
116
+ this.voiceResponse.closeMediaPipe();
117
+ this.isCallHandover = true;
118
+ }
94
119
  await this.effects.invokeEffects(intent, this.status, async () => {
95
120
  await this.stopPlayback();
96
121
  if (this.config.activationIntentId === intent.ref) {
@@ -104,8 +129,32 @@ class Cerebro {
104
129
  this.startActiveTimer();
105
130
  }
106
131
  }
132
+ }, () => {
133
+ logger.verbose("invokeEffects cleanup callback", {
134
+ sessionId: this.voiceRequest.sessionId
135
+ });
136
+ this.cleanup();
137
+ });
138
+ if (this.isDead || this.isCallHandover) {
139
+ try {
140
+ logger.verbose("the call was handover or cerebro was cleaned up", {
141
+ sessionId: this.voiceRequest.sessionId
142
+ });
143
+ this.voiceResponse.hangup();
144
+ }
145
+ catch (e) {
146
+ // All we can do is try as the call may have already been hung up
147
+ }
148
+ return;
149
+ }
150
+ logger.verbose("cerebro finished processing intent effects", {
151
+ sessionId: this.voiceRequest.sessionId
107
152
  });
108
- // Need to save this to avoid duplicate intents
153
+ // Reset the interactions timer
154
+ if (!this.config.activationIntentId) {
155
+ this.resetInteractionTimer();
156
+ }
157
+ // WARNING: It doesn't appear that we are using this anywhere
109
158
  this.lastIntent = intent;
110
159
  }
111
160
  });
@@ -133,6 +182,57 @@ class Cerebro {
133
182
  clearTimeout(this.activeTimer);
134
183
  this.startActiveTimer();
135
184
  }
185
+ /**
186
+ * Start the interactions timer
187
+ * If the user doesn't say anything we should play the welcome message
188
+ * If it gets played twice many times we should hangup
189
+ * If the user says something we should reset the timer
190
+ * If we just finish an effect we should reset the timer
191
+ */
192
+ startInteractionTimer() {
193
+ logger.verbose("cerebro is starting interactions timer", {
194
+ sessionId: this.voiceRequest.sessionId
195
+ });
196
+ this.interactionsTimer = setInterval(async () => {
197
+ this.failedInteractions++;
198
+ logger.verbose("cerebro is counting interactions", {
199
+ sessionId: this.voiceRequest.sessionId,
200
+ failedInteractions: this.failedInteractions
201
+ });
202
+ let intentId = "welcome";
203
+ if (this.failedInteractions >= this.maxIteractionsBeforeHangup) {
204
+ logger.verbose("there was no interaction so for a long time so we hangup", {
205
+ sessionId: this.voiceRequest.sessionId
206
+ });
207
+ intentId = "goodbye";
208
+ clearTimeout(this.interactionsTimer);
209
+ }
210
+ const intent = await this.intentsEngine.findIntentWithEvent(intentId, {
211
+ telephony: {
212
+ caller_id: this.voiceRequest.callerNumber
213
+ }
214
+ });
215
+ await this.effects.invokeEffects(intent, this.status, () => {
216
+ logger.verbose("invokeEffects callback", {
217
+ sessionId: this.voiceRequest.sessionId
218
+ });
219
+ }, () => {
220
+ logger.verbose("invokeEffects cleanup callback", {
221
+ sessionId: this.voiceRequest.sessionId
222
+ });
223
+ this.cleanup();
224
+ });
225
+ }, this.activationTimeout);
226
+ }
227
+ resetInteractionTimer() {
228
+ logger.verbose("cerebro is reseting interactions timer", {
229
+ sessionId: this.voiceRequest.sessionId
230
+ });
231
+ // Reset the failed interactions timer
232
+ this.failedInteractions = 0;
233
+ clearTimeout(this.interactionsTimer);
234
+ this.startInteractionTimer();
235
+ }
136
236
  async stopPlayback() {
137
237
  const { playbackId } = this.config.voiceConfig;
138
238
  if (playbackId) {
@@ -151,5 +251,14 @@ class Cerebro {
151
251
  }
152
252
  }
153
253
  }
254
+ // Cleanup all timers and events
255
+ async cleanup() {
256
+ this.isDead = true;
257
+ await this.voiceResponse.closeMediaPipe();
258
+ this.stream.close();
259
+ this.cerebroEvents.removeAllListeners();
260
+ clearTimeout(this.activeTimer);
261
+ clearTimeout(this.interactionsTimer);
262
+ }
154
263
  }
155
264
  exports.Cerebro = Cerebro;
@@ -5,7 +5,7 @@ export declare class EffectsManager {
5
5
  voice: VoiceResponse;
6
6
  config: EffectsManagerConfig;
7
7
  constructor(config: EffectsManagerConfig);
8
- invokeEffects(intent: Intent, status: CerebroStatus, activateCallback: Function): Promise<void>;
8
+ invokeEffects(intent: Intent, status: CerebroStatus, activateCallback: () => void, cleanupCallback: () => void): Promise<void>;
9
9
  run(effect: Effect): Promise<void>;
10
- transferEffect(voice: VoiceResponse, effect: Effect): Promise<void>;
10
+ transferEffect(effect: Effect): Promise<void>;
11
11
  }
@@ -15,8 +15,9 @@ class EffectsManager {
15
15
  this.voice = config.voice;
16
16
  this.config = config;
17
17
  }
18
- async invokeEffects(intent, status, activateCallback) {
18
+ async invokeEffects(intent, status, activateCallback, cleanupCallback) {
19
19
  activateCallback();
20
+ logger.verbose("intent received", { intentRef: intent.ref });
20
21
  if (this.config.activationIntentId === intent.ref) {
21
22
  logger.verbose("fired activation intent");
22
23
  return;
@@ -28,9 +29,20 @@ class EffectsManager {
28
29
  // before we can have any effects
29
30
  return;
30
31
  }
32
+ // eslint-disable-next-line no-loops/no-loops
31
33
  for (const e of intent.effects) {
32
- logger.verbose("effects running effect", { type: e.type });
33
- await this.run(e);
34
+ try {
35
+ logger.verbose("effects running", { type: e.type });
36
+ await this.run(e);
37
+ }
38
+ catch (e) {
39
+ const axiosError = e;
40
+ if (axiosError.response?.status !== 404) {
41
+ logger.error("error running effect", { error: e });
42
+ return;
43
+ }
44
+ cleanupCallback();
45
+ }
34
46
  }
35
47
  }
36
48
  async run(effect) {
@@ -46,7 +58,7 @@ class EffectsManager {
46
58
  break;
47
59
  case "transfer":
48
60
  // TODO: Add record effect
49
- await this.transferEffect(this.voice, effect);
61
+ await this.transferEffect(effect);
50
62
  break;
51
63
  case "send_data":
52
64
  // Only send if client support events
@@ -62,7 +74,7 @@ class EffectsManager {
62
74
  throw new Error(`effects received unknown effect ${effect.type}`);
63
75
  }
64
76
  }
65
- async transferEffect(voice, effect) {
77
+ async transferEffect(effect) {
66
78
  await this.voice.closeMediaPipe();
67
79
  const stream = await this.voice.dial(effect.parameters["destination"]);
68
80
  const playbackId = (0, nanoid_1.nanoid)();
@@ -73,16 +85,26 @@ class EffectsManager {
73
85
  await control.stop();
74
86
  };
75
87
  stream.on("answer", () => {
88
+ logger.verbose("call answered", {
89
+ destination: effect.parameters["destination"]
90
+ });
76
91
  moveForward();
77
92
  });
78
93
  stream.on("busy", async () => {
94
+ logger.verbose("call busy", {
95
+ destination: effect.parameters["destination"]
96
+ });
79
97
  await moveForward();
80
98
  await (0, helper_1.playBusyAndHangup)(this.voice, playbackId, this.config);
81
99
  });
82
100
  stream.on("noanswer", async () => {
101
+ logger.verbose("call no answer", {
102
+ destination: effect.parameters["destination"]
103
+ });
83
104
  await moveForward();
84
105
  await (0, helper_1.playNoAnswerAndHangup)(this.voice, playbackId, this.config);
85
106
  });
107
+ // eslint-disable-next-line no-loops/no-loops
86
108
  while (stay) {
87
109
  await (0, helper_1.playTransfering)(this.voice, playbackId, this.config);
88
110
  }
package/dist/envs.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export declare const APISERVER_AUTOPILOT_PORT: number;
2
2
  export declare const APISERVER_AUTOPILOT_DEFAULT_LANGUAGE_CODE: string;
3
+ export declare const APISERVER_AUTOPILOT_MEDIA_BUSY_MESSAGE: string;
4
+ export declare const APISERVER_AUTOPILOT_MEDIA_NOANSWER_MESSAGE: string;
package/dist/envs.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.APISERVER_AUTOPILOT_DEFAULT_LANGUAGE_CODE = exports.APISERVER_AUTOPILOT_PORT = void 0;
3
+ exports.APISERVER_AUTOPILOT_MEDIA_NOANSWER_MESSAGE = exports.APISERVER_AUTOPILOT_MEDIA_BUSY_MESSAGE = exports.APISERVER_AUTOPILOT_DEFAULT_LANGUAGE_CODE = exports.APISERVER_AUTOPILOT_PORT = void 0;
4
4
  /*
5
5
  * Copyright (C) 2023 by Fonoster Inc (https://fonoster.com)
6
6
  * http://github.com/fonoster/fonoster
@@ -23,3 +23,5 @@ exports.APISERVER_AUTOPILOT_PORT = process.env.APISERVER_AUTOPILOT_PORT
23
23
  ? parseInt(process.env.APISERVER_AUTOPILOT_PORT)
24
24
  : 6445;
25
25
  exports.APISERVER_AUTOPILOT_DEFAULT_LANGUAGE_CODE = process.env.APISERVER_AUTOPILOT_DEFAULT_LANGUAGE_CODE || "en-US";
26
+ exports.APISERVER_AUTOPILOT_MEDIA_BUSY_MESSAGE = process.env.APISERVER_AUTOPILOT_MEDIA_BUSY_MESSAGE;
27
+ exports.APISERVER_AUTOPILOT_MEDIA_NOANSWER_MESSAGE = process.env.APISERVER_AUTOPILOT_MEDIA_NOANSWER_MESSAGE;
@@ -128,10 +128,7 @@ class DialogFlow {
128
128
  ref: responses[0].queryResult.intent.displayName || "unknown",
129
129
  effects,
130
130
  confidence: responses[0].queryResult.intentDetectionConfidence || 0,
131
- allRequiredParamsPresent: responses[0].queryResult
132
- .allRequiredParamsPresent
133
- ? true
134
- : false
131
+ allRequiredParamsPresent: responses[0].queryResult.allRequiredParamsPresent
135
132
  };
136
133
  }
137
134
  getEffects(fulfillmentMessages) {
package/dist/pilot.js CHANGED
@@ -30,11 +30,12 @@ const server_1 = require("./events/server");
30
30
  const nanoid_1 = require("nanoid");
31
31
  const engines_1 = require("./intents/engines");
32
32
  const util_1 = require("./util");
33
+ const googleasr_1 = __importDefault(require("@fonoster/googleasr"));
33
34
  const logger_1 = require("@fonoster/logger");
34
35
  const googletts_1 = __importDefault(require("@fonoster/googletts"));
35
36
  const apps_1 = __importDefault(require("@fonoster/apps"));
36
37
  const secrets_1 = __importDefault(require("@fonoster/secrets"));
37
- const googleasr_1 = __importDefault(require("@fonoster/googleasr"));
38
+ const envs_1 = require("./envs");
38
39
  const logger = (0, logger_1.getLogger)({ service: "autopilot", filePath: __filename });
39
40
  function pilot(config) {
40
41
  logger.info("starting autopilot");
@@ -59,7 +60,7 @@ function pilot(config) {
59
60
  const apps = new apps_1.default(serviceCredentials);
60
61
  const secrets = new secrets_1.default(serviceCredentials);
61
62
  const app = await apps.getApp(voiceRequest.appRef);
62
- logger.verbose(`requested app [ref: ${app.ref}]`, { app });
63
+ logger.verbose("requested app", { app, ref: app.ref });
63
64
  const ieSecret = await secrets.getSecret(app.intentsEngineConfig.secretName);
64
65
  const intentsEngine = (0, engines_1.getIntentsEngine)(app)(JSON.parse(ieSecret.secret));
65
66
  intentsEngine?.setProjectId(app.intentsEngineConfig.projectId);
@@ -88,22 +89,38 @@ function pilot(config) {
88
89
  (0, util_1.sendClientEvent)(eventsClient, {
89
90
  eventName: types_1.CLIENT_EVENTS.ANSWERED
90
91
  });
91
- if (app.initialDtmf)
92
+ if (app.initialDtmf) {
92
93
  await voiceResponse.dtmf({ dtmf: app.initialDtmf });
94
+ }
93
95
  if (app.intentsEngineConfig.welcomeIntentId &&
94
96
  intentsEngine.findIntentWithEvent) {
95
- const response = await intentsEngine.findIntentWithEvent(app.intentsEngineConfig.welcomeIntentId, {
97
+ const response = await intentsEngine.findIntentWithEvent(
98
+ // TODO: This should be renamed to welcomeEventId
99
+ app.intentsEngineConfig.welcomeIntentId, {
96
100
  telephony: {
97
101
  caller_id: voiceRequest.callerNumber
98
102
  }
99
103
  });
100
104
  if (response.effects.length > 0) {
101
- await voiceResponse.say(response.effects[0].parameters["response"], voiceConfig);
105
+ // eslint-disable-next-line no-loops/no-loops
106
+ for await (const effect of response.effects) {
107
+ if (effect.type === "say") {
108
+ await voiceResponse.say(effect.parameters["response"], voiceConfig);
109
+ }
110
+ }
102
111
  }
103
112
  else {
104
- logger.warn(`no effects found for welcome intent: trigger '${app.intentsEngineConfig.welcomeIntentId}'`);
113
+ logger.warn("no effects found for welcome event", {
114
+ eventId: app.intentsEngineConfig.welcomeIntentId
115
+ });
105
116
  }
106
117
  }
118
+ const transfer = app.transferConfig;
119
+ transfer.messageNoAnswer = transfer.messageBusy =
120
+ transfer.messageBusy || envs_1.APISERVER_AUTOPILOT_MEDIA_BUSY_MESSAGE;
121
+ transfer.messageNoAnswer =
122
+ transfer.messageNoAnswer ||
123
+ envs_1.APISERVER_AUTOPILOT_MEDIA_NOANSWER_MESSAGE;
107
124
  const cerebro = new cerebro_1.Cerebro({
108
125
  voiceRequest,
109
126
  voiceResponse,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonoster/autopilot",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "license": "MIT",
5
5
  "main": "dist/index",
6
6
  "types": "dist/index",
@@ -12,12 +12,12 @@
12
12
  "build": "tsc --build ./tsconfig.json"
13
13
  },
14
14
  "dependencies": {
15
- "@fonoster/apps": "^0.5.0",
16
- "@fonoster/googleasr": "^0.5.0",
17
- "@fonoster/googletts": "^0.5.0",
18
- "@fonoster/logger": "^0.5.0",
19
- "@fonoster/secrets": "^0.5.0",
20
- "@fonoster/voice": "^0.5.0",
15
+ "@fonoster/apps": "^0.5.1",
16
+ "@fonoster/googleasr": "^0.5.1",
17
+ "@fonoster/googletts": "^0.5.1",
18
+ "@fonoster/logger": "^0.5.1",
19
+ "@fonoster/secrets": "^0.5.1",
20
+ "@fonoster/voice": "^0.5.1",
21
21
  "@google-cloud/dialogflow": "^4.3.1",
22
22
  "@google-cloud/dialogflow-cx": "^2.13.0",
23
23
  "date-fns": "^2.29.3",
@@ -37,5 +37,5 @@
37
37
  "publishConfig": {
38
38
  "access": "public"
39
39
  },
40
- "gitHead": "f3e53085462a1200c0c5fa086561f84c7d24895f"
40
+ "gitHead": "a6726245c50fc08aec29e04f7da04207b578a305"
41
41
  }