Skip to content

Commit

Permalink
feat(dotAI): Adding fallback mechanism when it comes to send models. (#…
Browse files Browse the repository at this point in the history
…29748)

Including the concepts of AIProxyClient, AIProxiedClient,
AIClientStrategy to decouple functionality of handling AI requests from
the actual provider implementation.

Refs: #29284
  • Loading branch information
victoralfaro-dotcms authored Aug 27, 2024
1 parent 2f3045b commit d7ab3bd
Show file tree
Hide file tree
Showing 25 changed files with 1,564 additions and 13 deletions.
4 changes: 2 additions & 2 deletions dotCMS/src/main/java/com/dotcms/ai/app/AIModels.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ public Set<String> getOrPullSupportedModels() {

final AppConfig appConfig = appConfigSupplier.get();
if (!appConfig.isEnabled()) {
AppConfig.debugLogger(getClass(), () -> "dotAI is not enabled, returning empty list of supported models");
throw new DotRuntimeException("App dotAI config without API urls or API key");
AppConfig.debugLogger(getClass(), () -> "dotAI is not enabled, returning empty set of supported models");
return Set.of();
}

final CircuitBreakerUrl.Response<OpenAIModels> response = fetchOpenAIModels(appConfig);
Expand Down
5 changes: 1 addition & 4 deletions dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.dotcms.security.apps.Secret;
import com.dotmarketing.exception.DotRuntimeException;
import com.dotmarketing.util.Config;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.UtilMethods;
import com.liferay.util.StringPool;
Expand All @@ -29,10 +28,8 @@ public class AppConfig implements Serializable {
private static final String AI_API_URL_KEY = "AI_API_URL";
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_LOGGER_KEY = "AI_DEBUG_LOGGER";
private static final String SYSTEM_HOST = "System Host";
private static final AtomicReference<AppConfig> SYSTEM_HOST_CONFIG = new AtomicReference<>();
private static final boolean DEBUG_LOGGING = Config.getBooleanProperty(AI_DEBUG_LOGGER_KEY, false);

public static final Pattern SPLITTER = Pattern.compile("\\s?,\\s?");

Expand Down Expand Up @@ -107,7 +104,7 @@ public static AppConfig getSystemHostConfig() {
* @param message The {@link Supplier} with the message to log.
*/
public static void debugLogger(final Class<?> clazz, final Supplier<String> message) {
if (getSystemHostConfig().getConfigBoolean(AppKeys.DEBUG_LOGGING) || DEBUG_LOGGING) {
if (getSystemHostConfig().getConfigBoolean(AppKeys.DEBUG_LOGGING)) {
Logger.info(clazz, message.get());
}
}
Expand Down
106 changes: 106 additions & 0 deletions dotCMS/src/main/java/com/dotcms/ai/client/AIClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.dotcms.ai.client;

import com.dotcms.ai.domain.AIProvider;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;

import javax.ws.rs.HttpMethod;
import java.io.OutputStream;
import java.io.Serializable;

/**
* Interface representing an AI client capable of sending requests to an AI service.
*
* <p>
* This interface defines methods for obtaining the AI provider and sending requests
* to the AI service. Implementations of this interface should handle the specifics
* of interacting with the AI service, including request formatting and response handling.
* </p>
*
* <p>
* The interface also provides a NOOP implementation that throws an
* {@link UnsupportedOperationException} for all operations.
* </p>
*
* @author vico
*/
public interface AIClient {

AIClient NOOP = new AIClient() {
@Override
public AIProvider getProvider() {
return AIProvider.NONE;
}

@Override
public <T extends Serializable> void sendRequest(final AIRequest<T> request, final OutputStream output) {
throwUnsupported();
}

private void throwUnsupported() {
throw new UnsupportedOperationException("Noop client does not support sending requests");
}
};

/**
* Resolves the appropriate HTTP method for the given method name and URL.
*
* @param method the HTTP method name (e.g., "GET", "POST", "PUT", "DELETE", "patch")
* @param url the URL to which the request will be sent
* @return the corresponding {@link HttpUriRequest} for the given method and URL
*/
static HttpUriRequest resolveMethod(final String method, final String url) {
switch(method) {
case HttpMethod.POST:
return new HttpPost(url);
case HttpMethod.PUT:
return new HttpPut(url);
case HttpMethod.DELETE:
return new HttpDelete(url);
case "patch":
return new HttpPatch(url);
case HttpMethod.GET:
default:
return new HttpGet(url);
}
}

/**
* Validates and casts the given AI request to a {@link JSONObjectAIRequest}.
*
* @param <T> the type of the request payload
* @param request the AI request to be validated and cast
* @return the validated and cast {@link JSONObjectAIRequest}
* @throws UnsupportedOperationException if the request is not an instance of {@link JSONObjectAIRequest}
*/
static <T extends Serializable> JSONObjectAIRequest useRequestOrThrow(final AIRequest<T> request) {
// When we get rid of JSONObject usage, we can remove this check
if (request instanceof JSONObjectAIRequest) {
return (JSONObjectAIRequest) request;
}

throw new UnsupportedOperationException("Only JSONObjectAIRequest (JSONObject) is supported");
}

/**
* Returns the AI provider associated with this client.
*
* @return the AI provider
*/
AIProvider getProvider();

/**
* Sends the given AI request to the AI service and writes the response to the provided output stream.
*
* @param <T> the type of the request payload
* @param request the AI request to be sent
* @param output the output stream to which the response will be written
* @throws Exception if any error occurs during the request execution
*/
<T extends Serializable> void sendRequest(AIRequest<T> request, OutputStream output);

}
39 changes: 39 additions & 0 deletions dotCMS/src/main/java/com/dotcms/ai/client/AIClientStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.dotcms.ai.client;

import java.io.OutputStream;
import java.io.Serializable;

/**
* Interface representing a strategy for handling AI client requests and responses.
*
* <p>
* This interface defines a method for applying a strategy to an AI client request,
* allowing for different handling mechanisms to be implemented. The NOOP strategy
* is provided as a default implementation that performs no operations.
* </p>
*
* <p>
* Implementations of this interface should define how to process the AI request
* and handle the response, potentially writing the response to an output stream.
* </p>
*
* @author vico
*/
public interface AIClientStrategy {

AIClientStrategy NOOP = (client, handler, request, output) -> AIResponse.builder().build();

/**
* Applies the strategy to the given AI client request and handles the response.
*
* @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 output the output stream to which the response will be written
*/
void applyStrategy(AIClient client,
AIResponseEvaluator handler,
AIRequest<? extends Serializable> request,
OutputStream output);

}
32 changes: 32 additions & 0 deletions dotCMS/src/main/java/com/dotcms/ai/client/AIDefaultStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.dotcms.ai.client;

import java.io.OutputStream;
import java.io.Serializable;

/**
* Default implementation of the {@link AIClientStrategy} interface.
*
* <p>
* This class provides a default strategy for handling AI client requests by
* directly sending the request using the provided AI client and writing the
* response to the given output stream.
* </p>
*
* <p>
* The default strategy does not perform any additional processing or handling
* of the request or response, delegating the entire operation to the AI client.
* </p>
*
* @author vico
*/
public class AIDefaultStrategy implements AIClientStrategy {

@Override
public void applyStrategy(final AIClient client,
final AIResponseEvaluator handler,
final AIRequest<? extends Serializable> request,
final OutputStream output) {
client.sendRequest(request, output);
}

}
83 changes: 83 additions & 0 deletions dotCMS/src/main/java/com/dotcms/ai/client/AIProxiedClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.dotcms.ai.client;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Optional;

/**
* A proxy client for interacting with an AI service using a specified strategy.
*
* <p>
* This class provides a mechanism to send requests to an AI service through a proxied client,
* applying a given strategy for handling the requests and responses. It supports a NOOP implementation
* that performs no operations.
* </p>
*
* <p>
* The class allows for the creation of proxied clients with different strategies and response evaluators,
* enabling flexible handling of AI service interactions.
* </p>
*
* @author vico
*/
public class AIProxiedClient {

public static final AIProxiedClient NOOP = new AIProxiedClient(null, AIClientStrategy.NOOP, null);

private final AIClient client;
private final AIClientStrategy strategy;
private final AIResponseEvaluator responseEvaluator;

private AIProxiedClient(final AIClient client,
final AIClientStrategy strategy,
final AIResponseEvaluator responseEvaluator) {
this.client = client;
this.strategy = strategy;
this.responseEvaluator = responseEvaluator;
}

/**
* Creates an AIProxiedClient with the specified client, strategy, and response evaluator.
*
* @param client the AI client to be proxied
* @param strategy the strategy to be applied for handling requests and responses
* @param responseParser the response evaluator to process responses
* @return a new instance of AIProxiedClient
*/
public static AIProxiedClient of(final AIClient client,
final AIProxyStrategy strategy,
final AIResponseEvaluator responseParser) {
return new AIProxiedClient(client, strategy.getStrategy(), responseParser);
}

/**
* Creates an AIProxiedClient with the specified client and strategy.
*
* @param client the AI client to be proxied
* @param strategy the strategy to be applied for handling requests and responses
* @return a new instance of AIProxiedClient
*/
public static AIProxiedClient of(final AIClient client, final AIProxyStrategy strategy) {
return of(client, strategy, null);
}

/**
* Sends the given AI request to the AI service and writes the response to the provided output stream.
*
* @param <T> the type of the request payload
* @param request the AI request to be sent
* @param output the output stream to which the response will be written
* @return the AI response
*/
public <T extends Serializable> AIResponse sendToAI(final AIRequest<T> request, final OutputStream output) {
final OutputStream finalOutput = Optional.ofNullable(output).orElseGet(ByteArrayOutputStream::new);

strategy.applyStrategy(client, responseEvaluator, request, finalOutput);

return Optional.ofNullable(output)
.map(out -> AIResponse.EMPTY)
.orElseGet(() -> AIResponse.builder().withResponse(finalOutput.toString()).build());
}

}
Loading

0 comments on commit d7ab3bd

Please sign in to comment.