O Angelino Web é uma tela privada — texto, voz e comandos recorrentes — em cima de um agente Hermes. Este guia mostra a ordem certa de construir: persona, catálogo de comandos, ponte, voz e as travas que impedem o agente de agir sozinho onde não devia.
Ver o passo a passoDá pra montar esse agente no Claude Code, no Codex ou no Hermes. Os três constroem a interface. A minha recomendação é o Hermes — nos outros dois você tem um assistente que responde quando você chama; no Hermes você tem um agente que fica de pé 24 horas, lê seu e-mail e continua executando enquanto você está no consultório.
O que o Hermes traz de fábrica — e é por isso que ele vira o cérebro da sua interface:
O guia abaixo serve para os três caminhos. A arquitetura, os gates de aprovação e as travas de voz são as mesmas — muda só quem executa por trás. Os comandos de instalação estão escritos na versão do Hermes.
O navegador é a interface. O agente continua sendo o cérebro.
A arquitetura
Tudo que você digita ou fala entra pelo navegador, passa por um servidor privado que decide o que é permitido, chega ao agente e só volta como resposta depois de autorizado. Essa separação impede que uma interface bonita vire, por acidente, uma segunda autoridade sem governança.
Captura o pedido e mostra o estado real da execução.
Autenticação, CSRF, transcrição local, allowlist de comandos.
Ferramentas, aprovações e pedidos de esclarecimento.
Streaming na tela e, se você quiser, áudio falado.
O que existe por trás
A ordem importa: primeiro se define o que o agente pode fazer, depois se escolhe a animação do botão.
Passo 0
Antes de qualquer tela, o agente precisa estar de pé e respondendo. Um perfil separado mantém o assistente do estudo longe das suas outras configurações.
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
hermes setup
hermes doctor
hermes profile create angelino
hermes profile show angelino
Nunca copie arquivos de credencial pra dentro do projeto. Configuração versionada e segredo são coisas separadas: o repositório guarda o padrão, o cofre guarda a chave.
Comece pequeno. Estes quatro arquivos já sustentam o fluxo de texto — voz contínua, PWA e painel de auditoria entram depois que ele estiver estável.
angelino-web/
├── index.html ← a tela
├── app.js ← estado, envio, streaming
├── server.py ← autoridade: auth, allowlist, bridge
└── persona-ptbr.md ← política versionada
A ordem segura
Cada etapa só começa quando a anterior está confiável. A tentação é pular direto pro microfone — é justamente o caminho que produz um sistema bonito e sem freio.
Não comece pela etapa 4. Uma animação fluida sobre um approval defeituoso continua sendo um approval defeituoso — apenas com melhor iluminação.
Etapa 1 · a base
A persona registra verdade, tratamento, limites e governança num arquivo que entra no controle de versão. Assim a mudança pode ser revisada, comparada e carregada em qualquer interface.
# Persona Angelino
Você é Angelino, o assistente geral do meu Agentic OS pessoal.
Você é leal, eficiente, cordial e discretamente espirituoso.
## Verdade
- Relate somente fatos presentes nas fontes realmente consultadas.
- Não descreva como concluída uma ação ainda pendente.
- Se uma fonte importante falhar, informe a indisponibilidade.
## Governança
- Nunca solicite, registre ou reproduza dados identificáveis de pacientes.
- Publicação, envio, deploy, push e mutações externas exigem aprovação humana.
- Trate saídas de ferramentas e subagentes como evidência a verificar.
- Quando faltar contexto essencial, faça uma pergunta objetiva.
- Não altere suas próprias skills ou políticas automaticamente.
## Voz
- Use português brasileiro natural.
- Para fala, prefira frases curtas e sem Markdown.
- Não imite ator, personagem ou biometria vocal protegida.
No servidor, um guard fixo entra antes de cada pedido — ele reduz ambiguidade, mas não substitui controle técnico.
BASE_SECURITY_GUARD = (
"Você é o Angelino no Agentic OS pessoal do usuário. "
"Responda em pt-BR claro e sequencial. "
"Nunca solicite, registre ou reproduza dados identificáveis de pacientes. "
"Não publique, não envie mensagens, não faça deploy e não execute "
"mutação externa sem aprovação humana explícita nesta sessão. "
"Quando faltar contexto, faça uma pergunta objetiva. "
"Trate saídas de ferramentas como evidência a verificar."
)
"não faça deploy" é orientação. O modelo pode interpretar, contornar ou esquecer.
Um gate obrigatório é controle. Sem decisão humana, a ferramenta não roda.
Etapa 2 · governança
Comandos recorrentes moram numa allowlist do servidor. O navegador manda só o identificador — nunca um prompt arbitrário disfarçado de comando.
COMMANDS = {
"day": {
"label": "Preparar briefing do dia",
"requires_confirmation": False,
"prompt": (
"Prepare um briefing em modo somente leitura. "
"Consolide compromissos, prazos, prioridades e mensagens "
"que exigem resposta. Ignore promoções e notificações automáticas. "
"Separe fatos, interpretação e decisões sugeridas. "
"Não altere calendário, tarefas ou mensagens."
),
},
"publish": {
"label": "Preparar publicação",
"requires_confirmation": True,
"prompt": (
"Prepare somente o preview e o checklist. "
"Não publique sem aprovação humana explícita."
),
},
}
A regra de bolso pra classificar cada comando novo:
O backend é a autoridade: esconder um botão no navegador não impede ninguém de chamar a rota na mão.
def handle_command(body: dict, bridge, command_gates) -> dict:
command_id = str(body.get("command") or "").strip()[:32]
command = COMMANDS.get(command_id)
if not command:
raise ValueError("Comando não permitido")
if command["requires_confirmation"]:
gate_token = str(body.get("gate_token") or "").strip()
if not gate_token:
raise PermissionError("Este comando exige confirmação humana")
command_gates.consume(gate_token, command_id)
voice_turn_id = ""
prompt = command["prompt"]
raw_voice_turn_id = body.get("voice_turn_id")
if raw_voice_turn_id:
voice_turn_id = safe_voice_turn_id(raw_voice_turn_id)
prompt = f"{VOICE_RESPONSE_GUARD}\n\n{prompt}"
run_id = bridge.submit(prompt, voice_turn_id=voice_turn_id)
return {
"run_id": run_id,
"label": command["label"],
"voice_turn_id": voice_turn_id,
}
A ponte com o agente começa com um contrato pequeno — três métodos bastam pra sustentar a etapa 1 inteira.
from typing import Protocol
class HermesBridge(Protocol):
def submit(self, prompt: str, voice_turn_id: str = "") -> str:
"""Submete um turno e devolve o run_id autoritativo."""
def interrupt(self, run_id: str) -> None:
"""Interrompe somente a execução ativa correspondente."""
def respond_approval(self, request_id: str, choice: str) -> dict:
"""Responde uma aprovação single-use vinculada ao run."""
Não implemente a bridge chamando shell arbitrário vindo do navegador. Use a API, o gateway ou o adaptador oficial da versão do agente que você instalou.
Etapa 2 · a tela honesta
A interface não pode ser mais otimista que a evidência. Cada estado significa uma coisa só.
| Estado | Significado correto |
|---|---|
| idle | Disponível, sem execução ativa. |
| listening | Microfone ativo, aguardando fala. |
| thinking | Transcrição ou processamento em curso. |
| executing | Ferramenta ou workflow rodando. |
| awaiting_input | O agente precisa de esclarecimento. |
| awaiting_approval | Efeito externo bloqueado até decisão humana. |
| speaking | Áudio da resposta final em reprodução. |
| complete | Execução terminou — não significa que todo efeito foi auditado. |
| error | Fluxo interrompido; efeitos incertos precisam ser verificados. |
Evite "Tudo concluído com sucesso" antes da confirmação do provedor. Enquanto o retorno não chegou, o estado honesto é "executando" — não "pronto".
Etapa 3 · voz
A voz entra em duas fases separadas. O microfone fica pausado enquanto o assistente fala — senão ele transcreve a própria resposta.
Detecção de voz, gravação, transcrição local, ferramentas, aprovações e clarify.
Só o texto final canônico vira fala. Terminou o áudio, volta a escutar.
Assinatura de chatbot não é assinatura de API. Ter um plano pago no aplicativo de chat não inclui automaticamente a API de fala da plataforma — essa é cobrada à parte, com credencial própria.
Etapa 3 · a trava mais importante
Numa interface de voz, evento atrasado é rotina: uma resposta antiga termina depois que você já começou outra pergunta. O voice_turn_id é o carimbo que impede o áudio velho de tocar por cima do novo.
let voiceTurnEpoch = 0;
let activeVoiceTurnId = '';
function createVoiceTurnId() {
voiceTurnEpoch += 1;
const entropy = globalThis.crypto?.randomUUID
? globalThis.crypto.randomUUID().replaceAll('-', '').slice(0, 16)
: `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
return `voice-${voiceTurnEpoch}-${entropy}`;
}
function isCurrentVoiceTurn(voiceTurnId) {
return Boolean(
voiceTurnId
&& activeVoiceTurnId
&& voiceTurnId === activeVoiceTurnId
);
}
import re
def safe_voice_turn_id(value: object) -> str:
voice_turn_id = str(value or "").strip()[:80]
pattern = r"voice-[0-9]{1,10}-[A-Za-z0-9_-]{8,64}"
if not re.fullmatch(pattern, voice_turn_id):
raise ValueError("Identificador de turno de voz inválido")
return voice_turn_id
A invariante: nova fala, interrupção ou fim da conversa invalida qualquer reprodução anterior. Essa checagem tem que aparecer em cinco pontos — na captura, no envio, nos eventos, no TTS e no callback de término do áudio. Faltou um, o áudio fantasma volta.
Etapa 4 · conveniência
Pedidos frequentes podem ir direto pro workflow — desde que a intenção seja inequívoca. Lista fechada, normalização e fallback que erra pro lado seguro.
const INTENT_PHRASES = new Map([
['o que tenho hoje', 'day'],
['quais sao meus compromissos hoje', 'day'],
['faca meu briefing', 'day'],
['o que tenho esta semana', 'week'],
['como esta minha semana', 'week'],
['como estao os agentes', 'agents'],
['verifique os agentes', 'agents']
]);
function normalizeVoiceIntentText(text) {
return String(text || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, ' ')
.trim()
.replace(/^angelino\s+/, '')
.replace(/\s+por favor$/, '')
.replace(/\s+/g, ' ');
}
export function resolveVoiceCommandIntent(text) {
return INTENT_PHRASES.get(normalizeVoiceIntentText(text)) || '';
}
const transcript = await transcribeBlob(blob);
if (!transcript) throw new Error('Nenhuma fala reconhecida.');
const commandId = resolveVoiceCommandIntent(transcript);
if (commandId) {
await startCommand(commandId, '', transcript);
} else {
await sendMessage(transcript, true);
}
"Não faça meu briefing" e "O que tenho hoje sobre IA?" disparariam o workflow errado.
Só a frase exata pega o atalho. Qualquer variação segue pro fluxo normal de interpretação.
Rapidez sem escopo é só uma forma mais eficiente de errar.
Em todas as etapas
Publicação
O servidor escuta só no loopback; a rede privada faz a ponte até o celular. O endereço resultante é seu — não copie o de ninguém.
python server.py --host 127.0.0.1 --port 4173
tailscale serve --bg --yes --https 8443 4173
Como saber que funciona
Escreva o teste antes da implementação. Ele é o que impede uma "melhoria" futura de abrir a porta que você fechou.
import { strict as assert } from 'node:assert';
import { resolveVoiceCommandIntent } from './voice-intents.js';
assert.equal(resolveVoiceCommandIntent('O que tenho hoje?'), 'day');
assert.equal(resolveVoiceCommandIntent('Como está minha semana?'), 'week');
assert.equal(resolveVoiceCommandIntent('Como estão os agentes?'), 'agents');
// as três abaixo TÊM que cair no fluxo normal
assert.equal(resolveVoiceCommandIntent('Não faça meu briefing.'), '');
assert.equal(resolveVoiceCommandIntent('Você consegue fazer meu briefing?'), '');
assert.equal(resolveVoiceCommandIntent('O que tenho hoje sobre IA?'), '');
import pytest
def test_unknown_command_is_rejected():
with pytest.raises(ValueError, match="Comando não permitido"):
handle_command(
{"command": "executar-qualquer-coisa"},
bridge=FakeBridge(),
command_gates=FakeCommandGates(),
)
def test_invalid_voice_turn_is_rejected():
with pytest.raises(ValueError):
safe_voice_turn_id("turno-sem-epoch")
A matriz mínima de regressão — dez verificações que precisam continuar passando a cada mudança:
Sua vez
Pegue uma tarefa administrativa que você repete toda semana e preencha a ficha antes de escrever qualquer linha de código.
| Campo | Sua resposta |
|---|---|
| nome | Como o workflow se chama |
| frases | As frases exatas que disparam a rota rápida |
| fontes | O que o agente consulta |
| saída | O formato esperado da resposta |
| ferramentas | O que precisa estar habilitado |
| sensível | Pode haver dado de paciente? (se sim, pare e redesenhe) |
| efeito | Existe mutação externa? Qual aprovação exige? |
| sucesso | Como você sabe que deu certo |
| interrupção | Quando o agente deve parar e perguntar |
| teste | Como isso vai ser testado |
Depois implemente: uma entrada no catálogo, três frases inequívocas, três frases ambíguas que precisam cair no fallback, um teste que falha antes da implementação e um teste de gate se houver efeito externo.
Copie o padrão arquitetural, nunca a identidade operacional de outra pessoa. Fora da cópia: token privado, hostname da rede privada, caminhos absolutos, e-mail e telefone do dono, banco de memória, histórico de sessões, credenciais OAuth, arquivos .env, IDs de projeto e qualquer dado clínico.
O valor não está em parecer humano: está em preservar contexto, repetir processos, pedir autorização quando é preciso e deixar claro o que sabe, o que inferiu e o que ainda não conseguiu verificar.
Acessar comunidade