CodeGym /Cursos /ChatGPT Apps /Procesamiento de resultados de la herramienta en el widge...

Procesamiento de resultados de la herramienta en el widget: ToolOutput → UI

ChatGPT Apps
Nivel 4 , Lección 3
Disponible

1. De ToolOutput al componente de React: flujo de datos general

En la lección anterior analizamos cómo la herramienta del servidor forma ToolOutput, una respuesta estructurada para el modelo y el widget. Ahora veremos la segunda mitad de ese recorrido: cómo ese ToolOutput llega al widget y se convierte en UI.

Para no percibir lo que ocurre como magia, repasemos de nuevo el recorrido de los datos desde el usuario hasta tu widget. En forma simplificada, todo se ve así:

  1. El usuario hace una pregunta en el chat.
  2. GPT analiza la solicitud, revisa la lista de herramientas y decide: «Ahora me ayudará suggest_gifts».
  3. GPT forma una llamada de herramienta con nombre y argumentos (ToolInput) y la envía a tu servidor (MCP o backend).
  4. El servidor ejecuta la lógica de la herramienta y devuelve el resultado en forma de ToolOutput: JSON estructurado con datos, más un resumen textual para el modelo.
  5. ChatGPT recibe ToolOutput y lo pasa más adelante: al modelo (para continuar el diálogo) y a tu widget a través del Apps SDK (window.openai.toolOutput o hooks).
  6. Tu widget —un componente React normal— lee toolOutput y renderiza la UI.

De forma esquemática se puede representar así:

flowchart TD
  U[Usuario] -->|consulta en el chat| GPT[GPT]
  GPT -->|callTool: suggest_gifts| B[Backend/MCP]
  B -->|"ToolOutput (JSON)"| GPT
  GPT -->|envía toolOutput| W["Widget (React)"]
  W -->|tarjetas, listas| U

Es importante fijar esta idea: ToolOutput no es solo «la respuesta del servidor». También es tu orden de renderizado para el widget y, al mismo tiempo, el contexto para el modelo. Una buena App es aquella en la que ese JSON se convierte en una interfaz útil, y no algo que el desarrollador hojea con la vista en las DevTools.

2. Anatomía de ToolOutput: qué hay dentro

El formato del resultado de la herramienta en el Apps SDK se divide en tres bloques lógicos: structuredContent, content y _meta (que llega al widget con el nombre toolResponseMetadata).

De forma aproximada, se puede representar así:

{
  "structuredContent": { /* datos para la UI + el modelo */ },
  "content": "Resumen textual breve para el modelo y el usuario",
  "_meta": { /* datos de servicio solo para el widget */ }
}

En la tabla se ve quién ve qué:

Campo Quién lo ve Para qué se usa
structuredContent
Modelo + widget Datos estructurados principales (listas, objetos, parámetros)
content
Modelo + usuario (en el texto) Resumen breve que GPT puede insertar en su respuesta
_meta
Solo el widget Datos técnicos que el modelo no necesita (ID, versiones, claves, etc.)

La documentación del Apps SDK subraya que el par structuredContent / content llega al modelo y puede usarse en sus respuestas posteriores. El campo _meta, por su parte, permanece oculto y solo está disponible dentro del widget a través de toolResponseMetadata.

Ejemplo de ToolOutput para GiftGenius

Supongamos que nuestra herramienta suggest_gifts en el servidor devuelve un cuerpo parecido a este:

{
  "structuredContent": {
    "items": [
      {
        "id": "boardgame-cozy-strategy",
        "title": "Cozy Strategy Board Game",
        "price": 39.99,
        "currency": "USD",
        "score": 0.92,
        "tags": ["board_game","strategy","2-4_players"]
      }
    ]
  },
  "content": "He encontrado algunas ideas de regalos. Abajo el widget las muestra como tarjetas.",
  "_meta": {
    "giftGenius": {
      "catalogVersion": "2025-10-01",
      "experimentBucket": "A"
    }
  }
}

