Skip to content

Plano de Integração: Salesforce Bot ↔ iFriend Agent (Cloud Run)

Objetivo: Conectar o Einstein Bot da Salesforce (vinculado ao WhatsApp de atendimento) ao agente iFriend rodando no Google Cloud Run, de forma que mensagens recebidas no WhatsApp sejam roteadas ao agente e as respostas retornem ao usuário pelo WhatsApp via Salesforce.

1. Visão Geral da Arquitetura

┌──────────┐    ┌──────────────────┐    ┌───────────────────┐    ┌──────────────────┐
│ Usuário  │───▶│  WhatsApp (Meta) │───▶│    Salesforce      │───▶│  Cloud Run       │
│ WhatsApp │    │  Business API    │    │  Messaging +       │    │  (iFriend Agent) │
│          │◀───│                  │◀───│  Einstein Bot      │◀───│                  │
└──────────┘    └──────────────────┘    └───────────────────┘    └──────────────────┘
                                              │
                                        Apex Callout
                                        (HTTP POST)
                                        /webchat/message

Fluxo: 1. Usuário envia mensagem no WhatsApp 2. Meta (WhatsApp Business API) entrega ao Salesforce Messaging 3. Salesforce roteia para o Einstein Bot 4. Einstein Bot executa um Apex Action que faz HTTP POST para o Cloud Run (/webchat/message) 5. Cloud Run processa com o agente iFriend e retorna a resposta (JSON síncrono) 6. Einstein Bot recebe a resposta e envia de volta ao usuário via WhatsApp

2. Viabilidade — Resposta Curta

Sim, é viável. A Salesforce oferece múltiplas formas de integrar bots com serviços externos. A abordagem mais robusta e recomendada é usar Einstein Bot + Apex Action + Named Credential para chamar o endpoint REST do Cloud Run.

Pontos de Atenção

Aspecto Detalhe
Timeout Apex callout tem limite de 120 segundos. O agente precisa responder dentro desse tempo. Nosso Cloud Run já tem timeout de 3600s, mas o gargalo é o Salesforce.
Latência Respostas do agente com tool calls complexas (busca + cotação + detalhes) podem levar 10-30s. Dentro do limite.
Sessão Precisamos manter contexto multi-turn. Usaremos session_id fixo por conversa WhatsApp.
Limite de payload Apex HTTP callout suporta até 12 MB no body. Mais que suficiente.
Concorrência Salesforce permite até 100 callouts por transação Apex. Nosso uso é 1:1 (1 mensagem → 1 callout).

3. Opções de Integração

Opção A: Einstein Bot + Apex Action (⭐ Recomendada)

O Einstein Bot invoca uma Apex Invocable Action que faz HTTP callout para o Cloud Run.

Prós: - Controle total sobre o fluxo no Einstein Bot - Pode combinar lógica Salesforce (CRM data) com respostas do agente - Named Credentials para autenticação segura - Suporte nativo a WhatsApp via Salesforce Messaging

Contras: - Requer desenvolvimento Apex - Limite de 120s para callout (geralmente suficiente)

Opção B: Salesforce Flow + HTTP Callout Action

Usa um Flow com HTTP Callout Action (GA desde Spring '23) para chamar o Cloud Run diretamente, sem código Apex.

Prós: - Low-code/no-code (configuração via Flow Builder) - Mais rápido de implementar

Contras: - Menos flexível para tratamento de erros - Timeout mais restritivo em Flows (~120s) - Parsing de JSON complexo pode ser limitado

Opção C: MuleSoft como Middleware

Usa MuleSoft Anypoint entre Salesforce e Cloud Run.

Prós: - Padrão enterprise Salesforce - Retry, circuit breaker, monitoramento

Contras: - Complexidade e custo desnecessários para este caso - Latência adicional

Opção D: Webhook Reverso (Cloud Run → Salesforce)

Cloud Run publica resposta como Platform Event no Salesforce.

Prós: - Assíncrono, sem limite de timeout

Contras: - Complexidade muito maior - Orquestração de request/response assíncrona - Overkill para o caso de uso

Decisão: Opção A — Einstein Bot + Apex Action com Named Credential.

4. Arquitetura Detalhada (Opção A)

4.1 Lado Salesforce

4.1.1 Named Credential

Armazena a URL do Cloud Run e credenciais de autenticação (JWT).

Named Credential:
  Label: iFriend_Agent_CloudRun
  URL: https://ifriend-agent-unified-stage-XXXXX-uc.a.run.app
  Authentication Protocol: Custom (JWT Bearer Token)
  ou: No Authentication (se usar JWT no header manualmente)

4.1.2 Apex Class — Callout para Cloud Run

/**
 * Classe Apex para integração com o agente iFriend no Cloud Run.
 * Invocada pelo Einstein Bot via Invocable Action.
 */
public class IFriendAgentService {

    // Input do Einstein Bot
    public class AgentRequest {
        @InvocableVariable(required=true)
        public String userMessage;

        @InvocableVariable(required=true)
        public String userId;

        @InvocableVariable
        public String sessionId;

        @InvocableVariable
        public String jwtToken;
    }

    // Output para o Einstein Bot
    public class AgentResponse {
        @InvocableVariable
        public String responseText;

        @InvocableVariable
        public String sessionId;

        @InvocableVariable
        public Boolean success;

        @InvocableVariable
        public String errorMessage;
    }

    /**
     * Método invocável pelo Einstein Bot.
     * Faz callout HTTP POST para /webchat/message no Cloud Run.
     */
    @InvocableMethod(
        label='Consultar Agente iFriend'
        description='Envia mensagem ao agente iFriend e retorna resposta'
        category='iFriend'
    )
    public static List<AgentResponse> consultarAgente(List<AgentRequest> requests) {
        List<AgentResponse> responses = new List<AgentResponse>();

        for (AgentRequest req : requests) {
            AgentResponse res = new AgentResponse();
            try {
                res = callCloudRunAgent(req);
            } catch (Exception e) {
                res.success = false;
                res.errorMessage = e.getMessage();
                res.responseText = 'Desculpe, houve um erro ao processar sua solicitação.';
            }
            responses.add(res);
        }

        return responses;
    }

    private static AgentResponse callCloudRunAgent(AgentRequest req) {
        // Monta payload
        Map<String, Object> payload = new Map<String, Object>{
            'user_id' => req.userId,
            'session_id' => req.sessionId,
            'message' => req.userMessage,
            'metadata' => new Map<String, Object>{
                'source' => 'salesforce',
                'platform' => 'whatsapp',
                'is_whitelabel' => false
            }
        };

        // Configura HTTP request
        HttpRequest httpReq = new HttpRequest();
        httpReq.setEndpoint('callout:iFriend_Agent_CloudRun/webchat/message');
        httpReq.setMethod('POST');
        httpReq.setHeader('Content-Type', 'application/json');
        httpReq.setTimeout(120000); // 120 segundos (máximo Apex)

        // JWT Token (se configurado)
        if (String.isNotBlank(req.jwtToken)) {
            httpReq.setHeader('Authorization', 'Bearer ' + req.jwtToken);
        }

        httpReq.setBody(JSON.serialize(payload));

        // Faz callout
        Http http = new Http();
        HttpResponse httpRes = http.send(httpReq);

        // Parse resposta
        AgentResponse res = new AgentResponse();

        if (httpRes.getStatusCode() == 200) {
            Map<String, Object> responseBody = (Map<String, Object>)
                JSON.deserializeUntyped(httpRes.getBody());

            res.responseText = (String) responseBody.get('response');
            res.sessionId = (String) responseBody.get('session_id');
            res.success = true;
        } else {
            res.success = false;
            res.errorMessage = 'HTTP ' + httpRes.getStatusCode() + ': ' + httpRes.getBody();
            res.responseText = 'Desculpe, não consegui processar sua mensagem no momento.';
        }

        return res;
    }
}

4.1.3 Einstein Bot — Dialog Flow

Einstein Bot Dialog: "Roteamento para Agente IA"
─────────────────────────────────────────────────
┌─ [Trigger] ─────────────────────────────────────┐
│  Qualquer mensagem do usuário                    │
│  (ou intent específico: "falar com agente IA")   │
└──────────────┬──────────────────────────────────┘
               │
┌──────────────▼──────────────────────────────────┐
│  [Apex Action] IFriendAgentService               │
│                                                  │
│  Input:                                          │
│    userMessage ← {!Input_Text}                   │
│    userId ← {!Contact.WhatsAppNumber} ou         │
│              {!EndUserParty.Id}                   │
│    sessionId ← {!LiveChatTranscript.Id} ou       │
│                {!ConversationId}                  │
│                                                  │
│  Output:                                         │
│    responseText → {!AgentResponse}               │
│    sessionId → {!SessionId}                      │
│    success → {!CallSuccess}                      │
└──────────────┬──────────────────────────────────┘
               │
┌──────────────▼──────────────────────────────────┐
│  [Decision] success == true?                     │
│                                                  │
│  ├─ YES → [Message] {!AgentResponse}             │
│  │         → Volta ao Trigger (loop multi-turn)  │
│  │                                               │
│  └─ NO  → [Message] "Ocorreu um erro..."        │
│           → [Transfer] Agente Humano             │
└─────────────────────────────────────────────────┘

4.2 Lado Cloud Run (iFriend Agent)

O endpoint /webchat/message já existe e suporta exatamente este fluxo. O payload esperado:

POST /webchat/message
Content-Type: application/json
Authorization: Bearer <jwt_token>  (opcional)

{
  "user_id": "whatsapp:+5511999999999",
  "session_id": "salesforce_conv_abc123",
  "message": "Quero conhecer experiências no Rio de Janeiro",
  "metadata": {
    "source": "salesforce",
    "platform": "whatsapp",
    "is_whitelabel": false
  }
}

Resposta:

{
  "response": "Encontrei várias experiências no Rio de Janeiro! ...",
  "session_id": "salesforce_conv_abc123",
  "metadata": {
    "platform": "webchat",
    "user_id": "whatsapp:+5511999999999"
  }
}

Nenhuma alteração é necessária no Cloud Run para este fluxo básico. O endpoint /webchat/message já é síncrono e retorna a resposta completa.

5. Mapeamento de Session/User ID

Para manter a conversa multi-turn (memória de contexto), é fundamental mapear corretamente os IDs:

Campo Valor no Salesforce Mapeamento Cloud Run
user_id EndUserParty.Id ou WhatsApp phone Identifica o usuário no session service
session_id ConversationId ou LiveChatTranscript.Id Mantém contexto da conversa

Regra: Usar o mesmo session_id para todas as mensagens de uma mesma conversa WhatsApp garante que o agente mantém contexto (state, histórico, memória).

Recomendação de formato:

user_id:    "sf_{Contact.Id}" ou "wa_{phone_number}"
session_id: "sf_conv_{ConversationId}"

6. Autenticação e Segurança

6.1 Cloud Run → Salesforce (Named Credential)

Método Descrição Recomendação
Named Credential + JWT Bearer Salesforce assina JWT, Cloud Run valida ⭐ Recomendado
API Key no Header Header X-API-Key simples Alternativa simples
OAuth Client Credentials Salesforce obtém token OAuth do Cloud Run Mais complexo

6.2 Opção Simples: API Key

Criar uma API Key no Cloud Run e configurar no Named Credential:

// No Apex, header customizado
httpReq.setHeader('X-SF-API-Key', '{!$Credential.iFriend_Agent.ApiKey}');

No Cloud Run, validar no middleware:

# middleware de validação (a implementar)
SF_API_KEY = os.getenv("SF_API_KEY")

@app.middleware("http")
async def validate_salesforce_key(request, call_next):
    if request.url.path == "/webchat/message":
        api_key = request.headers.get("X-SF-API-Key")
        source = (await request.json()).get("metadata", {}).get("source")
        if source == "salesforce" and api_key != SF_API_KEY:
            return JSONResponse(status_code=401, content={"error": "Unauthorized"})
    return await call_next(request)

6.3 Opção Robusta: JWT do Salesforce

O Salesforce pode gerar um JWT assinado que o Cloud Run valida usando a chave pública do Salesforce Connected App:

  1. Criar Connected App no Salesforce com certificado
  2. Configurar Named Credential com JWT Bearer flow
  3. No Cloud Run, validar JWT com a public key do Salesforce

Isso se integra nativamente com o fluxo JWT existente do iFriend (jwt_context_callback).

7. Fluxo de Configuração no Salesforce (Step-by-Step)

Passo 1: Configurar Messaging for WhatsApp

  1. Setup → Messaging → Messaging Settings
  2. Criar canal WhatsApp (requer WhatsApp Business Account + número verificado)
  3. Vincular ao Omni-Channel routing
  4. Ativar Einstein Bot para o canal

Pré-requisito: A Salesforce exige que o número WhatsApp seja registrado via Meta Business Manager e conectado ao Salesforce Messaging. Se já está configurado, pule para o Passo 2.

Passo 2: Criar Named Credential

  1. Setup → Named Credentials → New Named Credential
  2. Configurar:
  3. Label: iFriend_Agent_CloudRun
  4. URL: https://<cloud-run-url>
  5. Identity Type: Named Principal
  6. Authentication Protocol: Custom Header (ou JWT)
  7. Adicionar header: Authorization: Bearer <token> ou X-SF-API-Key: <key>

Passo 3: Criar Remote Site Setting

  1. Setup → Remote Site Settings → New
  2. Remote Site URL: https://<cloud-run-url>
  3. Active: checked

Passo 4: Deploy Apex Class

  1. Deploy IFriendAgentService.cls via Salesforce CLI ou Setup → Apex Classes
  2. Criar Test Class com cobertura ≥ 75% (obrigatório Salesforce)
@IsTest
private class IFriendAgentServiceTest {
    @IsTest
    static void testConsultarAgente() {
        // Mock HTTP callout
        Test.setMock(HttpCalloutMock.class, new IFriendAgentMock());

        IFriendAgentService.AgentRequest req = new IFriendAgentService.AgentRequest();
        req.userMessage = 'Olá, quero experiências no Rio';
        req.userId = 'test_user_123';
        req.sessionId = 'test_session_456';

        List<IFriendAgentService.AgentResponse> responses =
            IFriendAgentService.consultarAgente(
                new List<IFriendAgentService.AgentRequest>{ req }
            );

        System.assertEquals(1, responses.size());
        System.assert(responses[0].success);
        System.assertNotEquals(null, responses[0].responseText);
    }

    private class IFriendAgentMock implements HttpCalloutMock {
        public HttpResponse respond(HttpRequest req) {
            HttpResponse res = new HttpResponse();
            res.setStatusCode(200);
            res.setBody('{"response":"Olá! Encontrei experiências...","session_id":"test_session_456"}');
            return res;
        }
    }
}

Passo 5: Configurar Einstein Bot

  1. Setup → Einstein Bots → Criar novo bot (ou editar existente)
  2. Criar Dialog "Roteamento Agente IA"
  3. Adicionar Apex Action → selecionar IFriendAgentService.consultarAgente
  4. Mapear variáveis de input/output
  5. Configurar loop para multi-turn (o dialog re-executa a cada mensagem)
  6. Configurar fallback para transferir a agente humano em caso de erro

Passo 6: Ativar Bot no Canal WhatsApp

  1. Einstein Bot → Channel → WhatsApp (Messaging)
  2. Publish bot

8. Adaptações Necessárias no Cloud Run

8.1 Nenhuma Alteração Obrigatória

O endpoint /webchat/message já atende ao fluxo síncrono request/response. O Salesforce receberá a resposta JSON e exibirá no WhatsApp.

8.2 Adaptações Opcionais (Recomendadas)

Adaptação Prioridade Descrição
Middleware de API Key Alta Validar X-SF-API-Key para requests do Salesforce
Adapter dedicado Média Criar SalesforceAdapter no framework de messaging (padrão existente)
Formatação WhatsApp Média Garantir que respostas usem formatação compatível com WhatsApp (Markdown limitado)
Metadata source: salesforce Baixa Já suportado; permite analytics e routing condicional
Blocks/Cards → Texto Média Converter UI blocks (cards visuais) para texto puro no canal WhatsApp

8.3 Adapter Salesforce (Opcional, Futuro)

Se quiser um adapter dedicado (como os existentes para Slack/WhatsApp/Telegram), seria:

# messaging/adapters/salesforce_adapter.py
class SalesforceAdapter(MessagingAdapter):
    """
    Adapter para integração com Salesforce Einstein Bot.
    Recebe mensagens via webhook e responde sincronamente.
    """
    @property
    def platform_name(self) -> str:
        return "salesforce"

    @property
    def webhook_path(self) -> str:
        return "/salesforce/message"

Porém, como o /webchat/message já é genérico e funcional, não é obrigatório criar adapter dedicado na Fase 1.

9. Alternativa: Salesforce como "Pass-Through" (Sem Einstein Bot)

Se preferir não usar Einstein Bot e apenas rotear mensagens diretamente:

WhatsApp → Salesforce Messaging → Flow (HTTP Callout) → Cloud Run
                                                      ↓
WhatsApp ← Salesforce Messaging ← Flow (Response)  ← Cloud Run

Usar Flow Builder com HTTP Callout Action:

  1. Criar Flow do tipo Record-Triggered ou Messaging Flow
  2. Adicionar ação HTTP Callout configurada com:
  3. URL: https://<cloud-run>/webchat/message
  4. Method: POST
  5. Headers: Content-Type: application/json
  6. Body: template com {!message}, {!userId}, etc.
  7. Parse da resposta JSON
  8. Retornar texto ao canal

Vantagem: Zero código Apex. Desvantagem: Menos controle, parsing JSON limitado.

10. Cronograma Estimado de Implementação

Fase 1 — MVP (Funcional)

Etapa Descrição
1.1 Configurar Named Credential + Remote Site no Salesforce
1.2 Desenvolver e testar Apex Class (IFriendAgentService)
1.3 Configurar Einstein Bot dialog com Apex Action
1.4 Testar fluxo end-to-end (WhatsApp → Salesforce → Cloud Run → WhatsApp)
1.5 Implementar API Key no Cloud Run para requests Salesforce

Fase 2 — Hardening

Etapa Descrição
2.1 Implementar autenticação JWT (Salesforce Connected App ↔ Cloud Run)
2.2 Tratar formatação de resposta para WhatsApp (sem cards/blocks)
2.3 Logging e monitoring (Salesforce Debug Logs + Cloud Run logs)
2.4 Tratamento de erros robusto (retry, fallback para humano)

Fase 3 — Evolução

Etapa Descrição
3.1 Adapter dedicado SalesforceAdapter no framework de messaging
3.2 Sincronizar dados CRM (Contact, Opportunity) com contexto do agente
3.3 Analytics de conversas Salesforce vs outros canais
3.4 Suporte a anexos/mídia (imagens, PDFs)

11. Riscos e Mitigações

Risco Impacto Mitigação
Timeout Apex (120s) Respostas longas do agente são cortadas Otimizar prompts; monitorar P95 latency; fallback "processando..."
Cold start Cloud Run Primeira request pode levar 5-10s extra Manter min-instances=1 no Cloud Run
Rate limit Salesforce Limites de API calls por org (e.g. 100k/dia) Monitorar; 1 callout por mensagem é eficiente
Formatação incompatível Markdown/HTML do agente não renderiza no WhatsApp Sanitizar resposta: remover HTML, simplificar Markdown
Perda de sessão Conversas perdem contexto entre mensagens Usar ConversationId consistente como session_id
Custo Cada mensagem = 1 callout + 1 Cloud Run request Baixo custo unitário; monitorar volume

12. Referências

Recurso Link
Salesforce Einstein Bots https://help.salesforce.com/s/articleView?id=sf.bots_service_intro.htm
Einstein Bot Apex Actions https://help.salesforce.com/s/articleView?id=sf.bots_service_apex.htm
Salesforce Named Credentials https://help.salesforce.com/s/articleView?id=sf.named_credentials_about.htm
Salesforce Messaging for WhatsApp https://help.salesforce.com/s/articleView?id=sf.livemessage_whatsapp_overview.htm
HTTP Callout in Flows https://help.salesforce.com/s/articleView?id=sf.flow_http_callout.htm
Apex HTTP Callout Limits https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_callouts_timeouts.htm
Cloud Run endpoint /webchat/message unified_bot.py (já implementado)
Adapter Factory (messaging) messaging/factory.py

Resumo

A integração é totalmente viável usando a infraestrutura existente:

  1. Cloud Run já expõe o endpoint /webchat/message que aceita request síncrono e retorna resposta completa — zero mudanças obrigatórias no backend.
  2. Salesforce tem mecanismos nativos (Einstein Bot + Apex Action + Named Credential) para fazer HTTP callout para serviços externos.
  3. WhatsApp já está conectado ao Salesforce via Messaging — o bot apenas roteia mensagens.
  4. O session management do Cloud Run (CloudSQL/Redis) garante contexto multi-turn usando o ConversationId do Salesforce como session_id.

A implementação mais direta é: Einstein Bot → Apex Invocable Action → HTTP POST /webchat/message → resposta JSON → exibir no WhatsApp.