M. MachucaDesenvolvedor de Soluções em IA
EN

← Portfólio · M-07

M-07Estudo de caso

Nexo Assist

Um agente de service desk de TI, feito como desafio técnico de um processo seletivo.

O funcionário descreve o problema com as próprias palavras. O agente consulta os chamados dele, busca na central de ajuda, abre ou complementa um chamado depois da confirmação e chama um analista quando precisa. Cada resposta mostra o que ele entendeu, o que fez e por quê.

Demo pública: entre com um clique como Ana (funcionária) ou Rafael (analista), com dados fictícios. Até 100 mensagens por dia, somando todos os visitantes.

Meu papel
Arquitetura, regras, critérios de aceite e provas. Agentes de código escreveram o código, em incrementos pequenos e verificados.
Entregue
28 out 2026 · 154 commits
Stack
TypeScript estrito · Next.js 16 · Mastra · GPT-6 Luna (OpenRouter) · Drizzle + libSQL/Turso · Redux Toolkit · Vitest · Playwright
Código
Público no GitHub, com specs, relatórios de validação e o vídeo de demo.

Nexo Assist em 60 segundos

O problema
Uma empresa fictícia de cerca de 2.000 funcionários tem a TI afogada em triagem: chamados sem contexto, duplicados e com prioridade errada.
O que o agente faz
Conversa com o funcionário, busca na central de ajuda, abre ou complementa o chamado depois da confirmação e chama um analista quando precisa. Cada resposta mostra o que ele fez e por quê.
A prova
36/36 evals com o modelo real, 21/21 no red team e 472 testes sem LLM, cada número com a fonte no repositório.
Ver funcionando
Demo pública ↗, com um clique como Ana ou Rafael, ou o vídeo de 5:28.

N-01O desafio

O que foi pedido

Uma empresa fictícia de cerca de 2.000 funcionários tem a TI afogada em triagem: chamados sem contexto, duplicados, com prioridade errada, e dúvidas simples esperando na fila.

O processo seletivo pediu o Nexo Assist, um agente num chat que tira dúvidas pela central de ajuda, consulta e abre chamados, e passa a conversa para um analista quando precisa. Do outro lado, um console para o analista assumir a conversa com o agente pausado, devolver ou encerrar.

A parte que o próprio enunciado chama de mais importante: dar para entender, em cada resposta, o que o agente fez e por quê.

Requisitos

  • TypeScript com tipagem estrita; Mastra como framework do agente; qualquer LLM, com a escolha explicada.
  • Regras do service desk: prioridade por impacto e urgência, prazos em horas úteis, sem duplicidade, reabertura em até 7 dias.
  • Um serviço de chamados simulado, separado do agente, imitando um legado que às vezes cai.
  • Conversas, chamados e o estado de cada conversa sobrevivendo a um restart.
  • Segurança e privacidade (chamado de uma pessoa nunca aparece para outra) e custo por conversa sob controle.
  • Rodar localmente com poucos comandos, seguindo o protótipo de telas.

N-02Arquitetura

A planta do sistema

A mesma estrutura do src/ do repositório. Escolha uma etapa para a câmera ir até ela; no celular, as etapas avançam conforme você rola.

  1. À esquerda, o navegador. No centro, o app em camadas. Em volta, o modelo, o banco e as peças opcionais. Embaixo, a entrega. Linha tracejada é peça opcional.

  2. DDD enxuto. As dependências só apontam para dentro: app/, agent/ e infrastructure/ dependem de application/, que depende de domain/. O domínio não conhece Next, Mastra nem banco, então prioridade, prazo e reabertura são testados sem mock. A infraestrutura implementa as portas da aplicação, e um único arquivo liga as peças (container.ts).

  3. Um agente Mastra com 11 tools: recordUnderstanding, searchHelpCenter, askDiagnosticCheck, listMyTickets, getTicket, draftNewTicket, draftTicketUpdate, confirmDraft, cancelDraft, offerQuickReplies e transferToAnalyst. O primeiro passo é forçado a registrar o entendimento (intenção, confiança e motivo), e o código decide quais tools existem em cada passo. O modelo é o GPT-6 Luna, pelo OpenRouter; se ele falha, o passo vai para o Gemini 3.5 Flash Lite e o trace diz quem respondeu.

  4. Drizzle sobre libSQL: SQLite no desenvolvimento, Turso no deploy. Conversas, mensagens com o trace, rascunhos e chamados sobrevivem a um restart. O sistema de chamados legado é simulado por um decorator que falha em 15% das chamadas; o serviço tenta de novo sem duplicar. Os embeddings da busca ficam em cache no banco.

  5. O chat recebe cada turno em NDJSON: passos, cards e texto. O console do analista, e o chat enquanto um analista atende, abrem um stream SSE. O servidor confere uma assinatura barata do banco a cada 1 s e avisa quando algo muda. Se o stream cai, a tela volta ao polling sozinha.

  6. A produção roda na Vercel com o banco no Turso, publicada a partir de um git archive do último commit. Em paralelo, uma imagem Docker com prova de fumaça e o caminho CircleCI → Docker Hub → Komodo. Esse caminho roda desde 29 out 2026: a cada push no main com o CI verde, o Komodo puxa a imagem nova em 10 a 20 minutos.

  7. Telemetria OpenTelemetry com as convenções GenAI, pronta para o Langfuse, e SSO OpenID Connect (PKCE, state, nonce, JWKS). Os dois vêm desligados e foram testados contra servidores locais que seguem os protocolos, não contra Langfuse, Entra ID ou Okta de verdade.

