ChatGPTのノードエディタを作った話

## 前置き
こんにちは。半年ほど前になってしまいましたが、`ChatGraph`というChatGPTのノードエディタを作って公開していました。
https://uynet.booth.pm/items/4728147

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3437333837312f65356136333464372d343865342d393536622d356137302d3463346531633264613833362e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473871/36509f0a-6cb2-cf56-778f-d2baac482ed8.png)


少し話題になり、GIGAZINEにも記事が上がってたので詳しい使い方などはこちらをどうぞ(ありがとう)
https://gigazine.net/news/20230517-chatgraph-chatgpt-node-editor/

ソースコードはこちら
https://github.com/Uynet/ChatGraph-Beta

## 仕組み
Twitterにも少し書いたのですが、実は本質的にはChatGPTを扱うツールではありません。実態は任意のpythonコードを実行したときの標準出力を繋げるGUIで、その一つにChatGPTのサンプルがあるというものです。

https://x.com/uynet/status/1720376985380360464?s=20

このツイートで紹介しているコードはOpenAIにリクエストを送ってstreamが来る度にprintするというものです。
ChatGraph内で```chatGPTreq```というAPIを提供していて、トークン情報はこのAPI内でエディタ側で設定したものを参照しています。

このソースコードは一応ユーザーが書くこともできます。
https://github.com/Uynet/ChatGraph-Beta/blob/main/readme/spec.md

またシェルを使えるサンプルも用意していて、極端な話他のLLMにリクエストするコードを書いてexeファイルにしてしまうという手もあります。
https://x.com/NoContextAl/status/1654467895794233344?s=20

汎用性を求めすぎて、よく考えたら普通にビジュアルスクリプティングエディタになってました。
ChatGPT要素があるのは、別タブでトークンを入力する部分だけです。なんならトークンを直書きしたpythonコードを用意しておけば別に動きはするので、本当にAI関係なかったりします。
文字列をprintするものだったら別になんでも良いというエディタでした。

## 開発について
技術スタックはすべてpythonで、GUIライブラリはpyQtを使用しています。
- 選定理由
ChatGPTに聞いたらおすすめされたから
ChatGPTがpython得意だったので(自分はこの手のツールを作るのは始めてだったから、なんでも良かった)

- 反省点
ある程度進んできて面白そうになってきたので販売してみようかな、と考え始めた段階で、pyQtはコードを公開しない場合販売に有償ライセンスが必要ということを知ってしまった。
なのでエディタ自体はOSSにしていて、お布施版ではソースコードが含まれたjsonファイルを別売りするという手段を取っています。
あと動的型付け言語でアプリは二度と作りたくないなと思った。

### PyQtの感想
HTML/CSSとCanvasを直書きしているような感じでした。
CSSはそのまま使えたみたいなので、SCSSを読み込めるようにしたらちょっとは楽でした。
↓ちょっとしたパーサーみたいなもの
https://github.com/Uynet/ChatGraph-Beta/blob/main/src/utils/styles.py

本来はQt Designerというものでデザインしてレイアウトを読み込むためのライブラリらしいです。

## 苦労したとこ
### インスペクタ機能の双方向データバインディング
ChatGraphではインスペクタ機能というものがあります(画像左半分)。ノードの各プロパティを閲覧・編集できるというもので、編集はリアルタイムに反映されます。Unityのような感じです。
元々デバッグ用に作っていたのですが、思った以上に便利だったので本機能に組み込まれました。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3437333837312f35363964393932332d336335652d356262302d383963322d3463336631303362306161352e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473871/5cbde86f-2b2e-be49-d255-c9bd6641fe1e.png)

↓動いてる様子
https://x.com/NoContextAl/status/1649013023192014850?s=20


説明が難しいのですが、これは結構大変でした。一見簡単に見えるのですが、左のインスペクタを編集するとノードが変更されてその変更がインスペクタに通達されて...と無限ループが発生したりとかです。これは双方向データバインディングと呼ばれる設計が必要になります。これだけで記事ができてしまうので深くは触れません。これを実現するため大幅にリファクタリングしてて大変でした。
思いつきでなんでも実装するものではないな～



