【ぴよぴよPython】LINEボット作った話①

<h2>はじめに</h2>
とあるWeb会社で、IBM Watson Assistantを使ってチャットボット作成していた私。
Watson Assistantは簡単にチャットボットが作れるツールですが、少し複雑なことをしようとすると面倒くさいことが多いのが玉にキズでした。

自然言語処理系のオープンソースを色々連携したかったのですが、多分watsonはwatsonで完結するような方向のプロダクトのようにみえる・・(個人的感想)

私は何より色々な技術を連携したかった。**必要なところに必要な自然言語処理技術を連携することで**、かゆいところに手が届くチャットボットになるはず。ならばもうpythonで書くことにしよう。
という感じで見よう見真似&同僚に助けを請いながら、pythonによるLINEボットの構築&実装を行った。
その備忘録をここに残す。

<h3>世に溢れるチャットボットの課題(2019年4月時点)</h3>

●入力を正しく認識することができるか(認識精度)
●認識したテキストを正しく分類することができるか（分類精度)
●context使って何かしらやることができるか(文脈やユーザカスタムが可能になる)　
●自動で何らかの学習や拡張をおこなうことができるか(分類精度のトレーニング、回答範囲の拡張、回答文生成)　
●ユーザさてぃすふぁくしょん指標の設定(「満足しましたか？-はい-いいえ」で何が測れるというのか...)
●逃げ道がうまく設定されているか(ボットで解決しない場合の連携、アフターケア)

以下は営業に関わることだけど。
●KPI、チャットボット導入する費用対効果をもっとちゃんとしたい　



<h2>LINEボットのロードマップ</h2>
以下の順番で徐々に実装していく予定です。

【目標1】1つの入力に対し1つの応答返す
【目標2】contextを保持し、分岐つくる


#環境
・Python 3.7.3
・mac バージョン10.13.2



<h3>【目標1】の完成</h3>
早速、目標１が完成。
一つの入力を投げると、応答を一つ返すLINEボットを作成した。
今回は、私自身転職活動中なので、自己紹介をするLINEボットを作成した。その名も【MIYAGI自己紹介BOT】。

<img width="433" alt="スクリーンショット 2019-05-06 17.02.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399739/05479a82-9c2f-839a-d43d-52e03b3bf751.png">

特定のentityに当たった場合は特定の応答文を返し、それ以外の入力は
ランダムで用意しておいた応答文を返すようにした。●

:hatching_chick:【MIYAGI自己紹介BOT】よろしければLINE上で試してみてください！
<img width="465" alt="スクリーンショット 2019-05-06 17.13.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399739/3ece4fda-803d-5de6-2d80-f448a97f4ae8.png">


<h4>【目標1】でやった事①Python学習</h4>
ProgateでPython入門を学習しました。https://prog-8.com/languages/python
とてもわかりやすいのでおすすめです。Ⅰ〜Ⅴまで全てやります！
それでも足りない部分は、作りながらググっていきます。

