AI Apex Integrations LWC OpenAI 

Create and utilize an OpenAI Assistant through Apex Code

It’s simpler than it sounds. Seriously. Just the amount of callouts is a bit annoying.

Once you have your OpenAI assistant ready – there are blogs and articles out there that can describe better how to assistant than we do, like this Medium entry – all we need to do is to turn this OpenAI article into valid Salesforce code.

The following sequence of actions needs to be implemented:

  • Create a new thread in your assistant
  • Add your message to the thread (that you pass as parameter to the method)
  • Create a run of the assistant with optional instructions
  • Poll the run until successful
  • Retrieve the run results and extract the message

On the platform configuration side, you’d just need to make sure to:

  • have your OpenAI Subscription Key handy
  • allowlist the OpenAI endpoint under your Remote Site Settings – usually https://api.openai.com unless self-hosted
  • create an OpenAI Named Credential – this is optional but just makes the callouts handy – the code will go with a Named Credential

Now just paste and save the code below and call the method getOpenAIResponse() with the following three String parameters to return a beautiful (yet unparsed) OpenAI response:

  • assistantId: The OpenAI ID of the assistant to call.
  • userMessage: The user input for the assistant to process.
  • instructionsMessage: An optional message for the assistant run – put null if not required.
public class Gpt4Service {
    // Replace with your OpenAI subscription key
    private static final String SUBSCRIPTION_KEY = '{put your subscription key here}';
    // How many times the assistant run should be polled
    private static final Integer NUMBER_POLLS = 10; 
    // How many seconds should be skipped between two polls - make sure that NUMBER_POLLS * SLEEP_LENGTH < 120
    private static final Integer SLEEP_LENGTH = 10;

    @AuraEnabled(Cacheable=false)
    public static String getOpenAIResponse(String assistantId, String userMessage, String instructionsMessage) {
        // Instantiate a new HTTP request and set its endpoint, method, and headers
        //return response.getBody();
        // Step 2: Create a thread
        HttpRequest threadRequest = new HttpRequest();
        threadRequest.setEndpoint('callout:OpenAI/v1/threads');
        threadRequest.setMethod('POST');
        
        // Set headers
        threadRequest.setHeader('Content-Type', 'application/json');
        threadRequest.setHeader('Authorization', 'Bearer ' + SUBSCRIPTION_KEY);
        threadRequest.setHeader('OpenAI-Beta', 'assistants=v1');

        // Send the request and store the response
        Http threadHhttp = new Http();
        HttpResponse threadResponse;
        try {
            threadResponse = threadHhttp.send(threadRequest);
            // Check if the request was successful (HTTP status code 200)
            if (threadResponse.getStatusCode() >= 200 && threadResponse.getStatusCode() < 300) {
                System.debug('Success: ' + threadResponse.getBody());
                Map<String, Object> threadResponseData = (Map<String, Object>) JSON.deserializeUntyped(threadResponse.getBody());
                if(threadResponseData.containsKey('id')){
                    String threadId = String.valueOf(threadResponseData.get('id'));
                    // return threadResponse.getBody();
                    // Step 3: Add messages to thread                        
                    // Initialize HTTP objects
                    HttpRequest addMessageRequest = new HttpRequest();
                    HttpResponse addMessageResponse;
                    Http addMessageHttp = new Http();

                    // Configure the HttpRequest
                    addMessageRequest.setEndpoint('callout:OpenAI/v1/threads/' + threadId + '/messages');
                    addMessageRequest.setMethod('POST');
                    addMessageRequest.setHeader('Content-Type', 'application/json');
                    addMessageRequest.setHeader('Authorization', 'Bearer ' + SUBSCRIPTION_KEY);
                    addMessageRequest.setHeader('OpenAI-Beta', 'assistants=v1');
                    
                    // Set the request body
                    String requestBodyThread = '{' +
                    '"role": "user",' +
                    '"content": "' + userMessage + '"' +
                    '}';
                    addMessageRequest.setBody(requestBodyThread);

                    // Perform the HTTP request
                    try {
                        addMessageResponse = addMessageHttp.send(addMessageRequest);
                        if (addMessageResponse.getStatusCode() >= 200 && addMessageResponse.getStatusCode() < 300) {
                            System.debug('Success: ' + addMessageResponse.getBody());
                            //return addMessageResponse.getBody();
                            // Step 4: Create a Run
                            HttpRequest createRunRequest = new HttpRequest();
                            HttpResponse createRunResponse;
                            Http createRunHttp = new Http();

                            // Configure the HttpRequest object
                            createRunRequest.setEndpoint('callout:OpenAI/v1/threads/' + threadId + '/runs');
                            createRunRequest.setMethod('POST');
                            createRunRequest.setHeader('Authorization', 'Bearer ' + SUBSCRIPTION_KEY);
                            createRunRequest.setHeader('Content-Type', 'application/json');
                            createRunRequest.setHeader('OpenAI-Beta', 'assistants=v1');

                            // Set the JSON body for the request
                            // TODO: Update output instructions from Custom Metadata
                            String createRunRequestBody = '{' +
                                '"assistant_id": "' + assistantId + '"';
                                if(String.isNotBlank(instructionsMessage)){
                                    createRunRequestBody += (',"instructions": "' + instructionsMessage + '"');
                                } +
                            createRunRequestBody += '}';
                            createRunRequest.setBody(createRunRequestBody);

                            // Execute the HTTP request
                            try {
                                createRunResponse = createRunHttp.send(createRunRequest);
                                if (createRunResponse.getStatusCode() >= 200 && createRunResponse.getStatusCode() < 300) {
                                    System.debug('Response Body: ' + createRunResponse.getBody());
                                    Map<String, Object> createRunResponseData = (Map<String, Object>) JSON.deserializeUntyped(createRunResponse.getBody());
                                    if(createRunResponseData.containsKey('id')){
                                        String runId = String.valueOf(createRunResponseData.get('id'));
                                        // return createRunResponse.getBody();
                                        // Step 5: Poll the run
                                        Boolean runSuccessful = false;
                                        Integer countPolls = 0;
                                        while(runSuccessful == false && countPolls < NUMBER_POLLS){
                                            countPolls++;
                                            // Initialize HTTP request, response, and client objects
                                            HttpRequest pollRunRequest = new HttpRequest();
                                            HttpResponse pollRunResponse;
                                            Http pollRunHttp = new Http();
                                    
                                            // Configure the HttpRequest object
                                            pollRunRequest.setEndpoint('callout:OpenAI/v1/threads/' + threadId + '/runs/' + runId);
                                            pollRunRequest.setMethod('GET');
                                            pollRunRequest.setHeader('Content-Type', 'application/json');
                                            pollRunRequest.setHeader('Authorization', 'Bearer ' + SUBSCRIPTION_KEY);
                                            pollRunRequest.setHeader('OpenAI-Beta', 'assistants=v1');
                                            // Execute the HTTP request
                                            try {
                                                pollRunResponse = pollRunHttp.send(pollRunRequest);
                                                if (pollRunResponse.getStatusCode() >= 200 && pollRunResponse.getStatusCode() < 300) {
                                                    System.debug('Response Body: ' + pollRunResponse.getBody());
                                                    Map<String, Object> pollRunResponseData = (Map<String, Object>) JSON.deserializeUntyped(pollRunResponse.getBody());
                                                    if(pollRunResponseData.containsKey('status') && String.valueOf(pollRunResponseData.get('status')) == 'completed'){
                                                        runSuccessful = true;
                                                        // Step 6: Read Message
                                                        // Initialize the HTTP request, response, and client objects
                                                        HttpRequest listMessagesRequest = new HttpRequest();
                                                        HttpResponse listMessagesResponse;
                                                        Http listMessagesHttp = new Http();
    
                                                        // Configure the HttpRequest object
                                                        listMessagesRequest.setEndpoint('callout:OpenAI/v1/threads/' + threadId + '/messages');
                                                        listMessagesRequest.setMethod('GET');
                                                        listMessagesRequest.setHeader('Content-Type', 'application/json');
                                                        listMessagesRequest.setHeader('Authorization', 'Bearer ' + SUBSCRIPTION_KEY);
                                                        listMessagesRequest.setHeader('OpenAI-Beta', 'assistants=v1');
    
                                                        // Execute the HTTP request
                                                        try {
                                                            listMessagesResponse = listMessagesHttp.send(listMessagesRequest);
                                                            if (listMessagesResponse.getStatusCode() >= 200 && listMessagesResponse.getStatusCode() < 300) {
                                                                // Return or process the successful response
                                                                return listMessagesResponse.getBody();
                                                            } else {
                                                                // Handle Get Run Results Response Status Code Error
                                                            }
                                                        } catch(System.CalloutException e) {
                                                            // Handle Get Run Results Callout Exception
                                                        }
                                                    } else {
                                                        sleep(SLEEP_LENGTH);
                                                    }
                                                } else {
                                                    // Handle Poll Run Response Status Code Error
                                                }
                                            } catch(System.CalloutException e) {
                                                // Handle Poll Run Callout Exception
                                            }
                                        } // End while
                                    } else {
                                        // Handle No Run ID returned
                                    }
                                } else {
                                    // Handle Run Response Status Code Error
                                }
                            } catch(System.CalloutException e) {
                                // Handle Run Response Callout Exception
                            }
                        } else {
                            // Handle Instruction Response Status Code Error
                        }
                    } catch(System.CalloutException e) {
                        // Handle Instruction Callout Exception
                    }
                } else {
                    // Handle No Thread ID returned
                }
            } else {
                // Handle Thread Response Status Code Error
            }
        } catch(System.CalloutException e) {
            // Handle Thread Callout Exception
        }
        return null;
    }

    public static void sleep(Integer secs) {
        Long epochToDate = System.currentTimeMillis();
        Long epochStop = epochToDate + (secs * 1000);
    
        while (epochToDate <= epochStop) {
            epochToDate = System.currentTimeMillis();
        }
    }
}

Call the method from a LWC, wrap some frontend around it and you’ll basically have your own, functional but slightly slow (a callout might take a minute or two and has also already timed out in practice) chatbot.

You might want to optimize the process by moving out the status poll as Queueable class, if the AI response is not required synchronously.

Update, May 2024: Fancy your OpenAI Assistant analyzing your very own data instead of the web? Here’s a sequel how to upload your custom data for your OpenAI Assistant to consume.

Leave a Reply

Your email address will not be published. Required fields are marked *