FlaskとiFlytek Spark APIを使用したチャットボットの構築

本記事では、iFlytek Spark(讯飞星火)大規模言語モデルのAPIをPythonバックエンドとFlaskフレームワークに統合し、シンプルなチャットインターフェースを構築する方法について解説します。実装にはWebSocket通信を使用し、リアルタイムな対話機能を実現します。

事前に以下のライブラリをインストールしてください:

pip install flask websocket-client

1. 設定ファイルの作成

最初に、API認証情報を管理するモジュールを作成します。iFlytekのコンソールから取得したAPPID、API Secret、API Keyを設定します。ここでは、ドメインとエンドポイントURLも定義します。

# settings.py
def get_spark_config():
    """
    iFlytek Spark APIの認証情報と接続設定を返します。
    """
    return {
        "app_id": "YOUR_APP_ID",
        "api_secret": "YOUR_API_SECRET",
        "api_key": "YOUR_API_KEY",
        "domain": "generalv3",       # モージュルのバージョンに応じて変更
        "ws_url": "wss://spark-api.xf-yun.com/v3.1/chat" # 対応するエンドポイント
    }

2. Spark APIクライアントの実装

次に、WebSocket接続の確立、認証URLの生成、およびメッセージの送受信を担当するクライアントクラスを実装します。このクラスは、認証プロセスをカプセル化し、Global変数に依存しない設計にします。

# spark_client.py
import hashlib
import base64
import hmac
import json
import ssl
import websocket
from datetime import datetime
from time import mktime
from wsgiref.handlers import format_date_time
from urllib.parse import urlencode, urlparse

class SparkChatClient:
    def __init__(self, config):
        self.app_id = config['app_id']
        self.api_key = config['api_key']
        self.api_secret = config['api_secret']
        self.ws_url = config['ws_url']
        self.domain = config['domain']
        self.collected_messages = []

    def _create_auth_url(self):
        """認証用のURLを生成します"""
        parsed_url = urlparse(self.ws_url)
        host = parsed_url.netloc
        path = parsed_url.path
        
        now = datetime.now()
        date = format_date_time(mktime(now.timetuple()))
        
        # 署名の作成
        signature_origin = f"host: {host}\ndate: {date}\nGET {path} HTTP/1.1"
        signature_sha = hmac.new(
            self.api_secret.encode('utf-8'),
            signature_origin.encode('utf-8'),
            digestmod=hashlib.sha256
        ).digest()
        signature_base64 = base64.b64encode(signature_sha).decode(encoding='utf-8')
        
        authorization_origin = f'api_key="{self.api_key}", algorithm="hmac-sha256", headers="host date request-line", signature="{signature_base64}"'
        authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8')
        
        params = {
            "authorization": authorization,
            "date": date,
            "host": host
        }
        return f"{self.ws_url}?{urlencode(params)}"

    def _on_message(self, ws, message):
        """メッセージ受信時のコールバック"""
        data = json.loads(message)
        code = data['header']['code']
        if code != 0:
            print(f"エラーが発生しました: {code}")
            ws.close()
            return

        payload = data['payload']
        content = payload['choices']['text'][0]['content']
        self.collected_messages.append(content)
        
        status = payload['choices']['status']
        if status == 2:  # 最後のチャンク
            ws.close()

    def _on_error(self, ws, error):
        print(f"WebSocketエラー: {error}")

    def _on_close(self, ws, *args):
        pass

    def _on_open(self, ws):
        """接続確立時にリクエストを送信"""
        params = {
            "header": {
                "app_id": self.app_id,
                "uid": "user_123"
            },
            "parameter": {
                "chat": {
                    "domain": self.domain,
                    "temperature": 0.5,
                    "max_tokens": 2048
                }
            },
            "payload": {
                "message": {
                    "text": self.prompt_data
                }
            }
        }
        ws.send(json.dumps(params))

    def chat(self, user_input):
        """チャットの実行メソッド"""
        # 入力データの整形
        self.prompt_data = [{"role": "user", "content": user_input}]
        self.collected_messages = []

        ws = websocket.WebSocketApp(
            self._create_auth_url(),
            on_message=self._on_message,
            on_error=self._on_error,
            on_close=self._on_close,
            on_open=self._on_open
        )
        ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
        
        return "".join(self.collected_messages)

