天気予報を教えてくれる対話システム

# はじめに
この記事は、[SLP KBIT AdventCalendar2022](https://adventar.org/calendars/7569)  11日目の記事です。
今回は「Pythonでつくる対話システム」を参考に天気予報を教えてくれるタスク指向型対話システムを解説していきます。

# タスク指向型対話システムとは
タスク指向型対話システムは、対話によって特定のタスクを遂行する対話システムの事です。身近な例としてAlexaのようなAIスピーカ、宿泊施設の予約まで色々存在します。今回作成する天気予報のシステムはユーザが場所、日付を書き込み、それに対する返答として条件に一致する天気予報を返答します。

# 作成方針
①SCXML(State Chart XML)を用いて状態遷移のプログラムを書きます。これによって日付の指定、場所の指定といった状態を作成します。
②それぞれの状態で受け取った情報をもとに天気予報のサイトから自分の欲しい天気の情報を引き出して表示します。（この際はAPIを用いて情報を持ってきます）

# SCXMLで状態遷移を表すプログラム
SCXMLは状態遷移についてプログラムで表すことができる言語です。今回はユーザから場所や日程の情報を聞く状態、それらの情報をもとに探した天気予報を返却する状態という3つの状態を作成します。コードは下記のようになります。
```states.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">		//状態1	      
    <transition event="place" target="ask_date"/>
  </state>
  <state id="ask_date">			//状態2      
    <transition event="date" target="tell_info"/>
  </state>
  <final id="tell_info"/>       //状態3
</scxml>
```
上記のプログラムではユーザから天気を知りたい県名を聞く(状態1)、ユーザから日付を聞く(状態2)、ユーザに結果を返す(状態3)という状態を作成しました。
因みにこの状態遷移図は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2350040/8fad45bf-a82c-b933-96bb-5801389557e1.png)

# 状態遷移を用いたプログラム
先ほど作成したSCXMLのプログラムを使用して天気予報を出力する対話システムを作成します。この対話システムではユーザが入力した情報をもとに外部APIにアクセスして、条件に一致する情報を返却します。まず対話システムの手順としては以下のようになります。

①SCXMLのプログラムを読み込み、状態1にする。
②ユーザに入力を促し、入力後に状態を遷移させる。
③すべての情報をユーザが入力し終えると、外部APIにアクセスして入力情報をもとに天気予報を返却する。

まず、状態遷移をするプログラムだけ作成します。(外部AIPはアクセスしない)
コードは下記のようになります。
```weather.py
import sys
from PySide2 import QtCore, QtScxml

# 都道府県名のリスト
prefs = ['三重', '京都', '佐賀', '兵庫', '北海道', '千葉', '和歌山', '埼玉', '大分',
         '大阪', '奈良', '宮城', '宮崎', '富山', '山口', '山形', '山梨', '岐阜', '岡山',
         '岩手', '島根', '広島', '徳島', '愛媛', '愛知', '新潟', '東京',
         '栃木', '沖縄', '滋賀', '熊本', '石川', '神奈川', '福井', '福岡', '福島', '秋田',
         '群馬', '茨城', '長崎', '長野', '青森', '静岡', '香川', '高知', '鳥取', '鹿児島']

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

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

# Qtのおまじない
app = QtCore.QCoreApplication()
el  = QtCore.QEventLoop()

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

# 状態1に遷移
sm.start()
el.processEvents()

# システムプロンプト
print("SYS> 天気予報をお知らせします")

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

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

# 初期状態に紐づいたシステム発話の取得と出力
sysutt = uttdic[current_state]
print("SYS>", sysutt)

# ユーザ入力の処理
while True:
    text = input("> ")

    # ユーザ入力を用いて状態遷移
    if current_state == "ask_place":
        place = get_place(text)
        if place != "":
            sm.submitEvent("place")
            el.processEvents()
    elif current_state == "ask_date":
        date = get_date(text)
        if date != "":
            sm.submitEvent("date")
            el.processEvents()                

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

    # 遷移先がtell_infoの場合は情報を伝えて終了
    if current_state == "tell_info":
        print("天気をお伝えします")
        break
    else:
        # その他の遷移先の場合は状態に紐づいたシステム発話を生成
        sysutt = uttdic[current_state]
        print("SYS>", sysutt)           

print("終了します。お疲れさまでした")       

```
最初にある都道府県のリストは状態1において場所の入力をした際に関数get_placeでこのリスト内の要素と一致するものがあるか確認するためにあります。同様に、日程でも今日や明日といった文字列が入っているか判定するための関数get_dateがあります。
また、SCXMLファイルを読み込むこむと状態1に遷移させます。この際に辞書型のuttdicでキーが現在の状態名と一致している値を出力します。これによってユーザに入力を促します。もしユーザの入力が想定外の物であった場合はもう一度入力をするようループさせます。そしてユーザの入力をすべて終えて最後の状態まで行くとプログラムを終了します。

