Tutorial: Chat con ChatGPT - Parte 2

Pato para el Taller de Innovación 🚀 de SCV
#articles

Crearemos una webapp desde la cual permitiremos a los usuarios comunicarse con un chat de OpenAI que podremos ajustar con nuestros parámetros.

Si han realizado la parte 1 a esta altura tienen una webapp basada en React que puede comunicarse con un servidor WebSocket y envía y recibe mensajes de chat.

Tecnologías que utilizaremos en este tutorial:

  • Node
  • React
  • Librería de OpenAI
  • WebSocket

Vamos a checkear que estemos utilizando versiones similares de Node:

node -v

Yo estoy usando la v20.16.0


Parte 2: Conectar nuestro chat a los servicios de OpenAI

OpenAI

Vamos a agregar la interfaz con los servicios de OpenAI. En la carpeta donde esté el servidor instalaremos la librería oficial de OpenAI

npm install openai --save

La librería de openai leerá por default la variable de entorno OPENAI_API_KEY como su key. Para definirla puede usarse la librería dotenv para leerla de un archivo .env o la opción --env-file=.env al correr node (en versiones superiores a Node v20). En ese caso recuerde agregar .env al .gitignore para evitar subir la key a un repositorio. Para simplificar la definiremos en la terminal.

En Mac o Linux

OPENAI_API_KEY=mi-key

En Windows:

set OPENAI_API_KEY=mi-key

Luego en chat-ws-server.js, al comienzo:

const OpenAI = require('openai');

const openai = new OpenAI();

OpenAI provee diferentes servicios, por ejemplo, generación de imágenes, generación de text to speech y otros, pero el que utilizaremos específicamente para chat se llama chat.completions. La documentación completa está en https://platform.openai.com/docs/guides/chat-completions

El parámetro principal de este método se llama messages y es un array de objetos, cada uno tendrá las props role y content. Role puede ser system, assistant y user.

System es un mensaje invisible al usuario, es para darle parámetros de contexto e instrucciones al asistente. Por ejemplo podría ser "Eres un asistente de una tienda que ofrece estos productos: ..." o "No respondas preguntas relacionadas con política". No necesariamente debe ser breve. Luego los de de role assistant y user se irán intercalando en la medida que la conversación se desarrolle, siendo assistant los que responda OpenAI. En este caso el historial de mensajes lo guardaremos de manera local en el server así que los incluiré en un array.

Dentro del listener on connection:

...
myWsServer.on('connection', (theConnection) => {
...

  // Creamos un array que va a contener el historial
  const chatHistory = [];
  theConnection.on('message', async (receivedBufferData) => {
    // El WebSocket recibe la información como Buffer data
    const dataAsStringifiedObject = receivedBufferData.toString();
    // En realidad sabemos que es un json stringificado con la propiedad text porque nosotros lo hemos armado así en el cliente
    const dataAsObject = JSON.parse(dataAsStringifiedObject);
    // Agregamos el nuevo mensaje del usuario al historial de chat
    chatHistory.push({
      role: 'user',
      content: dataAsObject.text,
    });
    // chat.completions es el nombre de este servicio de generación de texto de OpenAI, buscar opciones en la documentación
    const aiConversation = await openai.chat.completions.create({
      messages: [
        {
          role: 'system',
          content: `Eres un asistente especializado en información sobre cocina y recetas.`
        },
        // Enviamos el historial cada vez. Podríamos limitarlo a n cantidad de los últimos elementos de extenderse mucho.
        ...chatHistory,
      ],
      // Otros modelos podrían ser 'gpt-4-vision-preview', 'gpt-3.5-turbo' o 'gpt-3.5-turbo-1106' etc.
      model: 'gpt-4o',
      max_tokens: 500,
      //
      temperature: 0.5,
    })
    // Obtenemos únicamente el texto de la respuesta
    const answer = aiConversation.choices[0].message.content.trim();
    // Agregamos también la respuesta al historial.
    chatHistory.push({
      role: 'assistant',
      content: answer,
    });
    theConnection.send(JSON.stringify({ text: answer }));
  });
});