Aquí structuredContent.items es lo que renderizará tu widget de React; content puede usarlo el modelo para explicar al usuario lo que está sucediendo; _meta.giftGenius es información interna que solo necesita tu UI o la analítica (por ejemplo, qué versión de catálogo usar para los enlaces).

Precisamente structuredContent es el objeto que mirarás en JSX en lugar de parsear a mano un JSON arbitrario del servidor.

3. Obtener ToolOutput en el widget: window.openai y hooks

Es hora de pasar de hablar de JSON a ver código. ¿Cómo llega ese ToolOutput a tu componente de React?

El template del Apps SDK lo hace de dos maneras principales: o bien directamente mediante window.openai.toolOutput, o bien, lo que es más cómodo, a través de hooks de React listos para usar (useWidgetProps, useToolOutput y similares). El enfoque recomendado es usar hooks, para no tocar window.openai directamente y tener un código más seguro y fácil de testear.

La opción más simple: directamente desde window.openai

Para entenderlo, se puede ver la opción «desnuda»:

'use client';

function RawToolOutputDebug() {
  const toolOutput = (window as any).openai?.toolOutput;
  return (
    <pre>{JSON.stringify(toolOutput, null, 2)}</pre>
  );
}

Esto no debería hacerse en producción, por supuesto, pero para depurar y «echar un primer vistazo» es perfectamente válido.

Opción práctica: a través de un hook de React

Es mucho más cómodo envolver el acceso a window.openai en un pequeño hook y trabajar ya con un objeto tipado. Supongamos que nuestro SDK de ejemplo ofrece un hook useWidgetProps, que devuelve toolOutput y toolResponseMetadata.

'use client';

import { useWidgetProps } from '@/lib/openai-widget';

export function GiftWidgetRoot() {
  const { toolOutput, toolResponseMetadata } = useWidgetProps();

  // Por ahora solo mostraremos el número de regalos
  const items = toolOutput?.structuredContent?.items ?? [];

  return (
    <div>
      Regalos encontrados: {items.length}
    </div>
  );
}

En un template real el nombre del hook puede variar, pero la idea siempre es la misma: el SDK toma los datos de window.openai y se los entrega a tu componente como props o mediante contexto. Es mucho más sencillo que meterse a cada rato en el objeto global y, además, permite sustituir fácilmente la fuente de datos en tests (por ejemplo, inyectando una fixture de toolOutput).

4. Renderizar regalos: de structuredContent a JSX

Vamos a lo interesante: tomemos structuredContent.items y dibujemos tarjetas con ellos. No olvidemos que nuestro widget es un componente cliente de React en Next.js ('use client' en la parte superior del archivo).

Primero definimos el tipo de un regalo:

type GiftItem = {
  id: string;
  title: string;
  price: number;
  currency: string;
  tags?: string[];
};

Ahora escribamos un pequeño componente de tarjeta:

function GiftCard({ gift }: { gift: GiftItem }) {
  return (
    <div className="gift-card">
      <div className="gift-title">{gift.title}</div>
      <div className="gift-price">
        {gift.price} {gift.currency}
      </div>
    </div>
  );
}

Y un componente de lista que toma los datos de toolOutput:

'use client';

import { useWidgetProps } from '@/lib/openai-widget';

export function GiftList() {
  const { toolOutput } = useWidgetProps();
  const items = (toolOutput?.structuredContent?.items ?? []) as GiftItem[];

  return (
    <div className="gift-list">
      {items.map(gift => (
        <GiftCard key={gift.id} gift={gift} />
      ))}
    </div>
  );
}

Fíjate en lo parecido que es todo esto al código React habitual. La única «magia» es la fuente de datos: en lugar de props o fetch leemos toolOutput del contenedor de ChatGPT.

Y sí, no pasa nada si al principio añades as GiftItem[]. Más adelante podrás tipar con cuidado structuredContent mediante tipos compartidos con el backend (por ejemplo, usar Zod / JSON Schema → tipos TS), pero para la demostración es suficiente.

