Commit Graph

15 Commits

Author SHA1 Message Date
Rodribm10
b561aa8451 revert(hermes): remove camadas 2/3/4 + reabilita memória
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>
2026-05-02 18:39:43 -03:00
Rodribm10
ebf98c90f4 feat(captain/hermes): camada 4 — topic gating contra info não pedida
Camada 3 (strip de linhas repetidas) não cobre quando LLM mistura tópico
antigo + atual numa LINHA SÓ (caso real: cliente pergunta sobre pet,
Hermes responde "A senha é Prime2025 e pode levar animais sim" —
overlap baixo, strip não dispara).

Camada 4 detecta tópicos factuais (Wi-Fi/senha/pet/estacionamento/preço/
cancelamento) presentes NA RESPOSTA mas AUSENTES da última pergunta do
cliente. Quando detectado, bloqueia entrega + dispara
[SISTEMA: force_topic_focus] no Hermes mandando responder
EXCLUSIVAMENTE sobre o tópico atual. 1 retry; persistindo, entrega.

Validado: cliente "Posso levar animais?" + resposta "Senha + pet" →
detecta [:wifi] como off-topic. Cliente "Qual senha wifi?" + resposta
"Senha é X" → vazio (passa normal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:22:41 -03:00
Rodribm10
cc58805722 feat(captain/hermes): camada 3 — strip de linhas repetidas + check FAQs
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>
2026-05-02 18:16:39 -03:00
Rodribm10
ed99f67525 feat(captain/hermes): camada 2 — gating de saída factual sem tool call
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>
2026-05-02 17:58:17 -03:00
Rodribm10
c960dc7e1e feat(captain/hermes): handoff por loop + label-guard em outgoing
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>
2026-05-02 17:30:58 -03:00
Rodribm10
c8785b999c fix(captain/hermes): intercepta placeholder e força tool call
Quando o LLM emite " Um momento — vou verificar." (ou variantes:
"deixa eu verificar", "aguarde um instante") sem chamar nenhuma tool,
o callback agora:

1. NÃO entrega a msg pro cliente (UX terrível ver "vou verificar" e
   ficar esperando indefinidamente).
2. Dispara notify_event pro Hermes com [SISTEMA: force_tool_call]
   instruindo a retomar e chamar a tool relevante (generate_pix,
   send_suite_images, faq_lookup) com base na última msg do cliente.
3. Limita 2 retries por conversation via Rails.cache (TTL 5min). Após
   esgotar, marca labels hermes_placeholder + triagem_humana e descarta.

Caso real do Rodrigo: cliente confirmou reserva ("Para hoje 23h por 4h")
e o LLM respondeu apenas o placeholder (api_calls=1 no daemon, sem
tool). Cliente ficava esperando sem resposta. Agora Captain força o
LLM a chamar a tool, ou cai pra triagem humana após 2 tentativas.

PLACEHOLDER_PATTERNS cobre as variações observadas. SKILL.md já proibia
"Um momento", mas o LLM ignorava — defesa em camadas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:01:18 -03:00
Rodribm10
ed21722dc4 fix(captain/hermes): callback aceita ?slug= além de ?inbox_id=
Hermes daemon faz POST /webhooks/captain/hermes_callback?slug=<profile>
mas controller só conhecia ?inbox_id. 404 → resposta do LLM nunca chegava
ao Captain. Cliente via só auto-react.

Fix: fetch_inbox resolve via Captain::Assistant.find_by(hermes_profile_name)
quando slug está presente. Inbox é a primeira CaptainInbox associada a
esse assistant. Suporta o pattern admin de re-apontar uma inbox de teste
(ex: Angelina) entre vários agentes Hermes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:53:12 -03:00
Rodribm10
5f6aed05c9 fix(captain/hermes-builder): callback resolve session via last_session
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>
2026-05-01 22:53:43 -03:00
Rodribm10
40fd0c8f50 feat(captain/hermes-builder): UI Vue + endpoints pra chat com Construtor
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>
2026-05-01 21:00:41 -03:00
Rodribm10
48fad2977b feat(captain/hermes): payload enriquecido + humanizadores + notif Pix proativa
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>
2026-05-01 20:15:50 -03:00
Rodribm10
9ed3491d55 feat(captain/mcp): suite de 9 tools MCP + pricing tables Dolce Amore
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>
2026-05-01 20:13:16 -03:00
Rodribm10
713bb16012 feat(captain): MCP controller aceita Bearer token além de HMAC
Hermes Agent (cliente MCP) usa `--auth header` que envia
`Authorization: Bearer <token>` — padrão MCP. Antes o Captain MCP server
só aceitava HMAC-SHA256 (X-Hub-Signature-256), incompatível com o que
Hermes manda nativamente.

Agora aceita qualquer um dos 2 modos:
- Bearer token (recomendado, padrão MCP) — Hermes envia automaticamente
- HMAC-SHA256 do body — pra clientes que preferem assinar payload

Ambos validados com ActiveSupport::SecurityUtils.secure_compare contra
o mesmo CAPTAIN_MCP_SECRET. Sem secret = validação desabilitada (PoC/dev).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:42:57 -03:00
Rodribm10
23911ea878 feat(captain): MCP server (HTTP) expondo tools pro Hermes Agent
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>
2026-05-01 15:32:38 -03:00
Rodribm10
cd519a73c4 fix(captain): converte markdown bold pra formato WhatsApp no callback Hermes
Hermes (e LLMs default em geral) emitem **negrito** no formato markdown
padrão. WhatsApp usa formato próprio: *negrito* (single asterisk). Sem
conversão, o cliente vê asteriscos literais no WhatsApp, parecendo bug.

Defesa em camadas:
1. SOUL.md da Valentina foi atualizado com regra explícita de formato
   WhatsApp (single asterisk pra bold, underscore pra itálico, etc).
2. Este controller faz normalização defensiva no callback recebido do
   Hermes: regex `**texto**` -> `*texto*` antes de criar a mensagem
   outgoing. Não afeta o resto do conteúdo.

normalize_for_whatsapp() é trivialmente reversível e idempotente
(executar 2x é igual a 1x).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:24:57 -03:00
Rodribm10
35de8b7fde feat(captain): cliente Captain ↔ Hermes (outgoing job + callback endpoint)
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>
2026-05-01 13:22:22 -03:00