はじめに
前回の記事ではFunctionsでGPT-4oのAPIを作りました。今回はNext.jsを使ってフロントエンドを構築する方法を解説します。バックエンドのコードも少し変更を加えています。
バックエンドのコードの変更内容
-
リクエストボディの処理の拡張:
-
system
、max_tokens
、temperature
、top_p
のパラメータが追加されました。 -
max_tokens
、temperature
、top_p
にはデフォルト値が設定しました(それぞれ1000、0、1)。
-
-
Azure Open AI API呼び出しのパラメータの拡張:
-
max_tokens
、temperature
、top_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: プロジェクトのセットアップ
- Node.js Command Promptを開きます。*
- フロントエンドプロジェクトを作りたいフォルダーに移動します。*
-
プロジェクトを作成します。以下のコマンドを実行します:*
npx create-next-app@latest
-
プロンプトに従って
y
を入力し、プロジェクト名を入力します。その他の質問はそのままエンターを押します。* -
プロジェクトディレクトリに移動します:*
cd <作成したプロジェクト>
-
依存関係をインストールします:*
npm install
-
開発サーバーを起動します:*
npm run dev
-
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
を追加します。
実行結果
まとめ
これで、Azure Functionsをバックエンドとして使用し、Next.jsを使ったフロントエンドが完成しました。フロントエンドからバックエンドにリクエストを送信し、リアルタイムでレスポンスを受け取ることができます。この記事が役に立ったら、ぜひ「いいね」や「シェア」をお願いします!