まるっとワーク

データ分析・電子工作など気になることを残していきます

凝ったGPTsを作る!(自作APIで差をつける)




GPT Storeが使えるようになり、注目を集めるGPTsを使ってみると、機能が洗練されているなぁ、と驚きます!各GPTには独自の機能とオリジナリティが備わっており、「自分もこのようなものを作りたい」という気持ちが湧き上がってきます。
しかし、ただプロンプトを操作しているだけでは、何かを調べて、まとめて、優先順位を決めて、報告して・・といった複雑なフローの作業を与えることが難しかったので、APIを自ら設計し、組み込むことで、その実現に挑戦しました。


実際に作成したGPTs↓
chat.openai.com

GPTsはスマホから使うというより、PCから使うことが多く、スマホで気軽に使えるGPTsがあれば良いなと思い、要望に応じた近くの飲食店を紹介するGPTsを作成しました。Google Maps APIも利用して、お店の概要、口コミ情報を集め、要約した結果を返します。
お店の概要、口コミ情報の収集してまとめる部分を自身で作成したAPIが担っています。
本機能の作成過程をこのページの以下にまとめています。

目次


本サイトで記載の実施事項は、以下ページの記載を参考にさせていただいています。感謝です!
独自性の強いGPTsを作る方法(GoogleSpreadSheet連携) #ChatGPT - Qiita
最新情報や独自データを使って回答するGPTsのつくりかた #ChatGPT - Qiita
【GPTs Actions】GPTsの外部API連携方法 #ChatGPT - Qiita


APIの作成

APIの作成は、Google Cloud Platform(GCP)のCloud Functionsを使いました。
GCPのCloud Functionは、似たようなサービスであるAWS Lambdaと比較して、Pythonの依存関係をLambda Layerとして準備しなくてもよいところが少し楽ですね。

Cloud Functionsの設定

今回は、トリガーをHTTPトリガー設定とします。
GPTsで使用することを想定して、認証不要な"Allow unauthenticated invocations" にチェックをしています。

Codeの設定

Pythonを指定して、エントリポイントを「hello_http」(初期のまま)としてmain.pyに処理を記載し、Pythonの依存関係をrequirements.txtに記載しています。
エントリ ポイントは、Cloud Functions の関数が呼び出されたときに実行されるコードです。このエントリ ポイントは、関数のデプロイ時に指定します。



main.pyの記載について、GPTsからのHTTPリクエストのボディとして、以下を想定しています。

  • address: 住所(検索個所)
  • radius: 検索範囲(m)
  • keyword: キーワード(寿司、焼肉など)
  • query: その他要望など


処理は以下フローで実行されます。

  1. GPTsからHTTPのボディとして上記4変数を取得
  2. 国土地理院APIを用いて、住所から緯度経度を取得
  3. 緯度経度情報とその他情報を用いて、Google map APIで店舗情報を取得
  4. 店舗情報の口コミ情報を取得
  5. 取得情報をまとめてレスポンスで返す


コードは以下の通りです

import math 
import requests, json
import urllib.parse
import pandas as pd
import functions_framework

def cal_average(num):
    sum_num = 0
    for t in num:
        sum_num = sum_num + t           
    avg = sum_num / len(num)
    return avg

def cal_distance(base, destination):

    # 緯度経度をラジアンに変換
    lat_base = math.radians(base[0])
    lon_base = math.radians(base[1])
    lat_destination = math.radians(destination[0])
    lon_destination = math.radians(destination[1])

    pole_radius = 6356752.314245                  # 極半径
    equator_radius = 6378137.0                    # 赤道半径

    lat_difference = lat_base - lat_destination       # 緯度差
    lon_difference = lon_base - lon_destination       # 経度差
    lat_average = (lat_base + lat_destination) / 2    # 平均緯度

    e2 = (math.pow(equator_radius, 2) - math.pow(pole_radius, 2)) \
            / math.pow(equator_radius, 2)  # 第一離心率^2

    w = math.sqrt(1- e2 * math.pow(math.sin(lat_average), 2))

    m = equator_radius * (1 - e2) / math.pow(w, 3) # 子午線曲率半径

    n = equator_radius / w                         # 卯酉線曲半径

    distance = math.sqrt(math.pow(m * lat_difference, 2) \
                   + math.pow(n * lon_difference * math.cos(lat_average), 2)) # 距離計測

    return distance

