Upgrade to Pro — share decks privately, control downloads, hide ads and more …

OpenAI APIをNuxt.jsに組み込んでみた

coconala_engineer
November 20, 2023
780

OpenAI APIをNuxt.jsに組み込んでみた

2023/10/28のVue Fes Japanの登壇資料になります

coconala_engineer

November 20, 2023
Tweet

More Decks by coconala_engineer

Transcript

  1. Copyright coconala Inc. All Rights Reserved. 目次 1. 自己紹介 2.

    ココナラについて 3. AIアシスタント機能(β)について 4. Nuxt 2へのOpenAI API組み込み 5. Nuxt 3へのOpenAI API組み込み 2
  2. Copyright coconala Inc. All Rights Reserved. 一人ひとりが「自分のストーリー」を 生きていく世の中を作る 6 Vision

    個人の知識・スキル・経験を可視化し、必要とするすべての人に結びつけ、 個人をエンパワーメントするプラットフォームを提供する Mission ココナラのVision&Mission
  3. Copyright coconala Inc. All Rights Reserved. UX より早くレスポンスが返って くるイベントストリーム形式 にすることでより良いユー

    ザー体験を提供したい Delivery Cost 要件 11 無制限に利用されるとコス トがかさむため、ユーザー ごとに1日あたりの利用回 数を制限したい 技術検証の必要がなく、よ り早くリリースできる手法を 採用したい
  4. Copyright coconala Inc. All Rights Reserved. UX Delivery Cost NuxtのServer

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

    04 14 ※ここに注釈テキストが入れられます。
  6. Copyright coconala Inc. All Rights Reserved. Server Middlewareでやるのは3つだけ 16 ・リクエストを受け取る

    ・OpenAI APIにリクエストを送る ・イベントストリーム形式でレスポンスする
  7. 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 // 〜 省略 〜 } }
  8. 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)
  9. 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()
  10. 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 } } ) }
  11. Copyright coconala Inc. All Rights Reserved. server/ directoryを利用 引用:https://nuxt.com/docs/guide/directory-structure/server 24

    server directory配下にファイルを配 置すると自動でエンドポイントが作ら れる
  12. Copyright coconala Inc. All Rights Reserved. defineEventHandler関数を使って定義 25 export default

    defineEventHandler(event => { const { req, res } = event.node // 〜 省略 〜 })
  13. Copyright coconala Inc. All Rights Reserved. エラー…(Stream<ChatCompletionChunk>に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 (<anonymous>) 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)
  14. Copyright coconala Inc. All Rights Reserved. AsyncIterableを作ってReadableのインスタンスを 生成する必要がある 34 const

    stream = await openAi.chat.completions.create(chatCompletionCreateParams) const chatCompletionAsyncIterator = async function* (): AsyncIterable<string> { 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)
  15. Copyright coconala Inc. All Rights Reserved. まとめ 37 ・OpenAI APIでストリーミングを使った

    APIは思った以上にサクッと実  装で きた ・認証、バリデーションなどの処理も一緒に組み込める ・Nuxt 3へもほぼそのままのコードで移行できそう