#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 deVocê constrói
A conversa como estado observável: messages, isTyping, statusCada 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 confirmadaDevolver o texto ao campo quando send lança um erro
Reconectar com backoff, fazer polling enquanto isso, buscar o que perdeu e deduplicar por idExibir content, que é Markdown escrito pelo agente: trate como não confiável
Guardar a conversa em UserDefaults, por workspace e por usuário logadoO 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 estadoO 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

#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:

#O que o SDK não faz

#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: