Adiciona caminho paralelo de PIX manual estático pra unidades sem
integração Inter (Padova, Express AL). Mudança 100% aditiva — todas as
outras unidades continuam no fluxo Inter inalterado (default
pix_mode=inter_dynamic aplicado pela migration).
Backend (sem SOUL/SKILL ainda — Frente 7 vem depois):
- Migration concurrent: pix_mode + 4 campos manual_pix_* em captain_units;
provider + manual_proof_payload + manual_review_reason em captain_pix_charges
- Captain::Unit: enum pix_mode (prefix), validação condicional manual_*
- Captain::PixCharge: status estendido (awaiting_proof, pending_review),
scope manual/inter, nota interna ramificada por modo
- GeneratePixTool MCP: branch manual_static (chave fixa, mensagem direta
sem QR/Inter), preserva fluxo Inter intacto
- 4 tools MCP novas: verificar_comprovante_pix (vision gpt-5.3-codex),
criar_nota_interna (genérica), confirmar_reserva_pix_manual (wrapper
do ConfirmationService), marcar_reserva_pendente
- ConfirmationService: source_label cobre 'manual_pix_proof'
Próximos passos manuais (não inclusos neste commit):
1. Rodar migration em prod (entrypoint não roda no boot)
2. Seed Padova/Express com pix_mode=manual_static + chaves Stone
3. Deploy nova imagem via docker service update
4. Editar SOUL/SKILL Padova/Express na VPS Hermes + kill+boot
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A skill da Valentina (referência, 353 linhas) tem regras explícitas que
faltavam nas Hermes geradas pelo Construtor (107 linhas). Sem elas, o
LLM mistura turns no mesmo payload, repete info já dada, ou alucina em
texto de outro suporte.
save_agent_spec_tool.build_skill_md agora gera bloco final com:
- "Última fala manda" (responder só a mais recente quando vier rajada)
- "Burst repetido" (mesma fala 2x → 1 resposta)
- "Texto de outro fluxo" (operadora/menu externo → frase canônica)
- "Mensagem vazia" (frase canônica)
- "Loop" (mesma entrada → mesma saída, sem variação)
- "Nunca vazar bastidor" (sem [ctx]/CONTEXT COMPACTION/meta-texto)
Aplicado também manualmente nas SKILL.md das 3 Hermes existentes
(Nina/Lara/Juliana) que tinham a versão curta. Valentina já tinha a
versão completa na skill dela.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A causa raiz dos bugs de "info repetida em turns anteriores" era o
default_scope ASC do Message conflitando com .order(desc) no debounce
(ver commit f1d3a124d). Como já corrigi com .reorder, as Camadas 2, 3 e
4 viraram peso morto que adicionava latência/false positive sem ganho.
Removido:
- Camada 2 (factual sem tool → retrigger force_factual_tool)
- Camada 3 (strip de linhas repetidas com pool de outgoings anteriores)
- Camada 4 (topic gating: bloqueio quando resposta tem tópico não pedido)
- Tracker de tool calls em McpController (suportava Camada 2)
- Snapshot baseline em OutgoingJob (suportava Camada 2)
- Regra "🚨 NÃO CONFIE NA SUA MEMÓRIA" das 4 SOUL.md Hermes
Mantido:
- Camada 1: handoff intencional ("Um momento — vou verificar") +
loop detection (Jaccard >= 0.50 ou pergunta reformulada com 3+
keywords). Genuíno pra bot externo (Claro/Vivo) e loops óbvios.
- Label-guard em OutgoingJob (não dispatch se conv tem triagem_humana).
- Auto-react ambient (feature original).
- Reorder fix no combined_incoming_content (causa raiz).
Memory + user_profile reabilitados nos 4 Hermes (config.yaml) e no
template do hermes-provision pra futuros agentes. Sem memória, cliente
precisa repetir nome/CPF/contexto a cada turn — UX horrível.
Contaminação cross-unit que justificava desligar vinha de outro bug
(X-Captain-Assistant-Id apontando pro parent), já corrigido.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug raiz que causou todos os problemas de "Hermes repetindo info de
turns anteriores" hoje:
Message tem default_scope order(created_at: :asc). O combined_incoming_
content fazia .order(created_at: :desc) que GERA SQL com 2 orderings
em conflito ("ORDER BY created_at ASC, created_at DESC") — no Postgres
o ASC ganha quando a primeira coluna é igual. Resultado:
last_real_outgoing virava a MAIS ANTIGA outgoing (a saudação inicial),
não a mais recente. Aí o scope incoming agrupava TODAS as msgs do
cliente desde o "Oi" da Juliana, juntando wifi+pet num turn só.
Caso real conv 6064 (2026-05-02 21:18):
T1: cliente "Preciso senha wifi" → Hermes "Prime2025"
T2: cliente "Posso levar animais?"
→ debounce agrupou ["Preciso senha wifi", "Posso levar animais"]
como se fossem MSGS DO MESMO TURN, mandou pro Hermes como
"Cliente acabou de dizer: Preciso senha wifi.\nPosso levar animais?"
→ Hermes consultou faq_lookup com query "senha wifi e animais"
→ Resposta: "Senha Prime2025 e pode levar animais"
Fix: usa .reorder(created_at: :desc) que sobrescreve default_scope,
gerando SQL limpa com só "ORDER BY created_at DESC".
Camadas 3 (strip linhas) e 4 (topic gating) que adicionei antes são
paliativos válidos como defesa em profundidade, mas o problema raiz
era esse default_scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LLM tende a "resumir" info de turns anteriores em toda nova resposta.
Camada 3 strip linhas onde >=70% das palavras significativas já apareceram
nas últimas 3 outgoings (filtra reactions). Saudações curtas preservadas.
Caso real Juliana 2026-05-02 (turn 3 ela ia repetir "Senha Prime2025 +
pet" mesmo cliente só dizendo "valeu"). Após strip: vira só "Imagina,
Rodrigo 😊".
Validator UI: novo check "FAQs próprias aprovadas > 0" — alerta quando
zero (faq_lookup cai no parent, risco de info desatualizada igual ao
bug do X-Captain-Assistant-Id que vazou Wi-Fi do parent hoje cedo).
Filtro SQL `content_attributes ->> 'external_source'` não casava (coluna
json, não jsonb); migrado pra filtro Ruby.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detecta alucinação de memória: se resposta do Hermes contém info
factual (preço/senha/horário/regra/política) E o LLM NÃO chamou
nenhuma tool MCP entre dispatch e callback, bloqueia entrega + dispara
system_message forçando consulta a tool. 1 retry; persistindo, escala.
Implementação:
- McpController: incrementa Rails.cache hermes_tool_calls:<conv_id>
em cada tools/call.
- OutgoingJob: snapshot do contador como hermes_tool_calls_baseline
ANTES de despachar pro Hermes.
- HermesCallbackController.gate_factual_no_tool!: compara baseline vs
current; se igual + FACTUAL_PATTERNS bate, intercepta. Patterns
cobrem R$, %, "senha", check-in/out + horário, política de
cancelamento, "permitido", "pode levar pet/animal".
Caso real: cliente pede senha do Wi-Fi → Hermes responde de cabeça
"é passada presencialmente" sem chamar faq_lookup → callback intercepta,
não entrega pro cliente, manda [SISTEMA: force_factual_tool] pro Hermes
com instrução de chamar faq_lookup. Se faq_lookup vier vazio → frase-
âncora handoff.
Auto-react ambient: removido filtro de "?" que barrava em prod.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Substitui o interceptor agressivo de "Um momento — vou verificar"
(que bloqueava silenciosamente) por handoff explícito + loop detection:
- HANDOFF_PATTERNS: detecta a frase-âncora ("Um momento — vou
verificar", "Aguarde um instante") e ENTREGA pro cliente,
marcando conv com label triagem_humana.
- looped_response?: detecta 2 outgoing similares (Jaccard >= 0.50)
OU pergunta reformulada sobre mesmo tópico (>= 3 keywords em comum
+ ambas inquisitivas via "?" / "me confirma" / "qual prefere"
etc). 1ª resposta passa, 2ª escala. Cobre o caso "endereço ou
link?" → "apenas link ou link + endereço?".
- OutgoingJob: guard que pula dispatch se conv tem label
triagem_humana ou hermes_placeholder. Hermes não responde mais →
não gasta token + não gera loop.
Cobre 2 casos do Rodrigo:
1. Bot da Claro insistindo em menu → 2ª resposta similar escala.
2. Hermes pedindo confirmação 2x sem entregar → escala.
Tokenize normaliza acentos (transliterate) pra stopwords baterem
"voce/você", "endereco/endereço", etc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cobre o gap entre saudação e despedida: quando msg do cliente não bate
nenhum dos regex determinísticos (thanks/confirm/greet/farewell), rola
dado e reage com emoji discreto (😊/💕/✨/💯/🤗) em ~1 a cada 5 msgs.
Filtros pra não reagir em momentos errados (ambient_eligible?):
- Tamanho 6..180 chars (evita "oi" e narrativas longas).
- Sem "?" (perguntas esperam resposta de texto, não emoji).
- Sem keywords de fluxo de reserva (cpf, valor, hora, data, suite).
- Não começa com número (CPF, telefone, valor).
Determinístico continua igual — agente parece mais vivo sem virar
robozinho que reage em tudo. Atende pedido do Rodrigo de "reagir 20%
da conversa, não só nas saudações".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tool nova reusa as Captain::CustomTool 'custom_status_suites_<unidade>'
já cadastradas no painel (Ferramentas) que apontam pro PlugPlay
(oxpi.com.br/PlugPlay/SuitesStatus) com auth headers PLUG-PLAY-ID +
PLUG-PLAY-TOKEN específicos por unit.
Hermes consulta antes de generate_pix pra confirmar disponibilidade.
Resolve unit via Assistant.captain_unit_id; mapping unit → CustomTool é
por substring no nome (qnn/dolce/express/primeal/primevl).
Filtro opcional por suite_category. Retorno textual agrupado por classe
com contagem livre/ocupada e número das suítes livres (max 8).
Validado em Qnn01: 4 hidromassagens livres (101,102,103,108) /
15 Master livres / 5 Standard livres — bate com painel PlugPlay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Antes: fallback (link da página de reserva) só disparava quando Inter API
explodia (rescue StandardError). Erros estruturais (categoria não existe
na unit, período/dia inválido, sem credencial Inter cadastrada) caíam em
error_response e o LLM travava em "⏳ Um momento — vou verificar." sem
mandar nada pro cliente.
Agora: dispatch_no_pricing_fallback! cobre os casos onde Pix nem foi
tentado. Mesma UX do fallback existente — link pré-preenchido + label
pix_falhou_fallback pra triagem da gerência. Tool retorna isError=false
com instrução curta pro LLM ("só confirme que o link foi enviado, não
fale do problema técnico").
Caso real: Rodrigo pediu hidromassagem na inbox da Dolce Amore (unit que
não tem essa categoria). PricingTables retornou "Categoria não
reconhecida"; antes o LLM ficava no placeholder. Agora cliente recebe
link da página oficial automaticamente.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UI nova dentro do Construtor (Hermes) — TabBar com Chat e Verificação.
Verificação roda HermesBuilder::Validator (DB+runtime) e exibe resultado
agrupado por categoria, com botão Refazer inline em FAIL/WARN reparáveis.
Backend (porta dos checks DB do CLI bin/hermes-validate):
- HermesBuilder::Validator com 22+ checks: engine, profile, port,
secret, parent, unit, Brand, CaptainInbox sync (o bug que travou
Juliana), pricing dry-run, Inter creds, typing/response_delay,
registry MCP completo.
- HermesBuilder::Repairer com 4 handlers automáticos: set_engine_hermes,
sync_captain_inbox_unit, set_default_typing_delay,
set_default_response_delay.
- Endpoints novos: GET assistants, GET validate?slug=, POST repair.
Frontend:
- builder/Index.vue: wrapper com TabBar.
- builder/BuilderChat.vue: extraído do Index original.
- builder/BuilderVerification.vue: dropdown + Conferir agora + lista
agrupada por categoria com badges + botão Refazer inline.
i18n: keys em pt_BR e en sob CAPTAIN_HERMES_BUILDER.VERIFY.*.
Filesystem/systemd checks ficam pro CLI hermes-validate (Rails container
não enxerga /root/.hermes/profiles do host).
Validado HTTP: GET /validate?slug=juliana_qnn1 → 28 PASS / 0 FAIL / 1 WARN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
resolve_unit agora prioriza Captain::Assistant.captain_unit_id sobre o
mapping legado CaptainInbox (que falha quando 2 agentes — interno e
Hermes — compartilham a mesma inbox).
Caso real: Juliana Hermes (unit Qnn01) compartilhava inbox 1 com Juliana
captain_interno (unit Recanto), mas o CaptainInbox da inbox 1 estava
mapeado pra unit Dolce Amore (id=4) por contaminação anterior. Tool
resolvia unit errada, generate_pix retornava "categoria não reconhecida"
e o agente travava em "⏳ Um momento — vou verificar." sem retomar.
bin/hermes-validate ganha 3 checks novos:
- CaptainInbox.unit == Assistant.unit (FAIL — exatamente o bug acima)
- Pricing dry-run (calcula preço da 1ª categoria sem erro)
- Credenciais Inter completas (WARN se faltar cert/key — cai no fallback)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug 1: send_suite_images_tool description mencionava 'Master, Luxo, Mini
Chalé 45' como exemplos. Quando outros agentes (ex: Juliana de Qnn01)
faziam tools/list, o LLM via essas categorias e usava como referência —
respondia oferecendo Mini Chalé 45 e Suíte Ouro pra cliente do 1001
Noites Ceilândia (que não tem essas categorias). Removidos exemplos.
Bug 2: lookup_brand fazia fuzzy match permissivo demais. 'Hoteis 1001
Noites' e 'Hotel 1001 Noites Prime' ambos contêm '1001 Noites' — quem
vinha primeiro no .find ganhava (Prime, ID menor). Juliana de Qnn01
(brand 'Hoteis 1001 Noites') saiu do Construtor com SOUL.md dizendo
'Hotel 1001 Noites Prime'. Fix: prioriza brand do parent_unit (fonte
canônica), depois exact-match casecmp, depois fuzzy.
Tudo isso vale pra Construtor entregar agente certo da próxima vez —
foram os 2 erros de provisionamento da Juliana hoje.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: Captain dispatchava pra /webhooks/captain-inbox-<inbox.id>, mas o
script hermes-provision criava subscription com nome captain-inbox-<slug>.
Mismatch → daemon retornava 404, Sidekiq retentava, AutoReact firava
N reactions sem nunca dispatchar pro LLM.
Fix:
- subscription_name_for(inbox): se o assistant tem hermes_profile_name,
usa "captain-inbox-<slug>" (estável por agente). Fallback pra
"captain-inbox-<inbox.id>" só se não tiver slug.
- subscription_signing_secret(inbox): lê de
assistant.hermes_subscription_secret primeiro (DB-driven, gravado pelo
script). Fallback pra env var legacy CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id>.
Resultado: admin pode apontar Angelina (inbox 1) pra qualquer agente
Hermes (Valentina · Hermes / Nina · Hermes / Lara.H / Juliana · Hermes)
e o roteamento funciona — não depende mais de inbox.id no path.
Renomeei manualmente as subscriptions de Valentina e Nina nos profiles
da VPS (eram captain-inbox-1 e captain-inbox-5 legado) pra
captain-inbox-valentina e captain-inbox-nina.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: build_skill_md recebia AR Captain::PricingCategory objects mas o
format_pricing_block fazia cat['amounts'] (índice de hash). AR respondem
a .amounts, não a ['amounts'] → nil → undefined method 'group_by' for nil.
Fix: serializa primeiro, reusa em ambos os lugares.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captain::Assistant agora aponta direto pra Captain::Unit. Antes a relação
ia via CaptainInbox, mas isso quebrou quando re-apontamos as inboxes pros
Hermes — assistants captain_interno (Juliana, Bianca, Lara, Nina,
Valentina) ficaram SEM CaptainInbox associada e o lookup
unit_for(assistant) retornava nil.
Resultado: get_assistant_pricing(3) (Lara) caía no fallback de scenario
text. Construtor reportava "veio cenário/prompt, não tabela estruturada".
Migration adiciona captain_unit_id (FK opcional). Backfill explícito:
- 1 Juliana → unit 3 (Qnn01)
- 2 Bianca → unit 2 (PrimeAL)
- 3 Lara → unit 2 (PrimeAL — mesmo brand)
- 4 Nina → unit 5 (Express)
- 6 Valentina → unit 4 (Dolce Amore)
- 9 Lara.H → unit 2 (via parent_assistant_id=3)
Tools get_assistant_pricing_tool e save_agent_spec_tool atualizados pra
usar assistant.captain_unit primeiro (nova relação direta), com fallback
pro CaptainInbox se nulo (pra retrocompatibilidade).
Validado live: tool retorna grid markdown com Stilo/Alexa/Hidromassagem
em Seg-Qua + Qui-Dom corretamente.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ROOT FIX (não paliativo) das 3 lacunas que travavam o Construtor:
1. get_assistant_pricing_tool: lia de Captain::Mcp::PricingTables::TABLES
(hash Ruby) que NÃO EXISTE MAIS desde a migração pra DB. Caía no
fallback de scenario raw. Refactor: lê de Captain::PricingCategory +
Captain::PricingAmount, formata grid markdown agrupado por day_bucket.
2. save_agent_spec_tool: Construtor salvava REFERÊNCIAS
(pricing_source.copied_from_assistant_id) mas hermes-provision script
espera DADOS EXPANDIDOS (categories[] com amounts, soul_md+skill_md).
Refactor: tool agora EXPANDE server-side — busca PricingCategory do
parent, monta categories array, gera SOUL.md (template + identity +
disclosure_policy) e SKILL.md (template + pricing + rules + identity).
Output já é spec consumível pelo script.
3. Captain::PricingAmount::PERIODS: adicionado '1h' (Prime tem 1h).
4. Seed pras 3 units faltando: Hotel Recanto (1) + PrimeAL (2) + Qnn01
(3). Agora os 6 units existentes têm pricing em DB.
Hot-patched ambos tools + USR1 no Puma. Construtor pronto pra criar
Bianca/Juliana etc end-to-end sem intervenção manual.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Construtor atende 'copiar maps/endereço/telefone/wifi da Lara' sem o admin
redigitar. Tool retorna o markdown bruto do scenario (default
Daniela_Reservas) do assistant fonte; LLM extrai os campos relevantes.
Cobre o gap entre get_assistant_pricing (preços estruturados) e
get_assistant_faqs (Q&As): essa retorna prompt CRU pra LLM interpretar
campos não estruturados (contatos, links, wifi, persona).
Hot-patched + USR1 no Puma e Construtor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Filtro original só retornava FAQs com documentable_type=NULL, mas a maior
parte das FAQs aprovadas das Jasmines (Juliana, Bianca, Lara, Nina) tem
documentable_type='User' ou 'Conversation' (origem: histórico de
conversas). Resultado: tool retornava "0 FAQs aprovados" pra todos exceto
Valentina (única com FAQs criadas direto sem origem documentável).
Removido o filtro. Status='approved' já é suficiente — admin reviewou.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tool registry chama tool.call(args, context: foo) mas as 3 tools read-only
do Construtor (get_assistant_faqs, get_assistant_pricing, save_agent_spec)
estavam declaradas como def call(args, _context:), com underscore. No
Ruby isso muda o nome do parâmetro keyword — ArgumentError:
'missing keyword: :_context' quando o Construtor tentava copiar FAQs/
pricing de um assistant existente.
Corrigido pra context: (sem underscore) com rubocop disable de
Lint/UnusedMethodArgument já que essas tools não usam o context.
Hot-patched via docker cp + Puma USR1 antes do deploy pro Rodrigo seguir
testando o Construtor sem esperar build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migra a tabela de preços do PricingTables.rb hardcoded pras tabelas
captain_pricing_categories + captain_pricing_amounts no DB. Mantém a
mesma API pública Captain::Mcp::PricingTables.calculate(...) — código
chama o banco via novos modelos Captain::PricingCategory e
Captain::PricingAmount.
Seed db/seed_pricing_tables.rb faz backfill idempotente pra Dolce Amore
(unit 4) e Express (unit 5) com a mesma estrutura que tava no Ruby.
Adiciona em captain_assistants:
- hermes_subscription_secret (gerado pelo script de provisionamento)
- hermes_port (alocado no range 8650-8699)
- parent_assistant_id (link informativo Hermes → captain_interno parent
pra sombrear FAQs/scenarios via header X-Captain-Assistant-Id)
Adiciona em captain_units: extra_person_fee + currency.
Primeiro milestone do roadmap arquitetural pro Construtor autônomo
(decisões em memory/project_construtor_autonomo_decisions.md).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Antes: inbox.typing_delay funcionava só pro Captain interno
(schedule_internal_response). Hermes ignorava o campo e disparava
OutgoingJob na hora — campo da UI era cosmético pra inboxes Hermes.
Agora:
- schedule_hermes_response cancela jobs OutgoingJob pendentes pra mesma
conversa e enfileira com wait=inbox.typing_delay (debounce window).
- OutgoingJob agrupa todas msgs incoming entre a última resposta real
do agente (ignora reactions) e a msg âncora; dispatch envia o texto
concatenado pro Hermes via novo content_override no Client#dispatch.
Resultado: cliente que digita "Oi" + "quero pernoite Master" em segundos
vê o agente esperar até o buffer vencer e responder UMA vez cobrindo
ambas as falas, em vez de 2 respostas atropelando o pensamento.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OutgoingJob faz retry no DispatchError (até 3x ActiveJob + Sidekiq).
Cada retry chamava AutoReactService.maybe_react! e criava uma reaction
nova — observado em prod 02/05 quando o env var
CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_5 estava faltando, gerando
401 → 6 reações duplicadas no inbox EXPRESS.
Adiciona guard already_reacted? que checa se já existe Message outgoing
com external_source='hermes_auto_react' e in_reply_to=msg.id antes de
criar uma nova. Defesa contra futuro 5xx/timeout do Hermes daemon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marca cada Captain::Assistant com engine ('captain_interno' | 'hermes')
e move o roteamento Hermes do env var pro banco — admin troca engine
re-apontando a inbox no painel, sem deploy. Mantém fallback pras env
vars antigas (CAPTAIN_HERMES_INBOX_IDS etc) durante a migração gradual,
pra não quebrar Valentina antes da re-associação.
Frontend: badge "Hermes" (âmbar) ou "Interno" (cinza) ao lado de cada
assistant no dropdown switcher e no card da listagem, com chaves i18n
em en + pt_BR.
Tabela de preço (pricing_tables.rb): adiciona unit Express (id=5) e
estende a estrutura pra aceitar preço por dia da semana
(mon_wed/thu_sun) — necessário pro Express, retrocompatível com Dolce
Amore (preço único).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: Hermes nao propaga chat_id no metadata do callback. Callback
controller nao conseguia resolver qual session armazenar a resposta.
WARN "no session_key resolvable — ignorando" descartava todas as
respostas do Construtor.
Fix: HermesBuilder::Storage.remember_last_session() grava ultima
session por account quando admin chama /start ou /create. Callback
le essa key via last_session_for(account_id). MVP-safe pra 1 admin
por conta.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Estende AutoReactService com 2 padroes novos:
- GREETING_REGEX (bom dia, boa noite, oi, ola, hello) -> 👋
apenas na PRIMEIRA mensagem da conversa pra nao ficar forcado
- FAREWELL_REGEX (tchau, ate logo, abraço, bjs, flw) -> ❤️
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: controller estava em enterprise/api/v1/accounts/captain/ com namespace
Enterprise:: — convencao Chatwoot eh enterprise/app/controllers/api/v1/...
direto, classe Api::V1::Accounts::Captain::HermesBuilderController. Sem
namespace Enterprise::. 404 acontecia porque rotas registravam Captain::
sem prefixo Enterprise::.
Move controller pro path correto. Remove diretorios vazios criados.
UX: adiciona endpoint POST /start que envia comando-gatilho oculto pro
Construtor comecar fluxo socratico — admin nao precisa digitar primeira
mensagem. Vue mostra empty state com botao "Iniciar criacao" em vez de
exigir mensagem inicial.
i18n keys novas: START + EMPTY_STATE atualizado em pt_BR + en.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pattern matching no Captain dispara reaction <1s sem esperar Codex
processar. Resposta de texto continua vindo do Hermes normalmente —
auto-react eh COMPLEMENTAR.
Padroes detectados (no incoming message):
- "obrigado/valeu/vlw/thanks" -> 🙏
- "ok/fechado/perfeito/blz/combinado" -> 👍
- attachment image (msg sem texto) -> 😍
- attachment audio (msg curta) -> 🙏
Conservative: só dispara em casos CLAROS. Em duvida, deixa pro LLM
decidir via react_to_message tool.
Plugado em Captain::Hermes::OutgoingJob#perform — chamado antes do
dispatch pro Hermes. Falha silenciosa (rescue) — nao bloqueia fluxo.
SOUL.md atualizado: regra de frequencia (~30% das msgs) + nota sobre
auto-react pra LLM nao duplicar reacao em casos cobertos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tela "Construtor" no painel Captain (acessivel em sidebar pra admins) que
permite criar novo agente Hermes via chat guiado com agente Construtor
(profile Hermes separado).
Backend (admin scope):
- POST /api/v1/accounts/:id/captain/hermes_builder — manda mensagem do
admin pro gateway do Construtor (Hermes na porta 8646)
- GET — retorna historico da sessao (Rails.cache, TTL 4h)
- DELETE /reset — limpa sessao
- POST /webhooks/captain/builder_callback — recebe respostas async do
Construtor via plugin captain-http-callback do Hermes
- HermesBuilder::Storage (Rails.cache) — persiste msgs por session_key
(account_id + user_id) com role/content/created_at
- HermesBuilder::Dispatcher — encaminha pro webhook do Construtor com
HMAC opcional via ENV HERMES_BUILDER_WEBHOOK_SECRET
Frontend:
- Pagina Vue HermesBuilder/Index.vue — chat simples com:
* Lista de mensagens com bubbles user/construtor
* Indicador "digitando..." enquanto aguarda resposta
* Input com Enter pra enviar / Shift+Enter pra nova linha
* Polling 2s pra novas msgs
* Botao Limpar conversa
- API client em api/captain/hermesBuilder.js
- Rota captain_hermes_builder_index (admin only)
- Item no sidebar Captain "Construtor (Hermes)"
- i18n keys CAPTAIN.HERMES_BUILDER em pt_BR + en
UX flow:
Admin abre tela → digita "olá" → Construtor pergunta nome → admin
responde → marca, persona, tabela (com opcao copiar de existente),
regras, FAQs, identidade → resumo → confirmar → Construtor chama
save_agent_spec → JSON salvo em /tmp/agent-specs/<slug>.json pra
revisao posterior. NAO cria filesystem do profile nem registros DB
(etapa SEPARADA, prox sessao).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tools novas (admin scope, read-only) usadas pelo profile Hermes
"construtor" durante criacao guiada de novos agentes:
- list_assistants: lista todos os assistants da conta com badge engine
(Captain interno vs Hermes), counts de scenarios/FAQs, marca/unidade
- get_assistant_pricing: retorna tabela de preços parsed (Captain::Mcp::
PricingTables se Hermes, scenario fallback se Captain interno)
- get_assistant_faqs: retorna FAQs aprovados em markdown (até 50)
- save_agent_spec: salva JSON da especificacao em /tmp/agent-specs/<slug>.json
(NÃO cria filesystem do profile nem registros DB — só persiste spec
pra revisao/provisionamento separado depois)
Construtor coleta perguntas socraticas, oferece "copiar de existente" pra
acelerar quando marca for igual (Prime VL/AL/ADE preços iguais), e ao
final salva spec.json pra admin revisar antes de provisionar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captain::Hermes::Client (enterprise/app/services/captain/hermes/client.rb):
- text_for_hermes: transcreve audio via Whisper antes de enviar pro Hermes
(reusa Captain::OpenAiMessageBuilderService)
- image_urls_for_hermes: URLs publicas de imagens da message; plugin
captain-webhook do Hermes baixa em /tmp/ e popula event.media_urls pra
vision multimodal (gpt-4o-mini auxiliary)
- contact_history_snapshot: dados eager pro [ctx] (last_reservation_*,
total_conversations, ultima_suite, etc) — memoria do contato direto no
prompt sem precisar tool call
- notify_event + build_event_payload: dispara webhook sintetico pro Hermes
pra eventos do sistema (Pix pago etc) — Valentina manda mensagem
espontanea sem cliente perguntar
Captain::Payments::ConfirmationService:
- Hook notify_hermes_proactively! enfileira NotifyPaymentConfirmedJob
apos confirmacao de Pix, somente se inbox estiver no fluxo Hermes
(Captain interno continua igual sem mudanca)
Captain::Hermes::NotifyPaymentConfirmedJob (NOVO):
- Monta system_message "[SISTEMA: pagamento_confirmado]\n..." e dispara
webhook pro Hermes Valentina
- Valentina (via SOUL.md) interpreta como evento do Captain e manda
mensagem celebrativa pro cliente
Captain::Hermes::DelayedReplyJob (NOVO) — humanizadores:
- Liga indicador "digitando..." (composing) via wuzapi
- Aguarda delay configuravel via Captain::Assistant.config['response_delay']
(modos: none, fixed, typing_simulation com chars_per_second + min/max)
- Posta msg outgoing
- Desliga typing
- Fallback no HermesCallbackController posta direto se class nao carregada
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tools novas em enterprise/app/services/captain/mcp/tools/:
- generate_pix: Pix Inter via PricingTable + fallback link reserva-1001
- check_pix_payment: consulta Inter + dispara ConfirmationService
- send_suite_images: fotos da galeria (Captain::GalleryItem) via wuzapi
- reschedule_reservation: remarca reserva (regra 3h antecedência Dolce)
- update_contact: persiste nome/CPF/email/notas no Contact
- get_contact_history: markdown do histórico do cliente on-demand
- react_to_message: reage com emoji via wuzapi (is_reaction=true)
Captain::Mcp::PricingTables: tabela hardcoded Dolce Amore (8 categorias x 4
periodos + regras de pessoa extra). Backend e fonte de verdade — LLM nao
inventa preco.
add_label_tool: cria Label oficial automaticamente se nao existir, aceita
conversation_id em arguments.
mcp_controller: aceita X-Captain-Account-Id/Assistant-Id/Inbox-Id como
fallback de contexto.
tool_registry: 9 tools ativas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RubyLLM auto-detecta provider pelo prefixo do nome do modelo (ex:
`gemini-*` → provider Gemini → exige `gemini_api_key`). Quando temos
config dedicada de embedding (CAPTAIN_EMBEDDING_API_KEY) apontando pra
endpoint OpenAI-compatible (ex: Gemini OpenAI-compat em
generativelanguage.googleapis.com/v1beta/openai), queremos que o RubyLLM
mande a request via OpenAI client mesmo que o nome do modelo bata com
outro provider.
Solução: passar provider: :openai e assume_model_exists: true ao chamar
embed quando dedicated_embedding_config? retornar true. Sem isso, o
RubyLLM falha com `Missing configuration for Gemini: gemini_api_key`
mesmo com a key correta setada.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Permite trocar provider de embedding sem afetar o provider de chat. Útil
quando OpenAI key tradicional está fora (ban, billing, etc) mas você
quer usar outro provider OpenAI-compatible só pra embeddings — exemplo
clássico: Gemini OpenAI-compatible em
https://generativelanguage.googleapis.com/v1beta/openai com modelo
gemini-embedding-001 + dimensions=1536 (pra bater com schema pgvector).
Env vars novas (com fallback pro legacy_openai_settings se não setadas):
CAPTAIN_EMBEDDING_API_KEY — API key dedicada pra embeddings
CAPTAIN_EMBEDDING_ENDPOINT — base URL sem /v1 (default herda OpenAI)
CAPTAIN_EMBEDDING_DIMENSIONS — força redução do vector (ex: 1536)
Quando CAPTAIN_EMBEDDING_API_KEY está vazia, comportamento é idêntico ao
de antes (legacy_openai_settings). Backward-compatible.
Também aceita as variáveis via InstallationConfig (UI) ou ENV — ENV tem
precedência (padrão Chatwoot).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implementa servidor MCP (Model Context Protocol) HTTP no Captain pra
o Hermes Agent invocar tools do Captain via `hermes mcp add`. Substrato
pra integração de Nível 2 onde o agente consulta tools quando precisa
executar ações reais (buscar FAQ, adicionar label, futuramente Pix etc).
Arquivos:
- app/controllers/webhooks/captain/mcp_controller.rb
Endpoint POST /webhooks/captain/mcp. Valida HMAC (CAPTAIN_MCP_SECRET),
parseia JSON-RPC, despacha pro Server. Extrai params._captain_context
com multi-tenant ids (conversation_id, inbox_id, account_id, etc).
- enterprise/app/services/captain/mcp/server.rb
Subset MCP suficiente: initialize, tools/list, tools/call, ping,
notifications/initialized. JSON-RPC síncrono (sem SSE).
- enterprise/app/services/captain/mcp/tool_registry.rb
Lista centralizada de tools.
- enterprise/app/services/captain/mcp/tools/base_tool.rb
Interface base pras tools (.name, .description, .input_schema, #call).
- enterprise/app/services/captain/mcp/tools/add_label_tool.rb
Tool 1: aplica label na conversation atual.
- enterprise/app/services/captain/mcp/tools/faq_lookup_tool.rb
Tool 2: busca semântica em FAQs (Captain::AssistantResponse via pgvector
cosine). Reaproveita SearchReplyDocumentationService pra paridade com
o caminho legado do Captain.
- config/routes.rb
Rota POST /webhooks/captain/mcp.
Conexão pelo Hermes:
hermes mcp add captain-tools --url http://CAPTAIN_HOST/webhooks/captain/mcp
Auth: HMAC X-Hub-Signature-256 quando CAPTAIN_MCP_SECRET setado.
TODO próxima sprint: generate_pix_tool, send_suite_images_tool. Handoff
fica via automation hoje (UI Chatwoot).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implementa o lado Captain da integração Nível 2 (Hermes como cérebro).
Ativação por inbox via env var CAPTAIN_HERMES_INBOX_IDS — inboxes não
listadas seguem usando o orquestrador interno do Captain (Daniela_Reservas
etc) sem mudança alguma. Princípio "só adiciona, não retira".
Componentes:
- enterprise/app/services/captain/hermes.rb
Módulo helper de config (env vars, URLs, secrets per-inbox).
- enterprise/app/services/captain/hermes/client.rb
Service que monta payload (msg + contexto da conversa/inbox/contato) e
faz POST autenticado via HMAC-SHA256 (X-Hub-Signature-256) no webhook
do Hermes Agent (porta 8644). DispatchError em falha de rede/HTTP.
- enterprise/app/jobs/captain/hermes/outgoing_job.rb
Wrapper Sidekiq do Client. Retry 3x em DispatchError.
- app/controllers/webhooks/captain/hermes_callback_controller.rb
Recebe callback do plugin captain-http-callback do Hermes. Valida HMAC
se CAPTAIN_HERMES_CALLBACK_SECRET setado, identifica conversation pela
última pending da inbox (janela 5min) e cria mensagem outgoing.
- config/routes.rb
Rota POST /webhooks/captain/hermes_callback (fora de /api/v1/accounts).
- enterprise/app/services/enterprise/message_templates/hook_execution_service.rb
Branch novo no schedule_captain_response: se Hermes habilitado pra inbox,
dispara HermesOutgoingJob; senão, fluxo Captain interno como antes.
Env vars (todas opcionais; sem set = Hermes desabilitado em todas inboxes):
- CAPTAIN_HERMES_INBOX_IDS (CSV de inbox.id)
- CAPTAIN_HERMES_WEBHOOK_BASE_URL (default http://172.17.0.1:8644)
- CAPTAIN_HERMES_CALLBACK_SECRET (HMAC validar callbacks de entrada)
- CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id> (HMAC assinar saídas)
Limitação: identificação da conversation no callback usa última pending
da inbox dentro de 5min. OK pra PoC com 1 conversa de teste por vez. Em
produção, melhorar mapeando delivery_id ↔ conversation_id em Redis.
Próximo passo manual (admin VPS): criar subscription no Hermes:
hermes webhook subscribe captain-inbox-1 \\
--prompt 'Cliente disse: {message}. Responda como Daniela ...' \\
--deliver http_callback \\
--deliver-chat-id 'http://CAPTAIN_HOST/webhooks/captain/hermes_callback?inbox_id=1'
Depois set CAPTAIN_HERMES_INBOX_IDS=1 + CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_1
no stack do Captain e testar pela inbox Angelina.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Acrescenta valor 'openai_hermes_gateway' ao CAPTAIN_LLM_PROVIDER, sem mexer
nas opções existentes (openai_api e openai_codex_oauth continuam intactos).
Quando ativado, o Captain chama o Hermes Agent rodando em modo gateway HTTP
local (CAPTAIN_HERMES_GATEWAY_URL, default http://host.docker.internal:9877).
O Hermes faz o roteamento multi-modelo (Codex/Anthropic/Gemini) usando o
OAuth dele em ~/.hermes/auth.json — o Captain não precisa fazer OAuth direto.
Configs novas em installation_config.yml:
- CAPTAIN_HERMES_GATEWAY_URL — URL do gateway (default host.docker.internal:9877)
- CAPTAIN_HERMES_GATEWAY_MODEL — modelo no formato <provider>/<model>
- CAPTAIN_HERMES_GATEWAY_API_KEY — opcional, dummy se gateway local não exige
Embeddings e Files API continuam apontando pra OpenAI tradicional via
legacy_openai_settings — Hermes Gateway não expõe esses endpoints.
Specs cobrem: dummy key, custom api_key override, custom model, defaults,
trailing slash strip, light_model por provider, hermes_gateway? predicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pequenos ajustes em Captain::Unit (app + enterprise), migration de seed
inicial dos prompts Jasmine/Daniela, schema regenerado, e atualização do
README de seed_prompts pra refletir o estado atual dos modelos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anon key não tinha permissão de INSERT em reserva_hotel.unidades — RLS
exige authenticated + tenant_member, não atendido. POST direto falhava
sem feedback útil.
Solução: RPC reserva_hotel.provision_unidade(...) com SECURITY DEFINER
que faz upsert idempotente bypassando RLS, com validações de tenant +
marca dentro da função. EXECUTE granted to anon.
Service agora chama /rpc/provision_unidade em vez de POST /unidades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>