feat(captain/mcp): get_assistant_scenario tool — Construtor copia identidade de outro agente

Construtor atende 'copiar maps/endereço/telefone/wifi da Lara' sem o admin
redigitar. Tool retorna o markdown bruto do scenario (default
Daniela_Reservas) do assistant fonte; LLM extrai os campos relevantes.

Cobre o gap entre get_assistant_pricing (preços estruturados) e
get_assistant_faqs (Q&As): essa retorna prompt CRU pra LLM interpretar
campos não estruturados (contatos, links, wifi, persona).

Hot-patched + USR1 no Puma e Construtor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rodribm10 2026-05-02 11:55:13 -03:00
parent dd9e11da14
commit 0f39945f43
2 changed files with 70 additions and 0 deletions

View File

@ -22,6 +22,7 @@ class Captain::Mcp::ToolRegistry
Captain::Mcp::Tools::ListAssistantsTool,
Captain::Mcp::Tools::GetAssistantPricingTool,
Captain::Mcp::Tools::GetAssistantFaqsTool,
Captain::Mcp::Tools::GetAssistantScenarioTool,
Captain::Mcp::Tools::SaveAgentSpecTool
# Captain::Mcp::Tools::HandoffTool — fluxo via automation hoje, MCP futuro
].freeze

View File

@ -0,0 +1,69 @@
# Tool MCP: retorna o texto completo de um cenário (instruction) de um
# assistente existente.
#
# Caso de uso: Construtor pergunta "copiar identidade/maps/wifi/telefone
# da Lara?". Tool retorna o markdown bruto do scenario solicitado pra o
# Construtor (LLM) extrair os campos relevantes.
#
# Diferente de get_assistant_pricing (que parseia preços) e
# get_assistant_faqs (que lista responses): essa retorna o RAW PROMPT
# pra LLM interpretar livremente.
class Captain::Mcp::Tools::GetAssistantScenarioTool < Captain::Mcp::Tools::BaseTool
DEFAULT_SCENARIO_TITLE = 'Daniela_Reservas'.freeze
MAX_CHARS = 20_000
class << self
def name
'get_assistant_scenario'
end
def description
'Retorna o texto MARKDOWN completo de um cenário (prompt) de um ' \
'assistente existente. Use pra copiar identidade da unidade ' \
'(endereço, telefone, WhatsApp, maps, wifi, etc), persona ou ' \
'qualquer outro detalhe que esteja no prompt do agente fonte. ' \
'Default scenario_title="Daniela_Reservas".'
end
def input_schema
{
type: 'object',
properties: {
assistant_id: {
type: 'integer',
description: 'ID do assistente fonte.'
},
scenario_title: {
type: 'string',
description: "Título do cenário (default: '#{DEFAULT_SCENARIO_TITLE}'). Use list_assistants pra ver opções.",
default: DEFAULT_SCENARIO_TITLE
}
},
required: ['assistant_id']
}
end
end
def call(args, context:) # rubocop:disable Lint/UnusedMethodArgument, Metrics/AbcSize
assistant = Captain::Assistant.find_by(id: args['assistant_id'])
return error_response("Assistente #{args['assistant_id']} não encontrado.") if assistant.blank?
title = args['scenario_title'].to_s.presence || DEFAULT_SCENARIO_TITLE
scenario = assistant.scenarios.find_by(title: title)
if scenario.blank?
avail = assistant.scenarios.pluck(:title)
return error_response(
"Cenário '#{title}' não existe em #{assistant.name}. Disponíveis: #{avail.join(', ')}."
)
end
text = scenario.instruction.to_s
text = "#{text.first(MAX_CHARS)}\n\n... [truncado em #{MAX_CHARS} chars] ..." if text.size > MAX_CHARS
text_response("# Cenário '#{title}' de #{assistant.name}\n\n#{text}")
rescue StandardError => e
Rails.logger.error("[Captain::Mcp::GetAssistantScenarioTool] error: #{e.class}: #{e.message}")
error_response("Erro ao buscar cenário: #{e.message}")
end
end