CodeGym /Cursos /ChatGPT Apps /Processamento do resultado da ferramenta no widget: ToolO...

Processamento do resultado da ferramenta no widget: ToolOutput → UI

ChatGPT Apps
Nível 4 , Lição 3
Disponível

1. De ToolOutput ao componente React: fluxo geral de dados

Na aula anterior, vimos como a ferramenta no servidor forma o ToolOutput — uma resposta estruturada para o modelo e o widget. Agora vamos olhar a segunda metade desse caminho: como esse ToolOutput chega ao widget e vira UI.

Para não encarar o que acontece como mágica, vamos repassar o caminho dos dados do usuário até o seu widget. Em forma simplificada, é assim:

  1. O usuário faz uma pergunta no chat.
  2. O GPT analisa o pedido, olha a lista de ferramentas e decide: “Agora vai me ajudar suggest_gifts”.
  3. O GPT forma um tool call com nome e argumentos (ToolInput) e o envia para o seu servidor (MCP ou backend).
  4. O servidor executa a lógica da ferramenta e retorna o resultado como ToolOutput — um JSON estruturado com dados, mais um resumo textual para o modelo.
  5. O ChatGPT recebe o ToolOutput e o passa adiante: para o modelo (para continuar o diálogo) e para o seu widget via Apps SDK (window.openai.toolOutput ou via hooks).
  6. Seu widget — um componente React comum — lê o toolOutput e renderiza a UI.

Esquematicamente, fica assim:

flowchart TD
  U[Usuário] -->|pergunta no chat| GPT[GPT]
  GPT -->|callTool: suggest_gifts| B[Backend/MCP]
  B -->|"ToolOutput (JSON)"| GPT
  GPT -->|passa toolOutput| W["Widget (React)"]
  W -->|cartões, listas| U

É importante fixar a ideia: ToolOutput não é apenas “resposta do servidor”. Ele também é seu comando de renderização para o widget e, ao mesmo tempo, contexto para o modelo. Um bom App é aquele em que esse JSON vira uma interface conveniente, e não algo que o desenvolvedor precisa rolar com os olhos no DevTools.

2. Anatomia do ToolOutput: o que há dentro

O formato do resultado da ferramenta no Apps SDK se divide em três blocos lógicos: structuredContent, content e _meta (que chega ao widget com o nome toolResponseMetadata).

De forma aproximada, podemos representá-lo assim:

{
  "structuredContent": { /* dados para UI + modelo */ },
  "content": "Resumo curto em texto para o modelo e o usuário",
  "_meta": { /* dados de serviço apenas para o widget */ }
}

Na tabela abaixo, veja quem enxerga o quê:

Campo Quem vê Para que serve
structuredContent
Modelo + widget Principais dados estruturados (listas, objetos, parâmetros)
content
Modelo + usuário (no texto) Resumo curto que o GPT pode incluir na resposta
_meta
Apenas o widget Dados técnicos que o modelo não precisa (IDs, versões, chaves etc.)

A documentação do Apps SDK enfatiza que a dupla structuredContent / content vai para o modelo e pode ser usada nas respostas subsequentes. O campo _meta permanece oculto e acessível apenas dentro do widget via toolResponseMetadata.

Exemplo de ToolOutput para GiftGenius

Suponha que nossa ferramenta de servidor suggest_gifts retorne algo assim:

{
  "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": "Encontrei algumas ideias de presentes. O widget abaixo as mostra como cartões.",
  "_meta": {
    "giftGenius": {
      "catalogVersion": "2025-10-01",
      "experimentBucket": "A"
    }
  }
}

Aqui, structuredContent.items é o que seu widget React vai renderizar; o content pode ser usado pelo modelo para explicar ao usuário o que está acontecendo; _meta.giftGenius é informação interna, necessária apenas para sua UI ou para analytics (por exemplo, qual versão do catálogo usar para os links).

É justamente o structuredContent o objeto que você vai observar no JSX, em vez de fazer parsing manual de um JSON arbitrário do servidor.

3. Obtendo o ToolOutput no widget: window.openai e hooks

Hora de sair da conversa sobre JSON e ir para o código. Como esse ToolOutput chega ao seu componente React?

