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 26, 2024
1 parent 3ecd733 commit 37d5586
Show file tree
Hide file tree
Showing 22 changed files with 1,557 additions and 0 deletions.
108 changes: 108 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,108 @@
package com.dotcms.ai.client;

import com.dotcms.ai.domain.AIProvider;
import com.dotcms.ai.domain.AIRequest;
import com.dotcms.ai.domain.JSONObjectAIRequest;
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);

}
42 changes: 42 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,42 @@
package com.dotcms.ai.client;

import com.dotcms.ai.domain.AIRequest;
import com.dotcms.ai.domain.AIResponse;

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

}
34 changes: 34 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,34 @@
package com.dotcms.ai.client;

import com.dotcms.ai.domain.AIRequest;

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

}
86 changes: 86 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,86 @@
package com.dotcms.ai.client;

import com.dotcms.ai.domain.AIRequest;
import com.dotcms.ai.domain.AIResponse;

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

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

import com.dotcms.ai.client.openai.OpenAIClient;
import com.dotcms.ai.client.openai.OpenAIResponseEvaluator;
import com.dotcms.ai.domain.AIProvider;
import com.dotcms.ai.domain.AIRequest;
import com.dotcms.ai.domain.AIResponse;
import io.vavr.Lazy;

import java.io.OutputStream;
import java.io.Serializable;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;

/**
* A proxy client for managing and interacting with multiple AI service providers.
*
* <p>
* This class provides a mechanism to send requests to various AI service providers through proxied clients,
* applying different strategies for handling the requests and responses. It supports adding new clients and
* switching between different AI providers.
* </p>
*
* <p>
* The class allows for flexible handling of AI service interactions by maintaining a map of proxied clients
* and providing methods to send requests to the current or specified provider.
* </p>
*
* @author vico
*/
public class AIProxyClient {

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

private final ConcurrentMap<AIProvider, AIProxiedClient> proxiedClients;
private final AtomicReference<AIProvider> currentProvider;

private AIProxyClient() {
proxiedClients = new ConcurrentHashMap<>();
addClient(
AIProvider.OPEN_AI,
AIProxiedClient.of(OpenAIClient.get(), AIProxyStrategy.MODEL_FALLBACK, OpenAIResponseEvaluator.get()));
currentProvider = new AtomicReference<>(AIProvider.OPEN_AI);
}

public static AIProxyClient get() {
return INSTANCE.get();
}

/**
* Adds a proxied client for the specified AI provider.
*
* @param provider the AI provider for which the client is added
* @param client the proxied client to be added
*/
public void addClient(final AIProvider provider, final AIProxiedClient client) {
proxiedClients.put(provider, client);
}

/**
* Sends the given AI request to the specified AI provider and writes the response to the provided output stream.
*
* @param provider the AI provider to which the request is sent
* @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 AIResponse callToAI(final AIProvider provider,
final AIRequest<? extends Serializable> request,
final OutputStream output) {
return Optional.ofNullable(proxiedClients.getOrDefault(provider, AIProxiedClient.NOOP))
.map(client -> client.sendToAI(request, output))
.orElse(AIResponse.EMPTY);
}

/**
* Sends the given AI request to the specified AI provider.
*
* @param <T> the type of the request payload
* @param provider the AI provider to which the request is sent
* @param request the AI request to be sent
* @return the AI response
*/
public <T extends Serializable> AIResponse callToAI(final AIProvider provider, final AIRequest<T> request) {
return callToAI(provider, request, null);
}

/**
* Sends the given AI request to the current AI provider 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 callToAI(final AIRequest<T> request, final OutputStream output) {
return callToAI(currentProvider.get(), request, output);
}

/**
* Sends the given AI request to the current AI provider.
*
* @param <T> the type of the request payload
* @param request the AI request to be sent
* @return the AI response
*/
public <T extends Serializable> AIResponse callToAI(final AIRequest<T> request) {
return callToAI(request, null);
}

}
Loading

0 comments on commit 37d5586

Please sign in to comment.