Cómo integrar Azure Text Analytics en una aplicación de análisis de feedback

danieljsaldana

Daniel J. Saldaña

Posted on March 24, 2024

Cómo integrar Azure Text Analytics en una aplicación de análisis de feedback

En la era digital, comprender las percepciones y emociones de los usuarios es esencial para mejorar productos y servicios. Microsoft Azure Text Analytics ofrece una solución robusta para analizar el sentimiento y las frases clave de los textos. En este post, te guiaré a través de una prueba de concepto para integrar Azure Text Analytics en una aplicación de análisis de feedback.

Paso 1: Configurar el entorno de desarrollo

Antes de comenzar, asegúrate de tener las siguientes herramientas y servicios configurados:

  • Node.js y npm instalados en tu máquina local.
  • Una cuenta de Azure con acceso al servicio Text Analytics.
  • Un entorno de base de datos PostgreSQL.

Paso 2: Preparar el frontend

Nuestra aplicación de feedback se construirá usando React. Crearemos un componente para enviar feedback y visualizar el análisis de sentimientos.

  1. Crea un nuevo proyecto React : Utiliza create-react-app para configurar tu entorno de frontend.

  2. Integra el componente AnalyzeFeedback : Este componente es responsable de enviar el título del feedback y mostrar los resultados del análisis.


'use client'

import { useEffect, useState } from 'react';
import styles from './AnalyzeFeedback.module.scss';
import ClassName from '@/types/ClassName';

interface AnalyzeFeedbackProps extends ClassName {
  title: string;
}

const AnalyzeFeedback = ({ className, title }: AnalyzeFeedbackProps) => {
  const [score, setScore] = useState<number | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    setIsLoading(true);
    fetch('https://api.danieljsaldana.dev/api/getfeedbackscores', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ title }),
    })
      .then(response => response.json())
      .then(data => {
        if (data.compositeScore !== undefined) {
          setScore(data.compositeScore);
        } else {
          setScore(null);
        }
      })
      .catch(() => setScore(null))
      .finally(() => setIsLoading(false));
  }, [title]);

  const getBackgroundColor = (score: number | null) => {
    if (score === null) {
      return '#d3d3d3';
    } else if (score > 80) {
      return '#0ac769';
    } else if (score >= 50 && score <= 79) {
      return '#ffad00';
    } else {
      return '#ea001e';
    }
  };

  const backgroundColor = getBackgroundColor(score);
  const circumference = 301.59289474462014;
  const offset = score !== null ? ((100 - score) / 100) * circumference : 0;

  return (
    <div className={`rounded-full h-[24px] w-[22px] ${className ?? ''}`}>
      <svg width="24" height="24" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
        <g shapeRendering="geometricPrecision">
          <circle cx="64" cy="64" fill={backgroundColor} r="64" />
          <circle cx="64" cy="64" fill="none" r="48" stroke="rgba(0,0,0,.1)" strokeWidth="10" />
          {!isLoading && score !== null && (
            <circle
              cx="64"
              cy="64"
              fill="none"
              r="48"
              stroke="white"
              strokeDasharray={`${circumference} ${circumference}`}
              strokeDashoffset={offset}
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="10"
              className="score_progress__quTNG" 
            />
          )}
          {(isLoading || score === null) && (
            <svg className={`${styles.withIconIcon__MHUeb} ${styles.fadeIn}`} data-testid="geist-icon" fill="none" height="70" shapeRendering="geometricPrecision" stroke="#999" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" viewBox="0 0 24 24" width="70" x="29" y="29">
              <path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
            </svg>
          )}
          {!isLoading && score !== null && (
            <text
              fill="white"
              fontSize="42"
              fontWeight="500"
              textAnchor="middle"
              x="64"
              y="79"
            >
              {score}
            </text>
          )}
        </g>
      </svg>
    </div>
  );
};

export default AnalyzeFeedback;

Enter fullscreen mode Exit fullscreen mode
  1. Estiliza tu componente : Usa los estilos predefinidos para hacer que tu componente sea visualmente atractivo y claro para los usuarios.
