Adobe AEMaaCS integration with OpenAI Assistants API / Blogs / Perficient

The OpenAI Assistants API allows you to build AI assistants within your own applications. An Assistant has instructions and can leverage models, tools, and knowledge to respond to user queries.

The Assistants API is designed to help developers build powerful AI assistants capable of performing a variety of tasks.

Different from OpenAI’s Chat Completions API, Assistants API is an agent framework. Your instruction is similar to a system prompt but is just one part of the message that includes other messaging and internal functions that are out of your control.

Assistants allow the AI, and the AI is encouraged to, make multiple calls persistently in calling for retrieval of parts of uploaded documents via internal functions, writing python code and emitting it by function to a sandbox that can run the code, and then finally emitting functions back to you in a similar fashion to chat completions.

It also has a record of user input and AI answers that make up a conversation, only allowing you to place a user question, run the thread, wait for an answer to be created, check the status, and then download the finished answer. Or you find that the AI has been waiting for you to run a tool function for it.

The AI is loaded with the maximum amount of conversation and documents that will fit in the model. The API return has no token usage statistic to show how much you will be billed.

To get more information on the Assistance APi, please check the introduction here: https://platform.openai.com/docs/assistants/overview

The API document can be found here:

https://platform.openai.com/docs/api-reference/assistants/createAssistant

 

In this blog, we customized the AEM’s Core Teaser component to help you understand how to integrate the OpenAI Assistances API with AEMaaCS together. The working demo will be like this:

screencast 2024-01-03 17-47-27

 

The AEM implementation includes the following parts:

  • A customized AEM Core Teaser component.
  • An AEM servlet to receive the request from the Teaser component dialog, and call the OSGi service, then return the response to the Teaser component dialog UI.
  • An AEM OSGi Service to access the OpenAI Assistances API
  • An OSGi Configuration to provide required configuration values during execution.
  • An AEM OSGi service that works as the HTTP Client factory. This factory class uses the values from the above OSGi configuration to access the OpenAI Assistance API.

Other related source can be found in the shared github repository.

1. Customize the AEM core Teaser Component

First, we want to customize the dialog of the Teaser component.

Under the Text tab of the dialog user interface, if the author de-selects the checkbox for ‘Get description from linked page’, which means the description text will be customized here, but not be retrieved from the blueprint page. We can add a text area to let the author input the instruction/prompt here. We also added a button under it, when the author clicks the button, a request will be sent to the AEM servlet to access the Assistances API.  The RTE under the button is used to display the response from the Assistances API. The author can make changes to the RTE and click the ‘Done’ button to update the Teaser component.

The new Teaser Dialog is like below:

Customized Teaser Dialog

Customized Teaser Dialog UI

Below is the updated source code on the Core Teaser dialog:

 

<?xml version="1.0" encoding="UTF-8"?>
...
                                            <descriptionFromLinkedPage
                                                    jcr:primaryType="nt:unstructured"
                                                    sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
                                                    checked="Booleantrue"
                                                    fieldDescription="When checked, populate the description with the linked page's description."
                                                    name="./descriptionFromPage"
                                                    text="Get description from linked page"
                                                    uncheckedValue="Booleanfalse"
                                                    value="Booleantrue"/>
                                            <descriptionGroup
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/coral/foundation/include"
                                                path="/mnt/overlay/openai-sample/components/commons/editor/dialog/chatgpt-rte-2">
                                            </descriptionGroup>
                                            <id
                                                jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                                fieldDescription="HTML ID attribute to apply to the component."
                                                fieldLabel="ID"
                                                name="./id"
                                                validation="html-unique-id-validator"/>
                                        </items>
                                    </column>
                                </items>
                            </columns>
                        </items>
                    </text>
...

