CodeGym /Corsi /ChatGPT Apps /Elaborazione dei risultati dello strumento nel widget: To...

Elaborazione dei risultati dello strumento nel widget: ToolOutput → UI

ChatGPT Apps
Livello 4 , Lezione 3
Disponibile

1. Da ToolOutput al componente React: flusso dati generale

Nella lezione precedente abbiamo visto come il tool lato server formi ToolOutput — una risposta strutturata per il modello e per il widget. Ora guardiamo alla seconda metà del percorso: come questo ToolOutput finisce nel widget e si trasforma in UI.

Per evitare che tutto sembri magia, ripercorriamo ancora una volta il viaggio dei dati dall’utente al vostro widget. In forma semplificata è così:

  1. L’utente fa una domanda in chat.
  2. GPT analizza la richiesta, guarda l’elenco degli strumenti e decide: «Ora mi aiuterà suggest_gifts».
  3. GPT forma una chiamata allo strumento con nome e argomenti (ToolInput) e la invia al vostro server (MCP o backend).
  4. Il server esegue la logica dello strumento e restituisce il risultato come ToolOutput — un JSON strutturato con i dati, più un riepilogo testuale per il modello.
  5. ChatGPT riceve ToolOutput e lo inoltra oltre: al modello (per proseguire il dialogo) e al vostro widget tramite Apps SDK (window.openai.toolOutput o hook).
  6. Il vostro widget — un normale componente React — legge toolOutput e renderizza la UI.

In modo schematico si può rappresentare così:

flowchart TD
  U[Utente] -->|richiesta in chat| GPT[GPT]
  GPT -->|callTool: suggest_gifts| B[Backend/MCP]
  B -->|"ToolOutput (JSON)"| GPT
  GPT -->|passa toolOutput| W["Widget (React)"]
  W -->|card, liste| U

È importante fissare un concetto: ToolOutput non è solo «risposta del server». È anche il vostro comando di rendering per il widget e contemporaneamente il contesto per il modello. Una buona App è quella in cui questo JSON si trasforma in un’interfaccia comoda, invece di essere fatto scorrere con gli occhi dallo sviluppatore in DevTools.

2. Anatomia di ToolOutput: cosa contiene

Il formato del risultato dello strumento in Apps SDK si divide in tre blocchi logici: structuredContent, content e _meta (che arriva al widget con il nome toolResponseMetadata).

In modo approssimativo si può rappresentare così:

{
  "structuredContent": { /* dati per UI + modello */ },
  "content": "Breve riepilogo testuale per il modello e l’utente",
  "_meta": { /* dati di servizio solo per il widget */ }
}

In tabella si vede chi vede cosa:

Campo Chi lo vede A cosa serve
structuredContent
Modello + widget Dati strutturati principali (liste, oggetti, parametri)
content
Modello + utente (nel testo) Breve riepilogo che GPT può inserire nella propria risposta
_meta
Solo widget Dati di servizio non necessari al modello (ID, versioni, chiavi, ecc.)

La documentazione di Apps SDK sottolinea che la coppia structuredContent / content arriva al modello e può essere usata nelle sue risposte successive. Il campo _meta invece resta nascosto ed è disponibile solo all’interno del widget tramite toolResponseMetadata.

Esempio di ToolOutput per GiftGenius

Supponiamo che il nostro strumento suggest_gifts sul server restituisca un corpo simile a questo:

{
  "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": "Ho trovato alcune idee regalo. Qui sotto il widget le mostra come card.",
  "_meta": {
    "giftGenius": {
      "catalogVersion": "2025-10-01",
      "experimentBucket": "A"
    }
  }
}

Qui structuredContent.items è ciò che il vostro widget React renderizzerà; il content può essere usato dal modello per spiegare all’utente cosa sta succedendo; _meta.giftGenius sono informazioni interne, utili solo alla vostra UI o all’analitica (per esempio quale versione del catalogo usare per i link).

Proprio structuredContent è l’oggetto che guarderete in JSX invece di fare il parsing manuale di un JSON arbitrario dal server.

3. Ottenere ToolOutput nel widget: window.openai e hook

Passiamo dal parlare di JSON al codice. Come arriva questo ToolOutput nel vostro componente React?

Il template di Apps SDK lo fa in due modi principali: o direttamente tramite window.openai.toolOutput, oppure — cosa comoda — tramite hook React già pronti (useWidgetProps, useToolOutput e simili). L’approccio raccomandato è usare gli hook per non toccare window.openai a mano e avere codice più testabile e sicuro.

Opzione più semplice: direttamente da window.openai

Per capire il meccanismo, guardiamo la versione “grezza”:

'use client';

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

In produzione così non si dovrebbe fare, ma per il debug e per “dare un’occhiata ai primi passi” va benissimo.

Opzione pratica: tramite un hook di React

È molto più comodo incapsulare l’accesso a window.openai in un piccolo hook e lavorare già con un oggetto tipizzato. Supponiamo che il nostro SDK fornisca l’hook useWidgetProps, che restituisce toolOutput e toolResponseMetadata.

'use client';

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

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

  // Per ora visualizziamo solo il numero di regali
  const items = toolOutput?.structuredContent?.items ?? [];

  return (
    <div>
      Regali trovati: {items.length}
    </div>
  );
}

Nel template reale il nome dell’hook può variare, ma l’idea è sempre la stessa: l’SDK prende i dati da window.openai e li passa al vostro componente come props o tramite contesto. È molto più semplice che andare ogni volta a leggere l’oggetto globale e, in più, permette nei test di sostituire facilmente la sorgente dei dati (per esempio iniettando una fixture di toolOutput).

4. Renderizzare i regali: da structuredContent a JSX

Passiamo alla parte interessante: prendiamo structuredContent.items e disegniamo delle card. Non dimentichiamo che il nostro widget è un normale componente client di React in Next.js ('use client' in cima al file).

Per prima cosa definiamo il tipo di un regalo:

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

Ora scriviamo un piccolo componente card:

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>
  );
}

E il componente lista che prende i dati da 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>
  );
}

Nota quanto qui tutto somigli al normale codice React. L’unica “magia” è la sorgente dei dati: invece di props o fetch leggiamo toolOutput dal contenitore di ChatGPT.

E sì, niente di grave se inizialmente aggiungerete as GiftItem[]. Più avanti si potrà tipizzare con cura structuredContent tramite tipi condivisi con il backend (per esempio usando Zod / JSON Schema → tipi TS), ma per una demo questo basta.

5. Stati UI attorno a ToolOutput: caricamento, vuoto, errore

Un’app che mostra solo delle card quando va tutto bene e tace in tutti gli altri casi non è molto amichevole. Bisogna gestire in modo esplicito almeno quattro stati: mentre lo strumento è in esecuzione, quando i dati non ci sono ancora, quando c’è il risultato e quando qualcosa è andato storto.

L’Apps SDK in genere fornisce alcune informazioni sullo stato della chiamata allo strumento: tramite l’elenco delle invocazioni (useToolInvocations) o flag associati a toolOutput. In questa lezione ci basta un modello semplice: se toolOutput non c’è ancora — siamo in stato “caricamento”; se c’è ma la lista è vuota — “vuoto”; se è arrivato un errore — “errore”.

Per semplicità supponiamo che il server, in caso di errore, metta in structuredContent il campo error, e che il flag ok alla radice di toolOutput sia false. Questo schema l’abbiamo già discusso nel tema precedente sull’implementazione server, quando progettavamo il contratto della risposta dello strumento.

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

Ora aggiorniamo il nostro componente lista:

'use client';

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

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

  if (!toolOutput) {
    return <div>Stiamo cercando i regali…</div>;
  }

  if (!toolOutput.ok) {
    const msg = toolOutput.structuredContent?.error?.message
      ?? 'Impossibile ottenere le raccomandazioni.';
    return <div>Errore: {msg}</div>;
  }

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

  if (items.length === 0) {
    return <div>Nessun regalo corrisponde ai tuoi criteri. Prova a modificare i parametri.</div>;
  }

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

Un codice del genere offre già un’esperienza adeguata all’utente:

  • Finché lo strumento lavora, si vede che qualcosa sta accadendo.
  • Se tutto è caduto — c’è un messaggio comprensibile, non uno schermo vuoto.
  • Se non si è trovato nulla — non facciamo finta che sia normale, ma spieghiamo onestamente cosa è successo.

In produzione probabilmente sostituirete il testo «Stiamo cercando i regali…» con un piccolo skeleton o uno spinner. Per errori complessi potete dare a GPT la possibilità di formulare una spiegazione leggibile. Ma la struttura di base dei componenti resterà la stessa.

6. Usare _meta e toolResponseMetadata nella UI

