2024-10-12



BotBuilder-SamplesのサンプルボットをローカルからAzureにデプロイする
にて、Azureクラウド上にボットを作成できたので、ローカルDebianマシンからHTTPS接続しAPIをコールし会話ができる。
Azure上のボットはLINEなど色々なプラットフォームと接続可能だが、今回はDebianから直接呼ぶのでDirect Lineチャネル(REST API)を使用する。
なにはともあれ、BotBuilder-Samples/44.prompt-for-user-inputをRESTで実装したサンプルサイトはこちら。
http://www.servernote.net:3978/
サンプルそのままなので、英語のみ。

以下のように制作。
【APIキーの取得とエンドポイント】
Azureポータルホームからさきほど作成したリソース、ボットを表示する。
ホーム→リソース グループ→hogeuser-group→hogeuser-bot-44-prompt-for-user-input
左のボット管理「チャンネル」を選択して、おすすめチャンネルの追加にある「Direct Lineチャンネルを構成」をクリック。
「シークレット キー」が表示されるので、コピー。RESTのエンドポイント(接続先)は現在
https://directline.botframework.com/v3/directline/conversations
となっている。シークレットキーはここでは例としてDIRECTLINEKEY-AAAAAAAAAAAA-BBBBBBBBBBとする。
【ローカルDebianからAPI接続するNode.jsソース】
| JavaScript | 44-prompt-for-user-input.js | GitHub Source |
var fs = require('fs');
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
const port = 3978;
const BOT_URL = "https://directline.botframework.com/v3/directline/conversations";
const BOT_KEY = "DIRECTLINEKEY-AAAAAAAAAAAA-BBBBBBBBBB";
var request = require('request');
request.debug = true;
function optJSONString(json,key){
if( json && key in json && json[key] != null && json[key].length > 0){
return json[key];
}
return "";
}
function optJSONNumber(json,key){
if( json && key in json && json[key] != null){
return json[key];
}
return 0;
}
function clearCookie(http_res){
http_res.clearCookie('azure_cnvid'); //delete sid
http_res.clearCookie('azure_token'); //delete sid
http_res.clearCookie('azure_watermark'); //delete sid
}
function errorAction(http_res,message){
clearCookie(http_res);
if(message === "Invalid token or secret"){
createSession(http_res);
}
else{
http_res.send('エラーが発生しました。恐れ入りますが暫く後、再度お試し下さい。');
}
}
function createSession(http_res){
request.post({
url: BOT_URL,
method: "POST",
headers: {
"Authorization": "Bearer " + BOT_KEY
},
json: true
},(error, res, body) => {
var ok = 0;
if(error){
console.log(error);
}
else{
console.log(JSON.stringify(body, null, 2));
var conversationId = optJSONString(body,"conversationId");
var token = optJSONString(body,"token");
var expires_in = optJSONNumber(body,"expires_in");
console.log("conversationId="+conversationId);
console.log("token="+token);
console.log("expires_in="+expires_in);
if(conversationId.length > 0 && token.length > 0){
ok = 1;
http_res.cookie('azure_cnvid', conversationId, {maxAge:3600000, httpOnly:false});
http_res.cookie('azure_token', token, {maxAge:3600000, httpOnly:false});
sendMessage(null,http_res,conversationId,token,null);
}
}
if( ok == 0 ){
errorAction(http_res,"");
}
});
}
function sendMessage(http_req,http_res,azure_cnvid,azure_token,azure_watermark){
var input_text = "Hello";
if(http_req != null) input_text = http_req.body.user_input_ta;
request.post({
url: BOT_URL + "/" + azure_cnvid + "/activities",
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + azure_token
},
json: {
type: "message",
from: { id: "user1" },
text: input_text
}
},(error, res, body) => {
if(error){
console.log(error);
errorAction(http_res,"");
}
else if(body && "error" in body){
console.log(JSON.stringify(body, null, 2));
errorAction(http_res,optJSONString(body.error,"message"));
}
else if(body){
console.log(JSON.stringify(body, null, 2));
request.get({
url: BOT_URL + "/" + azure_cnvid + "/activities?watermark=" +
(azure_watermark ? azure_watermark:"WATERMARK_STRING"),
method: "GET",
headers: {
"Authorization": "Bearer " + azure_token
},
json: true
},(error2, res2, body2) => {
if(error2){
console.log(error2);
errorAction(http_res,"");
}
else if(body2 && "error" in body2){
console.log(JSON.stringify(body2, null, 2));
errorAction(http_res,optJSONString(body2.error,"message"));
}
else if(body2){
console.log(JSON.stringify(body2, null, 2));
var watermark = optJSONNumber(body2,"watermark");
if( watermark > 0 ){
http_res.cookie('azure_watermark', watermark + "", {maxAge:3600000, httpOnly:false});
}
var rtext = '';
var pre_text = '';
if( "activities" in body2 ){
for(var i = 0; i < body2.activities.length; i++ ){
var replyToId = optJSONString(body2.activities[i],"replyToId");
var text = optJSONString(body2.activities[i],"text");
if(replyToId.length > 0 && text.length > 0 && text !== pre_text){
if(rtext.length > 0) rtext += '\n';
rtext += text;
pre_text = text;
}
}
}
if(rtext.length > 0){
http_res.send(rtext.replace(/\n/g, '<br>'));
}
else{
http_res.send("有効な回答を見つけられませんでした。");
}
}
});
}
});
}
app.use(express.static('public'));
app.use(cookieParser());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.json());
app.post('/chat-post', (http_req, http_res) => {
console.log(http_req.body);
var azure_cnvid = http_req.cookies.azure_cnvid;
console.log("azure_cnvid="+azure_cnvid);
var azure_token = http_req.cookies.azure_token;
console.log("azure_token="+azure_token);
var azure_watermark = http_req.cookies.azure_watermark;
console.log("azure_watermark="+azure_watermark);
if(azure_cnvid && azure_token){ //セッションidあり
sendMessage(http_req,http_res,azure_cnvid,azure_token,azure_watermark);
}
else{
createSession(http_res);
}
});
app.get('/', (http_req, http_res) => {
clearCookie(http_res);
fs.readFile('./44-prompt-for-user-input.html', 'utf-8' , readcallback );
function readcallback(err, data) {
http_res.send(data);
}
});
app.get('*', function(http_req, http_res) {
http_res.redirect('/');
});
app.listen(port, () => console.log(`app listening on port ${port}!`))
・使用ライブラリのインストール
npm install express body-parser cookie-parser
後述のユーザに表示するHTMLから入力を受け取るパーサ、セッションIDを保存するCookieを扱うため。
・このサンプルを立ち上げるportを記述
const port = 3978;
特に何番でもよいがとりあえず上記番号とした。
・http://www.servernote.net:3978/を叩くと呼ばれるのはapp.get('/'関数
ここで、同じディレクトリにある44-prompt-for-user-input.htmlを読み込んで表示。
トップアクセスなのでそれまでのセッションCookieをクリアしている。
・HTMLからの入力を受け取るのがapp.post('/chat-post',関数
CookieにすでにセッションIDやどこまで会話が進んだかを示すwatermarkが保存されてれば、会話を継続。=sendMessageへ
そうでなければAzureに初回接続しセッションIDを作成 = createSessionへ。
・createSessionで、Azureに接続しIDをJSONで取得しパース、ユーザブラウザのCookieへ出力保存。
このまま最初のダミー会話(Hello)をボットへ投げて、最初のあいさつリプライを得ている。
・sendMessageでは、ユーザ入力またはダミー入力を送ってリプライを得て、HTMLのajaxへ返している。
watermarkはAzure Botとのやり取りで都度更新される、どこまで会話をこちらが読んだかを知らせる「しおり」である。
これを指定すれば、それ以降の未読のやりとりのみをAzure Botは返してくる。
【ユーザーUI画面 HTMLソース(上記Node.jsから読み込み)】
| HTML | 44-prompt-for-user-input.html | GitHub Source |
<!doctype html>
<html lang="ja">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>44-prompt-for-user-input Direct Line REST UI</title>
<style type="text/css">
body {
word-break: break-all;
word-wrap: break-word;
margin-bottom: 50px;
background-color: #000000;
color: #ffffff;
}
textarea {
line-height: 1.2em;
}
</style>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<!--
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script>//<![CDATA[
function escape_html (string) {
if(typeof string !== 'string') {
return string;
}
return string.replace(/[&'`"<>]/g, function(match) {
return {
'&': '&',
"'": ''',
'`': '`',
'"': '"',
'<': '<',
'>': '>',
}[match]
});
}
function trimingForm(parts) {
if( $('#' + parts).length ) {
var pval = $('#' + parts).val();
if( pval.length > 0 ){
pval = pval.trim();
$('#' + parts).val(pval.trim());
}
}
}
//]]></script>
</head>
<body class="container-fluid"><a name="pagetop"></a>
<div class="row">
<div class="col-12 pb-2">
<span style="font-size: 1.3rem;">44-prompt-for-user-input Direct Line REST UI</span>
</div>
</div>
<div class="row px-3">
<div id="history" class="col-12"></div>
</div>
<div class="d-flex w-100 py-2 align-items-center">
<textarea id="user_input_ta" rows="2" style="width: 100%;"></textarea> <button id="user_input_bt" type="button" class="btn-danger btn-block p-2 rounded" style="margin-left: 3px; width: 120px; height: 50px;">送信</button>
</div>
<div class="row">
<div class="col-12">
※REVISION 0.0.0※
</div>
</div>
<script>
//<![CDATA[
var lastResponse = "";
function adjustTextArea(textarea) {
var lineHeight = parseInt(textarea.css("lineHeight"));
var lines = (textarea.val() + "\n").match(/\n/g).length;
if (lines < 2) lines = 2;
textarea.height(lineHeight * lines);
}
function sendMessage(text){
var query = "user_input_ta=" + encodeURIComponent(text);
var jqxhr = $.post("chat-post", query, function() {
//alert( "success" );
})
.done(function(data) {
//alert( "second success" );
//console.log(data);
lastResponse = data;
var history = $("#history").html();
history +=
'<div class="row py-2">' +
'<div class="col-9 rounded" style="border: 1px solid #0000ff;">' +
data +
"<\/div>" +
'<div class="col-3"><\/div>' +
"<\/div>";
$("#history").html(history);
//$(window).scrollTop($("#user_input_bt").offset().top);
})
.fail(function() {
//alert( "error" );
})
.always(function() {
//alert( "finished" );
});
jqxhr.always(function() {
//alert( "second finished" );
});
}
$(window).on('pageshow',function(){
sendMessage('');
});
$("#user_input_ta").on("input", function(e) {
adjustTextArea($(this));
});
$("#user_input_bt").on("click", function() {
trimingForm("user_input_ta");
var user_input_ta = $("#user_input_ta").val();
if (user_input_ta.length <= 0) return;
user_input_ta = user_input_ta.replace(/\r\n/g, "\n");
var disp_user_input_ta = escape_html(user_input_ta);
disp_user_input_ta = disp_user_input_ta.replace(/\n/g, "<br>");
user_input_ta = user_input_ta.replace(/\n/g, "");
var history = $("#history").html();
history +=
'<div class="row py-2">' +
'<div class="col-3"><\/div>' +
'<div class="col-9 rounded" style="border: 1px solid #ff0000;">' +
disp_user_input_ta +
"<\/div>" +
"<\/div>";
$("#history").html(history);
$("#user_input_ta").val("");
adjustTextArea($("#user_input_ta"));
//$(window).scrollTop($("#user_input_bt").offset().top);
sendMessage(user_input_ta);
});
//]]>
</script>
</body>
</html>
ユーザーのUIとなるHTML。送信ボタンがタップされるとTEXTAREAの内容を上記Node.jsへajaxを使って投げて、返答を受け取り、LINEライクなやりとり画面に出力をためていく。
【起動】
上記2ファイルを同じディレクトリへ置き、
node 44-prompt-for-user-input.js
とすれば、3978番ポートをリッスンして立ち上がる。
※本記事内容の無断転載を禁じます。
ご連絡は以下アドレスまでお願いします★
Wav2Lipのオープンソース版を改造して外部から呼べるAPI化する
Wav2Lipのオープンソース版で静止画の口元のみを動かして喋らせる
【iOS】アプリアイコン・ロゴ画像の作成・設定方法
オープンソースリップシンクエンジンSadTalkerをAPI化してアプリから呼ぶ【2】
オープンソースリップシンクエンジンSadTalkerをAPI化してアプリから呼ぶ【1】
【Xcode】iPhone is not available because it is unpairedの対処法
【Let's Encrypt】Failed authorization procedure 503の対処法
【Debian】古いバージョンでapt updateしたら404 not foundでエラーになる場合
ファイアウォール内部のWindows11 PCにmacOS Sequoiaからリモートデスクトップする
Windows11+WSL2でUbuntuを使う【2】ブリッジ接続+固定IPの設定
進研ゼミチャレンジタッチをAndroid端末化する
Intel Macbook2020にBootCampで入れたWindows11 Pro 23H2のBluetoothを復活させる
VirtualBoxの仮想マシンをWindows起動時に自動起動し終了時に自動サスペンドする
GitLabにHTTPS経由でリポジトリをクローン&読み書きを行う
【C/C++】小数点以下の切り捨て・切り上げ・四捨五入
【PHP】Mail/mimeDecodeを使ってメールの中身を解析(準備編)
【Apache】サーバーに同時接続可能なクライアント数を調整する
タスクスケジューラで変更を適用できません。ユーザーアカウントが不明であるか、パスワードが正しくないか、またはユーザーアカウントにタスクを変更する許可がありません。と出た