From line 12-15, the dialog is reusing a common widgets definition under ‘/mnt/overlay/openai-sample/components/commons/editor/dialog/chatgpt-rte-2’.  This is the source code of it:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"
    jcr:title="ChatGPT RTE"
    sling:resourceType="granite/ui/components/coral/foundation/well">
    <items jcr:primaryType="nt:unstructured">
        <prompt
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/form/textarea"
                fieldDescription="ChatGPT prompt: Tags, keyworks, phrases, etc..."
                emptyTextstring="Enter prompt for ChatGPT here"
                fieldLabel="Promps for ChatGPT"
                name="./txt_gptPrompt"
                rows="Long5"/>
        <gptButton jcr:primaryType="nt:unstructured"
                   name="./btnGroup"
                   required="Booleanfalse"
                   selectionMode="single"
                   sling:resourceType="granite/ui/components/coral/foundation/form/buttongroup">

            <items jcr:primaryType="nt:unstructured">
                <default jcr:primaryType="nt:unstructured"
                         name="./callAPI"
                         text="Generate"
                         value="false"
                         checked="Booleanfalse"
                         granite:class="chatGPTButton"
                         cq-msm-lockable="default"/>
            </items>
        </gptButton>
        <description
                jcr:primaryType="nt:unstructured"
                sling:resourceType="cq/gui/components/authoring/dialog/richtext"
                fieldDescription="A description to display as the subheadline for the teaser."
                fieldLabel="Description"
                name="./jcr:description"
                useFixedInlineToolbar="Booleantrue">
            <rtePlugins jcr:primaryType="nt:unstructured">
                <format
                        jcr:primaryType="nt:unstructured"
                        features="bold,italic"/>
                <justify
                        jcr:primaryType="nt:unstructured"
                        features="-"/>
                <links
                        jcr:primaryType="nt:unstructured"
                        features="modifylink,unlink"/>
                <lists
                        jcr:primaryType="nt:unstructured"
                        features="*"/>
                <misctools jcr:primaryType="nt:unstructured">
                    <specialCharsConfig jcr:primaryType="nt:unstructured">
                        <chars jcr:primaryType="nt:unstructured">
                            <default_copyright
                                    jcr:primaryType="nt:unstructured"
                                    entity="&amp;copy;"
                                    name="copyright"/>
                            <default_euro
                                    jcr:primaryType="nt:unstructured"
                                    entity="&amp;euro;"
                                    name="euro"/>
                            <default_registered
                                    jcr:primaryType="nt:unstructured"
                                    entity="&amp;reg;"
                                    name="registered"/>
                            <default_trademark
                                    jcr:primaryType="nt:unstructured"
                                    entity="&amp;trade;"
                                    name="trademark"/>
                        </chars>
                    </specialCharsConfig>
                </misctools>
                <paraformat
                        jcr:primaryType="nt:unstructured"
                        features="*">
                    <formats jcr:primaryType="nt:unstructured">
                        <default_p
                                jcr:primaryType="nt:unstructured"
                                description="Paragraph"
                                tag="p"/>
                        <default_h1
                                jcr:primaryType="nt:unstructured"
                                description="Heading 1"
                                tag="h1"/>
                        <default_h2
                                jcr:primaryType="nt:unstructured"
                                description="Heading 2"
                                tag="h2"/>
                        <default_h3
                                jcr:primaryType="nt:unstructured"
                                description="Heading 3"
                                tag="h3"/>
                        <default_h4
                                jcr:primaryType="nt:unstructured"
                                description="Heading 4"
                                tag="h4"/>
                        <default_h5
                                jcr:primaryType="nt:unstructured"
                                description="Heading 5"
                                tag="h5"/>
                        <default_h6
                                jcr:primaryType="nt:unstructured"
                                description="Heading 6"
                                tag="h6"/>
                        <default_blockquote
                                jcr:primaryType="nt:unstructured"
                                description="Quote"
                                tag="blockquote"/>
                        <default_pre
                                jcr:primaryType="nt:unstructured"
                                description="Preformatted"
                                tag="pre"/>
                    </formats>
                </paraformat>
                <table
                        jcr:primaryType="nt:unstructured"
                        features="-">
                    <hiddenHeaderConfig
                            jcr:primaryType="nt:unstructured"
                            hiddenHeaderClassName="cq-wcm-foundation-aria-visuallyhidden"
                            hiddenHeaderEditingCSS="cq-RichText-hiddenHeader--editing"/>
                </table>
                <tracklinks
                        jcr:primaryType="nt:unstructured"
                        features="*"/>
            </rtePlugins>
            <uiSettings jcr:primaryType="nt:unstructured">
                <cui jcr:primaryType="nt:unstructured">
                    <inline
                            jcr:primaryType="nt:unstructured"
                            toolbar="[format#bold,format#italic,format#underline,#justify,#lists,links#modifylink,links#unlink,#paraformat]">
                        <popovers jcr:primaryType="nt:unstructured">
                            <justify
                                    jcr:primaryType="nt:unstructured"
                                    items="[justify#justifyleft,justify#justifycenter,justify#justifyright]"
                                    ref="justify"/>
                            <lists
                                    jcr:primaryType="nt:unstructured"
                                    items="[lists#unordered,lists#ordered,lists#outdent,lists#indent]"
                                    ref="lists"/>
                            <paraformat
                                    jcr:primaryType="nt:unstructured"
                                    items="paraformat:getFormats:paraformat-pulldown"
                                    ref="paraformat"/>
                        </popovers>
                    </inline>
                    <dialogFullScreen
                            jcr:primaryType="nt:unstructured"
                            toolbar="[format#bold,format#italic,format#underline,justify#justifyleft,justify#justifycenter,justify#justifyright,lists#unordered,lists#ordered,lists#outdent,lists#indent,links#modifylink,links#unlink,table#createoredit,#paraformat,image#imageProps]">
                        <popovers jcr:primaryType="nt:unstructured">
                            <paraformat
                                    jcr:primaryType="nt:unstructured"
                                    items="paraformat:getFormats:paraformat-pulldown"
                                    ref="paraformat"/>
                        </popovers>
                    </dialogFullScreen>
                    <tableEditOptions
                            jcr:primaryType="nt:unstructured"
                            toolbar="[table#insertcolumn-before,table#insertcolumn-after,table#removecolumn,-,table#insertrow-before,table#insertrow-after,table#removerow,-,table#mergecells-right,table#mergecells-down,table#mergecells,table#splitcell-horizontal,table#splitcell-vertical,-,table#selectrow,table#selectcolumn,-,table#ensureparagraph,-,table#modifytableandcell,table#removetable,-,undo#undo,undo#redo,-,table#exitTableEditing,-]"/>
                </cui>
            </uiSettings>
        </description>
    </items>
</jcr:root>

To manipulate the behaviors of the ‘Generate’ button and the Description RTE, we need to create customized JavaScript in a Client Library and let the teaser component dialog use it.

In the new Client Library definition, we give the categories name which is called ‘core.wcm.components.teaser.v2.gpt.editor3’

Crxde Lite 2024 01 03 14 17 31

 

