AWS + nestjsでsse API構築

AWS

概要

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"]


コメント

タイトルとURLをコピーしました