@keyframes fadeIn {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

.fadeIn {
    animation: fadeIn ease-in 1s;
    animation-fill-mode: forwards;
}

Enter fullscreen mode Exit fullscreen mode

Paso 3: Configurar el backend

El backend de nuestra aplicación se construirá con Node.js y se comunicará con los servicios de Azure para analizar el feedback.

  1. Establece la API de análisis de feedback : Crea una API que reciba el feedback, analice el sentimiento y las frases clave usando Azure Text Analytics, e inserte los resultados en la base de datos.
import { TextAnalyticsClient, AzureKeyCredential } from '@azure/ai-text-analytics';
import { enableCors } from "@/src/middleware/enableCors";
import { methodValidator } from "@/src/utils/methodValidator";
import { insertFeedback } from "@/src/core/insertFeedback"; 
import dotenv from 'dotenv';

dotenv.config();

const textAnalyticsClient = new TextAnalyticsClient(
    process.env.AZURE_TEXT_ANALYTICS_ENDPOINT, 
    new AzureKeyCredential(process.env.AZURE_TEXT_ANALYTICS_KEY)
);

async function analyzeFeedback(req, res) {
    try {
        await methodValidator(req, res, 'POST');
        if (res.headersSent) return;

        const { title, text, user } = req.body;
        if (!title || !text || !user) {
            return res.status(400).json({ error: 'Faltan datos requeridos para el análisis' });
        }

        const sentimentResult = await textAnalyticsClient.analyzeSentiment([text]);
        const sentiment = sentimentResult[0];

        const keyPhrasesResult = await textAnalyticsClient.extractKeyPhrases([text]);
        const keyPhrases = keyPhrasesResult[0];

        await insertFeedback(
            title, 
            sentiment.sentiment, 
            sentiment.confidenceScores.positive, 
            sentiment.confidenceScores.neutral, 
            sentiment.confidenceScores.negative, 
            user
        );

        res.status(200).json({
            title,
            user,
            sentiment: sentiment.sentiment,
            confidenceScores: sentiment.confidenceScores,
            keyPhrases: keyPhrases.keyPhrases,
        });
    } catch (error) {
        console.error('Error al analizar el feedback:', error);
        res.status(500).json({ error: `Error al analizar el feedback: ${error.message}` });
    }
}

export default enableCors(analyzeFeedback);

Enter fullscreen mode Exit fullscreen mode
  1. Desarrolla la API para obtener puntajes de feedback : Esta API recupera los promedios de los puntajes de feedback basados en el título proporcionado.
import { enableCors } from "@/src/middleware/enableCors";
import { methodValidator } from "@/src/utils/methodValidator";
import { sanitizeTitleForFilename } from "@/src/utils/sanitizeTitleForFilename";
import { getAverageFeedbackScores } from "@/src/core/getAverageFeedbackScores";
import dotenv from 'dotenv';

dotenv.config();

export async function getFeedbackScores(req, res) {
    await methodValidator(req, res, 'POST'); 
    if (res.headersSent) return; 

    const { title } = req.body; 
    if (!title) { 
        return res.status(400).json({ error: 'Falta el título para el análisis' });
    }

    const sanitizedTitle = sanitizeTitleForFilename(title); 
    console.log('Título sanitizado:', sanitizedTitle);

    try {
        const averages = await getAverageFeedbackScores(sanitizedTitle);

        if (!averages || averages.average_positive === 0 && averages.average_neutral === 0 && averages.average_negative === 0) {
            return res.status(200).json({ error: 'No se encontraron datos para el título proporcionado', compositeScore: null });
        }

        const avgPositive = parseFloat(averages.average_positive);
        const avgNeutral = parseFloat(averages.average_neutral);
        const avgNegative = parseFloat(averages.average_negative);

        console.log('Promedios convertidos:', avgPositive, avgNeutral, avgNegative);

        let compositeScore = (avgPositive + 0.5 * avgNeutral - avgNegative);
        compositeScore = Math.max(0, Math.min(1, compositeScore)) * 100;

        compositeScore = Math.round(compositeScore);

        console.log('Puntuación compuesta:', compositeScore);
        res.status(200).json({ compositeScore: compositeScore });
    } catch (error) {
        console.error('Error al obtener los promedios del feedback:', error);
        res.status(500).json({ error: `Error al obtener los promedios del feedback: ${error.message}` });
    }
}

export default enableCors(getFeedbackScores);

Enter fullscreen mode Exit fullscreen mode
  1. Implementa funciones de base de datos : Usa estas funciones para interactuar con tu base de datos PostgreSQL y manejar la inserción y recuperación de datos de feedback.
import { sql } from '@vercel/postgres'; 

export async function getAverageFeedbackScores(title) {
    try {
        const result = await sql`
            SELECT 
                AVG(positive) AS average_positive, 
                AVG(neutral) AS average_neutral, 
                AVG(negative) AS average_negative 
            FROM post_feedback 
            WHERE title = ${title}
            GROUP BY title;
        `;

        if (result.rows && result.rows.length > 0) {
            return result.rows[0];
        } else {
            return { average_positive: 0, average_neutral: 0, average_negative: 0 };
        }
    } catch (error) {
        console.error(`Error al obtener los promedios de feedback para el título: ${title}`, error);
        throw error;
    }
}

Enter fullscreen mode Exit fullscreen mode

y

import { sql } from '@vercel/postgres';

export async function insertFeedback(title, sentiment, positive, neutral, negative, user) {
    console.log(`Insertando feedback para el título: ${title}`);

    try {
        await sql`
            INSERT INTO post_feedback
            (title, sentiment, positive, neutral, negative, "user", last_updated)
            VALUES 
            (${title}, ${sentiment}, ${positive}, ${neutral}, ${negative}, ${user}, CURRENT_TIMESTAMP)
            ON CONFLICT (title, "user") -- Actualiza esto para que coincida con la nueva restricción única
            DO UPDATE SET 
                sentiment = EXCLUDED.sentiment,
                positive = EXCLUDED.positive,
                neutral = EXCLUDED.neutral,
                negative = EXCLUDED.negative,
                last_updated = CURRENT_TIMESTAMP;
        `;
        console.log(`Feedback insertado con éxito para el título: ${title}`);
    } catch (error) {
        console.error(`Error al insertar feedback para el título: ${title}`, error);
        throw new Error(`Error al insertar feedback: ${error.message}`);
    }
}

Enter fullscreen mode Exit fullscreen mode

Paso 4: Configuración de la base de datos

Antes de que puedas comenzar a almacenar y recuperar datos de feedback, necesitas configurar tu base de datos PostgreSQL. Usa la siguiente query SQL para crear la tabla necesaria para almacenar los datos de feedback:

CREATE TABLE post_feedback (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    sentiment VARCHAR(50) NOT NULL,
    positive NUMERIC NOT NULL,
    neutral NUMERIC NOT NULL,
    negative NUMERIC NOT NULL,
    "user" VARCHAR(255) NOT NULL,
    last_updated TIMESTAMP NOT NULL,
    UNIQUE(title, "user")
);

Enter fullscreen mode Exit fullscreen mode

Esta tabla almacenará cada pieza de feedback junto con el análisis de sentimientos y la marca de tiempo de la última actualización. Asegúrate de que tu base de datos esté en funcionamiento y accesible desde tu backend antes de continuar.

Paso 5: Prueba de concepto

Para probar la integración completa de tu sistema, utiliza Postman para simular solicitudes de cliente a tu backend. A continuación, te proporciono una guía para configurar las solicitudes en Postman:

Configuración en Postman:

  1. POST para analizar feedback :

  2. POST para obtener puntajes de feedback :

Asegúrate de tener tu servidor backend en ejecución antes de enviar las solicitudes desde Postman. Estas solicitudes simularán la interacción entre el frontend y el backend, permitiéndote evaluar la funcionalidad completa de tu aplicación de análisis de feedback.

Después de configurar y ejecutar estas pruebas, deberías poder ver cómo tu aplicación procesa y responde al feedback, cómo se almacena en la base de datos y cómo se recuperan y calculan los promedios de los puntajes de sentimiento. Esto completará tu prueba de concepto, demostrando la funcionalidad y la integración entre todas las partes de tu aplicación.

Y con esto, has completado la integración de Azure Text Analytics en tu aplicación de análisis de feedback. ¡Felicidades por llegar hasta aquí! Si tienes alguna pregunta o comentario, no dudes en dejarlo a continuación.

💖 💪 🙅 🚩
danieljsaldana
Daniel J. Saldaña

Posted on March 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related