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

À propos de l’API OpenAI Assistants
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 finalement 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
Comment intégrer l’API OpenAI Assistances avec AEMaaCS
Dans ce blog, vous découvrirez comment nous avons personnalisé le composant Core Teaser d’AEM pour vous aider à comprendre comment intégrer l’API OpenAI Assistances à AEMaaCS. Vous pouvez visionner une démo vidéo ou suivre les instructions écrites ci-dessous.
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. Personnaliser le composant AEM Core Teaser
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 », le texte de la description sera personnalisé 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 ressemblera à l’image ci-dessous.

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 la définition d’un widget commun 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="&copy;" name="copyright"/> <default_euro jcr:primaryType="nt:unstructured" entity="&euro;" name="euro"/> <default_registered jcr:primaryType="nt:unstructured" entity="&reg;" name="registered"/> <default_trademark jcr:primaryType="nt:unstructured" entity="&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 ».
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.
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 demande 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 les lignes 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 OpenAI, alors le créerNouveauThreadRun() la fonction sera appelée, sinon nous réutiliserons le fil et ajouterons un nouveau message dessus : 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 :
- 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 l’Assistant a déjà été créé et que l’ID de l’Assistant a été fourni aux développeurs.
- Créer un Fil sur l’assistant lorsqu’un utilisateur démarre une conversation.
- Ajouter messages au fil de discussion lorsque l’utilisateur pose des questions.
- Courir l’assistant sur le fil de discussion pour déclencher des réponses. Cela appelle automatiquement les outils appropriés.
Faites attention aux lignes 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 à l’aide de l’exécuteur programmé. Le maximum est de 10 fois, ce qui signifie que nous espérons 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 pendant l’exécution
Dans cette démo, nous utilisons la configuration AEM OSGi pour fournir des données au programme. Vous trouverez ci-dessous 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.
5. Un service AEM OSGi qui fonctionne comme usine 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 leurs versions de personnalisation sur le composant Core Teaser. La v3 est destinée à l’intégration de l’API OpenAI Assistants avec AEMaaCS.
Les versions V1 et V2 sont basées sur l’intégration de l’API Chat Completion d’OpenAI ChatGPT dans le code source. Vous pouvez les référer si vous êtes intéressé.
Source link