凝った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: その他要望など
処理は以下フローで実行されます。
- 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を公開します。