0
1

はじめに

前回の記事ではFunctionsでGPT-4oのAPIを作りました。今回はNext.jsを使ってフロントエンドを構築する方法を解説します。バックエンドのコードも少し変更を加えています。

バックエンドのコードの変更内容

  1. リクエストボディの処理の拡張:

    • systemmax_tokenstemperaturetop_p のパラメータが追加されました。
    • max_tokenstemperaturetop_p にはデフォルト値が設定しました(それぞれ1000、0、1)。
  2. Azure Open AI API呼び出しのパラメータの拡張:

    • max_tokenstemperaturetop_p のパラメータが client.chat.completions.create メソッドに追加しました。

以下が更新後のバックエンドのコードです。フロントエンドを実行する前にデプロイして置いてください:

function_app.py
import azure.functions as func
import openai
from azurefunctions.extensions.http.fastapi import Request, StreamingResponse
import asyncio
import os
from dotenv import load_dotenv

load_dotenv()

# Azure Function App
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

endpoint = os.getenv("AZURE_OPEN_AI_ENDPOINT")
api_key = os.getenv("AZURE_OPEN_AI_API_KEY")

# Azure Open AI
deployment = "gpt-4o"

client = openai.AsyncAzureOpenAI(
    azure_endpoint=endpoint,
    api_key=api_key,
    api_version="2024-02-01"
)

# Get data from Azure Open AI
async def stream_processor(response):
    async for chunk in response:
        if len(chunk.choices) > 0:
            delta = chunk.choices[0].delta
            if delta.content:  # Get remaining generated response if applicable
                await asyncio.sleep(0.05)
                yield delta.content

# HTTP streaming Azure Function
@app.route(route="http_trigger", methods=[func.HttpMethod.POST])
async def http_trigger(req: Request) -> StreamingResponse:
    try:
        body = await req.json()  # req.get_json() を req.json() に変更
        prompt = body.get("prompt")
        system = body.get("system")
        max_tokens = body.get("max_tokens", 1000)  # デフォルト値を1000に設定
        temperature = body.get("temperature", 0)  # デフォルト値を0に設定
        top_p = body.get("top_p", 1)  # デフォルト値を1に設定

        if not prompt:
            return func.HttpResponse("プロンプトが見つかりませんでした", status_code=400)
        if not system:
            return func.HttpResponse("システムメッセージが見つかりませんでした", status_code=401)

    except ValueError:
        return func.HttpResponse("無効なJSON形式のリクエストボディです", status_code=400)

    azure_open_ai_response = await client.chat.completions.create(
        model=deployment,
        temperature=temperature,
        max_tokens=max_tokens,
        top_p=top_p,
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": prompt}
        ],
        stream=True
    )

    return StreamingResponse(stream_processor(azure_open_ai_response), media_type="text/event-stream")

開発環境

  • Next.js
  • TypeScript
  • CSS
  • fetch API
  • Tailwind CSS
  • Azure Functions
  • Python 3.11

導入

ステップ1: プロジェクトのセットアップ

  1. Node.js Command Promptを開きます。*
  2. フロントエンドプロジェクトを作りたいフォルダーに移動します。*
  3. プロジェクトを作成します。以下のコマンドを実行します:*
    npx create-next-app@latest
    
  4. プロンプトに従って y を入力し、プロジェクト名を入力します。その他の質問はそのままエンターを押します。*
  5. プロジェクトディレクトリに移動します:*
    cd <作成したプロジェクト>
    
  6. 依存関係をインストールします:*
    npm install
    
  7. 開発サーバーを起動します:*
    npm run dev
    
  8. VSCodeを開きます:*
    code .
    

ステップ2: 環境変数の設定

プロジェクトのルートディレクトリに .env.local ファイルを作成し、以下の内容を追加します:

NEXT_PUBLIC_ENDPOINT=http://localhost:7071/api/http_trigger

ステップ3: フロントエンドコードの追加

app/page.tsx ファイルを以下のコードで置き換えます:

page.tsx
"use client";

import { useState } from 'react';