実行結果は以下のようになります。
状態遷移が行われているのが確認できますね!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2350040/d4cb869a-dd93-3bff-a87d-429a7eb295df.png)

# 外部APIにアクセスして実際の天気予報を出力する
先ほどのプログラムでは状態遷移は確認できましたが天気予報は返ってきませんでした。そこで天気予報が返ってくるように外部APIを使用してプログラムを改良します。(ここでは追加機能の紹介はしますが、機能を組み込んだプログラムコード全体は割愛します)
今回はOpenWeatherMapのAPIを使います。OpenWeatherMapは全世界の気温、降水量、降水確率、湿度といった様々な気象データを取得できるサービスのことです。ユーザ登録をするとAPI Key(以下APPIDとする)を取得でき、以下の様に入力するとjson形式のデータを得ることができます。
(http\://api.openweathermap.org/data/2.5/weather?地名の緯度と経度&lang=ja&units=metric&APPID=自分のAPPID)

画像は札幌市の気象データで、今回必要な天気情報はdescriptionの"曇りがち"だけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2350040/c1544806-488b-edff-1f2c-aab61bd70d40.png)
ここまでの流れを見ると、ユーザの選択した県名をもとに自動的にこのjson形式のファイルから天気の情報だけ抜き出せば表示できそうです。その際に必要となるのは県名の緯度と経度ですが、これは事前に辞書型にしてそれぞれの県名と結び付けておくことで県名を打ち込むだけで参照できるようにします。(例：latlondic = {'北海道':(43.06, 141.35)}これを47都道府県するが多すぎるので略)
プログラムに追加する機能としては以下のようなものがあります。

① 先ほどのURLを自動的に入力した県に合わせてくれる処理
② 今日の時間と明日の時間を入力する処理

②の処理の際、今日の時間は特に指定する必要はないが、明日の時間は今日に1日足した時間を入力すればよい。(ここでは時間の処理は割愛させてもらう)
①を考えるとURLを自動的に入力した県に合わせる処理は以下のようになります。
```add_func.py
current_weather_url = 'http://api.openweathermap.org/data/2.5/weather'
appid = '' # 自身のAPPID

def get_current_weather(lat,lon): //lat,lonは県名がキーである辞書型の値(緯度,経度)
    # 天気情報を取得    
    response = requests.get("{}?lat={}&lon={}&lang=ja&units=metric&APPID={}".format(current_weather_url,lat,lon,appid))
    return response.json()
```
この処理ではjson形式のファイルを自動的に得られるように、URLの先頭部や緯度や経度、APPIDを変数として保持してそれぞれの条件に適するようにしています。

# 天気予報を出力してみる
外部APIを取得するために必要な機能を先ほど紹介しましたが、実際にこれらの機能を組み込んだプログラムを実行してみると以下のようになりました。しっかりと天気予報ができてますね!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2350040/6725ed34-f269-e8e4-fcc2-0d25f76e5351.png)

# おわりに
対話システムについて興味はありましたが、今まで理論だけやっていたので実際に対話システムを作ってみて楽しかったです。また、外部APIとかも触ってみて面白そうだと感じたので長期休みにでも触ってみます。

# 参考文献
・『Pythonで作る対話システム』(オーム社)