概要
nestjsを使用してserver side eventを利用してchatGPTからのレスポンスをストリームするAPIを構築する最小限のサンプルコード
構築手順
nest
下記のコマンドをプロジェクトディレクトリで実施
pnpm i @nestjs/cli
nestプロジェクトを作成する
npx nest new {プロジェクト名}
src/chatgpt.controller.ts
に簡易的なSSEエンドポイントを追加
import { Controller, Get, Query, Sse } from '@nestjs/common';
import { Observable } from 'rxjs';
import { ChatGptService } from './chatgpt.service';
@Controller('chat')
export class ChatController {
constructor(private readonly chatGptService: ChatGptService) {}
@Get('stream')
@Sse()
stream(@Query('prompt') prompt: string): Observable<any> {
return new Observable((subscriber) => {
this.chatGptService.streamChatCompletion(prompt)
.then((stream) => {
let buffer = '';
stream.on('data', (chunk) => {
buffer += chunk.toString('utf8');
// 行単位で分割
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 最後は中途半端な行の可能性があるので残す
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue; // 空行はスキップ
if (trimmed.startsWith('data:')) {
const dataStr = trimmed.replace(/^data:\s*/, '');
if (dataStr === '[DONE]') {
subscriber.complete();
return;
}
// JSONパース
try {
const json = JSON.parse(dataStr);
const content = json.choices?.[0]?.delta?.content;
if (content) {
// ユーザーへテキストチャンクを逐次送信
subscriber.next(content);
}
} catch (err) {
console.error('JSON parse error:', err);
// パース失敗しても次チャンクを待つ
}
}
}
});
stream.on('end', () => {
subscriber.complete();
});
stream.on('error', (err) => {
subscriber.error(err);
});
})
.catch((err) => {
subscriber.error(err);
});
});
}
}
src/chatgpt.service.ts
を追加する
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { Readable } from 'stream';
@Injectable()
export class ChatGptService {
private readonly apiUrl = 'https://api.openai.com/v1/chat/completions';
private readonly apiKey = process.env.GPT_API_KEY;
async streamChatCompletion(prompt: string): Promise<Readable> {
const response = await axios.post(
this.apiUrl,
{
model: 'gpt-3.5-turbo',
stream: true,
messages: [{ role: 'system', content: prompt }],
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
responseType: 'stream',
},
);
// Axiosのresponse.dataはNode.jsのReadableストリームです。
return response.data as Readable;
}
}
ローカルで動作確認
pnpm run start:dev
-アプリケーションをDockerとしてデプロイするため、Dockerfileを作成
# Use the official Node.js image as the base image
FROM node:20
# Set the working directory inside the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install the application dependencies
RUN npm install
# Copy the rest of the application files
COPY . .
# Build the NestJS application
RUN npm run build
# Expose the application port
EXPOSE 3000
# Command to run the application
CMD ["node", "dist/main"]
下記コマンドでビルドする
node_moduleがローカルに存在するとCOPYコマンドでエラーが発生するので削除しておく
docker build -t {コンテナ名} .
下記コマンドでコンテナ実行
docker run --rm -p 3000:3000 {コンテナ名}
`http://localhost:3000/chat/stream?prompt=hello`にアクセスして、ストリームレスポンスが返却されればOK
匿名ボリュームとしてマウント(ホストにnode_moduleが存在しないかつコンテナ内でnode_moduleをインストールしている場合)
docker run --rm -p 3000:3000 -v .:/usr/src/
app -v /usr/src/app/node_modules {コンテナ名}
単体テストの実行
docker run --rm {コンテナ名} npm test
コンテナへのログイン
docker exec -it {コンテナID} /bin/bash
コンテナ立ち上げメモ
docker run --rm -p 3000:3000 -v $(pwd):/usr/src/app -v /usr/src/app/node_modules sse-api pnpm run start:dev
コンテナの停止
docker stop {コンテナID}
ECSへのデプロイ
Dockerfileを作成する
# Use the official Node.js image as the base image
FROM node:20
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN npm install -g pnpm
# Set the working directory inside the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install the application dependencies
RUN pnpm install
# Copy the rest of the application files
COPY . .
# Build the NestJS application
RUN pnpm run build
# Expose the application port
EXPOSE 3000
# Command to run the application
CMD ["node", "dist/main"]
コメント