websocketを用いた対話型AIとの会話アプリケーションの開発①

現在、対話型AIを用いたアプリケーションは多くあり、言語処理用のライブラリや学習モデルも流通しています。
そこで、言語処理の簡単なアルゴリズムを用いて、自分で開発しようと試みました。
その過程を記事にしています。
今回の記事ではwebsocketによる通信の確認と、特定のメッセージの自動返信を行います。


## 必要なもの 
* Pythonを実行、デバッグできる環境
* 各種フレームワークやライブラリ(後ほど紹介します)

# 動作環境
* OS:Windows11
* Docker Desktop:4.23.0
* Docker:24.0.6
* VScode:1.82.2
* Python:3.11.3

# 環境構築
私は、Dockerを通じてAnaconda環境を構築しています。以下の記事のように構築していますので、ぜひ参考にしてください。

https://qiita.com/sumiyoshi01/items/06d5277692f0444473b9

# ディレクトリ構成
プロジェクトを作るにあたって、ホストマシン上のディレクトリと、コンテナ内のディレクトリを示します。マウントできていればこれらのファイルは自動的に同期されるので、実際に操作するのはコンテナ内のファイルになります。


```cmd
docker run -v "%cd%":/notebook ^
-p 8888:8888 --rm -it continuumio/anaconda3:latest ^
jupyter notebook --ip 0.0.0.0 --allow-root --no-browser ^
--NotebookApp.disable_check_xsrf=True --NotebookApp.token='' ^
--NotebookApp.password='' /notebook
```
最終的なディレクトリ構成は以下で示すようになります。
ただし、最初のディレクトリではtalk直下は何もありません。実装フェーズでファイル作成を行います。
## ローカル
カレントディレクトリとして指定したプロジェクトファイルは`Pyremote`です。
```
Pyremote/
└── talk/
    ├── templates/
    │   └── Index.html
    └── talkwithme.py
```

## コンテナ 
マウントするために作成したディレクトリは`notebook`です。
直下にtalkディレクトリを設置し、その中でアプリケーションを開発していきます。
``` 
notebook/
└── talk/
    ├── templates/
    │   └── Index.html
    └── talkwithme.py
```

# 利用するライブラリ
今回websocketを利用するにあたって参照したライブラリやフレームワークを紹介します。

## そもそもwebsocketって何？
WebSocketは、インターネットにおける``通信プロトコルの一つ``で、全二重通信チャンネルを提供します。WebSocketは、WebブラウザとWebサーバー間の双方向通信を可能にするために設計されており、特にリアルタイムアプリケーションに適しています。通常のHTTP接続と異なり、WebSocketは持続的で、一度接続が確立されると、クライアントとサーバーの間で任意の時点でデータを送受信できます。

* WebSocketの特徴
WebSocketはリアルタイムのデータ転送を可能にします。これにより、チャットアプリケーション、オンラインゲーム、株価のライブ更新などのリアルタイムアプリケーションの構築が可能になります。また、HTTPポーリングに比べて、データの送受信において非常に遅延が少ないです。これにより、迅速なデータのやり取りが可能です。さらに、一度のハンドシェイクで持続的な接続を確立するため、頻繁な接続開設と終了に伴うオーバーヘッドが削減されます。
## websocketを実現するフレームワーク


* Flask

> Flask is a lightweight WSGI web application framework. It is designed to 
> make getting started quick and easy, with the ability to scale up to 
> complex applications. It began as a simple wrapper around Werkzeug and 
> Jinja and has become one of the most popular Python web application 
> frameworks.
> Flask offers suggestions, but doesn't enforce any dependencies or project layout. It
> is up to the developer to choose the tools and libraries they want to use. There are 
> many extensions provided by the community that make adding new functionality easy. 

