Alexa Java Skill Tutorial

Als Vorbereitung müsst ihr sowohl einen Account bei developer.amazon.com als auch bei der AWS Konsole besitzen.

Im ersten Schritt werden wir einen Alexa Skill erstellen.

Gehe auf developer.amazon.com und logge dich ein. Gehe anschliessend in den Alexa Bereich und dort auf deine Alexa Konsole Skills.

 

Wähle „Create New Skill“ und gebt den Skillnamen ein. Ich nenne meinen Skill ItemCollector und wähle als Standardsprache Deutsch.

Screenshot der Maske zum erstellen von einem Alexa Skill
Screenshot der Maske zum erstellen von einem Alexa Skill

 

Ausserdem müsst ihr noch "Custom Skill" auswählen. 

Screenshot Auswahl Custom Skill
Screenshot Auswahl Custom Skill

 

Ihr seht nun einen Bildschirm in welchem ihr euren Skill konfigurieren und kompilieren werdet.

Screenshot Skill konfigurieren und kompilieren
Screenshot Skill konfigurieren und kompilieren

 

Wir halten uns zuerst im Bereich Interaction Model auf.

Dort gibt es folgende Bereiche:

  • Invocation

  • Intents

  • Slot Types

  • JSON Editor


Invocation

 Invocation ist der Bereich in welchem Ihr festlegt, wie eurer Skill aufgerufen werden soll. Hier ist die Namensgebung extrem wichtig. Mit diesem Namen startet ihr später jederzeit euren Skill. Aus Erfahrung sage ich, das bei einem deutschen Skill auch der Name deutsch sein sollte. Alexa ist sehr sprachempfindlich. Ausserdem solltet ihr darauf achten, dass man den Namen klar aussprechen kann und er flüssig von der Zunge geht. Der Name kann aus mehreren Wörtern bestehen, muss er aber nicht. Ich habe den Item Collector Skill „Videothek“ genannt. 


Intents

 Intents sind die tatsächlichen eigenen Aktionen. Ich habe in meinem Fall für alle individuellen Aktionen eigene Intents erstellt. Jeder Intent hat eigene Schlüsselwörter, welche dazu dienen genau diese Aktion auszuführen. 

Screenshot von Intents mit eigenen Schlüsselwörtern
Screenshot von Intents mit eigenen Schlüsselwörtern

AMAZON.StopIntent, AMAZON.HelpIntent und AMAZON.CancelIntent sind standard Intents, welche automatisch beim Erstellen hinzugefügt werden.

 

Die restlichen fünf sind individuelle Aktionen, welche wir im Laufe dieses Tutorials besprechen. 


SlotTypes

Slot Typen sind eigene Datentypen welche man festlegen kann. Ich verwende in diesem Skill 3 Slot Typen. Zwei eigene, und einen von Amazon bereitgestellten Slot. 

Screenshot über die Skill 3 Slot Typen
Screenshot über die Skill 3 Slot Typen

 

Die eigenen sind itemType - Liste von Items welche ausgeliehen werden können und filmList welche die Titel von Filmen enthält. Hier kommt es zu einem wichtigen Punkt bei Alexa Skills. Zwar erkennt Alexa Sprache, jedoch ist diese je nach Einsatzzweck manchmal nicht gut genug um Freitext zuverlässig zu erkennen, vorallem bei deutscher Sprache. Wenn ich also Filme am Titel erkennen möchte, muss ich diese vorher erfassen. Maximal sind 200000 Items zugelassen.

 

Schauen wir uns nun den Slot Type filmList genauer an.

Screenshot von dem Slot Type filmList
Screenshot von dem Slot Type filmList

 

Jeder Type hat einen Value (Titel des Films), eine ID (klarer ID könnte in einer Tabelle angewendet werden und wird, wenn ausgefüllt für den Wert gesendet) und Synonyms (Wenn befüllt, können diese ebenfalls für den Slot verwendet werden). ID und Synonym sind optional.

Für die Vornamen von Leuten welchen ich Filme verleihe habe ich den Amazon Slot AMAZON.DE_FIRST_NAME verwendet. Amazon bietet einige fertige Slot Types wie Namen, Städte, Essen usw. zur Auswahl. Eine Liste kann man einsehen, wenn man einen neuen Slot erstellt.

 Stand Heute stehen 31 Slot Typen zur Verfügung. 


