Slide 1

Slide 1 text

Copyright coconala Inc. All Rights Reserved. OpenAI APIをNuxt.js に組み込んでみた DATA : 2023.10.28 Yuta Ichihara

Slide 2

Slide 2 text

Copyright coconala Inc. All Rights Reserved. 目次 1. 自己紹介 2. ココナラについて 3. AIアシスタント機能(β)について 4. Nuxt 2へのOpenAI API組み込み 5. Nuxt 3へのOpenAI API組み込み 2

Slide 3

Slide 3 text

Copyright coconala Inc. All Rights Reserved. 自己紹介 chapter 01 3

Slide 4

Slide 4 text

Copyright coconala Inc. All Rights Reserved. 自己紹介 名前:市原 雄太 ニックネーム:いっちー 仕事:フロントエンド 趣味:猫、ゲーム 4

Slide 5

Slide 5 text

Copyright coconala Inc. All Rights Reserved. ココナラについて chapter 02 5

Slide 6

Slide 6 text

Copyright coconala Inc. All Rights Reserved. 一人ひとりが「自分のストーリー」を 生きていく世の中を作る 6 Vision 個人の知識・スキル・経験を可視化し、必要とするすべての人に結びつけ、 個人をエンパワーメントするプラットフォームを提供する Mission ココナラのVision&Mission

Slide 7

Slide 7 text

Copyright coconala Inc. All Rights Reserved. 7 「知識・スキル・経験」を 売り買いできる スキルマーケット

Slide 8

Slide 8 text

Copyright coconala Inc. All Rights Reserved. AIアシスタント機能(β)について chapter 03 8

Slide 9

Slide 9 text

Copyright coconala Inc. All Rights Reserved. AIアシスタント機能(β)をリリース 9

Slide 10

Slide 10 text

Copyright coconala Inc. All Rights Reserved. AIアシスタントが サービスについての説明文を提案 出品者はサービスの特徴、自身の簡単な経歴を入 力するだけ 10

Slide 11

Slide 11 text

Copyright coconala Inc. All Rights Reserved. UX より早くレスポンスが返って くるイベントストリーム形式 にすることでより良いユー ザー体験を提供したい Delivery Cost 要件 11 無制限に利用されるとコス トがかさむため、ユーザー ごとに1日あたりの利用回 数を制限したい 技術検証の必要がなく、よ り早くリリースできる手法を 採用したい

Slide 12

Slide 12 text

Copyright coconala Inc. All Rights Reserved. NuxtのServer Middlewareを採用 12

Slide 13

Slide 13 text

Copyright coconala Inc. All Rights Reserved. UX Delivery Cost NuxtのServer Middlewareを採用 13 過去にServer Middleware を使ってAPIの実装経験が あった イベントストリーム形式での レスポンスが可能 (AWS Lamdaがレスポンス ストリーミング対応していた がAPI Gatewayが対応して いなかった) Server Middleware内でバ リデーションを行うことで ユーザーごとの利用回数を 制限することが可能

Slide 14

Slide 14 text

Copyright coconala Inc. All Rights Reserved. Nuxt 2へのOpenAI API組み込み chapter 04 14 ※ここに注釈テキストが入れられます。

Slide 15

Slide 15 text

Copyright coconala Inc. All Rights Reserved. Server Middleware側の実装 15

Slide 16

Slide 16 text

Copyright coconala Inc. All Rights Reserved. Server Middlewareでやるのは3つだけ 16 ・リクエストを受け取る ・OpenAI APIにリクエストを送る ・イベントストリーム形式でレスポンスする

Slide 17

Slide 17 text