In the dialog properties, we add a new property called ‘extraClientlibs’. The value of this property is the category name of the new client library. When the Teaser dialog is open, the JavaScript in the client library will be loaded automatically.

 

Crxde Lite 2024 01 03 14 12 16

 

Here is the JavaScript file created in the Client Library:

 

(function($, Granite) {
    "use strict";

    var dialogContentSelector = ".cmp-teaser__editor";
    var actionsMultifieldSelector = ".cmp-teaser__editor-multifield_actions";
    var titleCheckboxSelector="coral-checkbox[name="./titleFromPage"]";
    var titleTextfieldSelector="input[name="./jcr:title"]";
    var descriptionCheckboxSelector="coral-checkbox[name="./descriptionFromPage"]";
    var descriptionCheckboxChatGPT = 'coral-checkbox[name="./descriptionFromChatGPT"]';
    var descriptionTextfieldSelector=".cq-RichText-editable[name="./jcr:description"]";
    var titleTypeSelectElementSelector = "coral-select[name="./titleType"]";
    var linkURLSelector="[name="./linkURL"]";
    var chatGptDisplayGroupSelector = ".chatGPTGroup";
    var CheckboxTextfieldTuple = window.CQ.CoreComponents.CheckboxTextfieldTuple.v1;
    var titleTuple;
    var descriptionTuple;
    var linkURL;
    var gptButton = ".chatGPTButton";
    var gptPromptTextSelector="textarea[name="./txt_gptPrompt"]";


    $(document).on("dialog-loaded", function(e) {
        var $dialog = e.dialog;
        var $dialogContent = $dialog.find(dialogContentSelector);
        var dialogContent = $dialogContent.length > 0 ? $dialogContent[0] : undefined;

        if (dialogContent) 
            var $descriptionTextfield = $(descriptionTextfieldSelector);
            if ($descriptionTextfield.length) 
                if (!$descriptionTextfield[0].hasAttribute("aria-labelledby")) 
                    associateDescriptionTextFieldWithLabel($descriptionTextfield[0]);
                
                var rteInstance = $descriptionTextfield.data("rteinstance");
                // wait for the description textfield rich text editor to signal start before initializing.
                // Ensures that any state adjustments made here will not be overridden.
                if (rteInstance && rteInstance.isActive) 
                    init(e, $dialog, $dialogContent, dialogContent);
                 else 
                    $descriptionTextfield.on("editing-start", function() 
                        init(e, $dialog, $dialogContent, dialogContent);
                    );
                
             else 
                // init without description field
                init(e, $dialog, $dialogContent, dialogContent);
            
            manageTitleTypeSelectDropdownFieldVisibility(dialogContent);
        
    });

    // Initialize all fields once both the dialog and the description textfield RTE have loaded
    function init(e, $dialog, $dialogContent, dialogContent) {
        titleTuple = new CheckboxTextfieldTuple(dialogContent, titleCheckboxSelector, titleTextfieldSelector, false);
        descriptionTuple = new CheckboxTextfieldTuple(dialogContent, descriptionCheckboxSelector, descriptionTextfieldSelector, true);
        retrievePageInfo($dialogContent);

        var $linkURLField = $dialogContent.find(linkURLSelector);
        if ($linkURLField.length) 
            linkURL = $linkURLField.adaptTo("foundation-field").getValue();
            $linkURLField.on("change", function() 
                linkURL = $linkURLField.adaptTo("foundation-field").getValue();
                retrievePageInfo($dialogContent);
            );
        

        var $actionsMultifield = $dialogContent.find(actionsMultifieldSelector);
        $actionsMultifield.on("change", function(event) {
            var $target = $(event.target);
            if ($target.is("foundation-autocomplete")) 
                updateText($target);
             else if ($target.is("coral-multifield")) 
                var $first = $(event.target.items.first());
                if (event.target.items.length === 1 && $first.is("coral-multifield-item")) 
                    var $input = $first.find(".cmp-teaser__editor-actionField-linkUrl");
                    if ($input.is("foundation-autocomplete")) 
                        var value = $linkURLField.adaptTo("foundation-field").getValue();
                        if (!$input.val() && value) 
                            $input.val(value);
                            updateText($input);
                        
                    
                
            
            retrievePageInfo($dialogContent);
        });

        //If get description from linked page: Unselect chatGPT checkbox and disable it,
        var $chatGPTChkBox = $(descriptionCheckboxChatGPT);
        $chatGPTChkBox.change(function() 
            if(this.checked) 
                $(chatGptDisplayGroupSelector).toggleClass('hide', false);
            else
                $(chatGptDisplayGroupSelector).toggleClass('hide', true);
            
        );

        //Show hide ChatGPTGroup components when click checkbox
        var $fromLinkChkBox = $(descriptionCheckboxSelector);
        $fromLinkChkBox.change(function() 
            if(this.checked) 
                $chatGPTChkBox.attr("disabled", true);
                $chatGPTChkBox.prop('checked', false);
                $(chatGptDisplayGroupSelector).toggleClass('hide', true);
            else
                $chatGPTChkBox.removeAttr("disabled");
            
        );

        //Call ChatGPT API when click button
        $(gptButton).on("click", function(event) 
            console.info("Calling ChatGPT api v3...");
            updateDescriptionWithChatGPT($dialogContent);
        );

    }


    function retrievePageInfo(dialogContent)  linkURL === "") 
            url = dialogContent.find('.cmp-teaser__editor-multifield_actions [data-cmp-teaser-v2-dialog-edit-hook="actionLink"]').val();
         else 
            url = linkURL;
        
        // get the info from the current page in case no link is provided.
        if ((url === undefined 

    function updateText(target) {
        var url = target.val();
        if (url && url.startsWith("/")) 
            var textField = target.parents("coral-multifield-item").find('[data-cmp-teaser-v2-dialog-edit-hook="actionTitle"]');
            if (textField && !textField.val()) 
                $.ajax(
                    url: url + "/_jcr_content.json"
                ).done(function(data) 
                    if (data) 
                        textField.val(data["jcr:title"]);
                    
                );
            
        
    }

    function associateDescriptionTextFieldWithLabel(descriptionTextfieldElement) 
        var richTextContainer = document.querySelector(".cq-RichText.richtext-container");
        if (richTextContainer) 
            var richTextContainerParent = richTextContainer.parentNode;
            var descriptionLabel = richTextContainerParent.querySelector("label.coral-Form-fieldlabel");
            if (descriptionLabel) 
                descriptionTextfieldElement.setAttribute("aria-labelledby", descriptionLabel.id);
            
        
    

    /**
     * Hides the title type select dropdown field if there's only one allowed heading element defined in a policy
     *
     * @param HTMLElement dialogContent The dialog content
     */
    function manageTitleTypeSelectDropdownFieldVisibility(dialogContent) 
        var titleTypeElement = dialogContent.querySelector(titleTypeSelectElementSelector);
        if (titleTypeElement) 
            Coral.commons.ready(titleTypeElement, function(element) 
                var titleTypeElementToggleable = $(element.parentNode).adaptTo("foundation-toggleable");
                var itemCount = element.items.getAll().length;
                if (itemCount < 2) 
                    titleTypeElementToggleable.hide();
                
            );
        
    


    function updateDescriptionWithChatGPT(dialogContent)

        var prompt = getPromptPhase();
        var urlChatGPTCall = "/bin/assistantServlet" + "?content=" + prompt + "&role=user";
        console.log("urlChatGPTCall = " + urlChatGPTCall);
        //For description
        return $.ajax(
            url: urlChatGPTCall
        ).done(function(data) 
            if (data) 
                data = JSON.parse(data);
                console.info("------ data = " + data);
                var chatGPTResponse = data.answer;
                console.info("------ chatGPTResponse = " + chatGPTResponse);

                var $descriptionTextfield = $(descriptionTextfieldSelector);
                if ($descriptionTextfield.length) 
                    console.log("I am here  ----- 1")
                    $descriptionTextfield.attr("data-previous-value","<p>"+chatGPTResponse+"</p>");
                    console.log("I am here  ----- 2")
                    $descriptionTextfield.html("<p>"+chatGPTResponse+"</p>");
                
            
        );
    

    function getPromptPhase()
        console.log("content = " + $(gptPromptTextSelector).val());
        return $(gptPromptTextSelector).val();
    


})(jQuery, Granite);

When the author clicks the ‘Generate’ button in the dialog UI, the function updateDescriptionWithChatGPT() in JavaScript will be called.  This function will call the AEM servlet with endpoint ‘/bin/assitantServlet’.  Two parameters will be sent with the request:

  • content: the prompt text which is the content of the text area in dialog
  • role: role value which is required by the Assistants API. Default value is ‘user’

2. An AEM servlet to receive the request from the Teaser component dialog

package com.perficient.aem.sample.openai.core.servlets;

import com.perficient.aem.sample.openai.core.services.ChatGPTAPIService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.auth.core.AuthConstants;
import org.jetbrains.annotations.NotNull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Slf4j
@Component(service =  Servlet.class , property = 
        "sling.servlet.paths=" + ChatGPTAssistantServlet.RESOURCE_PATH,
        "sling.servlet.methods=GET",
        AuthConstants.AUTH_REQUIREMENTS + "=-"+ ChatGPTAssistantServlet.RESOURCE_PATH)
public class ChatGPTAssistantServlet extends SlingSafeMethodsServlet {

    private static final long serialVersionUID = 1L;
    static final String RESOURCE_PATH = "/bin/assistantServlet";
    static final String CONTENT = "content";
    static final String ROLE = "role";

    @Reference
    private ChatGPTAPIService apiService;

    HttpSession session;

    @Override
    protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) throws ServletException, IOException 
        String content = request.getParameter(CONTENT);
        String role = request.getParameter(ROLE);
        JSONObject jsonObject = new JSONObject();

        try  StringUtils.isEmpty(sessionThreadId))
                jsonObject = createNewThreadRun(role,content);
            
            else
                //A threadId exist in session: retrieve the thread
                String respThreadId = apiService.retrieveThread(sessionThreadId);
                if(respThreadId != null && respThreadId.equalsIgnoreCase(sessionThreadId))
                    //This is a valid thread.
                    jsonObject = addNewMesssageOnThreadThenRun(respThreadId, role, content);
                
                else
                    //Thread not exist anymore, need work as a new request:
                    jsonObject = createNewThreadRun(role,content);
                


            
         catch (JSONException e) 
            log.error(e.getMessage());
        
        response.setContentType("text/html; charset=UTF-8");
        response.getWriter().print(jsonObject);

    

    private JSONObject createNewThreadRun(String role, String content)
        JSONObject jsonObject = new JSONObject();
        String result = apiService.creaetThreadAndRun(role, content);
        JSONObject resultJson = new JSONObject(result);
        String run_Id = resultJson.getString("id");
        String thread_id = resultJson.getString("thread_id");

        //Add thread to session
        session.setAttribute("thread_id", thread_id);
        jsonObject = retrieveRunAndGetAnswer(thread_id, run_Id);

        return jsonObject;
    

    private JSONObject addNewMesssageOnThreadThenRun(String threadId, String role, String content)
        JSONObject jsonObject = new JSONObject();
        String messageId =  apiService.createMessage(threadId,role,content);
        if(messageId != null)
            //Message is created. create a run
            String runId = apiService.createRun(threadId);
            //retrieveRun and get answers
            if(runId != null)
                jsonObject = retrieveRunAndGetAnswer(threadId, runId);
            
        
        return jsonObject;

    


    private JSONObject retrieveRunAndGetAnswer(String thread_id, String run_Id)

        JSONObject jsonObject = new JSONObject();
        String allAnswsers = "";
        //Retrieve Run to check status
        String status = apiService.retrieveRun(thread_id, run_Id);
        if(status.equalsIgnoreCase("completed"))
            //Run completed, need list messages
            String listMessageResponse = apiService.listMessages(thread_id);
            JSONObject listMessageResponseJson = new JSONObject(listMessageResponse);
            String lastMessage_id = listMessageResponseJson.getString("last_id");
            JSONArray messages =  listMessageResponseJson.getJSONArray("data");
            for(int i=0; i<messages.length(); i++)
                JSONObject aMessage = (JSONObject)messages.get(i);
                if(aMessage.getString("role").equalsIgnoreCase("assistant"))
                    //This is the answer?
                    JSONArray contentArray = aMessage.getJSONArray("content");
                    JSONObject aContent = (JSONObject)contentArray.get(0);
                    allAnswsers += aContent.getJSONObject("text").getString("value") + "\n";
                
            

            jsonObject.put("answer", allAnswsers);
        

        return jsonObject;
    
}

