@fonoster/autopilot 0.5.0 → 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.
@@ -11,16 +11,30 @@ 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;
20
25
  constructor(config: CerebroConfig);
21
26
  wake(): Promise<void>;
22
27
  sleep(): Promise<void>;
23
28
  startActiveTimer(): void;
24
29
  resetActiveTimer(): void;
30
+ /**
31
+ * Start the interactions timer
32
+ * If the user doesn't say anything we should play the welcome message
33
+ * If it gets played twice many times we should hangup
34
+ * If the user says something we should reset the timer
35
+ * If we just finish an effect we should reset the timer
36
+ */
37
+ startInteractionTimer(): void;
38
+ resetInteractionTimer(): void;
25
39
  stopPlayback(): Promise<void>;
26
40
  }
@@ -36,12 +36,17 @@ 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;
45
50
  constructor(config) {
46
51
  this.voiceResponse = config.voiceResponse;
47
52
  this.voiceRequest = config.voiceRequest;
@@ -62,6 +67,10 @@ class Cerebro {
62
67
  // Subscribe to events
63
68
  async wake() {
64
69
  this.status = types_1.CerebroStatus.AWAKE_PASSIVE;
70
+ // This timer becomes active only if we don't have an activation intent
71
+ if (!this.config.activationIntentId) {
72
+ this.startInteractionTimer();
73
+ }
65
74
  this.voiceResponse.on("error", (error) => {
66
75
  this.cerebroEvents.emit("error", error);
67
76
  (0, logger_1.ulogger)({
@@ -81,16 +90,31 @@ class Cerebro {
81
90
  this.stream = await this.voiceResponse.sgather(speechConfig);
82
91
  this.stream.on("transcript", async (data) => {
83
92
  if (data.isFinal && data.transcript) {
93
+ logger.verbose("clear interactions timer", {
94
+ sessionId: this.voiceRequest.sessionId
95
+ });
96
+ clearTimeout(this.interactionsTimer);
84
97
  const intent = await this.intentsEngine.findIntent(data.transcript, {
85
98
  telephony: {
86
99
  caller_id: this.voiceRequest.callerNumber
87
100
  }
88
101
  });
89
102
  logger.verbose("cerebro received new transcription from user", {
103
+ sessionId: this.voiceRequest.sessionId,
90
104
  text: data.transcript,
91
105
  ref: intent.ref,
92
106
  confidence: intent.confidence
93
107
  });
108
+ if (intent.effects.find((e) => e.type === "hangup") ||
109
+ intent.effects.find((e) => e.type === "transfer")) {
110
+ logger.verbose("call hand over: stop all the timers and close the stream", {
111
+ sessionId: this.voiceRequest.sessionId
112
+ });
113
+ clearTimeout(this.activeTimer);
114
+ clearTimeout(this.interactionsTimer);
115
+ this.voiceResponse.closeMediaPipe();
116
+ this.isCallHandover = true;
117
+ }
94
118
  await this.effects.invokeEffects(intent, this.status, async () => {
95
119
  await this.stopPlayback();
96
120
  if (this.config.activationIntentId === intent.ref) {
@@ -105,7 +129,23 @@ class Cerebro {
105
129
  }
106
130
  }
107
131
  });
108
- // Need to save this to avoid duplicate intents
132
+ logger.verbose("cerebro finished processing intent effects", {
133
+ sessionId: this.voiceRequest.sessionId
134
+ });
135
+ if (this.isCallHandover) {
136
+ try {
137
+ this.voiceResponse.hangup();
138
+ }
139
+ catch (e) {
140
+ // All we can do is try as the call may have already been hung up
141
+ }
142
+ return;
143
+ }
144
+ // Reset the interactions timer
145
+ if (!this.config.activationIntentId) {
146
+ this.resetInteractionTimer();
147
+ }
148
+ // WARNING: It doesn't appear that we are using this anywhere
109
149
  this.lastIntent = intent;
110
150
  }
111
151
  });
@@ -133,6 +173,52 @@ class Cerebro {
133
173
  clearTimeout(this.activeTimer);
134
174
  this.startActiveTimer();
135
175
  }
176
+ /**
177
+ * Start the interactions timer
178
+ * If the user doesn't say anything we should play the welcome message
179
+ * If it gets played twice many times we should hangup
180
+ * If the user says something we should reset the timer
181
+ * If we just finish an effect we should reset the timer
182
+ */
183
+ startInteractionTimer() {
184
+ logger.verbose("cerebro is starting interactions timer", {
185
+ sessionId: this.voiceRequest.sessionId
186
+ });
187
+ this.interactionsTimer = setInterval(async () => {
188
+ this.failedInteractions++;
189
+ logger.verbose("cerebro is counting interactions", {
190
+ sessionId: this.voiceRequest.sessionId,
191
+ failedInteractions: this.failedInteractions
192
+ });
193
+ let intentId = "welcome";
194
+ if (this.failedInteractions >= this.maxIteractionsBeforeHangup) {
195
+ logger.verbose("there was no interaction so for a long time so we hangup", {
196
+ sessionId: this.voiceRequest.sessionId
197
+ });
198
+ intentId = "goodbye";
199
+ clearTimeout(this.interactionsTimer);
200
+ }
201
+ const intent = await this.intentsEngine.findIntentWithEvent(intentId, {
202
+ telephony: {
203
+ caller_id: this.voiceRequest.callerNumber
204
+ }
205
+ });
206
+ await this.effects.invokeEffects(intent, this.status, () => {
207
+ logger.verbose("invokeEffects callback", {
208
+ sessionId: this.voiceRequest.sessionId
209
+ });
210
+ });
211
+ }, this.activationTimeout);
212
+ }
213
+ resetInteractionTimer() {
214
+ logger.verbose("cerebro is reseting interactions timer", {
215
+ sessionId: this.voiceRequest.sessionId
216
+ });
217
+ // Reset the failed interactions timer
218
+ this.failedInteractions = 0;
219
+ clearTimeout(this.interactionsTimer);
220
+ this.startInteractionTimer();
221
+ }
136
222
  async stopPlayback() {
137
223
  const { playbackId } = this.config.voiceConfig;
138
224
  if (playbackId) {
@@ -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): 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
  }
@@ -17,6 +17,7 @@ class EffectsManager {
17
17
  }
18
18
  async invokeEffects(intent, status, activateCallback) {
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,8 +29,9 @@ 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 });
34
+ logger.verbose("effects running", { type: e.type });
33
35
  await this.run(e);
34
36
  }
35
37
  }
@@ -46,7 +48,7 @@ class EffectsManager {
46
48
  break;
47
49
  case "transfer":
48
50
  // TODO: Add record effect
49
- await this.transferEffect(this.voice, effect);
51
+ await this.transferEffect(effect);
50
52
  break;
51
53
  case "send_data":
52
54
  // Only send if client support events
@@ -62,7 +64,7 @@ class EffectsManager {
62
64
  throw new Error(`effects received unknown effect ${effect.type}`);
63
65
  }
64
66
  }
65
- async transferEffect(voice, effect) {
67
+ async transferEffect(effect) {
66
68
  await this.voice.closeMediaPipe();
67
69
  const stream = await this.voice.dial(effect.parameters["destination"]);
68
70
  const playbackId = (0, nanoid_1.nanoid)();
@@ -73,16 +75,26 @@ class EffectsManager {
73
75
  await control.stop();
74
76
  };
75
77
  stream.on("answer", () => {
78
+ logger.verbose("call answered", {
79
+ destination: effect.parameters["destination"]
80
+ });
76
81
  moveForward();
77
82
  });
78
83
  stream.on("busy", async () => {
84
+ logger.verbose("call busy", {
85
+ destination: effect.parameters["destination"]
86
+ });
79
87
  await moveForward();
80
88
  await (0, helper_1.playBusyAndHangup)(this.voice, playbackId, this.config);
81
89
  });
82
90
  stream.on("noanswer", async () => {
91
+ logger.verbose("call no answer", {
92
+ destination: effect.parameters["destination"]
93
+ });
83
94
  await moveForward();
84
95
  await (0, helper_1.playNoAnswerAndHangup)(this.voice, playbackId, this.config);
85
96
  });
97
+ // eslint-disable-next-line no-loops/no-loops
86
98
  while (stay) {
87
99
  await (0, helper_1.playTransfering)(this.voice, playbackId, this.config);
88
100
  }
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.1",
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": "0f7b0ed84ded17ea6b3aa593fafc97914478f3a6"
41
41
  }