OpenAI×hotpepperのAPIで簡易型グルメchatbot開発

<iframe width="560" height="315" src="https://www.youtube.com/embed/6ZWhO7UZP_c?si=4_Ia14Z5lRAfR-aP" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# Intorduction
大学内でOpenAIのAPIを用いたchatbotを作ってみる機会があったので、作ってみた。

# Methods
## ディレクトリ構成
```:ディレクトリ構成
(ユーザ指定フォルダ ここでは"2505_chatbot_hutg")/
├── .env
├── main.py
├── openai_handler.py
└── hotpepper_handler.py  
```

## hotpepper APIの取得方法
[リクルートWEBサービス](https://webservice.recruit.co.jp/)から新規登録を行ってメールアドレスを登録すると、登録メールアドレスにAPI keyが届く。

## API keyを".env"で格納
プロジェクトのルートディレクトリ (2505_chatbot_hutg/) にある .env ファイルを以下のように作成する。
僕はtextファイルを１度作成して、下記のようなテキストを打ち込んで、名前変更をした。
```
OPENAI_API_KEY="ここにあなたのOpenAI APIキーを記述"
HOTPEPPER_API_KEY="ここにあなたのhotpepper APIキーを記述"
```

## 開発環境
### OpenAIモデル
"gpt-3.5-turbo"を使用

### python環境
```
openai                    1.82.1 
python                    3.9.22  
```


## openai_handler.py
```python:openai_handler.py
# openai_handler.py
import os
from openai import OpenAI
from dotenv import load_dotenv
import json

load_dotenv()

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def get_restaurant_search_params_from_text(user_input):
    """
    ユーザーのテキストからレストラン検索に必要な情報を抽出。
    ホットペッパーAPIのkeyword検索で扱いやすいように「検索キーワード」「エリア」「料理のジャンル」を抽出。
    """
    try:
        completion = client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[
                {"role": "system", "content": "あなたはユーザーの発言からレストラン検索に必要な情報を抽出するアシスタントです。抽出する情報は「検索キーワード」(料理名、店名、店の特徴など具体的な言葉)、「エリア」(地名、駅名など)、「料理のジャンル」です。該当する情報がない場合は抽出せず、その項目は空欄としてください。結果はJSON形式で返してください。例: {\"keyword\": \"個室\", \"area\": \"新宿\", \"genre\": \"焼肉\"}"},
                {"role": "user", "content": user_input}
            ],
            temperature=0, # 創造性よりも正確性を重視
            response_format={"type": "json_object"} # JSON形式での出力を指定
        )
        
        raw_content = completion.choices[0].message.content
        # print(f"Debug: OpenAI Raw Params Response: {raw_content}")
        search_params = json.loads(raw_content)
        
        # キーが存在しない場合に備えてデフォルト値を設定
        return {
            "keyword": search_params.get("keyword", ""),
            "area": search_params.get("area", ""),
            "genre": search_params.get("genre", "") # ここでは「ジャンル名」が返ることを想定
        }
    except json.JSONDecodeError as e:
        print(f"Error decoding JSON from OpenAI: {e}")
        print(f"Raw content was: {raw_content}")
        return {"keyword": user_input, "area": "", "genre": ""} # エラー時はユーザー入力をキーワードに
    except Exception as e:
        print(f"Error extracting search params from OpenAI: {e}")
        return {"keyword": user_input, "area": "", "genre": ""} # エラー時はユーザー入力をキーワードに

def generate_response_from_restaurant_info(user_input, restaurants_info):
    """
    レストラン情報リストに基づいて、ユーザーへの自然な応答文を生成
    ホットペッパーAPIから取得した情報に合わせて調整
    """
    if not restaurants_info:
        return "申し訳ありません、条件に合うレストランが見つかりませんでした。他の条件で試してみませんか？"

    prompt_message = f"ユーザーの元の質問:「{user_input}」\n\n以下のレストラン情報に基づいて、おすすめのレストランを最大3つまで紹介する形で、フレンドリーな応答文を作成してください。\nそれぞれのレストランについて、店名、アクセス、平均予算、お店の簡単な紹介(キャッチコピーなど)、そしてお店のURLを必ず含めてください。\n\nレストラン情報:\n"
    for i, r_info in enumerate(restaurants_info[:3]): # 最大3件まで
        prompt_message += f"{i+1}. 店名: {r_info.get('name', '情報なし')}\n"
        prompt_message += f"   アクセス: {r_info.get('access', '情報なし')}\n"
        prompt_message += f"   平均予算: {r_info.get('budget', '情報なし')}\n"
        prompt_message += f"   キャッチコピー: {r_info.get('catch', '情報なし')}\n" # キャッチコピーを追加
        prompt_message += f"   ジャンル: {r_info.get('genre', '情報なし')}\n" # ジャンルを追加
        prompt_message += f"   URL: {r_info.get('url', '情報なし')}\n\n"

    try:
        completion = client.chat.completions.create(
            model="gpt-3.5-turbo", # または "gpt-4" など、利用可能なモデル
            messages=[
                {"role": "system", "content": "あなたは親切でフレンドリーなレストラン紹介アシスタントです。ユーザーに役立つ情報を提供してください。"},
                {"role": "user", "content": prompt_message}
            ]
        )
        return completion.choices[0].message.content
    except Exception as e:
        print(f"Error generating response from OpenAI: {e}")
        return "レストラン情報の整形中にエラーが発生しました。申し訳ありません。"

if __name__ == '__main__':
    # テスト用 (OpenAI APIの呼び出しを含むため、APIキーが有効である必要がある)
    # test_input = "銀座で寿司が食べたいな。個室があると嬉しい。"
    # params = get_restaurant_search_params_from_text(test_input)
    # print(f"抽出された検索パラメータ: {params}")

    # sample_restaurants_hotpepper = [
    #     {"name": "銀座 鮨 きらら", "access": "銀座駅A3出口徒歩1分", "budget": "18000円", "catch": "極上の江戸前鮨を堪能", "genre": "寿司", "url": "http://example.com/sushi_ginza"},
    #     {"name": "渋谷バル Ricardo", "access": "渋谷駅ハチ公口徒歩5分", "budget": "5000円", "catch": "おしゃれな隠れ家バル", "genre": "イタリアン・フレンチ", "url": "http://example.com/bar_shibuya"},
    # ]
    # response = generate_response_from_restaurant_info("渋谷で夜景の見えるバル", sample_restaurants_hotpepper)
    # print(f"\n生成された応答:\n{response}")

    test_input_json = "札幌駅近くで、子連れでもいけるスープカレー屋さんを探しています。"
    params_json = get_restaurant_search_params_from_text(test_input_json)
    print(f"抽出された検索パラメータ (JSON): {params_json}")
    if isinstance(params_json, dict):
        print("JSONパース成功")
    else:
        print("JSONパース失敗")
```

## hotpepper_handler.py
```python:hotpepper_handler.py
# hotpepper_handler.py
import os
import requests
from dotenv import load_dotenv

load_dotenv()

HOTPEPPER_API_KEY = os.getenv("HOTPEPPER_API_KEY")
HOTPEPPER_GOURMET_API_URL = "http://webservice.recruit.co.jp/hotpepper/gourmet/v1/"

def search_restaurants(keyword="", area="", genre_name=""):
    """
    ホットペッパーAPIを使ってレストランを検索する。
    キーワード、エリア、ジャンル名で検索可能。
    """
    if not HOTPEPPER_API_KEY:
        print("エラー: ホットペッパーAPIキーが設定されていません。")
        return []

    params = {
        "key": HOTPEPPER_API_KEY,
        "format": "json",
        "count": 5,  # 取得件数 (適宜調整してください)
        # "order": 4, # おすすめ順 (1:店名かな順, 2:ジャンルコード順, 3:エリアコード順, 4:おすすめ順)
    }

    # 検索クエリの構築
    # ホットペッパーAPIの keyword パラメータは強力なので、複数の情報を組み合わせて渡す
    search_keywords = []
    if keyword:
        search_keywords.append(keyword)
    if area:
        search_keywords.append(area) # エリア情報もキーワードとして含める
    if genre_name:
        search_keywords.append(genre_name) # ジャンル名もキーワードとして含める

    if search_keywords:
        params["keyword"] = " ".join(search_keywords)
    else:
        # 何も指定がない場合は、例えば東京駅周辺などをデフォルトにするか、エラーとする
        # ここでは例として「東京駅」をキーワードに設定
        # print("検索条件が指定されていません。東京駅周辺で検索します。")
        # params["keyword"] = "東京駅"
        print("検索条件が何も指定されていません。")
        return []


    # より高度な検索をする場合:
    # if area:
    #     params["address"] = area # 住所で検索
    # if genre_name:
    # params["genre"] = "GXXX" # 事前にジャンル名からジャンルコードを引く処理が必要

    try:
        response = requests.get(HOTPEPPER_GOURMET_API_URL, params=params)
        response.raise_for_status()  # HTTPエラーがあれば例外を発生させる
        data = response.json()

        restaurants = []
        if "results" in data and "shop" in data["results"]:
            for shop in data["results"]["shop"]:
                restaurant_info = {
                    "name": shop.get("name"),
                    "access": shop.get("mobile_access") or shop.get("access", "情報なし"), # モバイルアクセスを優先
                    "budget": shop.get("budget", {}).get("average", "情報なし"),
                    "url": shop.get("urls", {}).get("pc", "情報なし"),
                    "address": shop.get("address", "情報なし"),
                    "genre": shop.get("genre", {}).get("name", "情報なし"),
                    "catch": shop.get("catch", "情報なし"),
                    "photo": shop.get("photo", {}).get("pc", {}).get("l", "") # 大きめの写真
                }
                restaurants.append(restaurant_info)
        
        # print(f"Debug HotPepper: Found {len(restaurants)} restaurants with params: {params}")
        return restaurants
    except requests.exceptions.RequestException as e:
        print(f"ホットペッパーAPIリクエストエラー: {e}")
        if response is not None:
            print(f"Response status: {response.status_code}")
            print(f"Response text: {response.text}")
        return []
    except ValueError as e:  # JSONデコードエラー
        print(f"ホットペッパーAPI JSONデコードエラー: {e}")
        if response is not None:
            print(f"Response text: {response.text}")
        return []
    except Exception as e:
        print(f"ホットペッパーAPI処理中に予期せぬエラー: {e}")
        return []

if __name__ == '__main__':
    # テスト用
    # test_restaurants = search_restaurants(keyword="美味しい", area="札幌駅", genre_name="ラーメン")
    test_restaurants = search_restaurants(keyword="新宿", genre_name="イタリアン")
    # test_restaurants = search_restaurants(area="大阪", keyword="個室")
    
    if test_restaurants:
        print(f"{len(test_restaurants)}軒のレストランが見つかりました。")
        for i, r in enumerate(test_restaurants[:2]):  # 最初の2件を表示
            print(f"\n--- レストラン {i+1} ---")
            for key, value in r.items():
                print(f"{key}: {value}")
    else:
        print("テスト検索でレストランは見つかりませんでした。")
```

## main.py
```python:main.py
# main.py
from openai_handler import get_restaurant_search_params_from_text, generate_response_from_restaurant_info
# from gurunavi_handler import search_restaurants # 変更前
from hotpepper_handler import search_restaurants  # 変更後
import json

def chat_with_bot():
    print("レストラン検索チャットボットへようこそ！ (ホットペッパーAPI版)")
    print("例: 「渋谷で美味しいイタリアンを探して」「新宿で個室のある居酒屋」などと話しかけてください。")
    print("終了する場合は 'さようなら' と入力してください。")

    while True:
        user_input = input("\nあなた: ")
        if user_input.lower() in ["さようなら", "終了", "exit", "quit"]:
            print("ボット: ご利用ありがとうございました！")
            break

        if not user_input.strip():
            print("ボット: 何かお探しですか？")
            continue

        print("ボット: (考え中...)")

        # 1. OpenAI APIでユーザーの意図を解釈し、検索パラメータを抽出
        search_params_dict = get_restaurant_search_params_from_text(user_input)
        
        if not isinstance(search_params_dict, dict):
            # OpenAIからのレスポンスが期待通り辞書でない場合のフォールバック
            print("ボット: 検索条件の解析に少し手間取りました。もう一度試してみます。")
            # ユーザー入力をそのままキーワードとして検索を試みるか、エラーとする
            search_params_dict = {"keyword": user_input, "area": "", "genre": ""}

        # 抽出した情報をホットペッパーAPIのパラメータにマッピング
        # openai_handlerから返されるキー名に合わせて取得
        keyword_param = search_params_dict.get("keyword", "") 
        area_param = search_params_dict.get("area", "")
        genre_param = search_params_dict.get("genre", "") # OpenAIからはジャンル名で抽出想定

        print(f"ボット: （「キーワード: {keyword_param or '指定なし'}」「エリア: {area_param or '指定なし'}」「ジャンル: {genre_param or '指定なし'}」で検索します）")

        # 2. ホットペッパーAPIでレストランを検索
        # search_restaurants 関数は keyword, area, genre_name を引数に取るように hotpepper_handler.py で定義
        restaurants = search_restaurants(keyword=keyword_param, area=area_param, genre_name=genre_param)

        # 3. OpenAI APIで検索結果を元に応答を生成
        bot_response = generate_response_from_restaurant_info(user_input, restaurants)
        print(f"\nボット: {bot_response}")

if __name__ == "__main__":
    chat_with_bot()
```