<h4>【目標1】でやった事②LINE Developers の登録</h4>
[LINE Developers](https://developers.line.biz/console/)
こちら参考にしました。https://qiita.com/nkjm/items/38808bbc97d6927837cd

<h4>【目標1】でやった事③AWSの登録</h4>
・LambdaやDynamoDBの設定を行います。DynamoDBはContext保存するために使います。
・ローカルからslsを利用して、AWSのS3にプログラムをデプロイできるようにします。


<h4>【目標1】でやった事④Pythonでスクリプト作成</h4>
以下のファイルをPythonで作成します。
ファイル１：ラインからメッセージを受けて,返答するスクリプト
ファイル２：チャットボット中身のスクリプト
ファイル３：サーバーレス設定ファイル

**ファイル１:(mibot.py)**

```
import json
import base64
import hashlib
import hmac
from introduce import hippocampus
from linebot import LineBotApi
from linebot.models import TextSendMessage
from linebot.exceptions import LineBotApiError
import boto3 
from boto3.session import Session
from boto3.dynamodb.conditions import Key

accesskey = "" #credential情報なので伏せてます
secretkey = ""　#credential情報なので伏せてます
region = "ap-northeast-1"
session = Session(aws_access_key_id=accesskey, aws_secret_access_key=secretkey, region_name=region)
dynamodb = session.resource('dynamodb')
table    = dynamodb.Table("")
channel_secret = ""　#credential情報なので伏せてます
line_bot_api = LineBotApi("")


#def hippocampus(input_text):
    #print("hippocampus", input_text)
    #return "はろー"


def talk(event, context):
    print("talk")
    body = event["body"]
    digest = hmac.new(channel_secret.encode('utf-8'), body.encode('utf-8'), hashlib.sha256).digest()
    signature = base64.b64encode(digest)
    # Compare X-Line-Signature request header and the signature
    if event["headers"]["X-Line-Signature"] == signature.decode():
        for e in json.loads(body)["events"]:
            print("event", e)
            if e["message"]["type"] == "text":
                input_text = e["messageå"]["text"]
                #context =(user_id)
                user_id = e["source"]["userId"]
                queryData = table.query(
                    KeyConditionExpression = Key("user_id").eq(user_id) 
                )
                if len(queryData["Items"]) == 0:
                    context = {"user_id": user_id}
                else:
                    context = queryData["Items"][0]
                output_text = hippocampus(input_text,context)
                response = table.put_item(Item=context)
                print (response)
                try:
                    line_bot_api.reply_message(e["replyToken"], TextSendMessage(text=output_text))
                except LineBotApiError as error:
                    print("えらーよ", error)
        response = {
            "statusCode": 200
        }
    else:
        print("しぐにちゃ不一致")
        response = {
            "statusCode": 401
        }
    return response

```

---
**ファイル2: 会話スクリプト**
・あらかじめentityに単語登録しておき、単語マッチすると用意していた応答を返すように設定します。
・正規表現も活用して、入力にある単語が含まれていれば、entityのkeyを返すようにも設定しました。
・おまけで、おみくじ機能もつけました。ランダムで結果を返します。

```

import random
import re

#入力はラインのインプット文字列。
#返却はentity文字列(key)。
#入力と辞書の値と一致するかどうかforで探索する(辞書のvalueを個別にみにいく、縦探索)
#あればvalueに対応するkeyを返す
#返した時点でforを自動的に中止される
def search_entity(text):
    bot_entities ={
        "career":
            ["^(?=.*職歴).*$","職務経歴","職務経験","仕事"],
        "edu":
            ["^(?=.*学歴).*$","学校","大学"],
        "hobby":
            ["^(?=.*趣味).*$"],
        "goodat":
            ["^(?=.*特技).*$","^(?=.*得意).*$"],
        "dream":
            ["^(?=.*夢).*$","今後したいことは","目標"],
        "greeting":
            ["こんにちは","こんばんは","こんにちわ","おはよう"]
    }
    for k,vs in bot_entities.items(): 
        for v in vs:
            #print(v)
            if text == v:
                return k 
    return None
def hippocampus(line_input,context):
    entity_key = search_entity(line_input)
    elif entity_key ==  "carreer":
        context["latest_entity_key"]=entity_key
        return ("大学卒業後は、・・・(以下略)")
    elif entity_key == "edu":
        return("大学は・・・(以下略)")
    elif entity_key == "hobby":
        return("趣味は、歌や音楽鑑賞です。🥁🎧あとは物づくりが好きです。💻🖌")
    elif entity_key == "greeting":
        return("こんにちは！😉\n\n私は【MIYAGI自己紹介BOT】です。\n本人に代わって、以下の質問にお答えします。\n\n・職歴について教えて\n・学歴について教えて\n・趣味は？\n・特技は？\n・今後の夢は？\n\nなどなど質問をお待ちしております。💁🏼✨")
    elif entity_key == "goodat":
        return ("得意な分野は、語学学習です。・・・(以下略)")
    elif entity_key == "dream":
        return("目標は、「知性」を感じさせる対話エンジンを構築することです。Qiitaにも色々書いてるので覗いてみてください！ → https://qiita.com/UNI-code")
    elif entity_key == "おみくじ":
        omikuji = ["今日の君は【大吉】：おめでとうございます！大吉の貴方は臨時収入があるでしょう。💰🤩💰","今日の君は【中吉】：まぁ、普通に良いでしょう！。普通が一番だったりするんですよねー。・・・でも普通って何？","今日の君は【凶】：まぁ・・・最底辺を経験することって大事よね。だって、あとは上にあがるだけじゃない。\n頑張ろ🙋🏻‍♀️"]
        return random.choice(omikuji)
    else:
        anything_else = ["・・・・・(やばい。その質問への回答は用意してなかった。。)\n今は「職歴」とか「学歴」「趣味」「目標」とかしか答えられません〜。。。あ、『おみくじ』なら出来ますよ😅。『おみくじ』って入力してください!","・・・・ごめんなさい,,,その質問には答えられないんですよ。\n\n急ぎの方はこちらから連絡ください！　https://qiita.com/UNI-code"]
        return random.choice(anything_else)

#以下はローカルでテストするときに使います
if __name__ == '__main__':
    import boto3 
    from boto3.session import Session
    from boto3.dynamodb.conditions import Key
    accesskey = ""
    secretkey = ""
    region = "ap-northeast-1"
    session = Session(aws_access_key_id=accesskey, aws_secret_access_key=secretkey, region_name=region)
    dynamodb = session.resource('dynamodb')
    table    = dynamodb.Table("introduce_bot")
    while True:
        user_id = ""
        queryData = table.query(
            KeyConditionExpression = Key("user_id").eq(user_id) # 取得するKey情報
        )
        
        user_message = input()
        print(user_message)
        if len(queryData["Items"]) == 0:
            context = {"user_id": user_id}
            a = hippocampus(user_message,context)
        else:
            context = queryData["Items"][0]
            a = hippocampus(user_message,context)

        print("OUTPUT", a, context)
        response = table.put_item(Item=context)
        print (response)
        #print(queryData)
        #console_input = input()
        #output_text = hippocampus(console_input)
        #print(output_text)
    
    
```
    
---
**ファイル3 サーバーレス設定ファイル(severless.yml)** 
今回、Lambda S3 APIゲートウェイを使い、デプロイにはslsを使います。その設定ファイルを作成します。

```
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!

service: linebotIntroduce # NOTE: update this with your service name

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"

provider:
  name: aws
  runtime: python3.7
# you can overwrite defaults here
#  stage: dev
  region: ap-northeast-1

# you can add statements to the Lambda function's IAM Role here
#  iamRoleStatements:
#    - Effect: "Allow"
#      Action:
#        - "s3:ListBucket"
#      Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
#    - Effect: "Allow"
#      Action:
#        - "s3:PutObject"
#      Resource:
#        Fn::Join:
#          - ""
#          - - "arn:aws:s3:::"
#            - "Ref" : "ServerlessDeploymentBucket"
#            - "/*"

# you can define service wide environment variables here
#  environment:
#    variable1: value1

# you can add packaging information here
#package:
#  include:
#    - include-me.py
#    - include-me-dir/**
#  exclude:
#    - exclude-me.py
#    - exclude-me-dir/**

functions:
  talk:
    handler: mibot2.talk
    events:
      - http:
          path: api/v1/talk
          method: post

#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
#    events:
#      - http:
#          path: users/create
#          method: get
#      - websocket: $connect
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx
#      - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp

#    Define function environment variables here
#    environment:
#      variable2: value2

# you can add CloudFormation resource templates here
#resources:
#  Resources:
#    NewResource:
#      Type: AWS::S3::Bucket
#      Properties:
#        BucketName: my-new-bucket
#  Outputs:
#     NewOutput:
#       Description: "Description for the output"
#       Value: "Some output value"

```


<h2>まとめ</h2>
今回は『1つの入力に対し1つの応答返す』LINEボットを作成しました。
難しかった・・でも慣れれば色々できそうです。
次はcontextを保存して、文脈判定や分岐のボットを作成予定です。