GBFSをChatGPTで操作可能にしてみる

GBFSアドベントカレンダー2023の5日目です。

みなさん、ChatGPT使ってますか？
いやー、昨年の発表以来、大盛り上がりですね。Qiitaの人気記事にも毎週なにかしらのChatGPT関連の記事がランクインしているように思います。

折しもChatGPTを自分でカスタマイズして特化させる機能がOpenAIから発表されましたね。GPTs。
今回はGBFSとこれを絡めていきたいと思います。

# 前提
まず今回の記事の前提です。

* ChatGPT4(2023/11/17時点のモデル)
* ChatGPT Plus(or Enterprise)

:::note info
本記事では、OpenStreet株式会社（ハローサイクリング） / 公共交通オープンデータ協議会のバイクシェア関連情報(GBFS形式)をCC-BY4.0に従って利用しています。
https://ckan.odpt.org/dataset/c_bikeshare_gbfs-openstreet
:::

# GBFSとはなんぞや
この記事を読むような方はそもそもGBFSをご存知かと思うのですが…
ChatGPTから流れてくる方もいらっしゃると思うので、説明記事をご紹介します！！（他力本願）

https://qiita.com/kumatira/items/f9229c21d9f0db3b5ae3

@kumatiraさんが素晴らしい記事を書いてくださっているので、私のGBFS関連の記事の始まりは全てこの記事の紹介です。

# GPTsとは
ChatGPTについては万巻の書があるので説明を省きますが、GPTsについては少し補足です。

ChatGPTの開発元であるOpenAIが2023/11/6に公開した、ChatGPTの新機能です。
機能的には **RAG（Retrieval Augmented Generation）** です。
単なる対話AIでしかないChatGPTに、ウェブ検索などのさまざまな機能を（その呼び出しの判断ごと）追加するもので、Langchainを使って実現できるものと同様のことができます。

ただそれをChatGPTと対話しながら作れたり、ノーコードでウィザード式に作れるのが特徴です。

詳しくは公式ページを。

https://openai.com/blog/introducing-gpts

今回は、GBFSのデータが取れるAPIをGPTsに組み込み、ChatGPTでGBFSを操作できるようにします。

# GPTsの作り方（基本設定）

まず、GPTsの作り方の簡単な説明をしつつ、今回のGPTsの設定をしていきます。

なお、前提にあるとおり、GPTsはPlusかEnterpriseユーザー限定です。無償ユーザーの方は、フーンと思いながら読んでください。

またChatGPTはしばしばUIが変わります。この記事は2023/11月時点のUIで説明をしています。
今後、UIが変更されたら読み替えてください。

ChatGPTのトップ画面から説明していきます。

1. ChatGPT画面右上の「**Explore**」を選択
1. 「**Create a GPT**」を選択

そうすると次のような画面になります。

![スクリーンショット 2023-11-17 23.08.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343307/3f7aa436-d268-ec90-49ec-84274dc7f58e.png)

画面左側がGPTsの設定画面、右側がプレビュー画面です。
初期では「**Create**」になっていて、ChatGPTと対話しながらGPTsを作れる画面になっています。

「**Configure**」を選ぶと、次の画面になります。

![スクリーンショット 2023-11-17 23.02.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343307/76ea03c1-b3fd-19bf-22a3-d779c14f3014.png)

私はこちらの方が好きなので、こちらをメインに説明していきます。

