ChatGPTプラグインを作る方法まとめ

ChatGPTプラグインの開発方法がOpenAI社から公開されていたので、内容を簡単にまとめました！

これからChatGPTのプラグインの開発を考えている人はぜひ参考にしてみてください

https://platform.openai.com/docs/plugins/introduction

プラグイン開発の具体的な手順や、プラグインストアのレビューについての情報は以下の記事でまとめていますので、ぜひこちらも参考にしてみてください。

https://generativeinfo365.com/?p=261

## ChatGPT プラグインとは
ChatGPTプラグインは、ChatGPTをサードパーティのアプリケーションと連携させるツールです。

これは、GoogleスプレッドシートのアドオンやGoogle Chromeの拡張機能と同様に、公式だけでなく第三者の開発者が作成した機能をChatGPTに追加することで機能を拡充できるものとなっています。

具体的には、プラグインを利用することで、ChatGPTに以下のような機能を追加できるようになります。

- リアルタイムの情報を取得：スポーツのスコアや株価、最新のニュースなどを確認
- ナレッジベースの情報を取得：企業の文書や個人のメモなどを閲覧
- ユーザーの代わりにアクションを実行：フライトの予約や食事の注文などを行う

日本の企業の中にも、すでにこのプラグインを開発しているところがあります。例えば、食べログがその一つです。

食べログのプラグインをChatGPTで有効にすることで、食べログでネット予約が可能な店舗の空席情報を、指定した条件に合わせて簡単に検索できるようになります。

https://corporate.kakaku.com/press/release/20230508

## ChatGPTプラグインの開発から公開までの流れ
ChatGPTプラグインの開発から公開までは以下の流れで行う必要があります。

### 1. マニフェストファイルを作成し、yourdomain.com/.well-known/ai-plugin.json でホスト

プラグイン作成には、プラグインの情報（名前、ロゴ等）、必要な認証詳細（認証タイプ、OAuth URL等）、公開したいエンドポイントのOpenAPIスペックを含むマニフェストファイルを作成し、特定のURLでホストする必要があります。

プラグインの説明、APIリクエスト、APIレスポンスは全てChatGPTとの対話に挿入され、これはモデルのコンテキスト制限に影響します。

必要なai-plugin.jsonファイルの最小限の定義は以下の通りです。

```json:ai-plugin.json
{
    "schema_version": "v1",
    "name_for_human": "TODO Plugin",
    "name_for_model": "todo",
    "description_for_human": "Plugin for managing a TODO list. You can add, remove and view your TODOs.",
    "description_for_model": "Plugin for managing a TODO list. You can add, remove and view your TODOs.",
    "auth": {
        "type": "none"
    },
    "api": {
        "type": "openapi",
        "url": "http://localhost:3333/openapi.yaml",
        "is_user_authenticated": false
    },
    "logo_url": "http://localhost:3333/logo.png",
    "contact_email": "support@example.com",
    "legal_info_url": "http://www.example.com/legal"
}
```

## ChatGPTにプラグインを登録
ChatGPTのUIでプラグインを登録します。プラグインモデルをドロップダウンから選択し、「プラグイン」、「プラグインストア」を選び、「未検証のプラグインをインストール」または「自分のプラグインを開発」をクリックします。

認証が必要な場合、OAuth 2のclient_idとclient_secret、またはAPIキーを指定します。

## ユーザーがプラグインを有効化
ユーザーはChatGPT上で手動でプラグインを有効化することで利用できるようになります。

認証が必要な場合、ユーザーはOAuth経由でプラグインにリダイレクトされ、新規アカウントの作成も可能です。

## ユーザーが会話を開始
OpenAIはユーザーが見えない形でChatGPTにプラグインの要約を提供し、ユーザーが関連する質問をした際、モデルはAPIコールを使ってプラグインから情報を取得できます。

この結果はユーザーへの応答に反映され、APIから返されるリンクはリッチプレビューとして表示されます。これらのリンクはOpenGraphプロトコルを用いて各種情報を抽出します。

また、モデルはAPIからの画像URLなどのマークダウンを出力し、それはChatGPT UIでレンダリングされます。

## 注意点
### ①プラグインの名前を付ける際には、ブランドガイドラインに沿った名前をつける

これらのガイドラインに従わないプラグインは、プラグインストアで承認されません。

例えば、〇〇GPTやGPT〇〇という名前は禁止されているため、プラグインストアで承認されない可能性が高いです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3356814/e6d1a60c-4c5c-1896-cddd-dc80f03ffebe.png)

ブランドガイドラインは以下の記事でまとめています。

https://generativeinfo365.com/?p=14

## クイックスタート

OpenAI社は開発者が5分以内にプラグインを動かせるように、「プラグインクイックスタート」を公開しています。

プラグインを試したことがなく、どうすれば動かせるのかを簡単に学びたいなら、「プラグインクイックスタート」のリポジトリから始めてみるのがおすすめです。

プラグインを動かすための基本的な手順を学ぶことができます。

https://github.com/openai/plugins-quickstart


## プラグインサンプル
OpenAI社の公式で紹介されているプラグイン例です。