O template do Apps SDK faz isso de duas formas principais: ou diretamente via window.openai.toolOutput, ou, o que é ótimo, por meio de hooks React prontos (useWidgetProps, useToolOutput e similares). A abordagem recomendada é usar hooks, para não mexer no window.openai diretamente e ter um código mais testável e seguro.

Opção mais simples: direto de window.openai

Para entender, veja a opção “crua”:

'use client';

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

Não faça isso em produção; porém, para depuração e “dar uma olhada” nos primeiros passos — funciona.

Opção prática: via hook do React

É bem mais conveniente envolver o acesso a window.openai em um pequeno hook e trabalhar já com um objeto tipado. Suponha que nosso SDK provisório forneça um hook useWidgetProps, retornando toolOutput e toolResponseMetadata.

'use client';

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

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

  // Por enquanto, só exibimos a quantidade de presentes
  const items = toolOutput?.structuredContent?.items ?? [];

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

No template real, o nome do hook pode variar, mas a ideia é sempre a mesma: o SDK busca os dados em window.openai e os entrega ao seu componente como props ou via contexto. Isso é bem mais simples do que ir ao objeto global toda hora e, além disso, permite nos testes trocar facilmente a fonte de dados (por exemplo, injetar uma fixture de toolOutput).

4. Renderizando presentes: de structuredContent para JSX

Vamos à parte legal: pegar structuredContent.items e desenhar cartões a partir deles. Lembre-se de que nosso widget é um componente cliente React comum no Next.js ('use client' no topo do arquivo).

Primeiro, definimos o tipo de um presente:

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

Agora um pequeno componente de cartão:

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 um componente de lista que busca os dados em 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>
  );
}

Note como tudo aqui parece código React comum. A única “mágica” é a fonte de dados: em vez de props ou fetch, lemos o toolOutput do contêiner do ChatGPT.

E sim, não tem problema se no começo você adicionar as GiftItem[]. Depois, você pode tipar o structuredContent com cuidado por meio de tipos compartilhados com o backend (por exemplo, usar Zod / JSON Schema → tipos TS), mas para demonstração isso é suficiente.

5. Estados de UI ao redor do ToolOutput: carregando, vazio, erro

Um app que só mostra cartões quando dá sorte e fica mudo no resto do tempo não é muito amigável. É preciso tratar explicitamente, no mínimo, quatro estados: enquanto a ferramenta está executando, quando ainda não há dados, quando há resultado e quando algo deu errado.

O Apps SDK geralmente fornece alguma informação sobre o status da chamada da ferramenta: via lista de invocações de tool (useToolInvocations) ou flags associadas ao toolOutput. Para esta aula, basta um modelo simples: se o toolOutput ainda não chegou — estamos em “carregamento”; se chegou, mas a lista está vazia — “vazio”; se veio um erro — “erro”.

Para simplificar, vamos supor que o servidor, em caso de erro, coloca em structuredContent o campo error, e a flag ok na raiz do toolOutput fica false. Já discutimos esse esquema na aula anterior sobre a implementação no servidor, quando projetamos o contrato de resposta da ferramenta.

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

Agora atualizamos nosso componente de lista:

'use client';

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

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

  if (!toolOutput) {
    return <div>Procurando presentes…</div>;
  }

  if (!toolOutput.ok) {
    const msg = toolOutput.structuredContent?.error?.message
      ?? 'Não foi possível obter as recomendações.';
    return <div>Erro: {msg}</div>;
  }

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

  if (items.length === 0) {
    return <div>Não encontramos presentes para as suas condições. Tente ajustar os parâmetros.</div>;
  }

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

Esse código já oferece uma experiência decente ao usuário:

  • Enquanto a ferramenta trabalha, dá para ver que algo está acontecendo.
  • Se tudo cair — há uma mensagem compreensível, e não uma tela vazia.
  • Se nada for encontrado — não fingimos que é normal; explicamos honestamente o que aconteceu.

Em produção, você provavelmente vai trocar o texto “Procurando presentes…” por um pequeno skeleton ou spinner. Para erros complexos, dá para permitir que o GPT formule uma explicação legível. Mas a estrutura básica dos componentes permanece a mesma.