| 項目 | 説明 | 今回の設定例 |
|------|------|--------------|
| ⚪︎に＋のところ | アイコンの設定です。アイコン画像を用意しなくてもDALL-Eで生成できます。 |DALL-Eを使って作ってもらいました。ださい。  |
| Name | このGPTsの名前です。 | 「OpenStreetGBFS」 |
| Description | このGPTsの説明です。 | 「OpenStreetが公開するGBFSのstation_informationを操作します」 |
| Instruction | このGPTsになにをさせたいか、役割や、口調、予備知識などを与えます。 | 「あなたはOpenStreetのエンジニアです。GBFSのstation_information APIを使い、指示された情報を示したり、コードを実装、実行してください」 |
| Conversation starters | ユーザープロンプトの例示の設定です。このGPTsをどう使っていいかの例を示すなど、まずユーザーに試してほしいことなどを記述します。 | 今回は設定しません |
| Knowledge | データソースになるファイルを指定できます。指定できるファイルは決まっているようですが、PDF、CSVなどが指定できます。文章ファイルの場合、ベクトル化されてChatGPTが参照できるようです。 | 今回は使いません。 |
| Capabilities | このGPTsで使う拡張機能です。 | Web Browsing: OFF<br>DALL-E Image Generation: OFF<br>Code Interpreter: ON |
| Actions | このGPTsで使用するAPIを指定できます。 | 後述するGBFSのAPIエンドポイントを指定します。 |


さて、Actionsに指定するGBFSのAPIエンドポイントの設定については次章に譲ります。
まずそれ以外の項目を埋めます。埋めたのがこちら。

![スクリーンショット 2023-11-17 23.25.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343307/4920b4c0-ae65-47e0-dc01-acda68bfedb3.png)

# Actions

では、ActionsにGBFSのAPIを設定していきましょう。

Actionsの「**Create new action**」を選択します。
すると次のAPIの設定画面になります。

![スクリーンショット 2023-11-17 23.30.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343307/774df89c-e050-afce-9c35-6b268688608d.png)

Schemaのプレースホルダーに書いてあるとおり、OpenAPI形式で指定ができます。
また「Import from URL」や「Examples」には、既存のAPI定義を取り込んだり、テンプレを指定できます。