Check the line 50-63:

The java code is using the HttpSession to save the Assitants thread Id. If the HttpSession does not exist(or the HttpSession is expired), or the thread Id is not valid anymore in OpanAI Assistants, then the createNewThreadRun() function will be called, otherwise we will reuse the thread and add new message on it: the function addNewMessageOnThreadThenRun() will be called.

 

3. An AEM OSGi Service to access the OpenAI Assistances API

package com.perficient.aem.sample.openai.core.services.impl;

import com.perficient.aem.sample.openai.core.bean.CreateThreadRun;
import com.perficient.aem.sample.openai.core.bean.Message;
import com.perficient.aem.sample.openai.core.bean.SummaryBean;
import com.perficient.aem.sample.openai.core.services.ChatGPTAPIService;
import com.perficient.aem.sample.openai.core.services.ChatGptHttpClientFactory;
import com.perficient.aem.sample.openai.core.services.JSONConverter;
import com.perficient.aem.sample.openai.core.services.config.ChatGptHttpClientFactoryConfig;
import com.perficient.aem.sample.openai.core.utils.StringObjectResponseHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.entity.ContentType;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import java.io.IOException;
import java.util.concurrent.*;

@Slf4j
@Component(service = ChatGPTAPIService.class)
public class ChatGptAPIServiceImpl implements ChatGPTAPIService {

