@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.
- package/dist/cerebro/cerebro.d.ts +14 -0
- package/dist/cerebro/cerebro.js +87 -1
- package/dist/cerebro/effects.d.ts +2 -2
- package/dist/cerebro/effects.js +15 -3
- package/dist/envs.d.ts +2 -0
- package/dist/envs.js +3 -1
- package/dist/intents/dialogflow_es.js +1 -4
- package/dist/pilot.js +23 -6
- package/package.json +8 -8
|
@@ -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
|
}
|
package/dist/cerebro/cerebro.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
8
|
+
invokeEffects(intent: Intent, status: CerebroStatus, activateCallback: () => void): Promise<void>;
|
|
9
9
|
run(effect: Effect): Promise<void>;
|
|
10
|
-
transferEffect(
|
|
10
|
+
transferEffect(effect: Effect): Promise<void>;
|
|
11
11
|
}
|
package/dist/cerebro/effects.js
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
16
|
-
"@fonoster/googleasr": "^0.5.
|
|
17
|
-
"@fonoster/googletts": "^0.5.
|
|
18
|
-
"@fonoster/logger": "^0.5.
|
|
19
|
-
"@fonoster/secrets": "^0.5.
|
|
20
|
-
"@fonoster/voice": "^0.5.
|
|
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": "
|
|
40
|
+
"gitHead": "0f7b0ed84ded17ea6b3aa593fafc97914478f3a6"
|
|
41
41
|
}
|