From d2c2c6b7fe69c6a9c9c1dcd9e911476fae7dca31 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Wed, 22 Apr 2026 04:19:39 -0300 Subject: [PATCH] fix(captain): pre-reservation semantics + no duplicate pix links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../contact_memories/extraction_service.rb | 28 ++++++++++++++----- .../captain/tools/generate_pix_tool.rb | 28 +++++++++---------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/enterprise/app/services/captain/contact_memories/extraction_service.rb b/enterprise/app/services/captain/contact_memories/extraction_service.rb index 4922ba51c..2a04171f6 100644 --- a/enterprise/app/services/captain/contact_memories/extraction_service.rb +++ b/enterprise/app/services/captain/contact_memories/extraction_service.rb @@ -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 **só agradece ou silencia**, isso **NÃO é reserva consumada**. Pix gerado é um convite pra pagar — a reserva só 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 só 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 só 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 há 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 só 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 só agradeceu / ficou em silêncio / não há 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" SÓ 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" SÓ 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 há 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. diff --git a/enterprise/app/services/captain/tools/generate_pix_tool.rb b/enterprise/app/services/captain/tools/generate_pix_tool.rb index 28891d906..58f2c0d9f 100644 --- a/enterprise/app/services/captain/tools/generate_pix_tool.rb +++ b/enterprise/app/services/captain/tools/generate_pix_tool.rb @@ -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,