Abbiamo già imparato a renderizzare i dati principali da structuredContent e a gestire gli stati di base loading/empty/error. Rimane un altro pezzo importante di ToolOutput, che il modello non usa: il campo _meta.

Torniamo al campo _meta. Non è visibile al modello, ma arriva al vostro widget come toolResponseMetadata (il nome può variare, ma la sostanza è la stessa).

È un ottimo posto per ciò che non deve influenzare il ragionamento di GPT ma è importante per la UI:

  • versioni del catalogo o della configurazione;
  • ID interni di campagna / esperimenti A/B;
  • flag su quali “bottoni” mostrare all’utente;
  • qualsiasi elemento tecnico che non volete confondere con i dati di dominio.

Per esempio, il server può restituire questo _meta:

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

Il widget può leggerlo e, per esempio, disegnare un badge “Idea nuova” su alcune card.

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">
          Catalogo del {meta.catalogVersion}
        </div>
      )}
      <div className="gift-list">
        {items.map(gift => (
          <GiftCard
            key={gift.id}
            gift={gift}
          />
        ))}
      </div>
    </div>
  );
}

Qui il modello non c’entra affatto: non conosce catalogVersion e showExperimentalBadges, ma la vostra UI può usarli come preferisce.

La documentazione sottolinea proprio questa separazione: i dati importanti per il dialogo e il ragionamento del modello li mettiamo in structuredContent e content; tutto ciò che è puramente tecnico per la UI va in _meta / toolResponseMetadata.

7. Un po’ sugli stati di ToolInvocation e «Sto eseguendo X…»

Mentre lo strumento è in esecuzione, ChatGPT mostra autonomamente all’utente cosa sta accadendo: nella parte alta della chat appare uno stato come «Sto eseguendo GiftGenius…» o «Mi sto rivolgendo a un’app esterna». Non siete voi a stampare queste stringhe, bensì l’ambiente host di ChatGPT che reagisce ai metadati della chiamata allo strumento.

Sotto il cofano questo è descritto tramite chiavi di servizio del tipo _meta["openai/toolInvocation/invoking"] e _meta["openai/toolInvocation/invoked"], che segnalano che l’azione è in corso o completata. Questi campi sono usati dalla piattaforma per visualizzare lo stato e, di norma, non serve toccarli: l’SDK se ne occupa lato server.

Per la UX questo significa un bonus piacevole: anche se il widget non è ancora riuscito a disegnare lo skeleton, l’utente vede già che il sistema sta facendo qualcosa. Il vostro compito è integrare questo stato globale con stati locali come «Stiamo cercando i regali…» e uno skeleton nel widget, come abbiamo fatto sopra.

8. Dimensione dei dati e prestazioni: non carichiamo il mondo intero in structuredContent

Vale la pena parlare separatamente del tema «quanto si può mettere dentro structuredContent». Intuitivamente è allettante: «Ho l’intero catalogo dei regali — diamolo tutto, e il widget filtrerà». In pratica non conviene.

Primo, structuredContent finisce nel contesto del modello (LLM) e il volume totale dei token è limitato. La documentazione e le guide pratiche raccomandano insistentemente di mantenere il volume contenuto: non è un archivio dati, ma il risultato di un’azione.

Secondo, più grande è il payload, più lentamente arriva la risposta e più alto è il rischio di incappare in limiti o in tagli/ errori inattesi.

Un approccio sensato è questo:

  • Il backend filtra e ordina i dati in anticipo, restituendo esattamente ciò che serve per lo step corrente: per esempio, i 10–20 migliori regali.
  • Se servono le pagine successive, è un’azione separata (nuova chiamata dello strumento, nuovo ToolOutput).
  • Per elementi puramente UI (ad esempio, l’elenco di tutti i possibili tag per il filtro) si può usare _meta, ma senza esagerare.

Nel modulo sullo stato abbiamo già discusso il concetto «il backend è la fonte di verità, il widget è cache/rappresentazione». Qui vale lo stesso: il risultato dello strumento è un “ritaglio” pulito dello stato al momento della chiamata, non una copia completa del vostro database.

9. Integrazione con lo stato del widget e il dialogo successivo

Anche se questa lezione è ufficialmente su ToolOutput → UI, non si può non ricordare che nei paraggi vive un altro pezzo importante: widgetState. È proprio lui che permette di ricordare la scelta dell’utente tra i render e di trasformare il vostro widget non in una semplice vetrina, ma in un vero wizard o «configuratore di regali».

