OpenAIのChatGPT API (gpt-3.5-turbo )を使ってみた
こんにちはstdと申します。
OpenAIのChatGPT API(gpt-3.5-turbo)が使えるようになった旨のメールが届きましたので試しに実装してみましたので実装方法を書いていきます。
先日作った個人開発のAI Senseiに組み込んでみます。
できたもの
実装
このコードを参考にしました。
事前にOpenAIのAPI KEYを.envに保存しておきます。
OPENAI_API_KEY=YOUR_API_KEY
型を作成
text-davinci-003ではpayloadでprompt: stringを渡していましたが、
gpt-3.5-turboではこのファイルで作っているmessages: Message[]を渡します。
//src/types/types.ts
export type Role = "system" | "user" | "assistant";
export type Message = {
role: Role;
content: string;
};
export interface OpenAIStreamPayload {
model: string;
messages: Message[];
temperature: number;
top_p: number;
frequency_penalty: number;
presence_penalty: number;
max_tokens: number;
stream: boolean;
n: number;
}
export interface ChatResponseData {
id: string;
object: string;
created: string;
model: string;
choices: { delta: Partial<Message> }[];
}
ChatGPT APIを呼び出す関数を作成
ほぼ参考のコードと同じですが
fetchのURLをChatGPT用に変更しているのと
textの取り方を
const text = responseData.choices[0].delta.content;
に変更しています。
//src/utils/OpenAIStream.ts
import {
createParser,
ParsedEvent,
ReconnectInterval,
} from "eventsource-parser";
import { OpenAIStreamPayload, ChatResponseData } from "@/types/types";
export async function OpenAIChatStream(payload: OpenAIStreamPayload) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let counter = 0;
const res = await fetch("https://api.openai.com/v1/chat/completions", {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ""}`,
},
method: "POST",
body: JSON.stringify(payload),
});
if (!res.ok) {
throw new Error(res.statusText);
}
const stream = new ReadableStream({
async start(controller) {
// callback
function onParse(event: ParsedEvent | ReconnectInterval) {
if (event.type === "event") {
const data = event.data;
// https://platform.openai.com/docs/api-reference/chat/create
if (data === "[DONE]") {
controller.close();
return;
}
try {
const json = JSON.parse(data);
const responseData = json as ChatResponseData;
const text = responseData.choices[0].delta.content;
if (!text || (counter < 2 && (text.match(/\n/) || []).length)) {
// this is a prefix character (i.e., "\n\n"), do nothing
return;
}
const queue = encoder.encode(text);
controller.enqueue(queue);
counter++;
} catch (e) {
// maybe parse error
controller.error(e);
}
}
}
// stream response (SSE) from OpenAI may be fragmented into multiple chunks
// this ensures we properly read chunks and invoke an event for each SSE event stream
const parser = createParser(onParse);
// https://web.dev/streams/#asynchronous-iteration
for await (const chunk of res.body as any) {
parser.feed(decoder.decode(chunk));
}
},
});
return stream;
}
Edge Functions作成
apiディレクトリにopenai.tsを作成。
このファイルで先ほどのOpenAIStreamを呼び出してストリームでレスポンスを返していきます。
messagesは先頭にシステムのコンテンツを挿入します。
これにより全体のChatGPTの回答内容に指示できるようです。
//src/pages/api/openai.ts
import { OpenAIChatStream } from "@/utils/OpenAIStream";
import { Message, OpenAIStreamPayload } from "@/types/types";
const systemMessage: Message = {
role: "system",
content:
"You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.",
};
if (!process.env.OPENAI_API_KEY) {
throw new Error("Missing env var from OpenAI");
}
export const config = {
runtime: "edge",
};
const handler = async (req: Request): Promise<Response> => {
try {
const { messages } = (await req.json()) as {
messages?: Message[];
};
if (!messages) {
return new Response("No prompt in the request", { status: 400 });
}
const payload: OpenAIStreamPayload = {
model: "gpt-3.5-turbo",
messages: [systemMessage, ...messages],
temperature: 0.7,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
max_tokens: 1000,
stream: true,
n: 1,
};
const stream = await OpenAIChatStream(payload);
return new Response(stream);
} catch (error: any) {
console.error(error);
return new Response("Something went wrong", { status: 500 });
}
};
export default handler;
hook作成
Edge Functions呼び出し用のhookを作ります。
レスポンスがストリームで流れてくるので以前の文字列の後ろに結合して保存していきます。
//src/hooks/useOpenAI.ts
import { showNotification } from "@mantine/notifications";
import { Message } from "@/types/types";
const useOpenAI = () => {
const fetchStream = async (
messages: Message[],
setLoading: (value: boolean) => void,
setReply: (value: string, isNew?: boolean) => void,
) => {
setLoading(true);
try {
setReply("", true);
const body = JSON.stringify({ messages });
const responseVercel = await fetch("/api/openai", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: body,
});
console.log("Edge function returned.");
if (!responseVercel.ok) {
throw new Error(responseVercel.statusText);
}
// This data is a ReadableStream
const data = responseVercel.body;
if (!data) {
return;
}
const reader = data.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value);
setReply(chunkValue);
}
} catch (error: any) {
alert(error);
}
setLoading(false);
};
return {
fetchStream,
};
};
export { useOpenAI };
Front
あとは先ほどのhookをfrontから呼び出したら完成です。
過去のプロンプトとAIからの回答と最新のプロンプトをOpenAIに渡します。
roleがuser, assistantと交互に入ります。
assistantはChatGPTの返答です。
//抜粋
const messages: Message[] = [
{ role: "user", content: "こんにちは" },
];
const setReply = (newVal: string, isNew?: boolean) => {
setValue((v) => isNew ? newVal: v + newVal)
};
await fetchStream(messages, setAILoading, setReply);
これで完成です。
ぜひ試してみてください。
すぐOpenAIの無料分なくなるかもですが笑
一緒に開発してくれる方募集!
もし参加したい方いらっしゃいましたらお声がけしていただけるとうれしいです!
Discussion
streamで実践している記事があまりなかったので助かりました!
ところで、こちらをNestJSでやってみたのですが、うまくいきませんでした。
NextJSがstreamをよしなにやってくれているため、NestJSではもう少し改良する必要?があるようです。