「pythonで作る対話システム」を参考にlinebot作成を試みて失敗したプログラム

#12/26できた
<a href="https://qiita.com/shinoshishino/items/0d6ff037e90d787cf4b6">著書「python で作る対話システム」の天気予報ボットを参考に公式ラインに天気予報させてみた。</a>
#状態遷移の情報を保持できずに未完成に終わった天気予報のプログラム
下記のプログラムを実行したところ、状態遷移がうまく進まなかった。
linebot＞　地名を入力してください

ユーザー＞　奈良県

linebot＞　日付を入力してください

ユーザー＞　今日

linebot＞　地名を入力してください

#考えた事
地名を送ることで一度ask_placeからask_dateまで遷移していることから
・遷移情報を保持できていない
・ユーザーからメッセージを受け取るたびに遷移情報を初期化している
のどちらかである可能性がある。

#試したこと
weather_system1.pyではreplyメソッドを呼び出すたびにsm.start()で初期状態に遷移していたためsm.activeStateNames()が空であるときに初期状態に遷移するようにした。

```python
    def reply(self, push_text):
         sm  = QtScxml.QScxmlStateMachine.fromFile('states.scxml')
         if sm.activeStateNames() == []:
             sm.start()
             self.el.processEvents() 
         else:
             text = push_text
             self.sessiondic = {"statemachine":sm, "place":"", "date":"", "type":""}
             current_state = sm.activeStateNames()[0]
             print("current_state=", current_state)....... 
```

#結果
変化なし。