J
SON Editor

Im JSON Editor sieht man den gesamten Skill im JSON Format. Dies macht jedoch kaum Sinn. Da das JSON schnell sehr gross wird macht es wenig Sinn den Skill direkt im JSON zu bearbeiten. Ich kann mir vorstellen das es durchaus sinnvoll sein kann gewisse Teile (Slot Types) mit externen Programmen zu erstellen und einzufügen. Ausserdem könnte man den Skill auch über das JSON in einem Repository vorhalten und so Änderungen sehen. Leider gibt es noch keine Anbindung dafür.


Die Intents

 Als ersten Intent schauen wir uns ADDITEM an.

Screenshot Intent Additem
Screenshot Intent Additem

 

Hier zuerst etwas Grundlegendes über Intent Aufrufe. Ein Intent wird immer wie folgt aufgerufen.

{Skillaufruf} {Intent aufruf}

In unserem Skill also zu Beispiel:

  1. Alexa, starte Videothek und füge einen Film hinzu.

  2. Alexa antwortet: Welchen Film

  3. Und ich sage den Titel des Films.

  4. Alexa Fragt mich ob es den Film {Filmtitel} hinzufügen soll und ich bestätige mit ja.

 So sieht ein Custom Intent in Alexa aus. Möchte ich einen Slot verwenden so schreibe ich diesen in {} Klammern. Um zu bestimmen welche Slots Vorkommen kann ich bei Intent Slot Slots hinzufügen. An Dieser Stelle kann ich anschliessend durch Klick auf den Slot bestimmen ob dieser Slot benötigt wird oder nicht.

Screenshot Custom Intent in Alexa
Screenshot Custom Intent in Alexa

 

Hier ist im ersten Aufruf der Titel nicht angegeben aber benötigt. Also wird mich Alexa nach dem Titel fragen und ich werde diesen sagen.

Aus Erfahrung finde ich die Variante mit den einzelnen Abfragen der Slots angenehmer als ganze Sätze welche alle Slots enthalten zu bilden. Dies birgt meistens das Risiko, das Alexa mich nicht richtig versteht. Durch die einzelne Abfrage kann ich sicher gehen, dass alle Slots richtig gefüllt sind.

Achtung! Das ein interaktiver Dialog mit Alexa stattfinden kann, muss noch etwas im Service beim Programmieren beachtet werden. Dazu komme ich später.

Allgemeines zum Aufruf von Intents

In der Dokumentation wird gut erklärt wie der Aufruf der Skills funktionieren muss.

Als weitere Intents habe ich angelegt

  1. ADDITEM – Item hinzufügen

  2. LISTITEMS – Liste aller Items

  3. LENDOUT – Ausleihen eines Items

  4. BACKITEM – Ausgeliehenes Item zurückgeben

  5. WHATISLEND – Was ist ausgeliehen

 Wenn wir den Skill vollständig erstellt haben gehen wir auf BuildModel (jede Änderung speichern) Ist das Modell erfolgreich gebuildet gehen wir auf Interfaces. Hier stehen mir verschiedene Möglichkeiten für Services von Amazon zur Verfügung. Zum Beispiel Display Interface auf welche wir hier nicht näher eingehen werden.

Endpoint

Am Endpoint definieren wir den Endpunkt mit welchen Alexa kommuniziert. Dieser kann ein Lambda Service sein aber auch ein eigener Server mit eigenem Skill. 

Screenshot vom Endpunkt mit welchen Alexa komuniziert
Screenshot vom Endpunkt mit welchen Alexa komuniziert

 

Wir wählen AWS Lambda ARN, also machen wir hier einen Break in Alexa Skill und Kopieren uns die Skill ID. Denn unsere ARNAdresse kennen wir ja noch nicht.

 

Der Lambda Service

Um eine Kommunikation zwischen Alexa und unserer Datenbank zu ermöglichen erstellen wir einen Lambda Service in unserem Fall wird es ein Lambda Service mit Anbindung an eine Dynamo DB sein.