今回は、[OpenStreetがODPTで公開しているGBFS](https://ckan.odpt.org/dataset/c_bikeshare_gbfs-openstreet)の定義を作っていきます。
フルスクラッチで定義を書いてもいいのですが、今回、これもChatGPTに作ってもらいましょう。

ここは面倒なので、作った過程を共有します。

https://chat.openai.com/share/4ae057c6-1bb8-4572-bd3b-abcd405064d0

できたYAML形式の定義がこちらなのですが、3点誤りがあるため直します。
貼り付けた定義は修正後のものです。

* servers:urlで、最後のスラッシュが不要。urlにスラッシュがついていると、呼び出し時、hellocycling//station_information.jsonとなり、404になります
* operationIdの追加。不要かと思いましたがこれ必須でした
* GBFSの共通レスポンスでlast_updatedとversionを指示し忘れてたのを修正

```YAML
openapi: 3.0.0
info:
  title: GBFS Of OpenStreet Bikeshare Service
  description: This is a GBFS API Server of OpenStreet.
  version: 1.0.0
servers:
  - url: https://api-public.odpt.org/api/v4/gbfs/hellocycling
    description: Product API end point

paths:
  /station_information.json:
    get:
      summary: List all station information. id, position, vehicle capacity and soon.
      operationId: List stations
      responses:
        '200':
          description: A list of station information including location, name, and vehicle capacity.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ttl:
                    type: integer
                  last_updated:
                    type: integer
                  version:
                    type: string
                  data:
                    type: object
                    properties:
                      stations:
                        type: array
                        items:
                          $ref: '#/components/schemas/Station'

components:
  schemas:
    Station:
      type: object
      properties:
        lat:
          type: number
          format: float
          description: Latitude of the station.
        lon:
          type: number
          format: float
          description: Longitude of the station.
        name:
          type: string
          description: Name of the station.
        address:
          type: string
          description: Address of the station.
        station_id:
          type: string
          description: Unique identifier of the station.
        rental_uris:
          type: object
          properties:
            ios:
              type: string
              description: Station URL for iOS devices.
            web:
              type: string
              description: Station URL for PC web browsers.
            android:
              type: string
              description: Station URL for Android devices.
        parking_hoop:
          type: boolean
          description: Are parking hoops present at this station?
        parking_type:
          type: string
          description: Type of parking. Values are "parking_lot", "street_parking", "underground_parking", "sidewalk_parking", and "other".
        contact_phone:
          type: string
          description: Contact phone number for the station.
        is_charging_station:
          type: boolean
          description: Does the station support charging of electric vehicles?
        vehicle_type_capacity:
          type: object
          properties:
            num_bikes_now:
              type: integer
              description: Current number of bikes at the station.
            num_bikes_limit:
              type: integer
              description: Maximum number of bikes the station can hold.
            num_bikes_parkable:
              type: integer
              description: Current number of bikes the station can park.
            num_bikes_rentable:
              type: integer
              description: Current number of bikes the station can rent.
```

さて、この定義を**Schema**に貼り付けます。
ODPTのこのAPIは認証がないので、**Authentication**は「None」のままでいいです。**Privacy Policy**も特に指定がないので空にします。

貼り付けると、このように利用可能なアクションが表示され、テスト可能になります。

![スクリーンショット 2023-11-17 23.51.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343307/efde3a6d-3694-75d7-d69c-7d2e123c721e.png)

「Test」をすると実際にAPIが実行され、プレビュー画面でデバッグできます。

![スクリーンショット 2023-11-17 23.53.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343307/09edb11d-e0eb-56e2-5648-d3aeb4d5e16e.png)

はい、ダメでした…
要するに「レスポンスとは受け取れたけどデカすぎて使えないよ」ということです…

## APIを絞る

仕方ないのでAPIを絞ります。
ここはこの記事の本題ではないので、やったことだけ説明します。

1. python flaskでsystem_information.jsonを「東京都小平市」に絞るAPIを実装
1. 同APIをローカルホストで実行
1. VisualStudioCodeのポートフォワーディングでローカルホストのAPIを外部実行可能なURLを発行
1. APIを認証が不要な「公開」に設定
1. GPTsのActionsのSchemaに指定したOpenAPIのserversを発行されたURLに変更

これで無事、APIが動作しました。

大したソースではありませんが、1.のソースは一応示しておきます。
（このソースも半ばGithub Copilotに書かせています）

```Python
import requests
import json
import flask

app = flask.Flask(__name__)

@app.route('/station_information.json')
def station_information():
    url = "https://api-public.odpt.org/api/v4/gbfs/hellocycling/station_information.json"
    res = requests.get(url)
    station_information = json.loads(res.text)

    _d = {}
    _d["last_updated"] = station_information["last_updated"]
    _d["ttl"] = station_information["ttl"]
    _d["version"] = station_information["version"]
    _d["data"] = {}
    _d["data"]["stations"] = []

    for station in station_information["data"]["stations"]:
        if "東京都小平市" in station["address"]:
            _d["data"]["stations"].append(station)

    return flask.jsonify(_d)

if __name__ == '__main__':
    app.run()
```

# 動かしてみる

動かしてみた結果がこちらです。

![スクリーンショット 2023-11-23 0.31.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343307/616d5e61-a464-aec7-a4cd-d209d059bc1c.png)

![スクリーンショット 2023-11-23 0.31.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343307/226f507e-cfb8-116a-c4a7-475bf04592bd.png)

![スクリーンショット 2023-11-23 0.31.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343307/64f09591-3c63-028c-3df9-f81d0880bced.png)

コードを示してくれと言ったけど、結果を示しているところはありますが…、全般的にはだいぶいいですね！

GBFSはJSON仕様としては、比較的シンプルなものの、非エンジニア職にはやはり扱いにくいものですし、エンジニア職でも解析しようとしたらコードを書かねばなりません。
GPTsにしておくことで、その操作がパッとできるのはちょっとしたやりたいこと、知りたいことをパッと形にしてくれます。

APIやドキュメントと組み合わせることで、会社やオープンデータとしてある資産を生かす道が広がるのではないでしょうか。

と言ったところで、今回の記事を終わりたいと思います。