### 例①シンプルなスポーツ統計プラグインの構築方法

このプラグインは、シンプルなスポーツ統計APIの一例です。実際にプラグインを作る際には、ドメインポリシーや利用ポリシーに留意してください。

まず、以下のフィールドを持つai-plugin.jsonファイルを定義します

```json:ai-plugin.json
{
    "schema_version": "v1",
    "name_for_human": "Sport Stats",
    "name_for_model": "sportStats",
    "description_for_human": "Get current and historical stats for sport players and games.",
    "description_for_model": "Get current and historical stats for sport players and games. Always display results using markdown tables.",
    "auth": {
        "type": "none"
    },
    "api": {
        "type": "openapi",
        "url": "PLUGIN_HOSTNAME/openapi.yaml",
        "is_user_authenticated": false
    },
    "logo_url": "PLUGIN_HOSTNAME/logo.png",
    "contact_email": "support@example.com",
    "legal_info_url": "https://example.com/legal"
}

```
PLUGIN_HOSTNAMEは、プラグインサーバーの実際のホスト名であることに注意してください。

次に、シンプルなスポーツサービスプラグインのモックAPIを定義します。

```python:main.py
import json
import requests
import urllib.parse

import quart
import quart_cors
from quart import request

# Note: Setting CORS to allow chat.openapi.com is only required when running a localhost plugin
app = quart_cors.cors(quart.Quart(__name__), allow_origin="https://chat.openai.com")
HOST_URL = "https://example.com"

@app.get("/players")
async def get_players():
    query = request.args.get("query")
    res = requests.get(
        f"{HOST_URL}/api/v1/players?search={query}&page=0&per_page=100")
    body = res.json()
    return quart.Response(response=json.dumps(body), status=200)


@app.get("/teams")
async def get_teams():
    res = requests.get(
        "{HOST_URL}/api/v1/teams?page=0&per_page=100")
    body = res.json()
    return quart.Response(response=json.dumps(body), status=200)


@app.get("/games")
async def get_games():
    query_params = [("page", "0")]
    limit = request.args.get("limit")
    query_params.append(("per_page", limit or "100"))
    start_date = request.args.get("start_date")
    if start_date:
        query_params.append(("start_date", start_date))
    end_date = request.args.get("end_date")

    if end_date:
        query_params.append(("end_date", end_date))
    seasons = request.args.getlist("seasons")

    for season in seasons:
        query_params.append(("seasons[]", str(season)))
    team_ids = request.args.getlist("team_ids")

    for team_id in team_ids:
        query_params.append(("team_ids[]", str(team_id)))

    res = requests.get(
        f"{HOST_URL}/api/v1/games?{urllib.parse.urlencode(query_params)}")
    body = res.json()
    return quart.Response(response=json.dumps(body), status=200)


@app.get("/stats")
async def get_stats():
    query_params = [("page", "0")]
    limit = request.args.get("limit")
    query_params.append(("per_page", limit or "100"))
    start_date = request.args.get("start_date")
    if start_date:
        query_params.append(("start_date", start_date))
    end_date = request.args.get("end_date")

    if end_date:
        query_params.append(("end_date", end_date))
    player_ids = request.args.getlist("player_ids")

    for player_id in player_ids:
        query_params.append(("player_ids[]", str(player_id)))
    game_ids = request.args.getlist("game_ids")

    for game_id in game_ids:
        query_params.append(("game_ids[]", str(game_id)))
    res = requests.get(
        f"{HOST_URL}/api/v1/stats?{urllib.parse.urlencode(query_params)}")
    body = res.json()
    return quart.Response(response=json.dumps(body), status=200)


@app.get("/season_averages")
async def get_season_averages():
    query_params = []
    season = request.args.get("season")
    if season:
        query_params.append(("season", str(season)))
    player_ids = request.args.getlist("player_ids")

    for player_id in player_ids:
        query_params.append(("player_ids[]", str(player_id)))
    res = requests.get(
        f"{HOST_URL}/api/v1/season_averages?{urllib.parse.urlencode(query_params)}")
    body = res.json()
    return quart.Response(response=json.dumps(body), status=200)


@app.get("/logo.png")
async def plugin_logo():
    filename = 'logo.png'
    return await quart.send_file(filename, mimetype='image/png')


@app.get("/.well-known/ai-plugin.json")
async def plugin_manifest():
    host = request.headers['Host']
    with open("ai-plugin.json") as f:
        text = f.read()
        # This is a trick we do to populate the PLUGIN_HOSTNAME constant in the manifest
        text = text.replace("PLUGIN_HOSTNAME", f"https://{host}")
        return quart.Response(text, mimetype="text/json")


@app.get("/openapi.yaml")
async def openapi_spec():
    host = request.headers['Host']
    with open("openapi.yaml") as f:
        text = f.read()
        # This is a trick we do to populate the PLUGIN_HOSTNAME constant in the OpenAPI spec
        text = text.replace("PLUGIN_HOSTNAME", f"https://{host}")
        return quart.Response(text, mimetype="text/yaml")


def main():
    app.run(debug=True, host="0.0.0.0", port=5001)


if __name__ == "__main__":
    main()
```
最後に、OpenAPI仕様の定義を行います

```yaml:openapi.yaml
openapi: 3.0.1
info:
    title: Sport Stats
    description: Get current and historical stats for sport players and games.
    version: "v1"
servers:
    - url: PLUGIN_HOSTNAME
paths:
    /players:
        get:
            operationId: getPlayers
            summary: Retrieves all players from all seasons whose names match the query string.
            parameters:
                - in: query
                  name: query
                  schema:
                      type: string
                  description: Used to filter players based on their name. For example, ?query=davis will return players that have 'davis' in their first or last name.
            responses:
                "200":
                    description: OK
    /teams:
        get:
            operationId: getTeams
            summary: Retrieves all teams for the current season.
            responses:
                "200":
                    description: OK
    /games:
        get:
            operationId: getGames
            summary: Retrieves all games that match the filters specified by the args. Display results using markdown tables.
            parameters:
                - in: query
                  name: limit
                  schema:
                      type: string
                  description: The max number of results to return.
                - in: query
                  name: seasons
                  schema:
                      type: array
                      items:
                          type: string
                  description: Filter by seasons. Seasons are represented by the year they began. For example, 2018 represents season 2018-2019.
                - in: query
                  name: team_ids
                  schema:
                      type: array
                      items:
                          type: string
                  description: Filter by team ids. Team ids can be determined using the getTeams function.
                - in: query
                  name: start_date
                  schema:
                      type: string
                  description: A single date in 'YYYY-MM-DD' format. This is used to select games that occur on or after this date.
                - in: query
                  name: end_date
                  schema:
                      type: string
                  description: A single date in 'YYYY-MM-DD' format. This is used to select games that occur on or before this date.
            responses:
                "200":
                    description: OK
    /stats:
        get:
            operationId: getStats
            summary: Retrieves stats that match the filters specified by the args. Display results using markdown tables.
            parameters:
                - in: query
                  name: limit
                  schema:
                      type: string
                  description: The max number of results to return.
                - in: query
                  name: player_ids
                  schema:
                      type: array
                      items:
                          type: string
                  description: Filter by player ids. Player ids can be determined using the getPlayers function.
                - in: query
                  name: game_ids
                  schema:
                      type: array
                      items:
                          type: string
                  description: Filter by game ids. Game ids can be determined using the getGames function.
                - in: query
                  name: start_date
                  schema:
                      type: string
                  description: A single date in 'YYYY-MM-DD' format. This is used to select games that occur on or after this date.
                - in: query
                  name: end_date
                  schema:
                      type: string
                  description: A single date in 'YYYY-MM-DD' format. This is used to select games that occur on or before this date.
            responses:
                "200":
                    description: OK
    /season_averages:
        get:
            operationId: getSeasonAverages
            summary: Retrieves regular season averages for the given players. Display results using markdown tables.
            parameters:
                - in: query
                  name: season
                  schema:
                      type: string
                  description: Defaults to the current season. A season is represented by the year it began. For example, 2018 represents season 2018-2019.
                - in: query
                  name: player_ids
                  schema:
                      type: array
                      items:
                          type: string
                  description: Filter by player ids. Player ids can be determined using the getPlayers function.
            responses:
                "200":
                    description: OK

```



## FAQ
### プラグインデータはどう使われるの？
プラグインはChatGPTと外部アプリをつなげる役割を果たします。ユーザーがプラグインを使うと、その会話の一部やユーザーの住んでいる国や地域の情報がプラグインに送られることがあります。

### APIへの要求がうまくいかなかったらどうなるの？
APIへの要求が失敗した場合、モデルはそのプラグインから返答が得られないとユーザーに伝える前に、最大10回まで再試行を試みます。

### 自分のプラグインを試してもらうために人を招待できる？
はい、確認がまだ済んでいないプラグインでも、プラグインの利用資格を持つ他の開発者15人までにインストールしてもらうことができます。

### 自分のプラグインの利用に対して料金を取ることはできる？
はい、可能です。プラグインを運用するにはコストがかかるので、無料で使えるAPIの回数を限定的に設定することをお勧めします。

## プラグイン開発事例

### 食べログ
冒頭でもお話しした食べログのプラグインの開発ストーリーが公開されています。

開発部分だけでなく、サービス設計や法務とのやり取りまで生々しく書かれているので非常に面白いです！

https://tech-blog.tabelog.com/entry/first-challenge-tabelog-chatgpt-plugin-devleopment

## まとめ
ChatGPTのプラグインはβ版がリリースされたばかりなので、まだプラグインストアにはプラグインが100種類も存在しておらず、ここから世界的に人気のプラグインを作るのも夢ではありません。

なので、ぜひこの記事を参考にプラグインを作れるように試していただければと思います。

また情報が更新され次第、追記していきます。

プラグイン開発の具体的な手順や、プラグインストアのレビューについての情報は以下の記事でまとめていますので、ぜひこちらも参考にしてみてください。

https://generativeinfo365.com/?p=261