2023-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番ポートをリッスンして立ち上がる。
※本記事内容の無断転載を禁じます。
ご連絡は以下アドレスまでお願いします★
Windowsのデスクトップ画面をそのまま配信するための下準備
WindowsでGPUの状態を確認するには(ASUS系監視ソフトの自動起動を停止する)
CORESERVER v1プランからさくらインターネットスタンダートプランへ引っ越しメモ
さくらインターネットでPython MecabをCGIから使う
さくらインターネットのPHPでAnalytics-G4 APIを使う
インクルードパスの調べ方
【Git】特定ファイルを除外する.gitignore
【Ubuntu/Debian】NVIDIA関係のドライバを自動アップデートさせない
【Python】Spacyを使用して文章から出発地と目的地を抜き出す
【Windows10】リモートデスクトップ間のコピー&ペーストができなくなった場合の対処法
Windows11+WSL2でUbuntuを使う【2】ブリッジ接続+固定IPの設定
Windows版Google Driveが使用中と言われアンインストールできない場合
VirtualBoxの仮想マシンをWindows起動時に自動起動し終了時に自動サスペンドする
【Apache】サーバーに同時接続可能なクライアント数を調整する
【C/C++】小数点以下の切り捨て・切り上げ・四捨五入
Googleファミリーリンクで子供の端末の現在地がエラーで取得できない場合
【Linux】iconv/libiconvをソースコードからインストール
Ubuntu Server 21.10でイーサリアムブロックチェーン【その5】