3. Flaskアプリケーションの設定

Flaskサーバーを設定し、フロントエンドからのリクエストを受け付けるエンドポイントを作成します。ここでは、作成したSparkChatClientをインスタンス化し、対話処理を行います。

# app.py
from flask import Flask, render_template, request, jsonify
from settings import get_spark_config
from spark_client import SparkChatClient

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False

# クライアントの初期化
config = get_spark_config()
chat_client = SparkChatClient(config)

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

@app.route('/api/chat', methods=['POST'])
def chat():
    try:
        req_data = request.get_json()
        user_message = req_data.get('message', '')
        
        if not user_message:
            return jsonify({'error': 'メッセージが空です'}), 400
            
        response_text = chat_client.chat(user_message)
        return jsonify({'reply': response_text})
        
    except Exception as e:
        print(f"サーバーエラー: {e}")
        return jsonify({'error': '内部サーバーエラーが発生しました'}), 500

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

4. フロントエンドの作成 (HTML/JS)

templatesフォルダ内にchat.htmlを作成します。このファイルでは、ユーザーインターフェースとAJAX通信を実装します。jQueryなどの外部ライブラリに依存せず、標準のFetch APIを使用して軽量化します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Spark AI Chat</title>
    <style>
        body { font-family: sans-serif; display: flex; justify-content: center; height: 100vh; margin: 0; background-color: #f4f4f9; }
        .main-container { width: 100%; max-width: 500px; display: flex; flex-direction: column; height: 100%; background: white; box-shadow: 0 0 15px rgba(0,0,0,0.1); }
        .chat-area { flex: 1; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
        .message { max-width: 80%; padding: 10px 15px; border-radius: 15px; line-height: 1.4; }
        .msg-user { align-self: flex-end; background-color: #007bff; color: white; border-bottom-right-radius: 2px; }
        .msg-bot { align-self: flex-start; background-color: #e9ecef; color: #333; border-bottom-left-radius: 2px; }
        .input-zone { padding: 15px; border-top: 1px solid #ddd; display: flex; gap: 10px; }
        .input-zone input { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 5px; outline: none; }
        .input-zone button { padding: 10px 20px; background-color: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; }
        .input-zone button:disabled { background-color: #ccc; }
    </style>
</head>
<body>
    <div class="main-container">
        <div class="chat-area" id="chatBox"></div>
        <form class="input-zone" id="chatForm">
            <input type="text" id="userInput" placeholder="メッセージを入力..." autocomplete="off" required>
            <button type="submit" id="sendBtn">送信</button>
        </form>
    </div>

    <script>
        const chatForm = document.getElementById('chatForm');
        const chatBox = document.getElementById('chatBox');
        const userInput = document.getElementById('userInput');
        const sendBtn = document.getElementById('sendBtn');

        function appendMessage(text, sender) {
            const div = document.createElement('div');
            div.className = `message ${sender === 'user' ? 'msg-user' : 'msg-bot'}`;
            div.textContent = text;
            chatBox.appendChild(div);
            chatBox.scrollTop = chatBox.scrollHeight;
        }

        chatForm.addEventListener('submit', async (e) => {
            e.preventDefault();
            const text = userInput.value.trim();
            if (!text) return;

            appendMessage(text, 'user');
            userInput.value = '';
            sendBtn.disabled = true;

            try {
                const response = await fetch('/api/chat', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ message: text })
                });

                const data = await response.json();
                if (data.reply) {
                    appendMessage(data.reply, 'bot');
                } else {
                    appendMessage("エラー: " + (data.error || "応答なし"), 'bot');
                }
            } catch (err) {
                appendMessage("通信エラーが発生しました", 'bot');
            } finally {
                sendBtn.disabled = false;
                userInput.focus();
            }
        });
    </script>
</body>
</html>

以上のファイル構成(settings.py, spark_client.py, app.py, templates/chat.html)でプロジェクトを作成し、python app.pyを実行すると、ローカルホスト上でチャットボットを起動できます。

タグ: Python flask iFlytek-Spark websocket web-development

6月23日 23:59 投稿