こんにちは、@0yanです。
Python+Heroku PostgresでLINE WORKS トークBotのトークスクリプトを管理(表示・作成・更新・削除)するWebアプリを作りました。
初めてのWebアプリ作成だったので相当苦労しました・・・。次回つくる際の備忘録兼同じように社内でトークBotを導入したい方の参考になればと思い、投稿することにしました。
想定する読者
- Python初心者で、データベースを使ったWebアプリを作ってみたい方
- LINE WORKS トークBotのトーク内容を自分以外に管理させたい方
- 自分でDBサーバーたてるのはしんどい、Heroku Postgresに興味があるという方
Webアプリをつくった背景
当社のバックオフィス部門には経理財務課、総務課、そして私が所属する人事課がありますが、バックオフィスにいると当然ですが、**社員からの問い合わせが毎日のように発生します。**そしてその度、業務を中断するので集中力も途切れ、非常に生産性が低いな・・・と感じておりました。**特に内線。**あれは本当に廃止したい。強制的に出させられるので本当に不愉快。
ということで、少しでも問い合わせを減らすためにLINE WORKSでトークBotを作ろうと思ったのですが、質疑応答のトークスクリプトを登録できるのが自分だけだと、現場の問い合わせ対応は減っても依頼されたトークスクリプトを登録するという業務が発生します。工数減りません。
なので、Heroku Postgres(以下、DB)を使用し、DBにトークスクリプトを登録・更新・削除するための管理用インターフェースとして、初めてWebアプリを作りました。
環境
- Windows 10 Home Edition
- Python 3.7.3(Anaconda3をインストール)
- Heroku Postgres
ライブラリ
- Flask 1.1.1
- gunicorn 19.9.0
- lineworks 0.0.5
- SQLAlchemy 1.3.7
前提条件
- Herokuのアカウント取得済&GitHubからのデプロイ方法を理解している
- LINE WORKSのAPI情報取得済
完成イメージ
まだプロトタイプなのでCSSは殆ど手をつけておりませんが、Webアプリ側の完成イメージは下図のような感じです。
運用の際は、他のメンバーにこのアプリへのログイン権限を付与し、適時、トークスクリプトを作成・更新・削除して貰う予定です。
なお、LINE WORKSでBotに話しかけると下図のような挙動をします。
ゆくゆくは自然言語処理できるAIも組み合わせて、「●●について」という問いかけでなくても返信できるようにしたいです。
完成までの大まかな流れ
実際には試行錯誤しながらなので順番前後しましたが、ざっくり下記のとおりです。
- Herokuアプリを作成、Heroku Postgresをadd on
- models.pyの作成
- views.pyと対応するテンプレート(html)の作成
- GitHub経由でHerokuにデプロイ
- テーブル作成
- BotのコールバックURLを指定&テスト
1.Herokuアプリを作成、Heroku Postgresをadd on
Herokuアプリの作成については、過去記事をご参照頂ければと思います。
なお、この際、過去記事同様にLINE WORKS API情報も設定しておいて頂ければと思います。
アプリ作成後はHeroku Postgresをadd onしますが、その方法は@mickie895さんの以下の記事が参考になりましたのでご紹介致します。
なお、Heroku Postgresのadd onができると、環境変数にDATABASE_URL
が自動設定されます。
2.models.pyの作成
SQLAlchemyにおけるテーブルは、Baseクラスを継承したクラスで定義します。
# coding: utf-8
import sqlalchemy
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Talk(Base):
__tablename__ = 'talks'
check_box = sqlalchemy.Column(sqlalchemy.Boolean, default=False)
talk_id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True, autoincrement=True)
receive_msg = sqlalchemy.Column(sqlalchemy.String(100), nullable=False)
reply_msg = sqlalchemy.Column(sqlalchemy.String(1000), nullable=False)
check_boxは削除するレコードを選択するために用意しました。
talk_idはレコードを一意に識別するための主キー。
receive_msgはBotが受信するメッセージ、reply_msgはBotに返信させたいメッセージです。
3.views.pyと対応するテンプレート(html)の作成
いよいよWebアプリのView部分をつくっていきます。
まず、views.pyです。各メソッドが何をしているかは、メソッド説明文に記述しましたので、コードをご覧ください。
# coding: utf-8
import os
from flask import Flask, abort, redirect, render_template, request, url_for
from lineworks.talkbot_api import TalkBotApi
from sqlalchemy.orm import sessionmaker
from models import *
app = Flask(__name__)
# DATABASE_URL、LINE WORKS API情報はHerokuの環境変数に設定
engine = sqlalchemy.create_engine(os.environ.get('DATABASE_URL'), echo=True)
talkbot = TalkBotApi(
api_id=os.environ.get('API_ID'),
private_key=os.environ.get('PRIVATE_KEY'),
server_api_consumer_key=os.environ.get('SERVER_API_CONSUMER_KEY'),
server_id=os.environ.get('SERVER_ID'),
bot_no=os.environ.get('BOT_NO'),
domain_id=os.environ.get('DOMAIN_ID'),
account_id=os.environ.get('ACCOUNT_ID'),
room_id=os.environ.get('ROOM_ID')
)
@app.route('/create_table', methods=['GET'])
def create_table():
'''
models.pyのクラスのうち、一度も作成していないテーブルを作成します。
:return: ステータスコード200
'''
Base.metadata.create_all(bind=engine)
return '', 200
@app.route('/delete_table', methods=['DELETE'])
def delete_table():
'''
talksテーブルを削除します。
Curlコマンドで削除することを想定しています。
:return: ステータスコード200
'''
if request.method == 'DELETE':
sql = 'DROP TABLE talks;'
engine.execute(sql)
return '', 200
else:
return abort(405)
@app.route('/', methods=['GET', 'POST'])
def index():
'''
本記事では紹介しませんが、トークスクリプト一覧ページの手前にログイン画面を表示しようと思っております。
現在はプロトタイプのため、/authenticationにID・PWをPOSTする簡単なものになっておりますが、
本番運用するときにはFlask-loginを使いたいなぁと考えております。
:return: index.htmlのレンダリング結果
'''
title = 'Login'
if request.method == 'GET':
msg = ''
return render_template('index.html', title=title, msg=msg)
elif request.method == 'POST':
msg = 'ログインIDまたはパスワードが間違っています。'
return render_template('index.html', title=title, msg=msg)
else:
return abort(405)
@app.route('/authentication', methods=['POST'])
def authentication():
'''
ログインページから遷移してくるページです。
Herokuの環境変数に設定されたLOGIN_IDとPASSWORDと、ログインページからPOSTされたものが一致するか確認します。
一致した場合はトークスクリプト一覧ページに、一致しなかった場合はログインページに転送します。
:return: トークスクリプト一覧ページまたはログインページに転送
'''
if request.method == 'POST':
LOGIN_ID = os.environ.get('LOGIN_ID')
PASSWORD = os.environ.get('PASSWORD')
if request.form['login_id'] == LOGIN_ID and request.form['password'] == PASSWORD:
return redirect(url_for('talkscript'))
else:
return redirect(url_for('index'))
else:
return abort(405)
@app.route('/talkscript', methods=['GET'])
def talkscript():
'''
トークスクリプト一覧ページです。
Heroku Postgresに登録されているトークスクリプトを表示します。
作成ボタンを押下するとトークスクリプト作成画面に遷移します。
削除したいトーク内容にチェックを入れて削除ボタンを押下すると、当該トークが削除されます。
更新したいトーク内容のIDをクリックすると、トークスクリプト更新画面に遷移します。
認証に失敗した場合、ログインページに転送します。
:return: talkscript.htmlのレンダリング結果 または ログインページに転送
'''
if request.method == 'GET':
Session = sessionmaker(bind=engine)
session = Session()
title = 'トーク一覧'
talks = session.query(Talk).order_by(Talk.talk_id).all()
session.close()
return render_template('talkscript.html', title=title, talks=talks)
else:
return abort(405)
@app.route('/talkscript/create', methods=['GET'])
def create_view():
'''
トークスクリプト一覧ページから遷移してくるページです。
トークスクリプトの作成ページを表示します。
:return: create.htmlのレンダリング結果
'''
if request.method == 'GET':
title = 'トークの作成'
return render_template('create.html', title=title)
else:
return abort(405)
@app.route('/talkscript/update', methods=['GET'])
def update_view():
'''
トークスクリプト一覧ページから遷移してくるページです。
トークスクリプトの更新ページを表示します。
update_idはトークスクリプト一覧ページで押下したトークのID(talk_id)です。
DBに登録されているレコード(トーク)のうち、talk_id = update_idとなるレコードをupdate.htmlに渡します。
:return: update.htmlのレンダリング結果
'''
if request.method == 'GET':
Session = sessionmaker(bind=engine)
session = Session()
title = 'トークの更新'
update_id = request.args.get('talk_id')
talks = session.query(Talk).filter(Talk.talk_id == update_id).all()
session.close()
return render_template('update.html', title=title, update_id=update_id, talks=talks)
else:
return abort(405)
@app.route('/create_script', methods=['POST'])
def create_script():
'''
トークスクリプト作成ページから遷移してくるページです。
トークスクリプトを作成後、トークスクリプト一覧ページに転送します。
:return: トークスクリプト一覧ページに転送
'''
if request.method == 'POST':
Session = sessionmaker(bind=engine)
session = Session()
talk = Talk()
talk.receive_msg = request.form['receive_msg']
talk.reply_msg = request.form['reply_msg']
session.add(talk)
session.commit()
session.close()
return redirect(url_for('talkscript'))
else:
return abort(405)
@app.route('/update_script', methods=['POST'])
def update_script():
'''
トークスクリプト更新ページから遷移してくるページです。
トークスクリプトを更新後、トークスクリプト一覧ページに転送します。
:return: トークスクリプト一覧ページに転送
'''
if request.method == 'POST':
Session = sessionmaker(bind=engine)
session = Session()
talk = session.query(Talk).filter(Talk.talk_id == request.form['update_id']).first()
talk.receive_msg = request.form['receive_msg']
talk.reply_msg = request.form['reply_msg']
session.commit()
session.close()
return redirect(url_for('talkscript'))
else:
return abort(405)
@app.route('/delete_script', methods=['POST'])
def delete_script():
'''
トークスクリプト一覧ページで選択したレコード(トーク)を削除後、トークスクリプト一覧ページに転送します。
:return: トークスクリプト一覧ページに転送
'''
if request.method == 'POST':
Session = sessionmaker(bind=engine)
session = Session()
selected_ids = request.form.getlist('selected')
for selected_id in selected_ids:
print(selected_id)
session.query(Talk).filter(Talk.talk_id == selected_id).delete()
session.commit()
session.close()
return redirect(url_for('talkscript'))
else:
return abort(405)
@app.route('/callback', methods=['POST'])
def callback():
'''
本URLはLINE WORKSのトークBotのコールバックURLに設定されており、「●●について」という形式で問い合わせが来ると、
「●●」の部分とreceive_msgと一致するレコードがあるか、トークスクリプトから検索します。
一致するレコードがある場合は、それに対応するreply_msgを返信します。
一致しない場合は問い合わせ形式が「●●について」になっているか確認するよう促すメッセージを返信します。
なお、LINE WORKSからのPOSTリクエストでない場合はステータスコード400を返します。
:return: ステータスコード200, 400, 405のいずれか
'''
if request.method == 'POST':
body = request.json
if body['type'] == 'message':
try:
Session = sessionmaker(bind=engine)
session = Session()
# 「●●について」と質問が来るが、DB登録値は「●●」なので後ろ4文字削除
row_msg = str(body['content']['text'])
receive_msg = row_msg[:-4]
reply_msg_list = session.query(Talk.reply_msg).filter(Talk.receive_msg == receive_msg).first()
reply_msg = str(reply_msg_list[0])
talkbot.send_text_message(send_text=reply_msg)
return '', 200
except:
error_msg = ('問い合わせの仕方は「●●について」でお願いします。'
'問い合わせ方法が合っていても、この返信の場合はすみません、わかりません。')
talkbot.send_text_message(send_text=error_msg)
return '', 200
else:
return abort(400)
else:
return abort(405)
if __name__ == '__main__':
app.run()
次はテンプレートです。layout.htmlを作り、それを拡張する形で作りました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<link rel="stylesheet" type="text/css" href="..\static\css\style.css">
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
{% extends 'layout.html' %}
{% block content %}
<h3>トーク一覧</h3>
<button><a href="/talkscript/create">トークの作成</a></button>
<form action="/delete_script" method="post">
<table>
<tr>
<th>□</th>
<th>ID</th>
<th>受信メッセージ</th>
<th>返信メッセージ</th>
</tr>
{% for talk in talks %}
<tr>
<td>{% if talk.check_bok == True %}
<input type="checkbox" value="{{talk.talk_id}}" name="selected" checked>
{% else %}
<input type="checkbox" value="{{talk.talk_id}}" name="selected">
{% endif %}</td>
<td><a href="/talkscript/update?talk_id={{talk.talk_id}}">{{talk.talk_id}}</a></td>
<td>{{talk.receive_msg}}</td>
<td>{{talk.reply_msg}}</td>
</tr>
{% endfor %}
</table>
<input type="submit" value="トークの削除">
</form>
{% endblock %}
{% extends 'layout.html' %}
{% block content %}
<h3>{{title}}</h3>
<p>作成したいトーク内容を入力してください。</p>
<form action="/create_script" method="POST">
<label for="receive_msg">受信メッセージ: </label>
<input id="receive_msg" type="text" name="receive_msg">
<label for="reply_msg">返信メッセージ: </label>
<input id="reply_msg" type="text" name="reply_msg">
<input type="submit" value="作成">
</form>
{% endblock %}
{% extends 'layout.html' %}
{% block content %}
<h3>{{title}}</h3>
<p>トークの更新内容を入力してください。</p>
<p>【更新前】</p>
<table>
<tr>
<th>受信メッセージ</th>
<th>返信メッセージ</th>
</tr>
{% for talk in talks %}
<tr>
<td>{{talk.receive_msg}}</td>
<td>{{talk.reply_msg}}</td>
</tr>
{% endfor %}
</table>
<br>
<p>【更新後】</p>
<form action="/update_script" method="POST">
<table>
<tr>
<th>受信メッセージ</th>
<th>返信メッセージ</th>
</tr>
<tr>
<td><input id="receive_msg" type="text" name="receive_msg"></td>
<td><input id="reply_msg" type="text" name="reply_msg"></td>
</tr>
</table>
<input id="update_id" type="hidden" value="{{update_id}}" name="update_id">
<input type="submit" value="更新">
</form>
{% endblock %}
4. GitHub経由でHerokuにデプロイ
以下の記事を参考にして頂ければと思います。
【備忘録】GitHub経由でHerokuにデプロイするまでの流れ
5. テーブル作成
デプロイ成功後、https://{アプリ名}.herokuapp.com/create_table
にアクセスすることでテーブルが作成されます。
6. BotのコールバックURLを指定&テスト
LINE WORKS Developer Console > Bot画面で、BotのコールバックURLを指定します。指定するURLはhttps://{アプリ名}.herokuapp.com/callback
です。
設定後、DBのreceive_msgフィールドに登録したワード+「について」をBotに送信し、完成イメージのような返信が返ってくれば完成です!
さいごに
長文となりましたが、閲覧頂きありがとうございました。この記事が誰かの役に立てば幸いです。
その他参考にした記事
本Webアプリ開発にあたり、ハマったところと助けて貰った参考記事をご紹介致します。
SQLAlchemyの使い方
公式ドキュメントは当然英語。読みたくないなぁ・・・と思っていた自分を救ってくれたのは@ckernさんの記事でした。
大変分かり易かったです!
Flaskのリダイレクトでエラーになる
url_forに渡すのは、routeデコレーターの引数に指定している値ではなく、メソッド名であることを知らず、四苦八苦していました。
間違えに気付かせてくださったのは以下の記事です。
CSSの変更が反映されない
Google Chromeにキャッシュが残っており、そちらが表示されていただけでした。
それに気づかせてくださったのは以下の記事です。
複数レコードの一括削除の実装方法がわからない
レコード一覧で、チェックボックスによる複数選択&一括削除をするWebアプリはよく見るので、自分もそれを使いたいと思っておりました。
が、どうやれば良いのだろう・・・という状態を救ってくれたのは以下の記事でした。
レコード毎に違うページに遷移する方法がわからない
データベースのレコード更新にあたり、レコード選択したらそのトーク内容を表示する画面を作りたかったものの、そのやり方がわかりませんでした。
それを助けてくれたのは以下の記事です。
aタグでGETする際にパラメータ渡す方法をとりました。
Python Flask URLからパラメータを取得する方法?
以上、参考にした記事でした。
記事を書いてくださった皆様、ありがとうございました!