😋

ChatGPTとLangChainで、ホットペッパーAPIを使って、オススメのお店を紹介してもらう

2023/05/13に公開

はじめに

LangChainからAPIを叩く処理を試してみました。
LLMにAPIの仕様を教え、質問からその仕様にあわせたURLリクエストを作成してもらっています。
最後は、この結果をチャットボットに繋ぐChainも作りました。
ホットペッパーAPIを叩けるようにし、おすすめのお店を紹介してもらいます。

HotPepper API

こちらがHotPepperのAPIリファレンスです。
https://webservice.recruit.co.jp/doc/hotpepper/reference.html

こちらのAPIの仕様をJsonにして保存したいと思います。
ChatGPTを使うことで簡単に整形できます。
LlamaIndexなどで適当にページを読み込んで、Json出力を依頼すると以下のような出力を得ることができます。
こういう変換処理にChatGPTは本当に便利ですね。

APIリファレンスJson化
{
  "apiSpec": {
    "name": "店名サーチAPI",
    "requestUrl": "http://webservice.recruit.co.jp/hotpepper/shop/v1/",
    "method": "GET",
    "queryParams": [
      {
        "name": "key",
        "description": "APIを利用するために割り当てられたキーを設定します。",
        "required": true
      },
      {
        "name": "keyword",
        "description": "お店の名前・読みがな・住所で検索(部分一致)します。文字コードはUTF8。半角スペース区切りの文字列を渡すことでAND検索になる。複数指定可能。",
        "required": false
      },
      {
        "name": "tel",
        "description": "お店の電話番号で検索(完全一致)します。半角数字(ハイフンなし)",
        "required": false
      },
      {
        "name": "start",
        "description": "検索結果の何件目から出力するかを指定します。",
        "required": false,
        "defaultValue": 1
      },
      {
        "name": "count",
        "description": "検索結果の最大出力データ数を指定します。",
        "required": false,
        "defaultValue": 30,
        "min": 1,
        "max": 30
      },
      {
        "name": "format",
        "description": "レスポンスをXMLかJSONかJSONPかを指定します。JSONPの場合、さらにパラメータ callback=コールバック関数名 を指定する事により、javascript側コールバック関数の名前を指定できます。",
        "required": false,
        "defaultValue": "xml",
        "options": ["xml", "json", "jsonp"]
      }
    ],
    "multiValueParamStyle": "multipleParamsOrCommaSeparated",
    "notes": [
      "検索条件にヒットする店舗が30件より多い場合はエラーになり「条件を絞り込んでください。」とメッセージがレスポンスされます。条件を追加して30件以内の店緒がヒットするようにしてください。店舗名や住所、電話番号等の一部がある程度わかっている場合にご利用いただける仕様となっております。"
    ]
  }
}

APIの利用には、API Keyが必要です。
こちらは、先ほどのAPIリファレンスページから申請できます。
取得したAPI Keyを以下のようにdescriptionに書き込みました。
こちらを、hotpepper_api.jsonとして保存しておきます。

      {
        "name": "key",
        "description": "Use this value:xxxxxxxxx",
        "required": true
      },

API Chain

POSTリクエストにも対応させたかったので、下のissueを参考にPowerfulAPIChainを作ります。
https://github.com/hwchase17/langchain/issues/2184

プロンプト

まずは、プロンプトです。
質問とAPIドキュメントから、llmが自分でリクエストを作ります。

text-davinci-003だとあまり問題が起きにくかったのですが、費用を抑えるためにgpt-3.5-turboを使うと、リクエストのフォーマットがばらついたので、結果を見ながら細かい指示を追加しました。

from langchain.prompts.prompt import PromptTemplate

API_URL_PROMPT_TEMPLATE = """You are given the below API Documentation:
{api_docs}
Using this documentation, generate the full API url to call for answering the user question.
You should build the API url in order to get a response that is as short as possible, while still getting the necessary information to answer the question. Pay attention to deliberately exclude any unnecessary pieces of data in the API call.
You should extract the request METHOD from doc, and generate the BODY data in JSON format according to the user question if necessary. The BODY data could be empty dict.
If method is GET, you should append the BODY data to the end of the url, and separate them with `?` and `&`.
You should separate the question keywords with the white space with `+` in the url.
Strictly add json format to the end of the url.

Question:{question}
"""

API_REQUEST_PROMPT_TEMPLATE = API_URL_PROMPT_TEMPLATE + """Output API_url|METHOD|BODY, join them with `|`. DO NOT GIVE ANY EXPLANATION."""

API_REQUEST_PROMPT = PromptTemplate(
    input_variables=[
        "api_docs",
        "question",
    ],
    template=API_REQUEST_PROMPT_TEMPLATE,
)

API_RESPONSE_PROMPT_TEMPLATE = (
    API_URL_PROMPT_TEMPLATE
    + """API url: {api_url}

Here is the response from the API:

{api_response}

Summarize this response to answer the original question.

Summary:"""
)

API_RESPONSE_PROMPT = PromptTemplate(
    input_variables=["api_docs", "question", "api_url", "api_response"],
    template=API_RESPONSE_PROMPT_TEMPLATE,
)

Chain作成

このプロンプトを処理するChainを作ります。

import json

from templates.api import API_REQUEST_PROMPT, API_RESPONSE_PROMPT  # change this to the path you placed the templates
from langchain.chains import APIChain
from typing import Any, Dict, Optional
from langchain.prompts import BasePromptTemplate
from langchain.requests import TextRequestsWrapper
from langchain.base_language import BaseLanguageModel
from langchain.chains.llm import LLMChain

