Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import com.dotcms.ai.exception.DotAIModelNotFoundException;
import com.dotcms.ai.rest.forms.CompletionsForm;
import com.dotcms.ai.util.EncodingUtil;
import com.dotcms.analytics.Util;
import com.dotcms.api.web.HttpServletRequestThreadLocal;
import com.dotcms.mock.request.FakeHttpRequest;
import com.dotcms.mock.response.BaseResponse;
Expand Down
89 changes: 85 additions & 4 deletions dotCMS/src/main/java/com/dotcms/ai/app/AIAppUtil.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package com.dotcms.ai.app;

import com.dotcms.ai.config.AiModelConfig;
import com.dotcms.ai.config.AiModelConfigCatalog;
import com.dotcms.ai.config.AiModelConfigCatalogImpl;
import com.dotcms.ai.config.AiVendor;
import com.dotcms.ai.config.parser.AiModelConfigParser;
import com.dotcms.ai.config.parser.AiVendorCatalogData;
import com.dotcms.security.apps.AppsUtil;
import com.dotcms.security.apps.Secret;
import com.dotcms.security.apps.Type;
import com.dotmarketing.util.Config;
import com.dotmarketing.util.StringUtils;
import com.dotmarketing.util.UtilMethods;
import com.liferay.util.StringPool;
import io.vavr.Lazy;
Expand All @@ -25,7 +34,9 @@
public class AIAppUtil {

private static final Lazy<AIAppUtil> INSTANCE = Lazy.of(AIAppUtil::new);

// we will hold this by now until the json configuration is moved to official and only one
private static ThreadLocal<AiModelConfigCatalog> modelConfigCatalogThreadLocal = new ThreadLocal<>();
private static final AiModelConfigParser modelConfigParser = new AiModelConfigParser();
private AIAppUtil() {
// Private constructor to prevent instantiation
}
Expand All @@ -34,16 +45,43 @@ public static AIAppUtil get() {
return INSTANCE.get();
}

private Optional<AiModelConfigCatalog> getModelConfigCatalog(final Map<String, Secret> secrets, final AppKeys appKeys) {

if (modelConfigCatalogThreadLocal.get() == null) {

final String aiJsonConfiguration = this.discoverSecret(secrets, appKeys);
if (StringUtils.isSet(aiJsonConfiguration)) {

final AiVendorCatalogData vendorCatalogData = modelConfigParser.parse(aiJsonConfiguration,
(key) -> {
// default ValueResolver impl, based on dotCMS config and context (the context encapsulates the dotAI Secrets App
return Config.getStringProperty(key, secrets.getOrDefault(key,
Secret.builder().withValue("UNKNOWN").withType(Type.STRING).build()).getString());
});
final AiModelConfigCatalog modelConfigCatalog = AiModelConfigCatalogImpl.from(vendorCatalogData);
modelConfigCatalogThreadLocal.set(modelConfigCatalog);
}
}

return Optional.ofNullable(modelConfigCatalogThreadLocal.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<String, Secret> secrets) {
final List<String> modelNames = splitDiscoveredSecret(secrets, AppKeys.TEXT_MODEL_NAMES);
if (CollectionUtils.isEmpty(modelNames)) {
return AIModel.NOOP_MODEL;

List<String> modelNames = getModelNamesFromCatalogs(secrets, AppKeys.ADVANCE_PROVIDER_SETTINGS_KEY, AiVendor.OPEN_AI.getVendorName());

if (modelNames == null) {

modelNames = splitDiscoveredSecret(secrets, AppKeys.TEXT_MODEL_NAMES);
if (CollectionUtils.isEmpty(modelNames)) {
return AIModel.NOOP_MODEL;
}
}

return AIModel.builder()
Expand All @@ -56,6 +94,19 @@ public AIModel createTextModel(final Map<String, Secret> secrets) {
.build();
}

private List<String> getModelNamesFromCatalogs(final Map<String, Secret> secrets, final AppKeys appKeys,
final String vendorName) {

final Optional<AiModelConfigCatalog> modelConfigCatalogOpt = this.getModelConfigCatalog(secrets, appKeys);
if(modelConfigCatalogOpt.isPresent()) {
return modelConfigCatalogOpt.get().getChatModelNames(vendorName);
}

return null;
}



/**
* Creates an image model instance based on the provided secrets.
*
Expand Down Expand Up @@ -85,6 +136,8 @@ public AIModel createImageModel(final Map<String, Secret> secrets) {
* @return the created embeddings model instance
*/
public AIModel createEmbeddingsModel(final Map<String, Secret> secrets) {
// todo: we have to do the same here, taking the config from the json
// however I am not sure if it is worthy until to bring the embedding new langchain stuff
final List<String> modelNames = splitDiscoveredSecret(secrets, AppKeys.EMBEDDINGS_MODEL_NAMES);
if (CollectionUtils.isEmpty(modelNames)) {
return AIModel.NOOP_MODEL;
Expand All @@ -110,6 +163,7 @@ public AIModel createEmbeddingsModel(final Map<String, Secret> secrets) {
* @return the resolved secret value or the default value if the secret is not found
*/
public String discoverSecret(final Map<String, Secret> secrets, final AppKeys key, final String defaultValue) {

return Try.of(() -> secrets.get(key.key).getString()).getOrElse(defaultValue);
}

Expand Down Expand Up @@ -183,4 +237,31 @@ private int toInt(final String value) {
return Try.of(() -> Integer.parseInt(value)).getOrElse(0);
}

/**
* Discover the api key
* @param secrets
* @param appKeys
* @return
*/
public String discoverApiKeySecret(final Map<String, Secret> secrets) {

final Optional<AiModelConfigCatalog> modelConfigCatalogOpt = this.getModelConfigCatalog(secrets, AppKeys.ADVANCE_PROVIDER_SETTINGS_KEY);
if(!modelConfigCatalogOpt.isPresent()) {

return this.discoverSecret(secrets, AppKeys.API_KEY);
}

return modelConfigCatalogOpt.get().getChatConfig(AiVendor.OPEN_AI.getVendorName()).get(AiModelConfig.API_KEY);
}

public String discoverApiUrlEnvSecret(final Map<String, Secret> secrets, final String aiApiUrlKey) {

final Optional<AiModelConfigCatalog> modelConfigCatalogOpt = this.getModelConfigCatalog(secrets, AppKeys.ADVANCE_PROVIDER_SETTINGS_KEY);
if(!modelConfigCatalogOpt.isPresent()) {

return this.discoverEnvSecret(secrets, AppKeys.API_URL, aiApiUrlKey);
}

return modelConfigCatalogOpt.get().getChatConfig(AiVendor.OPEN_AI.getVendorName()).get(AiModelConfig.API_KEY);
}
}
4 changes: 2 additions & 2 deletions dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public AppConfig(final String host, final Map<String, Secret> secrets) {
this.host = host;

final AIAppUtil aiAppUtil = AIAppUtil.get();
apiKey = aiAppUtil.discoverSecret(secrets, AppKeys.API_KEY);
apiUrl = aiAppUtil.discoverEnvSecret(secrets, AppKeys.API_URL, AI_API_URL_KEY);
apiKey = aiAppUtil.discoverApiKeySecret(secrets);
apiUrl = aiAppUtil.discoverApiUrlEnvSecret(secrets, 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);

Expand Down
1 change: 1 addition & 0 deletions dotCMS/src/main/java/com/dotcms/ai/app/AppKeys.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

public enum AppKeys {

ADVANCE_PROVIDER_SETTINGS_KEY("advanceProviderSettings", null),
API_KEY("apiKey", null),
API_URL("apiUrl", "https://api.openai.com/v1/chat/completions"),
API_IMAGE_URL("apiImageUrl", "https://api.openai.com/v1/images/generations"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@
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;
Expand All @@ -29,8 +27,6 @@
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.
Expand Down
40 changes: 40 additions & 0 deletions dotCMS/src/main/java/com/dotcms/ai/config/AiModel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.dotcms.ai.config;

import java.util.Map;

/**
* Model encapsulates a default configuration for known models, however the Models still able to be created dynamically
* @author jsanca
*/
public enum AiModel {

OPEN_AI_GPT_40(AiVendor.OPEN_AI, "gpt-4o-mini", "https://api.openai.com/v1"),
OPEN_AI_TEXT_EMBEDDING_3_SMALL(AiVendor.OPEN_AI, "text-embedding-3-small", "https://api.openai.com/v1"),
ANTHROPIC_CLAUDE_3_7(AiVendor.ANTHROPIC, "claude-3-7-sonnet-20250219", "https://api.openai.com/v1");

private final AiVendor vendor;
private final String model;
private final String apiUrl;

AiModel(final AiVendor vendor, final String model, final String apiUrl) {
this.vendor = vendor;
this.model = model;
this.apiUrl = apiUrl;
}

public AiVendor getVendor() { return vendor; }
public String getModel() { return model; }
public String getApiUrl() { return apiUrl; }
public String getProviderName() {
return vendor.getVendorName() + "/" + model;
}

public AiModelConfig toConfig(final String apiKey) {

return new AiModelConfig(this.model,
Map.of(AiModelConfig.API_KEY, apiKey,
AiModelConfig.VENDOR, vendor.getVendorName(),
AiModelConfig.MODEL, model,
AiModelConfig.API_URL, apiUrl));
}
}
47 changes: 47 additions & 0 deletions dotCMS/src/main/java/com/dotcms/ai/config/AiModelConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.dotcms.ai.config;

import java.util.Map;

/**
* Encapsulates the Configuration for a model
* @author jsanca
*/
public class AiModelConfig {

public static final String API_KEY = "key";
public static final String VENDOR = "vendor";
public static final String MODEL = "model";
public static final String API_URL = "apiUrl";

private final String name;
private final Map<String, String> config;

public AiModelConfig(final String name, final Map<String, String> config) {
this.name = name;
this.config = config;
}

public String getName() {
return name;
}

public String get(final String key) {
return config.get(key);
}

public String getOrDefault(final String key, final String defaultValue) {
return config.getOrDefault(key, defaultValue);
}

public Map<String, String> asMap() {
return Map.copyOf(config);
}

@Override
public String toString() {
return "AiModelConfig{" +
"name='" + name + '\'' +
", config=" + config +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.dotcms.ai.config;

import java.util.List;

/**
* Model Config Catalog
* Returns the ModelConfig for Chat and Embeddings based on vendor and model
* @author jsanca
*/
public interface AiModelConfigCatalog {

AiModelConfig getChatConfig(AiVendor vendor);

AiModelConfig getChatConfig(String vendor);

AiModelConfig getChatConfig(String vendor, String modelKey);

AiModelConfig getEmbeddingsConfig(String vendor);

AiModelConfig getEmbeddingsConfig(String vendor, String modelKey);

// Opcional: "openai.chat.gpt-4o-mini" / "openai.embeddings.text-embedding-3-small"
AiModelConfig getByPath(String path);

List<String> getChatModelNames(String vendorName);
}
Loading
Loading