Fermer

janvier 6, 2024

Intégration d’Adobe AEMaaCS avec l’API OpenAI Assistants / Blogs / Perficient

Intégration d’Adobe AEMaaCS avec l’API OpenAI Assistants / Blogs / Perficient


L’API OpenAI Assistants vous permet de créer des assistants IA au sein de vos propres applications. Un assistant dispose d’instructions et peut exploiter des modèles, des outils et des connaissances pour répondre aux requêtes des utilisateurs.

L’API Assistants est conçue pour aider les développeurs à créer de puissants assistants IA capables d’effectuer diverses tâches.

Différent de l’API Chat Completions d’OpenAI, l’API Assistants est un framework d’agent. Votre instruction est similaire à une invite système, mais ne constitue qu’une partie du message qui inclut d’autres fonctions de messagerie et internes qui échappent à votre contrôle.

Les assistants permettent à l’IA, et l’IA est encouragée, de passer plusieurs appels de manière persistante en appelant à la récupération de parties de documents téléchargés via des fonctions internes, en écrivant du code python et en l’émettant par fonction vers un bac à sable qui peut exécuter le code, puis enfin en émettant. fonctionne vers vous de la même manière que les achèvements de chat.

Il dispose également d’un enregistrement des entrées des utilisateurs et des réponses de l’IA qui constituent une conversation, vous permettant uniquement de poser une question d’utilisateur, d’exécuter le fil de discussion, d’attendre qu’une réponse soit créée, de vérifier l’état, puis de télécharger la réponse terminée. Ou vous constatez que l’IA attend que vous exécutiez une fonction d’outil pour elle.

L’IA est chargée du nombre maximum de conversations et de documents pouvant tenir dans le modèle. Le retour de l’API ne contient aucune statistique d’utilisation des jetons indiquant le montant qui vous sera facturé.

Pour obtenir plus d’informations sur l’API Assistance, veuillez consulter l’introduction ici : https://platform.openai.com/docs/assistants/overview

Le document API peut être trouvé ici :

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

Dans ce blog, nous avons personnalisé le composant Core Teaser d’AEM pour vous aider à comprendre comment intégrer ensemble l’API OpenAI Assistances avec AEMaaCS. La démo fonctionnelle ressemblera à ceci :

capture d’écran 2024-01-03 17-47-27

L’implémentation d’AEM comprend les parties suivantes :

  • Un composant AEM Core Teaser personnalisé.
  • Un servlet AEM pour recevoir la demande de la boîte de dialogue du composant Teaser et appeler le service OSGi, puis renvoyer la réponse à l’interface utilisateur de la boîte de dialogue du composant Teaser.
  • Un service AEM OSGi pour accéder à l’API OpenAI Assistances
  • Une configuration OSGi pour fournir les valeurs de configuration requises lors de l’exécution.
  • Un service AEM OSGi qui fonctionne comme usine de client HTTP. Cette classe d’usine utilise les valeurs de la configuration OSGi ci-dessus pour accéder à l’API OpenAI Assistance.

D’autres sources connexes peuvent être trouvées dans le référentiel github partagé.

1. Personnalisez le composant teaser principal d’AEM

Tout d’abord, nous souhaitons personnaliser la boîte de dialogue du composant Teaser.

Sous l’onglet Texte de l’interface utilisateur de la boîte de dialogue, si l’auteur décoche la case « Obtenir la description à partir de la page liée », cela signifie que le texte de la description sera personnalisé ici, mais ne sera pas récupéré de la page du modèle. Nous pouvons ajouter une zone de texte pour permettre à l’auteur de saisir les instructions/invites ici. Nous avons également ajouté un bouton en dessous, lorsque l’auteur clique sur le bouton, une requête sera envoyée au servlet AEM pour accéder à l’API Assistances. Le RTE sous le bouton permet d’afficher la réponse de l’API Assistances. L’auteur peut apporter des modifications au RTE et cliquer sur le bouton « Terminé » pour mettre à jour le composant Teaser.

La nouvelle boîte de dialogue teaser ressemble à ci-dessous :

Boîte de dialogue teaser personnalisée

Interface utilisateur de boîte de dialogue teaser personnalisée

Vous trouverez ci-dessous le code source mis à jour dans la boîte de dialogue Core Teaser :

<?xml version="1.0" encoding="UTF-8"?>
...
                                            <descriptionFromLinkedPage
                                                    jcr:primaryType="nt:unstructured"
                                                    sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
                                                    checked="{Boolean}true"
                                                    fieldDescription="When checked, populate the description with the linked page's description."
                                                    name="./descriptionFromPage"
                                                    text="Get description from linked page"
                                                    uncheckedValue="{Boolean}false"
                                                    value="{Boolean}true"/>
                                            <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>
...

À partir des lignes 12 à 15, la boîte de dialogue réutilise une définition de widgets commune sous ‘/mnt/overlay/openai-sample/components/commons/editor/dialog/chatgpt-rte-2’. En voici le code source :

<?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="{Long}5"/>
        <gptButton jcr:primaryType="nt:unstructured"
                   name="./btnGroup"
                   required="{Boolean}false"
                   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="{Boolean}false"
                         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="{Boolean}true">
            <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>

Pour manipuler les comportements du bouton « Générer » et du RTE de description, nous devons créer du JavaScript personnalisé dans une bibliothèque cliente et laisser la boîte de dialogue du composant teaser l’utiliser.

Dans la nouvelle définition de la bibliothèque client, nous donnons le nom des catégories appelé « core.wcm.components.teaser.v2.gpt.editor3 ».

Crxde Lite 2024 01 03 14 17 31

Dans les propriétés de la boîte de dialogue, nous ajoutons une nouvelle propriété appelée « extraClientlibs ». La valeur de cette propriété est le nom de catégorie de la nouvelle bibliothèque cliente. Lorsque la boîte de dialogue Teaser est ouverte, le JavaScript de la bibliothèque client sera chargé automatiquement.

Crxde Lite 2024 01 03 14 12 16

Voici le fichier JavaScript créé dans la bibliothèque client :

(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) {
        var url;
        if (linkURL === undefined || 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 || url === "") && (Granite.author && Granite.author.page)) {
            url = Granite.author.page.path;
        }

        if (url && url.startsWith("/")) {
            return $.ajax({
                url: url + "/_jcr_content.json"
            }).done(function(data) {
                if (data) {
                    titleTuple.seedTextValue(data["jcr:title"]);
                    titleTuple.update();
                    descriptionTuple.seedTextValue(data["jcr:description"]);
                    descriptionTuple.update();
                }
            });
        } else {
            titleTuple.update();
            descriptionTuple.update();
        }
    }

    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);

Lorsque l’auteur clique sur le bouton « Générer » dans l’interface utilisateur de la boîte de dialogue, la fonction updateDescriptionAvecChatGPT() en JavaScript sera appelé. Cette fonction appellera le servlet AEM avec le point de terminaison ‘/bin/assistantServlet‘. Deux paramètres seront envoyés avec la requête :

  • contenu: le texte d’invite qui est le contenu de la zone de texte dans la boîte de dialogue
  • rôle: valeur de rôle requise par l’API Assistants. La valeur par défaut est « utilisateur »

2. Un servlet AEM pour recevoir la requête de la boîte de dialogue du composant Teaser

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 {
            session = request.getSession(true);
            session.setMaxInactiveInterval(30*60);
            String sessionThreadId = null;
            sessionThreadId = (String)session.getAttribute("thread_id");
            if(sessionThreadId == null || 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;
    }
}

Vérifiez la ligne 50-63 :

Le code Java utilise HttpSession pour enregistrer l’ID du thread Assistants. Si la HttpSession n’existe pas (ou si la HttpSession a expiré) ou si l’ID du thread n’est plus valide dans les assistants OpanAI, alors le créerNouveauThreadRun() la fonction sera appelée, sinon nous réutiliserons le fil de discussion et y ajouterons un nouveau message : la fonction addNewMessageOnThreadThenRun() sera appelé.

3. Un service AEM OSGi pour accéder à l’API OpenAI Assistances

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 + "\"}";
    }
}

La classe de service ci-dessus suit la logique des concepts de l’API Assistants :

  1. Créez un assistant dans l’API en définissant ses instructions personnalisées et en choisissant un modèle. Dans notre démo, nous supposons que les Assiants ont déjà été créés et que l’identifiant de l’assistant a été fourni aux développeurs.
  2. Créer un Fil sur l’assistant lorsqu’un utilisateur démarre une conversation.
  3. Ajouter messages au fil de discussion lorsque l’utilisateur pose des questions.
  4. Courir l’assistant sur le fil de discussion pour déclencher des réponses. Cela appelle automatiquement les outils appropriés.

Faites attention à la ligne 119-137 en surbrillance : étant donné que l’API Assistants peut prendre du temps pour générer des réponses aux questions, nous vérifions l’état du traitement toutes les 2 secondes en utilisant l’exécuteur programmé. Le maximum est de 10 fois, ce qui signifie que nous nous attendons à obtenir une réponse dans 20 secondes. Sinon, nous obtiendrons une erreur de délai d’attente.

4. Une configuration OSGi pour fournir les valeurs de configuration requises lors de l’exécution

Dans cette démo, nous utilisons la configuration AEM OSGi pour fournir des données au programme.

Voici le code source de la classe Java pour la définition de la configuration :

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;

}

Le com.perficient.aem.sample.openai.core.services.impl.ChatGptHttpClientFactoryImpl~openai-sample.cfg.json Le fichier de configuration fournit les valeurs de chaque variable de configuration dans le mode d’exécution de l’auteur :

{
  "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
}

Vous pouvez obtenir le clé API valeur (https://platform.openai.com/api-keys) et identifiant d’assistant (https://platform.openai.com/assistants) valeur à partir des paramètres de votre propre compte OpenAI.

Vous pouvez vérifier les valeurs de configuration à partir du gestionnaire de configuration AEM http://localhost:4502/system/console/configMgr

Configuration de la console Web Adobe Experience Manager 2024 01 03 16 53 03(1)

5. Un service AEM OSGi qui fonctionne comme usine de client HTTP.

Dans cette démo, nous utilisons le client HTTP Apache dans notre usine de clients HTTP pour envoyer des requêtes à l’API OpenAI Assistants. Voici le code source :

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());
        }
    };
}

Ci-dessus se trouvent tous les points clés pour intégrer l’API OpenAI Assistants avec Adobe AEM as Cloud Service (AEMaaCS). Vous pouvez obtenir le code source de la démo ici :

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

Note:

Dans le code source, nous avons là des versions de personnalisation sur le composant Core Teaser. La v3 est destinée à l’intégration de l’API OpenAI Assistants avec AEMaaCS.

Dans le code source, les V1 et V2 sont basées sur l’intégration de l’API Chat Completation d’OpenAI ChatGPT. Vous pouvez les référer si vous êtes intéressé.






Source link