ChatGPTを使いながらChatGPT Pluginを開発した話 by 人間

# はじめに
こんにちは、皆さん。今回の記事は、人間が作成しました。（ChatGPTお任せにしたかったけど、全然だめだったので人間が担当)
- この記事では、ChatGPTのプラグイン開発についてお伝えします。
- 私は最適化問題を解決するための新しいプラグインの開発に取り組みました。一連の作業で得たノウハウやコツを共有します。

ChatGPTが記載した記事は以下：
https://qiita.com/___monta___/items/6e1408895b28dffb2907

作ったPluginは以下にて公開しています。
金が有り余っていて、人類のためにオープンなサービスを作ってほしいという人がいたら、寄付いただければ気が向いたらサービス作るかも。

https://github.com/84monta/mlpplugin

ちなみに作成したプラグインで以下のような結果を得ることができます。
プラグイン機能なしでは解けない問題です。

![SnapCrab_NoName_2023-5-17_7-24-11_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121950/7df7788d-85d1-e4c8-591a-cba7a3db7965.png)

# プラグインを作ろうと思った背景

数学の問題解決においては、有名なプラグインであるWolframが存在します。Wolframは非常に強力なモデルであり、幅広い分野の問題に対応できます。しかし、私自身は線形計画問題やその他の最適化問題に関して、Wolframの限定された能力に直面してきました。

私はこの制約を克服するために、新たなプラグインの開発に取り組んでいます。最近、ライセンスが変更され、使いやすくなったSCIPというソルバーに注目しました。SCIPは最適化問題に特化しており、特に線形計画問題の解決において優れた性能を発揮します。

私はChatGPTの有料版を契約していますが、その時間を有効に活用できていないと感じていました。せっかくなので元をとりたいという貧乏根性から、Pluginリリース直後から数日で動くものを作り上げました。

あと、個人的には数理最適化結構楽しいので皆さん知ってほしいなぁという思い。

# 環境
普段はWindowsを使っていますが、プログラミングはWSL上で実施しています。
なので、SCIPもWSL上で実行しますが、サービスを公開するわけではないので通信をlocalhost(Windowsのホスト側）で実施する際にPortfowardingで解決しました。

# 作り方

あまり具体的な記事がないので、OpenAIのPluginページを参照しました。

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

また、上記に記載がありますが、以下のGithubに非常にシンプルなToDoプラグインがあります。

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

こいつをコピーして適当にいじればOKでしょう。

## 処理全体の流れ
以下のような分担になっています。
1) 【人間】人間が問題をなげる。（なんならここもChatGPTにお願いしてもよい）
2) 【ChatGPT】定式化（問題文を理解して数式にする）をする。
3) 【ChatGPT】定式化した内容をPluginに投げる
4) 【Plugin】問題をうけとり、解いて再度ChatGPTに返す
5) 【ChatGPT】問題文に合わせて回答を変換して表示する。


## 最初に
あまり記載の少ない超重要なことを説明します。
それはChatGPTのSettings→General→Open plugin devtoolsを有効にすることです。

![SnapCrab_NoName_2023-5-28_12-44-17_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121950/456d1dc5-0cb1-4a70-ef29-a30aada477c0.png)

これでChatGPTの右側にプラグインの情報が出ますが、記載ミスがあれば、教えてくれます

![SnapCrab_NoName_2023-5-28_12-48-21_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121950/255952bd-f87d-5484-a325-90094b8b13d9.png)

正直、これだけでもこの記事を読んだ70％の価値があります。


## マニフェストファイルを作る。

.well-known/ai-plugin.json ファイルを作成します。
どんなプラグインか説明するものです。

記載ルールはPlugin説明のページに記載があります。
以外に文字制限などに引っ掛かりますので、注意しましょう。文字制限に引っ掛かると、なぜかプラグインが動かない状態になります。

![SnapCrab_NoName_2023-5-28_12-41-50_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121950/79adeec1-6d6d-564c-d96d-9bc0e527cbc4.png)

私が作成したマニフェストは以下の通り。
```ai-plugin.json
{
    "schema_version": "v1",
    "name_for_human": "MLP Plugin (no auth)",
    "name_for_model": "MLPPlugin",
    "description_for_human": "Plugin for solve linear programming problem.",
    "description_for_model": "Plugin for solve linear programming problem. You can request to solve linear programming problems in lp file format and get the solution",
    "auth": {
      "type": "none"
    },
    "api": {
      "type": "openapi",
      "url": "http://localhost:5003/openapi.yaml",
      "is_user_authenticated": false
    },
    "logo_url": "http://localhost:5003/logo.png",
    "contact_email": "XXXX",
    "legal_info_url": "https://github.com/84monta/mlpplugin/blob/main/LICENSE"
  }
```