Copyright coconala Inc. All Rights Reserved. リクエストを受け取る 17 const middleware: { path: string; handler: ServerMiddleware } = { path: '/api/openai', async handler (req, res) { if (req.method !== 'POST') { res.statusCode = 404 res.end() return } let body = '' for await (const chunk of req) { body += chunk.toString() } const bodyJson = JSON.parse(body) const { prompt } = bodyJson // 〜 省略 〜 } }

Slide 18

Slide 18 text

Copyright coconala Inc. All Rights Reserved. OpenAI APIにリクエストを送る 18 const chatCompletionCreateParams: OpenAI.ChatCompletionCreateParamsStreaming = { model: 'gpt-3.5-turbo', messages: [ { role: 'user', content: prompt } ], stream: true // レスポンスをストリーミングで受け取る } const stream = await openAi.chat.completions.create(chatCompletionCreateParams)

Slide 19

Slide 19 text

Copyright coconala Inc. All Rights Reserved. イベントストリーム形式でレスポンスする 19 res.setHeader('Content-Type', 'text/event-stream;charset=utf-8') res.setHeader('Cache-Control', 'no-cache, no-transform') for await (const chunk of stream) { const chunkString = chunk.choices.map(v => v.delta.content).filter(v => !!v).join() res.write(chunkString) } res.end()

Slide 20

Slide 20 text

Copyright coconala Inc. All Rights Reserved. ブラウザ側の実装 20

Slide 21

Slide 21 text

Copyright coconala Inc. All Rights Reserved. ブラウザ側の実装 21 import { ref } from 'vue' import axios from 'axios' const prompt = ref('') const response = ref('') const send = () => { axios.post( '/api/openai', { prompt: prompt.value }, { onDownloadProgress: (v) => { response.value = v.target.responseText } } ) }

Slide 22

Slide 22 text

Copyright coconala Inc. All Rights Reserved. Nuxt 3へのOpenAI API組み込み chapter 05 22

Slide 23

Slide 23 text

Copyright coconala Inc. All Rights Reserved. server/ directoryを利用 23

Slide 24

Slide 24 text

Copyright coconala Inc. All Rights Reserved. server/ directoryを利用 引用:https://nuxt.com/docs/guide/directory-structure/server 24 server directory配下にファイルを配 置すると自動でエンドポイントが作ら れる

Slide 25

Slide 25 text

Copyright coconala Inc. All Rights Reserved. defineEventHandler関数を使って定義 25 export default defineEventHandler(event => { const { req, res } = event.node // 〜 省略 〜 })

Slide 26

Slide 26 text

Copyright coconala Inc. All Rights Reserved. req、resを取り出したあとはほぼ一緒 26

Slide 27

Slide 27 text

Copyright coconala Inc. All Rights Reserved. Nuxt 3の公式ドキュメントに気になる記述が… 27

Slide 28

Slide 28 text

Copyright coconala Inc. All Rights Reserved. sendStream使えないか…? 引用:https://nuxt.com/docs/guide/directory-structure/server#sending-streams 28 experimentalではあるものの sendStream使えればもう少し処理 が簡潔にできそう🤔

Slide 29

Slide 29 text

Copyright coconala Inc. All Rights Reserved. Nuxt 3でsendStream使ってみた 番外編 29

Slide 30

Slide 30 text

Copyright coconala Inc. All Rights Reserved. 愚直にそのままsendStreamへ渡してみるが… 引用:https://nuxt.com/docs/guide/directory-structure/server#sending-streams 30 const stream = await openAi.chat.completions.create(chatCompletionCreateParams) return sendStream(event, stream)

Slide 31

Slide 31 text

Copyright coconala Inc. All Rights Reserved. エラー…(Streamにpipeがない) 31 [nuxt] [request error] [unhandled] [500] data.pipe is not a function at ./node_modules/h3/dist/index.mjs:556:10 at new Promise () at sendStream (./node_modules/h3/dist/index.mjs:555:10) at ./.nuxt/dev/index.mjs:645:10 at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async Object.handler (./node_modules/h3/dist/index.mjs:990:19) at async Server.toNodeHandle (./node_modules/h3/dist/index.mjs:1065:7)

Slide 32

Slide 32 text

Copyright coconala Inc. All Rights Reserved. OpenAIのStream が期待してるものとなんか違う… 32

Slide 33

Slide 33 text

Copyright coconala Inc. All Rights Reserved. NodeJS.ReadableStreamをimplements してない… 33

Slide 34

Slide 34 text

Copyright coconala Inc. All Rights Reserved. AsyncIterableを作ってReadableのインスタンスを 生成する必要がある 34 const stream = await openAi.chat.completions.create(chatCompletionCreateParams) const chatCompletionAsyncIterator = async function* (): AsyncIterable { for await (const chunk of stream) yield chunk.choices.map(v => v.delta.content).filter(v => !!v).join() } const readable = Stream.Readable.from(chatCompletionAsyncIterator()) res.setHeader('Content-Type', 'text/event-stream;charset=utf-8') res.setHeader('Cache-Control', 'no-cache, no-transform') return sendStream(event, readable)

Slide 35

Slide 35 text

Copyright coconala Inc. All Rights Reserved. あまり簡潔にはならなかった… 35

Slide 36

Slide 36 text

Copyright coconala Inc. All Rights Reserved. まとめ 36

Slide 37

Slide 37 text

Copyright coconala Inc. All Rights Reserved. まとめ 37 ・OpenAI APIでストリーミングを使った APIは思った以上にサクッと実  装で きた ・認証、バリデーションなどの処理も一緒に組み込める ・Nuxt 3へもほぼそのままのコードで移行できそう

Slide 38

Slide 38 text

Copyright coconala Inc. All Rights Reserved. ご清聴ありがとうございました 38