Cómo integrar Azure Text Analytics en una aplicación de análisis de feedback
 Daniel J. Saldaña- 24 de marzo de 2024
 - Puntuación 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.
Crea un nuevo proyecto React: Utiliza
create-react-apppara configurar tu entorno de frontend.Integra el componente AnalyzeFeedback: Este componente es responsable de enviar el título del feedback y mostrar los resultados del análisis.
Archivo de ejemplo a integrar:
'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;Estiliza tu componente: Usa los estilos predefinidos para hacer que tu componente sea visualmente atractivo y claro para los usuarios.
Archivo de ejemplo a integrar:
@keyframes fadeIn {  from {    opacity: 0;  }  to {    opacity: 1;  }}
.fadeIn {  animation: fadeIn ease-in 1s;  animation-fill-mode: forwards;}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.
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.
Archivos de ejemplo a integrar:
analyzeFeedback.js
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);Desarrolla la API para obtener puntajes de feedback: Esta API recupera los promedios de los puntajes de feedback basados en el título proporcionado.
Archivos de ejemplo a integrar:
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);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.
Archivos de ejemplo a integrar:
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;  }}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}`);  }}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"));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:
POST para analizar feedback:
- URL: 
http://localhost:3000/analyzeFeedback(ajusta el puerto según tu configuración). - Método: POST
 - Body: raw, JSON{"title": "Ejemplo de Título","text": "Ejemplo de feedback del usuario","user": "usuario_ejemplo"}
 
- URL: 
 POST para obtener puntajes de feedback:
- URL: 
http://localhost:3000/getFeedbackScores - Método: POST
 - Body: raw, JSON{"title": "Ejemplo de Título"}
 
- URL: 
 
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.


