diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 3391bab2b5a3..f739f4a53406 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -27,6 +27,7 @@ 25.0.1 1.13.10 3.3.0 + 1.0.0 @@ -70,13 +71,55 @@ import + + dev.langchain4j + langchain4j-bom + ${langchain4j.version} + pom + import + + - - + + io.projectreactor + reactor-core + 3.4.41 + - io.netty - netty-all - --> + + + io.netty + netty-common + 4.1.118.Final + + + io.netty + netty-buffer + 4.1.118.Final + + + io.netty + netty-transport + 4.1.118.Final + + + io.netty + netty-resolver + 4.1.118.Final + + + io.netty + netty-codec + 4.1.118.Final + + + io.netty + netty-handler + 4.1.118.Final + + dev.langchain4j + langchain4j-open-ai + + + + dev.langchain4j + langchain4j-azure-open-ai + jakarta.inject jakarta.inject-api diff --git a/dotCMS/src/main/java/com/dotcms/ai/AiKeys.java b/dotCMS/src/main/java/com/dotcms/ai/AiKeys.java index 0dca7f4b2941..979553504400 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/AiKeys.java +++ b/dotCMS/src/main/java/com/dotcms/ai/AiKeys.java @@ -62,6 +62,7 @@ public class AiKeys { public static final String COUNT = "count"; public static final String INPUT = "input"; public static final String RESPONSE_FORMAT = "response_format"; + public static final String B64_JSON = "b64_json"; private AiKeys() {} diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/AsyncEmbeddingsCallStrategy.java b/dotCMS/src/main/java/com/dotcms/ai/api/AsyncEmbeddingsCallStrategy.java index 17fdb0fe8213..8a34151ec237 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/api/AsyncEmbeddingsCallStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/AsyncEmbeddingsCallStrategy.java @@ -15,7 +15,7 @@ public class AsyncEmbeddingsCallStrategy implements EmbeddingsCallStrategy { @Override public void bulkEmbed(final List inodes, final EmbeddingsForm embeddingsForm) { - DotConcurrentFactory.getInstance().getSubmitter(OPEN_AI_THREAD_POOL_KEY).submit(new BulkEmbeddingsRunner(inodes, embeddingsForm)); + DotConcurrentFactory.getInstance().getSubmitter(AI_THREAD_POOL_KEY).submit(new BulkEmbeddingsRunner(inodes, embeddingsForm)); } @Override @@ -23,7 +23,7 @@ public void embed(final EmbeddingsAPIImpl embeddingsAPI, final Contentlet contentlet, final String content, final String indexName) { - DotConcurrentFactory.getInstance().getSubmitter(OPEN_AI_THREAD_POOL_KEY).submit(new EmbeddingsRunner(embeddingsAPI, contentlet, content, indexName)); + DotConcurrentFactory.getInstance().getSubmitter(AI_THREAD_POOL_KEY).submit(new EmbeddingsRunner(embeddingsAPI, contentlet, content, indexName)); } } diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/CompletionsAPIImpl.java b/dotCMS/src/main/java/com/dotcms/ai/api/CompletionsAPIImpl.java index c0a7eac033c1..74126d6a701c 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/api/CompletionsAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/CompletionsAPIImpl.java @@ -23,6 +23,7 @@ import com.dotmarketing.business.web.WebAPILocator; import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.util.Config; +import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; import com.dotmarketing.util.json.JSONArray; import com.dotmarketing.util.json.JSONObject; @@ -73,6 +74,9 @@ public JSONObject prompt(final String systemPrompt, final Model model = config.resolveModelOrThrow(modelIn, AIModelType.TEXT)._2; final JSONObject json = new JSONObject(); + if (temperature <= 0) { + Logger.warn(this.getClass(), "Temperature is " + temperature + ". Set a positive value in providerConfig if unintended."); + } json.put(AiKeys.TEMPERATURE, temperature); buildMessages(systemPrompt, userPrompt, json); @@ -130,10 +134,10 @@ public void summarizeStream(final CompletionsForm summaryRequest, final OutputSt @Override public JSONObject raw(final JSONObject json, final String userId) { - config.debugLogger(this.getClass(), () -> "OpenAI request:" + json.toString(2)); + config.debugLogger(this.getClass(), () -> "AI request:" + json.toString(2)); final String response = sendRequest(config, json, userId).getResponse(); - config.debugLogger(this.getClass(), () -> "OpenAI response:" + response); + config.debugLogger(this.getClass(), () -> "AI response:" + response); return new JSONObject(response); } @@ -226,10 +230,18 @@ private ResolvedModel resolveModel(final CompletionsForm completionsForm) { .collect(Collectors.toList()); if (UtilMethods.isSet(models)) { - final Tuple2 modelTuple = config - .resolveModelOrThrow(completionsForm.model, AIModelType.TEXT); - - return new ResolvedModel(modelTuple._2.getName(), modelTuple._1.getMaxTokens()); + if (UtilMethods.isSet(completionsForm.model)) { + final Tuple2 modelTuple = config + .resolveModelOrThrow(completionsForm.model, AIModelType.TEXT); + final int maxTokens = modelTuple._1.getMaxTokens() > 0 + ? modelTuple._1.getMaxTokens() + : DEFAULT_AI_MAX_NUMBER_OF_TOKENS_VALUE.get(); + return new ResolvedModel(modelTuple._2.getName(), maxTokens); + } + final int maxTokens = aiModel.getMaxTokens() > 0 + ? aiModel.getMaxTokens() + : DEFAULT_AI_MAX_NUMBER_OF_TOKENS_VALUE.get(); + return new ResolvedModel(aiModel.getCurrentModel(), maxTokens); } else if (UtilMethods.isSet(completionsForm.model)) { return new ResolvedModel(completionsForm.model, DEFAULT_AI_MAX_NUMBER_OF_TOKENS_VALUE.get()); } else { @@ -304,12 +316,15 @@ private String reduceStringToTokenSize(final String incomingString, final int ma private JSONObject buildRequestJson(final CompletionsForm form) { final AIModel aiModel = config.getModel(); + final int effectiveMaxTokens = aiModel.getMaxTokens() > 0 + ? aiModel.getMaxTokens() + : DEFAULT_AI_MAX_NUMBER_OF_TOKENS_VALUE.get(); final int promptTokens = countTokens(form.prompt); final JSONArray messages = new JSONArray(); final String textPrompt = reduceStringToTokenSize( form.prompt, - aiModel.getMaxTokens() - form.responseLengthTokens - promptTokens); + effectiveMaxTokens - form.responseLengthTokens - promptTokens); messages.add(Map.of(AiKeys.ROLE, AiKeys.USER, AiKeys.CONTENT, textPrompt)); diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPI.java b/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPI.java index e619ae11ab03..db967d0df90d 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPI.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPI.java @@ -22,7 +22,7 @@ */ public interface EmbeddingsAPI { - String OPEN_AI_THREAD_POOL_KEY = "OpenAIThreadPool"; + String AI_THREAD_POOL_KEY = "AIThreadPool"; void shutdown(); diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPIImpl.java b/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPIImpl.java index e72b82ae105e..df063efee3d1 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsAPIImpl.java @@ -132,7 +132,7 @@ public int deleteByQuery(@NotNull final String deleteQuery, final OptionalDotConcurrentFactory.getInstance().shutdown(OPEN_AI_THREAD_POOL_KEY)); + Try.run(()->DotConcurrentFactory.getInstance().shutdown(AI_THREAD_POOL_KEY)); } @Override @@ -196,7 +196,7 @@ public boolean generateEmbeddingsForContent(@NotNull final Contentlet contentlet return false; } - DotConcurrentFactory.getInstance().getSubmitter(OPEN_AI_THREAD_POOL_KEY).submit(new EmbeddingsRunner(this, contentlet, parsed.get(), indexName)); + DotConcurrentFactory.getInstance().getSubmitter(AI_THREAD_POOL_KEY).submit(new EmbeddingsRunner(this, contentlet, parsed.get(), indexName)); return true; } @@ -343,9 +343,11 @@ public Tuple2> pullOrGenerateEmbeddings(final String conten .getEncoding() .map(encoding -> encoding.encode(content)) .orElse(List.of()); + final int tokenCount = tokens.isEmpty() ? content.split("\\s+").length : tokens.size(); if (tokens.isEmpty()) { - config.debugLogger(this.getClass(), () -> String.format("No tokens for content ID '%s' were encoded: %s", contentId, content)); - return Tuple.of(0, List.of()); + config.debugLogger(this.getClass(), () -> String.format( + "Encoding unavailable for content ID '%s', using word count (%d) as token estimate", + contentId, tokenCount)); } final Tuple3> dbEmbeddings = @@ -358,13 +360,13 @@ public Tuple2> pullOrGenerateEmbeddings(final String conten return Tuple.of(dbEmbeddings._2, dbEmbeddings._3); } - final Tuple2> openAiEmbeddings = Tuple.of( - tokens.size(), - sendTokensToOpenAI(contentId, tokens, userId)); - saveEmbeddingsForCache(content, openAiEmbeddings); - EMBEDDING_CACHE.put(hashed, openAiEmbeddings); + final Tuple2> embeddings = Tuple.of( + tokenCount, + generateEmbeddings(contentId, content, userId)); + saveEmbeddingsForCache(content, embeddings); + EMBEDDING_CACHE.put(hashed, embeddings); - return openAiEmbeddings; + return embeddings; } @CloseDBIfOpened @@ -434,20 +436,20 @@ private void saveEmbeddingsForCache(final String content, final Tuple2 sendTokensToOpenAI(final String contentId, - @NotNull final List tokens, + private List generateEmbeddings(final String contentId, + @NotNull final String content, final String userId) { final JSONObject json = new JSONObject(); json.put(AiKeys.MODEL, config.getEmbeddingsModel().getCurrentModel()); - json.put(AiKeys.INPUT, tokens); - config.debugLogger(this.getClass(), () -> String.format("Content tokens for content ID '%s': %s", contentId, tokens)); + json.put(AiKeys.INPUT, content); + config.debugLogger(this.getClass(), () -> String.format("Generating embeddings for content ID '%s'", contentId)); final String responseString = AIProxyClient.get() .callToAI(JSONObjectAIRequest.quickEmbeddings(config, json, userId)) .getResponse(); - config.debugLogger(this.getClass(), () -> String.format("OpenAI Response for content ID '%s': %s", + config.debugLogger(this.getClass(), () -> String.format("AI Response for content ID '%s': %s", contentId, responseString.replace("\n", BLANK))); final JSONObject jsonResponse = Try.of(() -> new JSONObject(responseString)).getOrElseThrow(e -> { - Logger.error(this, "OpenAI Response String is not a valid JSON", e); + Logger.error(this, "AI Response String is not a valid JSON", e); config.debugLogger(this.getClass(), () -> String.format("Invalid JSON Response: %s", responseString)); return new DotCorruptedDataException(e); }); diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsCallStrategy.java b/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsCallStrategy.java index 28256b4b8bcb..ca9b7d3c0f13 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsCallStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/EmbeddingsCallStrategy.java @@ -17,7 +17,7 @@ */ public interface EmbeddingsCallStrategy { - String OPEN_AI_THREAD_POOL_KEY = "OpenAIThreadPool"; + String AI_THREAD_POOL_KEY = "AIThreadPool"; /** * Embeds contentlets based on the provided inodes and form data. * diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/OpenAIImageAPIImpl.java b/dotCMS/src/main/java/com/dotcms/ai/api/OpenAIImageAPIImpl.java index 8178f6a31c03..42bfe20ecccb 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/api/OpenAIImageAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/OpenAIImageAPIImpl.java @@ -25,8 +25,11 @@ import io.vavr.control.Try; import javax.servlet.http.HttpServletRequest; +import java.io.ByteArrayInputStream; +import java.net.URI; import java.net.URL; import java.text.SimpleDateFormat; +import java.util.Base64; import java.util.Date; public class OpenAIImageAPIImpl implements ImageAPI { @@ -60,7 +63,7 @@ public JSONObject sendRequest(final JSONObject jsonObject) { String responseString = ""; try { - responseString = doRequest(config.getApiImageUrl(), jsonObject); + responseString = doRequest(jsonObject); JSONObject returnObject = new JSONObject(responseString); if (returnObject.containsKey(AiKeys.ERROR)) { @@ -99,21 +102,30 @@ public JSONObject sendTextPrompt(final String textPrompt) { } private JSONObject createTempFile(final JSONObject imageResponse) { - final String url = imageResponse.optString(AiKeys.URL); - if (UtilMethods.isEmpty(() -> url)) { - Logger.warn(this.getClass(), "imageResponse does not include URL:" + imageResponse); - throw new DotRuntimeException("Image Response does not include URL:" + imageResponse); - } - try { final String fileName = generateFileName(imageResponse.getString(AiKeys.ORIGINAL_PROMPT)); imageResponse.put("tempFileName", fileName); - final DotTempFile file = tempFileApi.createTempFileFromUrl(fileName, getRequest(), new URL(url), 20); + final String url = imageResponse.optString(AiKeys.URL); + final String b64 = imageResponse.optString(AiKeys.B64_JSON); + final DotTempFile file; + + if (!UtilMethods.isEmpty(() -> url)) { + file = tempFileApi.createTempFileFromUrl(fileName, getRequest(), URI.create(url).toURL(), 20); + } else if (!UtilMethods.isEmpty(() -> b64)) { + final byte[] imageBytes = Base64.getDecoder().decode(b64); + file = tempFileApi.createTempFile(fileName, getRequest(), new ByteArrayInputStream(imageBytes)); + } else { + Logger.warn(this.getClass(), "imageResponse does not include URL or base64 data:" + imageResponse); + throw new DotRuntimeException("Image Response does not include URL or base64 data:" + imageResponse); + } + imageResponse.put(AiKeys.RESPONSE, file.id); imageResponse.put("tempFile", file.file.getAbsolutePath()); return imageResponse; + } catch (DotRuntimeException e) { + throw e; } catch (Exception e) { imageResponse.put(AiKeys.RESPONSE, e.getMessage()); imageResponse.put(AiKeys.ERROR, e.getMessage()); @@ -173,7 +185,7 @@ private String generateFileName(final String originalPrompt) { } @VisibleForTesting - String doRequest(final String urlIn, final JSONObject json) { + String doRequest(final JSONObject json) { return AIProxyClient.get() .callToAI(JSONObjectAIRequest.quickImage(config, json, UtilMethods.extractUserIdOrNull(user))) .getResponse(); diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AIAppUtil.java b/dotCMS/src/main/java/com/dotcms/ai/app/AIAppUtil.java index 3cb2a1dad162..dee628871fc3 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/app/AIAppUtil.java +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AIAppUtil.java @@ -6,7 +6,6 @@ import com.liferay.util.StringPool; import io.vavr.Lazy; import io.vavr.control.Try; -import org.apache.commons.collections4.CollectionUtils; import java.util.Arrays; import java.util.List; @@ -34,72 +33,6 @@ public static AIAppUtil get() { return INSTANCE.get(); } - /** - * Creates a text model instance based on the provided secrets. - * - * @param secrets the map of secrets - * @return the created text model instance - */ - public AIModel createTextModel(final Map secrets) { - final List modelNames = splitDiscoveredSecret(secrets, AppKeys.TEXT_MODEL_NAMES); - if (CollectionUtils.isEmpty(modelNames)) { - return AIModel.NOOP_MODEL; - } - - return AIModel.builder() - .withType(AIModelType.TEXT) - .withModelNames(modelNames) - .withTokensPerMinute(discoverIntSecret(secrets, AppKeys.TEXT_MODEL_TOKENS_PER_MINUTE)) - .withApiPerMinute(discoverIntSecret(secrets, AppKeys.TEXT_MODEL_API_PER_MINUTE)) - .withMaxTokens(discoverIntSecret(secrets, AppKeys.TEXT_MODEL_MAX_TOKENS)) - .withIsCompletion(discoverBooleanSecret(secrets, AppKeys.TEXT_MODEL_COMPLETION)) - .build(); - } - - /** - * Creates an image model instance based on the provided secrets. - * - * @param secrets the map of secrets - * @return the created image model instance - */ - public AIModel createImageModel(final Map secrets) { - final List modelNames = splitDiscoveredSecret(secrets, AppKeys.IMAGE_MODEL_NAMES); - if (CollectionUtils.isEmpty(modelNames)) { - return AIModel.NOOP_MODEL; - } - - return AIModel.builder() - .withType(AIModelType.IMAGE) - .withModelNames(modelNames) - .withTokensPerMinute(discoverIntSecret(secrets, AppKeys.IMAGE_MODEL_TOKENS_PER_MINUTE)) - .withApiPerMinute(discoverIntSecret(secrets, AppKeys.IMAGE_MODEL_API_PER_MINUTE)) - .withMaxTokens(discoverIntSecret(secrets, AppKeys.IMAGE_MODEL_MAX_TOKENS)) - .withIsCompletion(discoverBooleanSecret(secrets, AppKeys.IMAGE_MODEL_COMPLETION)) - .build(); - } - - /** - * Creates an embeddings model instance based on the provided secrets. - * - * @param secrets the map of secrets - * @return the created embeddings model instance - */ - public AIModel createEmbeddingsModel(final Map secrets) { - final List modelNames = splitDiscoveredSecret(secrets, AppKeys.EMBEDDINGS_MODEL_NAMES); - if (CollectionUtils.isEmpty(modelNames)) { - return AIModel.NOOP_MODEL; - } - - return AIModel.builder() - .withType(AIModelType.EMBEDDINGS) - .withModelNames(modelNames) - .withTokensPerMinute(discoverIntSecret(secrets, AppKeys.EMBEDDINGS_MODEL_TOKENS_PER_MINUTE)) - .withApiPerMinute(discoverIntSecret(secrets, AppKeys.EMBEDDINGS_MODEL_API_PER_MINUTE)) - .withMaxTokens(discoverIntSecret(secrets, AppKeys.EMBEDDINGS_MODEL_MAX_TOKENS)) - .withIsCompletion(discoverBooleanSecret(secrets, AppKeys.EMBEDDINGS_MODEL_COMPLETION)) - .build(); - } - /** * Resolves a secret value from the provided secrets map using the specified key. * If the secret is not found, the default value is returned. diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AIModel.java b/dotCMS/src/main/java/com/dotcms/ai/app/AIModel.java index 39558755bbe7..849699aceeed 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/app/AIModel.java +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AIModel.java @@ -98,7 +98,7 @@ public String getCurrentModel() { public Model getModel(final String modelName) { final String normalized = modelName.trim().toLowerCase(); return models.stream() - .filter(model -> normalized.equals(model.getName())) + .filter(model -> normalized.equalsIgnoreCase(model.getName())) .findFirst() .orElseThrow(() -> new DotAIModelNotFoundException(String.format("Model [%s] not found", modelName))); } diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java b/dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java deleted file mode 100644 index e8819cfaf455..000000000000 --- a/dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java +++ /dev/null @@ -1,360 +0,0 @@ -package com.dotcms.ai.app; - -import com.dotcms.ai.domain.Model; -import com.dotcms.ai.domain.ModelStatus; -import com.dotcms.ai.exception.DotAIModelNotFoundException; -import com.dotcms.ai.model.OpenAIModel; -import com.dotcms.ai.model.OpenAIModels; -import com.dotcms.ai.model.SimpleModel; -import com.dotcms.business.SystemTableUpdatedKeyEvent; -import com.dotcms.http.CircuitBreakerUrl; -import com.dotcms.system.event.local.model.EventSubscriber; -import com.dotmarketing.business.APILocator; -import com.dotmarketing.exception.DotRuntimeException; -import com.dotmarketing.util.Config; -import com.dotmarketing.util.Logger; -import com.dotmarketing.util.UtilMethods; -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.google.common.annotations.VisibleForTesting; -import io.vavr.Lazy; -import io.vavr.Tuple; -import io.vavr.Tuple2; -import io.vavr.Tuple3; -import org.apache.commons.collections4.CollectionUtils; - -import java.time.Duration; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -/** - * Manages the AI models used in the application. This class handles loading, caching, - * and retrieving AI models based on the host and model type. It also fetches supported - * models from external sources and maintains a cache of these models. - * - * @author vico - */ -public class AIModels implements EventSubscriber { - - private static final String SUPPORTED_MODELS_KEY = "supportedModels"; - private static final String AI_MODELS_FETCH_ATTEMPTS_KEY = "ai.models.fetch.attempts"; - private static final String AI_MODELS_FETCH_TIMEOUT_KEY = "ai.models.fetch.timeout"; - private static final String AI_MODELS_API_URL_KEY = "AI_MODELS_API_URL"; - private static final String AI_MODELS_API_URL_DEFAULT = "https://api.openai.com/v1/models"; - private static final int AI_MODELS_CACHE_TTL = 28800; // 8 hours - private static final int AI_MODELS_CACHE_SIZE = 256; - private static final Lazy INSTANCE = Lazy.of(AIModels::new); - public static final String BEARER = "Bearer "; - - private final ConcurrentMap>> internalModels; - private final ConcurrentMap, AIModel> modelsByName; - private final Cache> supportedModelsCache; - private final AtomicInteger modelsFetchAttempts; - private final AtomicLong modelsFetchTimeout; - private final AtomicReference aiModelsApiUrl; - - public static AIModels get() { - return INSTANCE.get(); - } - - private static int resolveModelsFetchAttempts() { - return Config.getIntProperty(AI_MODELS_FETCH_ATTEMPTS_KEY, 90); - } - - private static long resolveModelsFetchTimeout() { - return Config.getLongProperty(AI_MODELS_FETCH_TIMEOUT_KEY, 4000); - } - - private static String resolveAiModelsApiUrl() { - return Config.getStringProperty(AI_MODELS_API_URL_KEY, AI_MODELS_API_URL_DEFAULT); - } - - private AIModels() { - internalModels = new ConcurrentHashMap<>(); - modelsByName = new ConcurrentHashMap<>(); - supportedModelsCache = - Caffeine.newBuilder() - .expireAfterWrite(Duration.ofSeconds(AI_MODELS_CACHE_TTL)) - .maximumSize(AI_MODELS_CACHE_SIZE) - .build(); - modelsFetchAttempts = new AtomicInteger(resolveModelsFetchAttempts()); - modelsFetchTimeout = new AtomicLong(resolveModelsFetchTimeout()); - aiModelsApiUrl = new AtomicReference<>(resolveAiModelsApiUrl()); - APILocator.getLocalSystemEventsAPI().subscribe(SystemTableUpdatedKeyEvent.class, this); - } - - /** - * Loads the given list of AI models for the specified host. If models for the host - * are already loaded, this method does nothing. It also maps model names to their - * corresponding AIModel instances. - * - * @param appConfig app config - * @param loading the list of AI models to load - */ - public void loadModels(final AppConfig appConfig, final List loading) { - final String host = appConfig.getHost(); - - final List> currentModels = internalModels.get(host); - - if (UtilMethods.isSet(currentModels)) { - for (AIModel loadingAIModel : loading) { - final AIModelType type = loadingAIModel.getType(); - - for (Tuple2 currentTupla : currentModels) { - if(type == currentTupla._1()) { - final AIModel currentAIModel = currentTupla._2; - - for (Model currentModel : currentAIModel.getModels()) { - final Optional optionalModel = loadingAIModel.getModels().stream() - .filter(model -> model.getName().equals(currentModel.getName())) - .findFirst(); - - if (optionalModel.isPresent() && optionalModel.get().getStatus() == null) { - optionalModel.get().setStatus(currentModel.getStatus()); - } - } - } - } - } - } - - - final List> added = internalModels.put( - host, - loading.stream() - .map(model -> Tuple.of(model.getType(), model)) - .collect(Collectors.toList())); - - loading.forEach(aiModel -> aiModel - .getModels() - .forEach(model -> { - final Tuple3 key = Tuple.of(host, model, aiModel.getType()); - modelsByName.put(key, aiModel); - })); - - if (added == null) { - activateModels(host); - } - } - - /** - * Finds an AI model by the host and model name. The search is case-insensitive. - * - * @param appConfig the AppConfig for the host - * @param modelName the name of the model to find - * @param type the type of the model to find - * @return an Optional containing the found AIModel, or an empty Optional if not found - */ - public Optional findModel(final AppConfig appConfig, - final String modelName, - final AIModelType type) { - final String lowered = modelName.toLowerCase(); - return Optional.ofNullable( - modelsByName.get( - Tuple.of( - appConfig.getHost(), - Model.builder().withName(lowered).build(), - type))); - } - - /** - * Finds an AI model by the host and model type. - * - * @param host the host for which the model is being searched - * @param type the type of the model to find - * @return an Optional containing the found AIModel, or an empty Optional if not found - */ - public Optional findModel(final String host, final AIModelType type) { - return Optional.ofNullable(internalModels.get(host)) - .flatMap(tuples -> tuples.stream() - .filter(tuple -> tuple._1 == type) - .map(Tuple2::_2) - .findFirst()); - } - - /** - * Resolves a model-specific secret value from the provided secrets map using the specified key and model type. - * - * @param host the host for which the model is being resolved - * @param type the type of the model to find - */ - public AIModel resolveModel(final String host, final AIModelType type) { - return findModel(host, type).orElse(AIModel.NOOP_MODEL); - } - - /** - * Resolves a model-specific secret value from the provided secrets map using the specified key and model type. - * - * @param appConfig the AppConfig for the host - * @param modelName the name of the model to find - * @param type the type of the model to find - */ - public AIModel resolveAIModelOrThrow(final AppConfig appConfig, final String modelName, final AIModelType type) { - return findModel(appConfig, modelName, type) - .orElseThrow(() -> new DotAIModelNotFoundException( - String.format("Unable to find model: [%s] of type [%s].", modelName, type))); - } - - /** - * Resolves a model-specific secret value from the provided secrets map using the specified key and model type. - * If the model is not found or is not operational, it throws an appropriate exception. - * - * @param appConfig the AppConfig for the host - * @param modelName the name of the model to find - * @param type the type of the model to find - * @return a Tuple2 containing the AIModel and the Model - */ - public Tuple2 resolveModelOrThrow(final AppConfig appConfig, - final String modelName, - final AIModelType type) { - final AIModel aiModel = resolveAIModelOrThrow(appConfig, modelName, type); - return Tuple.of(aiModel, aiModel.getModel(modelName)); - } - - /** - * Resets the internal models cache for the specified host. - * - * @param host the host for which the models are being reset - */ - public void resetModels(final String host) { - Optional.ofNullable(internalModels.get(host)).ifPresent(models -> { - models.clear(); - internalModels.remove(host); - }); - modelsByName.keySet() - .stream() - .filter(key -> key._1.equals(host)) - .collect(Collectors.toSet()) - .forEach(modelsByName::remove); - cleanSupportedModelsCache(); - } - - /** - * Retrieves the list of supported models, either from the cache or by fetching them - * from an external source if the cache is empty or expired. - * - * @return a set of supported model names - */ - public Set getOrPullSupportedModels(final AppConfig appConfig) { - final Set cached = supportedModelsCache.getIfPresent(SUPPORTED_MODELS_KEY); - if (CollectionUtils.isNotEmpty(cached)) { - return cached; - } - - if (!appConfig.isEnabled()) { - appConfig.debugLogger(getClass(), () -> "dotAI is not enabled, returning empty set of supported models"); - return Set.of(); - } - - final CircuitBreakerUrl.Response response = fetchOpenAIModels(appConfig); - - if (Objects.nonNull(response.getResponse().getError())) { - throw new DotRuntimeException("Found error in AI response: " + response.getResponse().getError().getMessage()); - } - - final Set supported = response - .getResponse() - .getData() - .stream() - .map(OpenAIModel::getId) - .map(String::toLowerCase) - .collect(Collectors.toSet()); - supportedModelsCache.put(SUPPORTED_MODELS_KEY, supported); - - return supported; - } - - /** - * Retrieves the list of available models that are both configured and supported. - * - * @return a list of available model names - */ - public List getAvailableModels() { - return internalModels.entrySet() - .stream() - .flatMap(entry -> entry.getValue().stream()) - .map(Tuple2::_2) - .filter(AIModel::isOperational) - .flatMap(aiModel -> aiModel.getModels() - .stream() - .filter(Model::isOperational) - .map(model -> new SimpleModel( - model.getName(), - aiModel.getType(), - aiModel.getCurrentModelIndex() == model.getIndex()))) - .distinct() - .collect(Collectors.toList()); - } - - @Override - public void notify(final SystemTableUpdatedKeyEvent event) { - Logger.info(this, String.format("Notify for event [%s]", event.getKey())); - if (event.getKey().contains(AI_MODELS_FETCH_ATTEMPTS_KEY)) { - modelsFetchAttempts.set(resolveModelsFetchAttempts()); - } else if (event.getKey().contains(AI_MODELS_FETCH_TIMEOUT_KEY)) { - modelsFetchTimeout.set(Config.getIntProperty(AI_MODELS_FETCH_TIMEOUT_KEY, 14)); - } else if (event.getKey().contains(AI_MODELS_API_URL_KEY)) { - aiModelsApiUrl.set(resolveAiModelsApiUrl()); - } - } - - @VisibleForTesting - void cleanSupportedModelsCache() { - supportedModelsCache.invalidate(SUPPORTED_MODELS_KEY); - } - - private CircuitBreakerUrl.Response fetchOpenAIModels(final AppConfig appConfig) { - final CircuitBreakerUrl.Response response = CircuitBreakerUrl.builder() - .setMethod(CircuitBreakerUrl.Method.GET) - .setUrl(aiModelsApiUrl.get()) - .setTimeout(modelsFetchTimeout.get()) - .setTryAgainAttempts(modelsFetchAttempts.get()) - .setAuthHeaders(BEARER + appConfig.getApiKey()) - .setThrowWhenError(true) - .build() - .doResponse(OpenAIModels.class); - - if (response.getStatusCode() == 401) { - throw new InvalidAIKeyException("AI key authentication failed. Please ensure the key is valid, active, and correctly configured."); - } else if (!CircuitBreakerUrl.isSuccessResponse(response)) { - appConfig.debugLogger( - AIModels.class, - () -> String.format( - "Error fetching OpenAI supported models from [%s] (status code: [%d])", - aiModelsApiUrl.get(), - response.getStatusCode())); - throw new DotRuntimeException("Error fetching OpenAI supported models"); - } - - return response; - } - - private void activateModels(final String host) { - final List aiModels = internalModels.get(host) - .stream() - .map(tuple -> tuple._2) - .collect(Collectors.toList()); - - aiModels.forEach(aiModel -> - aiModel.getModels().forEach(model -> { - final String modelName = model.getName().trim().toLowerCase(); - final ModelStatus status = ModelStatus.ACTIVE; - if (aiModel.getCurrentModelIndex() == AIModel.NOOP_INDEX) { - aiModel.setCurrentModelIndex(model.getIndex()); - } - Logger.debug( - this, - String.format("Model [%s] is supported by OpenAI, marking it as [%s]", modelName, status)); - model.setStatus(status); - })); - } - -} diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java b/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java index 69cbad32958e..a46f7ac058f9 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java @@ -1,23 +1,27 @@ package com.dotcms.ai.app; import com.dotcms.ai.domain.Model; +import com.dotcms.ai.exception.DotAIModelNotFoundException; import com.dotcms.security.apps.Secret; +import com.dotcms.rest.api.v1.DotObjectMapperProvider; import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; import com.dotmarketing.util.UtilMethods; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.liferay.util.StringPool; +import io.vavr.Tuple; import io.vavr.Tuple2; import io.vavr.control.Try; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; import java.io.Serializable; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Supplier; import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * The AppConfig class provides a configuration for the AI application. @@ -29,6 +33,7 @@ public class AppConfig implements Serializable { private static final String AI_IMAGE_API_URL_KEY = "AI_IMAGE_API_URL"; private static final String AI_EMBEDDINGS_API_URL_KEY = "AI_EMBEDDINGS_API_URL"; private static final String AI_DEBUG_LOGGING_KEY = "AI_DEBUG_LOGGING"; + private static final ObjectMapper MAPPER = DotObjectMapperProvider.createDefaultMapper(); public static final Pattern SPLITTER = Pattern.compile("\\s?,\\s?"); @@ -45,6 +50,8 @@ public class AppConfig implements Serializable { private final String imagePrompt; private final String imageSize; private final String listenerIndexer; + private final String providerConfig; + private final String providerConfigHash; private final Map configValues; public AppConfig(final String host, final Map secrets) { @@ -55,20 +62,22 @@ public AppConfig(final String host, final Map secrets) { apiUrl = aiAppUtil.discoverEnvSecret(secrets, AppKeys.API_URL, AI_API_URL_KEY); apiImageUrl = aiAppUtil.discoverEnvSecret(secrets, AppKeys.API_IMAGE_URL, AI_IMAGE_API_URL_KEY); apiEmbeddingsUrl = aiAppUtil.discoverEnvSecret(secrets, AppKeys.API_EMBEDDINGS_URL, AI_EMBEDDINGS_API_URL_KEY); - - if (!secrets.isEmpty() || isEnabled()) { - AIModels.get().loadModels( - this, - List.of( - aiAppUtil.createTextModel(secrets), - aiAppUtil.createImageModel(secrets), - aiAppUtil.createEmbeddingsModel(secrets))); + final String rawProviderConfig = aiAppUtil.discoverSecret(secrets, AppKeys.PROVIDER_CONFIG); + providerConfig = rawProviderConfig != null ? rawProviderConfig.replaceAll("[\\r\\n\\t]", "") : null; + + if (StringUtils.isNotBlank(providerConfig)) { + providerConfigHash = DigestUtils.sha256Hex(providerConfig); + final JsonNode providerConfigRoot = parseProviderConfig(providerConfig); + model = buildModelFromProviderConfigNode(providerConfigRoot, "chat", AIModelType.TEXT); + imageModel = buildModelFromProviderConfigNode(providerConfigRoot, "image", AIModelType.IMAGE); + embeddingsModel = buildModelFromProviderConfigNode(providerConfigRoot, "embeddings", AIModelType.EMBEDDINGS); + } else { + providerConfigHash = "no-config"; + model = AIModel.NOOP_MODEL; + imageModel = AIModel.NOOP_MODEL; + embeddingsModel = AIModel.NOOP_MODEL; } - model = resolveModel(AIModelType.TEXT); - imageModel = resolveModel(AIModelType.IMAGE); - embeddingsModel = resolveModel(AIModelType.EMBEDDINGS); - rolePrompt = aiAppUtil.discoverSecret(secrets, AppKeys.ROLE_PROMPT); textPrompt = aiAppUtil.discoverSecret(secrets, AppKeys.TEXT_PROMPT); imagePrompt = aiAppUtil.discoverSecret(secrets, AppKeys.IMAGE_PROMPT); @@ -140,6 +149,7 @@ public String getApiEmbeddingsUrl() { * * @return the API Key */ + @Deprecated public String getApiKey() { return apiKey; } @@ -279,7 +289,12 @@ public String getConfig(final AppKeys appKey) { * @param type the type of the model to find */ public AIModel resolveModel(final AIModelType type) { - return AIModels.get().resolveModel(host, type); + switch (type) { + case TEXT: return model; + case IMAGE: return imageModel; + case EMBEDDINGS: return embeddingsModel; + default: return AIModel.NOOP_MODEL; + } } /** @@ -291,16 +306,86 @@ public AIModel resolveModel(final AIModelType type) { * @return the resolved Model */ public Tuple2 resolveModelOrThrow(final String modelName, final AIModelType type) { - return AIModels.get().resolveModelOrThrow(this, modelName, type); + final AIModel aiModel = resolveModel(type); + if (aiModel == AIModel.NOOP_MODEL) { + throw new DotAIModelNotFoundException( + String.format("Unable to find model: [%s] of type [%s].", modelName, type)); + } + final Model model = aiModel.getCurrent(); + if (model == null) { + throw new DotAIModelNotFoundException( + String.format("No operational model found of type [%s].", type)); + } + return Tuple.of(aiModel, model); + } + + /** + * Returns the raw {@code providerConfig} JSON string, or {@code null} if not set. + */ + public String getProviderConfig() { + return providerConfig; + } + + /** + * Returns the SHA-256 hex digest of the {@code providerConfig} JSON, or {@code null} if not set. + * Computed once at construction time — safe to use as a cache key on every request. + */ + public String getProviderConfigHash() { + return providerConfigHash; } /** * Checks if the configuration is enabled. + * Returns true when a non-blank {@code providerConfig} JSON is present and at least one + * model section (chat, image, embeddings) parsed successfully. * * @return true if the configuration is enabled, false otherwise */ public boolean isEnabled() { - return Stream.of(apiUrl, apiImageUrl, apiEmbeddingsUrl, apiKey).allMatch(StringUtils::isNotBlank); + if (StringUtils.isBlank(providerConfig)) { + Logger.debug(AppConfig.class, "dotAI not enabled for host [" + host + "]: providerConfig is blank"); + return false; + } + if (model == AIModel.NOOP_MODEL && imageModel == AIModel.NOOP_MODEL && embeddingsModel == AIModel.NOOP_MODEL) { + Logger.debug(AppConfig.class, "dotAI not enabled for host [" + host + "]: providerConfig set but no model section parsed successfully"); + return false; + } + return true; + } + + @com.google.common.annotations.VisibleForTesting + static JsonNode parseProviderConfig(final String json) { + try { + return MAPPER.readTree(json); + } catch (final Exception e) { + Logger.warn(AppConfig.class, "Failed to parse providerConfig JSON" + + " (" + e.getClass().getSimpleName() + "): " + e.getMessage(), e); + return MAPPER.createObjectNode(); + } + } + + private static AIModel buildModelFromProviderConfigNode(final JsonNode root, final String section, final AIModelType type) { + try { + final JsonNode sectionNode = root.get(section); + if (sectionNode == null) { + return AIModel.NOOP_MODEL; + } + final JsonNode modelNode = sectionNode.get("model"); + if (modelNode == null || modelNode.asText().isBlank()) { + return AIModel.NOOP_MODEL; + } + final AIModel.Builder builder = AIModel.builder() + .withType(type) + .withModelNames(modelNode.asText()); + final JsonNode maxTokensNode = sectionNode.get("maxTokens"); + if (maxTokensNode != null && maxTokensNode.isInt()) { + builder.withMaxTokens(maxTokensNode.asInt()); + } + return builder.build(); + } catch (final Exception e) { + Logger.warn(AppConfig.class, "Failed to parse model from providerConfig section '" + section + "': " + e.getMessage()); + return AIModel.NOOP_MODEL; + } } public void debugLogger(final Class clazz, final Supplier message) { @@ -312,9 +397,9 @@ public String toString() { return "AppConfig{\n" + " host='" + host + "',\n" + " apiKey='" + Optional.ofNullable(apiKey).map(key -> "*****").orElse(StringPool.BLANK) + "',\n" + - " model=" + model + "',\n" + - " imageModel=" + imageModel + "',\n" + - " embeddingsModel=" + embeddingsModel + "',\n" + + " model='" + model + "',\n" + + " imageModel='" + imageModel + "',\n" + + " embeddingsModel='" + embeddingsModel + "',\n" + " apiUrl='" + apiUrl + "',\n" + " apiImageUrl='" + apiImageUrl + "',\n" + " apiEmbeddingsUrl='" + apiEmbeddingsUrl + "',\n" + @@ -322,7 +407,8 @@ public String toString() { " textPrompt='" + textPrompt + "',\n" + " imagePrompt='" + imagePrompt + "',\n" + " imageSize='" + imageSize + "',\n" + - " listenerIndexer='" + listenerIndexer + "'\n" + + " listenerIndexer='" + listenerIndexer + "',\n" + + " providerConfig='" + (StringUtils.isNotBlank(providerConfig) ? "[set]" : "[not set]") + "'\n" + '}'; } diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/AppKeys.java b/dotCMS/src/main/java/com/dotcms/ai/app/AppKeys.java index a40c57c959f8..0c091bc800de 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/app/AppKeys.java +++ b/dotCMS/src/main/java/com/dotcms/ai/app/AppKeys.java @@ -15,21 +15,6 @@ public enum AppKeys { TEXT_PROMPT("textPrompt", "Use Descriptive writing style."), IMAGE_PROMPT("imagePrompt", "Use 16:9 aspect ratio."), IMAGE_SIZE("imageSize", "1024x1024"), - TEXT_MODEL_NAMES("textModelNames", null), - TEXT_MODEL_TOKENS_PER_MINUTE("textModelTokensPerMinute", "180000"), - TEXT_MODEL_API_PER_MINUTE("textModelApiPerMinute", "3500"), - TEXT_MODEL_MAX_TOKENS("textModelMaxTokens", "16384"), - TEXT_MODEL_COMPLETION("textModelCompletion", "true"), - IMAGE_MODEL_NAMES("imageModelNames", null), - IMAGE_MODEL_TOKENS_PER_MINUTE("imageModelTokensPerMinute", "0"), - IMAGE_MODEL_API_PER_MINUTE("imageModelApiPerMinute", "50"), - IMAGE_MODEL_MAX_TOKENS("imageModelMaxTokens", "0"), - IMAGE_MODEL_COMPLETION("imageModelCompletion", StringPool.FALSE), - EMBEDDINGS_MODEL_NAMES("embeddingsModelNames", null), - EMBEDDINGS_MODEL_TOKENS_PER_MINUTE("embeddingsModelTokensPerMinute", "1000000"), - EMBEDDINGS_MODEL_API_PER_MINUTE("embeddingsModelApiPerMinute", "3000"), - EMBEDDINGS_MODEL_MAX_TOKENS("embeddingsModelMaxTokens", "8191"), - EMBEDDINGS_MODEL_COMPLETION("embeddingsModelCompletion", StringPool.FALSE), EMBEDDINGS_SPLIT_AT_TOKENS("com.dotcms.ai.embeddings.split.at.tokens", "512"), EMBEDDINGS_MINIMUM_TEXT_LENGTH_TO_INDEX("com.dotcms.ai.embeddings.minimum.text.length", "64"), EMBEDDINGS_MINIMUM_FILE_SIZE_TO_INDEX("com.dotcms.ai.embeddings.minimum.file.size", "1024"), @@ -51,8 +36,7 @@ public enum AppKeys { "Answer this question\\n\\\"$!{prompt}?\\\"\\n\\nby using only the information in" + " the following text:\\n\"\"\"\\n$!{supportingContent} \\n\"\"\"\\n"), LISTENER_INDEXER("listenerIndexer", "{}"), - AI_MODELS_CACHE_TTL("com.dotcms.ai.models.supported.ttl", "28800"), - AI_MODELS_CACHE_SIZE("com.dotcms.ai.models.supported.size", "64"); + PROVIDER_CONFIG("providerConfig", null); public static final String APP_KEY = "dotAI"; diff --git a/dotCMS/src/main/java/com/dotcms/ai/app/InvalidAIKeyException.java b/dotCMS/src/main/java/com/dotcms/ai/app/InvalidAIKeyException.java deleted file mode 100644 index abc645f4e725..000000000000 --- a/dotCMS/src/main/java/com/dotcms/ai/app/InvalidAIKeyException.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.dotcms.ai.app; - -public class InvalidAIKeyException extends RuntimeException { - public InvalidAIKeyException(String message) { - super(message); - } -} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIModelFallbackStrategy.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIModelFallbackStrategy.java deleted file mode 100644 index e08239e47900..000000000000 --- a/dotCMS/src/main/java/com/dotcms/ai/client/AIModelFallbackStrategy.java +++ /dev/null @@ -1,280 +0,0 @@ -package com.dotcms.ai.client; - -import com.dotcms.ai.AiKeys; -import com.dotcms.ai.app.AIModel; -import com.dotcms.ai.app.AIModels; -import com.dotcms.ai.app.AppConfig; -import com.dotcms.ai.domain.AIResponseData; -import com.dotcms.ai.domain.Model; -import com.dotcms.ai.exception.DotAIAllModelsExhaustedException; -import com.dotcms.ai.validator.AIAppValidator; -import com.dotmarketing.exception.DotRuntimeException; -import com.dotmarketing.util.Logger; -import com.dotmarketing.util.UtilMethods; -import io.vavr.Tuple; -import io.vavr.Tuple2; -import io.vavr.control.Try; -import org.apache.commons.io.IOUtils; - -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.io.Serializable; -import java.util.Optional; - -/** - * Implementation of the {@link AIClientStrategy} interface that provides a fallback mechanism - * for handling AI client requests. - * - *

- * This class attempts to send a request using a primary AI model and, if the request fails, - * it falls back to alternative models until a successful response is obtained or all models - * are exhausted. - *

- * - *

- * The fallback strategy ensures that the AI client can continue to function even if some models - * are not operational or fail to process the request. - *

- * - * @author vico - */ -public class AIModelFallbackStrategy implements AIClientStrategy { - - /** - * Applies the fallback strategy to the given AI client request and handles the response. - * - *

- * This method first attempts to send the request using the primary model. If the request - * fails, it falls back to alternative models until a successful response is obtained or - * all models are exhausted. - *

- * - * @param client the AI client to which the request is sent - * @param handler the response evaluator to handle the response - * @param request the AI request to be processed - * @param incoming the output stream to which the response will be written - * @return response data object - * @throws DotAIAllModelsExhaustedException if all models are exhausted and no successful response is obtained - */ - @Override - public AIResponseData applyStrategy(final AIClient client, - final AIResponseEvaluator handler, - final AIRequest request, - final OutputStream incoming) { - final JSONObjectAIRequest jsonRequest = AIClient.useRequestOrThrow(request); - final Tuple2 modelTuple = resolveModel(jsonRequest); - - final AIResponseData firstAttempt = sendRequest(client, handler, jsonRequest, incoming, modelTuple); - if (firstAttempt.isSuccess()) { - return firstAttempt; - } - - return runFallbacks(client, handler, jsonRequest, incoming, modelTuple); - } - - private static Tuple2 resolveModel(final JSONObjectAIRequest request) { - final AppConfig appConfig = request.getConfig(); - final String modelName = request.getPayload().optString(AiKeys.MODEL); - if (UtilMethods.isSet(modelName)) { - return appConfig.resolveModelOrThrow(modelName, request.getType()); - } - - final Optional aiModelOpt = AIModels.get().findModel(appConfig.getHost(), request.getType()); - if (aiModelOpt.isPresent()) { - final AIModel aiModel = aiModelOpt.get(); - if (aiModel.isOperational()) { - aiModel.repairCurrentIndexIfNeeded(); - return appConfig.resolveModelOrThrow(aiModel.getCurrentModel(), aiModel.getType()); - } - - notifyFailure(aiModel, request); - } - - throw new DotAIAllModelsExhaustedException(String.format("No models found for type [%s]", request.getType())); - } - - private static boolean isSameAsFirst(final Model firstAttempt, final Model model) { - return firstAttempt.equals(model); - } - - private static boolean isOperational(final Model model, final AppConfig config) { - if (!model.isOperational()) { - config.debugLogger( - AIModelFallbackStrategy.class, - () -> String.format("Model [%s] is not operational. Skipping.", model.getName())); - return false; - } - - return true; - } - - private static AIResponseData doSend(final AIClient client, - final JSONObjectAIRequest request, - final OutputStream incoming, - final boolean isStream) { - final OutputStream output = Optional.ofNullable(incoming).orElseGet(ByteArrayOutputStream::new); - client.sendRequest(request, output); - - return AIClientStrategy.response(output, isStream); - } - - private static void notifyFailure(final AIModel aiModel, final JSONObjectAIRequest request) { - AIAppValidator.get().validateModelsUsage(aiModel, request); - } - - private static void handleResponse(final Tuple2 modelTuple, - final JSONObjectAIRequest request, - final AIResponseData responseData) { - if (responseData.isSuccess()) { - request.getConfig().debugLogger( - AIModelFallbackStrategy.class, - () -> String.format("Model [%s] succeeded. No need to fallback.", modelTuple._2.getName())); - return; - } - - handleFailure(modelTuple, request, responseData); - } - - private static void handleFailure(final Tuple2 modelTuple, - final JSONObjectAIRequest request, - final AIResponseData responseData) { - logFailure(modelTuple, request, responseData); - - if (responseData.getStatus().doesNeedToThrow()) { - throw responseData.getException(); - } - - final AIModel aiModel = modelTuple._1; - final Model model = modelTuple._2; - final AppConfig appConfig = request.getConfig(); - - appConfig.debugLogger( - AIModelFallbackStrategy.class, - () -> String.format( - "Model [%s] failed then setting its status to [%s].", - model.getName(), - responseData.getStatus())); - model.setStatus(responseData.getStatus()); - - if (model.getIndex() == aiModel.getModels().size() - 1) { - aiModel.setCurrentModelIndex(AIModel.NOOP_INDEX); - appConfig.debugLogger( - AIModelFallbackStrategy.class, - () -> String.format( - "Model [%s] is the last one. Cannot fallback anymore.", - model.getName())); - - notifyFailure(aiModel, request); - - throw new DotAIAllModelsExhaustedException( - String.format("All models for type [%s] has been exhausted.", aiModel.getType())); - } else { - aiModel.setCurrentModelIndex(model.getIndex() + 1); - } - } - - private static AIResponseData sendRequest(final AIClient client, - final AIResponseEvaluator evaluator, - final JSONObjectAIRequest request, - final OutputStream output, - final Tuple2 modelTuple) { - final boolean isStream = AIClientStrategy.isStream(request) && output != null; - final AIResponseData responseData = Try - .of(() -> doSend(client, request, output, isStream)) - .getOrElseGet(exception -> fromException(evaluator, exception)); - - try { - if (responseData.isSuccess()) { - evaluator.fromResponse(responseData.getResponse(), responseData, !isStream); - } else { - handleException(request, modelTuple, responseData); - } - - handleResponse(modelTuple, request, responseData); - - if (responseData.isSuccess()) { - return responseData; - } - } catch (DotRuntimeException e) { - Logger.error(AIModelFallbackStrategy.class, - "Something went wrong while trying to process the request with the AI service." + e.getMessage()); - throw e; - } finally { - if (!isStream) { - IOUtils.closeQuietly(responseData.getOutput()); - } - } - - return responseData; - } - - private static void handleException(final JSONObjectAIRequest request, - final Tuple2 modelTuple, - final AIResponseData responseData) { - if (!modelTuple._1.isOperational()) { - request.getConfig().debugLogger( - AIModelFallbackStrategy.class, - () -> String.format( - "All models from type [%s] are not operational. Throwing exception.", - modelTuple._1.getType())); - notifyFailure(modelTuple._1, request); - } - - if (responseData.getStatus().doesNeedToThrow()) { - throw responseData.getException(); - } - } - - private static void logFailure(final Tuple2 modelTuple, - final JSONObjectAIRequest request, - final AIResponseData responseData) { - Optional - .ofNullable(responseData.getResponse()) - .ifPresentOrElse( - response -> request.getConfig().debugLogger( - AIModelFallbackStrategy.class, - () -> String.format( - "Model [%s] failed with response:%s%sTrying next model.", - modelTuple._2.getName(), - System.lineSeparator(), - response)), - () -> request.getConfig().debugLogger( - AIModelFallbackStrategy.class, - () -> String.format( - "Model [%s] failed with error: [%s]. Trying next model.", - modelTuple._2.getName(), - responseData.getError()))); - } - - private static AIResponseData fromException(final AIResponseEvaluator evaluator, final Throwable exception) { - final AIResponseData metadata = new AIResponseData(); - evaluator.fromException(exception, metadata); - return metadata; - } - - private static AIResponseData runFallbacks(final AIClient client, - final AIResponseEvaluator evaluator, - final JSONObjectAIRequest request, - final OutputStream output, - final Tuple2 modelTuple) { - for(final Model model : modelTuple._1.getModels()) { - if (isSameAsFirst(modelTuple._2, model) || !isOperational(model, request.getConfig())) { - continue; - } - - request.getPayload().put(AiKeys.MODEL, model.getName()); - final AIResponseData responseData = sendRequest( - client, - evaluator, - request, - output, - Tuple.of(modelTuple._1, model)); - if (responseData.isSuccess()) { - return responseData; - } - } - - return null; - } - -} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyClient.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyClient.java index b8776fe6db6c..3f5cfc57b04c 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyClient.java +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyClient.java @@ -1,7 +1,6 @@ package com.dotcms.ai.client; -import com.dotcms.ai.client.openai.OpenAIClient; -import com.dotcms.ai.client.openai.OpenAIResponseEvaluator; +import com.dotcms.ai.client.langchain4j.LangChain4jAIClient; import com.dotcms.ai.domain.AIProvider; import com.dotcms.ai.domain.AIResponse; import io.vavr.Lazy; @@ -39,9 +38,9 @@ public class AIProxyClient { private AIProxyClient() { proxiedClients = new ConcurrentHashMap<>(); addClient( - AIProvider.OPEN_AI.name(), - AIProxiedClient.of(OpenAIClient.get(), AIProxyStrategy.MODEL_FALLBACK, OpenAIResponseEvaluator.get())); - currentProvider = new AtomicReference<>(AIProvider.OPEN_AI); + AIProvider.LANGCHAIN4J.name(), + AIProxiedClient.of(LangChain4jAIClient.get(), AIProxyStrategy.DEFAULT)); + currentProvider = new AtomicReference<>(AIProvider.LANGCHAIN4J); } public static AIProxyClient get() { diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyStrategy.java b/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyStrategy.java index 08b2c34f0a6b..80f2e5b34e40 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyStrategy.java +++ b/dotCMS/src/main/java/com/dotcms/ai/client/AIProxyStrategy.java @@ -4,9 +4,8 @@ * Enumeration representing different strategies for proxying AI client requests. * *

- * This enum provides different strategies for handling AI client requests, including - * a default strategy and a model fallback strategy. Each strategy is associated with - * an implementation of the {@link AIClientStrategy} interface. + * This enum provides strategies for handling AI client requests. Each strategy is + * associated with an implementation of the {@link AIClientStrategy} interface. *

* *

@@ -18,8 +17,7 @@ */ public enum AIProxyStrategy { - DEFAULT(new AIDefaultStrategy()), - MODEL_FALLBACK(new AIModelFallbackStrategy()); + DEFAULT(new AIDefaultStrategy()); private final AIClientStrategy strategy; diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClient.java b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClient.java new file mode 100644 index 000000000000..32fae81c14a9 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClient.java @@ -0,0 +1,414 @@ +package com.dotcms.ai.client.langchain4j; + +import com.dotcms.ai.AiKeys; +import com.dotcms.ai.app.AIModelType; +import com.dotcms.ai.app.AppConfig; +import com.dotcms.ai.client.AIClient; +import com.dotcms.ai.client.AIRequest; +import com.dotcms.ai.client.JSONObjectAIRequest; +import com.dotcms.ai.domain.AIProvider; +import com.dotcms.ai.exception.DotAIAppConfigDisabledException; +import com.dotcms.ai.exception.DotAIClientConnectException; +import com.dotcms.rest.api.v1.DotObjectMapperProvider; +import com.dotmarketing.util.Logger; +import com.dotmarketing.util.json.JSONArray; +import com.dotmarketing.util.json.JSONObject; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.image.Image; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.image.ImageModel; +import dev.langchain4j.model.output.FinishReason; +import dev.langchain4j.model.output.Response; +import dev.langchain4j.model.output.TokenUsage; +import io.vavr.Lazy; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.util.concurrent.UncheckedExecutionException; + +/** + * {@link AIClient} implementation backed by LangChain4J. + * + *

Replaces the custom OpenAI HTTP client ({@code OpenAIClient}) with a unified LangChain4J + * abstraction layer that supports multiple AI providers without custom HTTP handling. + * + *

Model instances are cached per host and provider configuration to avoid rebuilding + * them on every request. The cache key is {@code hostname:configHash:type} where + * {@code configHash} is the SHA-256 hex digest of the {@code providerConfig} JSON (credentials + * are never stored in heap keys) and {@code type} is {@code chat}, {@code embeddings}, + * or {@code image}. + * + *

The response JSON is formatted in OpenAI-compatible structure so that all + * existing upper-layer code ({@code CompletionsAPIImpl}, {@code EmbeddingsAPIImpl}, etc.) + * can parse it without modification. + */ +public class LangChain4jAIClient implements AIClient { + + private static final Lazy INSTANCE = Lazy.of(LangChain4jAIClient::new); + private static final ObjectMapper MAPPER = DotObjectMapperProvider.createDefaultMapper(); + private static final long MODEL_CACHE_TTL_HOURS = 1; + private static final long STREAMING_TIMEOUT_SECONDS = 300; + + private final Cache chatModelCache = CacheBuilder.newBuilder() + .expireAfterWrite(MODEL_CACHE_TTL_HOURS, TimeUnit.HOURS) + .build(); + private final Cache streamingChatModelCache = CacheBuilder.newBuilder() + .expireAfterWrite(MODEL_CACHE_TTL_HOURS, TimeUnit.HOURS) + .build(); + private final Cache embeddingModelCache = CacheBuilder.newBuilder() + .expireAfterWrite(MODEL_CACHE_TTL_HOURS, TimeUnit.HOURS) + .build(); + private final Cache imageModelCache = CacheBuilder.newBuilder() + .expireAfterWrite(MODEL_CACHE_TTL_HOURS, TimeUnit.HOURS) + .build(); + + private LangChain4jAIClient() {} + + public static LangChain4jAIClient get() { + return INSTANCE.get(); + } + + /** + * Evicts cached model instances for the specified host. Should be called when the provider + * config for a host changes (e.g., API key rotation) to ensure stale credentials are not reused. + * + *

Cache keys are prefixed with the hostname, so only entries for the affected host + * are invalidated — other hosts' cached models are unaffected. + * + * @param hostname the hostname whose cached models should be evicted + */ + public void flushCachesForHost(final String hostname) { + final String prefix = hostname + ":"; + chatModelCache.asMap().keySet().removeIf(key -> key.startsWith(prefix)); + streamingChatModelCache.asMap().keySet().removeIf(key -> key.startsWith(prefix)); + embeddingModelCache.asMap().keySet().removeIf(key -> key.startsWith(prefix)); + imageModelCache.asMap().keySet().removeIf(key -> key.startsWith(prefix)); + } + + @Override + public AIProvider getProvider() { + return AIProvider.LANGCHAIN4J; + } + + /** + * Executes the AI request and writes an OpenAI-compatible JSON response to {@code output}. + * + *

Routing is determined by {@link AIModelType} in the request: + *

    + *
  • {@code TEXT} → chat completion
  • + *
  • {@code EMBEDDINGS} → embedding generation
  • + *
  • {@code IMAGE} → image generation
  • + *
+ */ + @Override + public void sendRequest(final AIRequest request, final OutputStream output) { + final JSONObjectAIRequest jsonRequest = AIClient.useRequestOrThrow(request); + final AppConfig appConfig = jsonRequest.getConfig(); + + if (!appConfig.isEnabled()) { + throw new DotAIAppConfigDisabledException("App dotAI config is not enabled — set providerConfig"); + } + + final String providerConfigJson = appConfig.getProviderConfig(); + final AIModelType type = jsonRequest.getType(); + final JSONObject payload = jsonRequest.getPayload(); + + AppConfig.debugLogger(appConfig, LangChain4jAIClient.class, + () -> "LangChain4jAIClient: type=" + type + " payload=" + payload.toString(2)); + + final String cacheKeyPrefix = appConfig.getHost() + ":" + appConfig.getProviderConfigHash(); + + if (type == AIModelType.IMAGE) { + writeToOutput(executeImageRequest(cacheKeyPrefix, providerConfigJson, payload), output); + } else if (type == AIModelType.EMBEDDINGS) { + writeToOutput(executeEmbeddingRequest(cacheKeyPrefix, providerConfigJson, payload), output); + } else if (Boolean.TRUE.equals(payload.opt(AiKeys.STREAM))) { + executeStreamingChatRequest(cacheKeyPrefix, providerConfigJson, payload, output); + } else { + writeToOutput(executeChatRequest(cacheKeyPrefix, providerConfigJson, payload), output); + } + } + + private String executeChatRequest(final String cacheKeyPrefix, final String providerConfigJson, final JSONObject payload) { + final ChatModel model; + try { + model = chatModelCache.get( + cacheKeyPrefix + ":chat", + () -> LangChain4jModelFactory.buildChatModel(parseSection(providerConfigJson, "chat"))); + } catch (ExecutionException | UncheckedExecutionException e) { + final Throwable cause = e.getCause() != null ? e.getCause() : e; + throw new IllegalArgumentException("Failed to initialize chat model: " + cause.getMessage(), cause); + } + + final List messages = toMessages(payload.optJSONArray(AiKeys.MESSAGES)); + if (messages.isEmpty()) { + throw new IllegalArgumentException("Chat request must contain at least one message"); + } + + final ChatResponse response = model.chat( + ChatRequest.builder().messages(messages).build()); + return toChatResponseJson(response); + } + + private void executeStreamingChatRequest(final String cacheKeyPrefix, + final String providerConfigJson, + final JSONObject payload, + final OutputStream output) { + final StreamingChatModel model; + try { + model = streamingChatModelCache.get( + cacheKeyPrefix + ":chat:streaming", + () -> LangChain4jModelFactory.buildStreamingChatModel(parseSection(providerConfigJson, "chat"))); + } catch (ExecutionException | UncheckedExecutionException e) { + final Throwable cause = e.getCause() != null ? e.getCause() : e; + throw new IllegalArgumentException("Failed to initialize streaming chat model: " + cause.getMessage(), cause); + } + + final List messages = toMessages(payload.optJSONArray(AiKeys.MESSAGES)); + if (messages.isEmpty()) { + throw new IllegalArgumentException("Chat request must contain at least one message"); + } + + final ChatRequest chatRequest = ChatRequest.builder().messages(messages).build(); + + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference error = new AtomicReference<>(); + final AtomicReference cancelled = new AtomicReference<>(false); + + model.chat(chatRequest, new StreamingChatResponseHandler() { + @Override + public void onPartialResponse(final String token) { + if (cancelled.get()) { + return; + } + try { + output.write(toSseChunk(token).getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + error.set(e); + latch.countDown(); + } + } + + @Override + public void onCompleteResponse(final ChatResponse response) { + try { + output.write("data: [DONE]\n\n".getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + Logger.warn(LangChain4jAIClient.class, "Failed to write [DONE] marker: " + e.getMessage()); + } finally { + latch.countDown(); + } + } + + @Override + public void onError(final Throwable e) { + error.set(e); + latch.countDown(); + } + }); + + try { + final boolean completed = latch.await(STREAMING_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!completed) { + cancelled.set(true); + throw new DotAIClientConnectException( + "Streaming timed out after " + STREAMING_TIMEOUT_SECONDS + " seconds", + new java.util.concurrent.TimeoutException()); + } + } catch (InterruptedException e) { + cancelled.set(true); + Thread.currentThread().interrupt(); + throw new DotAIClientConnectException("Streaming interrupted: " + e.getMessage(), e); + } + + if (error.get() != null) { + final Throwable t = error.get(); + throw new DotAIClientConnectException("Streaming failed: " + t.getMessage(), t); + } + } + + private static String toSseChunk(final String token) { + final JSONObject delta = new JSONObject(); + delta.put(AiKeys.CONTENT, token); + final JSONObject choice = new JSONObject(); + choice.put("delta", delta); + choice.put(AiKeys.INDEX, 0); + final JSONArray choices = new JSONArray(); + choices.put(choice); + final JSONObject chunk = new JSONObject(); + chunk.put("choices", choices); + return "data: " + chunk + "\n\n"; + } + + private void writeToOutput(final String responseJson, final OutputStream output) { + try { + output.write(responseJson.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + Logger.error(this, "Failed to write AI response to output stream: " + e.getMessage(), e); + throw new DotAIClientConnectException("Failed to write AI response to output stream: " + e.getMessage(), e); + } + } + + private String executeEmbeddingRequest(final String cacheKeyPrefix, final String providerConfigJson, final JSONObject payload) { + final EmbeddingModel model; + try { + model = embeddingModelCache.get( + cacheKeyPrefix + ":embeddings", + () -> LangChain4jModelFactory.buildEmbeddingModel(parseSection(providerConfigJson, "embeddings"))); + } catch (ExecutionException | UncheckedExecutionException e) { + final Throwable cause = e.getCause() != null ? e.getCause() : e; + throw new IllegalArgumentException("Failed to initialize embedding model: " + cause.getMessage(), cause); + } + + final String input = payload.getString(AiKeys.INPUT); + final Response response = model.embed(TextSegment.from(input)); + return toEmbeddingResponseJson(response.content()); + } + + private String executeImageRequest(final String cacheKeyPrefix, final String providerConfigJson, final JSONObject payload) { + final ImageModel model; + try { + model = imageModelCache.get( + cacheKeyPrefix + ":image", + () -> LangChain4jModelFactory.buildImageModel(parseSection(providerConfigJson, "image"))); + } catch (ExecutionException | UncheckedExecutionException e) { + final Throwable cause = e.getCause() != null ? e.getCause() : e; + throw new IllegalArgumentException("Failed to initialize image model: " + cause.getMessage(), cause); + } + + final String prompt = payload.getString(AiKeys.PROMPT); + final Response response = model.generate(prompt); + return toImageResponseJson(response.content()); + } + + static List toMessages(final JSONArray messagesArray) { + final List messages = new ArrayList<>(); + if (messagesArray == null) { + return messages; + } + for (int i = 0; i < messagesArray.length(); i++) { + final JSONObject msg = messagesArray.getJSONObject(i); + final String role = msg.optString(AiKeys.ROLE, AiKeys.USER).toLowerCase(); + final String content = msg.optString(AiKeys.CONTENT, ""); + if ("system".equals(role)) { + messages.add(new SystemMessage(content)); + } else if ("assistant".equals(role)) { + messages.add(new AiMessage(content)); + } else { + messages.add(new UserMessage(content)); + } + } + return messages; + } + + static String toChatResponseJson(final ChatResponse response) { + final JSONObject message = new JSONObject(); + message.put(AiKeys.ROLE, "assistant"); + message.put(AiKeys.CONTENT, response.aiMessage().text()); + + final FinishReason finishReason = response.finishReason(); + final JSONObject choice = new JSONObject(); + choice.put(AiKeys.MESSAGE, message); + choice.put(AiKeys.INDEX, 0); + choice.put("finish_reason", finishReason != null ? finishReason.name().toLowerCase() : "stop"); + choice.put("logprobs", JSONObject.NULL); + + final JSONArray choices = new JSONArray(); + choices.put(choice); + + final TokenUsage tokenUsage = response.tokenUsage(); + final JSONObject usage = new JSONObject(); + usage.put("prompt_tokens", tokenUsage != null && tokenUsage.inputTokenCount() != null ? tokenUsage.inputTokenCount() : 0); + usage.put("completion_tokens", tokenUsage != null && tokenUsage.outputTokenCount() != null ? tokenUsage.outputTokenCount() : 0); + usage.put("total_tokens", tokenUsage != null && tokenUsage.totalTokenCount() != null ? tokenUsage.totalTokenCount() : 0); + + final JSONObject result = new JSONObject(); + result.put("id", response.id() != null ? response.id() : "chatcmpl-langchain4j"); + result.put("object", "chat.completion"); + result.put("created", System.currentTimeMillis() / 1000); + result.put(AiKeys.MODEL, response.modelName() != null ? response.modelName() : "unknown"); + result.put("choices", choices); + result.put("usage", usage); + result.put("system_fingerprint", JSONObject.NULL); + return result.toString(); + } + + static String toEmbeddingResponseJson(final Embedding embedding) { + final JSONArray embeddingArray = new JSONArray(); + for (final float value : embedding.vector()) { + embeddingArray.put((double) value); + } + + final JSONObject data = new JSONObject(); + data.put(AiKeys.EMBEDDING, embeddingArray); + data.put(AiKeys.INDEX, 0); + data.put("object", "embedding"); + + final JSONArray dataArray = new JSONArray(); + dataArray.put(data); + + final JSONObject result = new JSONObject(); + result.put(AiKeys.DATA, dataArray); + return result.toString(); + } + + static String toImageResponseJson(final Image image) { + final JSONObject data = new JSONObject(); + if (image != null && image.url() != null) { + data.put(AiKeys.URL, image.url().toString()); + } else if (image != null && image.base64Data() != null) { + data.put(AiKeys.URL, ""); + data.put(AiKeys.B64_JSON, image.base64Data()); + } else { + data.put(AiKeys.URL, ""); + } + + final JSONArray dataArray = new JSONArray(); + dataArray.put(data); + + final JSONObject result = new JSONObject(); + result.put(AiKeys.DATA, dataArray); + return result.toString(); + } + + private static ProviderConfig parseSection(final String providerConfigJson, final String section) { + try { + final JsonNode root = MAPPER.readTree(providerConfigJson); + final JsonNode sectionNode = root.get(section); + if (sectionNode == null) { + throw new IllegalArgumentException( + "Missing '" + section + "' section in providerConfig"); + } + return MAPPER.treeToValue(sectionNode, ProviderConfig.class); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new IllegalArgumentException( + "Failed to parse '" + section + "' from providerConfig: " + e.getMessage(), e); + } + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jModelFactory.java b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jModelFactory.java new file mode 100644 index 000000000000..c7eeb2a585ef --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jModelFactory.java @@ -0,0 +1,237 @@ +package com.dotcms.ai.client.langchain4j; + +import dev.langchain4j.model.azure.AzureOpenAiChatModel; +import dev.langchain4j.model.azure.AzureOpenAiEmbeddingModel; +import dev.langchain4j.model.azure.AzureOpenAiImageModel; +import dev.langchain4j.model.azure.AzureOpenAiStreamingChatModel; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.image.ImageModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.model.openai.OpenAiEmbeddingModel; +import dev.langchain4j.model.openai.OpenAiImageModel; +import dev.langchain4j.model.openai.OpenAiStreamingChatModel; + +import java.time.Duration; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Factory for creating LangChain4J model instances from a {@link ProviderConfig}. + * + *

This is the only class that contains provider-specific builder logic. + * To add support for a new provider, add a case to each switch block below. + * No other class needs to change. + * + *

Supported providers: {@code openai}, {@code azure_openai} + *

Planned (Phase 2): {@code bedrock}, {@code vertex_ai} + */ +public class LangChain4jModelFactory { + + private LangChain4jModelFactory() {} + + /** + * Builds a {@link ChatModel} for the given provider configuration. + * + * @param config provider-specific configuration for the chat section + * @return a configured {@link ChatModel} + * @throws IllegalArgumentException if config or provider is null, or the provider is unsupported + */ + public static ChatModel buildChatModel(final ProviderConfig config) { + return build(config, "chat", + LangChain4jModelFactory::buildOpenAiChatModel, + LangChain4jModelFactory::buildAzureOpenAiChatModel); + } + + /** + * Builds a {@link StreamingChatModel} for the given provider configuration. + * + * @param config provider-specific configuration for the chat section + * @return a configured {@link StreamingChatModel} + * @throws IllegalArgumentException if config or provider is null, or the provider is unsupported + */ + public static StreamingChatModel buildStreamingChatModel(final ProviderConfig config) { + return build(config, "chat", + LangChain4jModelFactory::buildOpenAiStreamingChatModel, + LangChain4jModelFactory::buildAzureOpenAiStreamingChatModel); + } + + /** + * Builds an {@link EmbeddingModel} for the given provider configuration. + * + * @param config provider-specific configuration for the embeddings section + * @return a configured {@link EmbeddingModel} + * @throws IllegalArgumentException if config or provider is null, or the provider is unsupported + */ + public static EmbeddingModel buildEmbeddingModel(final ProviderConfig config) { + return build(config, "embeddings", + LangChain4jModelFactory::buildOpenAiEmbeddingModel, + LangChain4jModelFactory::buildAzureOpenAiEmbeddingModel); + } + + /** + * Builds an {@link ImageModel} for the given provider configuration. + * + * @param config provider-specific configuration for the image section + * @return a configured {@link ImageModel} + * @throws IllegalArgumentException if config or provider is null, or the provider is unsupported + */ + public static ImageModel buildImageModel(final ProviderConfig config) { + return build(config, "image", + LangChain4jModelFactory::buildOpenAiImageModel, + LangChain4jModelFactory::buildAzureOpenAiImageModel); + } + + private static T build(final ProviderConfig config, + final String modelType, + final Function openAiFn, + final Function azureOpenAiFn) { + if (config == null || config.provider() == null) { + throw new IllegalArgumentException("ProviderConfig or provider name is null for model type: " + modelType); + } + requireNonBlank(config.model(), "model", modelType); + switch (config.provider().toLowerCase()) { + case "openai": + validateOpenAi(config, modelType); + return openAiFn.apply(config); + case "azure_openai": + validateAzureOpenAi(config, modelType); + return azureOpenAiFn.apply(config); + default: + throw new IllegalArgumentException("Unsupported " + modelType + " provider: " + + config.provider() + ". Supported: openai, azure_openai"); + } + } + + private static void validateOpenAi(final ProviderConfig config, final String modelType) { + requireNonBlank(config.apiKey(), "apiKey", modelType); + } + + private static void validateAzureOpenAi(final ProviderConfig config, final String modelType) { + requireNonBlank(config.apiKey(), "apiKey", modelType); + requireNonBlank(config.endpoint(), "endpoint", modelType); + } + + private static void requireNonBlank(final String value, final String field, final String modelType) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException( + "providerConfig." + modelType + "." + field + " is required but was not set"); + } + } + + // ── OpenAI builders ─────────────────────────────────────────────────────── + + private static void applyCommonConfig(final ProviderConfig config, + final Consumer baseUrlFn, + final Consumer retriesFn, + final Consumer timeoutFn) { + if (config.endpoint() != null) baseUrlFn.accept(config.endpoint()); + if (config.maxRetries() != null) retriesFn.accept(config.maxRetries()); + if (config.timeout() != null) timeoutFn.accept(Duration.ofSeconds(config.timeout())); + } + + private static ChatModel buildOpenAiChatModel(final ProviderConfig config) { + final OpenAiChatModel.OpenAiChatModelBuilder builder = OpenAiChatModel.builder() + .apiKey(config.apiKey()) + .modelName(config.model()); + applyCommonConfig(config, builder::baseUrl, builder::maxRetries, builder::timeout); + if (config.temperature() != null) { + builder.temperature(config.temperature()); + } + if (config.maxCompletionTokens() != null) { + builder.maxCompletionTokens(config.maxCompletionTokens()); + } else if (config.maxTokens() != null) { + builder.maxTokens(config.maxTokens()); + } + return builder.build(); + } + + private static StreamingChatModel buildOpenAiStreamingChatModel(final ProviderConfig config) { + final OpenAiStreamingChatModel.OpenAiStreamingChatModelBuilder builder = OpenAiStreamingChatModel.builder() + .apiKey(config.apiKey()) + .modelName(config.model()); + applyCommonConfig(config, builder::baseUrl, ignored -> {}, builder::timeout); + if (config.temperature() != null) builder.temperature(config.temperature()); + if (config.maxCompletionTokens() != null) { + builder.maxCompletionTokens(config.maxCompletionTokens()); + } else if (config.maxTokens() != null) { + builder.maxTokens(config.maxTokens()); + } + return builder.build(); + } + + private static EmbeddingModel buildOpenAiEmbeddingModel(final ProviderConfig config) { + final OpenAiEmbeddingModel.OpenAiEmbeddingModelBuilder builder = OpenAiEmbeddingModel.builder() + .apiKey(config.apiKey()) + .modelName(config.model()); + applyCommonConfig(config, builder::baseUrl, builder::maxRetries, builder::timeout); + if (config.dimensions() != null) { + builder.dimensions(config.dimensions()); + } + return builder.build(); + } + + private static ImageModel buildOpenAiImageModel(final ProviderConfig config) { + final OpenAiImageModel.OpenAiImageModelBuilder builder = OpenAiImageModel.builder() + .apiKey(config.apiKey()) + .modelName(config.model()); + applyCommonConfig(config, builder::baseUrl, builder::maxRetries, builder::timeout); + if (config.size() != null) { + builder.size(config.size()); + } + return builder.build(); + } + + // ── Azure OpenAI builders ───────────────────────────────────────────────── + + private static StreamingChatModel buildAzureOpenAiStreamingChatModel(final ProviderConfig config) { + final AzureOpenAiStreamingChatModel.Builder builder = AzureOpenAiStreamingChatModel.builder() + .apiKey(config.apiKey()) + .endpoint(config.endpoint()) + .deploymentName(config.deploymentName() != null ? config.deploymentName() : config.model()); + if (config.apiVersion() != null) builder.serviceVersion(config.apiVersion()); + if (config.maxRetries() != null) builder.maxRetries(config.maxRetries()); + if (config.timeout() != null) builder.timeout(Duration.ofSeconds(config.timeout())); + if (config.temperature() != null) builder.temperature(config.temperature()); + if (config.maxTokens() != null) builder.maxTokens(config.maxTokens()); + return builder.build(); + } + + private static ChatModel buildAzureOpenAiChatModel(final ProviderConfig config) { + final AzureOpenAiChatModel.Builder builder = AzureOpenAiChatModel.builder() + .apiKey(config.apiKey()) + .endpoint(config.endpoint()) + .deploymentName(config.deploymentName() != null ? config.deploymentName() : config.model()); + if (config.apiVersion() != null) builder.serviceVersion(config.apiVersion()); + if (config.maxRetries() != null) builder.maxRetries(config.maxRetries()); + if (config.timeout() != null) builder.timeout(Duration.ofSeconds(config.timeout())); + if (config.temperature() != null) builder.temperature(config.temperature()); + if (config.maxTokens() != null) builder.maxTokens(config.maxTokens()); + return builder.build(); + } + + private static EmbeddingModel buildAzureOpenAiEmbeddingModel(final ProviderConfig config) { + final AzureOpenAiEmbeddingModel.Builder builder = AzureOpenAiEmbeddingModel.builder() + .apiKey(config.apiKey()) + .endpoint(config.endpoint()) + .deploymentName(config.deploymentName() != null ? config.deploymentName() : config.model()); + if (config.apiVersion() != null) builder.serviceVersion(config.apiVersion()); + if (config.maxRetries() != null) builder.maxRetries(config.maxRetries()); + if (config.timeout() != null) builder.timeout(Duration.ofSeconds(config.timeout())); + return builder.build(); + } + + private static ImageModel buildAzureOpenAiImageModel(final ProviderConfig config) { + final AzureOpenAiImageModel.Builder builder = AzureOpenAiImageModel.builder() + .apiKey(config.apiKey()) + .endpoint(config.endpoint()) + .deploymentName(config.deploymentName() != null ? config.deploymentName() : config.model()); + if (config.apiVersion() != null) builder.serviceVersion(config.apiVersion()); + if (config.maxRetries() != null) builder.maxRetries(config.maxRetries()); + if (config.timeout() != null) builder.timeout(Duration.ofSeconds(config.timeout())); + if (config.size() != null) builder.size(config.size()); + return builder.build(); + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/ProviderConfig.java b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/ProviderConfig.java new file mode 100644 index 000000000000..26445210c6a4 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/ProviderConfig.java @@ -0,0 +1,80 @@ +package com.dotcms.ai.client.langchain4j; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.immutables.value.Value; + +import javax.annotation.Nullable; + +/** + * Immutable representation of a single provider section in the {@code providerConfig} JSON. + * + *

Each section (chat, embeddings, image) in the JSON maps to one instance of this class. + * Unknown fields are ignored to allow forward-compatible configuration. + * + *

Common fields (all providers): + *

    + *
  • {@code provider} – identifier: {@code openai}, {@code azure_openai}, {@code bedrock}, {@code vertex_ai}
  • + *
  • {@code model} – model name or ID
  • + *
  • {@code maxTokens} – max output tokens
  • + *
  • {@code temperature} – sampling temperature (0.0–2.0)
  • + *
  • {@code maxRetries} – retry attempts on transient failures
  • + *
  • {@code timeout} – request timeout in seconds
  • + *
+ * + *

OpenAI / Azure OpenAI: + *

    + *
  • {@code apiKey}
  • + *
  • {@code size} – image size, e.g. {@code 1024x1024} (image only)
  • + *
  • {@code dimensions} – embedding vector size (embeddings only); required for models like {@code text-embedding-3-small/large}
  • + *
  • {@code endpoint} – Azure base URL
  • + *
  • {@code deploymentName} – Azure deployment name
  • + *
  • {@code apiVersion} – Azure API version, e.g. {@code 2024-02-01}
  • + *
+ * + *

AWS Bedrock: + *

    + *
  • {@code region}
  • + *
  • {@code accessKeyId}
  • + *
  • {@code secretAccessKey}
  • + *
+ * + *

Google Vertex AI: + *

    + *
  • {@code projectId}
  • + *
  • {@code location}
  • + *
+ */ +@Value.Immutable +@JsonSerialize(as = ImmutableProviderConfig.class) +@JsonDeserialize(as = ImmutableProviderConfig.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface ProviderConfig { + + @Nullable String provider(); + @Nullable String model(); + @Nullable Integer maxTokens(); + @Nullable Integer maxCompletionTokens(); + @Nullable Double temperature(); + @Nullable Integer maxRetries(); + @Nullable Integer timeout(); + + // OpenAI / Azure OpenAI + @Value.Redacted @Nullable String apiKey(); + @Nullable String size(); + @Nullable Integer dimensions(); + @Nullable String endpoint(); + @Nullable String deploymentName(); + @Nullable String apiVersion(); + + // AWS Bedrock + @Nullable String region(); + @Value.Redacted @Nullable String accessKeyId(); + @Value.Redacted @Nullable String secretAccessKey(); + + // Google Vertex AI + @Nullable String projectId(); + @Nullable String location(); + +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIClient.java b/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIClient.java deleted file mode 100644 index 36d138266642..000000000000 --- a/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIClient.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.dotcms.ai.client.openai; - -import com.dotcms.ai.AiKeys; -import com.dotcms.ai.app.AIModel; -import com.dotcms.ai.app.AIModels; -import com.dotcms.ai.app.AppConfig; -import com.dotcms.ai.app.AppKeys; -import com.dotcms.ai.client.AIClient; -import com.dotcms.ai.client.AIRequest; -import com.dotcms.ai.client.JSONObjectAIRequest; -import com.dotcms.ai.domain.AIProvider; -import com.dotcms.ai.domain.Model; -import com.dotcms.ai.exception.DotAIAppConfigDisabledException; -import com.dotcms.ai.exception.DotAIClientConnectException; -import com.dotcms.ai.exception.DotAIModelNotFoundException; -import com.dotcms.ai.exception.DotAIModelNotOperationalException; -import com.dotcms.business.SystemTableUpdatedKeyEvent; -import com.dotcms.http.CircuitBreakerUrl; -import com.dotcms.rest.exception.GenericHttpStatusCodeException; -import com.dotcms.system.event.local.model.EventSubscriber; -import com.dotmarketing.util.Config; -import com.dotmarketing.util.Logger; -import com.dotmarketing.util.json.JSONObject; -import io.vavr.Lazy; -import io.vavr.Tuple2; - -import javax.ws.rs.HttpMethod; -import javax.ws.rs.core.Response; -import java.io.OutputStream; -import java.io.Serializable; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Implementation of the {@link AIClient} interface for interacting with the OpenAI service. - * - *

- * This class provides methods to send requests to the OpenAI service and handle responses. - * It includes functionality to manage rate limiting and ensure that models are operational - * before sending requests. - *

- * - *

- * The class uses a singleton pattern to ensure a single instance of the client is used - * throughout the application. It also maintains a record of the last REST call for each - * model to enforce rate limiting. - *

- * - * @auhor vico - */ -public class OpenAIClient implements AIClient { - - private static final String AI_OPEN_AI_TIMEOUT_KEY = "AI_OPEN_AI_TIMEOUT_KEY"; - private static final String AI_OPEN_AI_ATTEMPTS_KEY = "AI_OPEN_AI_ATTEMPTS_KEY"; - private static final Lazy INSTANCE = Lazy.of(OpenAIClient::new); - - public static OpenAIClient get() { - return INSTANCE.get(); - } - - private static long resolveTimeout() { - return Config.getLongProperty(AI_OPEN_AI_TIMEOUT_KEY, 120 * 1000L); - } - - private static int resolveAttempts() { - return Config.getIntProperty(AI_OPEN_AI_ATTEMPTS_KEY, 3); - } - - private static CircuitBreakerUrl.Method resolveMethod(final JSONObjectAIRequest request) { - switch(request.getMethod().toUpperCase()) { - case HttpMethod.POST: - return CircuitBreakerUrl.Method.POST; - case HttpMethod.PUT: - return CircuitBreakerUrl.Method.PUT; - case HttpMethod.DELETE: - return CircuitBreakerUrl.Method.DELETE; - case HttpMethod.PATCH: - return CircuitBreakerUrl.Method.PATCH; - case HttpMethod.GET: - default: - return CircuitBreakerUrl.Method.GET; - } - } - - - - - /** - * {@inheritDoc} - */ - @Override - public AIProvider getProvider() { - return AIProvider.OPEN_AI; - } - - /** - * {@inheritDoc} - */ - @Override - public void sendRequest(final AIRequest request, final OutputStream output) { - final JSONObjectAIRequest jsonRequest = AIClient.useRequestOrThrow(request); - final AppConfig appConfig = jsonRequest.getConfig(); - - appConfig.debugLogger( - OpenAIClient.class, - () -> String.format( - "Posting to [%s] with method [%s]%s with app config:%s%s the payload: %s", - jsonRequest.getUrl(), - jsonRequest.getMethod(), - System.lineSeparator(), - appConfig, - System.lineSeparator(), - jsonRequest.payloadToString())); - - if (!appConfig.isEnabled()) { - appConfig.debugLogger(OpenAIClient.class, () -> "App dotAI is not enabled and will not send request."); - throw new DotAIAppConfigDisabledException("App dotAI config without API urls or API key"); - } - - final JSONObject payload = jsonRequest.getPayload(); - final String modelName = Optional - .ofNullable(payload.optString(AiKeys.MODEL)) - .orElseThrow(() -> new DotAIModelNotFoundException("Model is not present in the request")); - final Tuple2 modelTuple = appConfig.resolveModelOrThrow(modelName, jsonRequest.getType()); - //final AIModel aiModel = modelTuple._1; - - if (!modelTuple._2.isOperational()) { - appConfig.debugLogger( - getClass(), - () -> String.format("Resolved model [%s] is not operational, avoiding its usage", modelName)); - throw new DotAIModelNotOperationalException(String.format("Model [%s] is not operational", modelName)); - } - - try { - CircuitBreakerUrl.builder() - .setMethod(resolveMethod(jsonRequest)) - .setAuthHeaders(AIModels.BEARER + appConfig.getApiKey()) - .setUrl(jsonRequest.getUrl()) - .setRawData(payload.toString()) - .setTimeout(resolveTimeout()) - .setTryAgainAttempts(resolveAttempts()) - .setOverrideException(statusCode -> resolveException(jsonRequest, modelName, statusCode)) - .setRaiseFailsafe(true) - .build() - .doOut(output); - } catch (DotAIModelNotFoundException e) { - throw e; - } catch (Exception e) { - if (appConfig.getConfigBoolean(AppKeys.DEBUG_LOGGING)) { - Logger.warn(this, "INVALID REQUEST: " + e.getMessage(), e); - } else { - Logger.warn(this, "INVALID REQUEST: " + e.getMessage()); - } - - Logger.warn(this, " - " + jsonRequest.getMethod() + " : " + payload); - - throw new DotAIClientConnectException("Error while sending request to OpenAI", e); - } - } - - private Exception resolveException(final JSONObjectAIRequest jsonRequest, - final String modelName, - final int statusCode) { - return statusCode == 404 - ? new DotAIModelNotFoundException(String.format("Model [%s] not found", modelName)) - : new GenericHttpStatusCodeException( - String.format( - "Got invalid response for url: [%s] response: [%d]", - jsonRequest.getUrl(), - statusCode), - Response.Status.fromStatusCode(statusCode)); - } -} diff --git a/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIResponseEvaluator.java b/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIResponseEvaluator.java deleted file mode 100644 index 268fc834cdfd..000000000000 --- a/dotCMS/src/main/java/com/dotcms/ai/client/openai/OpenAIResponseEvaluator.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.dotcms.ai.client.openai; - -import com.dotcms.ai.AiKeys; -import com.dotcms.ai.client.AIResponseEvaluator; -import com.dotcms.ai.domain.AIResponseData; -import com.dotcms.ai.domain.ModelStatus; -import com.dotcms.ai.exception.DotAIModelNotFoundException; -import com.dotcms.ai.exception.DotAIModelNotOperationalException; -import com.dotcms.rest.exception.GenericHttpStatusCodeException; -import com.dotmarketing.exception.DotRuntimeException; -import com.dotmarketing.util.json.JSONObject; -import io.vavr.Lazy; - -import javax.ws.rs.core.Response; -import java.util.Optional; -import java.util.stream.Stream; - -/** - * Evaluates AI responses from OpenAI and updates the provided metadata. - * This class implements the singleton pattern and provides methods to process responses and exceptions. - * - *

Methods:

- *
    - *
  • \fromResponse\ - Processes a response string and updates the metadata.
  • - *
  • \fromThrowable\ - Processes an exception and updates the metadata.
  • - *
- * - * @author vico - */ -public class OpenAIResponseEvaluator implements AIResponseEvaluator { - - private static final String JSON_ERROR_FIELD = "\"error\":"; - private static final Lazy INSTANCE = Lazy.of(OpenAIResponseEvaluator::new); - - public static OpenAIResponseEvaluator get() { - return INSTANCE.get(); - } - - private OpenAIResponseEvaluator() {} - - /** - * {@inheritDoc} - */ - @Override - public void fromResponse(final String response, final AIResponseData metadata, final boolean jsonExpected) { - Optional.ofNullable(response) - .ifPresent(resp -> { - if (jsonExpected || resp.contains(JSON_ERROR_FIELD)) { - final JSONObject jsonResponse = new JSONObject(resp); - if (jsonResponse.has(AiKeys.ERROR)) { - final JSONObject error = jsonResponse.getJSONObject(AiKeys.ERROR); - final String message = error.getString(AiKeys.MESSAGE); - metadata.setError(message); - metadata.setStatus(resolveStatus(message)); - } - } - }); - } - - /** - * {@inheritDoc} - */ - @Override - public void fromException(final Throwable exception, final AIResponseData metadata) { - metadata.setError(exception.getMessage()); - metadata.setStatus(resolveStatus(exception)); - metadata.setException(exception instanceof DotRuntimeException - ? (DotRuntimeException) exception - : new DotRuntimeException(exception)); - } - - private ModelStatus resolveStatus(final String error) { - if (error.contains("has been deprecated")) { - return ModelStatus.DECOMMISSIONED; - } else if (error.contains("does not exist or you do not have access to it")) { - return ModelStatus.INVALID; - } else { - return ModelStatus.UNKNOWN; - } - } - - private ModelStatus resolveStatus(final Throwable throwable) { - if (Stream - .of(DotAIModelNotFoundException.class, DotAIModelNotOperationalException.class) - .anyMatch(exception -> exception.isInstance(throwable))) { - return ModelStatus.INVALID; - } - - - if (throwable instanceof GenericHttpStatusCodeException) { - final GenericHttpStatusCodeException statusCodeException = (GenericHttpStatusCodeException) throwable; - final Response.Status status = Response.Status.fromStatusCode(statusCodeException.getResponse().getStatus()); - if (status == Response.Status.NOT_FOUND) { - return ModelStatus.INVALID; - } - } - - return ModelStatus.UNKNOWN; - } -} diff --git a/dotCMS/src/main/java/com/dotcms/ai/domain/AIProvider.java b/dotCMS/src/main/java/com/dotcms/ai/domain/AIProvider.java index 9e844d47619f..fcc4ca22576e 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/domain/AIProvider.java +++ b/dotCMS/src/main/java/com/dotcms/ai/domain/AIProvider.java @@ -18,9 +18,12 @@ public enum AIProvider { NONE("None"), + LANGCHAIN4J("LangChain4J"), OPEN_AI("OpenAI"), + AZURE_OPENAI("Azure OpenAI"), BEDROCK("Amazon Bedrock"), - GEMINI("Google Gemini"); + GEMINI("Google Gemini"), + VERTEX_AI("Google Vertex AI"); private final String provider; diff --git a/dotCMS/src/main/java/com/dotcms/ai/listener/AIAppListener.java b/dotCMS/src/main/java/com/dotcms/ai/listener/AIAppListener.java index a95b8f8df520..491ca828353f 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/listener/AIAppListener.java +++ b/dotCMS/src/main/java/com/dotcms/ai/listener/AIAppListener.java @@ -1,10 +1,7 @@ package com.dotcms.ai.listener; -import com.dotcms.ai.app.AIModels; -import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.AppKeys; -import com.dotcms.ai.app.ConfigService; -import com.dotcms.ai.validator.AIAppValidator; +import com.dotcms.ai.client.langchain4j.LangChain4jAIClient; import com.dotcms.security.apps.AppSecretSavedEvent; import com.dotcms.system.event.local.model.EventSubscriber; import com.dotcms.system.event.local.model.KeyFilterable; @@ -45,8 +42,7 @@ public AIAppListener() { *
    *
  • Logs a debug message if the event is null or the event's host identifier is blank.
  • *
  • Finds the host associated with the event's host identifier.
  • - *
  • Resets the AI models for the found host's hostname.
  • - *
  • Validates the AI configuration using the {@link AIAppValidator}.
  • + *
  • Evicts cached LangChain4J model instances for the host so new credentials take effect immediately.
  • *
*

* @@ -67,10 +63,7 @@ public void notify(final AppSecretSavedEvent event) { final String hostId = event.getHostIdentifier(); final Host host = Try.of(() -> hostAPI.find(hostId, APILocator.systemUser(), false)).getOrNull(); - Optional.ofNullable(host).ifPresent(found -> AIModels.get().resetModels(found.getHostname())); - final AppConfig appConfig = ConfigService.INSTANCE.config(host); - - AIAppValidator.get().validateAIConfig(appConfig, event.getUserId()); + Optional.ofNullable(host).ifPresent(found -> LangChain4jAIClient.get().flushCachesForHost(found.getHostname())); } @Override diff --git a/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModel.java b/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModel.java deleted file mode 100644 index eeebccd7c12f..000000000000 --- a/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModel.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.dotcms.ai.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.io.Serializable; - -/** - * Represents an OpenAI model with details such as ID, object type, creation timestamp, and owner. - * This class is immutable and uses Jackson annotations for JSON serialization and deserialization. - * - * @author vico - */ - public class OpenAIModel implements Serializable { - - private final String id; - private final String object; - private final long created; - private final String ownedBy; - - @JsonCreator - public OpenAIModel(@JsonProperty("id") final String id, - @JsonProperty("object") final String object, - @JsonProperty("created") final long created, - @JsonProperty("owned_by") final String ownedBy) { - this.id = id; - this.object = object; - this.created = created; - this.ownedBy = ownedBy; - } - - public String getId() { - return id; - } - - public String getObject() { - return object; - } - - public long getCreated() { - return created; - } - - @JsonProperty("owned_by") - public String getOwnedBy() { - return ownedBy; - } - -} diff --git a/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModels.java b/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModels.java deleted file mode 100644 index b98317557246..000000000000 --- a/dotCMS/src/main/java/com/dotcms/ai/model/OpenAIModels.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.dotcms.ai.model; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.io.Serializable; -import java.util.List; - -/** - * Represents a collection of OpenAI models with details such as the type of object and the list of models. - * This class is immutable and uses Jackson annotations for JSON serialization and deserialization. - * - * @author vico - */ -public class OpenAIModels implements Serializable { - - private final String object; - private final List data; - private final OpenAIError error; - - @JsonCreator - public OpenAIModels(@JsonProperty("object") final String object, - @JsonProperty("data") final List data, - @JsonProperty("error") final OpenAIError error) { - this.object = object; - this.data = data; - this.error = error; - } - - public String getObject() { - return object; - } - - public List getData() { - return data; - } - - public OpenAIError getError() { - return error; - } - - public static class OpenAIError { - - private final String message; - private final String type; - private final String param; - private final String code; - - @JsonCreator - public OpenAIError(@JsonProperty("object") final String message, - @JsonProperty("type") final String type, - @JsonProperty("param") final String param, - @JsonProperty("code") final String code) { - this.message = message; - this.type = type; - this.param = param; - this.code = code; - } - - public String getMessage() { - return message; - } - - public String getType() { - return type; - } - - public String getParam() { - return param; - } - - public String getCode() { - return code; - } - } - -} diff --git a/dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java b/dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java index 4016b76fa4f3..b86a81b61b3c 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java +++ b/dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java @@ -1,19 +1,21 @@ package com.dotcms.ai.rest; import com.dotcms.ai.AiKeys; -import com.dotcms.ai.app.AIModels; import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.AppKeys; import com.dotcms.ai.app.ConfigService; -import com.dotcms.ai.model.SimpleModel; import com.dotcms.ai.rest.forms.CompletionsForm; import com.dotcms.ai.util.LineReadingOutputStream; import com.dotcms.rest.WebResource; +import com.dotcms.rest.api.v1.DotObjectMapperProvider; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.web.WebAPILocator; -import com.dotmarketing.util.UtilMethods; +import com.dotmarketing.util.Logger; import com.dotmarketing.util.json.JSONObject; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.liferay.portal.model.User; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -37,8 +39,10 @@ import javax.ws.rs.core.StreamingOutput; import java.io.OutputStream; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; @@ -50,6 +54,9 @@ @Tag(name = "AI", description = "AI-powered content generation and analysis endpoints") public class CompletionsResource { + private static final ObjectMapper REDACTION_MAPPER = DotObjectMapperProvider.createDefaultMapper(); + private static final Set CREDENTIAL_FIELDS = Set.of("apiKey", "secretAccessKey", "accessKeyId"); + /** * Handles POST requests to generate completions based on a given prompt. * @@ -170,19 +177,50 @@ public final Response getConfig(@Context final HttpServletRequest request, final Map map = new HashMap<>(); map.put(AiKeys.CONFIG_HOST, host.getHostname() + " (falls back to system host)"); - for (final AppKeys config : AppKeys.values()) { - map.put(config.key, appConfig.getConfig(config)); - } - final String apiKey = UtilMethods.isSet(appConfig.getApiKey()) ? "*****" : "NOT SET"; - map.put(AppKeys.API_KEY.key, apiKey); + final String providerConfig = appConfig.getProviderConfig(); + if (StringUtils.isNotBlank(providerConfig)) { + map.put(AppKeys.PROVIDER_CONFIG.key, redactCredentials(providerConfig)); + } - final List models = AIModels.get().getAvailableModels(); - map.put(AiKeys.AVAILABLE_MODELS, models); + map.put(AppKeys.ROLE_PROMPT.key, appConfig.getRolePrompt()); + map.put(AppKeys.TEXT_PROMPT.key, appConfig.getTextPrompt()); + map.put(AppKeys.IMAGE_PROMPT.key, appConfig.getImagePrompt()); + map.put(AppKeys.IMAGE_SIZE.key, appConfig.getImageSize()); + map.put(AppKeys.LISTENER_INDEXER.key, appConfig.getListenerIndexer()); + map.put(AppKeys.DEBUG_LOGGING.key, appConfig.getConfig(AppKeys.DEBUG_LOGGING)); return Response.ok(map).build(); } + private static String redactCredentials(final String json) { + try { + final JsonNode root = REDACTION_MAPPER.readTree(json); + redactNode(root); + return REDACTION_MAPPER.writeValueAsString(root); + } catch (Exception e) { + Logger.warn(CompletionsResource.class, "Failed to parse providerConfig for redaction: " + e.getMessage(), e); + return "[CONFIG PRESENT — REDACTION FAILED]"; + } + } + + private static void redactNode(final JsonNode node) { + if (node.isObject()) { + final ObjectNode obj = (ObjectNode) node; + final Iterator> fields = obj.fields(); + while (fields.hasNext()) { + final Map.Entry field = fields.next(); + if (CREDENTIAL_FIELDS.contains(field.getKey())) { + obj.put(field.getKey(), "*****"); + } else { + redactNode(field.getValue()); + } + } + } else if (node.isArray()) { + node.forEach(CompletionsResource::redactNode); + } + } + private static Response badRequestResponse() { return Response.status(Response.Status.BAD_REQUEST).entity(Map.of(AiKeys.ERROR, "query required")).build(); } @@ -222,9 +260,8 @@ private static Response getResponse(final HttpServletRequest request, } final long startTime = System.currentTimeMillis(); - final CompletionsForm resolvedForm = resolveForm(request, response, formIn); - if (resolvedForm.stream) { + if (formIn.stream) { final StreamingOutput streaming = output -> { outputStream.accept(output); output.flush(); diff --git a/dotCMS/src/main/java/com/dotcms/ai/rest/ImageResource.java b/dotCMS/src/main/java/com/dotcms/ai/rest/ImageResource.java index abd877a20113..7607248cbd6a 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/rest/ImageResource.java +++ b/dotCMS/src/main/java/com/dotcms/ai/rest/ImageResource.java @@ -100,7 +100,7 @@ public Response handleImageRequest(@Context final HttpServletRequest request, readParameters(request.getParameterMap()), Marshaller.marshal(aiImageRequestDTO))); final AppConfig config = ConfigService.INSTANCE.config(WebAPILocator.getHostWebAPI().getHost(request)); - if (UtilMethods.isEmpty(config.getApiKey())) { + if (!config.isEnabled()) { return Response .status(Status.INTERNAL_SERVER_ERROR) .entity(Map.of(AiKeys.ERROR, "App Config missing")) diff --git a/dotCMS/src/main/java/com/dotcms/ai/validator/AIAppValidator.java b/dotCMS/src/main/java/com/dotcms/ai/validator/AIAppValidator.java deleted file mode 100644 index 196f207750be..000000000000 --- a/dotCMS/src/main/java/com/dotcms/ai/validator/AIAppValidator.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.dotcms.ai.validator; - -import com.dotcms.ai.app.AIModel; -import com.dotcms.ai.app.AIModels; -import com.dotcms.ai.app.AppConfig; -import com.dotcms.ai.app.InvalidAIKeyException; -import com.dotcms.ai.client.AIProxyClient; -import com.dotcms.ai.client.AIRequest; -import com.dotcms.ai.client.JSONObjectAIRequest; -import com.dotcms.ai.domain.Model; -import com.dotcms.api.system.event.message.MessageSeverity; -import com.dotcms.api.system.event.message.SystemMessageEventUtil; -import com.dotcms.api.system.event.message.builder.SystemMessage; -import com.dotcms.api.system.event.message.builder.SystemMessageBuilder; -import com.dotmarketing.util.DateUtil; -import com.google.common.annotations.VisibleForTesting; -import com.liferay.portal.language.LanguageUtil; -import io.vavr.Lazy; -import io.vavr.control.Try; - -import java.util.Collections; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * The AIAppValidator class is responsible for validating AI configurations and model usage. - * It ensures that the AI models specified in the application configuration are supported - * and not exhausted. - * - * @author vico - */ -public class AIAppValidator { - - private static final Lazy INSTANCE = Lazy.of(AIAppValidator::new); - - private SystemMessageEventUtil systemMessageEventUtil; - - private AIAppValidator() { - setSystemMessageEventUtil(SystemMessageEventUtil.getInstance()); - } - - public static AIAppValidator get() { - return INSTANCE.get(); - } - - /** - * Validates the AI configuration for the specified user. - * If the user ID is null, the validation is skipped. - * Checks if the models specified in the application configuration are supported. - * If any unsupported models are found, a warning message is pushed to the user. - * - * @param appConfig the application configuration - * @param userId the user ID - */ - public void validateAIConfig(final AppConfig appConfig, final String userId) { - if (Objects.isNull(userId)) { - appConfig.debugLogger(getClass(), () -> "User Id is null, skipping AI configuration validation"); - return; - } - - try { - final Set supportedModels = AIModels.get().getOrPullSupportedModels(appConfig); - final Set unsupportedModels = Stream.of( - appConfig.getModel(), - appConfig.getImageModel(), - appConfig.getEmbeddingsModel()) - .flatMap(aiModel -> aiModel.getModels().stream()) - .map(Model::getName) - .filter(model -> !supportedModels.contains(model)) - .collect(Collectors.toSet()); - if (unsupportedModels.isEmpty()) { - return; - } - - sendUnsupportedModelsNotification(userId, unsupportedModels); - } catch (InvalidAIKeyException e) { - sendInvalidAIKeyNotification(userId); - } - } - - private void sendInvalidAIKeyNotification(final String userId) { - final String message = Try - .of(() -> LanguageUtil.get("ai.key.invalid")) - .getOrElse("AI key authentication failed. Please ensure the key is valid, active, and correctly configured."); - - final SystemMessage systemMessage = new SystemMessageBuilder() - .setMessage(message) - .setSeverity(MessageSeverity.ERROR) - .setLife(DateUtil.SEVEN_SECOND_MILLIS) - .create(); - - systemMessageEventUtil.pushMessage(systemMessage, Collections.singletonList(userId)); - } - - /** - * Sends a notification to the specified user about unsupported AI models. - * Creates a warning message containing the names of the unsupported models - * and pushes it to the user's notification system. - * - * @param userId the ID of the user to receive the notification - * @param unsupportedModels a set of model names that are not supported - */ - private void sendUnsupportedModelsNotification(String userId, Set unsupportedModels) { - final String unsupported = String.join(", ", unsupportedModels); - final String message = Try - .of(() -> LanguageUtil.get("ai.unsupported.models", unsupported)) - .getOrElse(String.format("The following models are not supported: [%s]", unsupported)); - final SystemMessage systemMessage = new SystemMessageBuilder() - .setMessage(message) - .setSeverity(MessageSeverity.WARNING) - .setLife(DateUtil.SEVEN_SECOND_MILLIS) - .create(); - - systemMessageEventUtil.pushMessage(systemMessage, Collections.singletonList(userId)); - } - - /** - * Validates the usage of AI models for the specified user. - * If the user ID is null, the validation is skipped. - * Checks if the models specified in the AI model are exhausted or invalid. - * If any exhausted or invalid models are found, a warning message is pushed to the user. - * - * @param aiModel the AI model - * @param request the ai request - */ - public void validateModelsUsage(final AIModel aiModel, final JSONObjectAIRequest request) { - if (Objects.isNull(request.getUserId())) { - request.getConfig().debugLogger(getClass(), () -> "User Id is null, skipping AI models usage validation"); - return; - } - - final String unavailableModels = aiModel.getModels() - .stream() - .map(Model::getName) - .collect(Collectors.joining(", ")); - final String message = Try - .of(() -> LanguageUtil.get("ai.models.exhausted", aiModel.getType(), unavailableModels)). - getOrElse( - String.format( - "All the %s models: [%s] have been exhausted since they are invalid or has been decommissioned", - aiModel.getType(), - unavailableModels)); - final SystemMessage systemMessage = new SystemMessageBuilder() - .setMessage(message) - .setSeverity(MessageSeverity.WARNING) - .setLife(DateUtil.SEVEN_SECOND_MILLIS) - .create(); - - systemMessageEventUtil.pushMessage(systemMessage, Collections.singletonList(request.getUserId())); - } - - @VisibleForTesting - void setSystemMessageEventUtil(SystemMessageEventUtil systemMessageEventUtil) { - this.systemMessageEventUtil = systemMessageEventUtil; - } - -} diff --git a/dotCMS/src/main/java/com/dotcms/ai/viewtool/CompletionsTool.java b/dotCMS/src/main/java/com/dotcms/ai/viewtool/CompletionsTool.java index adc3a77cc60c..d43594238293 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/viewtool/CompletionsTool.java +++ b/dotCMS/src/main/java/com/dotcms/ai/viewtool/CompletionsTool.java @@ -55,9 +55,7 @@ public Map getConfig() { AppKeys.COMPLETION_ROLE_PROMPT.key, this.config.getConfig(AppKeys.COMPLETION_ROLE_PROMPT), AppKeys.COMPLETION_TEXT_PROMPT.key, - this.config.getConfig(AppKeys.COMPLETION_TEXT_PROMPT), - AppKeys.TEXT_MODEL_NAMES.key, - this.config.getConfig(AppKeys.TEXT_MODEL_NAMES)); + this.config.getConfig(AppKeys.COMPLETION_TEXT_PROMPT)); } /** diff --git a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagActionlet.java b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagActionlet.java index 989edd2b4860..4b9e42a2ccad 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagActionlet.java +++ b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagActionlet.java @@ -1,7 +1,5 @@ package com.dotcms.ai.workflow; -import com.dotcms.ai.app.AppConfig; -import com.dotcms.ai.app.ConfigService; import com.dotmarketing.portlets.workflows.actionlet.Actionlet; import com.dotmarketing.portlets.workflows.actionlet.WorkFlowActionlet; import com.dotmarketing.portlets.workflows.model.MultiKeyValue; @@ -38,21 +36,7 @@ public List getParameters() { ) ); - final AppConfig appConfig = ConfigService.INSTANCE.config(); - return List.of( - overwriteParameter, - limitTagsToHost, - new WorkflowActionletParameter( - OpenAIParams.MODEL.key, - "The AI model to use, defaults to " + appConfig.getModel().getCurrentModel(), - appConfig.getModel().getCurrentModel(), - false), - new WorkflowActionletParameter( - OpenAIParams.TEMPERATURE.key, - "The AI temperature for the response. Between .1 and 2.0.", - ".1", - false) - ); + return List.of(overwriteParameter, limitTagsToHost); } @Override diff --git a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagRunner.java b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagRunner.java index a63c03743686..202497022932 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagRunner.java +++ b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIAutoTagRunner.java @@ -1,7 +1,5 @@ package com.dotcms.ai.workflow; -import com.dotcms.ai.app.AppKeys; -import com.dotcms.ai.app.ConfigService; import com.dotcms.ai.util.ContentToStringUtil; import com.dotcms.ai.util.VelocityContextFactory; import com.dotcms.api.system.event.message.MessageSeverity; @@ -45,8 +43,6 @@ public class OpenAIAutoTagRunner implements Runnable { final User user; final boolean overwriteField; final boolean limitTags; - final String model; - final float temperature; final Contentlet contentlet; OpenAIAutoTagRunner(final WorkflowProcessor processor, final Map params) { @@ -54,18 +50,14 @@ public class OpenAIAutoTagRunner implements Runnable { processor.getContentlet(), processor.getUser(), Boolean.parseBoolean(params.get(OpenAIParams.OVERWRITE_FIELDS.key).getValue()), - Boolean.parseBoolean(params.get(OpenAIParams.LIMIT_TAGS_TO_HOST.key).getValue()), - params.get(OpenAIParams.MODEL.key).getValue(), - Try.of(() -> Float.parseFloat(params.get(OpenAIParams.TEMPERATURE.key).getValue())).getOrElse(ConfigService.INSTANCE.config().getConfigFloat(AppKeys.COMPLETION_TEMPERATURE)) + Boolean.parseBoolean(params.get(OpenAIParams.LIMIT_TAGS_TO_HOST.key).getValue()) ); } OpenAIAutoTagRunner(final Contentlet contentlet, final User user, final boolean overwriteField, - final boolean limitTags, - final String model, - final float temperature) { + final boolean limitTags) { if (UtilMethods.isEmpty(contentlet::getIdentifier)) { throw new IllegalArgumentException("Content must be saved and have an identifier before running AI Content Prompt"); @@ -74,8 +66,6 @@ public class OpenAIAutoTagRunner implements Runnable { this.overwriteField = overwriteField; this.limitTags = limitTags; this.user = user; - this.model = model; - this.temperature = temperature; } @@ -146,7 +136,7 @@ private String openAIRequest(final Contentlet workingContentlet, final String co final String parsedContentPrompt = VelocityUtil.eval(contentToTag, ctx); final JSONObject openAIResponse = APILocator.getDotAIAPI().getCompletionsAPI() - .prompt(parsedSystemPrompt, parsedContentPrompt, model, temperature, 2000, user.getUserId()); + .prompt(parsedSystemPrompt, parsedContentPrompt, null, 0f, 2000, user.getUserId()); return openAIResponse.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getString("content"); } diff --git a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptActionlet.java b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptActionlet.java index f64b05cb6358..121dd00f6660 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptActionlet.java +++ b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptActionlet.java @@ -1,8 +1,5 @@ package com.dotcms.ai.workflow; -import com.dotcms.ai.app.AppConfig; -import com.dotcms.ai.app.AppKeys; -import com.dotcms.ai.app.ConfigService; import com.dotmarketing.portlets.workflows.actionlet.Actionlet; import com.dotmarketing.portlets.workflows.actionlet.WorkFlowActionlet; import com.dotmarketing.portlets.workflows.model.MultiKeyValue; @@ -29,7 +26,6 @@ public List getParameters() { new MultiKeyValue(Boolean.toString(false), Boolean.toString(false)), new MultiKeyValue(Boolean.toString(true), Boolean.toString(true))) ); - final AppConfig appConfig = ConfigService.INSTANCE.config(); return List.of( new WorkflowActionletParameter( @@ -44,18 +40,7 @@ public List getParameters() { OpenAIParams.OPEN_AI_PROMPT.key, "The prompt that will be sent to the AI", "We need an attractive search result in Google. Return a json object that includes the fields \"pageTitle\" for a meta title of less than 55 characters and \"metaDescription\" for the meta description of less than 300 characters using this content:\\n\\n${fieldContent}\\n\\n", - true), - new WorkflowActionletParameter( - OpenAIParams.MODEL.key, - "The AI model to use, defaults to " + appConfig.getModel().getCurrentModel(), - appConfig.getModel().getCurrentModel(), - false), - new WorkflowActionletParameter( - OpenAIParams.TEMPERATURE.key, - "The AI temperature for the response. Between .1 and 2.0. Defaults to " - + appConfig.getConfig(AppKeys.COMPLETION_TEMPERATURE), - appConfig.getConfig(AppKeys.COMPLETION_TEMPERATURE), - false) + true) ); } diff --git a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptRunner.java b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptRunner.java index 5ebca943a044..a5f6f3d8f138 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptRunner.java +++ b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIContentPromptRunner.java @@ -1,7 +1,5 @@ package com.dotcms.ai.workflow; -import com.dotcms.ai.app.AppKeys; -import com.dotcms.ai.app.ConfigService; import com.dotcms.ai.util.ContentToStringUtil; import com.dotcms.ai.util.VelocityContextFactory; import com.dotcms.api.system.event.Payload; @@ -52,8 +50,6 @@ public class OpenAIContentPromptRunner implements Runnable { final String prompt; final boolean overwriteField; final String fieldToWrite; - final String model; - final float temperature; final Contentlet contentlet; OpenAIContentPromptRunner(final WorkflowProcessor processor, @@ -64,14 +60,7 @@ public class OpenAIContentPromptRunner implements Runnable { processor.getUser(), params.get(OpenAIParams.OPEN_AI_PROMPT.key).getValue(), Boolean.parseBoolean(params.get(OpenAIParams.OVERWRITE_FIELDS.key).getValue()), - params.get(OpenAIParams.FIELD_TO_WRITE.key).getValue(), - params.get(OpenAIParams.MODEL.key).getValue(), - Try.of(() -> - Float.parseFloat( - params - .get(OpenAIParams.TEMPERATURE.key) - .getValue())) - .getOrElse(ConfigService.INSTANCE.config().getConfigFloat(AppKeys.COMPLETION_TEMPERATURE)) + params.get(OpenAIParams.FIELD_TO_WRITE.key).getValue() ); } @@ -79,9 +68,7 @@ public class OpenAIContentPromptRunner implements Runnable { final User user, final String prompt, final boolean overwriteField, - final String fieldToWrite, - final String model, - final float temperature) { + final String fieldToWrite) { if (UtilMethods.isEmpty(contentlet::getIdentifier)) { throw new IllegalArgumentException( @@ -92,8 +79,6 @@ public class OpenAIContentPromptRunner implements Runnable { this.overwriteField = overwriteField; this.fieldToWrite = fieldToWrite; this.user = user; - this.model = model; - this.temperature = temperature; } @Override @@ -147,7 +132,7 @@ private String openAIRequest(final Contentlet workingContentlet) throws Exceptio final String parsedPrompt = VelocityUtil.eval(prompt, ctx); final JSONObject openAIResponse = APILocator.getDotAIAPI() .getCompletionsAPI() - .raw(buildRequest(parsedPrompt, model, temperature), user.getUserId()); + .raw(buildRequest(parsedPrompt), user.getUserId()); try { return openAIResponse @@ -199,14 +184,12 @@ private void setJsonProperties(final Contentlet contentlet, final JSONObject jso } } - private JSONObject buildRequest(final String prompt, final String model, final float temperature) { + private JSONObject buildRequest(final String prompt) { final JSONArray messages = new JSONArray(); messages.add(Map.of("role", "user", "content", prompt)); final JSONObject json = new JSONObject(); json.put("messages", messages); - json.put("model", model); - json.put("temperature", temperature); json.put("stream", false); Logger.debug(this.getClass(), "Open AI Request:\n" + json.toString(2)); diff --git a/dotCMS/src/main/resources/apps/dotAI.yml b/dotCMS/src/main/resources/apps/dotAI.yml index d694a4c6bf96..3192c882d892 100644 --- a/dotCMS/src/main/resources/apps/dotAI.yml +++ b/dotCMS/src/main/resources/apps/dotAI.yml @@ -2,25 +2,26 @@ name: "dotAI" iconUrl: "https://static.dotcms.com/assets/icons/apps/chatgpt_logo.svg" allowExtraParameters: true description: | - Credentials and options for using OpenAI/ChatGPT with dotCMS. There are a number of config - properties that can be set in the extra parameter fields. Please see the "config" tab of the [dotAI tool](#/c/dotai) to see what can be configured. We recommend you have a single config for all your sites, - by placing all config on the SYSTEM_HOST or provide a configuration PER site. + Configuration for dotAI, powered by LangChain4J. Supports multiple AI providers + configured via a single JSON field (Provider Config). Each provider section (chat, embeddings, image) + is declared independently, allowing you to mix providers or models per capability. + We recommend a single configuration on SYSTEM_HOST, or override per site as needed. params: - apiKey: + providerConfig: value: "" hidden: true type: "STRING" - label: "API Key" - hint: "Your ChatGPT API key" + label: "Provider Config (JSON)" + hint: | + JSON configuration for the AI provider. Each section (chat, embeddings, image) declares its own provider independently. + Example for OpenAI: + { + "chat": { "provider": "openai", "apiKey": "sk-...", "model": "gpt-4o", "maxTokens": 16384, "temperature": 1.0, "maxRetries": 3 }, + "embeddings": { "provider": "openai", "apiKey": "sk-...", "model": "text-embedding-ada-002" }, + "image": { "provider": "openai", "apiKey": "sk-...", "model": "dall-e-3", "size": "1024x1024" } + } required: true - textModelNames: - value: "gpt-4o" - hidden: false - type: "STRING" - label: "Allowed Model Names" - hint: "Comma delimited list of models allowed to generate OpenAI API response (e.g. gpt-4o,gpt-4o-mini). If the list is empty, then all models are allowed." - required: false rolePrompt: value: "You are dotCMSbot, and AI assistant to help content creators generate and rewrite content in their content management system." hidden: false @@ -35,41 +36,6 @@ params: label: "Text Prompt" hint: "A prompt describing writing style." required: false - textModelTokensPerMinute: - value: "180000" - hidden: false - type: "STRING" - label: "Tokens per Minute" - hint: "Tokens per minute used to generate OpenAI API response." - required: false - textModelApiPerMinute: - value: "3500" - hidden: false - type: "STRING" - label: "API per Minute" - hint: "API per minute used to generate OpenAI API response." - required: false - textModelMaxTokens: - value: "16384" - hidden: false - type: "STRING" - label: "Max Tokens" - hint: "Maximum number of tokens used to generate OpenAI API response." - required: false - textModelCompletion: - value: "true" - hidden: false - type: "BOOL" - label: "Completion model enabled" - hint: "Enable completion model used to generate OpenAI API response." - required: false - imageModelNames: - value: "dall-e-3" - hidden: false - type: "STRING" - label: "Image Model Names" - hint: "Comma delimited list of image models used to generate OpenAI API response(e.g. dall-e-3)." - required: true imagePrompt: value: "Use 16:9 aspect ratio." hidden: false @@ -82,7 +48,7 @@ params: type: "SELECT" label: "Image size" hint: "Image size to generate" - required: true + required: false value: - label: "1792x1024 (Blog Image 3:2)" value: "1792x1024" @@ -103,69 +69,6 @@ params: value: "1920x1080" - label: "256x256 (Small Square 1:1)" value: "256x256" - imageModelTokensPerMinute: - value: "0" - hidden: false - type: "STRING" - label: "Image Tokens per Minute" - hint: "Tokens per minute used to generate OpenAI API response." - required: false - imageModelApiPerMinute: - value: "50" - hidden: false - type: "STRING" - label: "Image API per Minute" - hint: "API per minute used to generate OpenAI API response." - required: false - imageModelMaxTokens: - value: "0" - hidden: false - type: "STRING" - label: "Image Max Tokens" - hint: "Maximum number of tokens used to generate OpenAI API response." - required: false - imageModelCompletion: - value: "false" - hidden: false - type: "BOOL" - label: "Image Completion model enabled" - hint: "Enable completion model used to generate OpenAI API response." - required: false - embeddingsModelNames: - value: "text-embedding-ada-002" - hidden: false - type: "STRING" - label: "Embeddings Model Names" - hint: "Comma delimited list of embeddings models used to generate OpenAI API response (e.g. text-embedding-ada-002)." - required: true - embeddingsModelTokensPerMinute: - value: "1000000" - hidden: false - type: "STRING" - label: "Embeddings Tokens per Minute" - hint: "Tokens per minute used to generate OpenAI API response." - required: false - embeddingsModelApiPerMinute: - value: "3000" - hidden: false - type: "STRING" - label: "Embeddings API per Minute" - hint: "API per minute used to generate OpenAI API response." - required: false - embeddingsModelMaxTokens: - value: "8191" - hidden: false - type: "STRING" - label: "Embeddings Max Tokens" - hint: "Maximum number of tokens used to generate OpenAI API response." - required: false - embeddingsModelCompletion: - value: "false" - hidden: false - type: "BOOL" - label: "Embeddings Completion model enabled" - hint: "Enable completion model used to generate OpenAI API response." - required: false listenerIndexer: value: "" hidden: false @@ -179,7 +82,7 @@ params: "blogsOnly": "blog.blogcontent" } ``` - means that blog, news and webPageContent will be indexed in the `default` index and the blog field `blog.blogcontent` will be - indexed into the `blogsOnly` index. The list of content types is a comma separated list content types and can optionally + means that blog, news and webPageContent will be indexed in the `default` index and the blog field `blog.blogcontent` will be + indexed into the `blogsOnly` index. The list of content types is a comma separated list content types and can optionally include the field that should be indexed when a contentlet is published. All unpublished content will be removed from the index. required: false diff --git a/dotCMS/src/main/webapp/WEB-INF/jsp/dotai/render.jsp b/dotCMS/src/main/webapp/WEB-INF/jsp/dotai/render.jsp index d2846e2045e3..079ee9399aee 100644 --- a/dotCMS/src/main/webapp/WEB-INF/jsp/dotai/render.jsp +++ b/dotCMS/src/main/webapp/WEB-INF/jsp/dotai/render.jsp @@ -11,8 +11,7 @@ refreshConfigs().then(() => { writeConfigTable(); writeModelToDropdown(); - alert - if (dotAiState.config["apiKey"] != "*****") { + if (!dotAiState.config["providerConfig"]) { document.getElementById("openAIKeyWarn").style.display = "block"; } }); @@ -23,7 +22,7 @@ diff --git a/dotCMS/src/test/java/com/dotcms/ai/api/OpenAIImageAPIImplTest.java b/dotCMS/src/test/java/com/dotcms/ai/api/OpenAIImageAPIImplTest.java index e73d9352a59b..7ef23c537560 100644 --- a/dotCMS/src/test/java/com/dotcms/ai/api/OpenAIImageAPIImplTest.java +++ b/dotCMS/src/test/java/com/dotcms/ai/api/OpenAIImageAPIImplTest.java @@ -201,7 +201,7 @@ private ImageAPI prepareService(final String response, final User user) { return new OpenAIImageAPIImpl(config, user, hostApi, tempFileApi) { @Override - public String doRequest(final String urlIn, final JSONObject json) { + public String doRequest(final JSONObject json) { return response; } diff --git a/dotCMS/src/test/java/com/dotcms/ai/app/AIAppUtilTest.java b/dotCMS/src/test/java/com/dotcms/ai/app/AIAppUtilTest.java index 8c1cd1e79c4e..3dc9f4f44eb0 100644 --- a/dotCMS/src/test/java/com/dotcms/ai/app/AIAppUtilTest.java +++ b/dotCMS/src/test/java/com/dotcms/ai/app/AIAppUtilTest.java @@ -1,12 +1,10 @@ package com.dotcms.ai.app; -import com.dotcms.ai.domain.Model; import com.dotcms.security.apps.Secret; import org.junit.Before; import org.junit.Test; import java.util.Map; -import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -116,55 +114,6 @@ public void testDiscoverBooleanSecret() { assertTrue(result); } - /** - * Given a map of secrets containing a key with a text model name - * When the createTextModel method is called - * Then an AIModel instance should be created with the specified type and model name. - */ - @Test - public void testCreateTextModel() { - when(secrets.get(AppKeys.TEXT_MODEL_NAMES.key)).thenReturn(secret); - when(secret.getString()).thenReturn("textModel"); - - AIModel model = aiAppUtil.createTextModel(secrets); - assertNotNull(model); - assertEquals(AIModelType.TEXT, model.getType()); - assertTrue(model.getModels().stream().map(Model::getName).collect(Collectors.toList()).contains("textmodel")); - } - - /** - * Given a map of secrets containing a key with an image model name - * When the createImageModel method is called - * Then an AIModel instance should be created with the specified type and model name. - */ - @Test - public void testCreateImageModel() { - when(secrets.get(AppKeys.IMAGE_MODEL_NAMES.key)).thenReturn(secret); - when(secret.getString()).thenReturn("imageModel"); - - AIModel model = aiAppUtil.createImageModel(secrets); - assertNotNull(model); - assertEquals(AIModelType.IMAGE, model.getType()); - assertTrue(model.getModels().stream().map(Model::getName).collect(Collectors.toList()).contains("imagemodel")); - } - - /** - * Given a map of secrets containing a key with an embeddings model name - * When the createEmbeddingsModel method is called - * Then an AIModel instance should be created with the specified type and model name. - */ - @Test - public void testCreateEmbeddingsModel() { - when(secrets.get(AppKeys.EMBEDDINGS_MODEL_NAMES.key)).thenReturn(secret); - when(secret.getString()).thenReturn("embeddingsModel"); - - AIModel model = aiAppUtil.createEmbeddingsModel(secrets); - assertNotNull(model); - assertEquals(AIModelType.EMBEDDINGS, model.getType()); - assertTrue(model.getModels().stream().map(Model::getName).collect(Collectors.toList()) - .contains("embeddingsmodel")); - } - @Test public void testDiscoverEnvSecret() { // Mock the secret value in the secrets map diff --git a/dotCMS/src/test/java/com/dotcms/ai/app/AppConfigTest.java b/dotCMS/src/test/java/com/dotcms/ai/app/AppConfigTest.java new file mode 100644 index 000000000000..5f338edf854e --- /dev/null +++ b/dotCMS/src/test/java/com/dotcms/ai/app/AppConfigTest.java @@ -0,0 +1,218 @@ +package com.dotcms.ai.app; + +import com.dotcms.security.apps.Secret; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for AppConfig JSON parsing logic. + * + * Key scenario being tested: when a user pastes a providerConfig with long API keys + * into the dotCMS Apps textarea, the UI may wrap those keys with literal newline + * characters (\n). Jackson rejects JSON strings containing unescaped control chars, + * so the parse fails silently and all models stay as NOOP_MODEL (isEnabled() = false). + */ +public class AppConfigTest { + + // 164-char dummy key split at position 80 to simulate textarea line-wrap. + // The \n is a *literal* newline embedded inside the JSON string value. + private static final String CHAT_API_KEY_PART1 = "sk-proj-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + private static final String CHAT_API_KEY_PART2 = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; + private static final String EMBED_API_KEY_PART1 = "sk-proj-CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + private static final String EMBED_API_KEY_PART2 = "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"; + + /** Clean JSON — no embedded newlines anywhere. */ + private static final String CLEAN_PROVIDER_CONFIG = + "{\n" + + " \"chat\": {\n" + + " \"provider\": \"openai\",\n" + + " \"apiKey\": \"" + CHAT_API_KEY_PART1 + CHAT_API_KEY_PART2 + "\",\n" + + " \"model\": \"gpt-4o-mini\",\n" + + " \"maxTokens\": 16384\n" + + " },\n" + + " \"embeddings\": {\n" + + " \"provider\": \"openai\",\n" + + " \"apiKey\": \"" + EMBED_API_KEY_PART1 + EMBED_API_KEY_PART2 + "\",\n" + + " \"model\": \"text-embedding-ada-002\"\n" + + " }\n" + + "}"; + + /** + * Simulates what gets stored when the textarea wraps long values: + * the apiKey string contains a literal \n character in the middle. + */ + private static final String WRAPPED_PROVIDER_CONFIG = + "{\n" + + " \"chat\": {\n" + + " \"provider\": \"openai\",\n" + + " \"apiKey\": \"" + CHAT_API_KEY_PART1 + "\n" + CHAT_API_KEY_PART2 + "\",\n" + + " \"model\": \"gpt-4o-mini\",\n" + + " \"maxTokens\": 16384\n" + + " },\n" + + " \"embeddings\": {\n" + + " \"provider\": \"openai\",\n" + + " \"apiKey\": \"" + EMBED_API_KEY_PART1 + "\n" + EMBED_API_KEY_PART2 + "\",\n" + + " \"model\": \"text-embedding-ada-002\"\n" + + " }\n" + + "}"; + + // ------------------------------------------------------------------------- + // Real-world JSON format from the dotAI Apps config (formatted, 3 sections) + // API key matches the actual structure: sk-proj-<164 chars> + // ------------------------------------------------------------------------- + + private static final String REAL_API_KEY = + "sk-proj-EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + + /** Exact format the user pastes into the Apps UI textarea (pretty-printed, 3 sections). */ + private static final String REAL_WORLD_FORMATTED_CONFIG = + "{\n" + + " \"chat\":{\n" + + " \"provider\":\"openai\",\n" + + " \"apiKey\":\"" + REAL_API_KEY + "\",\n" + + " \"model\":\"o4-mini\",\n" + + " \"maxCompletionTokens\":16384,\n" + + " \"temperature\":1.0,\n" + + " \"maxRetries\":3\n" + + " },\n" + + " \"embeddings\":{\n" + + " \"provider\":\"openai\",\n" + + " \"apiKey\":\"" + REAL_API_KEY + "\",\n" + + " \"model\":\"text-embedding-3-small\"\n" + + " },\n" + + " \"image\":{\n" + + " \"provider\":\"openai\",\n" + + " \"apiKey\":\"" + REAL_API_KEY + "\",\n" + + " \"model\":\"gpt-image-1\",\n" + + " \"size\":\"1024x1024\"\n" + + " }\n" + + "}"; + + @Test + public void test_parseProviderConfig_realWorldFormattedJson_parsesAllSections() { + final JsonNode root = AppConfig.parseProviderConfig(REAL_WORLD_FORMATTED_CONFIG); + + assertFalse("Formatted JSON should parse successfully", root.isEmpty()); + assertEquals("o4-mini", root.path("chat").path("model").asText()); + assertEquals("text-embedding-3-small", root.path("embeddings").path("model").asText()); + assertEquals("gpt-image-1", root.path("image").path("model").asText()); + } + + @Test + public void test_appConfig_realWorldFormattedJson_isEnabled_allModelsSet() { + final AppConfig config = buildAppConfig(REAL_WORLD_FORMATTED_CONFIG); + + assertTrue("AppConfig should be enabled with real-world formatted providerConfig", config.isEnabled()); + assertEquals("o4-mini", config.getModel().getCurrentModel()); + assertEquals("text-embedding-3-small", config.getEmbeddingsModel().getCurrentModel()); + assertEquals("gpt-image-1", config.getImageModel().getCurrentModel()); + } + + // ------------------------------------------------------------------------- + // parseProviderConfig — unit tests on the static method directly + // ------------------------------------------------------------------------- + + @Test + public void test_parseProviderConfig_cleanJson_parsesModelNames() { + final JsonNode root = AppConfig.parseProviderConfig(CLEAN_PROVIDER_CONFIG); + + assertFalse("Parse should not return empty node for valid JSON", root.isEmpty()); + assertEquals("gpt-4o-mini", root.path("chat").path("model").asText()); + assertEquals("text-embedding-ada-002", root.path("embeddings").path("model").asText()); + } + + // Note: parseProviderConfig receives already-sanitized JSON (sanitization happens in the + // constructor before calling this method). Embedded-newline scenarios are covered by + // test_appConfig_withWrappedProviderConfig_isEnabled below. + + @Test + public void test_parseProviderConfig_null_returnsEmptyObjectNode() { + final JsonNode root = AppConfig.parseProviderConfig(null); + + assertNotNull(root); + assertTrue("Null input should yield empty ObjectNode, not a missing node", root.isObject()); + assertEquals(0, root.size()); + } + + @Test + public void test_parseProviderConfig_emptyString_doesNotThrow() { + // Empty string reaches Jackson's readTree which returns NullNode (not an ObjectNode). + // This path never happens in production since the constructor guards with isNotBlank, + // but we verify at least that it doesn't throw. + final JsonNode root = AppConfig.parseProviderConfig(""); + assertNotNull(root); + } + + @Test + public void test_parseProviderConfig_invalidJson_returnsEmptyObjectNode() { + final JsonNode root = AppConfig.parseProviderConfig("this is not json at all"); + + assertNotNull(root); + assertTrue(root.isObject()); + assertEquals(0, root.size()); + } + + // ------------------------------------------------------------------------- + // AppConfig constructor — end-to-end: wrapped config → isEnabled() + models + // ------------------------------------------------------------------------- + + @Test + public void test_appConfig_withCleanProviderConfig_isEnabled() { + final AppConfig config = buildAppConfig(CLEAN_PROVIDER_CONFIG); + + assertTrue("AppConfig should be enabled when providerConfig is valid", config.isEnabled()); + assertEquals("gpt-4o-mini", config.getModel().getCurrentModel()); + assertEquals("text-embedding-ada-002", config.getEmbeddingsModel().getCurrentModel()); + } + + @Test + public void test_appConfig_withWrappedProviderConfig_isEnabled() { + // This is the scenario that should work but was failing before the fix + final AppConfig config = buildAppConfig(WRAPPED_PROVIDER_CONFIG); + + assertTrue("AppConfig should be enabled even when apiKey contains embedded newlines", config.isEnabled()); + assertEquals("gpt-4o-mini", config.getModel().getCurrentModel()); + assertEquals("text-embedding-ada-002", config.getEmbeddingsModel().getCurrentModel()); + } + + @Test + public void test_appConfig_withBlankProviderConfig_isNotEnabled() { + final AppConfig config = buildAppConfig(" "); + + assertFalse(config.isEnabled()); + } + + @Test + public void test_appConfig_withNullProviderConfig_isNotEnabled() { + final AppConfig config = buildAppConfig(null); + + assertFalse(config.isEnabled()); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private static AppConfig buildAppConfig(final String providerConfigJson) { + final Map secrets = new HashMap<>(); + if (providerConfigJson != null) { + final Secret providerConfigSecret = mock(Secret.class); + when(providerConfigSecret.getString()).thenReturn(providerConfigJson); + secrets.put(AppKeys.PROVIDER_CONFIG.key, providerConfigSecret); + } + return new AppConfig("localhost", secrets); + } + +} diff --git a/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClientTest.java b/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClientTest.java new file mode 100644 index 000000000000..32d735fc3b07 --- /dev/null +++ b/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClientTest.java @@ -0,0 +1,165 @@ +package com.dotcms.ai.client.langchain4j; + +import com.dotcms.ai.AiKeys; +import com.dotcms.ai.app.AppConfig; +import com.dotcms.ai.client.JSONObjectAIRequest; +import com.dotcms.ai.exception.DotAIAppConfigDisabledException; +import com.dotmarketing.util.json.JSONArray; +import com.dotmarketing.util.json.JSONObject; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.image.Image; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.response.ChatResponse; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.net.URI; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class LangChain4jAIClientTest { + + @Test + public void test_toMessages_null_returnsEmptyList() { + final List messages = LangChain4jAIClient.toMessages(null); + assertTrue(messages.isEmpty()); + } + + @Test + public void test_toMessages_systemRole_producesSystemMessage() { + final JSONArray array = new JSONArray(); + array.put(Map.of(AiKeys.ROLE, "system", AiKeys.CONTENT, "You are helpful.")); + + final List messages = LangChain4jAIClient.toMessages(array); + + assertEquals(1, messages.size()); + assertTrue(messages.get(0) instanceof SystemMessage); + assertEquals("You are helpful.", ((SystemMessage) messages.get(0)).text()); + } + + @Test + public void test_toMessages_assistantRole_producesAiMessage() { + final JSONArray array = new JSONArray(); + array.put(Map.of(AiKeys.ROLE, "assistant", AiKeys.CONTENT, "I can help.")); + + final List messages = LangChain4jAIClient.toMessages(array); + + assertEquals(1, messages.size()); + assertTrue(messages.get(0) instanceof AiMessage); + assertEquals("I can help.", ((AiMessage) messages.get(0)).text()); + } + + @Test + public void test_toMessages_userRole_producesUserMessage() { + final JSONArray array = new JSONArray(); + array.put(Map.of(AiKeys.ROLE, "user", AiKeys.CONTENT, "Hello!")); + + final List messages = LangChain4jAIClient.toMessages(array); + + assertEquals(1, messages.size()); + assertTrue(messages.get(0) instanceof UserMessage); + assertEquals("Hello!", ((UserMessage) messages.get(0)).singleText()); + } + + @Test + public void test_toMessages_unknownRole_defaultsToUserMessage() { + final JSONArray array = new JSONArray(); + array.put(Map.of(AiKeys.ROLE, "unknown-role", AiKeys.CONTENT, "Some text")); + + final List messages = LangChain4jAIClient.toMessages(array); + + assertEquals(1, messages.size()); + assertTrue(messages.get(0) instanceof UserMessage); + } + + @Test + public void test_toMessages_multipleRoles_preservesOrder() { + final JSONArray array = new JSONArray(); + array.put(Map.of(AiKeys.ROLE, "system", AiKeys.CONTENT, "System prompt")); + array.put(Map.of(AiKeys.ROLE, "user", AiKeys.CONTENT, "User question")); + array.put(Map.of(AiKeys.ROLE, "assistant", AiKeys.CONTENT, "Assistant reply")); + + final List messages = LangChain4jAIClient.toMessages(array); + + assertEquals(3, messages.size()); + assertTrue(messages.get(0) instanceof SystemMessage); + assertTrue(messages.get(1) instanceof UserMessage); + assertTrue(messages.get(2) instanceof AiMessage); + } + + @Test + public void test_toChatResponseJson_correctStructure() { + final AiMessage aiMessage = new AiMessage("Test response content"); + final ChatResponse response = mock(ChatResponse.class); + when(response.aiMessage()).thenReturn(aiMessage); + when(response.modelName()).thenReturn("gpt-4o-mini"); + + final JSONObject json = new JSONObject(LangChain4jAIClient.toChatResponseJson(response)); + final JSONObject message = json.getJSONArray("choices").getJSONObject(0).getJSONObject(AiKeys.MESSAGE); + + assertEquals("assistant", message.getString(AiKeys.ROLE)); + assertEquals("Test response content", message.getString(AiKeys.CONTENT)); + assertEquals("gpt-4o-mini", json.getString(AiKeys.MODEL)); + } + + @Test + public void test_toEmbeddingResponseJson_valuesStoredAsDoubles() { + final Embedding embedding = Embedding.from(new float[]{0.1f, -0.2f, 0.3f}); + + final JSONObject json = new JSONObject(LangChain4jAIClient.toEmbeddingResponseJson(embedding)); + final JSONArray embeddingArray = json.getJSONArray(AiKeys.DATA) + .getJSONObject(0) + .getJSONArray(AiKeys.EMBEDDING); + + assertEquals(3, embeddingArray.length()); + assertTrue(embeddingArray.get(0) instanceof Double); + assertEquals(0.1, (Double) embeddingArray.get(0), 0.0001); + assertEquals(-0.2, (Double) embeddingArray.get(1), 0.0001); + assertEquals(0.3, (Double) embeddingArray.get(2), 0.0001); + } + + @Test + public void test_toImageResponseJson_containsUrl() throws Exception { + final Image image = Image.builder().url(new URI("https://example.com/image.png")).build(); + + final JSONObject json = new JSONObject(LangChain4jAIClient.toImageResponseJson(image)); + final String url = json.getJSONArray(AiKeys.DATA).getJSONObject(0).getString(AiKeys.URL); + + assertEquals("https://example.com/image.png", url); + } + + @Test + public void test_toImageResponseJson_nullUrl_returnsEmptyString() { + final Image image = Image.builder().build(); + + final JSONObject json = new JSONObject(LangChain4jAIClient.toImageResponseJson(image)); + final String url = json.getJSONArray(AiKeys.DATA).getJSONObject(0).getString(AiKeys.URL); + + assertEquals("", url); + } + + @Test + public void test_sendRequest_disabledConfig_throws() { + final AppConfig disabledConfig = mock(AppConfig.class); + when(disabledConfig.isEnabled()).thenReturn(false); + + final JSONObject payload = new JSONObject(); + payload.put(AiKeys.MODEL, "gpt-4o-mini"); + + final JSONObjectAIRequest request = JSONObjectAIRequest.quickText(disabledConfig, payload, "test-user"); + + assertThrows( + DotAIAppConfigDisabledException.class, + () -> LangChain4jAIClient.get().sendRequest(request, new ByteArrayOutputStream())); + } + +} diff --git a/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/LangChain4jModelFactoryTest.java b/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/LangChain4jModelFactoryTest.java new file mode 100644 index 000000000000..b0e1e0a26f19 --- /dev/null +++ b/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/LangChain4jModelFactoryTest.java @@ -0,0 +1,151 @@ +package com.dotcms.ai.client.langchain4j; + +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.image.ImageModel; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +public class LangChain4jModelFactoryTest { + + @Test + public void test_buildChatModel_nullConfig_throws() { + assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildChatModel(null)); + } + + @Test + public void test_buildChatModel_openai_returnsModel() { + final ChatModel model = LangChain4jModelFactory.buildChatModel(openAiConfig("gpt-4o-mini")); + assertNotNull(model); + } + + @Test + public void test_buildChatModel_missingModel_throws() { + final ProviderConfig config = ImmutableProviderConfig.builder() + .provider("openai") + .apiKey("test-key") + .build(); + assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildChatModel(config)); + } + + @Test + public void test_buildChatModel_openai_missingApiKey_throws() { + final ProviderConfig config = ImmutableProviderConfig.builder() + .provider("openai") + .model("gpt-4o-mini") + .build(); + assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildChatModel(config)); + } + + @Test + public void test_buildChatModel_azureOpenai_returnsModel() { + final ChatModel model = LangChain4jModelFactory.buildChatModel(azureOpenAiConfig("gpt-4o")); + assertNotNull(model); + } + + @Test + public void test_buildChatModel_azureOpenai_missingApiKey_throws() { + final ProviderConfig config = ImmutableProviderConfig.builder() + .provider("azure_openai") + .model("gpt-4o") + .endpoint("https://my-company.openai.azure.com/") + .build(); + assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildChatModel(config)); + } + + @Test + public void test_buildChatModel_azureOpenai_missingEndpoint_throws() { + final ProviderConfig config = ImmutableProviderConfig.builder() + .provider("azure_openai") + .model("gpt-4o") + .apiKey("test-key") + .build(); + assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildChatModel(config)); + } + + @Test + public void test_buildChatModel_unknownProvider_throws() { + final ProviderConfig config = ImmutableProviderConfig.builder() + .provider("unknown-provider") + .model("some-model") + .apiKey("key") + .build(); + assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildChatModel(config)); + } + + @Test + public void test_buildEmbeddingModel_nullConfig_throws() { + assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildEmbeddingModel(null)); + } + + @Test + public void test_buildEmbeddingModel_openai_returnsModel() { + final EmbeddingModel model = LangChain4jModelFactory.buildEmbeddingModel(openAiConfig("text-embedding-ada-002")); + assertNotNull(model); + } + + @Test + public void test_buildEmbeddingModel_azureOpenai_returnsModel() { + final EmbeddingModel model = LangChain4jModelFactory.buildEmbeddingModel(azureOpenAiConfig("text-embedding-ada-002")); + assertNotNull(model); + } + + @Test + public void test_buildEmbeddingModel_unknownProvider_throws() { + final ProviderConfig config = ImmutableProviderConfig.builder() + .provider("unknown-provider") + .model("some-model") + .apiKey("key") + .build(); + assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildEmbeddingModel(config)); + } + + @Test + public void test_buildImageModel_nullConfig_throws() { + assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildImageModel(null)); + } + + @Test + public void test_buildImageModel_openai_returnsModel() { + final ImageModel model = LangChain4jModelFactory.buildImageModel(openAiConfig("dall-e-3")); + assertNotNull(model); + } + + @Test + public void test_buildImageModel_azureOpenai_returnsModel() { + final ImageModel model = LangChain4jModelFactory.buildImageModel(azureOpenAiConfig("dall-e-3")); + assertNotNull(model); + } + + @Test + public void test_buildImageModel_unknownProvider_throws() { + final ProviderConfig config = ImmutableProviderConfig.builder() + .provider("unknown-provider") + .model("some-model") + .apiKey("key") + .build(); + assertThrows(IllegalArgumentException.class, () -> LangChain4jModelFactory.buildImageModel(config)); + } + + private static ProviderConfig openAiConfig(final String model) { + return ImmutableProviderConfig.builder() + .provider("openai") + .model(model) + .apiKey("test-key") + .build(); + } + + private static ProviderConfig azureOpenAiConfig(final String model) { + return ImmutableProviderConfig.builder() + .provider("azure_openai") + .model(model) + .apiKey("test-key") + .endpoint("https://my-company.openai.azure.com/") + .deploymentName(model) + .apiVersion("2024-02-01") + .build(); + } + +} diff --git a/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/ProviderConfigTest.java b/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/ProviderConfigTest.java new file mode 100644 index 000000000000..bf5feb3f41f7 --- /dev/null +++ b/dotCMS/src/test/java/com/dotcms/ai/client/langchain4j/ProviderConfigTest.java @@ -0,0 +1,69 @@ +package com.dotcms.ai.client.langchain4j; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class ProviderConfigTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + public void test_deserialize_fullConfig() throws Exception { + final String json = + "{\"provider\":\"openai\",\"model\":\"gpt-4o\",\"apiKey\":\"sk-test\"," + + "\"maxTokens\":1024,\"temperature\":0.7,\"maxRetries\":3,\"timeout\":30," + + "\"size\":\"1024x1024\",\"endpoint\":\"https://my.endpoint.com\"," + + "\"deploymentName\":\"my-deploy\",\"apiVersion\":\"2024-02-01\"," + + "\"region\":\"us-east-1\",\"accessKeyId\":\"AKID\",\"secretAccessKey\":\"secret\"," + + "\"projectId\":\"my-project\",\"location\":\"us-central1\"}"; + + final ProviderConfig config = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals("openai", config.provider()); + assertEquals("gpt-4o", config.model()); + assertEquals("sk-test", config.apiKey()); + assertEquals(Integer.valueOf(1024), config.maxTokens()); + assertEquals(Double.valueOf(0.7), config.temperature()); + assertEquals(Integer.valueOf(3), config.maxRetries()); + assertEquals(Integer.valueOf(30), config.timeout()); + assertEquals("1024x1024", config.size()); + assertEquals("https://my.endpoint.com", config.endpoint()); + assertEquals("my-deploy", config.deploymentName()); + assertEquals("2024-02-01", config.apiVersion()); + assertEquals("us-east-1", config.region()); + assertEquals("AKID", config.accessKeyId()); + assertEquals("secret", config.secretAccessKey()); + assertEquals("my-project", config.projectId()); + assertEquals("us-central1", config.location()); + } + + @Test + public void test_deserialize_minimalConfig() throws Exception { + final String json = "{\"provider\":\"openai\",\"model\":\"gpt-4o-mini\",\"apiKey\":\"sk-test\"}"; + + final ProviderConfig config = MAPPER.readValue(json, ProviderConfig.class); + + assertEquals("openai", config.provider()); + assertEquals("gpt-4o-mini", config.model()); + assertNull(config.maxTokens()); + assertNull(config.temperature()); + assertNull(config.maxRetries()); + assertNull(config.endpoint()); + } + + @Test + public void test_deserialize_unknownFieldsIgnored() throws Exception { + final String json = "{\"provider\":\"openai\",\"model\":\"gpt-4o\",\"apiKey\":\"sk-test\"," + + "\"futureField\":\"someValue\",\"anotherUnknown\":42}"; + + final ProviderConfig config = MAPPER.readValue(json, ProviderConfig.class); + + assertNotNull(config); + assertEquals("openai", config.provider()); + } + +} diff --git a/dotCMS/src/test/java/com/dotcms/ai/client/openai/OpenAIResponseEvaluatorTest.java b/dotCMS/src/test/java/com/dotcms/ai/client/openai/OpenAIResponseEvaluatorTest.java deleted file mode 100644 index 2782523b2482..000000000000 --- a/dotCMS/src/test/java/com/dotcms/ai/client/openai/OpenAIResponseEvaluatorTest.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.dotcms.ai.client.openai; - -import com.dotcms.ai.domain.AIResponseData; -import com.dotcms.ai.domain.ModelStatus; -import com.dotcms.ai.exception.DotAIModelNotFoundException; -import com.dotcms.rest.exception.GenericHttpStatusCodeException; -import com.dotmarketing.exception.DotRuntimeException; -import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; - -import javax.ws.rs.core.Response; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -/** - * Tests for the OpenAIResponseEvaluator class. - * - * @author vico - */ -public class OpenAIResponseEvaluatorTest { - - private OpenAIResponseEvaluator evaluator; - - @Before - public void setUp() { - evaluator = OpenAIResponseEvaluator.get(); - } - - /** - * Scenario: Processing a response with an error - * Given a response with an error message "Model has been deprecated" - * When the response is processed - * Then the metadata should contain the error message "Model has been deprecated" - * And the status should be set to DECOMMISSIONED - */ - @Test - public void testFromResponse_withError() { - final String response = new JSONObject() - .put("error", new JSONObject().put("message", "Model has been deprecated")) - .toString(); - final AIResponseData metadata = new AIResponseData(); - - evaluator.fromResponse(response, metadata, true); - - assertEquals("Model has been deprecated", metadata.getError()); - assertEquals(ModelStatus.DECOMMISSIONED, metadata.getStatus()); - } - - /** - * Scenario: Processing a response with an error - * Given a response with an error message "Model has been deprecated" - * When the response is processed as no JSON - * Then the metadata should contain the error message "Model has been deprecated" - * And the status should be set to DECOMMISSIONED - */ - @Test - public void testFromResponse_withErrorNoJson() { - final String response = new JSONObject() - .put("error", new JSONObject().put("message", "Model has been deprecated")) - .toString(); - final AIResponseData metadata = new AIResponseData(); - - evaluator.fromResponse(response, metadata, false); - - assertEquals("Model has been deprecated", metadata.getError()); - assertEquals(ModelStatus.DECOMMISSIONED, metadata.getStatus()); - } - - /** - * Scenario: Processing a response with an error - * Given a response with an error message "Model has been deprecated" - * When the response is processed as no JSON - * Then the metadata should contain the error message "Model has been deprecated" - * And the status should be set to DECOMMISSIONED - */ - @Test - public void testFromResponse_withoutErrorNoJson() { - final String response = "not a json response"; - final AIResponseData metadata = new AIResponseData(); - - evaluator.fromResponse(response, metadata, false); - - assertNull(metadata.getError()); - assertNull(metadata.getStatus()); - } - - /** - * Scenario: Processing a response without an error - * Given a response without an error message - * When the response is processed - * Then the metadata should not contain any error message - * And the status should be null - */ - @Test - public void testFromResponse_withoutError() { - final String response = new JSONObject().put("data", "some data").toString(); - final AIResponseData metadata = new AIResponseData(); - - evaluator.fromResponse(response, metadata, true); - - assertNull(metadata.getError()); - assertNull(metadata.getStatus()); - } - - /** - * Scenario: Processing an exception of type DotRuntimeException - * Given an exception of type DotAIModelNotFoundException with message "Model not found" - * When the exception is processed - * Then the metadata should contain the error message "Model not found" - * And the status should be set to INVALID - * And the exception should be set to the given DotRuntimeException - */ - @Test - public void testFromException_withDotRuntimeException() { - final DotRuntimeException exception = new DotAIModelNotFoundException("Model not found"); - final AIResponseData metadata = new AIResponseData(); - - evaluator.fromException(exception, metadata); - - assertEquals("Model not found", metadata.getError()); - assertEquals(ModelStatus.INVALID, metadata.getStatus()); - assertEquals(exception, metadata.getException()); - } - - @Test - public void testFromException_withGenericHttpStatusCodeException_notFound() { - final GenericHttpStatusCodeException exception = new GenericHttpStatusCodeException( - "Not found", - Response.Status.NOT_FOUND); - final AIResponseData metadata = new AIResponseData(); - - evaluator.fromException(exception, metadata); - - assertEquals("HTTP 404 Not Found", metadata.getError()); - assertEquals(ModelStatus.INVALID, metadata.getStatus()); - assertEquals(exception, metadata.getException().getCause()); - } - - @Test - public void testFromException_withGenericHttpStatusCodeException() { - final GenericHttpStatusCodeException exception = new GenericHttpStatusCodeException( - "Not found", - Response.Status.BAD_REQUEST); - final AIResponseData metadata = new AIResponseData(); - - evaluator.fromException(exception, metadata); - - assertEquals("HTTP 400 Bad Request", metadata.getError()); - assertEquals(ModelStatus.UNKNOWN, metadata.getStatus()); - assertEquals(exception, metadata.getException().getCause()); - } - - /** - * Scenario: Processing a general exception - * Given a general exception with message "General error" - * When the exception is processed - * Then the metadata should contain the error message "General error" - * And the status should be set to UNKNOWN - * And the exception should be wrapped in a DotRuntimeException - */ - @Test - public void testFromException_withOtherException() { - Exception exception = new Exception("General error"); - AIResponseData metadata = new AIResponseData(); - - evaluator.fromException(exception, metadata); - - assertEquals("General error", metadata.getError()); - assertEquals(ModelStatus.UNKNOWN, metadata.getStatus()); - assertEquals(DotRuntimeException.class, metadata.getException().getClass()); - } -} diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java index 1c84dfa2cd98..2a2bdbd100d4 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite2b.java @@ -1,10 +1,8 @@ package com.dotcms; -import com.dotcms.ai.app.AIModelsTest; import com.dotcms.ai.app.ConfigServiceTest; import com.dotcms.ai.client.AIProxyClientTest; import com.dotcms.ai.listener.EmbeddingContentListenerTest; -import com.dotcms.ai.validator.AIAppValidatorTest; import com.dotcms.ai.viewtool.AIViewToolTest; import com.dotcms.ai.viewtool.CompletionsToolTest; import com.dotcms.ai.viewtool.EmbeddingsToolTest; @@ -420,10 +418,8 @@ SearchToolTest.class, EmbeddingsToolTest.class, CompletionsToolTest.class, - AIModelsTest.class, ConfigServiceTest.class, AIProxyClientTest.class, - AIAppValidatorTest.class, TimeMachineAPITest.class, Task240513UpdateContentTypesSystemFieldTest.class, PruneTimeMachineBackupJobTest.class, diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/AiTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/AiTest.java index 557201f1aaa4..d022006ea510 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/AiTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/AiTest.java @@ -1,6 +1,7 @@ package com.dotcms.ai; import com.dotcms.ai.app.AppKeys; +import com.dotcms.ai.app.ConfigService; import com.dotcms.security.apps.AppSecrets; import com.dotcms.security.apps.Secret; import com.dotcms.util.WireMockTestHelper; @@ -9,8 +10,9 @@ import com.github.tomakehurst.wiremock.WireMockServer; import java.util.Map; -import java.util.Objects; -import java.util.concurrent.TimeUnit; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; public interface AiTest { @@ -31,56 +33,35 @@ static WireMockServer prepareWireMock() { return wireMockServer; } - static Map aiAppSecrets(final Host host, - final String apiKey, - final String textModels, - final String imageModels, - final String embeddingsModel) throws Exception { - final AppSecrets.Builder builder = new AppSecrets.Builder() + static void removeAiAppSecrets(final Host host) throws Exception { + APILocator.getAppsAPI().deleteSecrets(AppKeys.APP_KEY, host, APILocator.systemUser()); + } + + static String providerConfigJson(final int port, final String chatModel) { + final String endpoint = String.format("http://localhost:%d/", port); + return String.format( + "{" + + "\"chat\":{\"provider\":\"openai\",\"apiKey\":\"%s\",\"model\":\"%s\",\"endpoint\":\"%s\",\"maxRetries\":0}," + + "\"embeddings\":{\"provider\":\"openai\",\"apiKey\":\"%s\",\"model\":\"%s\",\"endpoint\":\"%s\",\"maxRetries\":0}," + + "\"image\":{\"provider\":\"openai\",\"apiKey\":\"%s\",\"model\":\"%s\",\"endpoint\":\"%s\",\"maxRetries\":0}" + + "}", + API_KEY, chatModel, endpoint, + API_KEY, EMBEDDINGS_MODEL, endpoint, + API_KEY, IMAGE_MODEL, endpoint); + } + + static Map aiAppSecretsWithProviderConfig( + final Host host, final String providerConfigJson) throws Exception { + final AppSecrets appSecrets = new AppSecrets.Builder() .withKey(AppKeys.APP_KEY) - .withSecret(AppKeys.API_URL.key, String.format(API_URL, PORT)) - .withSecret(AppKeys.API_IMAGE_URL.key, String.format(API_IMAGE_URL, PORT)) - .withSecret(AppKeys.API_EMBEDDINGS_URL.key, String.format(API_EMBEDDINGS_URL, PORT)) - .withHiddenSecret(AppKeys.API_KEY.key, apiKey) - .withSecret(AppKeys.IMAGE_SIZE.key, IMAGE_SIZE) + .withSecret(AppKeys.PROVIDER_CONFIG.key, providerConfigJson) .withSecret(AppKeys.LISTENER_INDEXER.key, "{\"default\":\"blog\"}") .withSecret(AppKeys.COMPLETION_ROLE_PROMPT.key, AppKeys.COMPLETION_ROLE_PROMPT.defaultValue) - .withSecret(AppKeys.COMPLETION_TEXT_PROMPT.key, AppKeys.COMPLETION_TEXT_PROMPT.defaultValue); - - if (Objects.nonNull(textModels)) { - builder.withSecret(AppKeys.TEXT_MODEL_NAMES.key, textModels); - } - if (Objects.nonNull(imageModels)) { - builder.withSecret(AppKeys.IMAGE_MODEL_NAMES.key, imageModels); - } - if (Objects.nonNull(embeddingsModel)) { - builder.withSecret(AppKeys.EMBEDDINGS_MODEL_NAMES.key, embeddingsModel); - } - - final AppSecrets appSecrets = builder.build(); + .withSecret(AppKeys.COMPLETION_TEXT_PROMPT.key, AppKeys.COMPLETION_TEXT_PROMPT.defaultValue) + .build(); APILocator.getAppsAPI().saveSecrets(appSecrets, host, APILocator.systemUser()); - TimeUnit.SECONDS.sleep(1); + await().atMost(5, SECONDS).until(() -> ConfigService.INSTANCE.config(host).isEnabled()); return appSecrets.getSecrets(); } - static Map aiAppSecrets(final Host host, final String apiKey) throws Exception { - return aiAppSecrets(host, apiKey, MODEL, IMAGE_MODEL, EMBEDDINGS_MODEL); - } - - static Map aiAppSecrets(final Host host, - final String textModels, - final String imageModels, - final String embeddingsModel) throws Exception { - return aiAppSecrets(host, API_KEY, textModels, imageModels, embeddingsModel); - } - - static Map aiAppSecrets(final Host host) throws Exception { - - return aiAppSecrets(host, MODEL, IMAGE_MODEL, EMBEDDINGS_MODEL); - } - - static void removeAiAppSecrets(final Host host) throws Exception { - APILocator.getAppsAPI().deleteSecrets(AppKeys.APP_KEY, host, APILocator.systemUser()); - } - } diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/api/OpenAIVisionAPIImplTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/api/OpenAIVisionAPIImplTest.java index 5ace5b798c61..be049e2bd103 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/api/OpenAIVisionAPIImplTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/api/OpenAIVisionAPIImplTest.java @@ -463,18 +463,6 @@ private static void setupAISecrets() throws Exception { Secret.builder() .withType(Type.STRING) .withValue(AiTest.API_KEY.toCharArray()) - .build(), - - AppKeys.TEXT_MODEL_NAMES.key, - Secret.builder() - .withType(Type.STRING) - .withValue(AiTest.MODEL.toCharArray()) - .build(), - - AppKeys.IMAGE_MODEL_NAMES.key, - Secret.builder() - .withType(Type.STRING) - .withValue(AiTest.IMAGE_MODEL.toCharArray()) .build() ); diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/app/AIModelsTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/app/AIModelsTest.java deleted file mode 100644 index 0b00b151ef55..000000000000 --- a/dotcms-integration/src/test/java/com/dotcms/ai/app/AIModelsTest.java +++ /dev/null @@ -1,317 +0,0 @@ -package com.dotcms.ai.app; - -import com.dotcms.ai.AiTest; -import com.dotcms.ai.domain.Model; -import com.dotcms.ai.domain.ModelStatus; -import com.dotcms.ai.exception.DotAIModelNotFoundException; -import com.dotcms.ai.model.SimpleModel; -import com.dotcms.datagen.SiteDataGen; -import com.dotcms.util.IntegrationTestInitService; -import com.dotcms.util.network.IPUtils; -import com.dotmarketing.beans.Host; -import com.dotmarketing.business.APILocator; -import com.dotmarketing.exception.DotRuntimeException; -import com.github.tomakehurst.wiremock.WireMockServer; -import io.vavr.Tuple2; -import io.vavr.control.Try; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -/** - * Integration tests for the \AIModels\ class. This test class verifies the functionality - * of methods in \AIModels\ such as loading models, finding models by host and type, and - * retrieving supported models. It uses \WireMockServer\ to simulate external dependencies - * and \IntegrationTestInitService\ for initializing the test environment. - * - * @author vico - */ -public class AIModelsTest { - - private static WireMockServer wireMockServer; - private final AIModels aiModels = AIModels.get(); - private Host host; - private Host otherHost; - - @BeforeClass - public static void beforeClass() throws Exception { - IntegrationTestInitService.getInstance().init(); - IPUtils.disabledIpPrivateSubnet(true); - wireMockServer = AiTest.prepareWireMock(); - AiTest.aiAppSecrets(APILocator.systemHost()); - } - - @AfterClass - public static void afterClass() { - wireMockServer.stop(); - IPUtils.disabledIpPrivateSubnet(false); - } - - @Before - public void before() { - host = new SiteDataGen().nextPersisted(); - otherHost = new SiteDataGen().nextPersisted(); - List.of(host, otherHost).forEach(h -> Try.of(() -> AiTest.aiAppSecrets(h)).get()); - } - - /** - * Given a set of models loaded into the AIModels instance - * When the findModel method is called with various model names and types - * Then the correct models should be found and returned. - */ - @Test - public void test_loadModels_andFindThem() throws Exception { - AiTest.aiAppSecrets( - host, - "text-model-1,text-model-2", - "image-model-3,image-model-4", - "embeddings-model-5,embeddings-model-6"); - AiTest.aiAppSecrets(otherHost, "text-model-1", null, null); - - final String hostId = host.getHostname(); - final AppConfig appConfig = ConfigService.INSTANCE.config(host); - - final Optional notFound = aiModels.findModel(appConfig, "some-invalid-model-name", AIModelType.TEXT); - assertTrue(notFound.isEmpty()); - - final Optional text1 = aiModels.findModel(appConfig, "text-model-1", AIModelType.TEXT); - final Optional text2 = aiModels.findModel(appConfig, "text-model-2", AIModelType.TEXT); - assertModels(text1, text2, AIModelType.TEXT, true); - - final Optional image1 = aiModels.findModel(appConfig, "image-model-3", AIModelType.IMAGE); - final Optional image2 = aiModels.findModel(appConfig, "image-model-4", AIModelType.IMAGE); - assertModels(image1, image2, AIModelType.IMAGE, true); - - final Optional embeddings1 = aiModels.findModel(appConfig, "embeddings-model-5", AIModelType.EMBEDDINGS); - final Optional embeddings2 = aiModels.findModel(appConfig, "embeddings-model-6", AIModelType.EMBEDDINGS); - assertModels(embeddings1, embeddings2, AIModelType.EMBEDDINGS, true); - - assertNotSame(text1.get(), image1.get()); - assertNotSame(text1.get(), embeddings1.get()); - assertNotSame(image1.get(), embeddings1.get()); - - final Optional text3 = aiModels.findModel(hostId, AIModelType.TEXT); - assertSameModels(text3, text1, text2); - - final Optional image3 = aiModels.findModel(hostId, AIModelType.IMAGE); - assertSameModels(image3, image1, image2); - - final Optional embeddings3 = aiModels.findModel(hostId, AIModelType.EMBEDDINGS); - assertSameModels(embeddings3, embeddings1, embeddings2); - - final AppConfig otherAppConfig = ConfigService.INSTANCE.config(otherHost); - final Optional text4 = aiModels.findModel(otherAppConfig, "text-model-1", AIModelType.TEXT); - assertTrue(text3.isPresent()); - assertNotSame(text1.get(), text4.get()); - - AiTest.aiAppSecrets( - host, - "text-model-7,text-model-8", - "image-model-9,image-model-10", - "embeddings-model-11, embeddings-model-12"); - - final Optional text7 = aiModels.findModel(otherAppConfig, "text-model-7", AIModelType.TEXT); - final Optional text8 = aiModels.findModel(otherAppConfig, "text-model-8", AIModelType.TEXT); - assertNotPresentModels(text7, text8); - - final Optional image9 = aiModels.findModel(otherAppConfig, "image-model-9", AIModelType.IMAGE); - final Optional image10 = aiModels.findModel(otherAppConfig, "image-model-10", AIModelType.IMAGE); - assertNotPresentModels(image9, image10); - - final Optional embeddings11 = aiModels.findModel(otherAppConfig, "embeddings-model-11", AIModelType.EMBEDDINGS); - final Optional embeddings12 = aiModels.findModel(otherAppConfig, "embeddings-model-12", AIModelType.EMBEDDINGS); - assertNotPresentModels(embeddings11, embeddings12); - - final List available = aiModels.getAvailableModels(); - final List availableNames = List.of( - "gpt-3.5-turbo-16k", "dall-e-3", "text-embedding-ada-002", - "text-model-1", "text-model-7", "text-model-8", - "image-model-9", "image-model-10", - "embeddings-model-11", "embeddings-model-12"); - assertTrue(available.stream().anyMatch(model -> availableNames.contains(model.getName()))); - } - - /** - * Given a set of models loaded into the AIModels instance - * When the resolveModel method is called with various model names and types - * Then the correct models should be resolved and their operational status verified. - */ - @Test - public void test_resolveModel() throws Exception { - AiTest.aiAppSecrets(host, "text-model-20", "image-model-21", "embeddings-model-22"); - ConfigService.INSTANCE.config(host); - AiTest.aiAppSecrets(otherHost, "text-model-23", null, null); - ConfigService.INSTANCE.config(otherHost); - - assertTrue(aiModels.resolveModel(host.getHostname(), AIModelType.TEXT).isOperational()); - assertTrue(aiModels.resolveModel(host.getHostname(), AIModelType.IMAGE).isOperational()); - assertTrue(aiModels.resolveModel(host.getHostname(), AIModelType.EMBEDDINGS).isOperational()); - assertTrue(aiModels.resolveModel(otherHost.getHostname(), AIModelType.TEXT).isOperational()); - assertFalse(aiModels.resolveModel(otherHost.getHostname(), AIModelType.IMAGE).isOperational()); - assertFalse(aiModels.resolveModel(otherHost.getHostname(), AIModelType.EMBEDDINGS).isOperational()); - } - - /** - * Given a set of models loaded into the AIModels instance - * When the resolveAIModelOrThrow method is called with various model names and types - * Then the correct models should be resolved and their operational status verified. - */ - @Test - public void test_resolveAIModelOrThrow() throws Exception { - AiTest.aiAppSecrets(host, "text-model-30", "image-model-31", "embeddings-model-32"); - - final AppConfig appConfig = ConfigService.INSTANCE.config(host); - final AIModel aiModel30 = aiModels.resolveAIModelOrThrow(appConfig, "text-model-30", AIModelType.TEXT); - final AIModel aiModel31 = aiModels.resolveAIModelOrThrow(appConfig, "image-model-31", AIModelType.IMAGE); - final AIModel aiModel32 = aiModels.resolveAIModelOrThrow( - appConfig, - "embeddings-model-32", - AIModelType.EMBEDDINGS); - - assertNotNull(aiModel30); - assertNotNull(aiModel31); - assertNotNull(aiModel32); - assertEquals("text-model-30", aiModel30.getModel("text-model-30").getName()); - assertEquals("image-model-31", aiModel31.getModel("image-model-31").getName()); - assertEquals("embeddings-model-32", aiModel32.getModel("embeddings-model-32").getName()); - - assertThrows( - DotAIModelNotFoundException.class, - () -> aiModels.resolveAIModelOrThrow(appConfig, "text-model-33", AIModelType.TEXT)); - assertThrows( - DotAIModelNotFoundException.class, - () -> aiModels.resolveAIModelOrThrow(appConfig, "image-model-34", AIModelType.IMAGE)); - assertThrows( - DotAIModelNotFoundException.class, - () -> aiModels.resolveAIModelOrThrow(appConfig, "embeddings-model-35", AIModelType.EMBEDDINGS)); - } - - /** - * Given a set of models loaded into the AIModels instance - * When the resolveModelOrThrow method is called with various model names and types - * Then the correct models should be resolved and their operational status verified. - */ - @Test - public void test_resolveModelOrThrow() throws Exception { - AiTest.aiAppSecrets(host, "text-model-40", "image-model-41", "embeddings-model-42"); - - final AppConfig appConfig = ConfigService.INSTANCE.config(host); - final Tuple2 modelTuple40 = aiModels.resolveModelOrThrow( - appConfig, - "text-model-40", - AIModelType.TEXT); - final Tuple2 modelTuple41 = aiModels.resolveModelOrThrow( - appConfig, - "image-model-41", - AIModelType.IMAGE); - final Tuple2 modelTuple42 = aiModels.resolveModelOrThrow( - appConfig, - "embeddings-model-42", - AIModelType.EMBEDDINGS); - - assertNotNull(modelTuple40); - assertNotNull(modelTuple41); - assertNotNull(modelTuple42); - assertEquals("text-model-40", modelTuple40._1.getModel("text-model-40").getName()); - assertEquals("image-model-41", modelTuple41._1.getModel("image-model-41").getName()); - assertEquals("embeddings-model-42", modelTuple42._1.getModel("embeddings-model-42").getName()); - - assertThrows( - DotAIModelNotFoundException.class, - () -> aiModels.resolveAIModelOrThrow(appConfig, "text-model-43", AIModelType.TEXT)); - assertThrows( - DotAIModelNotFoundException.class, - () -> aiModels.resolveAIModelOrThrow(appConfig, "image-model-44", AIModelType.IMAGE)); - assertThrows( - DotAIModelNotFoundException.class, - () -> aiModels.resolveAIModelOrThrow(appConfig, "embeddings-model-45", AIModelType.EMBEDDINGS)); - } - - /** - * Given a URL for supported models - * When the getOrPullSupportedModules method is called - * Then a list of supported models should be returned. - */ - @Test - public void test_getOrPullSupportedModels() { - AIModels.get().cleanSupportedModelsCache(); - final AppConfig appConfig = ConfigService.INSTANCE.config(host); - - Set supported = aiModels.getOrPullSupportedModels(appConfig); - assertNotNull(supported); - assertEquals(38, supported.size()); - } - - /** - * Given an invalid URL for supported models - * When the getOrPullSupportedModules method is called - * Then an exception should be thrown - */ - @Test - public void test_getOrPullSupportedModuels_withNetworkError() { - final AppConfig appConfig = ConfigService.INSTANCE.config(host); - AIModels.get().cleanSupportedModelsCache(); - IPUtils.disabledIpPrivateSubnet(false); - - assertThrows(DotRuntimeException.class, () ->aiModels.getOrPullSupportedModels(appConfig)); - IPUtils.disabledIpPrivateSubnet(true); - } - - /** - * Given no API key - * When the getOrPullSupportedModules method is called - * Then an exception should be thrown. - */ - @Test - public void test_getOrPullSupportedModels_noApiKey() throws Exception { - AiTest.aiAppSecrets(host, null); - final AppConfig appConfig = ConfigService.INSTANCE.config(host); - - AIModels.get().cleanSupportedModelsCache(); - final Set supported = aiModels.getOrPullSupportedModels(appConfig); - - assertTrue(supported.isEmpty()); - } - - private static void assertSameModels(final Optional text3, - final Optional text1, - final Optional text2) { - assertTrue(text3.isPresent()); - assertSame(text1.get(), text3.get()); - assertSame(text2.get(), text3.get()); - } - - private static void assertModels(final Optional model1, - final Optional model2, - final AIModelType type, - final boolean assertModelNames) { - assertTrue(model1.isPresent()); - assertTrue(model2.isPresent()); - assertSame(model1.get(), model2.get()); - assertSame(type, model1.get().getType()); - assertSame(type, model2.get().getType()); - if (assertModelNames) { - assertTrue(model1.get().getModels().stream().allMatch(model -> model.getStatus() == ModelStatus.ACTIVE)); - assertTrue(model2.get().getModels().stream().allMatch(model -> model.getStatus() == ModelStatus.ACTIVE)); - } - } - - private static void assertNotPresentModels(final Optional model1, final Optional model2) { - assertTrue(model1.isEmpty()); - assertTrue(model2.isEmpty()); - } - -} diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/app/ConfigServiceTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/app/ConfigServiceTest.java index 9cf43996ebeb..ab854c084e21 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/app/ConfigServiceTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/app/ConfigServiceTest.java @@ -72,34 +72,30 @@ public boolean hasValidLicense() { } /** - * Given a host with secrets and a ConfigService + * Given a host with providerConfig secrets and a ConfigService * When the config method is called with the host - * Then the models should be operational and the host should be correctly set in the AppConfig. + * Then the config should be enabled and the host should be correctly set in the AppConfig. */ @Test public void test_config_hostWithSecrets() throws Exception { - AiTest.aiAppSecrets(host, "text-model-0", "image-model-1", "embeddings-model-2"); + AiTest.aiAppSecretsWithProviderConfig(host, AiTest.providerConfigJson(AiTest.PORT, "gpt-4o-mini")); final AppConfig appConfig = configService.config(host); - assertTrue(appConfig.getModel().isOperational()); - assertTrue(appConfig.getImageModel().isOperational()); - assertTrue(appConfig.getEmbeddingsModel().isOperational()); + assertTrue(appConfig.isEnabled()); assertEquals(host.getHostname(), appConfig.getHost()); } /** - * Given a host without secrets and a ConfigService - * When the config method is called with the host - * Then the models should be operational and the host should be set to "System Host" in the AppConfig. + * Given only the system host has providerConfig secrets and a ConfigService + * When the config method is called with a host that has no secrets + * Then the config should fall back to the system host, be enabled, and report "System Host". */ @Test public void test_config_hostWithoutSecrets() throws Exception { - AiTest.aiAppSecrets(APILocator.systemHost(), "text-model-10", "image-model-11", "embeddings-model-12"); + AiTest.aiAppSecretsWithProviderConfig(APILocator.systemHost(), AiTest.providerConfigJson(AiTest.PORT, "gpt-4o-mini")); final AppConfig appConfig = configService.config(host); - assertTrue(appConfig.getModel().isOperational()); - assertTrue(appConfig.getImageModel().isOperational()); - assertTrue(appConfig.getEmbeddingsModel().isOperational()); + assertTrue(appConfig.isEnabled()); assertEquals("System Host", appConfig.getHost()); } diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/client/AIProxyClientTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/client/AIProxyClientTest.java index 6bd60b55a4fb..ad0aa99894ee 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/client/AIProxyClientTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/client/AIProxyClientTest.java @@ -2,48 +2,31 @@ import com.dotcms.ai.AiKeys; import com.dotcms.ai.AiTest; -import com.dotcms.ai.app.AIModel; -import com.dotcms.ai.app.AIModelType; -import com.dotcms.ai.app.AIModels; import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.AppKeys; import com.dotcms.ai.app.ConfigService; import com.dotcms.ai.domain.AIResponse; -import com.dotcms.ai.domain.Model; -import com.dotcms.ai.domain.ModelStatus; -import com.dotcms.ai.exception.DotAIAllModelsExhaustedException; -import com.dotcms.ai.exception.DotAIClientConnectException; import com.dotcms.ai.util.LineReadingOutputStream; import com.dotcms.datagen.SiteDataGen; import com.dotcms.datagen.UserDataGen; import com.dotcms.util.IntegrationTestInitService; import com.dotcms.util.network.IPUtils; import com.dotmarketing.beans.Host; -import com.dotmarketing.business.APILocator; import com.dotmarketing.util.UtilMethods; import com.dotmarketing.util.json.JSONArray; import com.dotmarketing.util.json.JSONObject; import com.github.tomakehurst.wiremock.WireMockServer; import com.liferay.portal.model.User; -import io.vavr.Tuple2; import org.junit.*; import java.io.ByteArrayOutputStream; -import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -/** - * Unit tests for the AIProxyClient class. - * - * @author vico - */ public class AIProxyClientTest { private static WireMockServer wireMockServer; @@ -57,9 +40,6 @@ public static void beforeClass() throws Exception { IntegrationTestInitService.getInstance().init(); IPUtils.disabledIpPrivateSubnet(true); wireMockServer = AiTest.prepareWireMock(); - final Host systemHost = APILocator.systemHost(); - AiTest.aiAppSecrets(systemHost); - ConfigService.INSTANCE.config(systemHost); user = new UserDataGen().nextPersisted(); } @@ -70,8 +50,12 @@ public static void afterClass() { } @Before - public void before() { + public void before() throws Exception { host = new SiteDataGen().nextPersisted(); + AiTest.aiAppSecretsWithProviderConfig( + host, + AiTest.providerConfigJson(AiTest.PORT, "gpt-4o-mini")); + appConfig = ConfigService.INSTANCE.config(host); } @After @@ -80,18 +64,15 @@ public void after() throws Exception { } /** - * Scenario: Calling AI with a valid model - * Given a valid model "gpt-4o-mini" - * When the request is sent to the AI service - * Then the response should contain the model name "gpt-4o-mini" + * Scenario: Calling AI with a valid providerConfig pointing to WireMock + * Given a providerConfig with model "gpt-4o-mini" and endpoint on WireMock + * When the request is sent + * Then the response should contain a non-null JSON with model name "gpt-4o-mini" */ @Test - public void test_callToAI_happiestPath() throws Exception { - final String model = "gpt-4o-mini"; - AiTest.aiAppSecrets(host, model, "dall-e-3", "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); + public void test_callToAI_withGpt4oMini_happiestPath() { final JSONObjectAIRequest request = textRequest( - model, + "gpt-4o-mini", "What are the major achievements of the Apollo space program?"); final AIResponse aiResponse = aiProxyClient.callToAI(request); @@ -102,217 +83,44 @@ public void test_callToAI_happiestPath() throws Exception { } /** - * Scenario: Calling AI with multiple models - * Given multiple models including "gpt-4o-mini" - * When the request is sent to the AI service - * Then the response should contain the model name "gpt-4o-mini" + * Scenario: Calling AI with a provided output stream + * Given a valid providerConfig and an output stream + * When the request is sent + * Then the response body goes to the stream; AIResponse.getResponse() is null */ @Test - public void test_callToAI_happyPath_withMultipleModels() throws Exception { - final String model = "gpt-4o-mini"; - AiTest.aiAppSecrets( - host, - String.format("%s,some-made-up-model-1", model), - "dall-e-3", - "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); - final JSONObjectAIRequest request = textRequest( - model, - "What are the major achievements of the Apollo space program?"); - - final AIResponse aiResponse = aiProxyClient.callToAI(request); - - assertNotNull(aiResponse); - assertNotNull(aiResponse.getResponse()); - assertEquals("gpt-4o-mini", new JSONObject(aiResponse.getResponse()).getString(AiKeys.MODEL)); - } - - /** - * Scenario: Calling AI with an invalid model - * Given an invalid model "some-made-up-model-10" - * When the request is sent to the AI service - * Then a DotAIAllModelsExhaustedException should be thrown - */ - @Test - public void test_callToAI_withInvalidModel() throws Exception { - final String invalidModel = "some-made-up-model-10"; - AiTest.aiAppSecrets(host, invalidModel, "dall-e-3", "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); + public void test_callToAI_withGpt4oMini_andProvidedOutput() { final JSONObjectAIRequest request = textRequest( - invalidModel, + "gpt-4o-mini", "What are the major achievements of the Apollo space program?"); - assertThrows(DotAIAllModelsExhaustedException.class, () -> aiProxyClient.callToAI(request)); - final Tuple2 modelTuple = appConfig.resolveModelOrThrow(invalidModel, AIModelType.TEXT); - assertSame(ModelStatus.INVALID, modelTuple._2.getStatus()); - assertEquals(-1, modelTuple._1.getCurrentModelIndex()); - assertTrue(AIModels.get() - .getAvailableModels() - .stream() - .noneMatch(model -> model.getName().equals(invalidModel))); - assertThrows(DotAIAllModelsExhaustedException.class, () -> aiProxyClient.callToAI(request)); - } - - /** - * Scenario: Calling AI with a decommissioned model - * Given a decommissioned model "some-decommissioned-model-20" - * When the request is sent to the AI service - * Then a DotAIAllModelsExhaustedException should be thrown - */ - @Test - public void test_callToAI_withDecommissionedModel() throws Exception { - final String decommissionedModel = "some-decommissioned-model-20"; - AiTest.aiAppSecrets(host, decommissionedModel, "dall-e-3", "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); - final JSONObjectAIRequest request = textRequest( - decommissionedModel, - "What are the major achievements of the Apollo space program?"); - - assertThrows(DotAIAllModelsExhaustedException.class, () -> aiProxyClient.callToAI(request)); - final Tuple2 modelTuple = appConfig.resolveModelOrThrow(decommissionedModel, AIModelType.TEXT); - assertSame(ModelStatus.DECOMMISSIONED, modelTuple._2.getStatus()); - assertEquals(-1, modelTuple._1.getCurrentModelIndex()); - assertTrue(AIModels.get() - .getAvailableModels() - .stream() - .noneMatch(model -> model.getName().equals(decommissionedModel))); - assertThrows(DotAIAllModelsExhaustedException.class, () -> aiProxyClient.callToAI(request)); - } - - /** - * Scenario: Calling AI with multiple models including invalid, decommissioned, and valid models - * Given models "some-made-up-model-30", "some-decommissioned-model-31", and "gpt-4o-mini" - * When the request is sent to the AI service - * Then the response should contain the model name "gpt-4o-mini" - */ - @Test - public void test_callToAI_withMultipleModels_invalidAndDecommissionedAndValid() throws Exception { - final String invalidModel = "some-made-up-model-30"; - final String decommissionedModel = "some-decommissioned-model-31"; - final String validModel = "gpt-4o-mini"; - AiTest.aiAppSecrets( - host, - String.format("%s,%s,%s", invalidModel, decommissionedModel, validModel), - "dall-e-3", - "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); - final JSONObjectAIRequest request = textRequest(invalidModel, "What are the major achievements of the Apollo space program?"); - - final AIResponse aiResponse = aiProxyClient.callToAI(request); - - assertNotNull(aiResponse); - assertNotNull(aiResponse.getResponse()); - assertSame(ModelStatus.INVALID, appConfig.resolveModelOrThrow(invalidModel, AIModelType.TEXT)._2.getStatus()); - assertSame( - ModelStatus.DECOMMISSIONED, - appConfig.resolveModelOrThrow(decommissionedModel, AIModelType.TEXT)._2.getStatus()); - final Tuple2 modelTuple = appConfig.resolveModelOrThrow(validModel, AIModelType.TEXT); - assertSame(ModelStatus.ACTIVE, modelTuple._2.getStatus()); - assertEquals(2, modelTuple._1.getCurrentModelIndex()); - assertTrue(AIModels.get() - .getAvailableModels() - .stream() - .noneMatch(model -> List.of(invalidModel, decommissionedModel).contains(model.getName()))); - assertTrue(AIModels.get() - .getAvailableModels() - .stream() - .anyMatch(model -> model.getName().equals(validModel))); - assertEquals("gpt-4o-mini", new JSONObject(aiResponse.getResponse()).getString(AiKeys.MODEL)); - } - - /** - * Scenario: Calling AI with a valid model and provided output stream - * Given a valid model "gpt-4o-mini" and a provided output stream - * When the request is sent to the AI service - * Then the response should be written to the output stream - */ - @Test - public void test_callToAI_withProvidedOutput() throws Exception { - final String model = "gpt-4o-mini"; - AiTest.aiAppSecrets(host, model, "dall-e-3", "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); - final JSONObjectAIRequest request = textRequest( - model, - "What are the major achievements of the Apollo space program?"); - request.getPayload().put(AiKeys.STREAM, true); - final AIResponse aiResponse = aiProxyClient.callToAI( request, new LineReadingOutputStream(new ByteArrayOutputStream())); + assertNotNull(aiResponse); assertNull(aiResponse.getResponse()); } /** - * Scenario: Calling AI with an invalid model and provided output stream - * Given an invalid model "some-made-up-model-40" and a provided output stream - * When the request is sent to the AI service - * Then a DotAIAllModelsExhaustedException should be thrown - */ - @Test - public void test_callToAI_withInvalidModel_withProvidedOutput() throws Exception { - final String invalidModel = "some-made-up-model-40"; - AiTest.aiAppSecrets(host, invalidModel, "dall-e-3", "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); - final JSONObjectAIRequest request = textRequest( - invalidModel, - "What are the major achievements of the Apollo space program?"); - - assertThrows(DotAIAllModelsExhaustedException.class, () -> aiProxyClient.callToAI(request)); - final Tuple2 modelTuple = appConfig.resolveModelOrThrow(invalidModel, AIModelType.TEXT); - assertSame(ModelStatus.INVALID, modelTuple._2.getStatus()); - assertEquals(-1, modelTuple._1.getCurrentModelIndex()); - assertTrue(AIModels.get() - .getAvailableModels() - .stream() - .noneMatch(model -> model.getName().equals(invalidModel))); - } - - /** - * Scenario: Calling AI with a valid model and provided output stream - * Given a valid model "gpt-4o-mini" and a provided output stream - * When the request is sent to the AI service - * Then a 404 is reported from the provider - * Then a DotAIAllModelsExhaustedException should be thrown - */ - @Test - public void test_callToAI_with404StatusCode() throws Exception { - final String model = "gpt-4o-mini"; - AiTest.aiAppSecrets(host, model, "dall-e-3", "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); - final JSONObjectAIRequest request = textRequest( - model, - "What is something you cannot answer?"); - request.getPayload().put(AiKeys.STREAM, true); - - assertThrows(DotAIAllModelsExhaustedException.class, () -> aiProxyClient.callToAI(request)); - } - - /** - * Scenario: Calling AI with network issues - * Given a valid model "gpt-4o-mini" - * And the AI service is unavailable due to network issues - * When the request is sent to the AI service - * Then a DotAIClientConnectException should be thrown - * And the model should remain operational after the network is restored + * Scenario: Network issues + * Given WireMock is stopped + * When the request is sent + * Then a RuntimeException is thrown (LangChain4J wraps the connection error) */ @Test - public void test_callToAI_withNetworkIssues() throws Exception { - final String model = "gpt-4o-mini"; - AiTest.aiAppSecrets(host, model, "dall-e-3", "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); + public void test_callToAI_withGpt4oMini_andNetworkIssues() { final JSONObjectAIRequest request = textRequest( - model, + "gpt-4o-mini", "What are the major achievements of the Apollo space program?"); wireMockServer.stop(); - assertThrows(DotAIClientConnectException.class, () -> aiProxyClient.callToAI(request)); - - wireMockServer = AiTest.prepareWireMock(); - - final Tuple2 modelTuple = appConfig.resolveModelOrThrow(model, AIModelType.TEXT); - assertTrue(modelTuple._2.isOperational()); + try { + assertThrows(RuntimeException.class, () -> aiProxyClient.callToAI(request)); + } finally { + wireMockServer = AiTest.prepareWireMock(); + } } private JSONObjectAIRequest textRequest(final String model, final String prompt) { diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/listener/EmbeddingContentListenerTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/listener/EmbeddingContentListenerTest.java index ce61bbb8b6d6..68028c0a13f4 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/listener/EmbeddingContentListenerTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/listener/EmbeddingContentListenerTest.java @@ -191,8 +191,8 @@ private static boolean waitForEmbeddings(final Contentlet blogContent, final Str } private static void addDotAISecrets() throws Exception { - AiTest.aiAppSecrets(host, AiTest.API_KEY); - AiTest.aiAppSecrets(APILocator.systemHost(), AiTest.API_KEY); + AiTest.aiAppSecretsWithProviderConfig(host, AiTest.providerConfigJson(AiTest.PORT, AiTest.MODEL)); + AiTest.aiAppSecretsWithProviderConfig(APILocator.systemHost(), AiTest.providerConfigJson(AiTest.PORT, AiTest.MODEL)); } private static void removeDotAISecrets() throws DotDataException, DotSecurityException { diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/validator/AIAppValidatorTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/validator/AIAppValidatorTest.java deleted file mode 100644 index fc91f78b8786..000000000000 --- a/dotcms-integration/src/test/java/com/dotcms/ai/validator/AIAppValidatorTest.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.dotcms.ai.validator; - -import com.dotcms.DataProviderWeldRunner; -import com.dotcms.ai.AiTest; -import com.dotcms.ai.app.AppConfig; -import com.dotcms.ai.app.ConfigService; -import com.dotcms.ai.client.JSONObjectAIRequest; -import com.dotcms.api.system.event.message.SystemMessageEventUtil; -import com.dotcms.api.system.event.message.builder.SystemMessage; -import com.dotcms.datagen.SiteDataGen; -import com.dotcms.util.IntegrationTestInitService; -import com.dotcms.util.network.IPUtils; -import com.dotmarketing.beans.Host; -import com.dotmarketing.business.APILocator; -import com.github.tomakehurst.wiremock.WireMockServer; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; - -import javax.enterprise.context.ApplicationScoped; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * Unit tests for the AIAppValidator class. - * This class tests the validation of AI configurations and model usage. - * It ensures that the AI models specified in the application configuration are supported - * and not exhausted. - * - * The tests cover scenarios for valid configurations, invalid configurations, and configurations - * with missing fields. - * - * @author vico - */ -@ApplicationScoped -@RunWith(DataProviderWeldRunner.class) -public class AIAppValidatorTest { - - private static WireMockServer wireMockServer; - private static SystemMessageEventUtil systemMessageEventUtil; - private Host host; - private AppConfig appConfig; - private AIAppValidator validator = AIAppValidator.get(); - - @BeforeClass - public static void beforeClass() throws Exception { - IntegrationTestInitService.getInstance().init(); - wireMockServer = AiTest.prepareWireMock(); - final Host systemHost = APILocator.systemHost(); - AiTest.aiAppSecrets(systemHost); - ConfigService.INSTANCE.config(systemHost); - systemMessageEventUtil = mock(SystemMessageEventUtil.class); - } - - @AfterClass - public static void afterClass() { - wireMockServer.stop(); - IPUtils.disabledIpPrivateSubnet(false); - } - - @Before - public void before() { - IPUtils.disabledIpPrivateSubnet(true); - host = new SiteDataGen().nextPersisted(); - validator.setSystemMessageEventUtil(systemMessageEventUtil); - } - - @After - public void after() throws Exception { - AiTest.removeAiAppSecrets(host); - } - - @Test - /** - * Scenario: Validating AI configuration with unsupported models - * Given an AI configuration with unsupported models - * When the configuration is validated - * Then a warning message should be pushed to the user - */ - public void test_validateAIConfig() throws Exception { - final String invalidModel = "some-made-up-model-10"; - AiTest.aiAppSecrets(host, invalidModel, "dall-e-3", "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); - - verify(systemMessageEventUtil, atLeastOnce()).pushMessage(any(SystemMessage.class), anyList()); - } - - /** - * Scenario: Validating AI models usage with exhausted models - * Given an AI model with exhausted models - * When the models usage is validated - * Then a warning message should be pushed to the user for each exhausted model - */ - @Test - public void test_validateModelsUsage() throws Exception { - final String invalidModels = "some-made-up-model-20,some-decommissioned-model-21"; - AiTest.aiAppSecrets(host, invalidModels, "dall-e-3", "text-embedding-ada-002"); - appConfig = ConfigService.INSTANCE.config(host); - - final JSONObjectAIRequest request = JSONObjectAIRequest.builder().withUserId("jon.snow").build(); - validator.validateModelsUsage(appConfig.getModel(), request); - - verify(systemMessageEventUtil, atLeastOnce()).pushMessage(any(SystemMessage.class), anyList()); - } -} diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AIViewToolTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AIViewToolTest.java index dfdc2ed7d06b..b054f65cd2e7 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AIViewToolTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/AIViewToolTest.java @@ -52,7 +52,7 @@ public static void beforeClass() throws Exception { IPUtils.disabledIpPrivateSubnet(true); wireMockServer = AiTest.prepareWireMock(); final Host systemHost = APILocator.systemHost(); - AiTest.aiAppSecrets(systemHost, "gpt-4o-mini", "dall-e-3", "text-embedding-ada-002"); + AiTest.aiAppSecretsWithProviderConfig(systemHost, AiTest.providerConfigJson(AiTest.PORT, "gpt-4o-mini")); config = ConfigService.INSTANCE.config(systemHost); } diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/CompletionsToolTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/CompletionsToolTest.java index e481133edbca..cc90bc1e79ef 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/CompletionsToolTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/CompletionsToolTest.java @@ -55,8 +55,8 @@ public static void beforeClass() throws Exception { IPUtils.disabledIpPrivateSubnet(true); host = new SiteDataGen().nextPersisted(); wireMockServer = AiTest.prepareWireMock(); - AiTest.aiAppSecrets(APILocator.systemHost(), "gpt-4o-mini", "dall-e-3", "text-embedding-ada-002"); - AiTest.aiAppSecrets(host, "gpt-4o-mini", "dall-e-3", "text-embedding-ada-002"); + AiTest.aiAppSecretsWithProviderConfig(APILocator.systemHost(), AiTest.providerConfigJson(AiTest.PORT, "gpt-4o-mini")); + AiTest.aiAppSecretsWithProviderConfig(host, AiTest.providerConfigJson(AiTest.PORT, "gpt-4o-mini")); appConfig = ConfigService.INSTANCE.config(host); user = new UserDataGen().nextPersisted(); } @@ -90,7 +90,6 @@ public void test_getConfig() { assertNotNull(config); assertEquals(AppKeys.COMPLETION_ROLE_PROMPT.defaultValue, config.get(AppKeys.COMPLETION_ROLE_PROMPT.key)); assertEquals(AppKeys.COMPLETION_TEXT_PROMPT.defaultValue, config.get(AppKeys.COMPLETION_TEXT_PROMPT.key)); - assertEquals("gpt-4o-mini", config.get(AppKeys.TEXT_MODEL_NAMES.key)); } /** @@ -199,19 +198,11 @@ private static void assertResult(final JSONObject result) { private static void assertResponse(final JSONObject result) { assertNotNull(result.getString("openAiResponse")); final JSONObject openAiResponse = result.getJSONObject("openAiResponse"); - assertTrue(StringUtils.isNotBlank(openAiResponse.getString("id"))); - assertTrue(StringUtils.isNotBlank(openAiResponse.getString("object"))); - assertTrue(openAiResponse.getInt("created") > 0); - assertEquals("gpt-3.5-turbo-16k-0613", openAiResponse.getString("model")); assertFalse(openAiResponse.getJSONArray("choices").isEmpty()); final JSONObject choice = openAiResponse.getJSONArray("choices").getJSONObject(0); assertTrue(choice.containsKey("message")); final JSONObject message = choice.getJSONObject("message"); assertTrue(StringUtils.isNotBlank(message.getString("content"))); - assertNotNull(openAiResponse.getJSONObject("usage")); - assertFalse(openAiResponse.getJSONObject("usage").isEmpty()); - assertNotNull(openAiResponse.getJSONObject("headers")); - assertFalse(openAiResponse.getJSONObject("headers").isEmpty()); } private static void assertAll(final JSONObject result) { diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/EmbeddingsToolTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/EmbeddingsToolTest.java index 5f5b875a7f31..90e6664be5ce 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/EmbeddingsToolTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/EmbeddingsToolTest.java @@ -51,7 +51,7 @@ public static void beforeClass() throws Exception { IntegrationTestInitService.getInstance().init(); IPUtils.disabledIpPrivateSubnet(true); wireMockServer = AiTest.prepareWireMock(); - AiTest.aiAppSecrets(APILocator.systemHost()); + AiTest.aiAppSecretsWithProviderConfig(APILocator.systemHost(), AiTest.providerConfigJson(AiTest.PORT, AiTest.MODEL)); } @Before @@ -59,7 +59,7 @@ public void before() throws Exception { final ViewContext viewContext = mock(ViewContext.class); when(viewContext.getRequest()).thenReturn(mock(HttpServletRequest.class)); host = new SiteDataGen().nextPersisted(); - AiTest.aiAppSecrets(host); + AiTest.aiAppSecretsWithProviderConfig(host, AiTest.providerConfigJson(AiTest.PORT, AiTest.MODEL)); appConfig = ConfigService.INSTANCE.config(host); user = new UserDataGen().nextPersisted(); embeddingsTool = prepareEmbeddingsTool(viewContext); diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/SearchToolTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/SearchToolTest.java index 4d7b7ef6e891..a9fae9aabea6 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/SearchToolTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/viewtool/SearchToolTest.java @@ -49,7 +49,7 @@ public class SearchToolTest { @BeforeClass public static void beforeClass() throws Exception { IntegrationTestInitService.getInstance().init(); - AiTest.aiAppSecrets(APILocator.systemHost(), "gpt-4o-mini", "dall-e-3", "text-embedding-ada-002"); + AiTest.aiAppSecretsWithProviderConfig(APILocator.systemHost(), AiTest.providerConfigJson(AiTest.PORT, "gpt-4o-mini")); } @Before @@ -58,7 +58,7 @@ public void before() throws Exception { when(viewContext.getRequest()).thenReturn(mock(HttpServletRequest.class)); host = new SiteDataGen().nextPersisted(); searchTool = prepareSearchTool(viewContext); - AiTest.aiAppSecrets(host, "gpt-4o-mini", "dall-e-3", "text-embedding-ada-002"); + AiTest.aiAppSecretsWithProviderConfig(host, AiTest.providerConfigJson(AiTest.PORT, "gpt-4o-mini")); } /** diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIAutoTagActionletTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIAutoTagActionletTest.java index b825ee8842c8..935e0f85921f 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIAutoTagActionletTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIAutoTagActionletTest.java @@ -62,12 +62,6 @@ public static void beforeClass() throws Exception { AppKeys.API_KEY.key, Secret.builder().withType(Type.STRING).withValue(AiTest.API_KEY.toCharArray()).build(), - AppKeys.TEXT_MODEL_NAMES.key, - Secret.builder().withType(Type.STRING).withValue(AiTest.MODEL.toCharArray()).build(), - - AppKeys.IMAGE_MODEL_NAMES.key, - Secret.builder().withType(Type.STRING).withValue(AiTest.IMAGE_MODEL.toCharArray()).build(), - AppKeys.IMAGE_SIZE.key, Secret.builder().withType(Type.SELECT).withValue(AiTest.IMAGE_SIZE.toCharArray()).build(), diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIContentPromptActionletTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIContentPromptActionletTest.java index 3db2f537423c..de855a316287 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIContentPromptActionletTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIContentPromptActionletTest.java @@ -62,12 +62,6 @@ public static void beforeClass() throws Exception { AppKeys.API_KEY.key, Secret.builder().withType(Type.STRING).withValue(AiTest.API_KEY.toCharArray()).build(), - AppKeys.TEXT_MODEL_NAMES.key, - Secret.builder().withType(Type.STRING).withValue(AiTest.MODEL.toCharArray()).build(), - - AppKeys.IMAGE_MODEL_NAMES.key, - Secret.builder().withType(Type.STRING).withValue(AiTest.IMAGE_MODEL.toCharArray()).build(), - AppKeys.IMAGE_SIZE.key, Secret.builder().withType(Type.SELECT).withValue(AiTest.IMAGE_SIZE.toCharArray()).build(), diff --git a/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIGenerateImageActionletTest.java b/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIGenerateImageActionletTest.java index d35e9a02a8d0..ac1fc2e36c98 100644 --- a/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIGenerateImageActionletTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIGenerateImageActionletTest.java @@ -63,12 +63,6 @@ public static void beforeClass() throws Exception { AppKeys.API_KEY.key, Secret.builder().withType(Type.STRING).withValue(AiTest.API_KEY.toCharArray()).build(), - AppKeys.TEXT_MODEL_NAMES.key, - Secret.builder().withType(Type.STRING).withValue(AiTest.MODEL.toCharArray()).build(), - - AppKeys.IMAGE_MODEL_NAMES.key, - Secret.builder().withType(Type.STRING).withValue(AiTest.IMAGE_MODEL.toCharArray()).build(), - AppKeys.IMAGE_SIZE.key, Secret.builder().withType(Type.SELECT).withValue(AiTest.IMAGE_SIZE.toCharArray()).build(), diff --git a/dotcms-integration/src/test/resources/mappings/ai-future-stub.json b/dotcms-integration/src/test/resources/mappings/ai-future-stub.json index 77db5a737fc4..e5b1cfce1099 100644 --- a/dotcms-integration/src/test/resources/mappings/ai-future-stub.json +++ b/dotcms-integration/src/test/resources/mappings/ai-future-stub.json @@ -1,11 +1,8 @@ { "request": { "method": "POST", - "url": "/c", + "urlPathPattern": "/chat/completions", "headers": { - "Content-Type": { - "equalTo": "application/json" - }, "Authorization": { "equalTo": "Bearer some-api-key-1a2bc3" } diff --git a/dotcms-integration/src/test/resources/mappings/buy-tesla-stub.json b/dotcms-integration/src/test/resources/mappings/buy-tesla-stub.json index 71189a855c76..004a6f6983bd 100644 --- a/dotcms-integration/src/test/resources/mappings/buy-tesla-stub.json +++ b/dotcms-integration/src/test/resources/mappings/buy-tesla-stub.json @@ -1,11 +1,8 @@ { "request": { "method": "POST", - "url": "/c", + "urlPathPattern": "/chat/completions", "headers": { - "Content-Type": { - "equalTo": "application/json" - }, "Authorization": { "equalTo": "Bearer some-api-key-1a2bc3" } diff --git a/dotcms-integration/src/test/resources/mappings/cabj-text-stub.json b/dotcms-integration/src/test/resources/mappings/cabj-text-stub.json index d64fdf40a564..ffdd46fb8d84 100644 --- a/dotcms-integration/src/test/resources/mappings/cabj-text-stub.json +++ b/dotcms-integration/src/test/resources/mappings/cabj-text-stub.json @@ -1,18 +1,16 @@ { + "priority": 1, "request": { "method": "POST", - "url": "/c", + "urlPathPattern": "/chat/completions", "headers": { - "Content-Type": { - "equalTo": "application/json" - }, "Authorization": { "equalTo": "Bearer some-api-key-1a2bc3" } }, "bodyPatterns": [ { - "matchesJsonPath": "$.messages[?(@.content == 'Short text about Club Atletico Boca Juniors')]" + "matches": ".*\"content\" : \"Short text about Club Atletico Boca Juniors.*" } ] }, diff --git a/dotcms-integration/src/test/resources/mappings/chaos-theory-text-stub.json b/dotcms-integration/src/test/resources/mappings/chaos-theory-text-stub.json index 377fbc8b516b..d96cc8593512 100644 --- a/dotcms-integration/src/test/resources/mappings/chaos-theory-text-stub.json +++ b/dotcms-integration/src/test/resources/mappings/chaos-theory-text-stub.json @@ -1,18 +1,16 @@ { + "priority": 1, "request": { "method": "POST", - "url": "/c", + "urlPathPattern": "/chat/completions", "headers": { - "Content-Type": { - "equalTo": "application/json" - }, "Authorization": { "equalTo": "Bearer some-api-key-1a2bc3" } }, "bodyPatterns": [ { - "matchesJsonPath": "$.messages[?(@.content == 'Short text about Theory of Chaos')]" + "matches": ".*\"content\" : \"Short text about Theory of Chaos.*" } ] }, diff --git a/dotcms-integration/src/test/resources/mappings/dalailama-slam-dunk-image-stub.json b/dotcms-integration/src/test/resources/mappings/dalailama-slam-dunk-image-stub.json index 694671e69ff0..49049fbcca43 100644 --- a/dotcms-integration/src/test/resources/mappings/dalailama-slam-dunk-image-stub.json +++ b/dotcms-integration/src/test/resources/mappings/dalailama-slam-dunk-image-stub.json @@ -1,11 +1,8 @@ { "request": { "method": "POST", - "url": "/i", + "urlPathPattern": "/images/generations", "headers": { - "Content-Type": { - "equalTo": "application/json" - }, "Authorization": { "equalTo": "Bearer some-api-key-1a2bc3" } diff --git a/dotcms-integration/src/test/resources/mappings/first-president.json b/dotcms-integration/src/test/resources/mappings/first-president.json index 5707e60072fe..2435d545767e 100644 --- a/dotcms-integration/src/test/resources/mappings/first-president.json +++ b/dotcms-integration/src/test/resources/mappings/first-president.json @@ -1,11 +1,8 @@ { "request": { "method": "POST", - "url": "/c", + "urlPathPattern": "/chat/completions", "headers": { - "Content-Type": { - "equalTo": "application/json" - }, "Authorization": { "equalTo": "Bearer some-api-key-1a2bc3" } diff --git a/dotcms-integration/src/test/resources/mappings/ganymede-moon-image-stub.json b/dotcms-integration/src/test/resources/mappings/ganymede-moon-image-stub.json index 77ac3032b92b..672e14f7e10d 100644 --- a/dotcms-integration/src/test/resources/mappings/ganymede-moon-image-stub.json +++ b/dotcms-integration/src/test/resources/mappings/ganymede-moon-image-stub.json @@ -1,11 +1,8 @@ { "request": { "method": "POST", - "url": "/i", + "urlPathPattern": "/images/generations", "headers": { - "Content-Type": { - "equalTo": "application/json" - }, "Authorization": { "equalTo": "Bearer some-api-key-1a2bc3" } diff --git a/dotcms-integration/src/test/resources/mappings/langchain4j-chat-stub.json b/dotcms-integration/src/test/resources/mappings/langchain4j-chat-stub.json new file mode 100644 index 000000000000..c38933be36c8 --- /dev/null +++ b/dotcms-integration/src/test/resources/mappings/langchain4j-chat-stub.json @@ -0,0 +1,38 @@ +{ + "request": { + "method": "POST", + "urlPathPattern": "/chat/completions", + "headers": { + "Authorization": { + "equalTo": "Bearer some-api-key-1a2bc3" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "id": "chatcmpl-test123", + "object": "chat.completion", + "created": 1713823646, + "model": "gpt-4o-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "This is a test response from the mocked AI provider." + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 20, + "completion_tokens": 15, + "total_tokens": 35 + } + } + } +} diff --git a/dotcms-integration/src/test/resources/mappings/langchain4j-embeddings-stub.json b/dotcms-integration/src/test/resources/mappings/langchain4j-embeddings-stub.json new file mode 100644 index 000000000000..06e44ab0e1f4 --- /dev/null +++ b/dotcms-integration/src/test/resources/mappings/langchain4j-embeddings-stub.json @@ -0,0 +1,1566 @@ +{ + "request": { + "method": "POST", + "urlPathPattern": "/embeddings", + "headers": { + "Authorization": { + "equalTo": "Bearer some-api-key-1a2bc3" + } + } + }, + "response": { + "status": 200, + "jsonBody": { + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": [ + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036, + 0.0037, + 0.0038, + 0.0039, + 0.004, + 0.0041, + 0.0042, + 0.0043, + 0.0044, + 0.0045, + 0.0046, + 0.0047, + 0.0048, + 0.0049, + 0.005, + 0.0051, + 0.0052, + 0.0053, + 0.0054, + 0.0055, + 0.0056, + 0.0057, + 0.0058, + 0.0059, + 0.006, + 0.0061, + 0.0062, + 0.0063, + 0.0064, + 0.0065, + 0.0066, + 0.0067, + 0.0068, + 0.0069, + 0.007, + 0.0071, + 0.0072, + 0.0073, + 0.0074, + 0.0075, + 0.0076, + 0.0077, + 0.0078, + 0.0079, + 0.008, + 0.0081, + 0.0082, + 0.0083, + 0.0084, + 0.0085, + 0.0086, + 0.0087, + 0.0088, + 0.0089, + 0.009, + 0.0091, + 0.0092, + 0.0093, + 0.0094, + 0.0095, + 0.0096, + 0.0097, + 0.0098, + 0.0099, + 0.01, + 0.0001, + 0.0002, + 0.0003, + 0.0004, + 0.0005, + 0.0006, + 0.0007, + 0.0008, + 0.0009, + 0.001, + 0.0011, + 0.0012, + 0.0013, + 0.0014, + 0.0015, + 0.0016, + 0.0017, + 0.0018, + 0.0019, + 0.002, + 0.0021, + 0.0022, + 0.0023, + 0.0024, + 0.0025, + 0.0026, + 0.0027, + 0.0028, + 0.0029, + 0.003, + 0.0031, + 0.0032, + 0.0033, + 0.0034, + 0.0035, + 0.0036 + ] + } + ], + "model": "text-embedding-ada-002", + "usage": { + "prompt_tokens": 8, + "total_tokens": 8 + } + } + } +} \ No newline at end of file diff --git a/dotcms-integration/src/test/resources/mappings/light-speed-stub.json b/dotcms-integration/src/test/resources/mappings/light-speed-stub.json index 12394ffec16d..090a779a3b4e 100644 --- a/dotcms-integration/src/test/resources/mappings/light-speed-stub.json +++ b/dotcms-integration/src/test/resources/mappings/light-speed-stub.json @@ -1,11 +1,8 @@ { "request": { "method": "POST", - "url": "/c", + "urlPathPattern": "/chat/completions", "headers": { - "Content-Type": { - "equalTo": "application/json" - }, "Authorization": { "equalTo": "Bearer some-api-key-1a2bc3" } diff --git a/dotcms-postman/src/main/resources/postman/AI.postman_collection.json b/dotcms-postman/src/main/resources/postman/AI.postman_collection.json index ae20a72b099e..5417a7d5ed3f 100644 --- a/dotcms-postman/src/main/resources/postman/AI.postman_collection.json +++ b/dotcms-postman/src/main/resources/postman/AI.postman_collection.json @@ -1,3431 +1,3411 @@ { - "info": { - "_postman_id": "140d143a-c58a-471c-9e22-2754cf252c22", - "name": "AI", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "5403727" - }, - "item": [ - { - "name": "pre", - "item": [ - { - "name": "dotAI App", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 200\", function () {", - " pm.response.to.have.status(200);", - "});" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotCMS.com", - "type": "string" - }, - { - "key": "saveHelperData", - "type": "any" - }, - { - "key": "showPassword", - "value": false, - "type": "boolean" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"apiKey\": {\n \"value\": \"some-api-key-1a2bc3\"\n },\n \"textModelNames\": {\n \"value\": \"gpt-4o-mini\"\n },\n \"textModelMaxTokens\": {\n \"value\":\"16384\"\n },\n \"imageModelNames\": {\n \"value\": \"dall-e-3\"\n },\n \"imageSize\": {\n \"value\": \"1024x1024\"\n },\n \"imageModelMaxTokens\": {\n \"value\":\"0\"\n },\n \"embeddingsModelNames\": {\n \"value\": \"text-embedding-ada-002\"\n },\n \"embeddingsModelMaxTokens\": {\n \"value\":\"8191\"\n },\n \"listenerIndexer\": {\n \"value\": \"{\\\"default\\\":\\\"blog,dotcmsdocumentation,feature,ProductBriefs,news,report.file,builds,casestudy\\\",\\\"documentation\\\":\\\"dotcmsdocumentation\\\"}\"\n }\n}\n" - }, - "url": { - "raw": "{{serverURL}}/api/v1/apps/dotAI/SYSTEM_HOST", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "apps", - "dotAI", - "SYSTEM_HOST" - ] - }, - "description": "This tests the endpoint that brings back one specific App/integration given the App-key followed by the site-id" - }, - "response": [] - } - ] - }, - { - "name": "Generative", - "item": [ - { - "name": "Test", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Type is returned', function () {", - " pm.expect(jsonData.type, 'Type is \"image\"').equals('image');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{serverURL}}/api/v1/ai/image/test", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "image", - "test" - ] - } - }, - "response": [] - }, - { - "name": "Generate Text", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Text is generated', function () {", - " pm.expect(jsonData.choices, 'Choices are included in dotAI response').not.undefined;", - " pm.expect(jsonData.choices, 'Choices are included in dotAI respons are not empty').not.empty;", - " pm.expect(jsonData.choices[0].text).contains('The FIFA World Cup in 2018 was won by the French national football team')", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prompt\": \"Who won the FIFA World Cup in 2018?\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/text/generate", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "text", - "generate" - ] - } - }, - "response": [] - }, - { - "name": "Generate Text", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Text is generated', function () {", - " pm.expect(jsonData.choices, 'Choices are included in dotAI response').not.undefined;", - " pm.expect(jsonData.choices, 'Choices are included in dotAI respons are not empty').not.empty;", - " pm.expect(jsonData.choices[0].text).contains('The theory of relativity, developed by Albert Einstein, consists of')", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{serverURL}}/api/v1/ai/text/generate?prompt=What%20is%20the%20theory%20of%20relativity%3F", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "text", - "generate" - ], - "query": [ - { - "key": "prompt", - "value": "What%20is%20the%20theory%20of%20relativity%3F" - } - ] - } - }, - "response": [] - }, - { - "name": "Generate Text without prompt", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be 400', function () {", - " pm.response.to.have.status(400);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Error message is returned', function () {", - " pm.expect(jsonData.message, 'Error message is included in response').equals('query/prompt cannot be null');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{serverURL}}/api/v1/ai/text/generate", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "text", - "generate" - ] - } - }, - "response": [] - }, - { - "name": "Generate Image", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Image is generated', function () {", - " pm.expect(jsonData.revised_prompt, 'Field revised_prompt is included in dotAI response').not.null;", - " pm.expect(jsonData.url, 'Field url is included in dotAI response').not.null;", - " pm.expect(jsonData.tempFileName, 'Field tempFileName is included in dotAI response').not.null;", - " pm.expect(jsonData.tempFile, 'Field tempFile is included in dotAI response').not.null;", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prompt\": \"Generate image of a turtle training for a marathon\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/image/generate", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "image", - "generate" - ] - } - }, - "response": [] - }, - { - "name": "Generate Image", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Image is generated', function () {", - " pm.expect(jsonData.revised_prompt, 'Field revised_prompt is included in dotAI response').not.null;", - " pm.expect(jsonData.url, 'Field url is included in dotAI response').not.null;", - " pm.expect(jsonData.tempFileName, 'Field tempFileName is included in dotAI response').not.null;", - " pm.expect(jsonData.tempFile, 'Field tempFile is included in dotAI response').not.null;", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{serverURL}}/api/v1/ai/image/generate?prompt=Image%20of%20a%20robot%20painting%20the%20sixteen%20chapel", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "image", - "generate" - ], - "query": [ - { - "key": "prompt", - "value": "Image%20of%20a%20robot%20painting%20the%20sixteen%20chapel" - } - ] - } - }, - "response": [] - }, - { - "name": "Generate Image without prompt", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be 400', function () {", - " pm.response.to.have.status(400);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Error message is returned', function () {", - " pm.expect(jsonData.error, 'Error message is included in response').equals('`prompt` is required');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{serverURL}}/api/v1/ai/image/generate", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "image", - "generate" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Embeddings", - "item": [ - { - "name": "Delete DB", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('DB is reset', function () {", - " pm.expect(jsonData.created, 'DB is deleted and created').equals(true);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{serverURL}}/api/v1/ai/embeddings/db", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "embeddings", - "db" - ] - } - }, - "response": [] - }, - { - "name": "Test", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Type is returned', function () {", - " pm.expect(jsonData.type, 'Type is \"embeddings\"').equals('embeddings');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{serverURL}}/api/v1/ai/embeddings/test", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "embeddings", - "test" - ] - } - }, - "response": [] - }, - { - "name": "Create Content Type", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "const seoContentTypeId = jsonData.entity[0].id;", - "pm.collectionVariables.set('seoContentTypeId', seoContentTypeId);", - "const seoContentTypeVar = jsonData.entity[0].variable;", - "pm.collectionVariables.set('seoContentTypeVar', seoContentTypeVar);", - "", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", - "const currentSeo = seos[seoIndex];", - "if (currentSeo) {", - " currentSeo.seoContentTypeId = seoContentTypeId;", - " currentSeo.seoContentTypeVar = seoContentTypeVar;", - " pm.collectionVariables.set('seoText', currentSeo.text);", - " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", - "}", - "", - "console.log('seoIndex', seoIndex);", - "console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", - "", - "pm.execution.setNextRequest('Add Field to Content Type');", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", - "", - " if (seoIndex === 0) {", - " pm.collectionVariables.clear();", - " }", - " pm.collectionVariables.set('seoIndex', seoIndex);", - "", - "const initialSeos = [", - " {", - " id: 'stock-market',", - " text: 'The stock market has shown significant volatility over the past few months. Analysts attribute this to geopolitical tensions and economic uncertainty. Investors are advised to diversify their portfolios to mitigate risks.'", - " },", - " {", - " id: 'popular-novel',", - " text: 'J.K. Rowling\\'s \\'Harry Potter and the Sorcerer\\'s Stone\\' follows the journey of a young boy, Harry Potter, who discovers he is a wizard on his eleventh birthday. He attends Hogwarts School of Witchcraft and Wizardry, where he makes friends, learns about his past, and uncovers the truth about his parents\\' mysterious deaths.'", - " },", - " {", - " id: 'historical-event',", - " text: 'The signing of the Declaration of Independence on July 4, 1776, marked the Thirteen Colonies\\' formal separation from Great Britain. This historic document, primarily authored by Thomas Jefferson, outlined the colonies\\' grievances against the British crown and asserted their right to self-governance.'", - " },", - " {", - " id: 'mental-health',", - " text: 'Mental health awareness has become increasingly important in recent years. Experts emphasize the need for regular mental health check-ups and advocate for reducing the stigma associated with mental illnesses.'", - " }", - "];", - "", - "const collectionSeos = pm.collectionVariables.get('seos');", - "const seos = collectionSeos ? JSON.parse(collectionSeos) : initialSeos;", - "pm.collectionVariables.set('seoId', seos[seoIndex].id);", - "const seosJson = JSON.stringify(seos, null, 2);", - "pm.collectionVariables.set('seos', seosJson);", - "console.log('seos', seosJson);", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"defaultType\":false,\n \"icon\":null,\n \"fixed\":false,\n \"system\":false,\n \"clazz\":\"com.dotcms.contenttype.model.type.ImmutableSimpleContentType\",\n \"description\":\"\",\n \"host\":\"8a7d5e23-da1e-420a-b4f0-471e7da8ea2d\",\n \"folder\":\"SYSTEM_FOLDER\",\n \"name\":\"{{seoId}}-ContentType\",\n \"systemActionMappings\":{\"NEW\":\"\"},\n \"workflow\":[\"d61a59e1-a49c-46f2-a929-db2b4bfa88b2\"]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/contenttype", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "contenttype" - ] - } - }, - "response": [] - }, - { - "name": "Add Field to Content Type", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.execution.setNextRequest('Create Contentlet');" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"layout\":[\n {\"divider\":{\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableRowField\",\n \"contentTypeId\":\"{{seoContentTypeId}}\",\n \"dataType\":\"SYSTEM\",\n \"fieldContentTypeProperties\":[],\n \"fieldType\":\"Row\",\n \"fieldTypeLabel\":\"Row\",\n \"fieldVariables\":[],\n \"fixed\":false,\n \"iDate\":1667572217000,\n \"indexed\":false,\n \"listed\":false,\n \"modDate\":1667572217000,\n \"name\":\"Row Field\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":-1,\n \"unique\":false},\n \"columns\":[\n {\n \"columnDivider\":{\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableColumnField\",\n \"contentTypeId\":\"{{seoContentTypeId}}\",\n \"dataType\":\"SYSTEM\",\n \"fieldContentTypeProperties\":[],\n \"fieldType\":\"Column\",\n \"fieldTypeLabel\":\"Column\",\n \"fieldVariables\":[],\n \"fixed\":false,\n \"iDate\":1667572217000,\n \"indexed\":false,\n \"listed\":false,\n \"modDate\":1667572217000,\n \"name\":\"Column Field\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":-1,\n \"unique\":false\n },\n \"fields\":[\n {\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableTextField\",\n \"name\":\"seo\",\n \"dataType\":\"TEXT\",\n \"regexCheck\":\"\",\n \"defaultValue\":\"\",\n \"hint\":\"\",\n \"required\":false,\n \"searchable\":false,\n \"indexed\":false,\n \"listed\":false,\n \"unique\":false,\n \"id\":null\n }\n ]\n }\n ]\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v3/contenttype/{{seoContentTypeId}}/fields/move", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v3", - "contenttype", - "{{seoContentTypeId}}", - "fields", - "move" - ] - } - }, - "response": [] - }, - { - "name": "Create Contentlet", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code should be ok 200\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "", - "let seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", - "console.log('seoIndex', seoIndex);", - "", - "const currentSeo = seos[seoIndex];", - "if (currentSeo) {", - " if (!currentSeo.contentlets) {", - " currentSeo.contentlets = [];", - " }", - "", - " const contentlet = {};", - " contentlet.identifier = jsonData.entity.identifier;", - " contentlet.inode = jsonData.entity.inode;", - " currentSeo.contentlets.push(contentlet);", - " console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", - " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", - "}", - "", - "seoIndex++;", - "pm.collectionVariables.set('seoIndex', seoIndex);", - "console.log('New seoIndex', seoIndex);", - "let nextRequest = null;", - "if (seoIndex < seos.length) {", - " console.log('Continuing with next SEO');", - " nextRequest = 'Create Content Type'", - "} else {", - " console.log('SEO loading done');", - " pm.collectionVariables.set('seoIndex', null);", - " pm.collectionVariables.set('seoId', null);", - " pm.collectionVariables.set('seoContentTypeId', null);", - " pm.collectionVariables.set('seoContentTypeVar', null);", - " pm.collectionVariables.set('seoText', null);", - " nextRequest = 'Create Embeddings without query';", - "}", - "pm.execution.setNextRequest(nextRequest);", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{ \n \"contentlet\" : {\n \"title\" : \"content_{{seoId}}\",\n \"languageId\" : 1,\n \"stInode\": \"{{seoContentTypeId}}\",\n \"seo\": \"{{seoText}}\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/PUBLISH", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "workflow", - "actions", - "default", - "fire", - "PUBLISH" - ] - } - }, - "response": [] - }, - { - "name": "Create Embeddings without query", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 400', function () {", - " pm.response.to.have.status(400);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Error message is returned', function () {", - " pm.expect(jsonData.message, 'Error message is included in response').equals('query cannot be null');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "packages": {}, - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/embeddings", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "embeddings" - ] - } - }, - "response": [] - }, - { - "name": "Create Embeddings", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "let seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", - "const currentSeo = seos[seoIndex];", - "if (currentSeo) {", - " if (!currentSeo.embedded) {", - " currentSeo.embedded = jsonData.totalToEmbed > 0;", - " }", - " ", - " console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", - " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", - "}", - "", - "pm.test('Emebeddings are created', function () {", - " pm.expect(jsonData.indexName, 'Index name should be \"default\"').equals('default');", - " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", - "});", - "", - "if (currentSeo.embedded) {", - " pm.expect(jsonData.totalToEmbed, 'Total to embed is greater than zero').greaterThan(0);", - "} else {", - " pm.expect(jsonData.totalToEmbed, 'Total to embed is zero').equals(0);", - "}", - "", - "seoIndex++;", - "pm.collectionVariables.set('seoIndex', seoIndex);", - "console.log('New seoIndex', seoIndex);", - "let nextRequest = null;", - "if (seoIndex < seos.length) {", - " console.log('Continuing with next SEO');", - " nextRequest = 'Create Embeddings';", - "} else {", - " console.log('Embeddings creation done');", - " pm.collectionVariables.set('seoIndex', null);", - " pm.collectionVariables.set('seoId', null);", - " pm.collectionVariables.set('seoContentTypeVar', null);", - " pm.collectionVariables.set('seoContentTypeId', null);", - " pm.collectionVariables.set('seoText', null);", - " nextRequest = 'Count Embeddings without prompt';", - "}", - "pm.execution.setNextRequest(nextRequest);", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]') || 0;", - "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", - "const currentSeo = seos[seoIndex];", - "if (currentSeo) {", - " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", - "}", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"query\": \"+contentType:{{seoContentTypeVar}}\",\n \"fields\": \"seo\",\n \"model\": \"text-embedding-ada-002\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/embeddings", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "embeddings" - ] - } - }, - "response": [] - }, - { - "name": "Count Embeddings without prompt", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 400', function () {", - " pm.response.to.have.status(400);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Error message is returned', function () {", - " pm.expect(jsonData.message).equals('query/prompt cannot be null');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prompt\": \"\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/embeddings/count", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "embeddings", - "count" - ] - } - }, - "response": [] - }, - { - "name": "Count Embeddings", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Embeddings are counted', function () {", - " pm.expect(jsonData.embeddingsCount, 'Embeddings count should be 2').equals(2);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seos = JSON.parse(pm.collectionVariables.get('seos'));", - "pm.collectionVariables.set('seoContentTypeVar', seos[0].seoContentTypeVar);", - "pm.collectionVariables.set('seoText', seos[0].text);" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"model\": \"text-embedding-ada-002\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/embeddings/count", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "embeddings", - "count" - ] - } - }, - "response": [] - }, - { - "name": "Index Count", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "const seos = JSON.parse(pm.collectionVariables.get('seos'));", - "", - "pm.test('Embeddings are counted by index', function () {", - " const defaultIndexCount = jsonData.indexCount['default'];", - " pm.expect(defaultIndexCount, 'Embeddings by index count should exist').not.undefined;", - " pm.expect(defaultIndexCount.contentTypes.split(',').length, 'Embeddings by index content types splitted by comma should be 4').equals(4);", - " pm.expect(defaultIndexCount.contents, 'Embeddings by index contents count should be 4').equals(4);", - " seos.forEach(seo => pm.expect(defaultIndexCount.contentTypes.includes(seo.seoContentTypeVar), 'Each seo content type should exist in response `contentTypes`').is.true);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{serverURL}}/api/v1/ai/embeddings/indexCount", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "embeddings", - "indexCount" - ] - } - }, - "response": [] - }, - { - "name": "Delete Embeddings", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Embeddings are deleted', function () {", - " pm.expect(jsonData.deleted, 'Number of embeddings deleted must be greater than zero').greaterThan(0);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seos = JSON.parse(pm.collectionVariables.get('seos'));", - "pm.collectionVariables.set('seoContentTypeVar', seos[0].seoContentTypeVar);", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"deleteQuery\": \"+contentType:{{seoContentTypeVar}}\",\n \"indexName\": \"default\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/embeddings", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "embeddings" - ] - } - }, - "response": [] - }, - { - "name": "Delete Embeddings without delete query", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Embeddings are deleted', function () {", - " pm.expect(jsonData.deleted, 'Number of embeddings deleted must be greater than zero').greaterThan(0);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seos = JSON.parse(pm.collectionVariables.get('seos'));", - "pm.collectionVariables.set('seoContentTypeVar', seos[3].seoContentTypeVar);", - "pm.collectionVariables.set('identifier', seos[3].contentlets[0].identifier);", - "pm.collectionVariables.set('inode', seos[3].contentlets[0].inode);", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "DELETE", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"indexName\": \"default\",\n \"identifier\": \"{{identifier}}\",\n \"inode\": \"{{inode}}\",\n \"contentType\": \"{{seoContentTypeVar}}\",\n \"language\": 1\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/embeddings", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "embeddings" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Search", - "item": [ - { - "name": "Test", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Type is returned', function () {", - " pm.expect(jsonData.type, 'Type is \"search\"').equals('search');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{serverURL}}/api/v1/ai/search/test", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "search", - "test" - ] - } - }, - "response": [] - }, - { - "name": "Search without query", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 400', function () {", - " pm.response.to.have.status(400);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Error message is returned', function () {", - " pm.expect(jsonData.message, 'Error message is included in response').equals('query/prompt cannot be null');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{serverURL}}/api/v1/ai/search", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "search" - ] - } - }, - "response": [] - }, - { - "name": "Search", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Search return results', function () {", - " const seoText = pm.collectionVariables.get('seoText');", - " const seoContentTypeId = pm.collectionVariables.get('seoContentTypeId');", - " const seoContentTypeVar = pm.collectionVariables.get('seoContentTypeVar');", - " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", - " pm.expect(jsonData.total, 'Total must be more than zero').greaterThan(0);", - " pm.expect(jsonData.query, 'Query must be kept').equals(seoText);", - " pm.expect(jsonData.operator, 'Operator must be kept').equals('<=>');", - " pm.expect(jsonData.threshold, 'Threshold must be kept').equals(0.25);", - " pm.expect(jsonData.dotCMSResults, 'DotCMS results must present').not.undefined;", - " pm.expect(jsonData.dotCMSResults.length, 'DotCMS results must not be empty').greaterThan(0);", - " const matchedResult = jsonData.dotCMSResults.filter(result => result.matches.find(match => match.distance === 0))[0];", - " pm.expect(matchedResult, 'There should be at least one match with zero distance').not.undefined;", - " pm.expect(matchedResult.stInode, 'Result must have the same content type id').equals(seoContentTypeId);", - " pm.expect(matchedResult.contentType, 'Result must have the same content type var').equals(seoContentTypeVar);", - " pm.expect(matchedResult.seo, 'Result must have the same SEO').equals(seoText);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "pm.collectionVariables.set('seoIndex', null);", - "pm.collectionVariables.set('seoContentTypeVar', null);", - "pm.collectionVariables.set('seoContentTypeId', null);", - "pm.collectionVariables.set('seoText', null);", - "", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "const currentSeo = seos[1];", - "if (currentSeo) {", - " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", - " pm.collectionVariables.set('seoContentTypeId', currentSeo.seoContentTypeId);", - " pm.collectionVariables.set('seoText', currentSeo.text);", - "}", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prompt\": \"{{seoText}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/search", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "search" - ] - } - }, - "response": [] - }, - { - "name": "Search", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Search return results', function () {", - " const seoText = decodeURI(pm.collectionVariables.get('seoText'));", - " const seoContentTypeId = pm.collectionVariables.get('seoContentTypeId');", - " const seoContentTypeVar = pm.collectionVariables.get('seoContentTypeVar');", - " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", - " pm.expect(jsonData.total, 'Total must be more than zero').greaterThan(0);", - " pm.expect(jsonData.query, 'Query must be kept').equals(seoText);", - " pm.expect(jsonData.operator, 'Operator must be kept').equals('<=>');", - " pm.expect(jsonData.threshold, 'Threshold must be kept').equals(0.5);", - " pm.expect(jsonData.dotCMSResults, 'DotCMS results must present').not.undefined;", - " pm.expect(jsonData.dotCMSResults.length, 'DotCMS results must not be empty').greaterThan(0);", - " const matchedResult = jsonData.dotCMSResults.filter(result => result.matches.find(match => match.distance === 0))[0];", - " pm.expect(matchedResult, 'There should be at least one match with zero distance').not.undefined;", - " pm.expect(matchedResult.stInode, 'Result must have the same content type id').equals(seoContentTypeId);", - " pm.expect(matchedResult.contentType, 'Result must have the same content type var').equals(seoContentTypeVar);", - " pm.expect(matchedResult.seo, 'Result must have the same SEO').equals(seoText);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "const currentSeo = seos[2];", - "if (currentSeo) {", - " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", - " pm.collectionVariables.set('seoContentTypeId', currentSeo.seoContentTypeId);", - " pm.collectionVariables.set('seoText', encodeURI(currentSeo.text));", - "}", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{serverURL}}/api/v1/ai/search?query={{seoText}}", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "search" - ], - "query": [ - { - "key": "query", - "value": "{{seoText}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Search Related not found", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 404', function () {", - " pm.response.to.have.status(404);", - "});", - "", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Error message is returned', function () {", - " pm.expect(jsonData.message).equals('contentlet not found');", - "});" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"inode\": \"UNKNOWN\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/search/related", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "search", - "related" - ] - } - }, - "response": [] - }, - { - "name": "Search Related by inode without fieldVar", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 404', function () {", - " pm.response.to.have.status(404);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Error message is returned', function () {", - " pm.expect(jsonData.message).equals('content not found');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "pm.collectionVariables.set('seoIndex', null);", - "pm.collectionVariables.set('seoContentTypeVar', null);", - "pm.collectionVariables.set('seoContentTypeId', null);", - "pm.collectionVariables.set('seoText', null);", - "", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "const currentSeo = seos[1];", - "if (currentSeo) {", - " pm.collectionVariables.set('inode', currentSeo.contentlets[0].inode);", - " pm.collectionVariables.set('identifier', currentSeo.contentlets[0].identifier);", - "}", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"inode\": \"{{inode}}\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/search/related", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "search", - "related" - ] - } - }, - "response": [] - }, - { - "name": "Create Contentlet - Popular Novel", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code should be ok 200\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "", - "let seoIndex = 1;", - "console.log('seoIndex', seoIndex);", - "", - "const currentSeo = seos[seoIndex];", - "if (currentSeo) {", - " if (!currentSeo.contentlets) {", - " currentSeo.contentlets = [];", - " }", - "", - " const contentlet = {};", - " contentlet.identifier = jsonData.entity.identifier;", - " contentlet.inode = jsonData.entity.inode;", - " currentSeo.contentlets.push(contentlet);", - " console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", - " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", - "}", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"contentlet\": {\n \"title\": \"content_popular-novel\",\n \"languageId\": 1,\n \"stInode\": \"8c12f1be13fb43db771731910991e759\",\n \"seo\": \"J.K. Rowling's 'Harry Potter and the Sorcerer's Stone' follows the journey of a young boy, Harry Potter, who discovers he is a wizard on his eleventh birthday. He attends Hogwarts School of Witchcraft and Wizardry, where he makes friends, learns about his past, and uncovers the truth about his parents' mysterious deaths.\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/PUBLISH", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "workflow", - "actions", - "default", - "fire", - "PUBLISH" - ] - } - }, - "response": [] - }, - { - "name": "Search Related by inode", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Search return results', function () {", - " const seoText = pm.collectionVariables.get('seoText');", - " const seoContentTypeId = pm.collectionVariables.get('seoContentTypeId');", - " const seoContentTypeVar = pm.collectionVariables.get('seoContentTypeVar');", - " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", - " pm.expect(jsonData.total, 'Total must be more than zero').greaterThan(0);", - " pm.expect(jsonData.query, 'Query must be kept').equals(seoText);", - " pm.expect(jsonData.operator, 'Operator must be kept').equals('<=>');", - " pm.expect(jsonData.threshold, 'Threshold must be kept').equals(0.25);", - " pm.expect(jsonData.dotCMSResults, 'DotCMS results must present').not.undefined;", - " pm.expect(jsonData.dotCMSResults.length, 'DotCMS results must not be empty').greaterThan(0);", - " const matchedResult = jsonData.dotCMSResults.filter(result => result.matches.find(match => match.distance === 0))[0];", - " pm.expect(matchedResult, 'There should be at least one match with zero distance').not.undefined;", - " pm.expect(matchedResult.stInode, 'Result must have the same content type id').equals(seoContentTypeId);", - " pm.expect(matchedResult.contentType, 'Result must have the same content type var').equals(seoContentTypeVar);", - " pm.expect(matchedResult.seo, 'Result must have the same SEO').equals(seoText);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "pm.collectionVariables.set('seoIndex', null);", - "pm.collectionVariables.set('seoContentTypeVar', null);", - "pm.collectionVariables.set('seoContentTypeId', null);", - "pm.collectionVariables.set('seoText', null);", - "", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "const currentSeo = seos[1];", - "if (currentSeo) {", - " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", - " pm.collectionVariables.set('seoContentTypeId', currentSeo.seoContentTypeId);", - " pm.collectionVariables.set('seoText', currentSeo.text);", - " const contentlet = currentSeo.contentlets[1];", - " pm.collectionVariables.set('inode', contentlet.inode);", - " pm.collectionVariables.set('identifier', contentlet.identifier);", - "}", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"inode\": \"{{inode}}\",\n \"fieldVar\": \"seo\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/search/related", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "search", - "related" - ] - } - }, - "response": [] - }, - { - "name": "Search Related by identifier", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Search return results', function () {", - " const seoText = pm.collectionVariables.get('seoText');", - " const seoContentTypeId = pm.collectionVariables.get('seoContentTypeId');", - " const seoContentTypeVar = pm.collectionVariables.get('seoContentTypeVar');", - " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", - " pm.expect(jsonData.total, 'Total must be more than zero').greaterThan(0);", - " pm.expect(jsonData.query, 'Query must be kept').equals(seoText);", - " pm.expect(jsonData.operator, 'Operator must be kept').equals('<=>');", - " pm.expect(jsonData.threshold, 'Threshold must be kept').equals(0.25);", - " pm.expect(jsonData.dotCMSResults, 'DotCMS results must present').not.undefined;", - " pm.expect(jsonData.dotCMSResults.length, 'DotCMS results must not be empty').greaterThan(0);", - " const matchedResult = jsonData.dotCMSResults.filter(result => result.matches.find(match => match.distance === 0))[0];", - " pm.expect(matchedResult, 'There should be at least one match with zero distance').not.undefined;", - " pm.expect(matchedResult.stInode, 'Result must have the same content type id').equals(seoContentTypeId);", - " pm.expect(matchedResult.contentType, 'Result must have the same content type var').equals(seoContentTypeVar);", - " pm.expect(matchedResult.seo, 'Result must have the same SEO').equals(seoText);", - "});", - "", - "pm.collectionVariables.clear();" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "pm.collectionVariables.set('seoIndex', null);", - "pm.collectionVariables.set('seoContentTypeVar', null);", - "pm.collectionVariables.set('seoContentTypeId', null);", - "pm.collectionVariables.set('seoText', null);", - "", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "const currentSeo = seos[1];", - "if (currentSeo) {", - " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", - " pm.collectionVariables.set('seoContentTypeId', currentSeo.seoContentTypeId);", - " pm.collectionVariables.set('seoText', currentSeo.text);", - " const contentlet = currentSeo.contentlets[1];", - " pm.collectionVariables.set('inode', contentlet.inode);", - " pm.collectionVariables.set('identifier', contentlet.identifier);", - "}", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"identifier\": \"{{identifier}}\",\n \"fieldVar\": \"seo\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/search/related", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "search", - "related" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "Completions", - "item": [ - { - "name": "Create Content Type - Completions", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "const seoContentTypeId = jsonData.entity[0].id;", - "pm.collectionVariables.set('seoContentTypeId', seoContentTypeId);", - "const seoContentTypeVar = jsonData.entity[0].variable;", - "pm.collectionVariables.set('seoContentTypeVar', seoContentTypeVar);", - "", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", - "const currentSeo = seos[seoIndex];", - "if (currentSeo) {", - " currentSeo.seoContentTypeId = seoContentTypeId;", - " currentSeo.seoContentTypeVar = seoContentTypeVar;", - " pm.collectionVariables.set('seoText', currentSeo.text);", - " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", - "}", - "", - "console.log('seoIndex', seoIndex);", - "console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", - "", - "pm.execution.setNextRequest('Add Field to Content Type - Completions');", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", - "", - " if (seoIndex === 0) {", - " pm.collectionVariables.clear();", - " }", - " pm.collectionVariables.set('seoIndex', seoIndex);", - "", - "const initialSeos = [", - " {", - " id: 'streamming-music',", - " text: 'How has streaming changed the music industry and affected the way artists release and promote their music?'", - " },", - " {", - " id: 'emerging-technologies',", - " text: 'How are emerging technologies like blockchain and the Internet of Things (IoT) shaping the future of smart cities?'", - " },", - " {", - " id: 'sports-data-analytics',", - " text: 'How has data analytics transformed the strategies and performance of professional sports teams?'", - " },", - " {", - " id: 'medical-research',", - " text: 'What recent breakthroughs in medical research have the potential to significantly impact public health?'", - " }", - "];", - "", - "const collectionSeos = pm.collectionVariables.get('seos');", - "const seos = collectionSeos ? JSON.parse(collectionSeos) : initialSeos;", - "pm.collectionVariables.set('seoId', seos[seoIndex].id);", - "const seosJson = JSON.stringify(seos, null, 2);", - "pm.collectionVariables.set('seos', seosJson);", - "console.log('seos', seosJson);", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"defaultType\":false,\n \"icon\":null,\n \"fixed\":false,\n \"system\":false,\n \"clazz\":\"com.dotcms.contenttype.model.type.ImmutableSimpleContentType\",\n \"description\":\"\",\n \"host\":\"8a7d5e23-da1e-420a-b4f0-471e7da8ea2d\",\n \"folder\":\"SYSTEM_FOLDER\",\n \"name\":\"{{seoId}}-ContentType\",\n \"systemActionMappings\":{\"NEW\":\"\"},\n \"workflow\":[\"d61a59e1-a49c-46f2-a929-db2b4bfa88b2\"]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/contenttype", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "contenttype" - ] - } - }, - "response": [] - }, - { - "name": "Add Field to Content Type - Completions", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.execution.setNextRequest('Create Contentlet - Completions');" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "packages": {}, - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"layout\":[\n {\"divider\":{\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableRowField\",\n \"contentTypeId\":\"{{seoContentTypeId}}\",\n \"dataType\":\"SYSTEM\",\n \"fieldContentTypeProperties\":[],\n \"fieldType\":\"Row\",\n \"fieldTypeLabel\":\"Row\",\n \"fieldVariables\":[],\n \"fixed\":false,\n \"iDate\":1667572217000,\n \"indexed\":false,\n \"listed\":false,\n \"modDate\":1667572217000,\n \"name\":\"Row Field\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":-1,\n \"unique\":false},\n \"columns\":[\n {\n \"columnDivider\":{\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableColumnField\",\n \"contentTypeId\":\"{{seoContentTypeId}}\",\n \"dataType\":\"SYSTEM\",\n \"fieldContentTypeProperties\":[],\n \"fieldType\":\"Column\",\n \"fieldTypeLabel\":\"Column\",\n \"fieldVariables\":[],\n \"fixed\":false,\n \"iDate\":1667572217000,\n \"indexed\":false,\n \"listed\":false,\n \"modDate\":1667572217000,\n \"name\":\"Column Field\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":-1,\n \"unique\":false\n },\n \"fields\":[\n {\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableTextField\",\n \"name\":\"seo\",\n \"dataType\":\"TEXT\",\n \"regexCheck\":\"\",\n \"defaultValue\":\"\",\n \"hint\":\"\",\n \"required\":false,\n \"searchable\":false,\n \"indexed\":false,\n \"listed\":false,\n \"unique\":false,\n \"id\":null\n }\n ]\n }\n ]\n }\n ]\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v3/contenttype/{{seoContentTypeId}}/fields/move", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v3", - "contenttype", - "{{seoContentTypeId}}", - "fields", - "move" - ] - } - }, - "response": [] - }, - { - "name": "Create Contentlet - Completions", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code should be ok 200\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "", - "let seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", - "console.log('seoIndex', seoIndex);", - "", - "const currentSeo = seos[seoIndex];", - "if (currentSeo) {", - " if (!currentSeo.contentlets) {", - " currentSeo.contentlets = [];", - " }", - "", - " const contentlet = {};", - " contentlet.identifier = jsonData.entity.identifier;", - " contentlet.inode = jsonData.entity.inode;", - " currentSeo.contentlets.push(contentlet);", - " console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", - " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", - "}", - "", - "seoIndex++;", - "pm.collectionVariables.set('seoIndex', seoIndex);", - "console.log('New seoIndex', seoIndex);", - "let nextRequest = null;", - "if (seoIndex < seos.length) {", - " console.log('Continuing with next SEO');", - " nextRequest = 'Create Content Type - Completions'", - "} else {", - " console.log('SEO loading done');", - " pm.collectionVariables.set('seoIndex', null);", - " pm.collectionVariables.set('seoId', null);", - " pm.collectionVariables.set('seoContentTypeId', null);", - " pm.collectionVariables.set('seoContentTypeVar', null);", - " pm.collectionVariables.set('seoText', null);", - " nextRequest = 'Create Embeddings - Completions';", - "}", - "pm.execution.setNextRequest(nextRequest);", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "PUT", - "header": [], - "body": { - "mode": "raw", - "raw": "{ \n \"contentlet\" : {\n \"title\" : \"content_{{seoId}}\",\n \"languageId\" : 1,\n \"stInode\": \"{{seoContentTypeId}}\",\n \"seo\": \"{{seoText}}\"\n }\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/PUBLISH", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "workflow", - "actions", - "default", - "fire", - "PUBLISH" - ] - } - }, - "response": [] - }, - { - "name": "Create Embeddings - Completions", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 200', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", - "let seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", - "const currentSeo = seos[seoIndex];", - "if (currentSeo) {", - " if (!currentSeo.embedded) {", - " currentSeo.embedded = jsonData.totalToEmbed > 0;", - " }", - " ", - " console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", - " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", - "}", - "", - "seoIndex++;", - "pm.collectionVariables.set('seoIndex', seoIndex);", - "console.log('New seoIndex', seoIndex);", - "let nextRequest = null;", - "if (seoIndex < seos.length) {", - " console.log('Continuing with next SEO');", - " nextRequest = 'Create Embeddings - Completions';", - "} else {", - " console.log('Embeddings creation done');", - " pm.collectionVariables.set('seoIndex', null);", - " pm.collectionVariables.set('seoId', null);", - " pm.collectionVariables.set('seoContentTypeVar', null);", - " pm.collectionVariables.set('seoContentTypeId', null);", - " pm.collectionVariables.set('seoText', null);", - " nextRequest = 'Summarize from Content without prompt';", - "}", - "pm.execution.setNextRequest(nextRequest);", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]') || 0;", - "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", - "const currentSeo = seos[seoIndex];", - "if (currentSeo) {", - " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", - "}", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"query\": \"+contentType:{{seoContentTypeVar}}\",\n \"model\": \"text-embedding-ada-002\",\n \"fields\": \"seo\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/embeddings", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "embeddings" - ] - } - }, - "response": [] - }, - { - "name": "Summarize from Content without prompt", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 400', function () {", - " pm.response.to.have.status(400);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Error message is returned', function () {", - " pm.expect(jsonData.message).equals('query/prompt cannot be null');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/completions", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "completions" - ] - } - }, - "response": [] - }, - { - "name": "Summarize from Content", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 20', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test('Response contains expected data', function () {", - " pm.expect(jsonData).to.have.property('timeToEmbeddings');", - " pm.expect(jsonData).to.have.property('total');", - " pm.expect(jsonData).to.have.property('query');", - " pm.expect(jsonData).to.have.property('threshold');", - " pm.expect(jsonData).to.have.property('dotCMSResults');", - " pm.expect(jsonData).to.have.property('operator');", - " pm.expect(jsonData).to.have.property('offset');", - " pm.expect(jsonData).to.have.property('limit');", - " pm.expect(jsonData).to.have.property('count');", - " pm.expect(jsonData).to.have.property('openAiResponse');", - " pm.expect(jsonData).to.have.property('totalTime');", - "});", - "", - "pm.test('dotCMSResults has results', function () {", - " pm.expect(jsonData.dotCMSResults).to.be.an('array');", - " pm.expect(jsonData.dotCMSResults.length).greaterThan(0);", - " ", - " const result = jsonData.dotCMSResults.find(res => !!res.matches.find(match => match.distance === 0));", - " pm.expect(result).not.undefined;", - " pm.expect(result).to.have.property('hostName');", - " pm.expect(result).to.have.property('modDate');", - " pm.expect(result).to.have.property('publishDate');", - " pm.expect(result).to.have.property('title');", - " pm.expect(result).to.have.property('baseType');", - " pm.expect(result).to.have.property('inode');", - " pm.expect(result).to.have.property('archived');", - " pm.expect(result).to.have.property('ownerUserName');", - " pm.expect(result).to.have.property('host');", - " pm.expect(result).to.have.property('working');", - " pm.expect(result).to.have.property('seo');", - " pm.expect(result.seo).equals('How has streaming changed the music industry and affected the way artists release and promote their music?');", - " pm.expect(result).to.have.property('locked');", - " pm.expect(result).to.have.property('stInode');", - " pm.expect(result).to.have.property('contentType');", - " pm.expect(result).to.have.property('live');", - " pm.expect(result).to.have.property('owner');", - " pm.expect(result).to.have.property('identifier');", - " pm.expect(result).to.have.property('publishUserName');", - " pm.expect(result).to.have.property('publishUser');", - " pm.expect(result).to.have.property('languageId');", - " pm.expect(result).to.have.property('creationDate');", - " pm.expect(result).to.have.property('url');", - " pm.expect(result).to.have.property('titleImage');", - " pm.expect(result).to.have.property('modUserName');", - " pm.expect(result).to.have.property('hasLiveVersion');", - " pm.expect(result).to.have.property('folder');", - " pm.expect(result).to.have.property('hasTitleImage');", - " pm.expect(result).to.have.property('sortOrder');", - " pm.expect(result).to.have.property('modUser');", - " pm.expect(result).to.have.property('__icon__');", - " pm.expect(result).to.have.property('contentTypeIcon');", - " pm.expect(result).to.have.property('variant');", - " pm.expect(result).to.have.property('matches');", - "});", - "", - "// Check if openAiResponse contains specific keys", - "pm.test('openAiResponse contains specific keys', function () {", - " pm.expect(jsonData.openAiResponse).to.have.property('id');", - " pm.expect(jsonData.openAiResponse).to.have.property('object');", - " pm.expect(jsonData.openAiResponse).to.have.property('created');", - " pm.expect(jsonData.openAiResponse).to.have.property('model');", - " pm.expect(jsonData.openAiResponse).to.have.property('choices');", - " pm.expect(jsonData.openAiResponse).to.have.property('usage');", - " pm.expect(jsonData.openAiResponse).to.have.property('system_fingerprint');", - "});", - "", - "pm.test('Each item in openAiResponse.choices contains specific keys', function () {", - " pm.expect(jsonData.openAiResponse.choices).to.be.an('array');", - " jsonData.openAiResponse.choices.forEach(function(choice) {", - " pm.expect(choice).to.have.property('index');", - " pm.expect(choice).to.have.property('message');", - " pm.expect(choice).to.have.property('logprobs');", - " pm.expect(choice).to.have.property('finish_reason');", - " });", - "});", - "", - "pm.test('usage contains specific keys', function () {", - " pm.expect(jsonData.openAiResponse.usage).to.have.property('prompt_tokens');", - " pm.expect(jsonData.openAiResponse.usage).to.have.property('completion_tokens');", - " pm.expect(jsonData.openAiResponse.usage).to.have.property('total_tokens');", - "});", - "", - "pm.test('query matches expected value', function () {", - " pm.expect(jsonData.query).to.equal('How has streaming changed the music industry and affected the way artists release and promote their music?');", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seos = JSON.parse(pm.collectionVariables.get('seos'));", - "pm.collectionVariables.set('seoText', seos[0].text);", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/completions", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "completions" - ] - } - }, - "response": [] - }, - { - "name": "Summarize from Content Stream", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 20', function () {", - " pm.response.to.have.status(200);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seos = JSON.parse(pm.collectionVariables.get('seos'));", - "pm.collectionVariables.set('seoText', seos[1].text);", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1,\n \"stream\": true\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/completions", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "completions" - ] - } - }, - "response": [] - }, - { - "name": "Raw Prompt", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 20', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const jsonData = pm.response.json();", - "", - "pm.test(\"Response contains expected data\", function () {", - " pm.expect(jsonData).to.have.property(\"id\");", - " pm.expect(jsonData).to.have.property(\"object\");", - " pm.expect(jsonData).to.have.property(\"created\");", - " pm.expect(jsonData).to.have.property(\"model\");", - " pm.expect(jsonData).to.have.property(\"choices\");", - " pm.expect(jsonData).to.have.property(\"usage\");", - " pm.expect(jsonData).to.have.property(\"system_fingerprint\");", - " pm.expect(jsonData).to.have.property(\"totalTime\");", - " pm.expect(parseInt(jsonData.totalTime.replace(\"ms\", \"\"))).to.be.below(400000);", - " pm.expect(jsonData.model).to.equal(\"gpt-3.5-turbo-16k-0613\");", - " pm.expect(jsonData.choices[0].message.content).to.equal(\"Data\");", - "});", - "", - "pm.test(\"Each item in choices contains specific keys\", function () {", - " pm.expect(jsonData.choices).to.be.an(\"array\");", - " jsonData.choices.forEach(function(choice) {", - " pm.expect(choice).to.have.property(\"index\");", - " pm.expect(choice).to.have.property(\"message\");", - " pm.expect(choice).to.have.property(\"logprobs\");", - " pm.expect(choice).to.have.property(\"finish_reason\");", - " });", - "});", - "", - "pm.test(\"message contains role and content\", function () {", - " jsonData.choices.forEach(function(choice) {", - " pm.expect(choice.message).to.have.property(\"role\");", - " pm.expect(choice.message).to.have.property(\"content\");", - " });", - "});", - "", - "pm.test(\"usage contains specific keys\", function () {", - " pm.expect(jsonData.usage).to.have.property(\"prompt_tokens\");", - " pm.expect(jsonData.usage).to.have.property(\"completion_tokens\");", - " pm.expect(jsonData.usage).to.have.property(\"total_tokens\");", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seos = JSON.parse(pm.collectionVariables.get('seos'));", - "pm.collectionVariables.set('seoText', seos[2].text);", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/completions/rawPrompt", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "completions", - "rawPrompt" - ] - } - }, - "response": [] - }, - { - "name": "Raw Prompt Stream", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test('Status code should be ok 20', function () {", - " pm.response.to.have.status(200);", - "});", - "", - "pm.test(\"Response contains expected data\", function () {", - " pm.expect(pm.response.body).not.equals('')", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "const seos = JSON.parse(pm.collectionVariables.get('seos'));", - "pm.collectionVariables.set('seoText', seos[3].text);", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1,\n \"stream\": true\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/ai/completions/rawPrompt", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "completions", - "rawPrompt" - ] - } - }, - "response": [] - }, - { - "name": "Config", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "const jsonData = pm.response.json();", - "", - "pm.test(\"Response contains properties\", function () {", - " pm.expect(jsonData).to.have.property(\"apiImageUrl\");", - " pm.expect(jsonData).to.have.property(\"apiKey\");", - " pm.expect(jsonData).to.have.property(\"apiUrl\");", - " pm.expect(jsonData).to.have.property(\"availableModels\");", - " pm.expect(jsonData.availableModels).to.be.an(\"array\");", - " pm.expect(jsonData.availableModels.length).greaterThan(1);", - " pm.expect(jsonData.availableModels.find(model => model.type === 'TEXT')).is.not.undefined", - " pm.expect(jsonData.availableModels.find(model => model.type === 'IMAGE')).is.not.undefined", - " pm.expect(jsonData[\"com.dotcms.ai.completion.default.temperature\"]).to.equal(\"1\");", - " pm.expect(jsonData[\"com.dotcms.ai.debug.logging\"]).to.equal(\"false\");", - " pm.expect(jsonData.embeddingsModelNames).to.equal(\"text-embedding-ada-002\");", - " pm.expect(jsonData.imageModelNames).to.equal(\"dall-e-3\");", - " pm.expect(jsonData.textPrompt).to.include(\"Descriptive writing style\");", - " pm.expect(jsonData.rolePrompt).to.include(\"dotCMSbot\");", - " pm.expect(jsonData.apiImageUrl).to.match(/^https?:\\/\\/.+/);", - " pm.expect(jsonData.apiUrl).to.match(/^https?:\\/\\/.+/);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "url": { - "raw": "{{serverURL}}/api/v1/ai/completions/config", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "ai", - "completions", - "config" - ] - } - }, - "response": [] - } - ] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "packages": {}, - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "packages": {}, - "exec": [ - "" - ] - } - } - ] + "info": { + "_postman_id": "140d143a-c58a-471c-9e22-2754cf252c22", + "name": "AI", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "5403727" + }, + "item": [ + { + "name": "pre", + "item": [ + { + "name": "dotAI App", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotCMS.com", + "type": "string" + }, + { + "key": "saveHelperData", + "type": "any" + }, + { + "key": "showPassword", + "value": false, + "type": "boolean" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"providerConfig\": {\n \"value\": \"{\\\"chat\\\":{\\\"provider\\\":\\\"openai\\\",\\\"apiKey\\\":\\\"some-api-key-1a2bc3\\\",\\\"model\\\":\\\"gpt-4o-mini\\\",\\\"maxTokens\\\":16384,\\\"maxRetries\\\":0,\\\"endpoint\\\":\\\"http://wm:8080/\\\"},\\\"embeddings\\\":{\\\"provider\\\":\\\"openai\\\",\\\"apiKey\\\":\\\"some-api-key-1a2bc3\\\",\\\"model\\\":\\\"text-embedding-ada-002\\\",\\\"maxRetries\\\":0,\\\"endpoint\\\":\\\"http://wm:8080/\\\"},\\\"image\\\":{\\\"provider\\\":\\\"openai\\\",\\\"apiKey\\\":\\\"some-api-key-1a2bc3\\\",\\\"model\\\":\\\"dall-e-3\\\",\\\"size\\\":\\\"1024x1024\\\",\\\"maxRetries\\\":0,\\\"endpoint\\\":\\\"http://wm:8080/\\\"}}\"\n },\n \"listenerIndexer\": {\n \"value\": \"{\\\"default\\\":\\\"blog,dotcmsdocumentation,feature,ProductBriefs,news,report.file,builds,casestudy\\\",\\\"documentation\\\":\\\"dotcmsdocumentation\\\"}\"\n }\n}" + }, + "url": { + "raw": "{{serverURL}}/api/v1/apps/dotAI/SYSTEM_HOST", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "apps", + "dotAI", + "SYSTEM_HOST" + ] + }, + "description": "This tests the endpoint that brings back one specific App/integration given the App-key followed by the site-id" + }, + "response": [] + } + ] + }, + { + "name": "Generative", + "item": [ + { + "name": "Test", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Type is returned', function () {", + " pm.expect(jsonData.type, 'Type is \"image\"').equals('image');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{serverURL}}/api/v1/ai/image/test", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "image", + "test" + ] + } + }, + "response": [] + }, + { + "name": "Generate Text", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Text is generated', function () {", + " pm.expect(jsonData.choices, 'Choices are included in dotAI response').not.undefined;", + " pm.expect(jsonData.choices, 'Choices are included in dotAI respons are not empty').not.empty;", + " pm.expect(jsonData.choices[0].message.content).contains('The FIFA World Cup in 2018 was won by the French national football team')", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prompt\": \"Who won the FIFA World Cup in 2018?\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/text/generate", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "text", + "generate" + ] + } + }, + "response": [] + }, + { + "name": "Generate Text", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Text is generated', function () {", + " pm.expect(jsonData.choices, 'Choices are included in dotAI response').not.undefined;", + " pm.expect(jsonData.choices, 'Choices are included in dotAI respons are not empty').not.empty;", + " pm.expect(jsonData.choices[0].message.content).contains('The theory of relativity, developed by Albert Einstein, consists of')", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/ai/text/generate?prompt=What%20is%20the%20theory%20of%20relativity%3F", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "text", + "generate" + ], + "query": [ + { + "key": "prompt", + "value": "What%20is%20the%20theory%20of%20relativity%3F" + } + ] + } + }, + "response": [] + }, + { + "name": "Generate Text without prompt", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be 400', function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Error message is returned', function () {", + " pm.expect(jsonData.message, 'Error message is included in response').equals('query/prompt cannot be null');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/ai/text/generate", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "text", + "generate" + ] + } + }, + "response": [] + }, + { + "name": "Generate Image", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Image is generated', function () {", + " pm.expect(jsonData.revised_prompt, 'Field revised_prompt is included in dotAI response').not.null;", + " pm.expect(jsonData.url, 'Field url is included in dotAI response').not.null;", + " pm.expect(jsonData.tempFileName, 'Field tempFileName is included in dotAI response').not.null;", + " pm.expect(jsonData.tempFile, 'Field tempFile is included in dotAI response').not.null;", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prompt\": \"Generate image of a turtle training for a marathon\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/image/generate", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "image", + "generate" + ] + } + }, + "response": [] + }, + { + "name": "Generate Image", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Image is generated', function () {", + " pm.expect(jsonData.revised_prompt, 'Field revised_prompt is included in dotAI response').not.null;", + " pm.expect(jsonData.url, 'Field url is included in dotAI response').not.null;", + " pm.expect(jsonData.tempFileName, 'Field tempFileName is included in dotAI response').not.null;", + " pm.expect(jsonData.tempFile, 'Field tempFile is included in dotAI response').not.null;", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{serverURL}}/api/v1/ai/image/generate?prompt=Image%20of%20a%20robot%20painting%20the%20sixteen%20chapel", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "image", + "generate" + ], + "query": [ + { + "key": "prompt", + "value": "Image%20of%20a%20robot%20painting%20the%20sixteen%20chapel" + } + ] + } + }, + "response": [] + }, + { + "name": "Generate Image without prompt", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be 400', function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Error message is returned', function () {", + " pm.expect(jsonData.error, 'Error message is included in response').equals('`prompt` is required');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{serverURL}}/api/v1/ai/image/generate", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "image", + "generate" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Embeddings", + "item": [ + { + "name": "Delete DB", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('DB is reset', function () {", + " pm.expect(jsonData.created, 'DB is deleted and created').equals(true);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{serverURL}}/api/v1/ai/embeddings/db", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "embeddings", + "db" + ] + } + }, + "response": [] + }, + { + "name": "Test", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Type is returned', function () {", + " pm.expect(jsonData.type, 'Type is \"embeddings\"').equals('embeddings');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{serverURL}}/api/v1/ai/embeddings/test", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "embeddings", + "test" + ] + } + }, + "response": [] + }, + { + "name": "Create Content Type", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "const seoContentTypeId = jsonData.entity[0].id;", + "pm.collectionVariables.set('seoContentTypeId', seoContentTypeId);", + "const seoContentTypeVar = jsonData.entity[0].variable;", + "pm.collectionVariables.set('seoContentTypeVar', seoContentTypeVar);", + "", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", + "const currentSeo = seos[seoIndex];", + "if (currentSeo) {", + " currentSeo.seoContentTypeId = seoContentTypeId;", + " currentSeo.seoContentTypeVar = seoContentTypeVar;", + " pm.collectionVariables.set('seoText', currentSeo.text);", + " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", + "}", + "", + "console.log('seoIndex', seoIndex);", + "console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", + "", + "pm.execution.setNextRequest('Add Field to Content Type');", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", + "", + " if (seoIndex === 0) {", + " pm.collectionVariables.clear();", + " }", + " pm.collectionVariables.set('seoIndex', seoIndex);", + "", + "const initialSeos = [", + " {", + " id: 'stock-market',", + " text: 'The stock market has shown significant volatility over the past few months. Analysts attribute this to geopolitical tensions and economic uncertainty. Investors are advised to diversify their portfolios to mitigate risks.'", + " },", + " {", + " id: 'popular-novel',", + " text: 'J.K. Rowling\\'s \\'Harry Potter and the Sorcerer\\'s Stone\\' follows the journey of a young boy, Harry Potter, who discovers he is a wizard on his eleventh birthday. He attends Hogwarts School of Witchcraft and Wizardry, where he makes friends, learns about his past, and uncovers the truth about his parents\\' mysterious deaths.'", + " },", + " {", + " id: 'historical-event',", + " text: 'The signing of the Declaration of Independence on July 4, 1776, marked the Thirteen Colonies\\' formal separation from Great Britain. This historic document, primarily authored by Thomas Jefferson, outlined the colonies\\' grievances against the British crown and asserted their right to self-governance.'", + " },", + " {", + " id: 'mental-health',", + " text: 'Mental health awareness has become increasingly important in recent years. Experts emphasize the need for regular mental health check-ups and advocate for reducing the stigma associated with mental illnesses.'", + " }", + "];", + "", + "const collectionSeos = pm.collectionVariables.get('seos');", + "const seos = collectionSeos ? JSON.parse(collectionSeos) : initialSeos;", + "pm.collectionVariables.set('seoId', seos[seoIndex].id);", + "const seosJson = JSON.stringify(seos, null, 2);", + "pm.collectionVariables.set('seos', seosJson);", + "console.log('seos', seosJson);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"defaultType\":false,\n \"icon\":null,\n \"fixed\":false,\n \"system\":false,\n \"clazz\":\"com.dotcms.contenttype.model.type.ImmutableSimpleContentType\",\n \"description\":\"\",\n \"host\":\"8a7d5e23-da1e-420a-b4f0-471e7da8ea2d\",\n \"folder\":\"SYSTEM_FOLDER\",\n \"name\":\"{{seoId}}-ContentType\",\n \"systemActionMappings\":{\"NEW\":\"\"},\n \"workflow\":[\"d61a59e1-a49c-46f2-a929-db2b4bfa88b2\"]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/contenttype", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "contenttype" + ] + } + }, + "response": [] + }, + { + "name": "Add Field to Content Type", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.execution.setNextRequest('Create Contentlet');" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"layout\":[\n {\"divider\":{\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableRowField\",\n \"contentTypeId\":\"{{seoContentTypeId}}\",\n \"dataType\":\"SYSTEM\",\n \"fieldContentTypeProperties\":[],\n \"fieldType\":\"Row\",\n \"fieldTypeLabel\":\"Row\",\n \"fieldVariables\":[],\n \"fixed\":false,\n \"iDate\":1667572217000,\n \"indexed\":false,\n \"listed\":false,\n \"modDate\":1667572217000,\n \"name\":\"Row Field\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":-1,\n \"unique\":false},\n \"columns\":[\n {\n \"columnDivider\":{\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableColumnField\",\n \"contentTypeId\":\"{{seoContentTypeId}}\",\n \"dataType\":\"SYSTEM\",\n \"fieldContentTypeProperties\":[],\n \"fieldType\":\"Column\",\n \"fieldTypeLabel\":\"Column\",\n \"fieldVariables\":[],\n \"fixed\":false,\n \"iDate\":1667572217000,\n \"indexed\":false,\n \"listed\":false,\n \"modDate\":1667572217000,\n \"name\":\"Column Field\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":-1,\n \"unique\":false\n },\n \"fields\":[\n {\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableTextField\",\n \"name\":\"seo\",\n \"dataType\":\"TEXT\",\n \"regexCheck\":\"\",\n \"defaultValue\":\"\",\n \"hint\":\"\",\n \"required\":false,\n \"searchable\":false,\n \"indexed\":false,\n \"listed\":false,\n \"unique\":false,\n \"id\":null\n }\n ]\n }\n ]\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v3/contenttype/{{seoContentTypeId}}/fields/move", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v3", + "contenttype", + "{{seoContentTypeId}}", + "fields", + "move" + ] + } + }, + "response": [] + }, + { + "name": "Create Contentlet", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code should be ok 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "", + "let seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", + "console.log('seoIndex', seoIndex);", + "", + "const currentSeo = seos[seoIndex];", + "if (currentSeo) {", + " if (!currentSeo.contentlets) {", + " currentSeo.contentlets = [];", + " }", + "", + " const contentlet = {};", + " contentlet.identifier = jsonData.entity.identifier;", + " contentlet.inode = jsonData.entity.inode;", + " currentSeo.contentlets.push(contentlet);", + " console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", + " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", + "}", + "", + "seoIndex++;", + "pm.collectionVariables.set('seoIndex', seoIndex);", + "console.log('New seoIndex', seoIndex);", + "let nextRequest = null;", + "if (seoIndex < seos.length) {", + " console.log('Continuing with next SEO');", + " nextRequest = 'Create Content Type'", + "} else {", + " console.log('SEO loading done');", + " pm.collectionVariables.set('seoIndex', null);", + " pm.collectionVariables.set('seoId', null);", + " pm.collectionVariables.set('seoContentTypeId', null);", + " pm.collectionVariables.set('seoContentTypeVar', null);", + " pm.collectionVariables.set('seoText', null);", + " nextRequest = 'Create Embeddings without query';", + "}", + "pm.execution.setNextRequest(nextRequest);", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \n \"contentlet\" : {\n \"title\" : \"content_{{seoId}}\",\n \"languageId\" : 1,\n \"stInode\": \"{{seoContentTypeId}}\",\n \"seo\": \"{{seoText}}\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/PUBLISH", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "workflow", + "actions", + "default", + "fire", + "PUBLISH" + ] + } + }, + "response": [] + }, + { + "name": "Create Embeddings without query", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 400', function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Error message is returned', function () {", + " pm.expect(jsonData.message, 'Error message is included in response').equals('query cannot be null');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "packages": {}, + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/embeddings", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "embeddings" + ] + } + }, + "response": [] + }, + { + "name": "Create Embeddings", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "let seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", + "const currentSeo = seos[seoIndex];", + "if (currentSeo) {", + " if (!currentSeo.embedded) {", + " currentSeo.embedded = jsonData.totalToEmbed > 0;", + " }", + " ", + " console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", + " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", + "}", + "", + "pm.test('Emebeddings are created', function () {", + " pm.expect(jsonData.indexName, 'Index name should be \"default\"').equals('default');", + " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", + "});", + "", + "if (currentSeo.embedded) {", + " pm.expect(jsonData.totalToEmbed, 'Total to embed is greater than zero').greaterThan(0);", + "} else {", + " pm.expect(jsonData.totalToEmbed, 'Total to embed is zero').equals(0);", + "}", + "", + "seoIndex++;", + "pm.collectionVariables.set('seoIndex', seoIndex);", + "console.log('New seoIndex', seoIndex);", + "let nextRequest = null;", + "if (seoIndex < seos.length) {", + " console.log('Continuing with next SEO');", + " nextRequest = 'Create Embeddings';", + "} else {", + " console.log('Embeddings creation done');", + " pm.collectionVariables.set('seoIndex', null);", + " pm.collectionVariables.set('seoId', null);", + " pm.collectionVariables.set('seoContentTypeVar', null);", + " pm.collectionVariables.set('seoContentTypeId', null);", + " pm.collectionVariables.set('seoText', null);", + " nextRequest = 'Count Embeddings without prompt';", + "}", + "pm.execution.setNextRequest(nextRequest);", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]') || 0;", + "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", + "const currentSeo = seos[seoIndex];", + "if (currentSeo) {", + " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"+contentType:{{seoContentTypeVar}}\",\n \"fields\": \"seo\",\n \"model\": \"text-embedding-ada-002\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/embeddings", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "embeddings" + ] + } + }, + "response": [] + }, + { + "name": "Count Embeddings without prompt", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 400', function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Error message is returned', function () {", + " pm.expect(jsonData.message).equals('query/prompt cannot be null');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prompt\": \"\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/embeddings/count", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "embeddings", + "count" + ] + } + }, + "response": [] + }, + { + "name": "Count Embeddings", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Embeddings are counted', function () {", + " pm.expect(jsonData.embeddingsCount, 'Embeddings count should be greater than 0').greaterThan(0);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seos = JSON.parse(pm.collectionVariables.get('seos'));", + "pm.collectionVariables.set('seoContentTypeVar', seos[0].seoContentTypeVar);", + "pm.collectionVariables.set('seoText', seos[0].text);" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"model\": \"text-embedding-ada-002\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/embeddings/count", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "embeddings", + "count" + ] + } + }, + "response": [] + }, + { + "name": "Index Count", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "const seos = JSON.parse(pm.collectionVariables.get('seos'));", + "", + "pm.test('Embeddings are counted by index', function () {", + " const defaultIndexCount = jsonData.indexCount['default'];", + " pm.expect(defaultIndexCount, 'Embeddings by index count should exist').not.undefined;", + " pm.expect(defaultIndexCount.contentTypes.split(',').length, 'Embeddings by index content types splitted by comma should be 4').equals(4);", + " pm.expect(defaultIndexCount.contents, 'Embeddings by index contents count should be 4').equals(4);", + " seos.forEach(seo => pm.expect(defaultIndexCount.contentTypes.includes(seo.seoContentTypeVar), 'Each seo content type should exist in response `contentTypes`').is.true);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{serverURL}}/api/v1/ai/embeddings/indexCount", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "embeddings", + "indexCount" + ] + } + }, + "response": [] + }, + { + "name": "Delete Embeddings", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Embeddings are deleted', function () {", + " pm.expect(jsonData.deleted, 'Number of embeddings deleted must be greater than zero').greaterThan(0);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seos = JSON.parse(pm.collectionVariables.get('seos'));", + "pm.collectionVariables.set('seoContentTypeVar', seos[0].seoContentTypeVar);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"deleteQuery\": \"+contentType:{{seoContentTypeVar}}\",\n \"indexName\": \"default\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/embeddings", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "embeddings" + ] + } + }, + "response": [] + }, + { + "name": "Delete Embeddings without delete query", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Embeddings are deleted', function () {", + " pm.expect(jsonData.deleted, 'Number of embeddings deleted must be greater than zero').greaterThan(0);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seos = JSON.parse(pm.collectionVariables.get('seos'));", + "pm.collectionVariables.set('seoContentTypeVar', seos[3].seoContentTypeVar);", + "pm.collectionVariables.set('identifier', seos[3].contentlets[0].identifier);", + "pm.collectionVariables.set('inode', seos[3].contentlets[0].inode);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"indexName\": \"default\",\n \"identifier\": \"{{identifier}}\",\n \"inode\": \"{{inode}}\",\n \"contentType\": \"{{seoContentTypeVar}}\",\n \"language\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/embeddings", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "embeddings" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Search", + "item": [ + { + "name": "Test", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Type is returned', function () {", + " pm.expect(jsonData.type, 'Type is \"search\"').equals('search');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{serverURL}}/api/v1/ai/search/test", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "search", + "test" + ] + } + }, + "response": [] + }, + { + "name": "Search without query", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 400', function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Error message is returned', function () {", + " pm.expect(jsonData.message, 'Error message is included in response').equals('query/prompt cannot be null');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{serverURL}}/api/v1/ai/search", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "search" + ] + } + }, + "response": [] + }, + { + "name": "Search", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Search return results', function () {", + " const seoText = pm.collectionVariables.get('seoText');", + " const seoContentTypeId = pm.collectionVariables.get('seoContentTypeId');", + " const seoContentTypeVar = pm.collectionVariables.get('seoContentTypeVar');", + " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", + " pm.expect(jsonData.total, 'Total must be more than zero').greaterThan(0);", + " pm.expect(jsonData.query, 'Query must be kept').equals(seoText);", + " pm.expect(jsonData.operator, 'Operator must be kept').equals('<=>');", + " pm.expect(jsonData.threshold, 'Threshold must be kept').equals(0.25);", + " pm.expect(jsonData.dotCMSResults, 'DotCMS results must present').not.undefined;", + " pm.expect(jsonData.dotCMSResults.length, 'DotCMS results must not be empty').greaterThan(0);", + " const matchedResult = jsonData.dotCMSResults.filter(result => result.matches.find(match => match.distance === 0))[0];", + " pm.expect(matchedResult, 'There should be at least one match with zero distance').not.undefined;", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set('seoIndex', null);", + "pm.collectionVariables.set('seoContentTypeVar', null);", + "pm.collectionVariables.set('seoContentTypeId', null);", + "pm.collectionVariables.set('seoText', null);", + "", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "const currentSeo = seos[1];", + "if (currentSeo) {", + " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", + " pm.collectionVariables.set('seoContentTypeId', currentSeo.seoContentTypeId);", + " pm.collectionVariables.set('seoText', currentSeo.text);", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prompt\": \"{{seoText}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/search", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "search" + ] + } + }, + "response": [] + }, + { + "name": "Search", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Search return results', function () {", + " const seoText = decodeURI(pm.collectionVariables.get('seoText'));", + " const seoContentTypeId = pm.collectionVariables.get('seoContentTypeId');", + " const seoContentTypeVar = pm.collectionVariables.get('seoContentTypeVar');", + " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", + " pm.expect(jsonData.total, 'Total must be more than zero').greaterThan(0);", + " pm.expect(jsonData.query, 'Query must be kept').equals(seoText);", + " pm.expect(jsonData.operator, 'Operator must be kept').equals('<=>');", + " pm.expect(jsonData.threshold, 'Threshold must be kept').equals(0.5);", + " pm.expect(jsonData.dotCMSResults, 'DotCMS results must present').not.undefined;", + " pm.expect(jsonData.dotCMSResults.length, 'DotCMS results must not be empty').greaterThan(0);", + " const matchedResult = jsonData.dotCMSResults.filter(result => result.matches.find(match => match.distance === 0))[0];", + " pm.expect(matchedResult, 'There should be at least one match with zero distance').not.undefined;", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "const currentSeo = seos[2];", + "if (currentSeo) {", + " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", + " pm.collectionVariables.set('seoContentTypeId', currentSeo.seoContentTypeId);", + " pm.collectionVariables.set('seoText', encodeURI(currentSeo.text));", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{serverURL}}/api/v1/ai/search?query={{seoText}}", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "search" + ], + "query": [ + { + "key": "query", + "value": "{{seoText}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Search Related not found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 404', function () {", + " pm.response.to.have.status(404);", + "});", + "", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Error message is returned', function () {", + " pm.expect(jsonData.message).equals('contentlet not found');", + "});" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"inode\": \"UNKNOWN\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/search/related", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "search", + "related" + ] + } + }, + "response": [] + }, + { + "name": "Search Related by inode without fieldVar", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 404', function () {", + " pm.response.to.have.status(404);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Error message is returned', function () {", + " pm.expect(jsonData.message).equals('content not found');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set('seoIndex', null);", + "pm.collectionVariables.set('seoContentTypeVar', null);", + "pm.collectionVariables.set('seoContentTypeId', null);", + "pm.collectionVariables.set('seoText', null);", + "", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "const currentSeo = seos[1];", + "if (currentSeo) {", + " pm.collectionVariables.set('inode', currentSeo.contentlets[0].inode);", + " pm.collectionVariables.set('identifier', currentSeo.contentlets[0].identifier);", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"inode\": \"{{inode}}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/search/related", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "search", + "related" + ] + } + }, + "response": [] + }, + { + "name": "Create Contentlet - Popular Novel", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code should be ok 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "", + "let seoIndex = 1;", + "console.log('seoIndex', seoIndex);", + "", + "const currentSeo = seos[seoIndex];", + "if (currentSeo) {", + " if (!currentSeo.contentlets) {", + " currentSeo.contentlets = [];", + " }", + "", + " const contentlet = {};", + " contentlet.identifier = jsonData.entity.identifier;", + " contentlet.inode = jsonData.entity.inode;", + " currentSeo.contentlets.push(contentlet);", + " console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", + " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"contentlet\": {\n \"title\": \"content_popular-novel\",\n \"languageId\": 1,\n \"stInode\": \"8c12f1be13fb43db771731910991e759\",\n \"seo\": \"J.K. Rowling's 'Harry Potter and the Sorcerer's Stone' follows the journey of a young boy, Harry Potter, who discovers he is a wizard on his eleventh birthday. He attends Hogwarts School of Witchcraft and Wizardry, where he makes friends, learns about his past, and uncovers the truth about his parents' mysterious deaths.\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/PUBLISH", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "workflow", + "actions", + "default", + "fire", + "PUBLISH" + ] + } + }, + "response": [] + }, + { + "name": "Search Related by inode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Search return results', function () {", + " const seoText = pm.collectionVariables.get('seoText');", + " const seoContentTypeId = pm.collectionVariables.get('seoContentTypeId');", + " const seoContentTypeVar = pm.collectionVariables.get('seoContentTypeVar');", + " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", + " pm.expect(jsonData.total, 'Total must be more than zero').greaterThan(0);", + " pm.expect(jsonData.query, 'Query must be kept').equals(seoText);", + " pm.expect(jsonData.operator, 'Operator must be kept').equals('<=>');", + " pm.expect(jsonData.threshold, 'Threshold must be kept').equals(0.25);", + " pm.expect(jsonData.dotCMSResults, 'DotCMS results must present').not.undefined;", + " pm.expect(jsonData.dotCMSResults.length, 'DotCMS results must not be empty').greaterThan(0);", + " const matchedResult = jsonData.dotCMSResults.filter(result => result.matches.find(match => match.distance === 0))[0];", + " pm.expect(matchedResult, 'There should be at least one match with zero distance').not.undefined;", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set('seoIndex', null);", + "pm.collectionVariables.set('seoContentTypeVar', null);", + "pm.collectionVariables.set('seoContentTypeId', null);", + "pm.collectionVariables.set('seoText', null);", + "", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "const currentSeo = seos[1];", + "if (currentSeo) {", + " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", + " pm.collectionVariables.set('seoContentTypeId', currentSeo.seoContentTypeId);", + " pm.collectionVariables.set('seoText', currentSeo.text);", + " const contentlet = currentSeo.contentlets[1];", + " pm.collectionVariables.set('inode', contentlet.inode);", + " pm.collectionVariables.set('identifier', contentlet.identifier);", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"inode\": \"{{inode}}\",\n \"fieldVar\": \"seo\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/search/related", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "search", + "related" + ] + } + }, + "response": [] + }, + { + "name": "Search Related by identifier", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Search return results', function () {", + " const seoText = pm.collectionVariables.get('seoText');", + " const seoContentTypeId = pm.collectionVariables.get('seoContentTypeId');", + " const seoContentTypeVar = pm.collectionVariables.get('seoContentTypeVar');", + " pm.expect(parseInt(jsonData.timeToEmbeddings.split('ms')[0]), 'Time to embeddings must be greater than zero').greaterThan(0);", + " pm.expect(jsonData.total, 'Total must be more than zero').greaterThan(0);", + " pm.expect(jsonData.query, 'Query must be kept').equals(seoText);", + " pm.expect(jsonData.operator, 'Operator must be kept').equals('<=>');", + " pm.expect(jsonData.threshold, 'Threshold must be kept').equals(0.25);", + " pm.expect(jsonData.dotCMSResults, 'DotCMS results must present').not.undefined;", + " pm.expect(jsonData.dotCMSResults.length, 'DotCMS results must not be empty').greaterThan(0);", + " const matchedResult = jsonData.dotCMSResults.filter(result => result.matches.find(match => match.distance === 0))[0];", + " pm.expect(matchedResult, 'There should be at least one match with zero distance').not.undefined;", + "});", + "", + "pm.collectionVariables.clear();" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set('seoIndex', null);", + "pm.collectionVariables.set('seoContentTypeVar', null);", + "pm.collectionVariables.set('seoContentTypeId', null);", + "pm.collectionVariables.set('seoText', null);", + "", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "const currentSeo = seos[1];", + "if (currentSeo) {", + " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", + " pm.collectionVariables.set('seoContentTypeId', currentSeo.seoContentTypeId);", + " pm.collectionVariables.set('seoText', currentSeo.text);", + " const contentlet = currentSeo.contentlets[1];", + " pm.collectionVariables.set('inode', contentlet.inode);", + " pm.collectionVariables.set('identifier', contentlet.identifier);", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"identifier\": \"{{identifier}}\",\n \"fieldVar\": \"seo\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/search/related", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "search", + "related" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Completions", + "item": [ + { + "name": "Create Content Type - Completions", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "const seoContentTypeId = jsonData.entity[0].id;", + "pm.collectionVariables.set('seoContentTypeId', seoContentTypeId);", + "const seoContentTypeVar = jsonData.entity[0].variable;", + "pm.collectionVariables.set('seoContentTypeVar', seoContentTypeVar);", + "", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", + "const currentSeo = seos[seoIndex];", + "if (currentSeo) {", + " currentSeo.seoContentTypeId = seoContentTypeId;", + " currentSeo.seoContentTypeVar = seoContentTypeVar;", + " pm.collectionVariables.set('seoText', currentSeo.text);", + " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", + "}", + "", + "console.log('seoIndex', seoIndex);", + "console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", + "", + "pm.execution.setNextRequest('Add Field to Content Type - Completions');", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", + "", + " if (seoIndex === 0) {", + " pm.collectionVariables.clear();", + " }", + " pm.collectionVariables.set('seoIndex', seoIndex);", + "", + "const initialSeos = [", + " {", + " id: 'streamming-music',", + " text: 'How has streaming changed the music industry and affected the way artists release and promote their music?'", + " },", + " {", + " id: 'emerging-technologies',", + " text: 'How are emerging technologies like blockchain and the Internet of Things (IoT) shaping the future of smart cities?'", + " },", + " {", + " id: 'sports-data-analytics',", + " text: 'How has data analytics transformed the strategies and performance of professional sports teams?'", + " },", + " {", + " id: 'medical-research',", + " text: 'What recent breakthroughs in medical research have the potential to significantly impact public health?'", + " }", + "];", + "", + "const collectionSeos = pm.collectionVariables.get('seos');", + "const seos = collectionSeos ? JSON.parse(collectionSeos) : initialSeos;", + "pm.collectionVariables.set('seoId', seos[seoIndex].id);", + "const seosJson = JSON.stringify(seos, null, 2);", + "pm.collectionVariables.set('seos', seosJson);", + "console.log('seos', seosJson);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"defaultType\":false,\n \"icon\":null,\n \"fixed\":false,\n \"system\":false,\n \"clazz\":\"com.dotcms.contenttype.model.type.ImmutableSimpleContentType\",\n \"description\":\"\",\n \"host\":\"8a7d5e23-da1e-420a-b4f0-471e7da8ea2d\",\n \"folder\":\"SYSTEM_FOLDER\",\n \"name\":\"{{seoId}}-ContentType\",\n \"systemActionMappings\":{\"NEW\":\"\"},\n \"workflow\":[\"d61a59e1-a49c-46f2-a929-db2b4bfa88b2\"]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/contenttype", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "contenttype" + ] + } + }, + "response": [] + }, + { + "name": "Add Field to Content Type - Completions", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.execution.setNextRequest('Create Contentlet - Completions');" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "packages": {}, + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"layout\":[\n {\"divider\":{\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableRowField\",\n \"contentTypeId\":\"{{seoContentTypeId}}\",\n \"dataType\":\"SYSTEM\",\n \"fieldContentTypeProperties\":[],\n \"fieldType\":\"Row\",\n \"fieldTypeLabel\":\"Row\",\n \"fieldVariables\":[],\n \"fixed\":false,\n \"iDate\":1667572217000,\n \"indexed\":false,\n \"listed\":false,\n \"modDate\":1667572217000,\n \"name\":\"Row Field\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":-1,\n \"unique\":false},\n \"columns\":[\n {\n \"columnDivider\":{\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableColumnField\",\n \"contentTypeId\":\"{{seoContentTypeId}}\",\n \"dataType\":\"SYSTEM\",\n \"fieldContentTypeProperties\":[],\n \"fieldType\":\"Column\",\n \"fieldTypeLabel\":\"Column\",\n \"fieldVariables\":[],\n \"fixed\":false,\n \"iDate\":1667572217000,\n \"indexed\":false,\n \"listed\":false,\n \"modDate\":1667572217000,\n \"name\":\"Column Field\",\n \"readOnly\":false,\n \"required\":false,\n \"searchable\":false,\n \"sortOrder\":-1,\n \"unique\":false\n },\n \"fields\":[\n {\n \"clazz\":\"com.dotcms.contenttype.model.field.ImmutableTextField\",\n \"name\":\"seo\",\n \"dataType\":\"TEXT\",\n \"regexCheck\":\"\",\n \"defaultValue\":\"\",\n \"hint\":\"\",\n \"required\":false,\n \"searchable\":false,\n \"indexed\":false,\n \"listed\":false,\n \"unique\":false,\n \"id\":null\n }\n ]\n }\n ]\n }\n ]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v3/contenttype/{{seoContentTypeId}}/fields/move", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v3", + "contenttype", + "{{seoContentTypeId}}", + "fields", + "move" + ] + } + }, + "response": [] + }, + { + "name": "Create Contentlet - Completions", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code should be ok 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "", + "let seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", + "console.log('seoIndex', seoIndex);", + "", + "const currentSeo = seos[seoIndex];", + "if (currentSeo) {", + " if (!currentSeo.contentlets) {", + " currentSeo.contentlets = [];", + " }", + "", + " const contentlet = {};", + " contentlet.identifier = jsonData.entity.identifier;", + " contentlet.inode = jsonData.entity.inode;", + " currentSeo.contentlets.push(contentlet);", + " console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", + " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", + "}", + "", + "seoIndex++;", + "pm.collectionVariables.set('seoIndex', seoIndex);", + "console.log('New seoIndex', seoIndex);", + "let nextRequest = null;", + "if (seoIndex < seos.length) {", + " console.log('Continuing with next SEO');", + " nextRequest = 'Create Content Type - Completions'", + "} else {", + " console.log('SEO loading done');", + " pm.collectionVariables.set('seoIndex', null);", + " pm.collectionVariables.set('seoId', null);", + " pm.collectionVariables.set('seoContentTypeId', null);", + " pm.collectionVariables.set('seoContentTypeVar', null);", + " pm.collectionVariables.set('seoText', null);", + " nextRequest = 'Create Embeddings - Completions';", + "}", + "pm.execution.setNextRequest(nextRequest);", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{ \n \"contentlet\" : {\n \"title\" : \"content_{{seoId}}\",\n \"languageId\" : 1,\n \"stInode\": \"{{seoContentTypeId}}\",\n \"seo\": \"{{seoText}}\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/PUBLISH", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "workflow", + "actions", + "default", + "fire", + "PUBLISH" + ] + } + }, + "response": [] + }, + { + "name": "Create Embeddings - Completions", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]');", + "let seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", + "const currentSeo = seos[seoIndex];", + "if (currentSeo) {", + " if (!currentSeo.embedded) {", + " currentSeo.embedded = jsonData.totalToEmbed > 0;", + " }", + " ", + " console.log('currentSeo', JSON.stringify(currentSeo, null, 2));", + " pm.collectionVariables.set('seos', JSON.stringify(seos, null, 2));", + "}", + "", + "seoIndex++;", + "pm.collectionVariables.set('seoIndex', seoIndex);", + "console.log('New seoIndex', seoIndex);", + "let nextRequest = null;", + "if (seoIndex < seos.length) {", + " console.log('Continuing with next SEO');", + " nextRequest = 'Create Embeddings - Completions';", + "} else {", + " console.log('Embeddings creation done');", + " pm.collectionVariables.set('seoIndex', null);", + " pm.collectionVariables.set('seoId', null);", + " pm.collectionVariables.set('seoContentTypeVar', null);", + " pm.collectionVariables.set('seoContentTypeId', null);", + " pm.collectionVariables.set('seoText', null);", + " nextRequest = 'Summarize from Content without prompt';", + "}", + "pm.execution.setNextRequest(nextRequest);", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seos = JSON.parse(pm.collectionVariables.get('seos') || '[]') || 0;", + "const seoIndex = parseInt(pm.collectionVariables.get('seoIndex')) || 0;", + "const currentSeo = seos[seoIndex];", + "if (currentSeo) {", + " pm.collectionVariables.set('seoContentTypeVar', currentSeo.seoContentTypeVar);", + "}", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"query\": \"+contentType:{{seoContentTypeVar}}\",\n \"model\": \"text-embedding-ada-002\",\n \"fields\": \"seo\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/embeddings", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "embeddings" + ] + } + }, + "response": [] + }, + { + "name": "Summarize from Content without prompt", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 400', function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Error message is returned', function () {", + " pm.expect(jsonData.message).equals('query/prompt cannot be null');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/completions", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "completions" + ] + } + }, + "response": [] + }, + { + "name": "Summarize from Content", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 20', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test('Response contains expected data', function () {", + " pm.expect(jsonData).to.have.property('timeToEmbeddings');", + " pm.expect(jsonData).to.have.property('total');", + " pm.expect(jsonData).to.have.property('query');", + " pm.expect(jsonData).to.have.property('threshold');", + " pm.expect(jsonData).to.have.property('dotCMSResults');", + " pm.expect(jsonData).to.have.property('operator');", + " pm.expect(jsonData).to.have.property('offset');", + " pm.expect(jsonData).to.have.property('limit');", + " pm.expect(jsonData).to.have.property('count');", + " pm.expect(jsonData).to.have.property('openAiResponse');", + " pm.expect(jsonData).to.have.property('totalTime');", + "});", + "", + "pm.test('dotCMSResults has results', function () {", + " pm.expect(jsonData.dotCMSResults).to.be.an('array');", + " pm.expect(jsonData.dotCMSResults.length).greaterThan(0);", + " ", + " const result = jsonData.dotCMSResults.find(res => !!res.matches.find(match => match.distance === 0));", + " pm.expect(result).not.undefined;", + " pm.expect(result).to.have.property('hostName');", + " pm.expect(result).to.have.property('modDate');", + " pm.expect(result).to.have.property('publishDate');", + " pm.expect(result).to.have.property('title');", + " pm.expect(result).to.have.property('baseType');", + " pm.expect(result).to.have.property('inode');", + " pm.expect(result).to.have.property('archived');", + " pm.expect(result).to.have.property('ownerUserName');", + " pm.expect(result).to.have.property('host');", + " pm.expect(result).to.have.property('working');", + " pm.expect(result).to.have.property('seo');", + " pm.expect(result).to.have.property('locked');", + " pm.expect(result).to.have.property('stInode');", + " pm.expect(result).to.have.property('contentType');", + " pm.expect(result).to.have.property('live');", + " pm.expect(result).to.have.property('owner');", + " pm.expect(result).to.have.property('identifier');", + " pm.expect(result).to.have.property('publishUserName');", + " pm.expect(result).to.have.property('publishUser');", + " pm.expect(result).to.have.property('languageId');", + " pm.expect(result).to.have.property('creationDate');", + " pm.expect(result).to.have.property('url');", + " pm.expect(result).to.have.property('titleImage');", + " pm.expect(result).to.have.property('modUserName');", + " pm.expect(result).to.have.property('hasLiveVersion');", + " pm.expect(result).to.have.property('folder');", + " pm.expect(result).to.have.property('hasTitleImage');", + " pm.expect(result).to.have.property('sortOrder');", + " pm.expect(result).to.have.property('modUser');", + " pm.expect(result).to.have.property('__icon__');", + " pm.expect(result).to.have.property('contentTypeIcon');", + " pm.expect(result).to.have.property('variant');", + " pm.expect(result).to.have.property('matches');", + "});", + "", + "// Check if openAiResponse contains specific keys", + "pm.test('openAiResponse contains specific keys', function () {", + " pm.expect(jsonData.openAiResponse).to.have.property('id');", + " pm.expect(jsonData.openAiResponse).to.have.property('object');", + " pm.expect(jsonData.openAiResponse).to.have.property('created');", + " pm.expect(jsonData.openAiResponse).to.have.property('model');", + " pm.expect(jsonData.openAiResponse).to.have.property('choices');", + " pm.expect(jsonData.openAiResponse).to.have.property('usage');", + " pm.expect(jsonData.openAiResponse).to.have.property('system_fingerprint');", + "});", + "", + "pm.test('Each item in openAiResponse.choices contains specific keys', function () {", + " pm.expect(jsonData.openAiResponse.choices).to.be.an('array');", + " jsonData.openAiResponse.choices.forEach(function(choice) {", + " pm.expect(choice).to.have.property('index');", + " pm.expect(choice).to.have.property('message');", + " pm.expect(choice).to.have.property('logprobs');", + " pm.expect(choice).to.have.property('finish_reason');", + " });", + "});", + "", + "pm.test('usage contains specific keys', function () {", + " pm.expect(jsonData.openAiResponse.usage).to.have.property('prompt_tokens');", + " pm.expect(jsonData.openAiResponse.usage).to.have.property('completion_tokens');", + " pm.expect(jsonData.openAiResponse.usage).to.have.property('total_tokens');", + "});", + "", + "pm.test('query matches expected value', function () {", + " pm.expect(jsonData.query).to.equal('How has streaming changed the music industry and affected the way artists release and promote their music?');", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seos = JSON.parse(pm.collectionVariables.get('seos'));", + "pm.collectionVariables.set('seoText', seos[0].text);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/completions", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "completions" + ] + } + }, + "response": [] + }, + { + "name": "Summarize from Content Stream", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 20', function () {", + " pm.response.to.have.status(200);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seos = JSON.parse(pm.collectionVariables.get('seos'));", + "pm.collectionVariables.set('seoText', seos[1].text);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1,\n \"stream\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/completions", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "completions" + ] + } + }, + "response": [] + }, + { + "name": "Raw Prompt", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 20', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const jsonData = pm.response.json();", + "", + "pm.test(\"Response contains expected data\", function () {", + " pm.expect(jsonData).to.have.property(\"id\");", + " pm.expect(jsonData).to.have.property(\"object\");", + " pm.expect(jsonData).to.have.property(\"created\");", + " pm.expect(jsonData).to.have.property(\"model\");", + " pm.expect(jsonData).to.have.property(\"choices\");", + " pm.expect(jsonData).to.have.property(\"usage\");", + " pm.expect(jsonData).to.have.property(\"system_fingerprint\");", + " pm.expect(jsonData).to.have.property(\"totalTime\");", + " pm.expect(parseInt(jsonData.totalTime.replace(\"ms\", \"\"))).to.be.below(400000);", + " pm.expect(jsonData.model).to.equal(\"gpt-3.5-turbo-16k-0613\");", + " pm.expect(jsonData.choices[0].message.content).to.equal(\"Data\");", + "});", + "", + "pm.test(\"Each item in choices contains specific keys\", function () {", + " pm.expect(jsonData.choices).to.be.an(\"array\");", + " jsonData.choices.forEach(function(choice) {", + " pm.expect(choice).to.have.property(\"index\");", + " pm.expect(choice).to.have.property(\"message\");", + " pm.expect(choice).to.have.property(\"logprobs\");", + " pm.expect(choice).to.have.property(\"finish_reason\");", + " });", + "});", + "", + "pm.test(\"message contains role and content\", function () {", + " jsonData.choices.forEach(function(choice) {", + " pm.expect(choice.message).to.have.property(\"role\");", + " pm.expect(choice.message).to.have.property(\"content\");", + " });", + "});", + "", + "pm.test(\"usage contains specific keys\", function () {", + " pm.expect(jsonData.usage).to.have.property(\"prompt_tokens\");", + " pm.expect(jsonData.usage).to.have.property(\"completion_tokens\");", + " pm.expect(jsonData.usage).to.have.property(\"total_tokens\");", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seos = JSON.parse(pm.collectionVariables.get('seos'));", + "pm.collectionVariables.set('seoText', seos[2].text);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/completions/rawPrompt", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "completions", + "rawPrompt" + ] + } + }, + "response": [] + }, + { + "name": "Raw Prompt Stream", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code should be ok 20', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Response contains expected data\", function () {", + " pm.expect(pm.response.body).not.equals('')", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const seos = JSON.parse(pm.collectionVariables.get('seos'));", + "pm.collectionVariables.set('seoText', seos[3].text);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"prompt\": \"{{seoText}}\",\n \"responseLengthTokens\": 1,\n \"stream\": true\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/ai/completions/rawPrompt", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "completions", + "rawPrompt" + ] + } + }, + "response": [] + }, + { + "name": "Config", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = pm.response.json();", + "", + "pm.test(\"Response contains properties\", function () {", + " pm.expect(jsonData).to.have.property(\"providerConfig\");", + " pm.expect(jsonData).to.have.property(\"rolePrompt\");", + " pm.expect(jsonData).to.have.property(\"textPrompt\");", + " pm.expect(jsonData).to.have.property(\"imagePrompt\");", + " pm.expect(jsonData).to.have.property(\"imageSize\");", + " pm.expect(jsonData).to.have.property(\"listenerIndexer\");", + " pm.expect(jsonData[\"com.dotcms.ai.debug.logging\"]).to.equal(\"false\");", + " pm.expect(jsonData.textPrompt).to.include(\"Descriptive writing style\");", + " pm.expect(jsonData.rolePrompt).to.include(\"dotCMSbot\");", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "admin", + "type": "string" + }, + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{serverURL}}/api/v1/ai/completions/config", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "ai", + "completions", + "config" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ] } \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/apollo-space-program.json b/dotcms-postman/src/test/resources/mappings/apollo-space-program.json index 74c682e0f7e4..be637099cdb8 100644 --- a/dotcms-postman/src/test/resources/mappings/apollo-space-program.json +++ b/dotcms-postman/src/test/resources/mappings/apollo-space-program.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"What are the major achievements of the Apollo space program.*" + "matches": ".*\"content\" : \"What are the major achievements of the Apollo space program.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "The Apollo space program, conducted by NASA, achieved several major milestones in space exploration. Its most significant achievement was the successful landing of humans on the Moon. Apollo 11, in 1969, saw astronauts Neil Armstrong and Buzz Aldrin become the first humans to set foot on the lunar surface. The program also provided extensive scientific data and technological advancements.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "The Apollo space program, conducted by NASA, achieved several major milestones in space exploration. Its most significant achievement was the successful landing of humans on the Moon. Apollo 11, in 1969, saw astronauts Neil Armstrong and Buzz Aldrin become the first humans to set foot on the lunar surface. The program also provided extensive scientific data and technological advancements." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/blockchain.json b/dotcms-postman/src/test/resources/mappings/blockchain.json index 8cf818ce2337..9f6d785f0473 100644 --- a/dotcms-postman/src/test/resources/mappings/blockchain.json +++ b/dotcms-postman/src/test/resources/mappings/blockchain.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"How does blockchain technology work.*" + "matches": ".*\"content\" : \"How does blockchain technology work.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "Blockchain technology works as a decentralized ledger that records transactions across many computers. Each block in the chain contains a number of transactions, and once a block is added to the chain, it cannot be altered. This ensures security and transparency, as the entire network verifies and validates transactions, preventing fraud and double-spending.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "Blockchain technology works as a decentralized ledger that records transactions across many computers. Each block in the chain contains a number of transactions, and once a block is added to the chain, it cannot be altered. This ensures security and transparency, as the entire network verifies and validates transactions, preventing fraud and double-spending." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/crispr.json b/dotcms-postman/src/test/resources/mappings/crispr.json index ac6876e996d0..9c71bb11fe5d 100644 --- a/dotcms-postman/src/test/resources/mappings/crispr.json +++ b/dotcms-postman/src/test/resources/mappings/crispr.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"What is CRISPR, and why is it important.*" + "matches": ".*\"content\" : \"What is CRISPR, and why is it important.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "CRISPR (Clustered Regularly Interspaced Short Palindromic Repeats) is a revolutionary gene-editing technology. It allows scientists to precisely alter DNA sequences and modify gene function. CRISPR is important for its potential applications in medicine, agriculture, and biology, including curing genetic diseases, creating disease-resistant crops, and advancing biological research.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "CRISPR (Clustered Regularly Interspaced Short Palindromic Repeats) is a revolutionary gene-editing technology. It allows scientists to precisely alter DNA sequences and modify gene function. CRISPR is important for its potential applications in medicine, agriculture, and biology, including curing genetic diseases, creating disease-resistant crops, and advancing biological research." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/emerging-technologies-embedding.json b/dotcms-postman/src/test/resources/mappings/emerging-technologies-embedding.json index 6cf9f1eba24f..b3270087cdf3 100644 --- a/dotcms-postman/src/test/resources/mappings/emerging-technologies-embedding.json +++ b/dotcms-postman/src/test/resources/mappings/emerging-technologies-embedding.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/e", "headers": { "Content-Type": { "equalTo": "application/json" @@ -10,11 +9,7 @@ "equalTo": "Bearer some-api-key-1a2bc3" } }, - "bodyPatterns": [ - { - "matches": ".*\"model\":\"text-embedding-ada-002\",\"input\":.*4438,527,24084,14645,1093,18428.*" - } - ] + "urlPathPattern": "/embeddings" }, "response": { "status": 200, @@ -327,7 +322,7 @@ 0.0029473086, 0.015312759, 0.013107245, - 0.000009915345, + 9.915345e-06, -0.03979198, -0.022664472, 0.012676739, @@ -592,7 +587,7 @@ 0.0043779123, 0.027393414, -0.01249129, - 0.000009262083, + 9.262083e-06, 0.0075040464, -0.013226462, -0.034202028, @@ -699,7 +694,7 @@ -0.013372172, -0.015842613, -0.042706173, - 0.000045663623, + 4.5663623e-05, -0.04238826, 0.029777752, 0.010861992, @@ -1042,7 +1037,7 @@ 0.0052720397, 0.014584211, -0.010530833, - -0.00004863887, + -4.863887e-05, -0.011398468, 0.01236545, -0.019988714, diff --git a/dotcms-postman/src/test/resources/mappings/emerging-technologies-stream.json b/dotcms-postman/src/test/resources/mappings/emerging-technologies-stream.json new file mode 100644 index 000000000000..2920bb17d634 --- /dev/null +++ b/dotcms-postman/src/test/resources/mappings/emerging-technologies-stream.json @@ -0,0 +1,27 @@ +{ + "priority": 1, + "request": { + "method": "POST", + "headers": { + "Content-Type": { + "equalTo": "application/json" + }, + "Authorization": { + "equalTo": "Bearer some-api-key-1a2bc3" + } + }, + "bodyPatterns": [ + { + "matches": ".*\"content\" : \".*How are emerging technologies like blockchain and the Internet of Things \\(IoT\\) shaping the future of smart cities.*" + }, + { + "matches": ".*\"stream\" : true.*" + } + ], + "urlPathPattern": "/chat/completions" + }, + "response": { + "status": 200, + "body": "data: {\"id\":\"chatcmpl-9eX3XcEpp42K4ekYmyG2QvaXHBlts\",\"object\":\"chat.completion.chunk\",\"created\":1719447215,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-9eX3XcEpp42K4ekYmyG2QvaXHBlts\",\"object\":\"chat.completion.chunk\",\"created\":1719447215,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-9eX3XcEpp42K4ekYmyG2QvaXHBlts\",\"object\":\"chat.completion.chunk\",\"created\":1719447215,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"length\"}]}\n\ndata: [DONE]\n\n" + } +} diff --git a/dotcms-postman/src/test/resources/mappings/emerging-technologies.json b/dotcms-postman/src/test/resources/mappings/emerging-technologies.json index c0c12261f26c..7e24f899dc2e 100644 --- a/dotcms-postman/src/test/resources/mappings/emerging-technologies.json +++ b/dotcms-postman/src/test/resources/mappings/emerging-technologies.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,12 +11,35 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\".*How are emerging technologies like blockchain and the Internet of Things (IoT) shaping the future of smart cities.*" + "matches": ".*\"content\" : \".*How are emerging technologies like blockchain and the Internet of Things \\(IoT\\) shaping the future of smart cities.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, - "body": "data: {\"id\":\"chatcmpl-9eX3XcEpp42K4ekYmyG2QvaXHBlts\",\"object\":\"chat.completion.chunk\",\"created\":1719447215,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\n data: {\"id\":\"chatcmpl-9eX3XcEpp42K4ekYmyG2QvaXHBlts\",\"object\":\"chat.completion.chunk\",\"created\":1719447215,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"The\"},\"logprobs\":null,\"finish_reason\":null}]}\n\n data: {\"id\":\"chatcmpl-9eX3XcEpp42K4ekYmyG2QvaXHBlts\",\"object\":\"chat.completion.chunk\",\"created\":1719447215,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"length\"}]}\n\n data: [DONE]" + "jsonBody": { + "id": "chatcmpl-9eX3XcEpp42K4ekYmyG2QvaXHBlts", + "object": "chat.completion", + "created": 1719447215, + "model": "gpt-3.5-turbo-16k-0613", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The" + }, + "logprobs": null, + "finish_reason": "length" + } + ], + "usage": { + "prompt_tokens": 116, + "completion_tokens": 1, + "total_tokens": 117 + }, + "system_fingerprint": null + } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/fifa-2018-winner.json b/dotcms-postman/src/test/resources/mappings/fifa-2018-winner.json index ff48b3440e5b..478fb3d3cf71 100644 --- a/dotcms-postman/src/test/resources/mappings/fifa-2018-winner.json +++ b/dotcms-postman/src/test/resources/mappings/fifa-2018-winner.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"Who won the FIFA World Cup in 2018.*" + "matches": ".*\"content\" : \"Who won the FIFA World Cup in 2018.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "The FIFA World Cup in 2018 was won by the French national football team. They defeated Croatia 4-2 in the final match held in Moscow, Russia.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "The FIFA World Cup in 2018 was won by the French national football team. They defeated Croatia 4-2 in the final match held in Moscow, Russia." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/harlem-renaissance-figures.json b/dotcms-postman/src/test/resources/mappings/harlem-renaissance-figures.json index dd448aafda0f..6ce1c12015a8 100644 --- a/dotcms-postman/src/test/resources/mappings/harlem-renaissance-figures.json +++ b/dotcms-postman/src/test/resources/mappings/harlem-renaissance-figures.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"Who are some influential figures in the Harlem Renaissance.*" + "matches": ".*\"content\" : \"Who are some influential figures in the Harlem Renaissance.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "The Harlem Renaissance was a cultural movement in the early 20th century that celebrated African American culture through literature, art, and music. Influential figures include Langston Hughes, Zora Neale Hurston, Claude McKay, Duke Ellington, and Louis Armstrong. Their work helped to reshape the cultural and artistic landscape of the United States.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "The Harlem Renaissance was a cultural movement in the early 20th century that celebrated African American culture through literature, art, and music. Influential figures include Langston Hughes, Zora Neale Hurston, Claude McKay, Duke Ellington, and Louis Armstrong. Their work helped to reshape the cultural and artistic landscape of the United States." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/historical-event.json b/dotcms-postman/src/test/resources/mappings/historical-event.json index 596f851a2e87..8581e6090bd7 100644 --- a/dotcms-postman/src/test/resources/mappings/historical-event.json +++ b/dotcms-postman/src/test/resources/mappings/historical-event.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/e", "headers": { "Content-Type": { "equalTo": "application/json" @@ -10,11 +9,7 @@ "equalTo": "Bearer some-api-key-1a2bc3" } }, - "bodyPatterns": [ - { - "matches": ".*\"model\":\"text-embedding-ada-002\",\"input\":.*791,16351,315,279,42021,315.*" - } - ] + "urlPathPattern": "/embeddings" }, "response": { "status": 200, @@ -55,7 +50,7 @@ -0.037107702, 0.023718579, 0.015053587, - 0.000099917415, + 9.9917415e-05, -0.0025334086, -0.0042039896, 0.0004061715, @@ -284,11 +279,11 @@ 0.00010269025, 0.010213185, -0.020463087, - 0.000057464465, + 5.7464465e-05, 0.0021723672, 0.0052014426, 0.005568603, - -0.00007921682, + -7.921682e-05, -0.005908227, 0.016069397, -0.017807292, @@ -686,7 +681,7 @@ -0.0034788472, -0.016558945, -0.0047975658, - -0.0000013311367, + -1.3311367e-06, -0.026827205, -0.016485514, -0.018602807, @@ -752,7 +747,7 @@ -0.019826675, 0.01594701, -0.01931265, - -0.000012836281, + -1.2836281e-05, -0.00052129163, -0.025774678, 0.021613523, @@ -865,7 +860,7 @@ 0.018896535, -0.012948533, 0.015273883, - -0.00005464383, + -5.464383e-05, 0.0039928723, 0.01017647, 0.0010402885, @@ -950,7 +945,7 @@ 0.0024752747, -0.0042315265, -0.00550741, - 0.000023724411, + 2.3724411e-05, -0.017207596, -0.033705346, -0.00029583205, @@ -1103,7 +1098,7 @@ -0.0388456, -0.010470198, 0.0053421874, - -0.000045584333, + -4.5584333e-05, -0.0116389925, -0.010409005, 0.04019185, @@ -1194,7 +1189,7 @@ 0.0018709895, 0.023473805, 0.020891441, - 0.000036979007, + 3.6979007e-05, -0.007496197, -0.032236706, -0.02518722, @@ -1431,7 +1426,7 @@ -0.022543665, 0.02053652, 0.0054921117, - 0.000090834015, + 9.0834015e-05, -0.043887936, 0.04210109, -0.005216741, diff --git a/dotcms-postman/src/test/resources/mappings/industrial-revolution-impact.json b/dotcms-postman/src/test/resources/mappings/industrial-revolution-impact.json index 6abe338838ba..c31b30bc1a40 100644 --- a/dotcms-postman/src/test/resources/mappings/industrial-revolution-impact.json +++ b/dotcms-postman/src/test/resources/mappings/industrial-revolution-impact.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"What was the impact of the Industrial Revolution on society.*" + "matches": ".*\"content\" : \"What was the impact of the Industrial Revolution on society.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "The Industrial Revolution, which began in the late 18th century, had a profound impact on society. It led to the rise of factories, urbanization, and significant technological advancements. While it resulted in increased production and economic growth, it also brought about social challenges such as poor working conditions, child labor, and environmental pollution.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "The Industrial Revolution, which began in the late 18th century, had a profound impact on society. It led to the rise of factories, urbanization, and significant technological advancements. While it resulted in increased production and economic growth, it also brought about social challenges such as poor working conditions, child labor, and environmental pollution." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/medical-research-stream.json b/dotcms-postman/src/test/resources/mappings/medical-research-stream.json new file mode 100644 index 000000000000..f89225e9d6be --- /dev/null +++ b/dotcms-postman/src/test/resources/mappings/medical-research-stream.json @@ -0,0 +1,27 @@ +{ + "priority": 1, + "request": { + "method": "POST", + "headers": { + "Content-Type": { + "equalTo": "application/json" + }, + "Authorization": { + "equalTo": "Bearer some-api-key-1a2bc3" + } + }, + "bodyPatterns": [ + { + "matches": ".*\"content\" : \".*What recent breakthroughs in medical research have the potential to significantly impact public health.*" + }, + { + "matches": ".*\"stream\" : true.*" + } + ], + "urlPathPattern": "/chat/completions" + }, + "response": { + "status": 200, + "body": "data: {\"id\":\"chatcmpl-9eXVV4FiRbkQG6WtGUWYRKc6wsxvY\",\"object\":\"chat.completion.chunk\",\"created\":1719448949,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-9eXVV4FiRbkQG6WtGUWYRKc6wsxvY\",\"object\":\"chat.completion.chunk\",\"created\":1719448949,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"There\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-9eXVV4FiRbkQG6WtGUWYRKc6wsxvY\",\"object\":\"chat.completion.chunk\",\"created\":1719448949,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"length\"}]}\n\ndata: [DONE]\n\n" + } +} diff --git a/dotcms-postman/src/test/resources/mappings/medical-research.json b/dotcms-postman/src/test/resources/mappings/medical-research.json index 760e400e1178..bab07acc8073 100644 --- a/dotcms-postman/src/test/resources/mappings/medical-research.json +++ b/dotcms-postman/src/test/resources/mappings/medical-research.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,12 +11,35 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\".*What recent breakthroughs in medical research have the potential to significantly impact public health.*" + "matches": ".*\"content\" : \".*What recent breakthroughs in medical research have the potential to significantly impact public health.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, - "body": "data: {\"id\":\"chatcmpl-9eXVV4FiRbkQG6WtGUWYRKc6wsxvY\",\"object\":\"chat.completion.chunk\",\"created\":1719448949,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-9eXVV4FiRbkQG6WtGUWYRKc6wsxvY\",\"object\":\"chat.completion.chunk\",\"created\":1719448949,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"content\":\"There\"},\"logprobs\":null,\"finish_reason\":null}]}\n\ndata: {\"id\":\"chatcmpl-9eXVV4FiRbkQG6WtGUWYRKc6wsxvY\",\"object\":\"chat.completion.chunk\",\"created\":1719448949,\"model\":\"gpt-3.5-turbo-16k-0613\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"length\"}]}\n\ndata: [DONE]\n\n" + "jsonBody": { + "id": "chatcmpl-9eXVV4FiRbkQG6WtGUWYRKc6wsxvY", + "object": "chat.completion", + "created": 1719448949, + "model": "gpt-3.5-turbo-16k-0613", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "There" + }, + "logprobs": null, + "finish_reason": "length" + } + ], + "usage": { + "prompt_tokens": 116, + "completion_tokens": 1, + "total_tokens": 117 + }, + "system_fingerprint": null + } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/mental-health.json b/dotcms-postman/src/test/resources/mappings/mental-health.json index e00bdcb38a46..b76a03832c52 100644 --- a/dotcms-postman/src/test/resources/mappings/mental-health.json +++ b/dotcms-postman/src/test/resources/mappings/mental-health.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/e", "headers": { "Content-Type": { "equalTo": "application/json" @@ -10,11 +9,7 @@ "equalTo": "Bearer some-api-key-1a2bc3" } }, - "bodyPatterns": [ - { - "matches": ".*\"model\":\"text-embedding-ada-002\",\"input\":.*44,6430,2890,17985,706,3719.*" - } - ] + "urlPathPattern": "/embeddings" }, "response": { "status": 200, @@ -198,7 +193,7 @@ 0.03053473, 0.029221142, -0.0370531, - 0.000019556664, + 1.9556664e-05, -0.007001673, 0.001672966, -0.006530764, @@ -397,7 +392,7 @@ 0.02116612, 0.0049104653, -0.0012554991, - 0.00006481775, + 6.481775e-05, -0.011469112, 0.0002182214, -0.014375116, @@ -615,7 +610,7 @@ -0.001054898, 0.033533677, -0.019864924, - -0.00004550313, + -4.550313e-05, -0.0007493494, 0.013358944, -0.0017628106, @@ -655,7 +650,7 @@ -0.034376357, 0.02773406, -0.02223186, - -0.000042985936, + -4.2985936e-05, 0.0048918766, 0.019356837, -0.0013585105, @@ -850,7 +845,7 @@ 0.011388562, -0.029468989, 0.0071937544, - -0.00003395792, + -3.395792e-05, -0.035169464, -0.0147344945, 0.0059576184, @@ -1031,7 +1026,7 @@ -0.0036123677, -0.05038726, -0.015800236, - 0.000071498005, + 7.1498005e-05, 0.033360183, -0.0057934197, 0.01098581, @@ -1232,7 +1227,7 @@ -0.011766529, 0.00030167607, -0.0016636718, - -0.000061380815, + -6.1380815e-05, -0.0165066, -0.012596815, 0.028502386, @@ -1258,7 +1253,7 @@ -0.022008799, 0.020620856, 0.014982341, - -0.000041412688, + -4.1412688e-05, 0.029518558, 0.029022863, 0.0013724518, @@ -1507,7 +1502,7 @@ -0.023471095, -0.03541731, -0.0010068777, - 0.000049036884, + 4.9036884e-05, -0.02254167, -0.014387509, 0.017163392, diff --git a/dotcms-postman/src/test/resources/mappings/mlb-home-runs-record.json b/dotcms-postman/src/test/resources/mappings/mlb-home-runs-record.json index 1ff8c55246ee..e12f0e715d78 100644 --- a/dotcms-postman/src/test/resources/mappings/mlb-home-runs-record.json +++ b/dotcms-postman/src/test/resources/mappings/mlb-home-runs-record.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"Who holds the record for the most home runs in a single MLB season.*" + "matches": ".*\"content\" : \"Who holds the record for the most home runs in a single MLB season.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "The record for the most home runs in a single Major League Baseball (MLB) season is held by Barry Bonds, who hit 73 home runs in 2001 while playing for the San Francisco Giants.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "The record for the most home runs in a single Major League Baseball (MLB) season is held by Barry Bonds, who hit 73 home runs in 2001 while playing for the San Francisco Giants." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/popular-novel.json b/dotcms-postman/src/test/resources/mappings/popular-novel.json index 827d5b2277fa..f3817bb04efb 100644 --- a/dotcms-postman/src/test/resources/mappings/popular-novel.json +++ b/dotcms-postman/src/test/resources/mappings/popular-novel.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/e", "headers": { "Content-Type": { "equalTo": "application/json" @@ -10,11 +9,7 @@ "equalTo": "Bearer some-api-key-1a2bc3" } }, - "bodyPatterns": [ - { - "matches": ".*\"model\":\"text-embedding-ada-002\",\"input\":.*41,11606,13,96607,596,364.*" - } - ] + "urlPathPattern": "/embeddings" }, "response": { "status": 200, @@ -114,7 +109,7 @@ 0.0048608696, 0.027956298, -0.016156726, - 0.0000060013167, + 6.0013167e-06, -0.019594595, 0.009362841, -0.009180244, @@ -638,7 +633,7 @@ -0.01333591, -0.033472, -0.016169319, - -0.0000071265636, + -7.1265636e-06, -0.005581815, 0.015804123, 0.0035386125, @@ -729,7 +724,7 @@ 0.022893941, -0.021231676, -0.012435515, - 0.00009744761, + 9.744761e-05, -0.011566604, -0.005761264, -0.013978149, @@ -973,7 +968,7 @@ 0.0035512054, -0.0038754733, 0.0013135996, - -0.0000074586037, + -7.4586037e-06, -0.0048671663, -0.0005588111, -0.001589857, @@ -1056,7 +1051,7 @@ -0.029291147, -0.014922619, 0.017151566, - -0.00007747601, + -7.747601e-05, -0.006598694, 0.03626763, 0.011868834, diff --git a/dotcms-postman/src/test/resources/mappings/quantum-computers.json b/dotcms-postman/src/test/resources/mappings/quantum-computers.json index db3960ef6a73..5b784977d762 100644 --- a/dotcms-postman/src/test/resources/mappings/quantum-computers.json +++ b/dotcms-postman/src/test/resources/mappings/quantum-computers.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"How do quantum computers differ from classical computers.*" + "matches": ".*\"content\" : \"How do quantum computers differ from classical computers.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "Quantum computers differ from classical computers in their fundamental operation. While classical computers use bits (0s and 1s) to process information, quantum computers use quantum bits or qubits, which can represent 0, 1, or both simultaneously due to superposition. This allows quantum computers to perform complex calculations much faster than classical computers for certain problems.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "Quantum computers differ from classical computers in their fundamental operation. While classical computers use bits (0s and 1s) to process information, quantum computers use quantum bits or qubits, which can represent 0, 1, or both simultaneously due to superposition. This allows quantum computers to perform complex calculations much faster than classical computers for certain problems." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/robot-painting.json b/dotcms-postman/src/test/resources/mappings/robot-painting.json index cb910a5b5262..5002c00ae562 100644 --- a/dotcms-postman/src/test/resources/mappings/robot-painting.json +++ b/dotcms-postman/src/test/resources/mappings/robot-painting.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/i", "headers": { "Content-Type": { "equalTo": "application/json" @@ -14,7 +13,8 @@ { "matchesJsonPath": "$[?(@.prompt == 'Image of a robot painting the sixteen chapel')]" } - ] + ], + "urlPathPattern": "/images/generations" }, "response": { "status": 200, @@ -28,4 +28,4 @@ ] } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/significance-of-day-of-the-dead.json b/dotcms-postman/src/test/resources/mappings/significance-of-day-of-the-dead.json index e68f1be5bdc6..879f59acaa5c 100644 --- a/dotcms-postman/src/test/resources/mappings/significance-of-day-of-the-dead.json +++ b/dotcms-postman/src/test/resources/mappings/significance-of-day-of-the-dead.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"What is the cultural significance of the Day of the Dead in Mexico.*" + "matches": ".*\"content\" : \"What is the cultural significance of the Day of the Dead in Mexico.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "The Day of the Dead (Día de los Muertos) is a Mexican holiday celebrated on November 1st and 2nd to honor and remember deceased loved ones. It is a blend of indigenous and Catholic traditions, featuring vibrant altars (ofrendas), marigold flowers, sugar skulls, and parades. The holiday reflects a view of death as a natural part of life and a time for family and community gatherings.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "The Day of the Dead (D\u00eda de los Muertos) is a Mexican holiday celebrated on November 1st and 2nd to honor and remember deceased loved ones. It is a blend of indigenous and Catholic traditions, featuring vibrant altars (ofrendas), marigold flowers, sugar skulls, and parades. The holiday reflects a view of death as a natural part of life and a time for family and community gatherings." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/significanse-of-mona-lisa.json b/dotcms-postman/src/test/resources/mappings/significanse-of-mona-lisa.json index e3d7ea2f96e5..a6509eb552c0 100644 --- a/dotcms-postman/src/test/resources/mappings/significanse-of-mona-lisa.json +++ b/dotcms-postman/src/test/resources/mappings/significanse-of-mona-lisa.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\".What is the significance of the Mona Lisa.*" + "matches": ".*\"content\" : \".What is the significance of the Mona Lisa.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "The Mona Lisa, painted by Leonardo da Vinci, is one of the most famous and iconic works of art in the world. Its significance lies in its exquisite detail, the mysterious expression of the subject, and its influence on the Renaissance art movement. The painting is also known for its use of sfumato, a technique that creates a soft transition between colors and tones.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "The Mona Lisa, painted by Leonardo da Vinci, is one of the most famous and iconic works of art in the world. Its significance lies in its exquisite detail, the mysterious expression of the subject, and its influence on the Renaissance art movement. The painting is also known for its use of sfumato, a technique that creates a soft transition between colors and tones." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/sports-data-analytics.json b/dotcms-postman/src/test/resources/mappings/sports-data-analytics.json index a74c6a3ce8fb..b15932bc9d47 100644 --- a/dotcms-postman/src/test/resources/mappings/sports-data-analytics.json +++ b/dotcms-postman/src/test/resources/mappings/sports-data-analytics.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\".*How has data analytics transformed the strategies and performance of professional sports teams.*" + "matches": ".*\"content\" : \".*How has data analytics transformed the strategies and performance of professional sports teams.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -42,4 +42,4 @@ "system_fingerprint": null } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/stock-market.json b/dotcms-postman/src/test/resources/mappings/stock-market.json index cb21c1eb5f53..e0a36c5b3ec1 100644 --- a/dotcms-postman/src/test/resources/mappings/stock-market.json +++ b/dotcms-postman/src/test/resources/mappings/stock-market.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/e", "headers": { "Content-Type": { "equalTo": "application/json" @@ -10,11 +9,7 @@ "equalTo": "Bearer some-api-key-1a2bc3" } }, - "bodyPatterns": [ - { - "matches": ".*\"model\":\"text-embedding-ada-002\",\"input\":.*791,5708,3157,706,6982,5199.*" - } - ] + "urlPathPattern": "/embeddings" }, "response": { "status": 200, @@ -35,7 +30,7 @@ 0.0076700626, 0.0017746049, -0.029046984, - 0.000005472026, + 5.472026e-06, 0.009717925, -0.007795698, 0.010302131, @@ -279,7 +274,7 @@ -0.020164538, -0.020252483, -0.0021436599, - 0.0000030412073, + 3.0412073e-06, -0.001961488, -0.0032979385, -0.040504966, @@ -391,7 +386,7 @@ -0.014448111, -0.008210296, -0.01282741, - -0.000073369316, + -7.3369316e-05, 0.0118977055, -0.011169018, 0.007487891, @@ -593,7 +588,7 @@ 0.025290476, -0.0067215124, 0.0051730517, - -0.00006448648, + -6.448648e-05, 0.0016929418, -0.042339247, -0.00039104128, @@ -851,7 +846,7 @@ -0.018506145, 0.011169018, -0.018568963, - 0.000058891757, + 5.8891757e-05, -0.017551314, -0.0063037737, 0.009485499, @@ -1000,7 +995,7 @@ -0.028720332, 0.038545046, 0.015101417, - 0.0000063492676, + 6.3492676e-06, -0.0010867493, -0.016068812, 0.04256539, @@ -1161,7 +1156,7 @@ -0.009812152, 0.010691602, -0.014548619, - 0.00009358882, + 9.358882e-05, -0.0016976531, 0.016294956, 0.023355685, @@ -1246,7 +1241,7 @@ 0.0167975, -0.009830997, -0.0067843306, - -0.0000061621636, + -6.1621636e-06, -0.023167232, -0.0023321137, 0.018393073, @@ -1319,7 +1314,7 @@ -0.023694903, -0.028443934, 0.015867796, - -0.000021949449, + -2.1949449e-05, 0.0045417324, -0.01953636, -0.013430461, @@ -1461,7 +1456,7 @@ 0.024699988, -0.009045774, -0.009146282, - 0.00001831779, + 1.831779e-05, 0.020013776, -0.0353539, 0.007852234, diff --git a/dotcms-postman/src/test/resources/mappings/streaming-music-embedding.json b/dotcms-postman/src/test/resources/mappings/streaming-music-embedding.json index 3b28dd403c52..1a34bc4bbd62 100644 --- a/dotcms-postman/src/test/resources/mappings/streaming-music-embedding.json +++ b/dotcms-postman/src/test/resources/mappings/streaming-music-embedding.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/e", "headers": { "Content-Type": { "equalTo": "application/json" @@ -10,11 +9,7 @@ "equalTo": "Bearer some-api-key-1a2bc3" } }, - "bodyPatterns": [ - { - "matches": ".*\"model\":\"text-embedding-ada-002\",\"input\":.*4438,706,17265,5614,279,4731.*" - } - ] + "urlPathPattern": "/embeddings" }, "response": { "status": 200, @@ -49,7 +44,7 @@ 0.0015331289, -0.0077358284, 0.009176939, - 0.000022298018, + 2.2298018e-05, 0.018279014, 0.024243088, -0.023332257, @@ -544,7 +539,7 @@ -0.023207486, -0.007199311, 0.099467784, - 0.000067698245, + 6.7698245e-05, 0.010761536, 0.020849306, -0.040351078, @@ -656,7 +651,7 @@ -0.014772937, -0.009669785, -0.015696246, - -0.00004067256, + -4.067256e-05, 0.03663289, 0.008303539, 0.01275164, @@ -737,7 +732,7 @@ 0.0047787456, 0.019701408, 0.020075724, - -0.00005790174, + -5.790174e-05, 0.009457674, -0.00032849977, -0.031217812, @@ -1219,7 +1214,7 @@ -0.007305367, -0.002746531, -0.020911692, - -0.000015840122, + -1.5840122e-05, 0.012333656, -0.03471141, 0.005000215, diff --git a/dotcms-postman/src/test/resources/mappings/streaming-music.json b/dotcms-postman/src/test/resources/mappings/streaming-music.json index 53965867c36f..6acd9fb4a876 100644 --- a/dotcms-postman/src/test/resources/mappings/streaming-music.json +++ b/dotcms-postman/src/test/resources/mappings/streaming-music.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\".*How has streaming changed the music industry and affected the way artists release and promote their music.*" + "matches": ".*\"content\" : \".*How has streaming changed the music industry and affected the way artists release and promote their music.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -42,4 +42,4 @@ "system_fingerprint": null } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/theory-of-relativity.json b/dotcms-postman/src/test/resources/mappings/theory-of-relativity.json index abe77efea4b1..4e570c98d3d6 100644 --- a/dotcms-postman/src/test/resources/mappings/theory-of-relativity.json +++ b/dotcms-postman/src/test/resources/mappings/theory-of-relativity.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"What is the theory of relativity.*" + "matches": ".*\"content\" : \"What is the theory of relativity.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "The theory of relativity, developed by Albert Einstein, consists of two parts: special relativity and general relativity. Special relativity deals with objects moving at constant speeds, particularly those moving at the speed of light. It introduced the famous equation E=mc², showing that energy and mass are interchangeable. General relativity extends this to include gravity, describing it as a curvature of spacetime caused by mass.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "The theory of relativity, developed by Albert Einstein, consists of two parts: special relativity and general relativity. Special relativity deals with objects moving at constant speeds, particularly those moving at the speed of light. It introduced the famous equation E=mc\u00b2, showing that energy and mass are interchangeable. General relativity extends this to include gravity, describing it as a curvature of spacetime caused by mass." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/turtle-marathon.json b/dotcms-postman/src/test/resources/mappings/turtle-marathon.json index 14db3aa8c981..0fd5d0675b2f 100644 --- a/dotcms-postman/src/test/resources/mappings/turtle-marathon.json +++ b/dotcms-postman/src/test/resources/mappings/turtle-marathon.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/i", "headers": { "Content-Type": { "equalTo": "application/json" @@ -14,7 +13,8 @@ { "matchesJsonPath": "$[?(@.prompt == 'Generate image of a turtle training for a marathon')]" } - ] + ], + "urlPathPattern": "/images/generations" }, "response": { "status": 200, @@ -28,4 +28,4 @@ ] } } -} +} \ No newline at end of file diff --git a/dotcms-postman/src/test/resources/mappings/wwi-causes.json b/dotcms-postman/src/test/resources/mappings/wwi-causes.json index 02fdbb9c0669..a5c7750be265 100644 --- a/dotcms-postman/src/test/resources/mappings/wwi-causes.json +++ b/dotcms-postman/src/test/resources/mappings/wwi-causes.json @@ -1,7 +1,6 @@ { "request": { "method": "POST", - "url": "/c", "headers": { "Content-Type": { "equalTo": "application/json" @@ -12,9 +11,10 @@ }, "bodyPatterns": [ { - "matches": ".*\"content\":\"What were the main causes of World War I.*" + "matches": ".*\"content\" : \"What were the main causes of World War I.*" } - ] + ], + "urlPathPattern": "/chat/completions" }, "response": { "status": 200, @@ -25,10 +25,13 @@ "model": "gpt-3.5-turbo-16k", "choices": [ { - "text": "The main causes of World War I include militarism, alliances, imperialism, and nationalism. The immediate cause was the assassination of Archduke Franz Ferdinand of Austria-Hungary. This event triggered a chain reaction of alliances and conflicts that escalated into a full-scale war.", "index": 0, "logprobs": null, - "finish_reason": "stop" + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "The main causes of World War I include militarism, alliances, imperialism, and nationalism. The immediate cause was the assassination of Archduke Franz Ferdinand of Austria-Hungary. This event triggered a chain reaction of alliances and conflicts that escalated into a full-scale war." + } } ], "usage": { @@ -38,4 +41,4 @@ } } } -} +} \ No newline at end of file