5. Estados de UI alrededor de ToolOutput: carga, vacío, error

Una aplicación que solo muestra tarjetas cuando hay suerte y se queda callada en el resto de casos no es muy amigable. Hay que manejar explícitamente, como mínimo, cuatro estados: mientras la herramienta se ejecuta, cuando aún no hay datos, cuando hay resultado y cuando algo ha salido mal.

El Apps SDK suele darte cierta información sobre el estado de la invocación de la herramienta: mediante la lista de tool invocations (useToolInvocations) o flags relacionados con toolOutput. En esta lección nos basta con un modelo simple: si aún no hay toolOutput, estamos en estado «cargando»; si lo hay, pero la lista está vacía, «vacío»; si llegó un error, «error».

Para simplificar, supondremos que el servidor, en caso de error, pone en structuredContent el campo error, y el flag ok en la raíz de toolOutput es false. Ya comentamos este esquema en el tema anterior sobre la implementación en el servidor, cuando diseñábamos el contrato de respuesta de la herramienta.

type ToolOutput = {
  ok: boolean;
  structuredContent?: {
    items?: GiftItem[];
    error?: { code: string; message: string };
  };
};

Actualicemos ahora nuestro componente de lista:

'use client';

import { useWidgetProps } from '@/lib/openai-widget';

export function GiftListWithStates() {
  const { toolOutput } = useWidgetProps() as { toolOutput?: ToolOutput };

  if (!toolOutput) {
    return <div>Buscando regalos…</div>;
  }

  if (!toolOutput.ok) {
    const msg = toolOutput.structuredContent?.error?.message
      ?? 'No ha sido posible obtener recomendaciones.';
    return <div>Error: {msg}</div>;
  }

  const items = toolOutput.structuredContent?.items ?? [];

  if (items.length === 0) {
    return <div>No se han encontrado regalos para tus condiciones. Prueba a cambiar los parámetros.</div>;
  }

  return (
    <div className="gift-list">
      {items.map(gift => (
        <GiftCard key={gift.id} gift={gift} />
      ))}
    </div>
  );
}

Este código ya proporciona una experiencia adecuada al usuario:

  • Mientras la herramienta trabaja, se ve que algo está ocurriendo.
  • Si todo falla, hay un mensaje comprensible, no una pantalla en blanco.
  • Si no se encuentra nada, no fingimos que es normal, sino que explicamos honestamente lo que ha pasado.

En producción, probablemente sustituirás el texto «Buscando regalos…» por un pequeño skeleton o spinner. Para errores complejos se puede dejar que GPT formule una explicación legible. Pero la estructura básica de los componentes seguirá siendo la misma.

6. Usar _meta y toolResponseMetadata en la UI

Ya hemos aprendido a renderizar los datos principales de structuredContent y a manejar los estados básicos loading/empty/error. Queda una parte importante de ToolOutput que el modelo no utiliza: el campo _meta.

Volvamos al campo _meta. No es visible para el modelo, pero llega a tu widget como toolResponseMetadata (el nombre puede variar, pero la idea es la misma).

Es un lugar excelente para aquello que no debe influir en el razonamiento de GPT, pero que es importante para la UI:

  • versiones del catálogo o de la configuración;
  • ID internos de campaña / experimento A/B;
  • flags sobre qué «botones» mostrar al usuario;
  • cualquier elemento técnico que no quieras mezclar con los datos de dominio.

Por ejemplo, el servidor puede devolver este _meta:

"_meta": {
  "giftGenius": {
    "catalogVersion": "2025-10-01",
    "showExperimentalBadges": true
  }
}

El widget puede leerlo y, por ejemplo, mostrar una insignia «Idea nueva» en algunas tarjetas.

type GiftMeta = {
  giftGenius?: {
    catalogVersion: string;
    showExperimentalBadges?: boolean;
  };
};