Zuerst melden wir uns an der AWS Konsole an.Nun ist es wichtig die Region auf Irland zu ändern in anderen europäischenRegionen ist der Alexa SDK Lambda noch nicht vergfügbar. Anschliessend gehen wir auf DynamoDB und erstellen eine Tabelle mit den Keys title und type.

Screenshot von einem Lambda Service mit Anbindung an eine Dynamo DB
Screenshot von einem Lambda Service mit Anbindung an eine Dynamo DB

 

Haben wir die DynamoDB erstellt wechseln wir in den Bereich Lambda und erstellen einen neuen Lambda Skill.

Screenshot vom erstellen eines neuen Lambda Skills
Screenshot vom erstellen eines neuen Lambda Skills

 

Wir wählen Custom Skill und erstellen eine Lambda Funktion.
Wichtig ist, dass unsere gewählte IAM Rolle Access auf DynamoDB hat. Falls noch keine Rolle vorhanden ist, 
kann man diese direkt erstellen.

 Nun gelangt man in den nächsten Schritt, wo man Alexa Skill Kit als Auslöser hinzufügt.

Screenshot Alexa Skill Kit als Auslöser hinzufügen
Screenshot Alexa Skill Kit als Auslöser hinzufügen

 

Man kann dort die Skill ID angeben, welche berechtigt ist diesen Lambda Service aufzurufen. Hier fügen wir also die ARN ein, welche vorhin in die Zwischenablage kopiert wurde.

Maven Project

Ich habe meinen Skill wie folgt aufgebaut.

Ein Maven Parent Projekt.

Darunter, als Subprojekte, einen Lambda Service für Alexa, einen Lambda Service für Web und ein JAR Projekt welches nur für die Kommunikation mit der DynamoDB da ist. In diesem Tutorial werde ich nur auf den Alexa Service eingehen die DB Connection und der Webservice können jedoch auf meinem Git Account eingesehen werden. https://github.com/bulli1979/CollectorMainProject

package biz.wgc.aws;

public enum IntentType {
	ADDITEM,DELETEITEM,LENDOUT,LISTITEMS,BACKITEM,WHATISLEND;
	public IntentType findIntent(String intent) {
		for(IntentType t : IntentType.values()) {
			if(t.toString().equals(intent)) {
				return this;
			}
		}
		return null;
	}
}
											

Die Aufgerufene Klasse ist ItemCollectorSpeechHandler.java diese Startet den Service.
Hier kann ich, falls nicht schon erledigt, noch einmal die supportedApplicationIds hinterlegen.

package biz.wgc.aws;
import java.util.HashSet;
import java.util.Set;
import com.amazon.speech.speechlet.lambda.SpeechletRequestStreamHandler;
public class ItemCollectorSpeechHandler extends SpeechletRequestStreamHandler{
	private static final Set supportedApplicationIds;
	static {
		supportedApplicationIds = new HashSet();
		supportedApplicationIds.add("amzn1.ask.skill.(skill-id)");
	}
	
	public ItemCollectorSpeechHandler() {
		super(new ItemCollector(), supportedApplicationIds);
	}
}
											

Im Handler wird dann mein eigenes Object ItemHandler aufgerufen. 
Das bringt mich zur Hauptklasse der Applikation. 
ItemCollector.java
Schauen wir zuerst die Imports an. 

 

package biz.wgc.aws;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import com.amazon.speech.json.SpeechletRequestEnvelope;
import com.amazon.speech.slu.Intent;
import com.amazon.speech.slu.Slot;
import com.amazon.speech.speechlet.Directive;
import com.amazon.speech.speechlet.IntentRequest;
import com.amazon.speech.speechlet.LaunchRequest;
import com.amazon.speech.speechlet.SessionEndedRequest;
import com.amazon.speech.speechlet.SessionStartedRequest;
import com.amazon.speech.speechlet.SpeechletResponse;
import com.amazon.speech.speechlet.SpeechletV2;
import com.amazon.speech.speechlet.dialog.directives.DelegateDirective;
import com.amazon.speech.speechlet.dialog.directives.DialogIntent;
import com.amazon.speech.ui.PlainTextOutputSpeech;
import biz.wgc.aws.data.CustomerItem;
											

Ich hole mir hier aus meinem Datenbank JAR die Klasse CustomerItem und verwende sonst sehr viele Klassen von Amazon. Die Funktionen onLaunch() (wird aufgerufen wenn der Service gestartet wird), on SessionStarted und onSessionEnded können wir ignorieren. Diese müssen immer angelegt sein und können zum Beispiel als Begrüssung dienen. Die Funktion generateItem() erstellen ein CustomerItem um es in der Datenbank zu speichern. Dabei wird Lendout Date nur erstellt wenn auch ein Name mitgegeben wird. 

private CustomerItem generateItem(String type, String title, String name) {
	CustomerItem item = new CustomerItem();
item.setTitle(title);
	item.setType(type);
	item.setLendOutOn(name!=null? new Date():null);
	item.setLendOutTo(name);
	return item;
}
											

Die Funktion generateDialogResponse() generiert eine Dialog Antwort Dies ist wichtig um einen Dialog zwischen Alexa und dem Service zu ermöglichen. Wird in bestimmten keine DialogResponse erstellt, wäre ein mehrstufiges Kommunizieren nicht möglich. 

private SpeechletResponse generateDialogResponse(SpeechletRequestEnvelope requestEnvelope) {
	DialogIntent dialogIntent = new DialogIntent(requestEnvelope.getRequest().getIntent());
	DelegateDirective dd = new DelegateDirective();
	dd.setUpdatedIntent(dialogIntent);
	List directiveList = new ArrayList<>();
	directiveList.add(dd);
	SpeechletResponse speechletResp = new SpeechletResponse();
	speechletResp.setDirectives(directiveList);
	speechletResp.setNullableShouldEndSession(false);
	return speechletResp;
}
											

Die Funktion generateLendString erstellt einen String welcher User was ausgeliehen worden ist. 

private String generateLendString(String name) {
	List itemList = DBGetService.getAllItems(null);
	StringBuilder lend = new StringBuilder(name + " hat");
	StringBuilder items = new StringBuilder("");
	itemList.forEach(ci ->{
		if(ci.getLendOutTo() != null 
&& ci.getLendOutTo().toLowerCase().equals(name.toLowerCase())) {
			if(items.length()>0) {
				items.append(", ");
			}
			items.append(ci.getType() + " " + ci.getTitle());
		}
	});
	if(items.length()>0) {
		lend.append(" folgendes ausgeliehen " + items.toString());
	}else {
		lend.append(" nichts ausgeliehen");
	}
	return lend.toString();
}
											

Aus der Datenbank wird eine Liste aller Items ausgeliehen. Dieser Liste wird nun iteriert und wenn der name den namen des ausgeliehen an entspricht wird der Film als ausgeliehen hinzugefügt. Als Ergebnis gibt sie einen String was ausgeliehen worden ist oder einen String das nichts ausgeliehen worden ist zurück. Kommen wir nun zur Hauptfunktion onIntent() Diese Funktion wird immer ausgeführt wenn der Lambda Service im Request einen Intent findet. Zurückgegeben wird immer eine SpeechletResponse

@Override
public SpeechletResponse onIntent(SpeechletRequestEnvelope
requestEnvelope) {
 PlainTextOutputSpeech speech = new PlainTextOutputSpeech();
 try {
 Intent intent = requestEnvelope.getRequest().getIntent();
 IntentType intentType = IntentType.valueOf(intent.getName());
 if(intentType == null) {
 return generateDialogResponse(requestEnvelope);
 }
 Slot type = intent.getSlot("Type");
 Slot title = intent.getSlot("Title");
 Slot name = intent.getSlot("Name");
 switch(intentType) {
…
 }
 }catch(Exception e) {
 e.printStackTrace();
 speech.setText("Entschuldigung es ist ein Fehler aufgetreten");
 }
 return SpeechletResponse.newTellResponse(speech);
}
											

Schauen wir uns zuerst den Funktionsaufbaue an. 

 

Zuerst wird der IntentType anhand des Namens ermittelt. Wenn kein Intent gefunden wird ein dialogResponse ausgelösst. Dies kann aber nur passieren wenn ich einen Intent im Skill anlege aber im Service nicht angelegt habe. 
Als nächstes werden alle Möglichen Slottypen ausgelesen. Wir haben 3 Slots angelegt also könne hier auch maximal 3 Slots ankommen. Als Response wird ein PlainTextOutputSpeech erstellt diese wandelt Text zu Sprache um. In der switch Anweisung wird dieser dann je nach Skill gefüllt. 

Dies ist auch der Grund warum ich den Type in ein Enum ausgelagert habe, so ist die Verarbeitung und Darstellung um einiges angenehmer. 

In der Switch Anweisung wird je nach IntentType anderer Code ausgeführt. Bestimmte Intents benötigen bestimmte Slots und so wird immer wenn nicht alle nötigen Slots vorhanden sind eine Dialogresponse gesendet. Wenn nicht wird der Code in der DB ausgeführt und eine Repsonsetext gebildet. Der Aufbau ist immer der Selbe und klar ersichtlich. Mit der Implementierung aller Schritte sind wir schon mit der Erstellung des Services fertig. 

switch(intentType) {
case ADDITEM :
if (type == null || title == null || type.getValue() == null ||
title.getValue() == null) {
 return generateDialogResponse(requestEnvelope);
 }
DBSaveService.save(generateItem(title.getValue(),type.getValue(),null));
 speech.setText("Dein " + type.getValue() + " " +
title.getValue() + " wurde hinzugefügt");
 break;
 case DELETEITEM:
 DBDeleteService.deleteItem(null);
 break;
 case LENDOUT:
 if (type == null || title == null || name == null ||
type.getValue() == null || title.getValue() == null || name.getValue() == null)
{
 return generateDialogResponse(requestEnvelope);
 }
 DBSaveService.lendItem(generateItem(type.getValue(),
title.getValue(), name.getValue()));
 speech.setText("Dein " + type.getValue() + " " +
title.getValue() + " wurde an " + name.getValue() + " verliehen");
 break;
 case BACKITEM :
 if (type == null || title == null || type.getValue() == null ||
title.getValue() == null) {
 return generateDialogResponse(requestEnvelope);
 }
 DBSaveService.lendItem(generateItem(type.getValue(),
title.getValue(), null));
 speech.setText("Dein " + type.getValue() + " " +
title.getValue() + " wurde zurückgegeben");
 break;
 case WHATISLEND :
 if (name == null || name.getValue() == null) {
 return generateDialogResponse(requestEnvelope);
 }
 speech.setText(generateLendString(name.getValue()));
 break;
 case LISTITEMS:
 List itemList =
DBGetService.getAllItems(type.getValue());
 StringBuilder b = new StringBuilder();
 itemList.forEach(item ->{b.append(item.getTitle()+", ");});
 speech.setText("Deine Filme " + b.toString());
 default:
 }
											

Nun fehlt noch die Einstellungen das dieses Projekt sauber gebuildet wird. Dafür verwende ich das Maven Shade Plugin dieses sorgt dafür das alle verwendeten Jars mit in das JAR gepackt werden. Die erstellte Maven Datei sieht dann wie folgt aus.

 

pom.xml

Beim Maven Run As Maven Install wird nun ein Shaded Jar erstellt welches ich als Jar für meinenAWS Lambda Service uploade.

Screenshot Erstellung Shaded Jar
Screenshot Erstellung Shaded Jar

Damit ist der Skill fertig erstellt.

Ich kopiere nun die ARN Adresse meines Lambda Services und füge sie als Endpoint in meinen Alexa Service ein.

Anschliessend kann ich meinen Skill direkt in der developer Konsole testen.

Screenshot Skill in developer Konsole testen
Screenshot Skill in developer Konsole testen

Durch klicken aktiviere ich das Mikrofon und mein Komando wird auf Alexa ausgeführt. Ich sehe sowohl was von Alexa gesendet wird, als auch was von dem Lambda Service zurück kommt.

Auch kann ich so ausprobieren ob mein gewählter Satzbau funktioniert.

Das vollständige Alexa JSON ist im parent Projekt unter resources zu finden. 

Kommentar einfügen: