Wir haben in den ersten beiden Teilen dieser Serie (Webseiten mit Alexa und Websockets navigieren und Eine Pipeline für Lambda mit AWS CodeStar erstellen ) gesehen, wie wir mit dem ASK einen Alexa Skill erstellen und mittels CodeStar eine Deployment Pipeline für unsere Lambda-Funktion erzeugen. In diesem Teil werden wir unsere Lambda so anpassen, dass sie auf unsere Intents des Skills reagiert und mit unserem Webservice kommuniziert, auf dem wir per Websockets die Navigation ermöglichen.
Alexa SDK: Einführung
Wir schauen uns zunächst einmal an, welcher Boilerplate Code in unserer index.js von CodeStar erzeugt wurde.
1'use strict';
2
3const Alexa = require('alexa-sdk');
4
5const APP_ID = undefined; // TODO replace with your app ID (OPTIONAL).
6
7const languageStrings = {
8 ...
9};
10
11const handlers = { 'LaunchRequest': function () {
12 this.emit('GetFact');
13 },
14 'GetNewFactIntent': function () {
15 this.emit('GetFact');
16 },
17 'GetFact': function () {
18 // Get a random space fact from the space facts list
19 // Use this.t() to get corresponding language data
20 const factArr = this.t('FACTS');
21 const factIndex = Math.floor(Math.random() * factArr.length);
22 const randomFact = factArr[factIndex];
23
24 // Create speech output
25 const speechOutput = this.t('GET_FACT_MESSAGE') + randomFact;
26 this.emit(':tellWithCard', speechOutput, this.t('SKILL_NAME'), randomFact);
27 },
28 'AMAZON.HelpIntent': function () {
29 const speechOutput = this.t('HELP_MESSAGE');
30 const reprompt = this.t('HELP_MESSAGE');
31 this.emit(':ask', speechOutput, reprompt);
32 },
33 'AMAZON.CancelIntent': function () {
34 this.emit(':tell', this.t('STOP_MESSAGE'));
35 },
36 'AMAZON.StopIntent': function () {
37 this.emit(':tell', this.t('STOP_MESSAGE'));
38 },
39 'SessionEndedRequest': function () {
40 this.emit(':tell', this.t('STOP_MESSAGE'));
41 },
42};
43
44exports.handler = (event, context) => { const alexa = Alexa.handler(event, context);
45 alexa.APP_ID = APP_ID;
46 // To enable string internationalization (i18n) features, set a resources object.
47 alexa.resources = languageStrings;
48 alexa.registerHandlers(handlers);
49 alexa.execute();
50};
Wir sehen hier einen fertigen Alexa Skill. Zwei Bereiche sind hier besonders wichtig. Die Definition des sog. „handlers“-Objektes und die Export-Anweisung am Ende der Datei. Zunächst zur Export-Anweisung: Wir exportieren hier die Funktion handler
. Diese wird aufgerufen, wenn unsere Lambda-Funktion getriggert wird. Sie bekommt das Event und einen Kontext als Parameter. Wir initialisieren zunächst das Alexa SDK, reichen das Event und den Kontext weiter und registrieren weiter unten die Handler mit alexa.registerHandlers(handlers)
. Die APP_ID
und die Ressourcen sind optionale Dinge, die ich in diesem Tutorial nicht weiter betrachte. Durch das Registrieren der Handler sagen wir dem SDK, welche Intents es verarbeiten soll. Mit alexa.execute()
wird die Antwort generiert und an die aufrufende Alexa zurückgesendet.
Nun zum Objekt handlers
. In diesem definieren wir die Intents, auf die wir reagieren wollen. Man sieht hier unter anderem die vorgegebenen Intents AMAZON.HelpIntent
, AMAZON.Cancellntent
und AMAZON.StopIntent
. Wofür diese sind, verrät bereits der Name und ich habe es auch im ersten Blogeintrag nochmal beschrieben. Dann finden wir noch den LaunchRequest
und den SessionEndedRequest
. Der LaunchRequest
wird immer dann aufgerufen, wenn unser Skill ohne zusätzliche Anweisung gestartet wird. Also wenn jemand sagen würde: „Alexa, starte Webseiten-Navigator“. Der SessionEndedRequest
wird aufgerufen, wenn der Nutzer über 7 Sekunden lang nicht mit dem Skill interagiert.
Dann sehen wir noch den GetNewFactIntent
und die Funktion GetFact
, welche vom GetNewFactIntent
aufgerufen wird. Das Alexa SDK kümmert sich darum, dass die passenden Funktionen aufgerufen werden, je nachdem, was wir in unserem Intent-Schema im Alexa Skills Kit definiert haben und was der Nutzer zur Alexa sagt. Wenn der Nutzer den Skill startet, wird die LaunchRequest
-Funktion ausgeführt. Wenn er „Stop“ sagt, wird der AMAZON.StopIntent
ausgeführt. Und wenn der Nutzer eine unserer Utterances des NavigateToPage
Intents sagt, dann wird unsere Funktion für NavigateToPage
aufgerufen. Bisher gibt es die Funktion nicht, was wir aber gleich nachholen. Der Rückgabewert aller dieser Funktionen ist jeweils die Antwort für den Nutzer.
Das Alexa SDK stellt uns mehrere Möglichkeiten bereit, eine Antwort zu erzeugen. Mittels this.emit(‘:ask’, ...)
und this.emit(‘:tell’, ...)
können wir direkt Antworten ausgeben, wobei der Unterschied ist, dass die „:ask“-Anweisung die Session offen hält und somit auf eine Antwort des Nutzers wartet, während „:tell“ nur einen Text ausgibt und dann die Konversation und damit den Skill beendet. Die sehr gute Dokumentation des Alexa SDKs findet man hier: https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs
Den Fact Skill Boilerplate Code aufräumen
Wir räumen in der index.js zunächst einmal auf und lassen nur das übrig, was wir für den weiteren Verlauf benötigen. Die bereinigte Funktion sieht dann so aus:
1'use strict';
2
3const Alexa = require('alexa-sdk');
4
5const handlers = {
6 'LaunchRequest': function () {
7 this.emit(':ask', "Wohin möchtest du navigieren?", "Wohin möchtest du navigieren, sage Home oder Impressum.");
8 },
9 'NavigateToPage': function () {
10
11 },
12 'AMAZON.HelpIntent': function () {
13 this.emit(':ask', "Wohin möchtest du navigieren?", "Wohin möchtest du navigieren, sage Home oder Impressum.");
14 },
15 'AMAZON.CancelIntent': function () {
16 this.emit(':tell', "Ok, bye");
17 },
18 'AMAZON.StopIntent': function () {
19 this.emit(':tell', "Ok, bye");
20 },
21 'SessionEndedRequest': function () {
22 this.emit(':tell', "Ok, bye");
23 },
24};
25
26exports.handler = (event, context) => {
27 const alexa = Alexa.handler(event, context);
28 alexa.registerHandlers(handlers);
29 alexa.execute();
30};
Der LaunchRequest
fragt den Nutzer nun, wohin er navigieren möchte und auch die anderen Intents haben eine sinnvolle Antwort. Die beiden Funktionen GetNewFactIntent
und GetFact
habe ich gelöscht. Wir haben in unserem Skill den NavigateToPage Intent definiert, deswegen habe ich dafür die Funktion NavigateToPage
angelegt. Hier wird nun unsere Magie stattfinden, mit der wir die Webseite fernsteuern.
Slot für den PAGE_NAME auslesen
Als nächstes wollen wir wissen, zu welcher Page der Nutzer navigieren möchte.
Wir haben den Slot PAGE_NAME im Alexa Skills Kit definiert. Dieser lässt sich sehr leicht mit dem Alexa SDK abrufen. Wir ergänzen folgende Zeilen in der NavigateToPage Funktion:
1'use strict';
2
3const Alexa = require('alexa-sdk');
4
5const pageNameToFile = { "home": "index.html", "impressum": "impressum.html"};
6const handlers = {
7 'LaunchRequest': function () {
8 ...
9 },
10 'NavigateToPage': function () { const pageName = this.event.request.intent.slots.PAGE_NAME.value; const file = pageNameToFile[pageName.toLowerCase()]; }, ...
11};
12
13...
Mit this.event.request.intent.slots.PAGE_NAME.value
kommen wir an den Slot-Wert heran, den der Nutzer genannt hat. Dieser kann entweder „Home“ oder „Impressum“ sein. In dem Objekt pageNameToFile
habe ich diese Seiten auf ihre entsprechenden HTML-Seiten gemappt.
Axios für die Kommunikation zum Webserver einbinden
Um einen Request zu unserem Webserver absetzen zu können, installieren wir uns das npm-Modul axios (https://www.npmjs.com/package/axios ) mittels npm i -S axios
. Durch const axios = require('axios');
importieren wir das Modul auch gleich in unserer index.js.
Den Request zum Webserver absetzen
1'use strict';
2
3const Alexa = require('alexa-sdk');
4const axios = require('axios');
5const pageNameToFile = {
6 "home": "index.html",
7 "impressum": "impressum.html"
8};
9
10const handlers = {
11 'LaunchRequest': function () {
12 ...
13 },
14 'NavigateToPage': function () {
15 const pageName = this.event.request.intent.slots.PAGE_NAME.value;
16 const file = pageNameToFile[pageName.toLowerCase()];
17
18 axios.get('https://alexa-navigate.now.sh/navigate?file=' + file).then(() => { this.emit(':ask', "Ok, ich navigiere. Was kann ich noch tun?", "Was kann ich noch tun?"); }); },
19 ...
20};
21
22...
Ich lese den Wert für die Page aus der Map pageNameToFile
aus und übergebe ihn anschließend als Parameter mit dem Namen „file“ an den GET-Request zu meinem Webserver. Wenn der Request erfolgreich abgeschlossen ist, soll Alexa noch fragen, was sie als nächstes tun soll.
Das ist auch schon die ganze Magie, die innerhalb der Lambda passiert. Wir triggern mit dem GET-Request unseren Webserver, der unter dem Pfad „/navigate“ erreichbar ist und darauf wartet, dass ihm eine Seite übergeben wird. Den Code pushe ich nun in mein CodeCommit Repository und die Pipeline sorgt dafür, dass meine Lambda aktualisiert wird.
Schauen wir uns nun noch an, wie der Webserver aufgebaut ist:
Webserver erstellen und Websockets konfigurieren
Der Webserver ist eine einfache express app (https://www.npmjs.com/package/express ). Sie hat folgende Verzeichnisstruktur:
Der Server wird in der server.js definiert. Die einzelnen Seiten der Page sind index.html und impressum.html. Jede dieser beiden HTML-Dateien bindet die client.js ein.
Die HTML-Dateien sind sehr simpel und sehen wie folgt aus:
1<!doctype html>
2<html>
3<head>
4 <meta charset="utf-8">
5 <title></title>
6</head>
7<body>
8<h1>Home</h1>
9<script src="js/client.js" async defer></script>
10</body>
11</html>
Die server.js sieht wie folgt aus:
1const express = require('express');
2const WebSocket = require('ws');
3const http = require('http');
4const path = require("path");
5
6const app = express();
7
8const server = http.createServer(app);
9const wss = new WebSocket.Server({ server });
10
11let myWebsocket = null;
12
13wss.on('connection', function connection(ws, req) {
14 myWebsocket = ws;
15 console.log("user connected");
16});
17
18app.use("js", express.static("js"));
19
20// e.g. mywebsite.com/navigate?file=impressum.html
21app.use('/navigate', function (req, res) { if (myWebsocket && req.query.file) { myWebsocket.send(JSON.stringify({href: req.query.file})); } res.send('OK');});
22app.use('/', function (req, res) {
23 res.sendFile(path.join(__dirname, req.path));
24});
25
26server.listen(8080, function listening() {
27 console.log('Listening on %d', server.address().port);
28});
Ich benutze das npm-Modul „ws“ (https://www.npmjs.com/package/ws ), um eine Websocket-Verbindung zwischen Client und Server aufzubauen. Die eigentliche Funktionalität des Servers liegt in der app.use('/navigate')
Funktion. Hier sende ich an meinen Websocket-Endpunkt auf dem Client ein JSON-Objekt, das ein einziges Property „href“ besitzt. Diesem Property gebe ich den Wert, den ich aus dem Request auslese (req.query.file
). Der restliche Code in der server.js ist typischer express.js Boilerplate Code.
Die client.js hat auch nur ein paar Zeilen Code:
1const mySocket = new WebSocket("wss://alexa-navigate.now.sh");
2
3mySocket.onopen = function (event) {
4 console.log("connected to backend");
5};
6
7mySocket.onmessage = function (event) { let data = JSON.parse(event.data); window.location.href = data.href;};
8mySocket.onerror = function (event) {
9 console.log(event);
10};
Hier wird nur eine neue Websocket-Verbindung zu unserem Webserver unter wss://alexa-navigate.now.sh aufgebaut. Mit mySocket.onmessage()
reagieren wir auf eine Nachricht, die vom Server kommt. Unter event.data
erhalten wir das JSON-Objekt mit dem „href“-Property, das wir vom Server gesendet haben. Die eigentliche Navigation der Page passiert dann mittels der Zeile window.location.href = data.href
. So leicht kann das sein 😉
Webserver deployen mit now
Um meinen Webserver zu deployen, führe ich einfach den Befehl > now
in meinem Terminal aus und die Node.js App wird automatisch in meinem kostenfreien Webspace deployt (https://zeit.co/now ). Damit meine Seite auch eine feste Subdomain hat, muss ich anschließend auch noch > now alias https://[generated-id].now.sh alexa-navigate
ausführen.
Nächste Schritte
Um unsere Anwendung produktiv einsetzen zu können, müsste man natürlich noch ein paar Sachen beachten, die aber den Umfang des Artikels sprengen würden. Z. B. würde die Webseite aktuell für alle Nutzer navigiert werden, die sich gleichzeitig auf der Webseite befinden. Denn der Server sendet an alle Websocket-Clients gleichzeitig die Message. Wir wollen ja eigentlich, dass nur der User, der seine Alexa steuert, auch durch unsere Seite navigiert wird. Dazu müssten wir den Nutzer identifizieren. Das geht einmal über den Alexa Skill, indem wir in unserem Skill das sog. Account Linking integrieren (https://developer.amazon.com/de/docs/custom-skills/link-an-alexa-user-with-a-user-in-your-system.html ) und außerdem müsste sich der Nutzer auch zuerst auf unserer Webseite einloggen, damit wir den Account der Alexa mit dem Benutzerkonto auf der Webseite zuordnen können. Anschließend könnten wir nur dem Client des Benutzers eine Websocket Message senden.
Zusammenfassung
In dem Tutorial haben wir gesehen, wie leicht es ist, einen Alexa Skill mit dem Alexa Skills Kit Web-Interface zu definieren und dass mit CodeStar auch sehr leicht die passende Lambda inklusive der Deployment Pipeline erzeugt werden kann.
Innerhalb der Lambda bietet uns das alexa-sdk eine komfortable Möglichkeit, die Antworten für unseren Skill zu definieren. Mit dem npm-Modul ws haben wir im Handumdrehen die Websockets zwischen Client und Server erstellt und mussten nur noch mit ein paar Zeilen JavaScript dafür sorgen, dass unsere Page navigiert, sobald ein bestimmter Endpoint auf unserem Webserver von der Lambda getriggert wird.
Wenn du noch Fragen oder Anregungen zu dieser Lösung hast, dann freue ich mich über Feedback in den Kommentaren.
Weitere Beiträge
von René Bohrenfeldt
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
René Bohrenfeldt
Agile Coach | People Lead
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.