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: その他要望など
処理は以下フローで実行されます。
- GPTsからHTTPのボディとして上記4変数を取得
- 国土地理院APIを用いて、住所から緯度経度を取得
- 緯度経度情報とその他情報を用いて、Google map APIで店舗情報を取得
- 店舗情報の口コミ情報を取得
- 取得情報をまとめてレスポンスで返す
コードは以下の通りです
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で記載するプロンプトについては、以下手順を踏めるようなプロンプト設定としています。
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を公開します。