fix(captain): pre-reservation semantics + no duplicate pix links

Three UX bugs from staging testing:

1. Duplicate Pix link in WhatsApp — the tool's formatted_message
   embedded the full link + instructions, so the LLM copied it into
   its own response on top of the dedicated link message sent by
   dispatch_direct_link_message. The tool now returns a short
   summary with no URL; dispatch is the single source of the link.

2. "Reserva confirmada!" sent before payment — the scenario prompt
   used the word "confirmação" loosely, which the LLM read as the
   reservation being closed. Now the prompt forces "pré-reserva /
   aguardando pagamento" until the Pix is actually paid, and the
   dispatched link message explains that the reservation is only
   secured after payment clears.

3. Memory extraction wrote "Reservou Hidromassagem para pernoite
   em 22/04/2026" when the customer only received a Pix link and
   replied "obrigado". Tightened the extraction prompt so
   padrao_comportamental of a reservation requires a literal
   payment confirmation — Pix generated alone no longer qualifies.
This commit is contained in:
Rodribm10 2026-04-22 04:19:39 -03:00
parent 6c9d12559d
commit d2c2c6b7fe
2 changed files with 35 additions and 21 deletions

View File

@ -54,10 +54,22 @@ class Captain::ContactMemories::ExtractionService
- **CERTO**: nada. O cliente não reservou disse que vai entrar em contato. Se amanhã ele reservar de fato, a conversa de amanhã vira memória.
### Sinais de AÇÃO CONSUMADA (pode virar `padrao_comportamental` com data):
- Bot gerou Pix / enviou link de reserva **E** cliente confirmou recebimento ou pagou.
- Cliente disse explicitamente "paguei", "confirmado", "pode confirmar", "tá feito", "perfeito, pode marcar".
- Bot respondeu confirmando suíte + data + valor **E** cliente não desdisse.
- Registro de estadia: "fiquei na Alexa em 03/02", "nos hospedamos no fim de semana".
- **Pagamento confirmado**: cliente disse literalmente "paguei o Pix", "Pix pago", "fiz o pagamento", "acabei de pagar", OU aparece mensagem automática "Pagamento confirmado" / "Pix recebido" do bot.
- Registro de estadia PASSADA: "fiquei na Alexa em 03/02", "nos hospedamos no fim de semana".
- Cliente disse "já estou no hotel", "cheguei", "fazendo check-in agora".
### ATENÇÃO — Pix GERADO ≠ Pix PAGO (diferença crítica)
Quando o bot gera um Pix e o cliente ** agradece ou silencia**, isso **NÃO é reserva consumada**. Pix gerado é um convite pra pagar a reserva vira real quando o pagamento cai.
| Situação na conversa | É `padrao_comportamental "Reservou X"`? |
|---|---|
| Bot gerou Pix + cliente disse "obrigado" / nada | NÃO (nem "solicitou reserva" vale evite o tipo `padrao_comportamental` inteiro) |
| Bot gerou Pix + cliente disse "paguei" / "fiz o Pix" | SIM, pode registrar "Reservou X em DD/MM/AAAA" |
| Bot gerou Pix + aparece msg automática "Pagamento confirmado" | SIM |
| Cliente pediu valor sem pedir pra reservar | NÃO |
Regra prática: se você não consegue apontar uma frase LITERAL de pagamento confirmado (do cliente ou do sistema), **não extraia `padrao_comportamental` de reserva**. Melhor memória ausente do que memória mentirosa dizendo que o cliente reservou quando gerou Pix.
### Sinais de INTENÇÃO FUTURA (NÃO MEMORIZE — retorne nada):
- "Entro em contato amanhã para reservar"
@ -123,14 +135,16 @@ class Captain::ContactMemories::ExtractionService
"Escolheu 4hrs em 14/03/2026"
SIM: "Sempre chego tarde, entre 23h e meia-noite" (declarou hábito)
SIM: "Costumo ficar só o pernoite" (declarou hábito)
SIM: "Reservou Alexa para pernoite em 23/05/2026" APENAS se a reserva foi consumada (Pix gerado + cliente não desistiu, OU cliente disse "pode confirmar" + bot confirmou)
SIM: "Reservou Alexa para pernoite em 23/05/2026" APENAS se confirmação LITERAL de pagamento (cliente disse "paguei"/"Pix pago" OU mensagem automática "Pagamento confirmado"). Pix meramente gerado não conta.
SIM: "Escolheu 4hrs na visita de 14/03/2026" se efetivamente escolheu e fechou
NÃO: "Costuma ficar 2 horas" (SEM DATA e SEM declaração banido)
NÃO: "Prefere permanência de 4 horas" (banido isso seria preferencia, que exige declaração explícita)
NÃO: "Vai chegar às 22h hoje" (intenção da conversa atual, não histórico)
NÃO: "Reservou X" quando o cliente disse "entro em contato amanhã para reservar" ou "quero reservar" (intenção futura violação da REGRA DE OURO).
NÃO: "Reservou X" quando o bot apenas cotou preço e o cliente não fechou explicitamente.
REGRA CRÍTICA: se você vai registrar uma escolha pontual, (a) a ação DEVE ter sido consumada, e (b) SEMPRE inclua a data no content. Memória sem data vira ruído; memória sem consumação vira mentira.
NÃO: "Reservou X em DD/MM" quando o bot GEROU Pix mas o cliente agradeceu / ficou em silêncio / não confirmação de pagamento na conversa. Pix gerado sem pagamento = pré-reserva, NÃO é ação consumada, NÃO vira memória.
NÃO: "Solicitou reserva de X" / "Pediu Pix para X" evite registrar a fase de intenção/pré-reserva como `padrao_comportamental`. Se a reserva foi paga, registra "Reservou X"; se não foi, não registra nada sobre essa tentativa.
REGRA CRÍTICA: se você vai registrar uma escolha pontual, (a) a ação DEVE ter sido consumada com pagamento confirmado na conversa, e (b) SEMPRE inclua a data no content. Memória sem data vira ruído; memória sem consumação vira mentira.
5. **reclamacao** queixa EXPLÍCITA sobre algo que desagradou/frustrou/causou problema, com sentimento negativo claro.
SIM: "O ar-condicionado estava barulhento demais, não dormi direito"
@ -169,7 +183,7 @@ class Captain::ContactMemories::ExtractionService
1. **Evidência OBRIGATÓRIA**: cada fato precisa de um trecho LITERAL da conversa. Se não tem trecho claro, não extraia.
2. **Perguntas/dúvidas NÃO são reclamação nem memória**: se o cliente fez uma pergunta ("tem X?", "aceita Y?"), isso é informação que ele queria, não fato sobre ele.
3. **Cortesia genérica NÃO é feedback**: "obrigado", "tá bom", "ok" NÃO viram feedback_positivo.
4. **Aplicar a REGRA DE OURO de ação-consumada vs intenção-futura**: "informou CPF" nunca é memória (é cadastro). "Escolheu X" ou "Reservou X em tal data" vira `padrao_comportamental` se a ação foi efetivamente CONSUMADA nesta conversa (Pix confirmado, cliente disse "pode marcar"+ bot confirmou, ou registro de estadia passada). Discussão/intenção sem fechamento = NÃO EXTRAIA.
4. **Aplicar a REGRA DE OURO de ação-consumada vs intenção-futura**: "informou CPF" nunca é memória (é cadastro). "Escolheu X" ou "Reservou X em tal data" vira `padrao_comportamental` se o pagamento do Pix foi CONFIRMADO na conversa (cliente disse "paguei" ou apareceu msg automática "Pagamento confirmado"). Pix gerado sem confirmação de pagamento = pré-reserva, NÃO é ação consumada, NÃO vira memória. Discussão/intenção sem pagamento = NÃO EXTRAIA.
5. **Ações do atendente NÃO são memória do cliente**: se o bot "incentivou X" ou "ofereceu Y", isso descreve o atendente, não o cliente. Ignore.
6. **Máximo 5 fatos por conversa**. Se dúvida entre extrair ou não, DESCARTE. Qualidade > quantidade.
7. **Se a conversa não tem NADA realmente memorável**, retorne `{"facts": []}`. Isso é o comportamento normal e esperado da maioria das conversas transacionais.

