💬

ChainlitのCopilotで簡単にReactアプリにチャット機能を追加する方法

2024/03/25に公開

はじめに

Chainlitとは

Chainlitは、Pythonで簡単にチャットアプリを作成できるライブラリです。
PythonだけでUI含めたWebアプリを作成できます。
同じようなものとしては、StreamlitやGradioなどがありますが、Chainlitはチャットアプリを作成することに特化しています。

本記事の目的

Chainlitのドキュメントを眺めていると、Copilotという実装方法があると記載されていました。
内容を読んでみたところ、既存のWebサイトに数行のコードを追加するだけで、Chainlitで作成したチャットアプリを埋め込むことができるようです。
(下図が公式の動作イメージ)


Copilot動作イメージ

出典

個人的にこれはかなり欲していたものなのですが、検索してもあまり情報が出てこなかったので、実際にどんな感じか試してみた結果、かなり簡単に実装できたのでその手順を共有します。

自分の会社ではReactが使われることが多いので、埋め込み先のサンプルとしてReactを選択しましたが、他のフレームワークでもほとんど変わらないと思います。

作業手順

1. Chainlitアプリを作成

まずはChainlitでチャットアプリを作成します。

中身はなんでもいいので、オウム返しするだけのシンプルなものを作成しました。

import chainlit as cl

@cl.on_message
async def on_message(message: cl.Message):
    response = f"{message.content}"
    await cl.Message(response).send()

動かすとこんな感じです。


作成したChainlitアプリ

動作確認も問題なくできたので、次に進みます。

2. Reactアプリを作成

次にReactアプリを作成しますが、残念ながら私がReactのスキルが全然ないので、公式のチュートリアルの三目並べのコードをそのままお借りしました。

https://ja.react.dev/learn/tutorial-tic-tac-toe

こちらのページの一番下にあるコードから、「Fork」を押してCodeSandboxを開き、プロジェクト内のコードをすべてダウンロードしました。


Reactチュートリアル 三目並べ

当然ですが、こちらも問題なく動作しました。

3. CopilotでChainlitを埋め込む

Chainlitのドキュメントに従って、埋め込みを行います。

<body>タグの最後に以下のコードを追加する必要があるので、public/index.htmlを編集します。

public/index.html
<head>
  <meta charset="utf-8" />
</head>
<body>
  <!-- ... -->
  <script src="http://localhost:8000/copilot/index.js"></script>
  <script>
    window.mountChainlitWidget({
      chainlitServer: "http://localhost:8000",
    });
  </script>
</body>

これだけで準備完了なので、ReactとChainlitを両方起動して試してみます。


Copilotの動作確認

もっと手こずるかと思っていたのですが、あっさり上手くいきました🎉

4. 関数呼び出しを試してみる

なんとこれだけではなく、Chainlitのチャットアプリからフロントアプリの関数を呼び出すこともできるようです。
これができると、チャットにフロントの情報を渡したり、逆にチャットからフロントを操作することもでき、色々幅が広がりそうです。
こちらも試してみました。

https://docs.chainlit.io/deployment/copilot#function-calling

オウム返しをやめて、チャットでメッセージを送信すると(メッセージ内容によらず)三目並べの戦況を返答するようにしてみたいと思います。

chainlit/main.py
import chainlit as cl

@cl.on_message
async def on_message(message: cl.Message):
-    response = f"{message.content}"
-    await cl.Message(response).send()
+    if cl.context.session.client_type == "copilot":
+        fn = cl.CopilotFunction(name="getBattleStatus", args={"msg": message.content})
+        battle_status = await fn.acall()
+        await cl.Message(battle_status).send()
+    else:
+        await cl.Message(message.content).send()
react/src/App.js
~~~

+ // 戦況を取得する関数
+ function getBattleStatus() {
+   const status = document.querySelector('.status');
+   const statusText = status ? status.textContent : '';
+   const boardRows = document.querySelectorAll('.board-row');
+   const boardState = [];
+   let battle_status = "";
+   boardRows.forEach((row) => {
+     const rowState = [];
+     row.childNodes.forEach((square) => {
+       rowState.push(square.textContent || '-');
+     });
+     boardState.push(rowState.join(','));
+   });
+   battle_status = "status: " + statusText + "\n";
+   battle_status += "boardState: \n";
+   return battle_status + boardState.join('\n');
+ }
+ 
+ window.addEventListener("chainlit-call-fn", (e) => {
+   const { name, args, callback } = e.detail;
+   if (name === "getBattleStatus") {
+     // メッセージを受け取れることの確認のためにログを出力
+     console.log(args);
+     callback(getBattleStatus());
+   }
+ });

Chainlitでメッセージを送信すると、Reactのwindow.addEventListenerを通じてgetBattleStatus関数が呼び出され、戦況が返されるようにしています。
また、Chainlitで送信されたメッセージをコンソールログに出力しているので、双方向のデータの受け渡しができていることが確認できるはずです。

再び両アプリを起動して動作確認を行います。

上手くいきました🎉

成果物

以下のリポジトリに今回作ったコードをまとめています。

https://github.com/0msys/chainlit-copilot-test

READMEにも書きましたが、動かす場合はクローンして以下を実行してください。
(要Docker)

docker compose up -d

以下にアクセスすると、Reactアプリが表示されます。

http://localhost:3000/

停止する場合は以下を実行してください。

docker compose down

まとめ

ChainlitのCopilotを使って、Reactアプリに埋め込むことができました。

関数呼び出しもできるので、色々活用の幅が広がりそうです。
(バックエンドAPIたたくのとどっちがいいかは個別に判断が必要だと思いますが)
例えば今回の三目並べのアプリでいうと、戦況からLLMを通してAIと対戦したり、解説させたりといったこともできそうです。

以前書いたこちらの記事で紹介したようなAIエージェントをChainlit側で実装すれば、既存のWebアプリにヘルプチャットボットなどを簡単に追加することができるので、かなり便利だと思います。

Chainlitはチャットボット作成に対してはかなり便利だし、見た目も良い感じなので気に入っているのですが、StreamlitやGradioに比べるとまだまだ知名度が低いので、この記事で「Chainlit良いじゃん!」ってなってくれる人が増えてくれると嬉しいです!

Discussion