export function GiftListWithMeta() {
  const { toolOutput, toolResponseMetadata } = useWidgetProps() as {
    toolOutput?: ToolOutput;
    toolResponseMetadata?: GiftMeta;
  };

  const meta = toolResponseMetadata?.giftGenius;
  const items = toolOutput?.structuredContent?.items ?? [];

  return (
    <div>
      {meta && (
        <div className="catalog-version">
          Catálogo de {meta.catalogVersion}
        </div>
      )}
      <div className="gift-list">
        {items.map(gift => (
          <GiftCard
            key={gift.id}
            gift={gift}
          />
        ))}
      </div>
    </div>
  );
}

El modelo aquí no pinta nada: no sabe nada de catalogVersion ni de showExperimentalBadges, pero tu UI puede usarlos como quiera.

La documentación subraya precisamente esta separación: los datos que son importantes para el diálogo y el razonamiento del modelo van en structuredContent y content; todo lo que sea puramente técnico de UI va en _meta / toolResponseMetadata.

7. Un poco sobre los estados de ToolInvocation y «Ejecutando X…»

Mientras la herramienta se ejecuta, ChatGPT muestra por sí mismo al usuario lo que ocurre: en la parte superior del chat aparece un estado como «Ejecutando GiftGenius…» o «Accediendo a una aplicación externa». No eres tú quien imprime esas cadenas, sino el entorno anfitrión de ChatGPT, que reacciona a los metadatos de la invocación de la herramienta.

Por debajo, esto se describe mediante claves de servicio del tipo _meta["openai/toolInvocation/invoking"] y _meta["openai/toolInvocation/invoked"], que señalan que la acción se está ejecutando o se ha completado. La plataforma utiliza estos campos para mostrar el estado y, por lo general, no necesitas tocarlos: el SDK se encarga de ello en el servidor.

Para UX esto supone una ventaja: incluso si el widget aún no ha renderizado el skeleton, el usuario ya ve que el sistema está haciendo algo. Tu tarea es complementar ese estado global con estados locales como «Buscando regalos…» y un skeleton en el widget, tal y como hicimos arriba.

8. Tamaño de los datos y rendimiento: no metas el mundo entero en structuredContent

Conviene hablar aparte del tema «¿cuánto puedo meter en structuredContent?». Intuitivamente resulta tentador: «Tengo todo el catálogo de regalos — devolvámoslo entero y que el widget filtre». En la práctica no conviene hacerlo.

En primer lugar, structuredContent entra en el contexto del modelo (LLM) y el volumen total de tokens está limitado. La documentación y las guías prácticas recomiendan encarecidamente mantener el volumen contenido: no es un almacén de datos, sino el resultado de una acción.

En segundo lugar, cuanto mayor sea el payload, más lento llegará la respuesta y mayor será la probabilidad de toparte con límites o recortes/errores inesperados.

Un enfoque sensato sería:

  • El backend filtra y ordena los datos de antemano, devolviendo exactamente lo necesario para el paso actual: por ejemplo, 10–20 mejores regalos.
  • Si se necesitan páginas siguientes, es una acción aparte (nueva llamada a la herramienta, nuevo ToolOutput).
  • Para aspectos puramente de UI (por ejemplo, la lista de todas las etiquetas posibles para filtrar) se puede usar _meta, pero también sin exagerar.

En el módulo sobre estado ya comentamos el concepto de «el backend es la fuente de la verdad y el widget es la caché/vista». Aquí es igual: el resultado de la herramienta es un «corte» limpio del estado en el momento de la invocación, no una copia completa de tu base.

9. Conexión con el estado del widget y el diálogo posterior

Aunque esta lección trata oficialmente de ToolOutput → UI, no podemos olvidar que al lado vive otra pieza importante: widgetState. Es justamente lo que permite recordar la elección del usuario entre renders y convertir tu widget no solo en un escaparate, sino en un asistente completo o un «configurador de regalos».

Un escenario típico sería:

  1. El primer ToolOutput trae una lista de regalos.
  2. El usuario hace clic en una de las tarjetas.
  3. El widget guarda en widgetState qué regalo se ha seleccionado y, quizá, envía un follow‑up o una nueva llamada de herramienta para obtener detalles.
  4. Los siguientes ToolOutput se apoyan en esa elección.