const Home = () => {
    const [prompt, setPrompt] = useState('');
    const [system, setSystem] = useState('');
    const [maxTokens, setMaxTokens] = useState(1000);
    const [temperature, setTemperature] = useState(0);
    const [topP, setTopP] = useState(1);
    const [response, setResponse] = useState('');
    const [showParameters, setShowParameters] = useState(false);

    const handleSubmit = async (event: React.FormEvent) => {
        event.preventDefault();
        setResponse('');

        try {
            const res = await fetch(process.env.NEXT_PUBLIC_ENDPOINT!, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    prompt,
                    system,
                    max_tokens: maxTokens,
                    temperature,
                    top_p: topP
                })
            });

            if (!res.ok) {
                throw new Error('Network response was not ok');
            }

            const reader = res.body?.getReader();
            const decoder = new TextDecoder('utf-8');
            let result = '';

            if (reader) {
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;
                    result += decoder.decode(value, { stream: true });
                    setResponse(result);
                }
            }
        } catch (error) {
            console.error('Error:', error);
            setResponse('Error occurred while processing your request.');
        }
    };

    return (
        <div className="flex flex-col items-center justify-center min-h-screen py-2 bg-gray-100">
            <div className="flex w-full max-w-4xl p-8 bg-white rounded-lg shadow-md">
                {showParameters && (
                    <div className="w-1/3 p-4 border-r border-gray-300">
                        <h2 className="mb-4 text-xl font-bold text-center">Parameters</h2>
                        <form className="flex flex-col">
                            <label htmlFor="prompt" className="mb-2 text-sm font-medium text-gray-700">System Message:</label>
                            <textarea
                                id="prompt"
                                className="p-2 mb-4 border border-gray-300 rounded-md"
                                value={prompt}
                                onChange={(e) => setPrompt(e.target.value)}
                                rows={4}
                                required
                            />
                            <label htmlFor="maxTokens" className="mb-2 text-sm font-medium text-gray-700">Max Tokens:</label>
                            <input
                                type="number"
                                id="maxTokens"
                                className="p-2 mb-4 border border-gray-300 rounded-md"
                                value={maxTokens}
                                onChange={(e) => setMaxTokens(Number(e.target.value))}
                                required
                            />
                            <label htmlFor="temperature" className="mb-2 text-sm font-medium text-gray-700">Temperature:</label>
                            <div className="flex items-center mb-4">
                                <input
                                    type="number"
                                    id="temperature"
                                    className="p-2 border border-gray-300 rounded-md w-20 mr-2"
                                    value={temperature}
                                    onChange={(e) => setTemperature(Number(e.target.value))}
                                    step="0.01"
                                    min="0"
                                    max="1"
                                    required
                                />
                                <input
                                    type="range"
                                    id="temperature-slider"
                                    className="flex-1"
                                    value={temperature}
                                    onChange={(e) => setTemperature(Number(e.target.value))}
                                    step="0.01"
                                    min="0"
                                    max="1"
                                />
                            </div>
                            <label htmlFor="topP" className="mb-2 text-sm font-medium text-gray-700">Top P:</label>
                            <div className="flex items-center mb-4">
                                <input
                                    type="number"
                                    id="topP"
                                    className="p-2 border border-gray-300 rounded-md w-20 mr-2"
                                    value={topP}
                                    onChange={(e) => setTopP(Number(e.target.value))}
                                    step="0.01"
                                    min="0"
                                    max="1"
                                    required
                                />
                                <input
                                    type="range"
                                    id="topP-slider"
                                    className="flex-1"
                                    value={topP}
                                    onChange={(e) => setTopP(Number(e.target.value))}
                                    step="0.01"
                                    min="0"
                                    max="1"
                                />
                            </div>
                        </form>
                    </div>
                )}
                <div className={`w-full ${showParameters ? 'w-2/3' : 'w-full'} p-4`}>
                    <h2 className="mb-4 text-xl font-bold text-center">Response</h2>
                    <form className="flex flex-col" onSubmit={handleSubmit}>
                        <label htmlFor="system" className="mb-2 text-sm font-medium text-gray-700">Message:</label>
                        <input
                            type="text"
                            id="system"
                            className="p-2 mb-4 border border-gray-300 rounded-md"
                            value={system}
                            onChange={(e) => setSystem(e.target.value)}
                            required
                        />
                        <button type="submit" className="p-2 mb-4 text-white bg-blue-500 rounded-md hover:bg-blue-600">Submit</button>
                    </form>
                    <button
                        onClick={() => setShowParameters(!showParameters)}
                        className="p-2 mb-4 text-white bg-gray-500 rounded-md hover:bg-gray-600"
                    >
                        {showParameters ? 'Hide Parameters' : 'Show Parameters'}
                    </button>
                    <div className="p-4 bg-gray-200 rounded-md whitespace-pre-wrap">{response}</div>
                </div>
            </div>
        </div>
    );
};

export default Home;

ステップ4: CORSエラーの修正

Azure FunctionsのCORS設定で、localhost:3000 を許可する必要があります。Azureポータルで関数アプリのCORS設定を開き、http://localhost:3000 を追加します。

image.png

実行結果

image.png

まとめ

これで、Azure Functionsをバックエンドとして使用し、Next.jsを使ったフロントエンドが完成しました。フロントエンドからバックエンドにリクエストを送信し、リアルタイムでレスポンスを受け取ることができます。この記事が役に立ったら、ぜひ「いいね」や「シェア」をお願いします!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1