View File

@ -426,12 +426,15 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
mark_conversation_as_awaiting_payment(reservation)
Rails.logger.info "[GeneratePixTool] Reserva #{reservation.id} → pending_payment"
final_prefix = prefix || 'Cobrança Pix gerada com sucesso.'
response = build_pix_response(charge, reservation, amount: charge_amount, prefix: final_prefix)
response = build_pix_response(charge, reservation, amount: charge_amount, prefix: prefix)
# Envia o link como mensagem direta pro cliente. Isso garante que o URL chegue
# no WhatsApp mesmo que a LLM parafraseie com placeholder tipo "[Link do Pix]".
dispatch_direct_link_message(response[:payment_link], 'Link do Pix:')
# A mensagem do link é a ÚNICA com URL — a resposta da LLM não deve conter link.
dispatch_direct_link_message(
response[:payment_link],
"Pix do sinal (R$ #{format('%.2f', charge_amount)}) — sua reserva só fica garantida depois que o pagamento cair:"
)
response
rescue StandardError => e
@ -772,8 +775,6 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
end
def build_pix_response(charge, reservation, amount:, prefix: nil)
intro = prefix.present? ? "#{prefix}\n\n" : ''
base_url = InstallationConfig.find_by(name: 'FRONTEND_URL')&.value.presence ||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
base_url = base_url.gsub('0.0.0.0', '127.0.0.1') if base_url.include?('0.0.0.0')
@ -781,14 +782,6 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
token = charge.to_sgid(expires_in: 2.hours, purpose: :pix_payment).to_s
link = Rails.application.routes.url_helpers.short_payment_link_url(token, host: base_url)
instructions = "Para facilitar, gerei um link de pagamento rápido com botão COPIAR.\n" \
"1. Clique no link abaixo\n" \
"2. Aperte 'COPIAR CÓDIGO PIX'\n" \
"3. Cole no app do seu banco e pague.\n\n" \
"Link Seguro: #{link}\n\n" \
'(IMPORTANTE PARA A I.A.: Envie este link EXATAMENTE como está acima. NÃO use formatação Markdown [texto](url). ' \
'O WhatsApp não reconhece. Envie APENAS a URL pura, solta no texto.)'
final_code = charge.pix_copia_e_cola.to_s.strip
if final_code.start_with?('/spi/')
header = '00020101021226930014BR.GOV.BCB.PIX2571spi-qrcode.bancointer.com.br'
@ -797,9 +790,16 @@ class Captain::Tools::GeneratePixTool < Captain::Tools::BaseTool
final_code = normalize_text(final_code)
# formatted_message vai pra LLM como tool output — deve ser CURTO, sem URL
# e sem código Pix, porque dispatch_direct_link_message já envia o link em
# mensagem separada. Se incluirmos link aqui, a LLM parafraseia/cola de
# novo e o cliente recebe 2 mensagens com a mesma URL.
# prefix opcional pra casos como "Pix ainda válido" / "Gerando novo Pix".
summary = prefix.presence || 'Pix do sinal gerado e enviado em mensagem separada.'
normalize_payload(
{
formatted_message: "#{intro}#{instructions}",
formatted_message: summary,
raw_payload: final_code,
payment_link: link,
amount: amount.to_f,