#わからないこと
会話を通して状態遷移をする方法。
python-telegram-botのdispatcherやidleに当たるものがline-bot-sdkにあるかどうか。
(一応探したけど英語は苦手だから見逃してるかもしれん）

#プログラムたち




```python:weather_system1.py
import sys
from PySide2 import QtCore, QtScxml
import requests
import json
from datetime import datetime, timedelta, time


class WeatherSystem:

    # 都道府県名のリスト
    prefs = ['三重', '京都', '佐賀', '兵庫', '北海道', '千葉', '和歌山', '埼玉', '大分',
             '大阪', '奈良', '宮城', '宮崎', '富山', '山口', '山形', '山梨', '岐阜', '岡山',
             '岩手', '島根', '広島', '徳島', '愛媛', '愛知', '新潟', '東京',
             '栃木', '沖縄', '滋賀', '熊本', '石川', '神奈川', '福井', '福岡', '福島', '秋田',
             '群馬', '茨城', '長崎', '長野', '青森', '静岡', '香川', '高知', '鳥取', '鹿児島','ksjgihurhtknskhifhsignrw22']
    
    # 都道府県名から緯度と経度を取得するための辞書
    latlondic = {'北海道': (43.06, 141.35), '青森': (40.82, 140.74), '岩手': (39.7, 141.15), '宮城': (38.27, 140.87),
                 '秋田': (39.72, 140.1), '山形': (38.24, 140.36), '福島': (37.75, 140.47), '茨城': (36.34, 140.45),
                 '栃木': (36.57, 139.88), '群馬': (36.39, 139.06), '埼玉': (35.86, 139.65), '千葉': (35.61, 140.12),
                 '東京': (35.69, 139.69), '神奈川': (35.45, 139.64), '新潟': (37.9, 139.02), '富山': (36.7, 137.21),
                 '石川': (36.59, 136.63), '福井': (36.07, 136.22), '山梨': (35.66, 138.57), '長野': (36.65, 138.18),
                 '岐阜': (35.39, 136.72), '静岡': (34.98, 138.38), '愛知': (35.18, 136.91), '三重': (34.73, 136.51),
                 '滋賀': (35.0, 135.87), '京都': (35.02, 135.76), '大阪': (34.69, 135.52), '兵庫': (34.69, 135.18),
                 '奈良': (34.69, 135.83), '和歌山': (34.23, 135.17), '鳥取': (35.5, 134.24), '島根': (35.47, 133.05),
                 '岡山': (34.66, 133.93), '広島': (34.4, 132.46), '山口': (34.19, 131.47), '徳島': (34.07, 134.56),
                 '香川': (34.34, 134.04), '愛媛': (33.84, 132.77), '高知': (33.56, 133.53), '福岡': (33.61, 130.42),
                 '佐賀': (33.25, 130.3), '長崎': (32.74, 129.87), '熊本': (32.79, 130.74), '大分': (33.24, 131.61),
                 '宮崎': (31.91, 131.42), '鹿児島': (31.56, 130.56), '沖縄': (26.21, 127.68)}

    # 状態とシステム発話を紐づけた辞書
    uttdic = {"ask_place": "地名を言ってください",
              "ask_date": "日付を言ってください",
              "ask_type": "情報種別を言ってください"}    

    current_weather_url = 'http://api.openweathermap.org/data/2.5/weather'
    forecast_url = 'http://api.openweathermap.org/data/2.5/forecast'
    appid = '6dbf61393fba9e88099d19dcdafc6c25' # 自身のAPPIDを入れてください    
    

    def __init__(self):
        # Qtに関するおまじない
        app = QtCore.QCoreApplication()

        # 対話セッションを管理するための辞書
        self.sessiondic = {}

       

        
    
    # テキストから都道府県名を抽出する関数．見つからない場合は空文字を返す．
    def get_place(self, text):
        for pref in self.prefs:
            if pref in text:
                print(pref)
                return pref
            elif pref == 'ksjgihurhtknskhifhsignrw22':
                return ""
            else:
                continue

    # テキストに「今日」もしくは「明日」があればそれを返す．見つからない場合は空文字を返す．
    def get_date(self, text):
        if "今日" in text:
            return "今日"
        elif "明日" in text:
            return "明日"
        else:
            return ""

    # テキストに「天気」もしくは「気温」があればそれを返す．見つからない場合は空文字を返す．    
    def get_type(self, text):
        if "天気" in text:
            return "天気"
        elif "気温" in text:
            return "気温"
        else:
            return ""

    def get_current_weather(self, lat,lon):
        # 天気情報を取得    
        response = requests.get("{}?lat={}&lon={}&lang=ja&units=metric&APPID={}".format(self.current_weather_url,lat,lon,self.appid))
        return response.json()

    def get_tomorrow_weather(self, lat,lon):
        # 今日の時間を取得
        today = datetime.today()
        # 明日の時間を取得
        tomorrow = today + timedelta(days=1)
        # 明日の正午の時間を取得
        tomorrow_noon = datetime.combine(tomorrow, time(12,0))
        # UNIX時間に変換
        timestamp = tomorrow_noon.timestamp()
        # 天気情報を取得
        response = requests.get("{}?lat={}&lon={}&lang=ja&units=metric&APPID={}".format(self.forecast_url,lat,lon,self.appid))
        dic = response.json()
        # 3時間おきの天気情報についてループ
        for i in range(len(dic["list"])):
            # i番目の天気情報（UNIX時間）
            dt = float(dic["list"][i]["dt"])
            # 明日の正午以降のデータになった時点でその天気情報を返す
            if dt >= timestamp:
                return dic["list"][i]
        return ""

    """def initial_message(self):
        self.el  = QtCore.QEventLoop()        

        # SCXMLファイルの読み込み
        sm  = QtScxml.QScxmlStateMachine.fromFile('states.scxml')

        # セッションIDとセッションに関連する情報を格納した辞書
        self.sessiondic = {"statemachine":sm, "place":"", "date":"", "type":""}

        # 初期状態に遷移
        sm.start()
        self.el.processEvents()

        # 初期状態の取得
        current_state = sm.activeStateNames()[0]
        print("current_state=", current_state)

        # 初期状態に紐づいたシステム発話の取得と出力
        sysutt = self.uttdic[current_state]

        return {"utt":"こちらは天気情報案内システムです。" + sysutt, "end":False}"""


    def reply(self, push_text):
        self.el  = QtCore.QEventLoop()        

        # SCXMLファイルの読み込み
        sm  = QtScxml.QScxmlStateMachine.fromFile('states.scxml')

        # セッションIDとセッションに関連する情報を格納した辞書
        self.sessiondic = {"statemachine":sm, "place":"", "date":"", "type":""}

        # 初期状態に遷移
        sm.start()
        self.el.processEvents()
        print('ここまでイケてるよ')
        text = push_text
        self.sessiondic = {"statemachine":sm, "place":"", "date":"", "type":""}
        current_state = sm.activeStateNames()[0]
        print("current_state=", current_state)        

        # ユーザ入力を用いて状態遷移
        if current_state == "ask_place":
            place = self.get_place(text)
            if place != "":
                sm.submitEvent("place")
                self.el.processEvents()
                self.sessiondic["place"] = place
                print('ほほほ')
                print("current_state=", current_state)
        elif current_state == "ask_date":
            date = self.get_date(text)
            if date != "":
                sm.submitEvent("date")
                self.el.processEvents()
                self.sessiondic["date"] = date
                print('へへへ')
                print("current_state=", current_state)
        elif current_state == "ask_type":
            _type = self.get_type(text)
            if _type != "":
                sm.submitEvent("type")
                self.el.processEvents()
                self.sessiondic["type"] = _type

        # 遷移先の状態を取得
        current_state = sm.activeStateNames()[0]
        print("current_state=", current_state)

        # 遷移先がtell_infoの場合は情報を伝えて終了
        if current_state == "tell_info":
            utts = []
            utts.append("お伝えします")
            place = self.sessiondic["place"]
            date = self.sessiondic["date"]
            _type = self.sessiondic["type"]

            lat = self.latlondic[place][0] # placeから緯度を取得
            lon = self.latlondic[place][1] # placeから経度を取得       
            print("lat=",lat,"lon=",lon)
            if date == "今日":
                cw = self.get_current_weather(lat,lon)
                if _type == "天気":
                    utts.append(cw["weather"][0]["description"]+"です")
                elif _type == "気温":
                    utts.append(str(cw["main"]["temp"])+"度です")
            elif date == "明日":
                tw = self.get_tomorrow_weather(lat,lon)
                if _type == "天気":
                    utts.append(tw["weather"][0]["description"]+"です")
                elif _type == "気温":
                    utts.append(str(tw["main"]["temp"])+"度です")
            utts.append("ご利用ありがとうございました")
            del self.sessiondic
            return {"utt":"。".join(utts), "end": True}

        else:
            # その他の遷移先の場合は状態に紐づいたシステム発話を生成
            sysutt = self.uttdic[current_state]
            return {'utt':sysutt,'end':False}
```

```python:main.py
from flask import Flask,request,abort
from linebot import LineBotApi,WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent,TextMessage,TextSendMessage
import os
import requests
import pprint
from weather_system1 import WeatherSystem
import weather_system1

app=Flask(__name__)
#環境変数の取得
YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]
line_bot_api=LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler=WebhookHandler(YOUR_CHANNEL_SECRET)
weather = WeatherSystem()

@app.route("/callback",methods=["POST"])
def callback():
    print("コールバック1")
    signature=request.headers["X-Line-Signature"]
    print(signature)
    body=request.get_data(as_text=True)
    app.logger.info("Request body"+body)

    try:
        handler.handle(body,signature)
    except InvalidSignatureError:
        abort(400)
    print('コールバック')
    return "OK"


@handler.add(MessageEvent,message=TextMessage)
def handle_message(event):
    print('ハンドルメッセージ')
    #入力された文字列を格納
    push_text = event.message.text
    #weather_system1.pyにあるWeather_systemクラスをインスタンスにする
   
    reply_text = weather.reply(push_text)["utt"]

    #リプライ部分の記述
    line_bot_api.reply_message(event.reply_token,TextSendMessage(text=reply_text))


if __name__=="__main__":
    port=int(os.getenv("PORT",5000))
    app.run(host="0.0.0.0",port=port)
```

```scxml:state.scxml
<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="ask_place">
  <state id="ask_place">			      
    <transition event="place" target="ask_date"/>
  </state>
  <state id="ask_date">			      
    <transition event="date" target="ask_type"/>
  </state>
  <state id="ask_type">			      
    <transition event="type" target="tell_info"/>
  </state>
  <final id="tell_info"/>
</scxml>
```