AccueilClientsExpertisesBlogOpen SourceContact

23 octobre 2024

Comment simuler une réponse streamée avec le SDK Vercel AI

4 minutes de lecture

Comment simuler une réponse streamée avec le SDK Vercel AI
🇺🇸 This post is also available in english

Chez Premier Octet, nous développons le SaaS Postula, une application permettant d'importer ses flux RSS et de générer facilement des posts LinkedIn à partir d'articles.

L'un des principaux composants est un widget permettant de générer des posts LinkedIn à partir d'un flux RSS. Nous voulions montrer ce widget (visible ici) dès la homepage, pour des raisons de démonstration : un exemple concret vaut mieux que des mots.

Le problème : celui-ci fait des appels API vers OpenAI, appels qui nous sont facturés.

Nous avons donc eu l'idée de simuler la réponse de manière progressive, pour donner l'illusion d'un vrai appel à un modèle LLM tel que GPT sans le coût associé.

L'existant

Dans notre composant, nous utilisons le hook useCompletion de Vercel AI SDK pour générer le contenu des posts LinkedIn :

component.tsx
import { useCompletion } from 'ai'

const { complete } = useCompletion({
  api: '/api/completion/post',
})

Du côté de l'API, nous avons donc un endpoint /api/completion/post qui fait un appel à OpenAI :

api/completion/post/route.ts
import { openai } from '@ai-sdk/openai'
import { streamText } from 'ai'

export async function POST() {
  const result = await streamText({
    model: openai('gpt-4o'),
    prompt: `Write an Haiku about React.js`,
  })

  return result.toDataStreamResponse()
}

Afin de voir le format de la réponse, nous allons faire un appel à cet endpoint puis inspecter la réponse via l'onglet "Network" de notre navigateur :

Headers
HTTP/1.1 200 OK
vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch
content-type: text/plain; charset=utf-8
x-vercel-ai-data-stream: v1
Date: Tue, 22 Oct 2024 15:41:15 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
Réponse
0:"Java"
0:"Script"
0:" dances"
0:","
0:"  \n"
0:"Components"
0:" weave"
0:" through"
0:" the"
0:" DOM"
0:","
0:"  \n"
0:"React"
0:" breath"
0:"es"
0:" new"
0:" life"
0:"."
e:{"finishReason":"stop","usage":{"promptTokens":14,"completionTokens":18},"isContinued":false}
d:{"finishReason":"stop","usage":{"promptTokens":14,"completionTokens":18}}

Les morceaux de texte sont envoyés au fur et à mesure, ligne par ligne, avec "0:" en préfixe puis à la fin, des informations indiquant que la réponse est terminée.

Nous allons donc simuler cette réponse.

Commençons par créer la fonction qui va découper le texte en chunks.

Création des chunks

Maintenant que nous connaissons la structure de la réponse, nous pouvons créer une fonction qui va découper un texte en chunks :

export const chunkText = (inputText: string): string[] => {
  // Découpe le texte en un tableau de mots et espaces
  const chunks = inputText.match(/\S+|\s/g) || []

  return chunks.map((chunk) => {
    // Gère les saut de lignes
    if (chunk === '\n') {
      return '0:"\\n"\n'
      // Gère les espaces
    } else if (chunk.trim() === '') {
      return chunk
        .split('')
        .map((char) => (char === '\n' ? '0:"\\n"\n' : `0:"${char}"\n`))
        .join('')
      // Gère les mots
    } else {
      return `0:"${chunk}"\n`
    }
  })
}

Cette fonction découpe le texte en morceaux (mots, espaces) et les prépare à être envoyés sous forme de réponse incrémentielle. Le préfixe 0: est ajouté pour imiter le format de réponse d'API que nous avons observé précédemment.

Création du endpoint pour streamer la réponse

La prochaine étape consiste à créer une route d'API qui va streamer notre texte chunk par chunk.