6. Usando _meta e toolResponseMetadata na UI

Já aprendemos a renderizar os dados principais de structuredContent e a tratar estados básicos de loading/empty/error. Falta ainda um pedaço importante do ToolOutput de que o modelo não faz uso — o campo _meta.

Voltando ao campo _meta. Ele não é visível ao modelo, mas chega ao seu widget como toolResponseMetadata (o nome pode variar, mas a ideia é essa).

É um ótimo lugar para aquilo que não deve afetar o raciocínio do GPT, mas é importante para a UI:

  • versões de catálogo ou de configuração;
  • ID interno de campanha/experimento A/B;
  • flags sobre quais “botões” mostrar ao usuário;
  • qualquer aspecto técnico que você não quer misturar com dados de domínio.

Por exemplo, o servidor pode retornar este _meta:

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

O widget pode ler isso e, digamos, exibir um badge “Ideia nova” em alguns cartões.

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

O modelo não tem nada a ver com isso: ele não sabe sobre catalogVersion e showExperimentalBadges, mas a sua UI pode usá-los como quiser.

A documentação enfatiza exatamente essa separação: dados relevantes para o diálogo e o raciocínio do modelo vão em structuredContent e content; tudo o que é puramente técnico de UI vai em _meta / toolResponseMetadata.

7. Um pouco sobre os status de ToolInvocation e “Executando X…”

Enquanto a ferramenta roda, o próprio ChatGPT mostra ao usuário o que está acontecendo: no topo do chat aparece um status como “Executando GiftGenius…” ou “Conectando-se a um app externo”. Não é você exibindo essas linhas, e sim o ambiente hospedeiro do ChatGPT reagindo aos metadados da invocação da ferramenta.

Por baixo dos panos, isso é descrito com chaves técnicas do tipo _meta["openai/toolInvocation/invoking"] e _meta["openai/toolInvocation/invoked"], que sinalizam que a ação está em execução ou concluída. Esses campos são usados pela própria plataforma para exibir o status e, em geral, você não precisa mexer neles: o SDK faz isso por você no lado do servidor.

Para UX, isso traz um bônus: mesmo que o widget ainda não tenha renderizado o skeleton, o usuário já vê que o sistema está fazendo algo. Sua tarefa é complementar esse status global com estados locais como “Procurando presentes…” e um skeleton no widget, como fizemos acima.

8. Tamanho dos dados e desempenho: não empurre o mundo inteiro para structuredContent

Vale falar sobre “quanto dá para colocar em structuredContent”. Pode parecer tentador: “Tenho o catálogo completo de presentes — vou enviá-lo inteiro, e o widget filtra”. Na prática, não faça isso.

Primeiro, structuredContent entra no contexto do modelo (LLM), e o volume total de tokens é limitado. A documentação e os guias práticos recomendam fortemente manter o volume enxuto: não é um repositório de dados, e sim o resultado de uma ação.

Segundo, quanto maior o payload, mais lento o retorno e maior a chance de topar com limites ou sofrer truncamentos/erros inesperados.

Abordagem sensata:

  • O backend filtra e ordena previamente, retornando exatamente o que é necessário para o passo atual: por exemplo, 10–20 melhores presentes.
  • Se precisar de próximas páginas, isso é uma nova ação (novo tool call, novo ToolOutput).
  • Para aspectos puramente de UI (por exemplo, a lista de possíveis tags para filtragem), você pode usar _meta, mas também com moderação.

No módulo sobre estado, já discutimos o conceito “backend — fonte da verdade; widget — cache/visualização”. Aqui é o mesmo: o resultado da ferramenta é um “recorte” cuidadoso do estado no momento da chamada, não uma cópia completa do seu banco.

9. Integração com o estado do widget e o diálogo subsequente

Embora esta aula seja oficialmente sobre ToolOutput → UI, vale lembrar que por perto existe outro pedaço importante — o widgetState. É ele que permite memorizar a escolha do usuário entre renderizações e transformar seu widget não apenas numa vitrine, mas num verdadeiro assistente ou “configurador de presentes”.

