diff --git a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIChatService.java b/dotCMS/src/main/java/com/dotcms/ai/api/ChatAPI.java similarity index 94% rename from dotCMS/src/main/java/com/dotcms/ai/service/OpenAIChatService.java rename to dotCMS/src/main/java/com/dotcms/ai/api/ChatAPI.java index af7b5bc2903c..363772bfa2b6 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIChatService.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/ChatAPI.java @@ -1,8 +1,8 @@ -package com.dotcms.ai.service; +package com.dotcms.ai.api; import com.dotmarketing.util.json.JSONObject; -public interface OpenAIChatService { +public interface ChatAPI { /** * Returns a JSONObject with the results of the text generation given the provided prompt diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/ChatAPIProvider.java b/dotCMS/src/main/java/com/dotcms/ai/api/ChatAPIProvider.java new file mode 100644 index 000000000000..097dda902ba8 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/api/ChatAPIProvider.java @@ -0,0 +1,6 @@ +package com.dotcms.ai.api; + +public interface ChatAPIProvider { + + ChatAPI getChatAPI(Object... initArguments); +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/DotAIAPI.java b/dotCMS/src/main/java/com/dotcms/ai/api/DotAIAPI.java index c2afd9303808..034d32261994 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/api/DotAIAPI.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/DotAIAPI.java @@ -6,7 +6,31 @@ */ public interface DotAIAPI { + /** + * Returns the completions API + * @param initArguments + * @return + */ CompletionsAPI getCompletionsAPI(Object... initArguments); + /** + * Returns the embeddings API + * @param initArguments + * @return + */ EmbeddingsAPI getEmbeddingsAPI(Object... initArguments); + + /** + * Returns the chat API + * @param initArguments + * @return + */ + ChatAPI getChatAPI(Object... initArguments); + + /** + * Returns the image API + * @param initArguments + * @return + */ + ImageAPI getImageAPI(Object... initArguments); } diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/DotAIAPIFacadeImpl.java b/dotCMS/src/main/java/com/dotcms/ai/api/DotAIAPIFacadeImpl.java index 7601c4e8caf3..a269ad8d20f6 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/api/DotAIAPIFacadeImpl.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/DotAIAPIFacadeImpl.java @@ -1,8 +1,12 @@ package com.dotcms.ai.api; import com.dotcms.ai.app.AppConfig; +import com.dotcms.rest.api.v1.temp.TempFileAPI; import com.dotmarketing.beans.Host; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.portlets.contentlet.business.HostAPI; import com.dotmarketing.util.Logger; +import com.liferay.portal.model.User; import java.util.Map; import java.util.Objects; @@ -16,14 +20,20 @@ */ public class DotAIAPIFacadeImpl implements DotAIAPI { - private static final AtomicReference currentApiProviderName = new AtomicReference<>("default"); + + private static final String DEFAULT = "default"; + private static final AtomicReference currentApiProviderName = new AtomicReference<>(DEFAULT); private static final Map completionsProviderMap = new ConcurrentHashMap<>(); private static final Map embeddingsProviderMap = new ConcurrentHashMap<>(); + private static final Map chatProviderMap = new ConcurrentHashMap<>(); + private static final Map imageProviderMap = new ConcurrentHashMap<>(); static { try { - completionsProviderMap.put("default", new DefaultCompletionsAPIProvider()); - embeddingsProviderMap.put("default", new DefaultEmbeddingsAPIProvider()); + completionsProviderMap.put(DEFAULT, new DefaultCompletionsAPIProvider()); + embeddingsProviderMap.put(DEFAULT, new DefaultEmbeddingsAPIProvider()); + chatProviderMap.put(DEFAULT, new DefaultChatAPIProvider()); + imageProviderMap.put(DEFAULT, new DefaultImageAPIProvider()); } catch (Exception e) { Logger.error(DotAIAPI.class, e.getMessage(), e); } @@ -35,6 +45,47 @@ private static T unwrap(final Class clazz, final Object... initArguments) && clazz.isInstance(initArguments[0]) ? clazz.cast(initArguments[0]) : null; } + /** + * Default provider for the ChatAPI + */ + public static class DefaultChatAPIProvider implements ChatAPIProvider { + + @Override + public ChatAPI getChatAPI(final Object... initArguments) { + if (Objects.nonNull(initArguments) && initArguments.length > 0 && initArguments[0] instanceof AppConfig) { + return new OpenAIChatAPIImpl((AppConfig) initArguments[0]); + } + + throw new IllegalArgumentException("To create a ChatAPI you need to provide an AppConfig"); + } + } + + /** + * Default provider for the ImageAPI + */ + public static class DefaultImageAPIProvider implements ImageAPIProvider { + + @Override + public ImageAPI getImageAPI(final Object... initArguments) { + if (Objects.nonNull(initArguments) && initArguments.length >= 4 + && initArguments[0] instanceof AppConfig + && (Objects.isNull(initArguments[1]) || initArguments[1] instanceof User) + ) { + + final AppConfig config = (AppConfig) initArguments[0]; + final User user = (User) initArguments[1]; + final HostAPI hostApi = APILocator.getHostAPI(); + final TempFileAPI tempFileApi = APILocator.getTempFileAPI(); + return new OpenAIImageAPIImpl(config, user, hostApi, tempFileApi); + } + + throw new IllegalArgumentException("To create an Image you need to provide an AppConfig"); + } + } + + /** + * Default provider for the CompletionsAPI + */ private static class DefaultCompletionsAPIProvider implements CompletionsAPIProvider { @Override @@ -47,6 +98,9 @@ private AppConfig unwrap(final Object... initArguments) { } } + /** + * Default provider for the EmbeddingsAPI + */ public static class DefaultEmbeddingsAPIProvider implements EmbeddingsAPIProvider { @Override @@ -72,19 +126,35 @@ public static final void setCurrentApiProviderName(final String apiName) { * @param completionsAPI */ public static final void setDefaultCompletionsAPIProvider(final CompletionsAPIProvider completionsAPI) { - completionsProviderMap.put("default", completionsAPI); + completionsProviderMap.put(DEFAULT, completionsAPI); } /** - * Adds the default embeddings API Provider. + * Set the default embeddings API Provider. * @param embeddingsAPI */ public static final void setDefaultEmbeddingsAPIProvider(final EmbeddingsAPIProvider embeddingsAPI) { - embeddingsProviderMap.put("default", embeddingsAPI); + embeddingsProviderMap.put(DEFAULT, embeddingsAPI); + } + + /** + * Set the default image API Provider. + * @param imageAPIProvider + */ + public static final void setDefaultImageAPIProvider(final ImageAPIProvider imageAPIProvider) { + imageProviderMap.put(DEFAULT, imageAPIProvider); + } + + /** + * Set the default chat API Provider. + * @param chatAPIProviderq + */ + public static final void setDefaultChatAPIProvider(final ChatAPIProvider chatAPIProvider) { + chatProviderMap.put(DEFAULT, chatAPIProvider); } /** - * Adds the default completions API provider. + * Adds completions API provider. * @param completionsAPI */ public static final void addCompletionsAPIImplementation(final String apiName, final CompletionsAPIProvider completionsAPI) { @@ -92,21 +162,49 @@ public static final void addCompletionsAPIImplementation(final String apiName, f } /** - * Sets the default embeddings API provider. + * Adds default embeddings API provider. * @param embeddingsAPI */ - public static final void addDefaultEmbeddingsAPIImplementation(final String apiName, final EmbeddingsAPIProvider embeddingsAPI) { + public static final void addEmbeddingsAPIImplementation(final String apiName, final EmbeddingsAPIProvider embeddingsAPI) { embeddingsProviderMap.put(apiName, embeddingsAPI); } + /** + * Adds default chat API provider. + * @param chatAPI + */ + public static final void addChatAPIImplementation(final String apiName, final ChatAPIProvider chatAPI) { + chatProviderMap.put(apiName, chatAPI); + } + + /** + * Adds default image API provider. + * @param imageAPI + */ + public static final void addImageAPIImplementation(final String apiName, final ImageAPIProvider imageAPI) { + imageProviderMap.put(apiName, imageAPI); + } + @Override - public CompletionsAPI getCompletionsAPI(Object... initArguments) { + public CompletionsAPI getCompletionsAPI(final Object... initArguments) { return completionsProviderMap.get(currentApiProviderName.get()).getCompletionsAPI(initArguments); } @Override - public EmbeddingsAPI getEmbeddingsAPI(Object... initArguments) { + public EmbeddingsAPI getEmbeddingsAPI(final Object... initArguments) { return embeddingsProviderMap.get(currentApiProviderName.get()).getEmbeddingsAPI(initArguments); } + + @Override + public ChatAPI getChatAPI(final Object... initArguments) { + + return chatProviderMap.get(currentApiProviderName.get()).getChatAPI(initArguments); + } + + @Override + public ImageAPI getImageAPI(final Object... initArguments) { + + return imageProviderMap.get(currentApiProviderName.get()).getImageAPI(initArguments); + } } diff --git a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIImageService.java b/dotCMS/src/main/java/com/dotcms/ai/api/ImageAPI.java similarity index 93% rename from dotCMS/src/main/java/com/dotcms/ai/service/OpenAIImageService.java rename to dotCMS/src/main/java/com/dotcms/ai/api/ImageAPI.java index d0fc19ee479f..cd7083d36510 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIImageService.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/ImageAPI.java @@ -1,4 +1,4 @@ -package com.dotcms.ai.service; +package com.dotcms.ai.api; import com.dotcms.ai.model.AIImageRequestDTO; import com.dotmarketing.util.json.JSONObject; @@ -6,7 +6,7 @@ /** * Service to interact with the OpenAI Image API */ -public interface OpenAIImageService { +public interface ImageAPI { /** * Sends a text prompt to the OpenAI API. diff --git a/dotCMS/src/main/java/com/dotcms/ai/api/ImageAPIProvider.java b/dotCMS/src/main/java/com/dotcms/ai/api/ImageAPIProvider.java new file mode 100644 index 000000000000..c52773f2c5f8 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/ai/api/ImageAPIProvider.java @@ -0,0 +1,11 @@ +package com.dotcms.ai.api; + + +/** + * This class is in charge of providing the {@link ImageAPI}. + * @author jsanca + */ +public interface ImageAPIProvider { + + ImageAPI getImageAPI(Object... initArguments); +} diff --git a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIChatServiceImpl.java b/dotCMS/src/main/java/com/dotcms/ai/api/OpenAIChatAPIImpl.java similarity index 87% rename from dotCMS/src/main/java/com/dotcms/ai/service/OpenAIChatServiceImpl.java rename to dotCMS/src/main/java/com/dotcms/ai/api/OpenAIChatAPIImpl.java index 08edb4d5d691..2e94dbe4218d 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIChatServiceImpl.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/OpenAIChatAPIImpl.java @@ -1,4 +1,4 @@ -package com.dotcms.ai.service; +package com.dotcms.ai.api; import com.dotcms.ai.AiKeys; import com.dotcms.ai.app.AppConfig; @@ -12,11 +12,11 @@ import java.util.List; import java.util.Map; -public class OpenAIChatServiceImpl implements OpenAIChatService { +public class OpenAIChatAPIImpl implements ChatAPI { private final AppConfig config; - public OpenAIChatServiceImpl(final AppConfig appConfig) { + public OpenAIChatAPIImpl(final AppConfig appConfig) { this.config = appConfig; } @@ -47,7 +47,7 @@ public JSONObject sendTextPrompt(final String textPrompt) { } @VisibleForTesting - String doRequest(final String urlIn, final JSONObject json) { + public String doRequest(final String urlIn, final JSONObject json) { return OpenAIRequest.doRequest(urlIn, HttpMethod.POST, config, json); } diff --git a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIImageServiceImpl.java b/dotCMS/src/main/java/com/dotcms/ai/api/OpenAIImageAPIImpl.java similarity index 93% rename from dotCMS/src/main/java/com/dotcms/ai/service/OpenAIImageServiceImpl.java rename to dotCMS/src/main/java/com/dotcms/ai/api/OpenAIImageAPIImpl.java index 57b571dc140b..041a49b6e2d1 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/service/OpenAIImageServiceImpl.java +++ b/dotCMS/src/main/java/com/dotcms/ai/api/OpenAIImageAPIImpl.java @@ -1,4 +1,4 @@ -package com.dotcms.ai.service; +package com.dotcms.ai.api; import com.dotcms.ai.AiKeys; import com.dotcms.ai.app.AppConfig; @@ -29,7 +29,7 @@ import java.text.SimpleDateFormat; import java.util.Date; -public class OpenAIImageServiceImpl implements OpenAIImageService { +public class OpenAIImageAPIImpl implements ImageAPI { private static StopWordsUtil stopWordsUtil = StopWordsUtil.get(); @@ -38,10 +38,10 @@ public class OpenAIImageServiceImpl implements OpenAIImageService { private final HostAPI hostApi; private final TempFileAPI tempFileApi; - public OpenAIImageServiceImpl(final AppConfig config, - final User user, - final HostAPI hostApi, - final TempFileAPI tempFileApi) { + public OpenAIImageAPIImpl(final AppConfig config, + final User user, + final HostAPI hostApi, + final TempFileAPI tempFileApi) { this.config = config; this.user = user; this.hostApi = hostApi; @@ -173,22 +173,22 @@ private String generateFileName(final String originalPrompt) { } @VisibleForTesting - String doRequest(final String urlIn, final JSONObject json) { + public String doRequest(final String urlIn, final JSONObject json) { return OpenAIRequest.doRequest(urlIn, HttpMethod.POST, config, json); } @VisibleForTesting - User getUser() { + public User getUser() { return APILocator.systemUser(); } @VisibleForTesting - AIImageRequestDTO.Builder getDtoBuilder() { + public AIImageRequestDTO.Builder getDtoBuilder() { return new AIImageRequestDTO.Builder(); } public static void setStopWordsUtil(final StopWordsUtil stopWordsUtil) { - OpenAIImageServiceImpl.stopWordsUtil = stopWordsUtil; + OpenAIImageAPIImpl.stopWordsUtil = stopWordsUtil; } } 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 21903b41e291..375625d58adf 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/rest/ImageResource.java +++ b/dotCMS/src/main/java/com/dotcms/ai/rest/ImageResource.java @@ -5,8 +5,8 @@ import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.ConfigService; import com.dotcms.ai.model.AIImageRequestDTO; -import com.dotcms.ai.service.OpenAIImageService; -import com.dotcms.ai.service.OpenAIImageServiceImpl; +import com.dotcms.ai.api.ImageAPI; +import com.dotcms.ai.api.OpenAIImageAPIImpl; import com.dotcms.rest.WebResource; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.web.WebAPILocator; @@ -113,7 +113,7 @@ public Response handleImageRequest(@Context final HttpServletRequest request, .build(); } - final OpenAIImageService service = new OpenAIImageServiceImpl( + final ImageAPI service = APILocator.getDotAIAPI().getImageAPI( config, user, APILocator.getHostAPI(), diff --git a/dotCMS/src/main/java/com/dotcms/ai/viewtool/AIViewTool.java b/dotCMS/src/main/java/com/dotcms/ai/viewtool/AIViewTool.java index 7891263733e4..050b56b1e535 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/viewtool/AIViewTool.java +++ b/dotCMS/src/main/java/com/dotcms/ai/viewtool/AIViewTool.java @@ -3,10 +3,10 @@ import com.dotcms.ai.AiKeys; import com.dotcms.ai.app.AppConfig; import com.dotcms.ai.app.ConfigService; -import com.dotcms.ai.service.OpenAIChatService; -import com.dotcms.ai.service.OpenAIChatServiceImpl; -import com.dotcms.ai.service.OpenAIImageService; -import com.dotcms.ai.service.OpenAIImageServiceImpl; +import com.dotcms.ai.api.ChatAPI; +import com.dotcms.ai.api.OpenAIChatAPIImpl; +import com.dotcms.ai.api.ImageAPI; +import com.dotcms.ai.api.OpenAIImageAPIImpl; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.web.WebAPILocator; import com.dotmarketing.util.json.JSONObject; @@ -28,8 +28,8 @@ public class AIViewTool implements ViewTool { private ViewContext context; private AppConfig config; - private OpenAIChatService chatService; - private OpenAIImageService imageService; + private ChatAPI chatService; + private ImageAPI imageService; @Override public void init(final Object obj) { @@ -127,13 +127,13 @@ User user() { } @VisibleForTesting - OpenAIChatService chatService() { - return new OpenAIChatServiceImpl(config); + ChatAPI chatService() { + return APILocator.getDotAIAPI().getChatAPI(config); } @VisibleForTesting - OpenAIImageService imageService() { - return new OpenAIImageServiceImpl(config, user(), APILocator.getHostAPI(), APILocator.getTempFileAPI()); + ImageAPI imageService() { + return APILocator.getDotAIAPI().getImageAPI(config, user(), APILocator.getHostAPI(), APILocator.getTempFileAPI()); } private

Try generate(final P prompt, final Function serviceCall) { diff --git a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIGenerateImageActionlet.java b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIGenerateImageActionlet.java index b1ef5eac9b75..8ded9b0bc9f1 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIGenerateImageActionlet.java +++ b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIGenerateImageActionlet.java @@ -38,7 +38,6 @@ public String getHowTo() { @Override public void executeAction(WorkflowProcessor processor, Map params) throws WorkflowActionFailureException { - final Runnable task = new OpenAIGenerateImageRunner(processor, params); task.run(); } diff --git a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIGenerateImageRunner.java b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIGenerateImageRunner.java index 29c3af893417..a1304a8f0b1f 100644 --- a/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIGenerateImageRunner.java +++ b/dotCMS/src/main/java/com/dotcms/ai/workflow/OpenAIGenerateImageRunner.java @@ -1,16 +1,22 @@ package com.dotcms.ai.workflow; import com.dotcms.ai.app.ConfigService; -import com.dotcms.ai.service.OpenAIImageService; -import com.dotcms.ai.service.OpenAIImageServiceImpl; +import com.dotcms.ai.api.ImageAPI; +import com.dotcms.ai.api.OpenAIImageAPIImpl; import com.dotcms.ai.util.VelocityContextFactory; +import com.dotcms.api.system.event.message.MessageSeverity; +import com.dotcms.api.system.event.message.MessageType; +import com.dotcms.api.system.event.message.SystemMessageEventUtil; +import com.dotcms.api.system.event.message.builder.SystemMessageBuilder; import com.dotcms.api.web.HttpServletRequestThreadLocal; import com.dotcms.contenttype.model.field.BinaryField; import com.dotcms.contenttype.model.field.Field; import com.dotcms.contenttype.model.type.ContentType; +import com.dotcms.exception.ExceptionUtil; import com.dotcms.rendering.velocity.util.VelocityUtil; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; +import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.dotmarketing.portlets.workflows.model.WorkflowActionClassParameter; import com.dotmarketing.portlets.workflows.model.WorkflowProcessor; @@ -22,6 +28,7 @@ import org.apache.velocity.context.Context; import javax.servlet.http.HttpServletRequest; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -30,60 +37,43 @@ * It implements the AsyncWorkflowRunner interface and overrides its methods to provide the functionality needed. * This class is designed to handle long-running tasks in a separate thread and needs to be serialized to a persistent storage. */ -public class OpenAIGenerateImageRunner implements AsyncWorkflowRunner { +public class OpenAIGenerateImageRunner implements Runnable { - final String identifier; - final long language; - final User user; - final String prompt; - final boolean overwriteField; - final String fieldToWrite; - final long runAt; + private final User user; + private final String prompt; + private final boolean overwriteField; + private final String fieldToWrite; + private final Contentlet contentlet; - OpenAIGenerateImageRunner(WorkflowProcessor processor, Map params) { + + public OpenAIGenerateImageRunner(final WorkflowProcessor processor, final Map params) { this( processor.getContentlet(), processor.getUser(), params.get(OpenAIParams.OPEN_AI_PROMPT.key).getValue(), Try.of(() -> Boolean.parseBoolean(params.get(OpenAIParams.OVERWRITE_FIELDS.key).getValue())) .getOrElse(false), - params.get(OpenAIParams.FIELD_TO_WRITE.key).getValue(), - Try.of(() -> Integer.parseInt(params.get(OpenAIParams.RUN_DELAY.key).getValue())).getOrElse(5) + params.get(OpenAIParams.FIELD_TO_WRITE.key).getValue() ); } - OpenAIGenerateImageRunner(final Contentlet contentlet, + public OpenAIGenerateImageRunner(final Contentlet contentlet, final User user, final String prompt, final boolean overwriteField, - final String fieldToWrite, - final int runDelay) { - this.identifier = contentlet.getIdentifier(); - this.language = contentlet.getLanguageId(); + final String fieldToWrite) { + + this.contentlet = contentlet; this.prompt = prompt; this.overwriteField = overwriteField; this.fieldToWrite = fieldToWrite; this.user = user; - this.runAt = System.currentTimeMillis() + runDelay; - } - - @Override - public long getRunAt() { - return this.runAt; - } - - @Override - public String getIdentifier() { - return this.identifier; } @Override - public long getLanguage() { - return this.language; - } + public void run() { - public void runInternal() { - final Contentlet workingContentlet = getLatest(identifier, language, user); + final Contentlet workingContentlet = this.contentlet; final Host host = Try.of( () -> APILocator.getHostAPI().find(workingContentlet.getHost(), APILocator.systemUser(), true)) .getOrElse(APILocator.systemHost()); @@ -120,13 +110,13 @@ public void runInternal() { } final String finalPrompt = VelocityUtil.eval(prompt, ctx); - final OpenAIImageService service = new OpenAIImageServiceImpl( + final ImageAPI imageAPI = APILocator.getDotAIAPI().getImageAPI( ConfigService.INSTANCE.config(host), user, APILocator.getHostAPI(), APILocator.getTempFileAPI()); - final JSONObject resp = Try.of(() -> service.sendTextPrompt(finalPrompt)) + final JSONObject resp = Try.of(() -> imageAPI.sendTextPrompt(finalPrompt)) .onFailure(e -> Logger.warn(OpenAIGenerateImageRunner.class, "error generating image:" + e)) .getOrElse(JSONObject::new); @@ -138,9 +128,7 @@ public void runInternal() { return; } - final Contentlet contentToSave = checkoutLatest(identifier, language, user); - contentToSave.setProperty(fieldToTry.get().variable(), tempFile); - saveContentlet(contentToSave, user); + contentlet.setProperty(fieldToTry.get().variable(), tempFile); } catch (Exception e) { handleError(e, user); } finally{ @@ -150,7 +138,29 @@ public void runInternal() { } } + /** + * Handles any exceptions that occur during the execution of the workflow. + * It logs the error, sends a system message to the user, and rethrows the exception as a DotRuntimeException. + * + * @param e the exception that occurred. + * @param user the user who is running the workflow. + * @throws DotRuntimeException if an exception occurs during the execution of the workflow. + */ + private void handleError(final Exception e, final User user) { + + final String errorMsg = String.format("Error: %s", ExceptionUtil.getErrorMessage(e)); + final SystemMessageBuilder message = new SystemMessageBuilder() + .setMessage(errorMsg) + .setLife(5000) + .setType(MessageType.SIMPLE_MESSAGE) + .setSeverity(MessageSeverity.ERROR); + SystemMessageEventUtil.getInstance().pushMessage(message.create(), List.of(user.getUserId())); + Logger.error(this.getClass(), errorMsg, e); + throw new DotRuntimeException(e); + } + private Optional resolveField(final Contentlet contentlet) { + final ContentType type = contentlet.getContentType(); final Optional fieldToTry = Try.of(() -> type.fieldMap().get(this.fieldToWrite)).toJavaOptional(); if (UtilMethods.isSet(this.fieldToWrite)) { diff --git a/dotCMS/src/main/java/com/dotcms/listeners/SessionMonitor.java b/dotCMS/src/main/java/com/dotcms/listeners/SessionMonitor.java index 74120fa57866..90dba56eac3b 100644 --- a/dotCMS/src/main/java/com/dotcms/listeners/SessionMonitor.java +++ b/dotCMS/src/main/java/com/dotcms/listeners/SessionMonitor.java @@ -96,10 +96,12 @@ public void attributeReplaced(HttpSessionBindingEvent event) { public void sessionCreated(final HttpSessionEvent event) { // Not implemented + Logger.debug(this, "Session created"); } public void sessionDestroyed(final HttpSessionEvent event) { + Logger.debug(this, "Session destroyed"); final String userId = (String) event.getSession().getAttribute(com.liferay.portal.util.WebKeys.USER_ID); if (userId != null) { diff --git a/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIChatServiceImplTest.java b/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIChatServiceImplTest.java index e110608dbb2c..e4c43486c3f1 100644 --- a/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIChatServiceImplTest.java +++ b/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIChatServiceImplTest.java @@ -1,5 +1,7 @@ package com.dotcms.ai.service; +import com.dotcms.ai.api.ChatAPI; +import com.dotcms.ai.api.OpenAIChatAPIImpl; import com.dotcms.ai.app.AIModel; import com.dotcms.ai.app.AIModelType; import com.dotcms.ai.app.AppConfig; @@ -21,7 +23,7 @@ public class OpenAIChatServiceImplTest { "{\"data\":[{\"url\":\"http://localhost:8080\",\"value\":\"this is a response\"}]}"; private AppConfig config; - private OpenAIChatService service; + private ChatAPI service; @Before public void setUp() { @@ -51,10 +53,12 @@ public void test_sendTextPrompt() { assertNotNull(result); } - private OpenAIChatService prepareService(final String response) { - return new OpenAIChatServiceImpl(config) { + private ChatAPI prepareService(final String response) { + return new OpenAIChatAPIImpl(config) { + + @Override - String doRequest(final String urlIn, final JSONObject json) { + public String doRequest(final String urlIn, final JSONObject json) { return response; } }; diff --git a/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIImageServiceImplTest.java b/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIImageServiceImplTest.java index 1338b3110c74..6c3fc6822473 100644 --- a/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIImageServiceImplTest.java +++ b/dotCMS/src/test/java/com/dotcms/ai/service/OpenAIImageServiceImplTest.java @@ -1,5 +1,7 @@ package com.dotcms.ai.service; +import com.dotcms.ai.api.ImageAPI; +import com.dotcms.ai.api.OpenAIImageAPIImpl; import com.dotcms.ai.app.AIModel; import com.dotcms.ai.app.AIModelType; import com.dotcms.ai.app.AppConfig; @@ -37,7 +39,7 @@ public class OpenAIImageServiceImplTest { private HostAPI hostApi; private TempFileAPI tempFileApi; private AIImageRequestDTO.Builder dtoBuilder; - private OpenAIImageService service; + private ImageAPI service; @Before public void setUp() { @@ -112,7 +114,7 @@ public void test_sendRequest_withErrorWhenGeneratingFileName() throws Exception final JSONObject jsonObject = prepareJsonObject("Hello World!"); final StopWordsUtil stopWordsUtil = mock(StopWordsUtil.class); - OpenAIImageServiceImpl.setStopWordsUtil(stopWordsUtil); + OpenAIImageAPIImpl.setStopWordsUtil(stopWordsUtil); when(stopWordsUtil.removeStopWords(anyString())).thenThrow(RuntimeException.class); final JSONObject result = service.sendRequest(jsonObject); @@ -197,21 +199,21 @@ private static void assertImageResponse(JSONObject result, boolean result1) { assertTrue(result.containsKey("tempFile")); } - private OpenAIImageService prepareService(final String response, - final User user) { - return new OpenAIImageServiceImpl(config, user, hostApi, tempFileApi) { + private ImageAPI prepareService(final String response, + final User user) { + return new OpenAIImageAPIImpl(config, user, hostApi, tempFileApi) { @Override - String doRequest(final String urlIn, final JSONObject json) { + public String doRequest(final String urlIn, final JSONObject json) { return response; } @Override - User getUser() { + public User getUser() { return user; } @Override - AIImageRequestDTO.Builder getDtoBuilder() { + public AIImageRequestDTO.Builder getDtoBuilder() { return dtoBuilder; } }; @@ -241,4 +243,4 @@ private JSONObject prepareJsonObject(final String prompt) throws Exception { return prepareJsonObject(prompt, false); } -} \ No newline at end of file +} diff --git a/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java b/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java index fbeac0706673..e2cc7ed38e3e 100644 --- a/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java +++ b/dotcms-integration/src/test/java/com/dotcms/MainSuite1a.java @@ -1,5 +1,6 @@ package com.dotcms; +import com.dotcms.ai.workflow.OpenAIGenerateImageActionletTest; import com.dotcms.content.elasticsearch.business.ESContentletAPIImplTest; import com.dotcms.contenttype.business.SiteAndFolderResolverImplTest; import com.dotcms.enterprise.publishing.remote.PushPublishBundleGeneratorTest; @@ -103,7 +104,8 @@ ExperimentUrlPatternCalculatorIntegrationTest.class, JsEngineTest.class, Task240306MigrateLegacyLanguageVariablesTest.class, - EmailActionletTest.class + EmailActionletTest.class, + OpenAIGenerateImageActionletTest.class }) public class MainSuite1a { 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 new file mode 100644 index 000000000000..76a172a0b788 --- /dev/null +++ b/dotcms-integration/src/test/java/com/dotcms/ai/workflow/OpenAIGenerateImageActionletTest.java @@ -0,0 +1,223 @@ +package com.dotcms.ai.workflow; + +import com.dotcms.ai.AiTest; +import com.dotcms.ai.api.DotAIAPIFacadeImpl; +import com.dotcms.ai.api.ImageAPI; +import com.dotcms.ai.api.ImageAPIProvider; +import com.dotcms.ai.app.AppConfig; +import com.dotcms.ai.app.AppKeys; +import com.dotcms.ai.model.AIImageRequestDTO; +import com.dotcms.contenttype.model.field.BinaryField; +import com.dotcms.contenttype.model.field.Field; +import com.dotcms.contenttype.model.field.StoryBlockField; +import com.dotcms.contenttype.model.type.ContentType; +import com.dotcms.datagen.ContentTypeDataGen; +import com.dotcms.datagen.FieldDataGen; +import com.dotcms.datagen.SiteDataGen; +import com.dotcms.security.apps.Secret; +import com.dotcms.security.apps.Type; +import com.dotcms.util.IntegrationTestInitService; +import com.dotmarketing.beans.Host; +import com.dotmarketing.business.APILocator; +import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.dotmarketing.portlets.workflows.model.WorkflowActionClassParameter; +import com.dotmarketing.portlets.workflows.model.WorkflowProcessor; +import com.dotmarketing.util.UUIDGenerator; +import com.dotmarketing.util.json.JSONObject; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * This class contains unit tests for the {@link OpenAIGenerateImageActionlet} class. + * + * @author jsanca + */ +public class OpenAIGenerateImageActionletTest { + + private static AppConfig config; + private static Host host; + + @BeforeClass + public static void beforeClass() throws Exception { + IntegrationTestInitService.getInstance().init(); + host = new SiteDataGen().nextPersisted(); + int port = 8080; + final Map secrets = Map.of( + AppKeys.API_URL.key, + Secret.builder() + .withType(Type.STRING) + .withValue(String.format("test", port).toCharArray()) + .build(), + + AppKeys.API_IMAGE_URL.key, + Secret.builder() + .withType(Type.STRING) + .withValue(String.format(AiTest.API_IMAGE_URL, port).toCharArray()) + .build(), + + 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(), + + AppKeys.LISTENER_INDEXER.key, + Secret.builder() + .withType(Type.STRING) + .withValue("{\"default\":\"blog\"}".toCharArray()) + .build()); + config = new AppConfig(host.getHostname(), secrets); + DotAIAPIFacadeImpl.setDefaultImageAPIProvider(new ImageAPIProvider() { + @Override + public ImageAPI getImageAPI(Object... initArguments) { + return new ImageAPI() { + @Override + public JSONObject sendTextPrompt(String prompt) { + return new JSONObject("{\n" + + " \"response\":\"image_id123\"\n" + + "}"); + } + + @Override + public JSONObject sendRawRequest(String prompt) { + return null; + } + + @Override + public JSONObject sendRequest(JSONObject jsonObject) { + return null; + } + + @Override + public JSONObject sendRequest(AIImageRequestDTO dto) { + return null; + } + }; + } + }); + } + + /** + * Method to test: {@link OpenAIGenerateImageActionlet#executeAction(WorkflowProcessor, Map)} + * Given Scenario: Creates a contentlet with a title, binary, but without any prompt + * ExpectedResult: since the prompt is null, should not generate the image + */ + @Test () + public void test_content_no_prompt_do_not_generated() { + // 1) create a content type with title, body and tags + final ContentTypeDataGen dataGen = new ContentTypeDataGen(); + + //Add new fields + final List inputFields = new ArrayList<>(); + inputFields.add(new FieldDataGen().velocityVarName("title").next()); + inputFields.add(new FieldDataGen().type(BinaryField.class).velocityVarName("image").next()); + dataGen.fields(inputFields); + final ContentType contentType = dataGen.nextPersisted(); + + // 2) create an instance with non integer + final Contentlet contentlet = new Contentlet(); + contentlet.setContentType(contentType); + contentlet.setProperty("title", "dotCMS Party"); + contentlet.setIdentifier(UUIDGenerator.generateUuid()); + + final WorkflowProcessor processor = new WorkflowProcessor(contentlet, APILocator.systemUser()); + final Map params = Map.of( + OpenAIParams.OPEN_AI_PROMPT.key, new WorkflowActionClassParameter(""), + OpenAIParams.OVERWRITE_FIELDS.key, new WorkflowActionClassParameter("true"), + OpenAIParams.FIELD_TO_WRITE.key, new WorkflowActionClassParameter("image") + ); + + new OpenAIGenerateImageActionlet().executeAction(processor, params); + + Assert.assertNull("No prompt sent, no image generated",contentlet.get("image")); + } + + /** + * Method to test: {@link OpenAIGenerateImageActionlet#executeAction(WorkflowProcessor, Map)} + * Given Scenario: Creates a contentlet with a title, No binary and NO prompt + * ExpectedResult: since the does not have any prompt do not generated any body + */ + @Test () + public void test_content_without_prompt_field_do_not_generated() { + // 1) create a content type with title, body and tags + final ContentTypeDataGen dataGen = new ContentTypeDataGen(); + + //Add new fields + final List inputFields = new ArrayList<>(); + inputFields.add(new FieldDataGen().velocityVarName("title").next()); + inputFields.add(new FieldDataGen().type(StoryBlockField.class).velocityVarName("body").next()); + dataGen.fields(inputFields); + final ContentType contentType = dataGen.nextPersisted(); + + // 2) create an instance with non integer + final Contentlet contentlet = new Contentlet(); + contentlet.setContentType(contentType); + contentlet.setProperty("title", "dotCMS Party"); + contentlet.setIdentifier(UUIDGenerator.generateUuid()); + + final WorkflowProcessor processor = new WorkflowProcessor(contentlet, APILocator.systemUser()); + final Map params = Map.of( + OpenAIParams.OPEN_AI_PROMPT.key, new WorkflowActionClassParameter("Create an image about dotCMS party"), + OpenAIParams.OVERWRITE_FIELDS.key, new WorkflowActionClassParameter("true"), + OpenAIParams.FIELD_TO_WRITE.key, new WorkflowActionClassParameter("image") + ); + + new OpenAIGenerateImageActionlet().executeAction(processor, params); + final Object object = contentlet.get("body"); + Assert.assertNull("When not binary field as part of the contentlet, should not generate the image", object); + } + + + /** + * Method to test: {@link OpenAIContentPromptActionlet#executeAction(WorkflowProcessor, Map)} + * Given Scenario: Creates a contentlet with a title, body + * ExpectedResult: The body should be generated + */ + @Test + public void test_body_generation() { + // 1) create a content type with title, body and tags + final ContentTypeDataGen dataGen = new ContentTypeDataGen(); + + //Add new fields + final List inputFields = new ArrayList<>(); + inputFields.add(new FieldDataGen().velocityVarName("title").next()); + inputFields.add(new FieldDataGen().type(BinaryField.class).velocityVarName("image").next()); + + dataGen.fields(inputFields); + final ContentType contentType = dataGen.nextPersisted(); + // 2) create an instance with some text and publish it + final Contentlet contentlet = new Contentlet(); + contentlet.setContentType(contentType); + contentlet.setProperty("title", "Write an article about dotCMS"); + contentlet.setIdentifier(UUIDGenerator.generateUuid()); + contentlet.setHost(host.getIdentifier()); + + // 3) Run the actionlet with the content + final WorkflowProcessor processor = new WorkflowProcessor(contentlet, APILocator.systemUser()); + final Map params = Map.of( + OpenAIParams.OPEN_AI_PROMPT.key, new WorkflowActionClassParameter("Create an image about dotCMS party"), + OpenAIParams.OVERWRITE_FIELDS.key, new WorkflowActionClassParameter("true"), + OpenAIParams.FIELD_TO_WRITE.key, new WorkflowActionClassParameter("image") + ); + + new OpenAIGenerateImageActionlet().executeAction(processor, params); + + final Object bodyObject = contentlet.get("image"); + Assert.assertNotNull("Body returned can not be null",bodyObject); + Assert.assertTrue("Body returned should be a String",bodyObject instanceof CharSequence); + final CharSequence body = (CharSequence) bodyObject; + Assert.assertEquals("Body returned should be not empty", body, "image_id123"); + } + +}