class PowerfulAPIChain(APIChain):
    def _call(self, inputs: Dict[str, str]) -> Dict[str, str]:
        question = inputs[self.question_key]
        request_info = self.api_request_chain.predict(
            question=question, api_docs=self.api_docs
        )
        # delete white space, new line, and tab
        request_info = request_info.replace(" ", "").replace("\n", "").replace("\t", "")
        print(f'request info: {request_info}')

        api_url, request_method, body = request_info.split('|')
        request_func = getattr(self.requests_wrapper, request_method.lower())

        if(request_method.lower() == 'get'):
            api_response = request_func(api_url)
        else:
            api_response = request_func(api_url, json.loads(body))

        return {self.output_key: api_response}

    @classmethod
    def from_llm_and_api_docs(
        cls,
        llm: BaseLanguageModel,
        api_docs: str,
        headers: Optional[dict] = None,
        api_url_prompt: BasePromptTemplate = API_REQUEST_PROMPT,
        api_response_prompt: BasePromptTemplate = API_RESPONSE_PROMPT,
        **kwargs: Any,
    ) -> APIChain:
        """Load chain from just an LLM and the api docs."""
        get_request_chain = LLMChain(llm=llm, prompt=api_url_prompt)
        print(f"""headers

Chain実行

作ったAPIChainを実行してみます。

from langchain.llms import OpenAI

llm = OpenAI(model_name="gpt-3.5-turbo", temperature=0)
with open("./hotpepper_api.json") as f:
    api_docs = f.read()

chain = PowerfulAPIChain.from_llm_and_api_docs(llm=llm, api_docs=api_docs)
result = chain.run("渋谷でジンギスカンのお店を教えて")

Jsonで出力

import json
data = json.loads(result)
print(json.dumps(data, ensure_ascii=False, indent=2))

ちゃんとHotPepper APIの結果を受け取れました。

結果
{
  "results": {
    "api_version": "1.20",
    "results_available": 2,
    "results_returned": "2",
    "results_start": 1,
    "shop": [
      {
        "address": "東京都渋谷区道玄坂2-20-9 道玄坂柳光ビル 1F",
        "desc": "1",
        "genre": {
          "name": "焼肉・ホルモン"
        },
        "id": "J001168440",
        "name": "ジンギスカン羊一 渋谷店",
        "name_kana": "じんぎすかんよういちしぶやてん",
        "urls": {
          "pc": "https://www.hotpepper.jp/strJ001168440/?vos=nhppalsa000016"
        }
      },
      {
        "address": "東京都渋谷区宇田川町15-1  渋谷パルコ7F",
        "desc": "1",
        "genre": {
          "name": "焼肉・ホルモン"
        },
        "id": "J001282079",
        "name": "松尾ジンギスカン 渋谷パルコ店",
        "name_kana": "マツオジンギスカンシブヤパルコテン",
        "urls": {
          "pc": "https://www.hotpepper.jp/strJ001282079/?vos=nhppalsa000016"
        }
      }
    ]
  }
}

Sequencial Chain

Jsonを表示するだけでは味気ないので、チャットボット風に丁寧に紹介するようにします。
そのために、Chainを繋いで、Jsonを受け取る→受け取った結果を元にチャットボット風に返答してみます。

Chain作成

返信を返すChainを作ります。

from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI
llm_2 = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
prompt_2 = PromptTemplate(
		input_variables=["results_json"],
		template="{results_json}の情報をもとに質問にあうお店を丁寧な口調で紹介してください。最後にそれぞれのお店のURLを付けてください"
)
chain_2 = LLMChain(llm=llm_2, prompt=prompt_2)

こちらをHotPepper APIを叩くchainと繋ぎます。

from langchain.chains import SimpleSequentialChain

seq_chain = SimpleSequentialChain(chains=[chain, chain_2], verbose=True)

result = seq_chain.run("渋谷でジンギスカンのお店を教えて")

結果

チャットボット風の返答になりました。
APIで受け取った情報以上のことを勝手に足しているところがChatGPT APIらしいですね。
このあたりは、色々とチューニング必要です。

結果

はい、お客様におすすめのお店を2つご紹介いたします。

1つ目は、渋谷にある「ジンギスカン羊一 渋谷店」です。こちらのお店は、焼肉・ホルモンのジャンルで人気があります。店内は清潔感があり、落ち着いた雰囲気でお食事を楽しめます。特に、ジンギスカンがおすすめで、羊肉の旨味がたっぷりと詰まっています。また、スタッフの方々も親切で、初めての方でも安心して利用できます。ぜひ一度足を運んでみてはいかがでしょうか。

2つ目は、渋谷パルコ7Fにある「松尾ジンギスカン 渋谷パルコ店」です。こちらも焼肉・ホルモンのジャンルで人気があります。店内は明るく、開放感があります。特に、ジンギスカンの種類が豊富で、自分好みの味を選ぶことができます。また、お肉の質が高く、とても美味しいと評判です。スタッフの方々も親切で、気持ちよくお食事を楽しめます。ぜひ一度足を運んでみてはいかがでしょうか。

それぞれのお店のURLは以下の通りです。
・ジンギスカン羊一 渋谷店:https://www.hotpepper.jp/strJ001168440/?vos=nhppalsa000016
・松尾ジンギスカン 渋谷パルコ店:https://www.hotpepper.jp/strJ001282079/?vos=nhppalsa000016

Discussion