Um cenário típico:

  1. O primeiro ToolOutput traz a lista de presentes.
  2. O usuário clica em um dos cartões.
  3. O widget grava no widgetState qual presente foi selecionado e, possivelmente, envia um follow-up ou um novo tool call para detalhes.
  4. Os ToolOutput seguintes se baseiam nessa escolha.

Do ponto de vista do código, isso parece um estado React normal mais uma chamada a setWidgetState, que salva a escolha no lado do ChatGPT. A diferença é que esse estado está acessível ao modelo e ao seu backend, então precisa ser compacto e não conter segredos.

Vamos detalhar isso nos módulos sobre workflows de múltiplas etapas e follow-ups. Já agora, é útil pensar assim: o ToolOutput lhe dá um “recorte de dados” do servidor, e o widgetState — o contexto da escolha do usuário ao redor desse recorte.

Erros comuns ao trabalhar com ToolOutput → UI

Erro nº 1: “A UI renderiza a árvore JSON crua sem adaptação ao usuário”.
Às vezes dá vontade de, para depurar, fazer um <pre>{JSON.stringify(toolOutput)}</pre> e parar por aí. Para desenvolvimento, tudo bem, mas em produção o usuário vê uma estrutura da qual você se orgulha, mas que ele não entende. É importante, o quanto antes, embrulhar o structuredContent em componentes significativos (listas, cartões, tabelas), em vez de obrigar a pessoa a ler uma resposta tokenizada do servidor.

Erro nº 2: Misturar dados de domínio e metadados técnicos em structuredContent.
O código fica muito mais limpo se você separar: “o que deve ser visível ao modelo e ao usuário” e “o que é necessário apenas à UI e à analytics”. Campos técnicos — flags de experimento, versões de catálogos, idempotency key — pertencem a _meta / toolResponseMetadata. Quando tudo isso está misturado em structuredContent, fica mais difícil evoluir o contrato e testar o comportamento do modelo.

Erro nº 3: Falta de estados explícitos de carregamento, vazio e erro.
Um <div></div> vazio no lugar de “Nada encontrado” ou “Algo deu errado” é caminho certo para o usuário concluir: “O App não funciona”. Mesmo placeholders mínimos e um skeleton simples melhoram dramaticamente o UX. Não dependa apenas do status do sistema do ChatGPT “Executando X…” — o widget também deve dizer o que está acontecendo com ele.

Erro nº 4: Tentar colocar o mundo inteiro em um único ToolOutput.
Retornar o catálogo inteiro de produtos, o histórico do usuário e ainda logs do servidor dentro de um structuredContent é uma má ideia. Isso estoura limites do modelo, deixa a resposta lenta e complica a UI. É melhor retornar exatamente o volume de dados necessário para o passo atual (página de lista, detalhes do item selecionado etc.) e tratar os passos seguintes como novas chamadas de ferramenta.

Erro nº 5: Acoplamento rígido da UI a uma forma instável de resposta, sem tipos.
Se, em todo lugar do código, você escrever toolOutput.structuredContent.items[0].whatever, sem verificar a existência de campos e sem tipos, qualquer evolução do schema no servidor vai derrubar o widget. Vale a pena sincronizar tipos com JSON Schema (geração de tipos TS) ou, pelo menos, descrever manualmente as interfaces (GiftItem, ToolOutput) e trabalhar com campos opcionais com cuidado.

Erro nº 6: Ignorar _meta e sobrecarregar o modelo com campos “extras”.
É tentador enfiar em structuredContent tudo o que vem à mente, porque “é JSON, não faz mal”. Mas cada campo aumenta o contexto do modelo, e muitos deles não são necessários ao modelo. Se a informação não deve afetar o raciocínio do GPT e não precisa ir para a resposta textual, coloque-a em _meta e trabalhe com ela apenas no widget.

Erro nº 7: Acessos diretos a window.openai a partir de uma dezena de componentes.
Sim, window.openai.toolOutput funciona, mas quando metade do app começa a acessar a variável global, depurar e testar vira um inferno. É muito melhor embrulhar isso uma vez em um hook/contexto (useWidgetProps/useToolOutput) e, depois, usar props normais e objetos tipados. É mais limpo e mais fácil de substituir por fixtures no Storybook/testes.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION