チャットボットとやり取りできるようにする

今回の位置づけ

前回までの復習

初回

  • チャットボット=自動応答するプログラム(チャットボットの概要
  • ライブラリ chatterbot を使ったPythonスクリプト

前回

  • チャットボットをWebアプリとして動かすための 準備
  • Djangoを使って手元のPCで動くアプリケーションを開発
  • チャットボットと会話できるWebアプリ

Djangoの設定

ここまでの実装内容
  • URL設定:http://127.0.0.1:8000/ (パスが "" (空文字列))へのリクエストに対しては、ビューの home (関数)を呼び出す
  • ビュー: home 関数は、テンプレート chat/home.html を含んだレスポンスを返す
  • テンプレート:空のHTMLファイルを置いただけ

chat/home.html にHTMLを書くと、ブラウザに表示できます

今回やること

表示したHTML上で、 チャットボットとやり取りできる ようにしていきます。

前回の開発の続きができるように準備

$ cd ~/programming/chatbot
$ source env/bin/activate
$ cd app
$ python manage.py runserver

注釈

今回からの方向け

前回の状態のファイルをzipにまとめて配布しています。 ダウンロードし、解凍してできた app ディレクトリを ~/programming/chatbot 以下に置いてください。

$ cd ~/programming/chatbot  # チャットボットの講義用ディレクトリ
$ python3.7 -m venv env  # 仮想環境をまだ作っていない場合は作る
$ source env/bin/activate
$ pip install 'Django<4'  # 仮想環境にDjangoをインストール
$ cd app  # ダウンロードして解凍したディレクトリを ~/programming/chatbot/app として配置済み
$ python manage.py runserver

http://127.0.0.1:8000/ をブラウザで開くと、真っ白い画面が表示される状態から始めます(前回はエラーを解決しながら設定しました)

開発tips:VSCodeの別のタブで runserver しておく

チャットボットとやり取りできる画面を作る

http://127.0.0.1:8000/ で表示される画面にメッセージ入力欄を作ります。

メッセージ入力欄はHTMLの form タグで実装します。 https://developer.mozilla.org/ja/docs/Web/HTML/Element/form

HTMLを書いてもいいのですが、Djangoがサポートしているフォーム(Djangoフォーム)を使います。 Djangoの流儀に沿ってPythonを書けば、HTMLのフォームができあがります。 (個人的な経験ですが、フォームのためにテンプレートに長くHTMLを書いたあとで、DjangoフォームにしておけばPythonで済ませられたことに気づき、Djangoフォームを選んでいればもっと開発しやすかったなと感じました)

フォーム

chat アプリケーションに forms.py を作ります。

touch chat/forms.py

app/chat/forms.py
1from django import forms
2
3
4class ChatMessageForm(forms.Form):
5    message = forms.CharField(required=True, max_length=100)

Djangoが提供する Form クラスを継承して、 ChatMessageForm クラスを定義します(4行目)。 フォームの入力欄はテキストが入力できる CharField のみとします(入力必須で、100文字まで)(5行目)。

https://docs.djangoproject.com/ja/3.2/ref/forms/fields/#charfield

テンプレートにフォームを表示

  • ビューでフォームのインスタンスを作り、テンプレートに渡します
  • テンプレートはフォームを表示します

ビュー

app/chat/views.py
1from .forms import ChatMessageForm
2
3
4def home(request):
5    form = ChatMessageForm()
6    context = {"form": form}
7    return render(request, "chat/home.html", context)

chat/forms.py に作った ChatMessageForm をインスタンス化します(5行目)。 render の第3引数に辞書(context という名前にしました)を渡します(7行目)。 context はビューの formChatMessageForm インスタンス)を form という名前で渡します(6行目)。 テンプレートとビューとで異なる名前を設定することもできますが、混乱を避けるため、私は揃えることが多いです。

テンプレート

空っぽの chat/home.html を以下のようにします。

templates/chat/home.html
 1<!DOCTYPE html>
 2<html lang="ja">
 3  <head>
 4    <meta charset="UTF-8" />
 5    <meta
 6      name="viewport"
 7      content="width=device-width, initial-scale=1, shrink-to-fit=no"
 8    />
 9    <title>Chatbot app</title>
10  </head>
11  <body>
12    <h1>Talk with chatbot</h1>
13
14    <form method="POST">
15      {% csrf_token %} {{ form.as_p }}
16      <button type="submit">Send</button>
17    </form>
18  </body>
19</html>

<body> の部分に注目してください(<head> については説明を省略します)。

{{ form }} とすればHTMLにメッセージ入力欄が表示されます(テンプレートに変数を埋め込む書き方です)。 Djangoフォームではinputができるので、<form> タグと <button> タグは書く必要があります。

