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:
- Criar Connected App no Salesforce com certificado
- Configurar Named Credential com JWT Bearer flow
- 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¶
- Setup → Messaging → Messaging Settings
- Criar canal WhatsApp (requer WhatsApp Business Account + número verificado)
- Vincular ao Omni-Channel routing
- 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¶
- Setup → Named Credentials → New Named Credential
- Configurar:
- Label:
iFriend_Agent_CloudRun - URL:
https://<cloud-run-url> - Identity Type: Named Principal
- Authentication Protocol: Custom Header (ou JWT)
- Adicionar header:
Authorization: Bearer <token>ouX-SF-API-Key: <key>
Passo 3: Criar Remote Site Setting¶
- Setup → Remote Site Settings → New
- Remote Site URL:
https://<cloud-run-url> - Active: checked
Passo 4: Deploy Apex Class¶
- Deploy
IFriendAgentService.clsvia Salesforce CLI ou Setup → Apex Classes - 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¶
- Setup → Einstein Bots → Criar novo bot (ou editar existente)
- Criar Dialog "Roteamento Agente IA"
- Adicionar Apex Action → selecionar
IFriendAgentService.consultarAgente - Mapear variáveis de input/output
- Configurar loop para multi-turn (o dialog re-executa a cada mensagem)
- Configurar fallback para transferir a agente humano em caso de erro
Passo 6: Ativar Bot no Canal WhatsApp¶
- Einstein Bot → Channel → WhatsApp (Messaging)
- 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:
- Criar Flow do tipo Record-Triggered ou Messaging Flow
- Adicionar ação HTTP Callout configurada com:
- URL:
https://<cloud-run>/webchat/message - Method: POST
- Headers: Content-Type: application/json
- Body: template com {!message}, {!userId}, etc.
- Parse da resposta JSON
- 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:
- Cloud Run já expõe o endpoint
/webchat/messageque aceita request síncrono e retorna resposta completa — zero mudanças obrigatórias no backend. - Salesforce tem mecanismos nativos (Einstein Bot + Apex Action + Named Credential) para fazer HTTP callout para serviços externos.
- WhatsApp já está conectado ao Salesforce via Messaging — o bot apenas roteia mensagens.
- O session management do Cloud Run (CloudSQL/Redis) garante contexto multi-turn usando o
ConversationIddo Salesforce comosession_id.
A implementação mais direta é: Einstein Bot → Apex Invocable Action → HTTP POST /webchat/message → resposta JSON → exibir no WhatsApp.