Objetivo da Aula
Ao concluir esta aula, você será capaz de:
Implementar streaming de texto via AsyncGenerator no React (chatbotStream)
Construir busca semântica por keyword como fallback de embeddings reais
Montar system prompt dinâmico com contexto de produtos para o chatbot
Completar os 2 TODOs do componente ChatSuporte
Conectar Firebase AI Logic real como alternativa ao mock
Por que isso importa
Chatbots que mostram a resposta inteira de uma vez após 3-5 segundos têm péssima UX — o usuário não sabe se o sistema está processando. Streaming resolve: o texto aparece palavra por palavra, como um humano digitando, com latência percebida de ~300ms.
A combinação RAG + chatbot é o pattern de suporte ao cliente mais adotado em 2024: busca semântica encontra produtos/artigos relevantes → LLM usa como contexto → resposta precisa e fundamentada.
Conceitos Fundamentais
Streaming com AsyncGenerator
async function* chatbotStream(mensagem: string, historico: Mensagem[], contextoProdutos: ProdutoInfo[]): AsyncGenerator<string> {
// Cada 'yield' entrega um chunk de texto ao componente React
yield 'Olá, '
yield 'tudo bem? '
yield 'Vou ajudar com sua dúvida.'
}No React, consumir o stream:
for await (const chunk of chatbotStream(input, mensagens, produtos)) {
textoCompleto += chunk
setStreamingText(textoCompleto) // re-render a cada chunk
}Cada yield causa um re-render que atualiza o texto na tela. O resultado visual: texto que “digita” em tempo real.
TODO 1: Busca Semântica
async function buscaSemantica(query: string): Promise<ProdutoInfo[]> {
// Implementação com embeddings reais (Firebase AI / OpenAI):
// const emb = await model.embedContent(query)
// const resultados = await firestore.collection('produtos')
// .where('embedding', nearestNeighbor(emb)).limit(3).get()
// Fallback por keyword (suficiente para o lab):
const queryLower = query.toLowerCase()
const termos = queryLower.split(' ').filter(t => t.length > 3)
const scored = PRODUTOS.map(p => {
const texto = `${p.nome} ${p.descricao}`.toLowerCase()
const score = termos.filter(t => texto.includes(t)).length
return { produto: p, score }
}).filter(({ score }) => score > 0)
scored.sort((a, b) => b.score - a.score)
return scored.slice(0, 3).map(({ produto }) => produto)
}TODO 2: Chatbot com Streaming
async function* chatbotStream(
mensagem: string,
historico: Mensagem[],
contextoProdutos: ProdutoInfo[],
): AsyncGenerator<string> {
// 1. Montar contexto de produtos
const contextoProdutosTexto = contextoProdutos.length > 0
? `PRODUTOS RELEVANTES:\n${contextoProdutos.map(p =>
`- ${p.nome} (R$${p.preco}/mês): ${p.descricao}`
).join('\n')}`
: 'Nenhum produto específico encontrado para esta query.'
// 2. System prompt com contexto
const systemPrompt = `Você é um assistente de suporte ao cliente.
Responda de forma clara e amigável em português.
Use os produtos abaixo para fundamentar suas respostas:
${contextoProdutosTexto}
Se a pergunta não for sobre os produtos, responda de forma geral mas útil.`
// 3. Converter histórico para formato do LLM
const mensagensFormatadas = historico.map(m => ({
role: m.role as 'user' | 'assistant',
content: m.content,
}))
mensagensFormatadas.push({ role: 'user', content: mensagem })
// 4. Stream com Anthropic SDK (Firebase AI tem API similar)
const stream = client.messages.stream({
model: 'claude-haiku-4-5-20251001',
max_tokens: 500,
system: systemPrompt,
messages: mensagensFormatadas,
})
// 5. Yield cada chunk de texto
for await (const event of stream) {
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
yield event.delta.text
}
}
}Aprofundamento Técnico
Firebase AI Logic Real
Para substituir o mock por Firebase AI real:
import { initializeApp } from 'firebase/app'
import { getAI, getGenerativeModel } from 'firebase/ai'
const app = initializeApp({
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
// ...
})
const ai = getAI(app)
const model = getGenerativeModel(ai, { model: 'gemini-2.0-flash' })
async function* chatbotStreamFirebase(
mensagem: string,
historico: Mensagem[],
contextoProdutos: ProdutoInfo[],
): AsyncGenerator<string> {
const prompt = montarPrompt(mensagem, historico, contextoProdutos)
const stream = await model.generateContentStream(prompt)
for await (const chunk of stream.stream) {
const texto = chunk.text()
if (texto) yield texto
}
}A estrutura de AsyncGenerator é idêntica — só o provider muda.
aria-live para Acessibilidade do Streaming
{streamingText && (
<div
role="status" // anuncia ao leitor de tela que é uma região de status
aria-live="polite" // anuncia quando o texto mudar sem interromper
aria-atomic={false} // false = anuncia incrementalmente (não re-lê tudo)
>
{streamingText}<span aria-hidden>▌</span> {/* cursor visual apenas */}
</div>
)}Sem aria-live, usuários de leitor de tela não ouvem o texto sendo digitado.
Exemplos Anotados
Exemplo 1: Todos os TODOs Implementados
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic()
// TODO 1: busca semântica com score de relevância
async function buscaSemantica(query: string): Promise<ProdutoInfo[]> {
const queryLower = query.toLowerCase()
const termos = queryLower.split(/\s+/).filter(t => t.length > 2)
return PRODUTOS
.map(p => {
const texto = `${p.nome} ${p.descricao}`.toLowerCase()
const score = termos.reduce((acc, t) => acc + (texto.includes(t) ? 1 : 0), 0)
return { produto: p, score }
})
.filter(({ score }) => score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map(({ produto }) => produto)
}
// TODO 2: chatbot com streaming usando Anthropic SDK
async function* chatbotStream(
mensagem: string,
historico: Mensagem[],
contextoProdutos: ProdutoInfo[],
): AsyncGenerator<string> {
const contextoProdutosTexto = contextoProdutos.length > 0
? 'PRODUTOS DISPONÍVEIS:\n' + contextoProdutos.map(p =>
`• ${p.nome} (R$${p.preco}/mês): ${p.descricao}`
).join('\n')
: 'Não foram encontrados produtos específicos para esta consulta.'
const system = `Você é um assistente de suporte ao cliente prestativo e amigável.
Responda sempre em português do Brasil, de forma clara e concisa.
${contextoProdutosTexto}`
const messages = [
...historico.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content })),
{ role: 'user' as const, content: mensagem },
]
const stream = client.messages.stream({
model: 'claude-haiku-4-5-20251001',
max_tokens: 500,
system,
messages,
})
for await (const event of stream) {
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
yield event.delta.text
}
}
}Padrões e Armadilhas
Padrões
Padrão 1: Mock antes de integração real O starter usa mockAI — permite desenvolver e testar o componente sem credenciais Firebase. Substitua por API real apenas quando a UI estiver funcionando.
Padrão 2: System prompt com contexto dinâmico
const system = `Dados relevantes: ${JSON.stringify(contextoProdutos, null, 2)}`O contexto de produtos muda por query — o system prompt é montado dinamicamente.
Padrão 3: setStreamingText('') após finalizar
setStreamingText(''); // limpa o indicador de streaming
setCarregando(false);Sem isso, o cursor ▌ fica visível após a resposta completa.
Armadilhas
⚠️ Armadilha 1: for await sem tratamento de erro
try {
for await (const chunk of chatbotStream(...)) {
textoCompleto += chunk
setStreamingText(textoCompleto)
}
} catch (err) {
setStreamingText('Erro ao carregar resposta. Tente novamente.')
} finally {
setCarregando(false)
}⚠️ Armadilha 2: Histórico muito longo = contexto explode Limite o histórico enviado para o LLM:
const historicoRecente = historico.slice(-10) // últimas 10 mensagens⚠️ Armadilha 3: client instanciado dentro do componente React
// ERRADO: nova instância a cada render
export default function Chat() {
const client = new Anthropic() // ← re-instancia a cada render!
}
// CORRETO: fora do componente (ou useMemo se depender de variável)
const client = new Anthropic()
export default function Chat() { ... }Se não for realizar o laboratório, pule para o próximo capítulo.
Ponte para o Lab
2 TODOs no starter:
TODO 1 — buscaSemantica(query):
const queryLower = query.toLowerCase()
return PRODUTOS.filter(p =>
p.nome.toLowerCase().includes(queryLower) ||
p.descricao.toLowerCase().includes(queryLower)
)(versão mínima — expanda para scoring de relevância se quiser)
TODO 2 — chatbotStream(...): Use o Anthropic SDK (já disponível no projeto) com client.messages.stream(). O Exemplo 1 desta aula tem a implementação completa.
Teste com perguntas como “Qual plano tem mais storage?” e verifique que a busca semântica retorna o produto relevante e o chatbot responde usando esse contexto.
Agora você está pronto para o lab.