
We can securely and seamlessly access the Salesforce Agent APIs from Lightning Web Component using Apex.
Salesforce Agent API Setup:
In order to store the Consumer Key, Consumer Secret, API Instance URL and My Domain URL, I have used Custom Metadata Type.

Apex Class:
public with sharing class ChatClientController {
public static Agentforce_Agent_Configuration__mdt objConfig = Agentforce_Agent_Configuration__mdt.getInstance( 'ServiceAgent' );
/*
* This method gets the access token from Salesforce if the Access Toke is expired or null.
* Session creation will be done if the Session Id is null.
* Message will be sent to the Agentforce Agent and the response will be returned.
* @param strToken - Access Token
* @param strLastTokenIssued - Last Token Issued
* @param strSessionId - Session Id
* @param strMessage - Message
* @return Map < String, Object > - Result Map
*/
@AuraEnabled
public static Map < String, Object > sendMessage( String strToken, String strLastTokenIssued, String strSessionId, String strMessage ) {
HttpResponse response;
Map < String, Object > resultMap = new Map < String, Object >();
if (
String.isBlank( strToken ) ||
String.isBlank( strLastTokenIssued ) ||
( ( DateTime.now().getTime() - DateTime.newInstance( Long.valueOf( strLastTokenIssued ) ).getTime() ) / 60000 ) > 25
) {
try {
response = accessToken();
if ( response.getStatusCode() == 200 && response.getBody() != null) {
Map < String, Object > jsonMap = ( Map < String, Object > )JSON.deserializeUntyped( response.getBody() );
String strAccessToken = ( String ) jsonMap.get( 'access_token' );
if ( strAccessToken != null ) {
strToken = strAccessToken;
resultMap.put( 'AccessToken', strAccessToken );
resultMap.put( 'LastTokenIssued', DateTime.now().getTime() );
} else {
resultMap.put( 'Error', 'Access Token is null' );
}
} else {
System.debug( 'Error: ' + response.getStatusCode() + ' - ' + response.getBody() );
resultMap.put( 'Error', response.getBody() );
}
} catch ( System.CalloutException e ) {
System.debug('HTTP Request failed: ' + e.getMessage());
resultMap.put( 'Error', e.getMessage() );
}
if ( !resultMap.containsKey( 'AccessToken' ) ) {
resultMap.put( 'LastTokenIssued', null );
}
}
if ( String.isBlank( strSessionId) ) {
try {
response = sessionRequest( strToken );
if ( response.getStatusCode() == 200 && response.getBody() != null) {
Map < String, Object > jsonMap = ( Map < String, Object > )JSON.deserializeUntyped( response.getBody() );
String strTempSessionId = ( String ) jsonMap.get( 'sessionId' );
if ( strTempSessionId != null ) {
resultMap.put( 'SessionId', strTempSessionId );
strSessionId = strTempSessionId;
} else {
resultMap.put( 'Error', 'Session Id is null' );
}
} else {
System.debug( 'Error: ' + response.getStatusCode() + ' - ' + response.getBody() );
resultMap.put( 'Error', response.getBody() );
}
} catch ( System.CalloutException e ) {
System.debug('HTTP Request failed: ' + e.getMessage());
resultMap.put( 'Error', e.getMessage() );
}
}
if ( !resultMap.containsKey( 'Error' ) ) {
try {
response = sendSynchronousMessage( strToken, strSessionId, strMessage );
if ( response.getStatusCode() == 200 && response.getBody() != null) {
String strReceievedMessage;
Map < String, Object > jsonMap = ( Map < String, Object > )JSON.deserializeUntyped( response.getBody() );
List < Object > messages = ( List < Object > ) jsonMap.get( 'messages' );
for ( Object objMessage : messages ) {
Map < String, Object > messageMap = ( Map < String, Object > ) objMessage;
System.debug( 'Message: ' + messageMap.get( 'message' ) );
strReceievedMessage = ( String ) messageMap.get( 'message' );
}
resultMap.put( 'Message', strReceievedMessage );
} else {
System.debug( 'Error: ' + response.getStatusCode() + ' - ' + response.getBody() );
resultMap.put( 'Error', response.getBody() );
}
} catch ( System.CalloutException e ) {
System.debug('HTTP Request failed: ' + e.getMessage());
resultMap.put( 'Error', e.getMessage() );
}
}
return resultMap;
}
/*
* This method gets the access token from Salesforce.
* @return HTTPResponse - Response
*/
public static HTTPResponse accessToken() {
HTTPResponse response;
HTTP http = new HTTP();
HTTPRequest request = new HTTPRequest();
request.setEndpoint( objConfig.My_Domain_URL__c + '/services/oauth2/token');
request.setMethod( 'POST' );
request.setHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
String strRequestBody = 'grant_type=client_credentials' +
'&client_id=' + EncodingUtil.urlEncode( objConfig.Consumer_Key__c,'UTF-8' ) +
'&client_secret=' + EncodingUtil.urlEncode( objConfig.Consumer_Secret__c, 'UTF-8' );
request.setBody( strRequestBody );
response = http.send( request );
System.debug( 'Response: ' + response.getBody() );
return response;
}
/*
* This method creates a session in Salesforce.
* @param strAccessToken - Access Token
* @return HTTPResponse - Response
*/
public static HTTPResponse sessionRequest( String strAccessToken ) {
HTTP http = new HTTP();
HTTPResponse response;
HTTPRequest request = new HTTPRequest();
request.setEndpoint( objConfig.API_Instance_URL__c + '/einstein/ai-agent/v1/agents/0XxKj000000cVuaKAE/sessions' );
request.setMethod( 'POST' );
request.setHeader( 'Content-Type', 'application/json' );
request.setHeader( 'Authorization', 'Bearer ' + strAccessToken );
UUID randomUUID = UUID.randomUUID();
SessionRequestPayload sessionPayload = new SessionRequestPayload( randomUUID.toString(), objConfig.My_Domain_URL__c );
String strRequestBody = JSON.serialize( sessionPayload );
System.debug( 'Request Body: ' + strRequestBody );
request.setBody( strRequestBody );
response = http.send( request );
System.debug( 'Response: ' + response.getBody() );
return response;
}
/*
* This method sends a message to the Agentforce Agent.
* @param strAccessToken - Access Token
* @param strSessionId - Session Id
* @param strMessage - Message
* @return HTTPResponse - Response
*/
public static HTTPResponse sendSynchronousMessage( String strAccessToken, String strSessionId, String strMessage ) {
HTTP http = new HTTP();
HTTPResponse response;
HTTPRequest request = new HTTPRequest();
request.setMethod( 'POST' );
request.setTimeout( 60000 );
request.setHeader( 'Accept', 'application/json' );
request.setHeader( 'Content-Type', 'application/json' );
request.setHeader( 'Authorization', 'Bearer ' + strAccessToken );
request.setEndpoint( objConfig.API_Instance_URL__c + '/einstein/ai-agent/v1/sessions/' + strSessionId + '/messages' );
UUID randomUUID = UUID.randomUUID();
SendMessageRequestPayload sendMsgPayload = new SendMessageRequestPayload( strMessage );
String strRequestBody = JSON.serialize( sendMsgPayload );
request.setBody( strRequestBody );
response = http.send( request );
System.debug( 'Response: ' + response.getBody() );
return response;
}
/*
* This method ends the Agentforce Agent session in Salesforce.
* @param strAccessToken - Access Token
* @param strSessionId - Session Id
* @return Map < String, Object > - Result Map
*/
@AuraEnabled
public static Map < String, Object > endSession( String strAccessToken, String strSessionId ) {
Map < String, Object > resultMap = new Map < String, Object >();
try {
HTTP http = new HTTP();
HTTPResponse response;
HTTPRequest request = new HTTPRequest();
request.setTimeout( 60000 );
request.setMethod( 'DELETE' );
request.setHeader( 'x-session-end-reason', 'UserRequest' );
request.setHeader( 'Authorization', 'Bearer ' + strAccessToken );
request.setEndpoint( objConfig.API_Instance_URL__c + '/einstein/ai-agent/v1/sessions/' + strSessionId );
response = http.send( request );
System.debug( 'Response: ' + response.getBody() );
if ( response.getStatusCode() == 200 && response.getBody() != null) {
Map < String, Object > jsonMap = ( Map < String, Object > )JSON.deserializeUntyped( response.getBody() );
List < Object > messages = ( List < Object > ) jsonMap.get( 'messages' );
for ( Object objMessage : messages ) {
Map < String, Object > messageMap = ( Map < String, Object > ) objMessage;
System.debug( 'Type: ' + messageMap.get( 'type' ) );
System.debug( 'Reason: ' + messageMap.get( 'reason' ) );
resultMap.put( 'Message', ( String ) messageMap.get( 'type' ) + ' - ' + ( String ) messageMap.get( 'reason' ));
}
} else {
System.debug( 'Error: ' + response.getStatusCode() + ' - ' + response.getBody() );
resultMap.put( 'Error', response.getBody() );
}
} catch ( System.CalloutException e ) {
System.debug('HTTP Request failed: ' + e.getMessage());
resultMap.put( 'Error', e.getMessage() );
}
return resultMap;
}
public class SessionRequestPayload {
public String externalSessionKey;
public InstanceConfig instanceConfig;
public StreamingCapabilities streamingCapabilities;
public SessionRequestPayload( String sessionKey, String orgURL ) {
this.externalSessionKey = sessionKey;
this.instanceConfig = new InstanceConfig(orgURL);
this.streamingCapabilities = new StreamingCapabilities();
}
}
public class InstanceConfig {
public String endpoint;
public InstanceConfig( String orgURL ) {
this.endpoint = orgURL;
}
}
public class StreamingCapabilities {
public List<String> chunkTypes = new List<String>();
public StreamingCapabilities() {
this.chunkTypes.add('Text');
}
}
public class SendMessageRequestPayload {
public List < String > variables;
public Message message;
public SendMessageRequestPayload( String msg ) {
this.variables = new List < String >();
this.message = new Message(msg);
}
}
public class Message {
public String text;
public String type = 'Text';
public String sequenceId = DateTime.now().getTime().toString();
public Message( String msg ) {
this.text = msg;
}
}
}
Sample Lightning Web Component:
HTML:
<template>
<div class="slds-align_absolute-center">
<div style="width: 450px;">
<lightning-card title="Chat with Agentforce Agent" icon-name="standard:live_chat" variant="narrow">
<lightning-button-icon
slot="actions"
disabled={downloadDisabled}
icon-name="utility:download"
alternative-text="Download Conversation"
onclick={downloadConversation}>
</lightning-button-icon>
<!-- Displaying spinner when sending and receiving messages -->
<template lwc:if={showSpinner}>
<lightning-spinner
alternative-text="Loading"
size="large">
</lightning-spinner>
</template>
<div class="slds-p-around_medium">
<div
class="scrollableArea"
style="height: 300px; overflow-x: hidden; overflow-y: auto;">
<lightning-formatted-text
value={consolidatedMessage}>
</lightning-formatted-text>
</div>
<lightning-textarea
type="text"
label="Message"
value={strMessage}
onchange={handleMessageChange}>
</lightning-textarea>
</div>
<!-- Displaying Buttons in the Footer -->
<div slot="footer">
<lightning-button
title="Chat"
label="Chat"
variant="brand"
onclick={handleChat}
disabled={chatDisabled}
icon-name="utility:chat"
class="slds-m-left_x-small">
</lightning-button>
<lightning-button
label="End Session"
title="End Session"
variant="destructive"
icon-name="utility:close"
onclick={handleEndSession}
disabled={endSessionDisabled}
class="slds-m-left_x-small">
</lightning-button>
</div>
</lightning-card>
</div>
</div>
</template>
JavaScript:
import { LightningElement } from 'lwc';
import endSession from '@salesforce/apex/ChatClientController.endSession';
import sendMessage from '@salesforce/apex/ChatClientController.sendMessage';
export default class ChatClient extends LightningElement {
strToken;
strMessage;
strSessionId;
strLastTokenIssued;
consolidatedMessage;
showSpinner = false;
chatDisabled = true;
downloadDisabled = true;
endSessionDisabled = true;
/*
* This method is called when Chat button is clicked
*/
handleChat() {
this.showSpinner = true;
this.chatDisabled = true;
console.log( 'Inside handleChat' );
console.log(
'strMessage: ' + this.strMessage
);
if ( this.consolidatedMessage ) {
this.consolidatedMessage += '\nYou: ' + this.strMessage;
} else {
this.consolidatedMessage = 'You: ' + this.strMessage;
}
this.consolidatedMessage += '\n' + new Date( Date.now() ).toUTCString() + '\n';
// Invoking the Apex method to send the message to the Agentforce Agent
sendMessage( {
strToken : this.strToken,
strLastTokenIssued : this.strLastTokenIssued,
strSessionId : this.strSessionId,
strMessage : this.strMessage
} )
.then( result => {
console.log( 'result is', JSON.stringify( result ) );
this.consolidatedMessage += '\nAgent: ';
if ( result.Message ) {
this.consolidatedMessage += result.Message;
} else if ( result.Error ) {
this.consolidatedMessage += result.Error;
}
this.consolidatedMessage += '\n' + new Date( Date.now() ).toUTCString() + '\n';
if ( result.LastTokenIssued ) {
this.strLastTokenIssued = result.LastTokenIssued;
}
if ( result.AccessToken ) {
this.strToken = result.AccessToken;
}
if ( result.SessionId ) {
this.strSessionId = result.SessionId;
this.endSessionDisabled = false;
}
this.template.querySelector( '.scrollableArea' ).scrollTop = this.template.querySelector( '.scrollableArea' ).scrollHeight;
this.showSpinner = false;
} )
.catch( error => {
this.showSpinner = false;
console.log( 'Error Occured', JSON.stringify( error ) );
} );
this.strMessage = '';
}
/*
* This method is called when the message is changed in the textarea
*/
handleMessageChange( event ) {
this.strMessage = event.detail.value;
if ( this.strMessage && this.strMessage.length > 0 ) {
this.chatDisabled = false;
} else {
this.chatDisabled = true;
}
}
/*
* This method is called when End Session button is clicked
*/
handleEndSession() {
this.showSpinner = true;
console.log( 'In End Session' );
this.downloadDisabled = false;
// Invoking the Apex method to end the Agentforce Agent session
endSession( { strAccessToken : this.strToken, strSessionId : this.strSessionId } )
.then( result => {
console.log( 'result is', JSON.stringify( result ) );
console.log( 'result.Message is', result.Message );
this.consolidatedMessage += '\nAgent: ';
if ( result.Message ) {
this.consolidatedMessage += result.Message;
} else if ( result.Error ) {
this.consolidatedMessage += result.Error;
}
this.consolidatedMessage += '\n' + new Date( Date.now() ).toUTCString() + '\n';
this.strSessionId = '';
this.downloadDisabled = false;
this.endSessionDisabled = true;
this.showSpinner = false;
} )
.catch( error => {
this.showSpinner = false;
console.log( 'Error Occured', JSON.stringify( error ) );
} );
}
/*
* This method is called when Download Conversation button is clicked
*/
downloadConversation() {
console.log( 'Download Start' );
this.downloadString( this.consolidatedMessage, "text/plain", "Conversation.txt" );
console.log( 'Download End' );
}
/*
* This method is called to download the conversation
*/
downloadString( text, fileType, fileName ) {
let objBlob = new Blob( [ text ], { type: fileType } );
let objAnchor = document.createElement( 'a' );
objAnchor.download = fileName;
objAnchor.href = URL.createObjectURL( objBlob );
objAnchor.dataset.downloadurl = [
fileType,
objAnchor.download,
objAnchor.href
].join( ':' );
objAnchor.style.display = "none";
objAnchor.click();
setTimeout(
function() {
URL.revokeObjectURL( objAnchor.href );
}, 1000
);
}
}
js-meta.xml:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__Tab</target>
</targets>
</LightningComponentBundle>
Output:

When the Session is ended using the “End Session” button, the download icon will be enabled. You can download the conversation transcript.