アプリケーション開発ポータルサイト
ServerNote.NET
カテゴリー【Microsoft AzureJavaScriptNode.jsDebian
【Miscrosoft Azure】デプロイしたサンプルボットへREST APIで接続し会話する
POSTED BY
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ソース】

JavaScript44-prompt-for-user-input.jsGitHub 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から読み込み)】

HTML44-prompt-for-user-input.htmlGitHub 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 {
        '&': '&amp;',
        "'": '&#x27;',
        '`': '&#x60;',
        '"': '&quot;',
        '<': '&lt;',
        '>': '&gt;',
      }[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番ポートをリッスンして立ち上がる。

※本記事は当サイト管理人の個人的な備忘録です。本記事の参照又は付随ソースコード利用後にいかなる損害が発生しても当サイト及び管理人は一切責任を負いません。
※本記事内容の無断転載を禁じます。
【WEBMASTER/管理人】
自営業プログラマーです。お仕事ください!
ご連絡は以下アドレスまでお願いします★

【キーワード検索】