Uno scenario tipico è questo:

  1. Il primo ToolOutput porta un elenco di regali.
  2. L’utente clicca su una delle card.
  3. Il widget registra in widgetState quale regalo è stato selezionato e, eventualmente, invia un follow‑up o una nuova chiamata di tool per i dettagli.
  4. I ToolOutput successivi si basano su questa scelta.

Dal punto di vista del codice, sembra un normale stato di React più la chiamata a setWidgetState, che salva la scelta lato ChatGPT. La differenza è che questo stato è accessibile sia al modello sia al vostro backend, quindi va mantenuto compatto e non bisogna conservarci segreti.

Ne parleremo in dettaglio nei moduli sui workflow multi‑step e sui follow‑up. Già ora è utile pensare così: ToolOutput vi dà un “ritaglio di dati” dal server, e widgetState è il contesto della scelta dell’utente attorno a quel ritaglio.

Errori tipici nel lavoro da ToolOutput → UI

Errore n. 1: «La UI renderizza l’albero JSON grezzo senza adattarlo all’utente».
A volte per il debug viene voglia di fare semplicemente <pre>{JSON.stringify(toolOutput)}</pre> e fermarsi lì. Per lo sviluppo va bene, ma in produzione l’utente vede una struttura di cui voi andate fieri, ma che lui non capisce. È importante incapsulare il prima possibile structuredContent in componenti significativi (liste, card, tabelle), senza costringere la persona a leggere una risposta tokenizzata del server.

Errore n. 2: Mescolare dati di dominio e metadati tecnici in structuredContent.
Il codice è molto più pulito se si separa: «ciò che deve essere visibile al modello e all’utente» da «ciò che serve solo a UI e analitica». I campi tecnici — flag sperimentali, versioni dei cataloghi, idempotency key — appartengono a _meta / toolResponseMetadata. Quando tutto questo è mescolato in structuredContent, è più difficile far evolvere il contratto e testare il comportamento del modello.

Errore n. 3: Mancanza di stati espliciti di caricamento, risultato vuoto ed errori.
Un <div></div> vuoto al posto di «Nessun risultato» o «Qualcosa è andato storto» porta l’utente a pensare: «L’app non funziona». Anche segnaposti testuali minimi e un semplice skeleton migliorano drasticamente la UX. Non fate affidamento solo sullo stato di sistema di ChatGPT «Sto eseguendo X…» — anche il widget deve comunicare cosa gli sta succedendo.

Errore n. 4: Cercare di infilare in un solo ToolOutput il mondo intero.
Restituire l’intero catalogo prodotti, la cronologia dell’utente e persino i log del server in un unico structuredContent è una cattiva idea. Va contro i limiti del modello, rallenta la risposta e complica la UI. Meglio restituire esattamente il volume di dati necessario per lo step corrente (pagina di lista, dettagli dell’elemento selezionato, ecc.) e i passi successivi implementarli con chiamate di tool separate.

Errore n. 5: Collegare rigidamente la UI a una forma instabile della risposta, senza tipi.
Se in tutto il codice scrivete toolOutput.structuredContent.items[0].whatever, senza verificare la presenza dei campi e senza tipi, qualsiasi evoluzione dello schema sul server porterà ai crash del widget. Conviene o sincronizzare i tipi con JSON Schema (generazione dei tipi TS), o almeno descrivere a mano le interfacce (GiftItem, ToolOutput) e lavorare con attenzione con i campi opzionali.

Errore n. 6: Ignorare _meta e sovraccaricare il modello con campi “superflui”.
A volte è forte la tentazione di mettere in structuredContent tutto, perché «tanto è JSON, non c’è niente di superfluo». Ma ogni campo aumenta il contesto del modello e molte cose al modello non servono affatto. Se un’informazione non deve influenzare il ragionamento di GPT e non serve nella risposta testuale, mettetela in _meta e usatela solo nel widget.

Errore n. 7: Accessi diretti a window.openai da una decina di componenti.
Sì, window.openai.toolOutput funziona, ma quando mezza applicazione inizia a pescare da una variabile globale, il debug e i test diventano un incubo. Molto meglio incapsulare una volta in un hook/contesto (useWidgetProps/useToolOutput) e poi usare props normali e oggetti tipizzati. È più pulito e più facile da sostituire con fixture in Storybook/test.

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