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>
This commit is contained in:
parent
bd494c424d
commit
c8785b999c
@ -16,6 +16,18 @@
|
|||||||
class Webhooks::Captain::HermesCallbackController < ApplicationController
|
class Webhooks::Captain::HermesCallbackController < ApplicationController
|
||||||
RECENT_WINDOW = 5.minutes
|
RECENT_WINDOW = 5.minutes
|
||||||
|
|
||||||
|
# Placeholders que o LLM emite quando "decide" mandar um sinal de espera
|
||||||
|
# antes de chamar uma tool — mas às vezes manda E PARA, sem chamar a tool
|
||||||
|
# (api_calls=1 no daemon). Quando detectamos, NÃO entregamos pro cliente
|
||||||
|
# e re-disparamos pro Hermes uma instrução pra retomar e chamar a tool.
|
||||||
|
PLACEHOLDER_PATTERNS = [
|
||||||
|
/\A\s*[⏳⌛]?\s*um\s+momento/i,
|
||||||
|
/\A\s*[⏳⌛]\s*vou\s+verificar/i,
|
||||||
|
/\A\s*aguarde\s+um\s+instante/i,
|
||||||
|
/\A\s*deixa\s+eu\s+(verificar|conferir|checar)/i
|
||||||
|
].freeze
|
||||||
|
MAX_PLACEHOLDER_RETRIES = 2
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token, raise: false
|
skip_before_action :verify_authenticity_token, raise: false
|
||||||
before_action :verify_signature
|
before_action :verify_signature
|
||||||
before_action :fetch_inbox
|
before_action :fetch_inbox
|
||||||
@ -27,6 +39,11 @@ class Webhooks::Captain::HermesCallbackController < ApplicationController
|
|||||||
conversation = recent_conversation_for(@inbox)
|
conversation = recent_conversation_for(@inbox)
|
||||||
return log_no_conversation_and_ack if conversation.blank?
|
return log_no_conversation_and_ack if conversation.blank?
|
||||||
|
|
||||||
|
if placeholder_response?(content)
|
||||||
|
handle_placeholder_response(conversation, content)
|
||||||
|
return head :ok
|
||||||
|
end
|
||||||
|
|
||||||
log_reply(conversation, content)
|
log_reply(conversation, content)
|
||||||
if defined?(Captain::Hermes::DelayedReplyJob)
|
if defined?(Captain::Hermes::DelayedReplyJob)
|
||||||
Captain::Hermes::DelayedReplyJob.perform_later(conversation.id, content)
|
Captain::Hermes::DelayedReplyJob.perform_later(conversation.id, content)
|
||||||
@ -42,6 +59,59 @@ class Webhooks::Captain::HermesCallbackController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def placeholder_response?(content)
|
||||||
|
return false if content.blank?
|
||||||
|
|
||||||
|
PLACEHOLDER_PATTERNS.any? { |re| content.match?(re) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Quando o Hermes responde só placeholder e para, fazemos 2 coisas:
|
||||||
|
# 1. NÃO entregamos a msg pro cliente (UX terrível ver "vou verificar" e
|
||||||
|
# ficar esperando indefinidamente);
|
||||||
|
# 2. Disparamos notify_event pro Hermes com instrução pra retomar e
|
||||||
|
# chamar a tool relevante. Limite 2 retries por conversation pra
|
||||||
|
# evitar loop. Após esgotar, marcamos pix_falhou_fallback pra
|
||||||
|
# triagem humana.
|
||||||
|
def handle_placeholder_response(conversation, content)
|
||||||
|
cache_key = "hermes_placeholder_retry:#{conversation.id}"
|
||||||
|
retries = Rails.cache.read(cache_key).to_i
|
||||||
|
|
||||||
|
Rails.logger.warn(
|
||||||
|
"[Hermes::Callback] placeholder detectado em conv #{conversation.display_id} (retries=#{retries}): #{content.truncate(80)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if retries >= MAX_PLACEHOLDER_RETRIES
|
||||||
|
Rails.logger.error("[Hermes::Callback] esgotou retries de placeholder em conv #{conversation.display_id} — marcando triagem")
|
||||||
|
mark_for_human_triage(conversation)
|
||||||
|
Rails.cache.delete(cache_key)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Rails.cache.write(cache_key, retries + 1, expires_in: 5.minutes)
|
||||||
|
retrigger_hermes_no_placeholder!(conversation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def retrigger_hermes_no_placeholder!(conversation)
|
||||||
|
Captain::Hermes::Client.new(@inbox).notify_event(
|
||||||
|
conversation: conversation,
|
||||||
|
event_type: 'force_tool_call',
|
||||||
|
system_message: '[SISTEMA: force_tool_call] Você acabou de emitir um placeholder ' \
|
||||||
|
'("Um momento", "vou verificar" etc) sem chamar tool. Cliente NÃO ' \
|
||||||
|
'recebeu essa mensagem. Releia a última mensagem do cliente e ' \
|
||||||
|
'CHAME A TOOL CORRESPONDENTE AGORA (generate_pix se confirmou ' \
|
||||||
|
'reserva, send_suite_images se pediu foto, faq_lookup se pediu ' \
|
||||||
|
'info de regra). Se faltar dado, pergunte direto sem placeholder.'
|
||||||
|
)
|
||||||
|
rescue Captain::Hermes::Client::DispatchError => e
|
||||||
|
Rails.logger.error("[Hermes::Callback] retrigger falhou: #{e.message}")
|
||||||
|
mark_for_human_triage(conversation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_for_human_triage(conversation)
|
||||||
|
current = conversation.label_list
|
||||||
|
conversation.update_labels((current + %w[hermes_placeholder triagem_humana]).uniq)
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_inbox
|
def fetch_inbox
|
||||||
inbox_id = params[:inbox_id].presence || params.dig(:metadata, :inbox_id).presence
|
inbox_id = params[:inbox_id].presence || params.dig(:metadata, :inbox_id).presence
|
||||||
if inbox_id.present?
|
if inbox_id.present?
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user