    private static final StringObjectResponseHandler HANDLER = new StringObjectResponseHandler();

    @Reference
    private ChatGptHttpClientFactory httpClientFactory;

    @Reference
    private JSONConverter jsonConverter;

    //1. Create Thread and Run (When session not exist)
    //POST   https://api.openai.com/v1/threads/runs
    @Override
    public String creaetThreadAndRun(String role, String content) 
        String responseString = StringUtils.EMPTY;
        try 
            ChatGptHttpClientFactoryConfig config =  httpClientFactory.getConfig();
            String assistantId= config.assistantId();
            String bodyString = generateMessage_createThreadRun(assistantId, content, role);

            responseString = httpClientFactory.getExecutor()
                    .execute(httpClientFactory.postCreateThreadRun().bodyString(bodyString, ContentType.APPLICATION_JSON))
                    .handleResponse(HANDLER);
         catch (IOException e) 
            log.error("Error occured while create thread and run ", e.getMessage());
        
        log.debug("creaetThreadAndRun: ", responseString);
        return responseString;
    


    //2. list Message
    //GET  https://api.openai.com/v1/threads/thread_id/messages
    @Override
    public String listMessages(String threadId) 
        String responseString = StringUtils.EMPTY;
        try 
            ChatGptHttpClientFactoryConfig config =  httpClientFactory.getConfig();
            String assistantId= config.assistantId();

            responseString = httpClientFactory.getExecutor()
                    .execute(httpClientFactory.listMessages(threadId))
                    .handleResponse(HANDLER);
         catch (IOException e) 
            log.error("Error occured while list messages ", e.getMessage());
        
        log.debug("listMessages: ", responseString);
        return responseString;
    

    //3. Create a message
    //POST   https://api.openai.com/v1/threads/thread_id/messages
    @Override
    public String createMessage(String threadId,String role, String content) 
        String responseString = StringUtils.EMPTY;
        try 

            String bodyString = getCreateMessageBody(role, content);

            responseString = httpClientFactory.getExecutor()
                    .execute(httpClientFactory.postCreateMessage(threadId).bodyString(bodyString, ContentType.APPLICATION_JSON))
                    .handleResponse(HANDLER);
         catch (IOException e) 
            log.error("Error occured while create Message ", e.getMessage());
        
        log.debug("creaetThreadAndRun: ", responseString);
        JSONObject jsonObject = new JSONObject(responseString);
        String messageId = jsonObject.getString("id"); //?completed
        return messageId;
    

    //4. Create a Run
    //POST   https://api.openai.com/v1/threads/thread_id/runs
    @Override
    public String createRun(String threadId) 
        String responseString = StringUtils.EMPTY;
        try 
            ChatGptHttpClientFactoryConfig config =  httpClientFactory.getConfig();
            String assistantId= config.assistantId();
            String bodyString = getCreateRunBody(assistantId);

            responseString = httpClientFactory.getExecutor()
                    .execute(httpClientFactory.postCreateRun(threadId).bodyString(bodyString, ContentType.APPLICATION_JSON))
                    .handleResponse(HANDLER);
         catch (IOException e) 
            log.error("Error occured while run Assistant ", e.getMessage());
        
        log.debug("creaetThreadAndRun: ", responseString);
        JSONObject jsonObject = new JSONObject(responseString);
        String respRunId = jsonObject.getString("id"); //thread id
        return respRunId;
    

    //5. Check Run
    //GET  https://api.openai.com/v1/threads/thread_id/runs/run_id
    @Override
    public String retrieveRun(String threadId, String runId) 

        int max_count = 10;
        String status = "";
        try
            ScheduleGetRun scheduleGetRun = new ScheduleGetRun(httpClientFactory, threadId, runId);
            ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
            for(int i=0; i<max_count;i++)
                ScheduledFuture<String> future = scheduledExecutorService.schedule(scheduleGetRun, 2, TimeUnit.SECONDS);
                status = future.get();
                if(status.equalsIgnoreCase("completed"))
                    break;
                
            
         catch (Exception e) 
            log.error("Error occured while list messages ", e.getMessage());
        
        return status;
    

    private class ScheduleGetRun implements Callable<String> 
        ChatGptHttpClientFactory httpClientFactory;
        String runId;
        String threadId;

        public ScheduleGetRun(ChatGptHttpClientFactory httpClientFactory, String threadId, String runId) 
            this.httpClientFactory = httpClientFactory;
            this.runId = runId;
            this.threadId = threadId;
        

        @Override
        public String call() throws Exception 
            String responseString = StringUtils.EMPTY;
            try 
                responseString = httpClientFactory.getExecutor()
                        .execute(httpClientFactory.retrieveRun(threadId, runId))
                        .handleResponse(HANDLER);
             catch (IOException e) 
                log.error("Error occured while list messages ", e.getMessage());
            
            JSONObject jsonObject = new JSONObject(responseString);
            String status = jsonObject.getString("status"); //?completed

            return status;
        
    

    //6. Retrive thread
    //GET  https://api.openai.com/v1/threads/thread_id
    @Override
    public String retrieveThread(String threadId) 
        String responseString = StringUtils.EMPTY;
        try 
            responseString = httpClientFactory.getExecutor()
                    .execute(httpClientFactory.retrieveThread(threadId))
                    .handleResponse(HANDLER);
         catch (IOException e) 
            log.error("Error occured while list messages ", e.getMessage());
        
        log.debug("retrieveThread: ", responseString);
        JSONObject jsonObject = new JSONObject(responseString);
        String respThreadId = jsonObject.getString("id"); //thread id
        return respThreadId;
    

     //-----------------------------------
    //Functions to generate request body
    //----------------------------------


    //Generate Prompt for Complete API
    private String generatePrompt(String bodyText, int maxTokens) 
        SummaryBean bodyBean = new SummaryBean();
        if(maxTokens != 0) 
            bodyBean.setMaxTokens(maxTokens);
        
        bodyBean.setPrompt(bodyText);
        return jsonConverter.convertToJsonString(bodyBean);
    

    //Generate body for Assistant API - Generate thread and run
    private String generateMessage_createThreadRun(String assistantId, String content, String role) 
        CreateThreadRun body = new CreateThreadRun(assistantId,content,role);
        return jsonConverter.convertToJsonString(body);
    

    private String getCreateMessageBody(String role, String content)
        Message message = new Message();
        message.setContent(content);
        message.setRole(role);
        return jsonConverter.convertToJsonString(message);
    

    private String getCreateRunBody(String assistantId)
        return "\"assistant_id\":\"" + assistantId + "\"";
    
}

Above service class follow the logic of Assitants API concepts:

  1. Create an Assistants in the API by defining its custom instructions and picking a model. In our demo, we assume the Assiants was already created, and the Assistant Id was provided to developers.
  2. Create a Thread on the assitant when a user starts a conversation.
  3. Add Messages to the Thread as the user ask questions.
  4. Run the Assistant on the Thread to trigger responses. This automatically calls the relevant tools.

Be careful about the highlighted line 119-137:  Since the Assistants API may take time to generate anwsers for questions, we are checking the processing status in every 2 seconds by using the scheduled execustor. Maxumum is 10 times, which means we expect to get answer in 20 seconds. Otherwise we will get timeout error.

4. An OSGi Configuration to provide required configuration values during execution

In this demo, we are using AEM OSGi configuration to provide data for the program.

Here is the source code of the Java class for the configuration definition:

package com.perficient.aem.sample.openai.core.services.config;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(name = "ChatGPT API Client Configuration", description = "ChatGPT Client Configuration")
public @interface ChatGptHttpClientFactoryConfig 

    @AttributeDefinition(name = "API Host Name", description = "API host name, e.g. https://example.com", type = AttributeType.STRING)
    String apiHostName() default "https://api.openai.com";

    @AttributeDefinition(name = "'Completion' API URI Type Path", description = "API URI type path, e.g. /v1/engines/davinci/completions", type = AttributeType.STRING)
    String uriType() default "/v1/engines/davinci/completions";

    @AttributeDefinition(name = "API Key", description = "Chat GPT API Key", type = AttributeType.STRING)
    String apiKey() default "";

    @AttributeDefinition(name = "Assistant ID", description = "Assistant ID", type = AttributeType.STRING)
    String assistantId() default "";

    @AttributeDefinition(name = "Relaxed SSL", description = "Defines if self-certified certificates should be allowed to SSL transport", type = AttributeType.BOOLEAN)
    boolean relaxedSSL() default true;

    @AttributeDefinition(name = "Maximum number of total open connections", description = "Set maximum number of total open connections, default 5", type = AttributeType.INTEGER)
    int maxTotalOpenConnections() default 4;

    @AttributeDefinition(name = "Maximum number of concurrent connections per route", description = "Set the maximum number of concurrent connections per route, default 5", type = AttributeType.INTEGER)
    int maxConcurrentConnectionPerRoute() default 2;

    @AttributeDefinition(name = "Default Keep alive connection in seconds", description = "Default Keep alive connection in seconds, default value is 1", type = AttributeType.LONG)
    int defaultKeepAliveconnection() default 15;

    @AttributeDefinition(name = "Default connection timeout in seconds", description = "Default connection timout in seconds, default value is 30", type = AttributeType.LONG)
    long defaultConnectionTimeout() default 30;

    @AttributeDefinition(name = "Default socket timeout in seconds", description = "Default socket timeout in seconds, default value is 30", type = AttributeType.LONG)
    long defaultSocketTimeout() default 30;

    @AttributeDefinition(name = "Default connection request timeout in seconds", description = "Default connection request timeout in seconds, default value is 30", type = AttributeType.LONG)
    long defaultConnectionRequestTimeout() default 30;


The com.perficient.aem.sample.openai.core.services.impl.ChatGptHttpClientFactoryImpl~openai-sample.cfg.json configuration file provides the values for each configuration variable in the author runmode:

  "apiHostName": "https://api.openai.com",
  "uriType": "/v1/engines/davinci/completions",
  "apiKey": "XXXXXXXXXXXXXXX",
  "assistantId": "asst_XXXXXXXXXXXX",
  "relaxedSSL": true,
  "maxTotalOpenConnections": 4,
  "maxConcurrentConnectionPerRoute": 2,
  "defaultKeepAliveconnection": 15,
  "defaultConnectionTimeout": 30,
  "defaultSocketTimeout": 30,
  "defaultConnectionRequestTimeout": 30