Fonte: docs/specs/04-arquitetura.md · src/agent/tools/index.ts · docs/DEPLOY.md

N-03O que foi construído

Dois lados do atendimento

Para o funcionárioResolver sem fila

  • Escreve do jeito dele. O agente busca na central de ajuda e pergunta “Isso resolveu?”; a resposta entra no indicador de resolução.
  • Acha o chamado sem o número e avisa quando o prazo está em risco (75% do prazo útil), com a conta explicada.
  • Monta um rascunho com prioridade e prazo calculados em código. Nada é criado antes de a pessoa confirmar; “Alterar detalhes” recalcula no servidor.
  • Quando três ou mais pessoas relatam o mesmo problema em 4 horas úteis, sobe o impacto para “equipe” sem revelar quem relatou.
  • Frustração alta faz aparecer o botão “Falar com um analista”. Pedido explícito, caso crítico, prazo vencido ou dúvida que persiste transferem a conversa.
  • O painel “por quê?” em cada resposta: intenção, confiança, motivo, tools com entrada e saída, regras que agiram, modelo, tokens e tempo.
Rascunho de chamado no chat: Monitor do notebook não liga, incidente de hardware, prioridade Alta com resposta até 12:00, só a funcionária afetada, e os botões Confirmar, Alterar detalhes e Cancelar
Do app · rascunho com prioridade e prazo do código
Painel por quê? aberto no chat: o que o agente entendeu, a intenção, a confiança de 100%, o motivo e os artigos que a busca achou, com a similaridade de cada um
Do vídeo de demo · o painel “por quê?” de uma resposta

Para o analistaChegar com contexto

  • Uma fila com sinais: frustração alta, possível incidente em massa, motivo da transferência.
  • O relatório da conversa: o que a pessoa precisa, o que o agente já fez e o que está pendente. É montado do histórico e dos traces, sem chamar o modelo.
  • Assume a conversa com o agente pausado, devolve ou encerra. O banco só aceita a mudança se o estado ainda for o esperado, então analista e agente não se atropelam.
  • “Sugerir resposta”: o modelo escreve, o analista revisa antes de enviar.
  • Tudo em tempo real pelo stream SSE, com o polling como reserva.
  • Indicadores honestos: resolução confirmada separada da presumida, sem contar chamado aberto nem quem voltou com o mesmo problema, e as lacunas da central com dados pessoais mascarados.
Console do analista com o relatório da conversa aberto: o que a funcionária precisa, o chamado citado com prioridade Alta, a pendência e o sinal de frustração alta
Do vídeo de demo · relatório da conversa no console
Tela de indicadores: resolução pelo agente, tempo de resposta, custo estimado e a resolução honesta separada em confirmadas, presumidas e abandonadas
Do vídeo de demo · indicadores calculados do banco

N-04Vídeo de demo

Da conversa ao chamado, e do chamado ao analista

Em 5:28, o funcionário resolve sozinho, o agente monta o chamado e o analista assume no console. As partes do app são gravações reais de 28/10, feitas com o Gemini; as esperas pelo modelo aparecem aceleradas e sinalizadas. Diagramas e números são animações, atualizadas em 30/10: busca com avaliador de relevância, GPT-6 Luna e os números atuais, e cada número mostra a fonte no repositório. Tem música original de fundo e efeitos sonoros.

H.264 · 1920×1080 · 27 MB · só toca quando você der play · narração com voz sintética (Fish Audio) · trilha original · efeitos sonoros: Pixabay · também no repositório ↗

N-05Decisões

O que escolhi, e por quê

D-01 · modelo

Do Gemini ao GPT-6 Luna, decidido por um benchmark

Escolhi o Gemini pelo tool calling confiável (inclusive a tool forçada no primeiro passo, base do painel “por quê?”), pela latência baixa com o raciocínio no mínimo e por custar perto de 1 centavo de dólar por resposta.

Depois troquei a escolha qualitativa por dados: rodei o agente de produção com 7 modelos, medindo efetividade, qualidade (G-Eval com 2 juízes), latência e custo real, com um teto de US$ 1,80. O GPT-6 Luna empatou com o Gemini dentro dos intervalos de confiança e custou 26 vezes menos. Mantive o Gemini enquanto ele tinha saldo. Quando os créditos do Google acabaram, em 29/10, troquei pelo Luna com uma variável de ambiente, depois de confirmá-lo nos evals: 36/36.

7 modelos · 24 conversas por modelo · Luna 26× mais barato, mesma qualidade · Flash Lite como reserva

D-02 · busca

Busca que sabe quando o artigo não se aplica

A central de ajuda é buscada por sentido (embeddings) e por palavras-chave. Quando o primeiro resultado não se destaca do segundo, a busca não devolve nada: um artigo parecido leva o agente a responder errado, e é melhor perguntar.

Medi em 50 perguntas escritas como um funcionário escreveria, incluindo perguntas sem artigo certo. Um conjunto de 17 foi escrito só depois da escolha, para conferir que ela não foi decorada. Nele o acerto geral empatou, e o relatório diz isso.

Com os artigos de diagnóstico apareceu o limite do embedding: ele acha o assunto, mas não sabe se o artigo se aplica. “Queria um teclado sem fio” caía no artigo de conserto, e “ventilador fazendo barulho” recebia “Computador não liga”. A busca virou duas etapas, como no Corrective RAG: a híbrida traz 5 candidatos e um modelo barato diz qual se aplica, qual talvez ou nenhum. Antes de corrigir, escrevi 18 perguntas de pedido × defeito × fora da central e congelei num commit. Comparei rerankers, limiares e juiz com o catálogo inteiro; o limiar só funcionava num valor exato, então ficou a decisão categórica.

81 perguntas: 56 → 78 certas · artigo errado: 22 → 0 · congeladas: 9/18 → 18/18 · US$ 0,00025 por busca · sem o avaliador, volta à margem

D-03 · garantias

Regras em código, não no prompt

O modelo decide o que fazer. O que precisa ser garantido fica em código testado: prioridade pela matriz impacto × urgência, prazo em horas úteis, confirmação obrigatória e atômica, reabertura só até 7 dias, identidade vinda do cookie assinado e nunca de um argumento do modelo.

Quais tools existem em cada passo também é decidido em código, no padrão action-selector. Um texto malicioso pode mudar o que o modelo quer fazer, mas não o que ele consegue fazer. O prompt cuida só do jeito de conversar.

step-policy.ts · domain/priority.ts · domain/business-time.ts · draft-service.ts

D-04 · medição

Evals honestos

Os evals conferem o estado do banco, não só o texto da resposta. Cada ataque roda 3 vezes, e passar 1 vez em 3 conta como falha. Quando o Gemini devolvia 503 dentro do stream, um ataque podia “passar” sem o modelo ter respondido; isso agora conta como falha do provedor, à parte.

O funcionário simulado (pass^k, como no τ-bench) é publicado com o resultado que deu, mesmo quando é baixo. Na primeira rodada ele achou quatro defeitos de verdade, como abrir chamado a partir de uma resposta rápida que não era um “sim”. Cada um virou regra em código com teste, não um ajuste no prompt, e a rodada seguinte foi medida do mesmo jeito.

banco conferido · ataques × 3 · falha do provedor separada · pass^1 0,72 → 0,93 e pass^3 0,50 → 0,80 depois das correções

N-06Qualidade e provas

Os números, com a fonte

Testes de unidade e E2E rodam sem LLM. O que depende do modelo roda com o modelo de verdade e tem um relatório com pergunta, método, números e limites.

Rodada de 30 out 2026, no main do repositório

Ver os números e as fontes
testes unitários
472
Vitest, sem LLM: prazos, prioridade, estados da conversa, privacidade, duplicidade, CSRF, OIDC contra token forjado, pass^k.
README · npm test
testes E2E
42
Playwright contra o build de produção, sem LLM: login e SSO, transferência, relatório, indicadores, tempo real, erros.
README · npm run test:e2e
evals com o modelo real
36/36
Intenção, tools esperadas e proibidas, transferência, caso crítico e vazamento de dados. Dentro deles, 8/8 ataques, 6 cenários tirados das falhas do funcionário simulado e 2 de verificações de diagnóstico. Rodada com o GPT-6 Luna, o modelo de produção desde 30/10: o único cenário que falhou com ele supunha o Gemini pulando o autoatendimento e foi corrigido no teste, não no agente.
docs/validacao/README.md
red team, mapeado ao OWASP
21/21
7 ataques × 3: injeção, vazamento, ação sem confirmação, prompt do sistema, desvio. Injeção indireta por um artigo da central: 3/3.
docs/validacao/RED-TEAM.md
funcionário simulado
0,93 pass^1 · antes 0,72
Um modelo faz o papel do funcionário, com fatos que só conta se perguntarem. Três rodadas no mesmo dia: 0,72 / 0,50 → 0,93 / 0,80 → 0,93 / 0,80, a terceira com uma persona repetida depois de corrigida. Com 10 personas × 3, 28/30 conversas certas; pass^3 0,50 → 0,80 (8/10 personas certas nas 3 execuções). A primeira rodada achou defeitos reais no agente, corrigidos com regras em código e testes, não com ajustes no prompt.
docs/validacao/SIMULACAO.md
busca na central
56 → 78 de 81 · 0 artigo errado
Palavras-chave contra híbrida com avaliador de relevância, em quatro conjuntos; 18 perguntas de pedido × defeito × fora da central congeladas antes da correção: 9/18 → 18/18.
docs/validacao/BUSCA.md
SSE contra polling
263–580 ms
Mediana com o stream, contra 1.505–1.684 ms no polling antigo. O teste E2E falha se o p95 passar de 2 s.
docs/validacao/TEMPO-REAL.md
sugestão de resposta
9/9
3 conversas × 3, sem número nem ação inventados no rascunho para o analista.
docs/validacao/README.md
revisões de segurança
4
Antes e depois do deploy, 42 verificações de fora contra a produção e uma rodada com skills de auditoria de terceiros. npm audit sem vulnerabilidades.
docs/DEPLOY.md

N-07Entrega

Do commit ao ar

Vercel, em produção

O app está no ar na Vercel, com o banco no Turso, como demo pública desde 29 out 2026. O custo fica preso em código: até 100 mensagens por dia somando todos os visitantes, limites por pessoa e um teto total, todos reservados no banco antes de chamar o modelo. A porta de acesso por link continua no código: sem a chave PUBLIC_DEMO, a Vercel volta a exigir o link.

A publicação sai de um git archive do último commit, então nada fora do git sobe. Há tetos de uso por pessoa, por dia e no total, reservados no banco antes de chamar o modelo.

git · maindeploy-prod.ps1Vercel + Turso

no ar

Docker, CircleCI e Komodo

A imagem Docker (Node 24, sem root, banco num volume) tem 486 MB e passou nas 25 checagens da prova de fumaça: migra, cria os dados de exemplo, faz login real, confere permissões e reinicia sem perder dados.

O CircleCI roda tipos, lint, testes, E2E e a mesma prova de fumaça, e publica a imagem no Docker Hub. O Komodo confere o digest da latest a cada 10 minutos e faz o redeploy. O CI nunca publica na Vercel.

git · mainCircleCIDocker HubKomodo

rodando

Na primeira execução, em 28 out 2026, o CircleCI pegou um erro que a máquina local escondia: o tipo LayoutProps que o Next 16 gera em .next/ não existe num clone limpo. A correção foi rodar next typegen antes do tsc. A imagem está no Docker Hub, e o Komodo a roda só na minha máquina, em 127.0.0.1:3300. docs/DEPLOY.md

N-08Em aberto

O que ainda falta

Ver o que ainda falta
  • O que o funcionário simulado pegou

    Todos os defeitos das três rodadas viraram regra em código, com testes: confirmação só explícita (e um “Sim, é esse mesmo” que só identifica o chamado não reabre nada), problema diferente vira chamado novo, Outlook entra como E-mail, analista só com motivo (prazo que não atende, chamado vencido ou frustração) e a pergunta sobre alternativa antes da prioridade. Repetidos depois das correções de 29 out, senha-sap e vpn-reuniao passaram 3 de 3, e a reabertura nos evals também. O que fica é a variação do próprio modelo: na suíte completa de 29 out, um cenário de incidente em massa falhou uma vez e passou 3 de 3 repetido.

  • Entrega por imagem só local

    O caminho CircleCI → Docker Hub → Komodo roda, mas o Komodo fica na minha máquina, só em loopback: prova a entrega por imagem, não é um segundo ambiente público. A produção continua na Vercel, publicada à mão.

  • Amostra pequena

    10 personas × 3 execuções é pouco para cravar uma taxa. Os números mostram a direção, não uma garantia.

  • Troca de modelo

    O benchmark recomenda o GPT-6 Luna depois de uma rodada de confirmação contra o Flash Lite. A troca não foi feita.

  • Chamadas repetidas

    A telemetria mostrou o modelo chamando a tool de entendimento várias vezes em paralelo no primeiro passo. Só a primeira vale, mas as outras gastam tokens.

  • Evals fora do CI

    Rodam à mão porque chamam o modelo real. O próximo passo é rodá-los no CI com um limiar mínimo.

  • Serviços de verdade

    O SSO só foi testado contra um provedor falso, e a telemetria contra um coletor local. Sair não revoga o JWT no servidor, e o console precisa de tela larga.

Usar a demo ↗ Ver o repositório ↗ Contato pelo LinkedIn ↗ ← Voltar ao portfólio