注釈

参考情報

オウム返しするチャットボットを作る

フォームを表示できるようになりました! しかし、「こんにちは」のように入力して Send をクリックすると、入力した文字が消えます。 今の設定ではこれは正常な挙動です。

チャットボットが応答するURLを用意する

チャットボットが応答するようにしましょう。 単純なチャットボットとして、オウム返しするチャットボットで考えます。

  • 追加するURL:http://127.0.0.1:8000/bot-response/
    • URL設定を追加する
    • フォームの action 属性に設定(入力をチャットボットに送る)
  • フォーム入力されたデータの POST リクエストに対して、同じ message を返す
    • ビューに関数を追加する

URL設定

chat/urls.pyurlpatterns が指すリストに path の行を1つ追加します。

app/chat/urls.py
 1from django.urls import path
 2
 3from chat import views
 4
 5app_name = "chat"
 6
 7urlpatterns = [
 8    path("", views.home, name="home"),
 9    path("bot-response/", views.bot_response, name="bot_response"),
10]

chat/views.pybot_response という関数がまだないのでエラーとなり、サーバが起動しません。

ヒント

urlpatterns の指す値は リスト なので、末尾のカンマが落ちていると SyntaxError が送出されます。

urlpatterns = [
    path("", views.home, name="home")  # 末尾のカンマ忘れに注意!
    path("bot-response/", views.bot_response, name="bot_response"),
]

ビュー

エラーを解消するための実装

bot_response 関数を作ります。

ビューの関数は引数に request を受け取り、Djangoの HttpResponse を返します。

from django.http import HttpResponse  # importの行に追加


def bot_response(request):
    response = HttpResponse()
    response.write("レスポンスです")
    return response

HttpResponsewrite メソッドでレスポンスの内容を書き込めます: https://docs.djangoproject.com/ja/3.2/ref/request-response/#django.http.HttpResponse.write

ヒント

ここでフォームの action 属性を設定すると、Sendしたあと「レスポンスです」とブラウザに表示されます。

<form method="POST" action="http://127.0.0.1:8000/bot-response/">

のちほど設定について案内します(もっといい設定方法があります)。

注釈

home 関数で呼び出している render も実は HttpResponse を返しています。 https://docs.djangoproject.com/ja/3.2/intro/tutorial03/#a-shortcut-render

オウム返しするための実装

引数 request には フォームから送信されたデータが含まれる ので、それを取り出して、レスポンスに含めます。 この実装により、決め打ちのレスポンスからオウム返しのレスポンスに変わります。

app/chat/views.py
from django.http import HttpResponse
from django.shortcuts import render

from .forms import ChatMessageForm
app/chat/views.py
1def bot_response(request):
2    if request.method == "POST":
3        form = ChatMessageForm(request.POST)
4        if form.is_valid():
5            response_message = form.data["message"]
6            http_response = HttpResponse()
7            http_response.write(response_message)
8            return http_response

request.method でPOSTリクエストかどうか判定します(2行目)。 フォームから送信するとPOSTリクエストになります(method="POST" と指定しているためです)。

POSTリクエストでは、request.POST にフォームから送信されたデータがあります。 これを渡して ChatMessageForm のインスタンスを作ります(3行目)。

4行目はフォームから送信されたデータの検証です。

検証がパスした場合、 data 属性(辞書)のキー "message" の値を取得します(5行目)。 これがフォームに入力されたメッセージです。 Djangoフォームで message = ... と指定したので、キーも "message" となります。

注釈

request についてドキュメントより

request.POST は辞書のようなオブジェクトです。

https://docs.djangoproject.com/ja/3.2/intro/tutorial04/#write-a-minimal-form

テンプレート

<form> タグの action 属性を指定します。

templates/chat/home.html
    <form method="POST" action="{% url 'chat:bot_response' %}">
      {% csrf_token %} {{ form.as_p }}
      <button type="submit">Send</button>
    </form>

{% url %} はDjangoのテンプレートで使えるタグの1つです。 第1引数は、URL設定にある name を指定します。 'chat:bot_response''<app_name>:<name>' という形式です。 chat アプリケーションの name="bot_response" というURL設定が見つかります。 結果として、 action 属性に http://127.0.0.1:8000/bot-response/ が設定されます。

注釈

なぜ {% url %} を使うのか?

ずばり、変更しやすくする ためです。 URLを直接書く(ハードコードする)と、パスの文字列の変更(例 "bot-response/""response/")に追従させるのが大変です。 {% url %} タグで name から逆引きすることで、パスの文字列を変えやすくしています。

このハードコードされた、密結合のアプローチの問題は、プロジェクトにテンプレートが多数ある場合、URLの変更が困難になってしまうことです。

https://docs.djangoproject.com/ja/3.2/intro/tutorial03/#removing-hardcoded-urls-in-templates

以上で、フォームからメッセージを送ると、画面遷移し、オウム返しされたメッセージが表示されるようになりました!

画面遷移なしでチャットボットとやり取りできる

フォームを送信すると、チャットボットはオウム返ししますが、画面繊維を伴います。 フォームがある画面にチャットボットのやり取りを出し、チャットが蓄積するようにします。

画面遷移なしにするには Ajax (Asynchronous JavaScript And XML)を使います。 テンプレートの chat/home.html にJavaScriptを書いていきます。

今回は jQuery というライブラリを使用してAjaxによる通信を組み込みます。 (現在のJavaScript周りの状況を見ると jQuery は下火ですが、学習コストは小さいので今回採用しています。VueやReactでやってもらってもかまいません)

使うメソッドについては次のメモにまとめました: https://scrapbox.io/nikkie-memos/jQuery%E3%83%A1%E3%83%A2%EF%BC%88%E3%83%81%E3%83%A3%E3%83%83%E3%83%88%E3%83%9C%E3%83%83%E3%83%88Django%E3%82%A2%E3%83%97%E3%83%AA%EF%BC%89

フォームが送信されたときの処理を指定する

テンプレートに <script> タグを書いていきます。

HTMLの <body> タグの末尾に書きます。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <!-- 省略 -->
  </head>
  <body>
    <h1>Talk with chatbot</h1>

    <form method="POST">
      {% csrf_token %} {{ form.as_p }}
      <button type="submit">Send</button>
    </form>
  </body>

  <!-- ここに script タグを書いていきます -->
</html>

まずは以下のように書いてください。

templates/chat/home.html
 1<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
 2<script>
 3  $("form").submit(function (event) {
 4    event.preventDefault();
 5    const form = $(this);
 6
 7    console.log(form.prop("action"));
 8    console.log(form.prop("method"));
 9    console.log(form.serialize());
10  });
11</script>

ライブラリ jQuery をCDN(Contents Delivery Network)から読み込みます(1行目)。 ドキュメント https://jquery.com/download/#using-jquery-with-a-cdn の中からGoogleのAPIを選びました。 この行により jQuery のメソッドが使えるようになります。

3行目で、フォームの送信というイベントについて処理する関数(ハンドラ)を登録します。

4行目 event.preventDefault() で画面遷移というフォームのデフォルトの挙動を無効化しています。

ここではフォームの属性の値やフォームに入力される値の取得の仕方を確認するために、ログ出力しました(5行目〜)。

ブラウザで http://127.0.0.1:8000/ を開き、開発者ツールのコンソールを開いてから、メッセージを送信してみてください。 preventDefault の効果で 画面は切り替わらず に、ログが出力されます。

開発者ツールのコンソールで確認した値
form.prop("action") http://127.0.0.1:8000/bot-response/
form.prop("method") post
form.serialize() csrfmiddlewaretoken=省略&message=hello%20world

form.serialize() は日本語をエンコードします(ブラウザのURL欄の日本語と同じです)。

フォームが送信されたときにAjaxで通信する

上で確認した項目を使い、チャットボットが応答するURLにAjaxで通信します。

templates/chat/home.html
 1<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
 2<script>
 3  $("form").submit(function (event) {
 4    event.preventDefault();
 5    const form = $(this);
 6
 7    $.ajax({
 8      url: form.prop("action"),
 9      type: form.prop("method"),
10      data: form.serialize(),
11      dataType: "text",
12    })
13      .done(function (statement) {
14        console.log(statement);
15      });
16</script>

フォームの action 属性のURLに、method 属性のHTTPメソッドで、フォームに入力されたデータを送ります。 成功した場合、done イベントが発火し、登録されているハンドラにより返ってきたレスポンスをコンソールに出力します(14行目)。

ブラウザで http://127.0.0.1:8000/ を開き、開発者ツールのコンソールを開いてから、メッセージを送信します。 送ったのと同じメッセージがコンソールに表示されます。

コンソールを使って、ユーザとチャットボットがやり取りしているようにログ出力してみましょう。

templates/chat/home.html
 1<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
 2<script>
 3  $("form").submit(function (event) {
 4    event.preventDefault();
 5    const form = $(this);
 6
 7    $.ajax({
 8      url: form.prop("action"),
 9      type: form.prop("method"),
10      data: form.serialize(),
11      dataType: "text",
12    })
13      .done(function (statement) {
14        const input = $("#id_message");
15        console.log("You -> " + input.val());
16        console.log("bot: " + statement);
17        input.val("");
18      });
19</script>

id id_message はDjangoにより、メッセージ入力の input 要素に設定されています (フォームのクラス定義の中で message という名前にしたので、 id_message となりました)。

フォームの input 要素には id が指定されている
<input type="text" name="message" maxlength="100" required="" id="id_message">

IDを指定して、この input 要素を jQuery で取得します。 valinput 要素のvalue(入力された文字列)です。 ユーザの入力をログ出力し、チャットボットのレスポンスも出力したあとで、フォームの入力を空文字列とし(17行目)、続けてやり取りできるようにします。

フォームが送信されたあと、画面にチャットを表示する

ログ出力したフォーマットで画面に出力しましょう。

まず、フォームの上に、チャットの履歴を表示 するための <div> 要素を追加します。

templates/chat/home.html
<h1>Talk with chatbot</h1>
<div id="chat-log"></div>

<form method="POST" action="{% url 'chat:bot_response' %}">
  <!-- 以下、省略 -->

Ajaxの通信が成功したときにチャットの履歴を表示する(画面に書き足す)ようにします。

templates/chat/home.html
 1<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
 2<script>
 3  const chatLog = $("#chat-log");
 4
 5  function createRow(text) {
 6    const row = $("<p></p>");
 7    row.text(text);
 8    chatLog.append(row);
 9  }
10
11  $("form").submit(function (event) {
12    event.preventDefault();
13    const form = $(this);
14
15    $.ajax({
16      url: form.prop("action"),
17      type: form.prop("method"),
18      data: form.serialize(),
19      dataType: "text",
20    })
21      .done(function (statement) {
22        const input = $("#id_message");
23        createRow("You -> " + input.val());
24        createRow("bot: " + statement);
25        input.val("");
26      });
27  });
28</script>

画面の表示を変えるために createRow 関数を作りました。 これは id=chat-log の要素(=上で加えた <div> 要素)の中に、 <p> 要素を追加します。 <p> の文字列は createRow 関数の引数です。 jQuery を使って <p> タグを作って、 <div> 要素の中に 付け足し ています。

Ajaxが成功したときのハンドラは、console.log から createRow 関数に差し替えました(22, 23行目)。

ブラウザで http://127.0.0.1:8000/ を開くと、オウム返しするチャットボットとやり取りできます!!

最終形

chat/home.html は最終的には以下のようになります。 変更箇所を優先して示したため、一部のタグ(<title> など)は省略しています。

templates/chat/home.html
 1  <head>
 2    <style>
 3      .log-window {
 4        max-width: 500px;
 5        max-height: 300px;
 6        overflow-y: scroll;
 7      }
 8    </style>
 9  </head>
10  <body>
11    <h1>Talk with chatbot</h1>
12    <div id="chat-log" class="log-window"></div>
13
14    <form method="POST" action="{% url 'chat:bot_response' %}">
15      {% csrf_token %} {{ form.as_p }}
16      <button type="submit">Send</button>
17    </form>
18
19    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
20    <script>
21      const chatLog = $("#chat-log");
22
23      function createRow(text) {
24        const row = $("<p></p>");
25        row.text(text);
26        chatLog.append(row);
27      }
28
29      $("form").submit(function (event) {
30        event.preventDefault();
31        const form = $(this);
32
33        $.ajax({
34          url: form.prop("action"),
35          type: form.prop("method"),
36          data: form.serialize(),
37          dataType: "text",
38        })
39          .done(function (statement) {
40            const input = $("#id_message");
41            createRow("You -> " + input.val());
42            createRow("bot: " + statement);
43            input.val("");
44            chatLog[0].scrollTop = chatLog[0].scrollHeight;
45          })
46          .fail(function () {
47            window.alert("もう一度やり直してください");
48          });
49      });
50    </script>
51  </body>

(1)フォームの上のチャット履歴表示部分は、高さを決めて自動でスクロールするようにしました。 12行目でclassを設定し、 <style> タグで最大の高さと、それを超えたときにスクロールするように設定します(2〜8行目)。 Ajax通信が成功したときのイベントハンドラに44行目を追加し、チャット履歴は常に一番下までスクロールした状態(=最新のメッセージとそれへのボットの応答が見える状態)としています。

(2)Ajax通信が失敗するときもあるので、 fail イベントのハンドラを登録しました(46〜48行目)。

以上で、単純なチャットボットとやり取りするWebアプリが手元のPCで動くようになりました! このあとはチャットボットをより賢くしていきます (初回で導入した chatterbot ライブラリを使います)。 Djangoについてはあまり触らず、チャットボットを賢くすることにフォーカスしていきます。