iachat/enterprise/app/services/captain/mcp/tools/faq_lookup_tool.rb
Rodribm10 23911ea878 feat(captain): MCP server (HTTP) expondo tools pro Hermes Agent
Implementa servidor MCP (Model Context Protocol) HTTP no Captain pra
o Hermes Agent invocar tools do Captain via `hermes mcp add`. Substrato
pra integração de Nível 2 onde o agente consulta tools quando precisa
executar ações reais (buscar FAQ, adicionar label, futuramente Pix etc).

Arquivos:

- app/controllers/webhooks/captain/mcp_controller.rb
  Endpoint POST /webhooks/captain/mcp. Valida HMAC (CAPTAIN_MCP_SECRET),
  parseia JSON-RPC, despacha pro Server. Extrai params._captain_context
  com multi-tenant ids (conversation_id, inbox_id, account_id, etc).

- enterprise/app/services/captain/mcp/server.rb
  Subset MCP suficiente: initialize, tools/list, tools/call, ping,
  notifications/initialized. JSON-RPC síncrono (sem SSE).

- enterprise/app/services/captain/mcp/tool_registry.rb
  Lista centralizada de tools.

- enterprise/app/services/captain/mcp/tools/base_tool.rb
  Interface base pras tools (.name, .description, .input_schema, #call).

- enterprise/app/services/captain/mcp/tools/add_label_tool.rb
  Tool 1: aplica label na conversation atual.

- enterprise/app/services/captain/mcp/tools/faq_lookup_tool.rb
  Tool 2: busca semântica em FAQs (Captain::AssistantResponse via pgvector
  cosine). Reaproveita SearchReplyDocumentationService pra paridade com
  o caminho legado do Captain.

- config/routes.rb
  Rota POST /webhooks/captain/mcp.

Conexão pelo Hermes:
  hermes mcp add captain-tools --url http://CAPTAIN_HOST/webhooks/captain/mcp

Auth: HMAC X-Hub-Signature-256 quando CAPTAIN_MCP_SECRET setado.

TODO próxima sprint: generate_pix_tool, send_suite_images_tool. Handoff
fica via automation hoje (UI Chatwoot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:32:38 -03:00

77 lines
2.6 KiB
Ruby

# Tool MCP: busca semântica em FAQs/documentação aprovada pelas gerentes.
#
# Caso de uso típico: cliente pergunta algo que NÃO está na skill estruturada
# do Hermes (ex: aceita pet, formas de pagamento alternativo, política de
# alguma situação específica). Em vez de inventar, Hermes chama esta tool
# e responde com base no FAQ atualizado em tempo real pelo Captain UI.
#
# Reaproveita Captain::Tools::SearchReplyDocumentationService — exatamente
# o mesmo serviço que o orquestrador interno do Captain usava antes,
# garantindo que Hermes vê os mesmos FAQs que o caminho legado veria.
class Captain::Mcp::Tools::FaqLookupTool < Captain::Mcp::Tools::BaseTool
class << self
def name
'faq_lookup'
end
def description
'Busca semântica em FAQs/documentação aprovada pelas gerentes do hotel. ' \
'Use quando o cliente perguntar algo que NÃO está na sua skill ' \
'(ex: política de pets, horários especiais, convênios, regras pontuais). ' \
'Retorna até 5 perguntas/respostas mais próximas semanticamente da query. ' \
'Se não encontrar nada relevante, prefira transferir pro humano em vez ' \
'de inventar.'
end
def input_schema
{
type: 'object',
properties: {
query: {
type: 'string',
description: 'Pergunta ou tema a buscar em linguagem natural ' \
'(ex: aceitam pets, estacionamento coberto, ' \
'forma de pagamento sem ser Pix).'
}
},
required: ['query']
}
end
end
def call(args, context:)
query = args['query'].to_s.strip
return error_response('Argumento "query" é obrigatório.') if query.blank?
account = resolve_account(context)
return error_response('Account não encontrada no contexto MCP.') if account.blank?
assistant = resolve_assistant(context, account)
result = ::Captain::Tools::SearchReplyDocumentationService.new(
account: account,
assistant: assistant
).execute(query: query)
text_response(result.presence || 'Nenhum FAQ relevante encontrado pra essa pergunta.')
rescue StandardError => e
Rails.logger.error("[Captain::Mcp::FaqLookupTool] error: #{e.class}: #{e.message}")
error_response("Falha na busca de FAQ: #{e.message}")
end
private
def resolve_account(context)
account_id = context[:account_id]
return nil if account_id.blank?
Account.find_by(id: account_id)
end
def resolve_assistant(context, account)
assistant_id = context[:assistant_id]
return nil if assistant_id.blank?
account.captain_assistants.find_by(id: assistant_id)
end
end