Desde el punto de vista del código, se ve como un estado de React normal más una llamada a setWidgetState, que guarda la elección en el lado de ChatGPT. La diferencia es que este estado está disponible tanto para el modelo como para tu backend, por lo que debe mantenerse compacto y no almacenar secretos.

Lo veremos en detalle en los módulos sobre workflows multi‑paso y follow‑ups. Ya ahora es útil pensar así: ToolOutput te da un «corte de datos» del servidor, y widgetState es el contexto de la elección del usuario alrededor de ese corte.

Errores habituales al trabajar con ToolOutput → UI

Error n.º 1: «La UI renderiza el árbol JSON en bruto sin adaptarlo al usuario».
A veces apetece, para depurar, simplemente hacer <pre>{JSON.stringify(toolOutput)}</pre> y quedarse ahí. Para desarrollo está bien, pero en producción el usuario ve una estructura de la que tú te sientes orgulloso, pero que él no entiende. Es importante envolver cuanto antes structuredContent en componentes con sentido (listas, tarjetas, tablas), en lugar de obligar a la gente a leer una respuesta tokenizada del servidor.

Error n.º 2: Mezclar datos de dominio y metadatos técnicos en structuredContent.
El código se vuelve mucho más limpio si separas «lo que debe ver el modelo y el usuario» de «lo que solo necesita la UI y la analítica». Los campos técnicos —flags de experimentos, versiones de catálogos, idempotency key— deben ir en _meta / toolResponseMetadata. Cuando todo eso está mezclado en structuredContent, es más difícil evolucionar el contrato y testear el comportamiento del modelo.

Error n.º 3: No tener estados explícitos de carga, resultado vacío y errores.
Un <div></div> vacío en lugar de «No se ha encontrado nada» o «Algo ha salido mal» conduce a que el usuario piense: «La app no funciona». Incluso placeholders textuales mínimos y un skeleton simple mejoran dramáticamente el UX. No te apoyes solo en el estado del sistema de ChatGPT «Ejecutando X…» — el widget también debe comunicar lo que le ocurre.

Error n.º 4: Intentar meter en un solo ToolOutput el mundo entero.
Devolver todo el catálogo de productos, el historial del usuario y además logs del servidor en un único structuredContent es una mala idea. Choca con los límites del modelo, ralentiza la respuesta y complica la UI. Es mejor devolver exactamente el volumen de datos necesario para el paso actual (página de lista, detalles del elemento seleccionado, etc.) y estructurar los pasos posteriores como llamadas separadas a la herramienta.

Error n.º 5: Acoplar la UI rígidamente a una forma inestable de respuesta sin tipos.
Si en todo el código escribes toolOutput.structuredContent.items[0].whatever sin comprobar la existencia de campos y sin tener tipos, cualquier evolución del esquema en el servidor llevará a caídas del widget. Conviene o bien sincronizar tipos con JSON Schema (generación de tipos TS), o al menos describir interfaces a mano (GiftItem, ToolOutput) y trabajar con cuidado con campos opcionales.

Error n.º 6: Ignorar _meta y sobrecargar al modelo con campos «extra».
Es tentador meter en structuredContent todo lo que se te ocurra, porque «es JSON, nunca sobra». Pero cada campo aumenta el contexto del modelo, y muchos de ellos no los necesita en absoluto. Si la información no debe influir en el razonamiento de GPT y no es necesaria en la respuesta textual, colócala en _meta y trabájala solo en el widget.

Error n.º 7: Acceder directamente a window.openai desde una decena de componentes.
Sí, window.openai.toolOutput funciona, pero cuando media aplicación empieza a toquetear una variable global, depurar y testear se vuelve un infierno. Es mucho mejor envolverlo una vez en un hook/contexto (useWidgetProps/useToolOutput) y, a partir de ahí, usar props normales y objetos tipados. Es más limpio y más fácil de sustituir por fixtures en Storybook/tests.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION