Initial commit
This commit is contained in:
commit
15a6bcb07f
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Program",
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"program": "${file}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
173
bot.js
Normal file
173
bot.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
/* Pakete die wir brauchen */
|
||||||
|
const querystring = require('querystring');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
var WebSocketClient = require('websocket').client;
|
||||||
|
|
||||||
|
/** Wissensbasis importieren und als Dictionary abspeichern */
|
||||||
|
var wissensbasis = require('./wissensbasis.json');
|
||||||
|
var answerDict = {};
|
||||||
|
wissensbasis.intents.forEach(e => {
|
||||||
|
answerDict[e.intent] = e.answers;
|
||||||
|
});
|
||||||
|
var repeat1Dict = {};
|
||||||
|
wissensbasis.intents.forEach(e => {
|
||||||
|
repeat1Dict[e.intent] = e.repeat1;
|
||||||
|
});
|
||||||
|
var repeat2Dict = {};
|
||||||
|
wissensbasis.intents.forEach(e => {
|
||||||
|
repeat2Dict[e.intent] = e.repeat2;
|
||||||
|
});
|
||||||
|
var fallbacks = [];
|
||||||
|
wissensbasis.fallbacks.forEach(e => {
|
||||||
|
fallbacks.push(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Die Intents der letzten 2 Anfragen werden hier gespeichert */
|
||||||
|
var savedIntents = {};
|
||||||
|
|
||||||
|
/** bot ist ein einfacher Websocket Chat Client */
|
||||||
|
class bot {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konstruktor baut den client auf. Er erstellt einen Websocket und verbindet sich zum Server
|
||||||
|
* Bitte beachten Sie, dass die Server IP hardcodiert ist. Sie müssen sie umsetzten
|
||||||
|
*/
|
||||||
|
constructor () {
|
||||||
|
|
||||||
|
/** Die Websocketverbindung */
|
||||||
|
this.client = new WebSocketClient()
|
||||||
|
|
||||||
|
/** Der Name des Bots */
|
||||||
|
this.botname = "Raspi-Bot"
|
||||||
|
|
||||||
|
/** Wenn der Websocket verbunden ist, dann setzten wir ihn auf true */
|
||||||
|
this.connected = false
|
||||||
|
|
||||||
|
/** LUIS Endpoint-Parameter */
|
||||||
|
this.endpointKey = "9c4cdfefdcde4156a0735c0d2100ef3e";
|
||||||
|
this.endpoint = "raspberry-pi-4-chatbot.cognitiveservices.azure.com/";
|
||||||
|
this.appId = "87bb26ac-91ee-4006-a144-6cc7db61b0fb";
|
||||||
|
|
||||||
|
/** Wenn die Verbindung nicht zustande kommt, dann läuft der Aufruf hier hinein */
|
||||||
|
this.client.on('connectFailed', function (error) {
|
||||||
|
console.log('[BOT] Connect Error: ' + error.toString())
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Wenn der Client sich mit dem Server verbindet sind wir hier */
|
||||||
|
this.client.on('connect', function (connection) {
|
||||||
|
this.con = connection
|
||||||
|
console.log('[BOT] WebSocket Client Connected')
|
||||||
|
connection.on('error', function (error) {
|
||||||
|
console.log('[BOT] Connection Error: ' + error.toString())
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Es kann immer sein, dass sich der Client disconnected
|
||||||
|
* (typischer Weise, wenn der Server nicht mehr da ist)
|
||||||
|
*/
|
||||||
|
connection.on('close', function () {
|
||||||
|
console.log('[BOT] echo-protocol Connection Closed')
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hier ist der Kern, wenn immmer eine Nachricht empfangen wird, kommt hier die
|
||||||
|
* Nachricht an.
|
||||||
|
*/
|
||||||
|
connection.on('message', function (message) {
|
||||||
|
if (message.type === 'utf8') {
|
||||||
|
var data = JSON.parse(message.utf8Data)
|
||||||
|
console.log('[BOT] Received: ' + data.msg + ' ' + data.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hier senden wir unsere Kennung damit der Server uns erkennt.
|
||||||
|
* Wir formatieren die Kennung als JSON
|
||||||
|
*/
|
||||||
|
function joinGesp () {
|
||||||
|
if (connection.connected) {
|
||||||
|
connection.sendUTF('{"type": "join", "name":"Raspi-Bot"}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
joinGesp()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methode um sich mit dem Server zu verbinden.
|
||||||
|
*/
|
||||||
|
connect () {
|
||||||
|
this.client.connect('ws://localhost:8181/', 'chat')
|
||||||
|
this.connected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hier muss ihre Verarbeitungslogik integriert werden.
|
||||||
|
* Diese Funktion wird automatisch im Server aufgerufen, wenn etwas ankommt, das wir
|
||||||
|
* nicht geschrieben haben
|
||||||
|
* @param nachricht auf die der bot reagieren soll
|
||||||
|
* @param nachricht name des Absenders
|
||||||
|
*/
|
||||||
|
async post (nachricht, uname) {
|
||||||
|
// Initialisiert den Intent-Speicher falls dies noch nicht geschehen ist
|
||||||
|
if(savedIntents[uname] == undefined){
|
||||||
|
savedIntents[uname] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sendet die Nachricht an LUIS und erhält extrahiert den Top-Intent und dessen Score aus der Antwort
|
||||||
|
var luisResponse = await this.sendLUISrequest(nachricht);
|
||||||
|
var intent = luisResponse.prediction.topIntent;
|
||||||
|
var intentScore = luisResponse.prediction.intents[intent].score;
|
||||||
|
|
||||||
|
// Antwort anhand des erhaltenen Intents und den vorherigen Intents ermitteln
|
||||||
|
var answer;
|
||||||
|
if(intentScore > 0.5){
|
||||||
|
if(intent != savedIntents[uname][0]){
|
||||||
|
var answerArray = answerDict[intent]
|
||||||
|
answer = answerArray[Math.floor(Math.random() * answerArray.length)];
|
||||||
|
}else{
|
||||||
|
if(intent == savedIntents[uname][1]){
|
||||||
|
var repeat2Array = repeat2Dict[intent]
|
||||||
|
answer = repeat2Array[Math.floor(Math.random() * repeat2Array.length)];
|
||||||
|
}else{
|
||||||
|
var repeat1Array = repeat1Dict[intent]
|
||||||
|
answer = repeat1Array[Math.floor(Math.random() * repeat1Array.length)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Speichert den neuen Intent ab
|
||||||
|
savedIntents[uname][1] = savedIntents[uname][0];
|
||||||
|
savedIntents[uname][0] = intent;
|
||||||
|
}else{
|
||||||
|
answer = fallbacks[Math.floor(Math.random() * fallbacks.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Antwort zusammensetzen und absenden
|
||||||
|
var msg = '{"type": "msg", "name": "Raspi-Bot", "msg":"' + answer + '"}'
|
||||||
|
this.client.con.sendUTF(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sendet die Nachricht zum LUIS Endpoint
|
||||||
|
*
|
||||||
|
* @param message die Nachricht
|
||||||
|
* @returns die Antwort
|
||||||
|
*/
|
||||||
|
async sendLUISrequest(message){
|
||||||
|
var queryParams = {
|
||||||
|
"show-all-intents": true,
|
||||||
|
"verbose": true,
|
||||||
|
"query": message,
|
||||||
|
"subscription-key": this.endpointKey
|
||||||
|
}
|
||||||
|
var URI =`https://${this.endpoint}/luis/prediction/v3.0/apps/${this.appId}/slots/production/predict?${querystring.stringify(queryParams)}`;
|
||||||
|
var result
|
||||||
|
await fetch(URI).then(d=>d.json()).then(e=>{
|
||||||
|
result = e
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = bot
|
1129
package-lock.json
generated
Normal file
1129
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "raspibot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "ChatBot to get information about the Raspberry Pi 4",
|
||||||
|
"main": "staticExpress.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node staticExpress.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"bot"
|
||||||
|
],
|
||||||
|
"author": "Andreas Greiner",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"querystring": "^0.2.1",
|
||||||
|
"websocket": "^1.0.34"
|
||||||
|
}
|
||||||
|
}
|
153
public/css/newchat.css
Normal file
153
public/css/newchat.css
Normal file
File diff suppressed because one or more lines are too long
30
public/index.html
Normal file
30
public/index.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link href="./css/newchat.css" rel="stylesheet" type="text/css">
|
||||||
|
<script src="./js/newchat.js"></script>
|
||||||
|
<title>Raspi-Bot</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section class="msger">
|
||||||
|
<header class="msger-header">
|
||||||
|
<div class="msger-header-title">
|
||||||
|
<i class="fas fa-comment-alt">Raspberry Pi 4 Chat Bot</i>
|
||||||
|
</div>
|
||||||
|
<div class="msger-header-options">
|
||||||
|
<span><i class="fas fa-cog"></i></span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="msger-chat"></main>
|
||||||
|
|
||||||
|
<form class="msger-inputarea" id="test">
|
||||||
|
<input type="text" class="msger-input" placeholder="Enter your message...">
|
||||||
|
<button type="submit" class="msger-send-btn">Send</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</body>
|
||||||
|
</html>
|
107
public/js/newchat.js
Normal file
107
public/js/newchat.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
var msgerChat = null;
|
||||||
|
var socketurl = 'ws://127.0.0.1:8181/';
|
||||||
|
|
||||||
|
/** Standartnachricht ausgeben und Websocket-Verbindung aufbauen */
|
||||||
|
window.onload=function(){
|
||||||
|
/* DOM-Variablen initialisieren */
|
||||||
|
var msgerForm = get(".msger-inputarea");
|
||||||
|
var msgerInput = get(".msger-input");
|
||||||
|
msgerChat = get(".msger-chat");
|
||||||
|
|
||||||
|
/* Standard Wilkommensnachricht ausgeben */
|
||||||
|
appendMessage(BOT_NAME, BOT_IMG, "left", "Hi, wilkommen beim Raspi-Bot!");
|
||||||
|
|
||||||
|
/* Websocket initialisieren */
|
||||||
|
var socket = new WebSocket(socketurl, 'chat');
|
||||||
|
var username = 'u1'
|
||||||
|
socket.onopen = function () {
|
||||||
|
username = "name" + Math.floor(Math.random() * Math.floor(700));
|
||||||
|
socket.send('{"type": "join", "name":" '+username+'"}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aktion bei Nachrichteneingang festlegen */
|
||||||
|
socket.onmessage = function (msg) {
|
||||||
|
var data = JSON.parse(msg.data);
|
||||||
|
if(data.type == 'msg'){
|
||||||
|
if(data.name !== "Raspi-Bot" ){
|
||||||
|
appendMessage("Nutzer", PERSON_IMG, "right", data.msg);
|
||||||
|
}else{
|
||||||
|
appendMessage(BOT_NAME, BOT_IMG, "left", data.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Aktion zum Absenden einer Nachricht festlegen */
|
||||||
|
msgerForm.addEventListener("submit", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const msgText = msgerInput.value;
|
||||||
|
if (!msgText) return;
|
||||||
|
socket.send('{"type": "msg", "msg": "' + msgerInput.value + '"}');
|
||||||
|
msgerInput.value = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icons und Botname für die Chat-Bubbles */
|
||||||
|
const BOT_IMG = "https://image.flaticon.com/icons/svg/327/327779.svg";
|
||||||
|
const PERSON_IMG = "https://image.flaticon.com/icons/svg/145/145867.svg";
|
||||||
|
const BOT_NAME = "Raspi-Bot";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neue Nachricht ins HTML hinzufügen
|
||||||
|
*
|
||||||
|
* @param img for the backround of the bubble
|
||||||
|
* @param side eighter left or right
|
||||||
|
* @param text the message text
|
||||||
|
*/
|
||||||
|
function appendMessage(name, img, side, text) {
|
||||||
|
const msgHTML = `
|
||||||
|
<div class="msg ${side}-msg">
|
||||||
|
<div class="msg-img" style="background-image: url(${img})"></div>
|
||||||
|
|
||||||
|
<div class="msg-bubble">
|
||||||
|
<div class="msg-info">
|
||||||
|
<div class="msg-info-name">${name}</div>
|
||||||
|
<div class="msg-info-time">${formatDate(new Date())}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="msg-text">${text}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
msgerChat.insertAdjacentHTML("beforeend", msgHTML);
|
||||||
|
msgerChat.scrollTop += 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DOM-Elemente selektieren
|
||||||
|
*
|
||||||
|
* @param selector der Selektor (z.B. Klasse oder ID)
|
||||||
|
* @param root das Root-Element von dem ausgegangen wird
|
||||||
|
* @returns das DOM-Element
|
||||||
|
*/
|
||||||
|
function get(selector, root = document) {
|
||||||
|
return root.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatiert einen Datumswert zum Uhrzeit-Format HH:mm
|
||||||
|
*
|
||||||
|
* @param {*} date das Datum
|
||||||
|
* @returns die formatierte Uhrzeit
|
||||||
|
*/
|
||||||
|
function formatDate(date) {
|
||||||
|
const h = "0" + date.getHours();
|
||||||
|
const m = "0" + date.getMinutes();
|
||||||
|
return `${h.slice(-2)}:${m.slice(-2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt eine zufällige ganze Zahl zwischen min und max zurück
|
||||||
|
*
|
||||||
|
* @param {*} min die Untergrenze
|
||||||
|
* @param {*} max die Obergrenze
|
||||||
|
* @returns die Zufallszahl
|
||||||
|
*/
|
||||||
|
function random(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min) + min);
|
||||||
|
}
|
179
public/pics/pi4.svg
Normal file
179
public/pics/pi4.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 40 KiB |
312
public/pics/pi4_gray.svg
Normal file
312
public/pics/pi4_gray.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 92 KiB |
96
staticExpress.js
Normal file
96
staticExpress.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/* Pakete die wir brauchen */
|
||||||
|
var bot = require('./bot.js')
|
||||||
|
var express = require('express')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
// Erzeugt die Express Applikation
|
||||||
|
var app = express()
|
||||||
|
|
||||||
|
/* Nutzen einer statischen WebSeite */
|
||||||
|
app.use(express.static('public'))
|
||||||
|
|
||||||
|
// Wir nutzen ein paar statische Ressourcen
|
||||||
|
app.use('/css', express.static(__dirname + '/public/css'))
|
||||||
|
app.use('/js', express.static(__dirname + '/public/js'))
|
||||||
|
app.use('/pics', express.static(__dirname + '/public/pics'))
|
||||||
|
|
||||||
|
// Wir starten den Express server
|
||||||
|
var server = app.listen(8081, function () {
|
||||||
|
var address = server.address()
|
||||||
|
console.log(address)
|
||||||
|
console.log('Server started at http://localhost:8081')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Das brauchen wir für unsere Websockets
|
||||||
|
var WSS = require('websocket').server,
|
||||||
|
http = require('http')
|
||||||
|
|
||||||
|
// Erzeugt den Webserver und hört auf den angegebenen Port
|
||||||
|
var server = http.createServer()
|
||||||
|
server.listen(8181)
|
||||||
|
|
||||||
|
/* Wir erstellen einen Bot, der kann sich aber noch nicht mit
|
||||||
|
* dem Socket Server verbinden, da dieser noch nicht läuft
|
||||||
|
*/
|
||||||
|
var myBot = new bot()
|
||||||
|
|
||||||
|
// Hier erstellen wir den Server für den Websocket
|
||||||
|
var wss = new WSS({
|
||||||
|
httpServer: server,
|
||||||
|
autoAcceptConnections: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Abspeichern der Verbindungen
|
||||||
|
var connections = {}
|
||||||
|
|
||||||
|
// Wenn Sich ein client Socket mit dem Server verbinden will kommt er hier an
|
||||||
|
wss.on('request', function (request) {
|
||||||
|
var connection = request.accept('chat', request.origin)
|
||||||
|
connection.on('message', function (message) {
|
||||||
|
var name = ''
|
||||||
|
for (var key in connections) {
|
||||||
|
if (connection === connections[key]) {
|
||||||
|
name = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = JSON.parse(message.utf8Data)
|
||||||
|
var msg = 'leer'
|
||||||
|
|
||||||
|
// Variablen um später den letzten Satz und den Sender zu speichern
|
||||||
|
var uname
|
||||||
|
var utype
|
||||||
|
var umsg
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case 'join':
|
||||||
|
// Wenn der Typ join ist füge ich den Client einfach unserer Liste hinzu
|
||||||
|
connections[data.name] = connection
|
||||||
|
msg = '{"type": "join", "names": ["' + Object.keys(connections).join('","') + '"]}'
|
||||||
|
if (myBot.connected === false) {
|
||||||
|
myBot.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'msg':
|
||||||
|
// Erstelle eine Nachricht in JSON mit Typ, Sender und Inhalt
|
||||||
|
msg = '{"type": "msg", "name": "' + name + '", "msg":"' + data.msg + '"}'
|
||||||
|
utype = 'msg'
|
||||||
|
uname = name
|
||||||
|
umsg = data.msg
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sende alle daten an alle verbundenen Sockets
|
||||||
|
for (var key in connections) {
|
||||||
|
if (connections[key] && connections[key].send) {
|
||||||
|
connections[key].send(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leite die Daten des Users an den Bot weiter, damit der antworten kann
|
||||||
|
if (uname !== 'Raspi-Bot' && utype === 'msg') {
|
||||||
|
var test = myBot.post(umsg,uname)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
18
wissensbasis.json
Normal file
18
wissensbasis.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{ "intents" :[
|
||||||
|
{"intent":"kaufen", "answers":["kaufen answer 1","kaufen answer 1","kaufen answer 1"], "repeat1": ["kaufen repeat 1"],"repeat2":["kaufen repeat 2"]},
|
||||||
|
{"intent":"netzwerk", "answers":["netzwerk answer 1","netzwerk answer 2","netzwerk answer 3"], "repeat1": ["netzwerk repeat 1"],"repeat2":["netzwerk repeat 2"]},
|
||||||
|
{"intent":"usb", "answers":["usb answer 1","usb answer 2","usb answer 3"], "repeat1": ["usb repeat 1"],"repeat2":["usb repeat 2"]},
|
||||||
|
{"intent":"display", "answers":["display answer 1","display answer 2","display answer 3"], "repeat1": ["display repeat 1"],"repeat2":["display repeat 2"]},
|
||||||
|
{"intent":"strom", "answers":["strom answer 1","strom answer 2","strom answer 3"], "repeat1": ["strom repeat 1"],"repeat2":["strom repeat 2"]},
|
||||||
|
{"intent":"ram", "answers":["ram answer 1","ram answer 2","ram answer 3"], "repeat1": ["ram repeat 1"],"repeat2":["ram repeat 2"]},
|
||||||
|
{"intent":"cpu", "answers":["cpu answer 1","cpu answer 2","cpu answer 3"], "repeat1": ["cpu repeat 1"],"repeat2":["cpu repeat 2"]},
|
||||||
|
{"intent":"gpio", "answers":["gpio answer 1","gpio answer 2","gpio answer 3"], "repeat1": ["gpio repeat 1"],"repeat2":["gpio repeat 2"]},
|
||||||
|
{"intent":"sd", "answers":["sd answer 1","sd answer 2","sd answer 3"], "repeat1": ["sd repeat 1"],"repeat2":["sd repeat 2"]},
|
||||||
|
{"intent":"os", "answers":["os answer 1","os answer 2","os answer 3"], "repeat1": ["os repeat 1"],"repeat2":["os repeat 2"]}
|
||||||
|
],
|
||||||
|
"fallbacks": [
|
||||||
|
"fallback 1",
|
||||||
|
"fallback 2",
|
||||||
|
"fallback 3"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user