def main(address, radius, keyword, query):
    ##################################
    Address = address
    radius = radius
    keyword = keyword
    add_query = address + " " + query
    ##################################

    ## msearch
    API_KEY = "Google map API KEYをここに入れます"
    makeUrl = "https://msearch.gsi.go.jp/address-search/AddressSearch?q="
    s_quote = urllib.parse.quote(Address)
    response = requests.get(makeUrl + s_quote)
    try:
        lat = response.json()[-1]["geometry"]["coordinates"][1]#"-33.8670522"
        lng = response.json()[-1]["geometry"]["coordinates"][0]#"151.1957362"
        seach_area = response.json()[-1]["properties"]["title"]
        print(response.json()[-1]["properties"])
    except Exception as e:
        return "緯度経度検索にヒットなし。地名を詳細に教えて下さい。/ No hit. Please fill in address at detail."
    base = [lat, lng]

    ## g_near_search
    url = "https://maps.googleapis.com/maps/api/place/textsearch/json?query={}&radius={}&language=ja&location={}&keyword={}&key={}&opennow".format(add_query, radius, base, keyword, API_KEY)
    payload = {}
    headers = {}

    response = requests.request("GET", url, headers=headers, data=payload)
    place = json.loads(response.text)

    ## summarize
    buff = pd.DataFrame()

    store_name = []
    #open_now = []
    place_id = []
    rating = []
    user_ratings_total = []
    user_ratings_num = []
    address = []
    distance = []
    lat_list = []
    lng_list = []
    route_url_list = []

    ### 現在地
    base = [lat, lng]
    for i in range(len(place["results"])):
        #print(i, place["results"][i])
        store_name.append(place["results"][i]["name"])
        #open_now.append(place["results"][i]["opening_hours"]["open_now"])
        place_id.append(place["results"][i]["place_id"])
        rating.append(place["results"][i]["rating"])
        user_ratings_total.append(place["results"][i]["user_ratings_total"])
        #route_url_list.append("https://www.google.com/maps/dir/?api=1&destination=${},${}".format(place["results"][i]["geometry"]["location"]["lat"], place["results"][i]["geometry"]["location"]["lng"]))
        route_url_list.append("https://www.google.com/maps/dir/?api=1&destination={}".format(place["results"][i]["name"]))
        if place["results"][i]["rating"] == 0:
            user_ratings_num.append(0)
        else:
            user_ratings_num.append(place["results"][i]["user_ratings_total"] / place["results"][i]["rating"])
        address.append(place["results"][i]["formatted_address"])
        lat_list.append(place["results"][i]["geometry"]["location"]["lat"])
        lng_list.append(place["results"][i]["geometry"]["location"]["lng"])
        distance.append(cal_distance(base, [place["results"][i]["geometry"]["location"]["lat"], place["results"][i]["geometry"]["location"]["lng"]]))

    buff["store_name"] = store_name
    #buff["open_now"] = open_now
    buff["place_id"] = place_id
    buff["rating"] = rating
    buff["user_ratings_total"] = user_ratings_total
    buff["user_ratings_num"] = user_ratings_num
    buff["address"] = address
    buff["route_to_store_google_map"] = route_url_list
    buff["distance_from_search_area[m]"] = distance
    buff["search_area"] = seach_area

    buff_fl = buff[buff["user_ratings_num"] >= 10]
    buff_fl = buff_fl[buff_fl["distance_from_search_area[m]"] < (int(radius))]

    buff_fl = buff_fl.sort_values("rating", ascending=False)
    buff_fl = buff_fl.reset_index(drop=True)
    buff_fl["comment"] = "dummy"
    buff_fl["comment_rating"] = "dummy"

    # 口コミ取得
    max_num = len(buff_fl)
    if max_num > 5:
        max_num = 5
    elif max_num == 0:
        return "データ個数が0です。検索条件を調整してください / No hit. Please change search condition."
    for i in range(max_num):
        place_id = buff_fl["place_id"][i]
        url = "https://maps.googleapis.com/maps/api/place/details/json?place_id={}&language=ja&key={}".format(place_id, API_KEY)
        payload = {}
        headers = {}

        response_c = requests.request("GET", url, headers=headers, data=payload)
        comment = json.loads(response_c.text)

        user_rating = []
        comment_list = []

        for j in range(len(comment["result"]["reviews"])):
            user_rating.append(comment["result"]["reviews"][j]["rating"])
            comment_list.append(comment["result"]["reviews"][j]["text"])

        # print(i, comment_list)
        # print(buff_fl)
        comment_join = "".join(comment_list)
        buff_fl.loc[i,"comment"] = comment_join
        ave_data = cal_average(user_rating)
        buff_fl.loc[i, "comment_rating"] = ave_data

    return buff_fl.iloc[0:(max_num-1), :].to_json(force_ascii=False)

