#Um agente de IA no seu app iOS
Para colocar um agente de IA dentro do seu app de iPhone, você adiciona o pacote Swift da Vatio, cria um token publicável que permita o bundle id do app e liga um VatioChat às suas próprias views SwiftUI. O agente é o mesmo que você descreve em um arquivo vatio.yml e que responde no WhatsApp, no Instagram e na web. No app, ele também pode saber quem é o usuário: o seu backend assina um token, e as tools privadas do agente leem os dados desse usuário.
Este guia adiciona um assistente ao app de uma academia, do pacote até um aluno logado perguntando pelas reservas dele.
#Por que colocar o agente no app
Um usuário logado no seu app é um visitante cuja identidade você já conhece. No WhatsApp e no Instagram, o contato precisa entrar na conta pelo seu site antes de uma tool privada rodar. No app, o mesmo agente pode responder “em quais aulas eu estou inscrito?” com os dados daquele aluno.
Não é um segundo agente. Um workspace roda um único agente, e o app é mais um canal para ele. As instruções, as tools, o conhecimento, as regras de transferência e o inbox são os mesmos, então uma correção que você publica chega a todos os canais de uma vez.
#O que o SDK entrega e o que você constrói
O SDK é um pacote Swift para iOS 15 ou posterior, sem dependências além de Foundation e Combine, e sem UI.
| O SDK cuida de | Você constrói |
|---|---|
A conversa como estado observável: messages, isTyping, status | Cada tela: lista de mensagens, balões, campo de texto, indicador de digitação |
A mensagem do usuário na tela na hora, com isPending, e depois confirmada | Devolver o texto ao campo quando send lança um erro |
| Reconectar com backoff, fazer polling enquanto isso, buscar o que perdeu e deduplicar por id | Exibir content, que é Markdown escrito pelo agente: trate como não confiável |
Guardar a conversa em UserDefaults, por workspace e por usuário logado | O seletor de fotos e o gravador que produzem os anexos |
| Os anexos, a transcrição de mensagens de voz e o momento de feedback, como estado | O cartão de avaliação e qualquer efeito de texto progressivo |
Nome, avatar, cor de destaque, saudação e sugestões do agente, com vatio.config() | O seu login e o endpoint do seu backend que assina o token do usuário |
#Libere o seu app: um token publicável
O app fala com a Vatio usando um token publicável (vatpub_…), feito para ir dentro dele. Um token corresponde a um workspace e a um ambiente, e só funciona a partir das origens permitidas. A origem de um app iOS é ios-app:// mais o bundle id:
vatio tokens create --env live --label ios --origin ios-app://example.academiaritmo.app
O create imprime o token completo uma única vez. Para adicionar o app a um token que já existe, vatio tokens origins PREFIX URL... substitui a lista desse token, então repita as origens que ele já tinha; a seção de tokens de Canais → Widget web, no inbox, faz o mesmo. O SDK envia o bundle id de Bundle.main em minúsculas, e um token que não o permite falha com origin_not_allowed.
A origem mantém o token no seu app. Ela não diz nada sobre quem é o usuário; isso vem do seu backend, mais abaixo. Nunca coloque um token de desenvolvedor (vat_…) em um app: com ele é possível fazer deploy, ler secrets e criar tokens.
#Adicione o pacote
No Xcode, escolha File → Add Package Dependencies…, digite https://github.com/vatio-ai/ios e adicione a biblioteca Vatio ao target do app. Em um Package.swift:
dependencies: [
.package(url: "https://github.com/vatio-ai/ios", from: "0.1.0")
],
targets: [
.target(name: "RitmoKit", dependencies: [.product(name: "Vatio", package: "ios")])
]
As tags seguem semver, então from: nunca leva você a uma nova versão major de surpresa.
#Uma primeira conversa
import SwiftUI
import Vatio
struct AssistenteView: View {
@ObservedObject var chat: VatioChat
@State private var texto = ""
var body: some View {
VStack {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(chat.messages) { message in
Text(LocalizedStringKey(message.content)) // Markdown básico
.frame(maxWidth: .infinity,
alignment: message.isFromVisitor ? .trailing : .leading)
.opacity(message.isPending ? 0.6 : 1)
}
if chat.isTyping { ProgressView() }
}
.padding()
}
HStack {
TextField("Pergunte o que quiser", text: $texto)
Button("Enviar", action: enviar)
}
.padding()
}
.task { await chat.resume() }
}
private func enviar() {
let pergunta = texto
texto = ""
Task {
do { try await chat.send(pergunta) }
catch { texto = pergunta } // o send removeu a mensagem pendente
}
}
}
Crie o chat uma única vez, por exemplo como um @StateObject com VatioChat(Vatio(workspace: "academia-ritmo", token: "vatpub_...")). Criá-lo não abre nada: a conversa começa no primeiro send, então um usuário que nunca escreve nunca vira contato. resume() reabre a conversa guardada, se houver, e nunca começa uma nova. send lança um VatioError com um code; as falhas fora de um send ficam em chat.lastError.
#Autentique o usuário com um token assinado pelo seu backend
Para dizer ao agente quem é o usuário, o seu backend assina um JWT e o app o repassa. A chave privada nunca vai no app: um binário pode ser desmontado, e com essa chave qualquer pessoa poderia se passar por qualquer usuário seu. A Vatio guarda só a metade pública, então consegue verificar uma identidade, mas nunca criar uma.
Rode vatio auth --new-key uma vez. O identity.pub fica no workspace, referenciado no bloco auth: do vatio.yml. O identity.pem vai para o seu backend como secret, não para o vatio secrets nem para o app. Depois adicione um endpoint, atrás do login do seu app, que assine um token RS256 de curta duração:
// O seu backend, por exemplo POST /me/vatio-token, atrás do login que você já tem
import jwt from "jsonwebtoken";
const visitorToken = jwt.sign(
{ sub: String(user.id), name: user.name, email: user.email },
process.env.VATIO_IDENTITY_PRIVATE_KEY,
{ algorithm: "RS256", audience: "academia-ritmo", expiresIn: "1h" }
);
O sub vira $auth.subject, o aud precisa ser exatamente o slug do workspace, e o exp é conferido de novo a cada chamada de tool. Se você adicionar um claim metadata com um link profile e badges, o inbox os mostra no cabeçalho da conversa, para quem lê saber de qual aluno se trata.
O VatioChat recebe o token na criação, então entrar ou sair da conta significa criar um novo. Um store pequeno, como o do app de exemplo do SDK, cuida disso:
@MainActor
final class AssistenteStore: ObservableObject {
@Published private(set) var chat: VatioChat
private let vatio: Vatio
init(vatio: Vatio) {
self.vatio = vatio
chat = VatioChat(vatio)
}
// visitorToken: o que o /me/vatio-token do seu backend devolveu
func entrou(visitorToken: String) async {
chat.close()
chat = VatioChat(vatio, visitorToken: visitorToken)
await chat.resume()
}
func saiu() {
chat.close()
chat = VatioChat(vatio)
}
}
#Agora as tools privadas rodam para esse usuário
Com um token verificado, as tools marcadas access: private rodam e chegam à sua API com o token do próprio usuário. tools/minhas_reservas.yml:
description: As próximas reservas de aulas do aluno logado.
when_to_use: Um aluno logado pergunta pelas próprias reservas ou aulas.
access: private
request:
method: GET
base_url: "$env.RITMO_API"
path: /me/reservas
headers:
Authorization: "Bearer $auth.token"
respond:
data:
reservas: "$.reservas"
message:
when_empty: Este aluno não tem reservas futuras.
default: "Encontrei {{count}} reservas futuras."
O seu endpoint verifica o JWT com a mesma chave pública e limita a consulta ao sub dele. A tool não recebe um id de aluno, e nem pode: uma tool privada que declara member_id, user_id, customer_id, account_id, patient_id ou subject faz o deploy falhar, porque quem preenche os parâmetros é o modelo, e ele pode ser convencido a colocar o de outra pessoa. reservar_aula também é privada e recebe só o id da aula.
workspace: academia-ritmo
agent:
name: Ritmo
instructions: |
Você ajuda os alunos da Academia Ritmo com aulas, horários e as
próprias reservas.
Horários: chame grade_de_aulas.
As reservas do aluno: chame minhas_reservas, e reservar_aula para
reservar. Se você não consegue ver as reservas porque ele não entrou
na conta, peça que ele entre no app primeiro. Nunca invente uma reserva.
tools: [grade_de_aulas, minhas_reservas, reservar_aula, remember, recall]
auth:
public_key: identity.pub
A grade_de_aulas é pública, então também responde no WhatsApp e no Instagram para quem não entrou na conta. Conectar seu backend aprofunda as tools e os tokens.
#Continuidade da conversa
- Reabrir. A conversa e o id do visitante ficam guardados em
UserDefaults, por workspace e por usuário logado. Chameresume()quando a view aparecer. - Entrar na conta mantém a conversa. Uma conversa anônima continua quando o usuário entra, então ele não precisa digitar de novo a pergunta que o levou a fazer login. Um token novo para o mesmo
submantém a mesma conversa. - Outro usuário é outra conversa. Um token com outro
subtem conversa e histórico próprios, então sair da conta no seu app também sai do chat, e um celular compartilhado nunca mostra a um aluno a conversa de outro. - Doze horas. As credenciais do chat valem 12 horas. Depois disso,
resume()não encontra nada e o próximosendcomeça uma conversa nova; o histórico fica no servidor. - Conversas anteriores.
vatio.conversations(visitorToken:)lista as conversas desse usuário neste aparelho, da mais recente para a mais antiga, echat.open(_:)troca para uma delas.chat.startOver()esquece a conversa guardada sem apagar nada no servidor. - Memória. A memória é do contato: com
remembererecallativadas, o agente vê as notas guardadas sobre um usuário logado, então uma conversa nova pode partir do que as anteriores já estabeleceram.
#Fotos e mensagens de voz
send(_:attachments:) aceita até quatro VatioUpload: imagens (JPEG, PNG, WebP, GIF, HEIC), PDFs, arquivos de texto ou áudio (M4A, MP3, OGG, WAV, AAC, FLAC), de até 8 MB cada. Com arquivos, o texto pode ir vazio:
try await chat.send("Essa é a pegada certa?", attachments: [
VatioUpload(data: jpegData, filename: "foto.jpg", contentType: "image/jpeg")
])
try await chat.send(attachments: [try VatioUpload(fileURL: gravacaoURL)]) // um .m4a do AVAudioRecorder
Uma mensagem de voz é transcrita em segundo plano: a mensagem mostra “Audio” no começo, o content vira a transcrição alguns segundos depois, e o agente responde depois disso. Quando isMediaLabel é true, desenhe o anexo em vez da palavra. Gravar exige NSMicrophoneUsageDescription no seu Info.plist.
#Teste contra o preview
O token decide com qual ambiente o app fala, então um build de desenvolvimento não deve levar o do live. Crie um para o preview:
vatio push
vatio tokens create --env preview --label ios-dev --origin ios-app://example.academiaritmo.app
vatio env preview mostra os dados desse ambiente. Escolha o token pela configuração do build:
#if DEBUG
let vatio = Vatio(workspace: "academia-ritmo", token: "vatpub_TOKEN_PREVIEW")
#else
let vatio = Vatio(workspace: "academia-ritmo", token: "vatpub_TOKEN_LIVE")
#endif
O vatio chat fala como você, o desenvolvedor, e não como um aluno, então as tools privadas se testam pelo próprio app. Entre na conta no simulador, pergunte pelas suas reservas e leia a conversa com vatio chat show CHAT_ID (o chat.conversationID tem o id). Ele mostra cada chamada de tool, o que o seu backend respondeu e se o token foi aceito: uma chave errada, um exp vencido ou um aud incorreto deixam o usuário anônimo em vez de dar erro. no_agent_deployed significa que ainda não há nenhum deploy no ambiente do token. Depois peça as reservas de outro aluno: sem um parâmetro de id não há o que o modelo preencher, e o seu endpoint só responde pelo sub do token. A documentação de deploys explica preview e live.
#Limites
Pelos limites do SDK, os mesmos da web:
- Mensagens: até 4.000 caracteres (
content_too_long), com até quatro arquivos de 8 MB cada. - O tipo do arquivo é lido dos bytes, não do nome; um tipo que o agente não consegue ler é recusado com
unsupported_file_type. - Limites de uso, por IP e workspace, por minuto: 10 conversas iniciadas, 30 mensagens enviadas, 60 leituras de configuração e 60 listagens de conversas. Acima disso, as requisições respondem HTTP 429 e o SDK lança
rate_limited: espere antes de tentar de novo, em vez de repetir na hora. - As credenciais valem 12 horas; uma vencida aparece como
subscription_rejected, e o próximosendcomeça uma conversa nova. - O estilo de resposta fica fixo quando a conversa começa:
.stream(o padrão: uma resposta completa, não token a token),.paced(mensagens curtas com indicadores de digitação) ou.instant. O app de exemplo pinta a resposta de forma progressiva na tela.
#O que o SDK não faz
- Não há notificações push. Nada no SDK se registra para recebê-las. As respostas chegam enquanto o app está conectado, e quando ele volta do segundo plano o SDK reconecta e busca o que perdeu.
- Não há chamadas de voz. Mensagens de voz, sim; uma conversa de voz ao vivo, não.
- O agente não envia arquivos. Ele lê imagens, áudio e PDFs, mas responde em texto: não consegue gerar nem enviar arquivos ou imagens.
- O agente não escreve primeiro. Ele responde ao que o usuário envia; nunca começa uma conversa.
#Publique
vatio publish
Lance o build com o token do live. As conversas do app chegam ao mesmo inbox que as do WhatsApp e da web, com cada chamada de tool. Um supervisor pode assumir uma conversa ou marcar uma resposta errada com uma flag, e cada flag vira um caso de eval a partir do qual o seu coding agent corrige: veja Melhorar seu agente. A versão web desta mesma configuração está em Um chat com IA para o seu site, e cada método, na documentação do SDK de iOS.
Perguntas frequentes
Como adiciono um agente de IA com chat ao meu app iOS?
Adicione o pacote Swift https://github.com/vatio-ai/ios, crie um token publicável cuja origem permitida seja ios-app:// mais o bundle id do seu app, e crie um VatioChat com ele. O SDK mantém a conversa como estado observável, então uma view SwiftUI que o observa é redesenhada conforme as respostas chegam. O agente é o mesmo vatio.yml que atende os seus outros canais.
É seguro colocar o token da Vatio dentro de um app iOS?
O token publicável vatpub_ foi feito para ir dentro do app. Ele corresponde a um workspace e a um ambiente e só funciona a partir das origens permitidas, mas não prova quem é o usuário. Nunca coloque em um app um token de desenvolvedor vat_ nem a sua chave privada de identidade.
O agente consegue ler os dados do usuário logado no meu app?
Sim. O seu backend assina um JWT RS256 de curta duração para o usuário, o app passa esse token ao VatioChat como visitor token, e as tools marcadas access: private rodam para esse usuário. O seu endpoint verifica o mesmo token e limita a consulta ao sub dele.
O SDK de iOS da Vatio vem com uma interface de chat?
Não. É um pacote Swift sem UI e sem dependências: ele entrega as mensagens, o indicador de digitação, o status da conexão, os arquivos e o momento de feedback como estado, e você desenha as telas. O README do pacote tem um exemplo completo em SwiftUI, e o repositório inclui um app de exemplo que dá para rodar.
O SDK de iOS tem notificações push ou chamadas de voz?
Não. As respostas chegam por um WebSocket enquanto o app está conectado, e o SDK busca o que perdeu quando reconecta. Os usuários podem mandar mensagens de voz, que são transcritas antes de o agente responder, mas não há chamadas de voz e o agente responde em texto.
Atualizado: