fix(captain): stop scenario->orchestrator handoff + narrow FAQ guardrail

Two behavioural regressions caught in live testing with a real customer
conversation:

1. Ping-pong scenario -> orchestrator -> scenario

   build_and_wire_agents was calling scenario_agents.register_handoffs(
   assistant_agent), which exposed handoff_to_jasmine as a tool INSIDE
   every scenario. Daniela (reservation scenario) kept calling it mid
   flow, the orchestrator resumed the turn, and customers got messages
   like "Vou te encaminhar para a Daniela..." after ALREADY being with
   Daniela. The back-edge is removed. When a customer legitimately
   changes topic mid-scenario, pick_starting_agent on the next turn
   already routes back to the orchestrator based on conversation state,
   so no manual handoff from the scenario side is needed.

2. FAQ_PRICE_PATTERNS was hijacking legitimate routing responses

   The previous regex matched the bare words "pernoite", "sinal",
   "diaria" WITHOUT requiring a numeric price nearby. A legitimate
   handoff response like "Vou transferir para a Daniela para confirmar
   a Stilo para pernoite" tripped the guardrail, which then substituted
   the response with raw FAQ content about rates. Narrowed to: R$
   values, numbers followed by "reais", and the explicit price-noun
   variants (preco/preço/valor/preços/valores/custo/custa). Incidental
   mentions of stay types no longer trigger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rodribm10 2026-04-19 10:51:45 -03:00
parent fa758e4848
commit e5d186c689

View File

@ -12,10 +12,14 @@ class Captain::Assistant::AgentRunnerService
/\bi don't (know|have|have access)\b/i,
/\bcan'?t (access|help|provide)\b/i
].freeze
# Patterns must imply an actual price answer, not casual mentions of
# stay types. E.g. "Vou te transferir pra Daniela pra confirmar a
# Stilo pra pernoite" legitimately mentions "pernoite" but is a
# handoff, NOT a price response — keep the guardrail from hijacking it.
FAQ_PRICE_PATTERNS = [
/r\$\s*\d+/i,
/\b\d+[,.]?\d*\s*(reais|real)\b/i,
/\b(preco|preço|valor|diaria|diária|pernoite|sinal)\b/i
/\b(preco|preço|valor|preços|valores|cust[ao])\b/i
].freeze
FAQ_NOT_FOUND_FALLBACK = 'Consultei o FAQ e não encontrei essa informação cadastrada ainda. Posso te ajudar com outro tema ou te transferir para um atendente.'.freeze
@ -369,8 +373,14 @@ class Captain::Assistant::AgentRunnerService
assistant_agent = build_orchestrator_agent_with_memory
scenario_agents = @assistant.scenarios.enabled.map(&:agent)
# Orchestrator can hand off INTO any scenario. Scenarios do NOT hand off
# back to the orchestrator — that creates a ping-pong where the scenario
# calls handoff_to_jasmine mid-flow, the orchestrator resumes the turn,
# and responses get duplicated or routed through the FAQ guardrail. When
# a customer changes topic mid-scenario, pick_starting_agent on the next
# turn already routes back to the orchestrator based on conversation
# state — no manual handoff needed from the scenario side.
assistant_agent.register_handoffs(*scenario_agents) if scenario_agents.any?
scenario_agents.each { |scenario_agent| scenario_agent.register_handoffs(assistant_agent) }
[assistant_agent] + scenario_agents
end