@functions_framework.http
def hello_http(request):
    """HTTP Cloud Function.
    Args:
        request (flask.Request): The request object.
        <https://flask.palletsprojects.com/en/1.1.x/api/#incoming-request-data>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response>.
    """
    #request_json = request.get_json(silent=True)
    request_args = request.args
    print("request",request)
    #print("request_json",request_json)
    print("request_args",request_args)

    if request_args and 'address' in request_args:
        address = request_args['address']
        radius = request_args['radius']
        keyword = request_args['keyword']
        query = request_args['query']
        print(address, radius, keyword, query)
    else:
        max_num = 0
        return "引数が適切ではありません / invalid argument."
    df = main(address, radius, keyword, query)

    #'GetDataNum{}!'.format(max_num), 
    return df


requirements.txtは以下の通りです。

functions-framework==3.*
certifi==2024.2.2
charset-normalizer==3.3.2
idna==3.6
numpy==1.26.4
pandas==2.2.1
python-dateutil==2.8.2
pytz==2024.1
requests==2.31.0
six==1.16.0
tzdata==2024.1
urllib3==2.2.1


このような設定で、関数をデプロイします。

GPTsの作成

Instructionsで記載するプロンプトについては、以下手順を踏めるようなプロンプト設定としています。

  1. ユーザーから、HTTPリクエストのボディとなる要素を聞く
  2. 先程デプロイしたアプリを呼び出す
  3. 結果を取得する
  4. 取得した結果を踏まえ、お店の違いやユースケースを作成
  5. ユーザーに結果を返す


Actionsの設定

各項目は以下の通り設定しています。

  • Authentication: None
  • Schemaは以下の通り記載しています。

urlの記載は、先程デプロイした関数のエンドポイントで、HTTPリクエストボディの詳細をparametersに記載しています。(最低限この記載が必要)

openapi: 3.0.0
info:
  title: Google Map Search API
  version: "1.0"
servers:
  - url: 'https://asia-northeast2-root-catfish-384505.cloudfunctions.net'
paths:
  /google_map_search:
    get:
      summary: 'Google Map検索結果を取得'
      operationId: searchGoogleMap
      parameters:
        - name: address
          in: query
          description: '地名情報を入れる[日本語で必ず入れる]'
          required: true
          language: jp
          schema:
            type: string
        - name: radius
          in: query
          description: '検索範囲[単位: m]'
          required: true
          schema:
            type: integer
        - name: keyword
          in: query
          description: 'お店のタイプ[restaurant, bar, bakery]'
          required: true
          schema:
            type: string
        - name: query
          in: query
          description: '料理のジャンル[焼肉、ラーメン等 日本語で確実に入れる]'
          required: true
          language: jp
          schema:
            type: string
      responses:
        '200':
          description: 成功時のレスポンス
          content:
            application/json:
              schema:
                type: object
                properties:
                  store_name:
                    type: object
                    description: お店の名前
                  route_to_store_google_map:
                    type: object
                    description: お店までのルートを示したmapへのURLリンク
                  place_id:
                    type: object
                    description: お店のID
                  rating:
                    type: object
                    description: お店の評価(悪い1~5良い)
                  user_ratings_total:
                    type: object
                    description: 全ユーザーの評価点の合計
                  user_ratings_num:
                    type: object
                    description: 評価ユーザー数
                  address:
                    type: object
                    description: お店の住所
                  distance_from_search_area:
                    type: object
                    description: 検索基準地点からの距離(単位m)
                  search_area:
                    type: object
                    description: 検索基準点
                  comment:
                    type: object
                    description: お店に対する口コミ
                  comment_rating:
                    type: object
                    description: 口コミ投稿者の平均評価点

  • Privacy policy

Google Sitesでプライバシーポリシーを用意しました。サイトにアクセスできない状態だと、GPTsを公開設定できなかったので、必ず必要です。

私が作成したプライバシーポリシーは以下です。
sites.google.com

設定できたら、GPTsを公開します。

まとめ

今回は、自身でAPIを設計して、組み込むことで少し凝ったGPTsを作成してみました。
単純にAPIだけを利用するとなると、エラーの時になんでかわからなかったり、帰ってきた結果が大量で、それを読み解くのに時間がかかりますが、
LLMを使うことで、ユーザーフレンドリーな使い方ができ、結果も要約してくれるのが良いですね。