引用元:[palletsprojects](https://palletsprojects.com/p/flask/)

簡単に言うとFlaskは、
` 軽量で、簡単・迅速に始められることを目指し、また複雑なアプリケーションへと拡張できるWSGIウェブアプリケーションフレームワークである。依存関係やプロジェクトレイアウトを強制せず、開発者が自由にツールやライブラリを選べるようになっている。コミュニティによる多くの拡張機能も利用可能で、新たな機能の追加も簡単である。`
というものです。

今回はこのフレームワークの` Flask-SocketIO `ライブラリを利用してwebsocketの実装を行います。
コンテナ内のターミナルでコマンドを実行してインストールを行います。
まずはフレームワークをインストールしましょう。
```
pip install flask
```
追加で先ほど挙げたライブラリもインストールします。
```
pip install flask-socketio
```
これで準備ができました。

# 実装
では、さっそく実装していきましょう。
`talk`ディレクトリ直下にサーバーとのやり取りを定義するファイル(`talkwithme.py`)を作ります。
```talkwithme.py
from flask import Flask, render_template
from flask_socketio import SocketIO

app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")

@app.route('/')
def index():
    return render_template('Index.html')  

@socketio.on('message')
def handle_message(message):
    print('Received message:', message)
    response = 'Hello, Client!'
    socketio.emit('message', response)

if __name__ == '__main__':
    socketio.run(app, debug=True, port=5000)

```

## 解説

上記のコードについて簡単な解説をします。

1. インポート
` from flask import Flask, render_template `と` from flask_socketio import SocketIO `で、FlaskとFlask-SocketIOライブラリから必要なクラスや関数をインポートしています。

2. 初期化
` app = Flask(__name__)とsocketio = SocketIO(app, cors_allowed_origins="*") `で、FlaskアプリケーションとSocketIOインスタンスを初期化しています。

3. ルーティング
` @app.route('/') `で、ルートURL（ホームページ）がアクセスされた時にindex関数が呼ばれるように定義しています。index関数はIndex.htmlテンプレートをレンダリングします。

4. イベント設定
` @socketio.on('message') `で、クライアントからのmessageイベントをリッスンし、handle_message関数をトリガーします。この関数は、メッセージを受信し、クライアントに対してレスポンスを送り返します。

5. デバッグ用のポート設定
` if __name__ == '__main__': `で、このスクリプトが直接実行された場合、アプリケーションが起動し、デバッグモードでポート5000で実行します。

## フロント
次に、フロント側のファイルである`Index.html`を作成します。
普通Webアプリケーションでは、スタイルシートとビュー用のファイルは分けます。
しかし今回は簡易的に表示の確認が取れればいいので、すべてビュー用のファイルに記載しています。
htmlとcss、Javascriptを用いてコーディングしました。

```Index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>WebSocket Chat Example</title>
    <style>
        #chat {
            overflow-y: scroll;
            height: 300px;
            border: 1px solid #ccc;
            margin-bottom: 10px;
        }
        .myMessage {
            text-align: right;
            background-color: #d1e7f3;
            margin: 5px;
            padding: 10px;
            border-radius: 10px;
        }
        .otherMessage {
            text-align: left;
            background-color: #f1f1f1;
            margin: 5px;
            padding: 10px;
            border-radius: 10px;
        }
    </style>
</head>
<body>
    <div id="chat"></div>
    <textarea id="messageInput"></textarea>
    <button onclick="sendMessage()">Send Message</button>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.0.3/socket.io.js"></script>
    <script>
        const socket = io.connect('http://localhost:5000');

        socket.on('connect', function() {
            console.log('Connected to Server');
        });

        socket.on('message', function(data) {
            console.log('Received message:', data);
            let chat = document.getElementById('chat');
            let messageDiv = document.createElement('div');
            messageDiv.className = 'otherMessage';
            messageDiv.textContent = data;
            chat.appendChild(messageDiv);
            chat.scrollTop = chat.scrollHeight;
        });

        function sendMessage() {
            const messageInput = document.getElementById('messageInput');
            const message = messageInput.value;
            console.log('Sending message:', message);
            socket.emit('message', message);
            
            let chat = document.getElementById('chat');
            let messageDiv = document.createElement('div');
            messageDiv.className = 'myMessage';
            messageDiv.textContent = message;
            chat.appendChild(messageDiv);
            chat.scrollTop = chat.scrollHeight;

            messageInput.value = '';
        }
    </script>
</body>
</html>
```

# 動作確認
コーディングが完了したので、実際にサーバーに接続して動作を確認します。
まず、サーバーに接続できるか確認しましょう。
`talkwithme.py`を実行して、ターミナルを観察します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290653/58f23dd4-d772-d62e-aa37-253f3e800e0c.png)

このような表示がされていれば、接続は成功しています。FlaskによるWebアプリケーションの開発サーバーが起動し、ファイルの変更を監視し、デバッグモードで動作していることを示しています。

では、[http://127.0.0.1:5000/](http://127.0.0.1:5000/)にアクセスしてアプリケーションが適切に動くか見てみましょう。
ページにアクセスし、実際に"Hi!"とメッセージを送ってみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290653/b752b313-7c8c-bd66-685b-1adc9e5f3bcc.png)

問題なく動作していることがわかります。
ターミナル上では通信のログが流れ、受け取ったメッセージを表示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290653/eb1b8865-66b2-6b81-53a7-3bf4c0a80dad.png)

今回のアプリケーションでは、こちらがどのようなメッセージを送信しても"Hello, Client!"としか返してくれません。
この状態から自然言語処理のロジックを組んで、ある程度まともな返信が返ってくるように開発していきます。

# さいごに
今回は、websocketを利用した基本的なチャットアプリケーションを制作しました。
フレームワークとライブラリをインストールして、通信・ルーティングを定義し、表示用のページを作成すればあっという間に簡易的なアプリケーションが完成します。
外観・デザインはともかく、アプリケーションが適切に稼働することを実感してもらえると嬉しいです。
私自身Pythonを用いたアプリケーション制作はほぼ初めてなので、これからもいろいろ試しながら模索していきたいです。
今後の投稿では実際に自然言語処理を用いて種類豊富な応答ができるように実装していこうと思います。