You can get the apiKey value (https://platform.openai.com/api-keys) and assistantId (https://platform.openai.com/assistants) value from your own OpenAI account settings.

You can check the configuration values from the AEM configuration manager http://localhost:4502/system/console/configMgr

Adobe Experience Manager Web Console Configuration 2024 01 03 16 53 03(1)

 

5. An AEM OSGi service that works as the HTTP Client factory.

In this demo, we are using Apache HTTP Client in our HTTP Client factory to send requests to OpenAI Assitants API. Here is the source code:

 

package com.perficient.aem.sample.openai.core.services.impl;

import com.perficient.aem.sample.openai.core.services.ChatGptHttpClientFactory;
import com.perficient.aem.sample.openai.core.services.config.ChatGptHttpClientFactoryConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.osgi.services.HttpClientBuilderFactory;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.osgi.service.component.annotations.*;
import org.osgi.service.metatype.annotations.Designate;

import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Implementation of @@link ChatGptHttpClientFactory.
 * <p>
 * HttpClientFactory provides service to handle API connection and executor.
 */
@Slf4j
@Component(service = ChatGptHttpClientFactory.class)
@Designate(ocd = ChatGptHttpClientFactoryConfig.class, factory = true)
public class ChatGptHttpClientFactoryImpl implements ChatGptHttpClientFactory {

    private Executor executor;
    private String baseUrl;
    private CloseableHttpClient httpClient;
    private ChatGptHttpClientFactoryConfig config;

    @Reference
    private HttpClientBuilderFactory httpClientBuilderFactory;

    @Activate
    @Modified
    protected void activate(ChatGptHttpClientFactoryConfig config) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException 
        log.info("########### OSGi Configs Start ###############");
        log.info("API Host Name : ", config.apiHostName());
        log.info("URI Type: ", config.uriType());
        log.info("########### OSGi Configs End ###############");
        closeHttpConnection();
        this.config = config;
        if (this.config.apiHostName() == null) 
            log.debug("Configuration is not valid. Both hostname is mandatory.");
            throw new IllegalArgumentException("Configuration is not valid. Both hostname is mandatory.");
        
        this.baseUrl = StringUtils.join(this.config.apiHostName(), this.config.uriType());
        initExecutor();
    

    private void initExecutor() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException 
        PoolingHttpClientConnectionManager connMgr = null;
        RequestConfig requestConfig = initRequestConfig();
        HttpClientBuilder builder = httpClientBuilderFactory.newBuilder();
        builder.setDefaultRequestConfig(requestConfig);
        if (config.relaxedSSL()) 
            connMgr = initPoolingConnectionManagerWithRelaxedSSL();
         else 
            connMgr = new PoolingHttpClientConnectionManager();
        
        connMgr.closeExpiredConnections();
        connMgr.setMaxTotal(config.maxTotalOpenConnections());
        connMgr.setDefaultMaxPerRoute(config.maxConcurrentConnectionPerRoute());
        builder.setConnectionManager(connMgr);
        List<Header> headers = new ArrayList<>();
        headers.add(new BasicHeader("Content-Type", "application/json"));
        headers.add(new BasicHeader("Authorization", "Bearer " + config.apiKey()));
        headers.add(new BasicHeader("OpenAI-Beta", "assistants=v1"));
        builder.setDefaultHeaders(headers);
        builder.setKeepAliveStrategy(keepAliveStratey);
        httpClient = builder.build();
        executor = Executor.newInstance(httpClient);
    

    private PoolingHttpClientConnectionManager initPoolingConnectionManagerWithRelaxedSSL()
            throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException 
        PoolingHttpClientConnectionManager connMgr;
        SSLContextBuilder sslbuilder = new SSLContextBuilder();
        sslbuilder.loadTrustMaterial(new TrustAllStrategy());
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslbuilder.build(),
                NoopHostnameVerifier.INSTANCE);
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslsf).build();
        connMgr = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        return connMgr;
    

    private RequestConfig initRequestConfig() 
        return RequestConfig.custom()
                .setConnectTimeout(Math.toIntExact(TimeUnit.SECONDS.toMillis(config.defaultConnectionTimeout())))
                .setSocketTimeout(Math.toIntExact(TimeUnit.SECONDS.toMillis(config.defaultSocketTimeout())))
                .setConnectionRequestTimeout(
                        Math.toIntExact(TimeUnit.SECONDS.toMillis(config.defaultConnectionRequestTimeout())))
                .build();
    

    @Deactivate
    protected void deactivate() 
        closeHttpConnection();
    

    private void closeHttpConnection() 
        if (null != httpClient) 
            try 
                httpClient.close();
             catch (final IOException exception) 
                log.debug("IOException while clossing API, ", exception.getMessage());
            
        
    

    @Override
    public Executor getExecutor() 
        return executor;
    

    @Override
    public ChatGptHttpClientFactoryConfig getConfig()
        return this.config;
    


    @Override
    public Request post() 
        return Request.Post(baseUrl);
    

    @Override
    public Request postCreateThreadRun() 
        String url = config.apiHostName();
        url += "/v1/threads/runs";
        return Request.Post(url);
    

    @Override
    public Request listMessages(String threadId) 
        String url = config.apiHostName();
        url += "/v1/threads/"+threadId+"/messages";
        return Request.Get(url);
    

    @Override
    public Request postCreateMessage(String threadId) 
        String url = config.apiHostName();
        url += "/v1/threads/"+threadId+"/messages";
        return Request.Post(url);
    

    @Override
    public Request postCreateRun(String threadId) 
        String url = config.apiHostName();
        url += "/v1/threads/"+threadId+"/runs";
        return Request.Post(url);
    

    @Override
    public Request retrieveRun(String threadId, String runId) 
        String url = config.apiHostName();
        url += "/v1/threads/"+threadId+"/runs/"+runId;
        return Request.Get(url);
    

    @Override
    public Request retrieveThread(String threadId) 
        String url = config.apiHostName();
        url += "/v1/threads/"+threadId;
        return Request.Get(url);
    

    ConnectionKeepAliveStrategy keepAliveStratey = new ConnectionKeepAliveStrategy() 

        @Override
        public long getKeepAliveDuration(HttpResponse response, HttpContext context) 
            /*
             * HeaderElementIterator headerElementIterator = new BasicHeaderElementIterator(
             * response.headerIterator(HTTP.CONN_KEEP_ALIVE));
             *
             * while (headerElementIterator.hasNext())  HeaderElement headerElement =
             * headerElementIterator.nextElement(); String param = headerElement.getName();
             * String value = headerElement.getValue(); if (value != null &&
             * param.equalsIgnoreCase("timeout"))  return
             * TimeUnit.SECONDS.toMillis(Long.parseLong(value));  
             */

            return TimeUnit.SECONDS.toMillis(config.defaultKeepAliveconnection());
        
    ;
}

 

Above are all the key points to integrate OpenAI Assistants API with Adobe AEM as Cloud Service (AEMaaCS). You can get the source code of the demo from here:

https://github.com/perficient1977/Blog-OpenAI-Demo

Note:

In the source code, we have there versions of customization on the Core Teaser component.  The v3 is for the OpenAI Assistants API integration with AEMaaCS.

In the source code, V1 and V2 are based on the OpenAI ChatGPT’s Chat Completation API integration. You can refer them if you interested.

 

 

 

 

 

Next Post

Air Purifiers and What They Do For Your Home.

Air purifiers are not just for the outdoors anymore. If you’re looking to keep your home clean and healthy, there’s an air purifier that can help. Air purifiers use a variety of ways to filter out harmful particles, such as dirt, pollution, dust, and odors. They also help to de-clutter […]
Air Purifiers and What They Do For Your Home.

You May Like