## OPENAPI定義
openapi.yamlファイルを作成します。
私は、元ファイルをベースにChatGPTに聞きながら雰囲気で作りました。ルールはマニフェストと同様、OpenAIのページを確認するのが良いでしょう。
マニフェストと同様にdevtools有効で記載ミスなどは発見できます。
ということで、賢いコメントはありません。

```
openapi: 3.0.1
info:
  title: Solving Linear Programming Plugin
  description: A plugin that allows the user to solve linear programming problem using ChatGPT. If you do not know the user's username, ask them first before making queries to the plugin. Otherwise, use the username "global".
  version: 'v1'
servers:
  - url: http://localhost:5003
paths:
  /MLP/{username}:
    post:
      operationId: solve
      summary: Solve Linear Programming(.lp file) and return the solution
      parameters:
      - in: path
        name: username
        schema:
            type: string
        required: true
        description: The name of the user.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/solveRequest'
      responses:
        '200':
          description: Solution for the LP file
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  solution:
                    type: object

components:
  schemas:
    solveRequest:
      type: object
      required:
      - fomulafile
      properties:
        fomulafile:
          type: string
          format: binary
          description: The .lp file to be solved
```

## プログラム本体
あとはPythonの本体（適当です）

```Python:main.py
import json

import quart
import quart_cors
from quart import request, jsonify
import tempfile
from pyscipopt import Model
from io import StringIO
import contextlib

app = quart_cors.cors(quart.Quart(__name__), allow_origin="https://chat.openai.com")

@app.post("/MLP/<string:username>")
async def solve(username):
    out = StringIO()
    err = StringIO()
    
    try:
        req_data = await request.get_json()
        fomulafile_txt = req_data["fomulafile"]
        with tempfile.NamedTemporaryFile(delete=False, suffix=".lp") as temp:
            temp.write(fomulafile_txt.encode())
            temp_name = temp.name
        model = Model()
        with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
            model.redirectOutput()
            ret = model.readProblem(temp_name)
 
        model.optimize()

        #set result to dict
        result = {}
        result["status"] = model.getStatus()

        # if variable number is 0 return error
        if len(model.getVars()) == 0:
            result["status"] = ".lp file format Error\n There is no defined Variables.fix it and try again"
        elif result["status"] == "optimal":
            result["objval"] = model.getObjVal()
            #convert all variables and values to dict
            result["sol"] = {}
            for v in model.getVars():
                result["sol"][v.name] = model.getVal(v)
        else:
            result["objval"] = None

        response_txt = json.dumps(result)
    except Exception as e:
        response_txt =  "Got following Error, review the syntax of .lp file text" + "\n\n" + out.getvalue() + "\n" + err.getvalue()
        
    return jsonify(response_txt)

@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("./.well-known/ai-plugin.json") as f:
        text = f.read()
        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()
        return quart.Response(text, mimetype="text/yaml")

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

if __name__ == "__main__":
    main()
```

# プラグイン作成時のトラブルと対処
実際プラグインを作成したときに直面したトラブルとその対処を記載します。
本プラグイン特有の内容も多く含みますが、参考にはなるかと思います。

## トラブル1． うまく定式ができない
これは今でも解決できていないのですが、開発大規模な問題も解けるようにという下心もあり、SCIPが対応している中のmps という形式で線形計画の問題をやり取りしていました。
### 問題点
mpsは、無駄を省いてパラメータ群のような表記となるため、前後のつながりから確率的に文章を生成するGPTと相性が最悪でした。また、mpsファイルは最大化問題に対応しておらず、使用する際は目的関数を―1倍して最小化問題としてとらえて解く必要があります。が、GPTはそのような行間を学習し、反映するレベルにはありませんでした。
### 対策
数学的な記載に近い.lpファイルを採用しました。
その対策により、入力情報と、ChatGPTの生成する情報の関連性が高くなりより正しく問題を定義することができるようになりました。

** これは結構重要なテクニックだと思います **

## トラブル2． それでもうまく定式ができない

### 問題点
比較的 記述の内容も数学的でネット上にも比較的例が多いと思われる.lpファイルですが、ChatGPTでは文法エラーのレベルでミスを連発します。

### 対策
ChatGPTを使ってプログラムを作成される方はわかると思いますが、たとえばプログラムでエラーが出た場合は、ChatGPTにそのエラーを渡して、修正してもらいます。
このプラグインの場合、try , except でエラーを拾って、SCIPの出力をChatGPT側で対処してもらうようにしました。

## トラブル3． エラーが拾えない
pyscipoptを介してSCIPをコールしていますが、その出力がネットにあるようなtracebackで拾うことができません。

### 問題点
具体的には以下のコードを記載しましたが、

```Pythohn: NGコード
response_txt = e.__class__.__name__ + ": " + " ".join(traceback.TracebackException.from_exception(e).format())   
```

以下の出力が出た場合に

```Text: ターミナル出力
[reader_lp.c:168] ERROR: Syntax error in line 9 ('*'): cannot have '*' outside of quadratic part. 
  input: maximize Profit: 500*x + 400*y + 600*z; 

                              ^
[reader_lp.c:3364] ERROR: Error <-2> in function call
``` 

" ERROR: Error <-2> in function call" くらいしか拾ってくれませんでした。

### 対策
ChatGPTに普通に聞いてもダメだったので、Browsingにて「PySCIPOptのマニュアルやインターネット上のやり取りを確認して、解決方法がないか探してください。」と依頼して糸口を見つけて、対策を詰めていきました。

![SnapCrab_NoName_2023-5-28_14-9-16_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121950/c152f586-16cd-2beb-2071-efb32b6865c7.png)


## トラブル4． 無限にやり取りされてしまう。

### 問題点
エラーが拾えるようになったが、1つ修正すると2つ前を忘れるなどが発生して、ChatGPTとPluginのピンポンが発生し、一気にTokenを消費してしまう。
### 対策
現在考え中（金をたんまり用意して、APIからGPT4使うくらい？）。いい案があったら教えてください。


# 免責事項

.lp ファイルをもらって、添付ファイル作成し読み込みというお行儀のよくないやり方で問題を解いています。
セキュリティなど全く考慮していないので、本コードの使用は個人、またはコントロール可能な範囲でおお楽しみください。



# 最後に

以下を入力として本当はこの記事を書きたかったなぁ。それは別で上げておきます。

```
■はじめに
・この記事はChatGPTで書いています。
・この記事はChatGPTのPluginを開発する話です。
■全体の流れ
・ 環境：Windows＋WSL
・ なぜ作ろうと思ったか： Wolframはとても強力だが、線形計画問題など最適化問題が解けない。SCIPが最近ライセンス変更され使いやすくなった。
・最初に考えた流れ
 1) 問題を役割分担を考える。問題を読み解いてLPへ定式化→GPTの仕事
 2) 定式化した問題を解いて、解を返す→SCIP
 3) 解を受け取り、問題へ合わせて回答する→GPT
・発生した問題
　1) うまく定式化できない。
　　最初 mps形式で作成してSCIPにファイルを渡していたが、うまくいかない。
　　これは
 　　 1-1)mpsが最小化にしか対応していな
　　 1-2) mpsは（比較的）大規模問題向けナタメ、パラメータなどをそっけなく記載する方法で、つながりが見えにくく、LLMでは作成しにくい
　　1-3) （おそらく）MPSファイルそのものの情報がネット上に少なく作りにくい
　　対策：比較的、記載が数式に近いLPファイル形式を採用した。
　2) それでも文法エラーでうまく.lpファイルを作成できない
　　対策：エラーの出力を返してGPTに直してもらう。
　3) エラー出力が取り込めない。
　　pyscipoptパッケージを使用しているが、Model.readProblem実行時のターミナルに出力されているメッセージが全文取り込めず（しかも最も重要な部分）、戻りでChatGPTに修正依頼が出来ない。
　　[2023-05-28 10:00:37 +0900] [1350] [INFO] 172.23.48.1:49656 OPTIONS /MLP/ChatGPT 1.1 200 0 1425
[reader_lp.c:168] ERROR: Syntax error in line 9 ('*'): cannot have '*' outside of quadratic part. 
  input: maximize Profit: 500*x + 400*y + 600*z; 

                              ^
[reader_lp.c:3364] ERROR: Error <-2> in function call
　　対策：Model.redirectOutput()をつかうことでキャッチできるようになった。
　　　　　ファイル出力は問題らしいが今回使ってないのでOK
　　4) ChatGPT Plugin作成時の時間上限が厳しい。
　　　対策：のんびりやる
■総括
　色々楽しい
```

没Chat：GPT4制限中にやり取りした内容

![SnapCrab_NoName_2023-5-28_12-16-45_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121950/63aa7a29-e6d7-fdab-b0ae-d8be9b079454.png)