From e5d186c689babb110081ab6a6e2b29ea41066708 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Sun, 19 Apr 2026 10:51:45 -0300 Subject: [PATCH] fix(captain): stop scenario->orchestrator handoff + narrow FAQ guardrail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../captain/assistant/agent_runner_service.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/enterprise/app/services/captain/assistant/agent_runner_service.rb b/enterprise/app/services/captain/assistant/agent_runner_service.rb index 243140191..9daf769e0 100644 --- a/enterprise/app/services/captain/assistant/agent_runner_service.rb +++ b/enterprise/app/services/captain/assistant/agent_runner_service.rb @@ -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