### GUI実装など
描画ライブラリの関係で、ノード内ウィンドウのサイズ変更や選択・コピペといった操作は自前で実装した。
↓動いている様子(音も出ます)
https://x.com/NoContextAl/status/1655585302574972937?s=20

windowのサイズを可変する様子
https://x.com/NoContextAl/status/1655524577383174144?s=20

マウスインタラクションなど
https://x.com/NoContextAl/status/1655525252905209856?s=20

テキストフィールド以外のウィンドウ、ソケット、辺などの部分についてはプリミティブな図形描画の機能だけでやっていて、ウィンドウを変更するとソケットの位置が追従するとか、本当に色々大変だった。
なんかイベント駆動をベースとしているGUIライブラリでやることではないですね。
ここ頑張っても別に新規性あるわけじゃないし、普通にUnity使えば良かった～

(ちなみに、効果音は自作しました)

## ファイル保存関連
作ったノードはjson形式でセーブ・ロードができます。
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3437333837312f31336265623064612d303965342d393334622d346462662d3434616165363737363731372e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473871/739924d2-42d7-684b-e632-b376a613ebd0.png)


### データ保存する時の意外な落とし穴
ネットワークであるため、ソケット同士の接続先の情報が必要になる。これはノードID+ソケットIDの一意な組み合わせとして保存するようにしている。
``` json
  "edges": [
    {
      "output": {
        "type": "output",
        "propertyType": "CHAT",
        "name": "False",
        "nodeId": "hYWORHsp",
        "socketId": 0
      },
      "input": {
        "type": "input",
        "propertyType": "CHAT",
        "name": "",
        "nodeId": "47yVGLd6",
        "socketId": 0
      }
    }
   ]
```
(↑こんな短いやつじゃなくて本当はUUID使ったほうがいいと思います)
IDは天文学的確率じゃないと衝突しないし、まぁその場でハッシュで作ればいいかな～と思ってたのですが、GUI上の操作でコピペしたり、同じファイルを複数回インポートしてきたりすると、IDが同一のノードが生成されるということがあり、ちゃんとシーン中で一意になるように管理する必要がありました。よく考えたらIPアドレスの振り分けと同じだし、確かに必要だな～

天文学的確率だと思ってたことが、別の要因によって意外と普通に起きて足元をすくわれてしまったので直感に反してて面白いな～と思った。気をつけましょう。

### モジュール機能
複数のノードからできるネットワークを1つのノードにまとめたら面白いかなと思って作った。実態としてはファイルパスのみを持っており、読み込み時に展開するというもの。

作ってみたらおもしろそう！と思ったけど、モジュール内でモジュールを使い始めるととんでもないことが起きます。
そう、**循環参照**ですね。
読み込み時にモジュール内で使われているすべてを初期化する、という実装にしていると、これで無限ループに陥ります。
なので循環参照は阻止するようにしたのですが.....再帰する構造があるネットワークを作りたかったので、動的インポートを実装しようとしていたのですが挫折してしまいまい残念でした。

とか言いつつ、作ってみると、中身でどう動いているのか見えないとあまり面白くないな～となりました。
思いつきで機能をつけるものではないな～

## リリース関連
- ユーザーがコードを書けるようにする場合やばいことが起きないかチェックする必要があってしんどい
自由度が高すぎるのは良くない

- ドキュメントを作るのが大変
機能が増えすぎて説明書やデモを作るというのがバカにならないくらい大変になり、完成してからリリースまで数日掛かった。このコストが段違いに減るという意味で、触っただけでわかるようなUXは望ましいなと思う。
デモ動画などは全然用意できておらず、どうしようと思っていたらGIGAZINEがものすごく丁寧に説明してくれて助かった。

## 感想
なんとなく2日くらいで作ってみようと思ってやり始めたら一ヶ月もやってしまった。
そもそもアプリを作るのは初めてだったので、リリースまでちゃんとやると色々勉強になり面白かった。
動的型付け言語は二度と使いたくないな～

AIにコードを書かせるとものすごい勢いで開発が進む一方、設計を怠ると個人では手に負えない量のメンテが必要になるということを意識しなければならないと思いました。

#### 今後
開発なのですが、AIは流行り廃りが激しいのと、自分は元々他にやりたいことがあるので続けないと思います。でもせっかく作ったので、面白い使い道思いついた人は教えてくれると嬉しいです。