api/completion/demo/post/route.ts
export async function POST() {
  const text = `Lorem ipsum dolor sit amet...` // Texte à streamer
  const predefinedChunks = chunkText(text) // Découpe du texte en chunks

  const encoder = new TextEncoder()

  const stream = new ReadableStream({
    async start(controller) {
      // Pause de 1s pour simuler l'attente
      await new Promise((resolve) => setTimeout(resolve, 1000))

      let index = 0

      function push() {
        if (index < predefinedChunks.length) {
          const chunk = predefinedChunks[index]
          controller.enqueue(encoder.encode(chunk)) // Envoi du chunk encodé
          index++

          // Intervalle de 10ms entre chaque envoi
          // pour simuler un stream avec un peu de délai
          setTimeout(push, 10)
        } else {
          controller.close() // Ferme le stream à la fin
        }
      }

      // Démarre le stream
      push()
    },
  })

  const headers = new Headers({
    'Content-Type': 'text/plain; charset=utf-8',
    'Transfer-Encoding': 'chunked',
  })

  return new Response(stream, { headers })
}

Cette route envoie chaque morceau de texte progressivement avec un délai de 10ms (avec un délai initial de 1s) donnant ainsi l’impression d’une réponse en direct.

Test du endpoint avec notre composant

Nous pouvons maintenant utiliser notre endpoint de démo pour simuler une réponse d'un modèle LLM :

useCompletion({
  api: isDemo ? '/api/completion/demo/post' : '/api/completion/post',
})

Cette approche permet de basculer facilement entre la démo et le vrai endpoint, en ajoutant une props qui active le mode démo.

Bien sûr, n'oubliez pas de sécuriser votre endpoint /api/completion/post en vérifiant si l'utilisateur est authentifié et dispose d'assez de crédits.

La réponse est bien streamée, et le composant fonctionne comme prévu !

À noter qu'il existe depuis la version 3.4 de Vercel AI SDK une fonction streamText qui permet de mock une réponse de stream :

import { streamText } from 'ai'
import { convertArrayToReadableStream, MockLanguageModelV1 } from 'ai/test'

const result = await streamText({
  model: new MockLanguageModelV1({
    doStream: async () => ({
      stream: convertArrayToReadableStream([
        { type: 'text-delta', textDelta: 'Hello' },
        { type: 'text-delta', textDelta: ', ' },
        { type: 'text-delta', textDelta: `world!` },
      ]),
    }),
  }),
})

Mais celle-ci est destinée à des tests unitaires ou E2E, et ne simule donc pas de délai entre chaque chunk. Ce qui rend le résultat moins réaliste.

👋

À découvrir également

Notre workflow de traduction d’articles avec l’API OpenAI

08 Oct 2024

Notre workflow de traduction d’articles avec l’API OpenAI

Chez Premier Octet, nous aimons partager nos articles avec le plus grand nombre, pour cela nous avons mis en place un workflow de traduction automatisée.

par

Baptiste

Comment construire des applications IA avec Vercel AI SDK — Part I

06 Sep 2024

Comment construire des applications IA avec Vercel AI SDK — Part I

Vercel a dévoilé une nouvelle version de Vercel AI SDK, conçu pour simplifier le processus de développement d'applications IA en fournissant des outils et des bibliothèques prêts à l'emploi pour les développeurs. Je vous propose un tour d'horizon des exemples clés abordés par Nico Albanese, ingénieur chez Vercel, dans sa vidéo didactique publiée sur YouTube.

par

Vincent

AI et UI #1 - Filtres intelligents avec le SDK Vercel AI et Next.js

12 Jun 2024

AI et UI #1 - Filtres intelligents avec le SDK Vercel AI et Next.js

Dans ce premier article d’une série consacrée à l’IA et l’UI, je vous propose de découvrir différentes manières d’intégrer ces modèles d’IA dans vos applications React pour améliorer l’expérience utilisateur.

par

Baptiste

Premier Octet vous accompagne dans le développement de vos projets avec nextjs

En savoir plusNous contacter
18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub
X (Twitter)
Flux RSS

Naviguez à vue