From 4ca4f2cc00b3d91b0fc07aa05f1163888d3f69fd Mon Sep 17 00:00:00 2001 From: SatyamSah5 Date: Tue, 2 Sep 2025 12:29:56 +0530 Subject: [PATCH] Initial commit for AI Summary Action Executor --- .../actions/ActionNodeParameterValidator.java | 11 +- .../executer/AISummaryActionExecuter.java | 189 ++++++++++++++++++ .../alfresco/action-services-context.xml | 13 ++ .../repo/action/ActionServiceImpl2Test.java | 34 +++- 4 files changed, 233 insertions(+), 14 deletions(-) create mode 100644 repository/src/main/java/org/alfresco/repo/action/executer/AISummaryActionExecuter.java diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/validator/actions/ActionNodeParameterValidator.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/validator/actions/ActionNodeParameterValidator.java index 5ebb36e827..562003941a 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/validator/actions/ActionNodeParameterValidator.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/validator/actions/ActionNodeParameterValidator.java @@ -39,17 +39,10 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import org.alfresco.repo.action.executer.*; import org.apache.commons.collections.MapUtils; import org.apache.logging.log4j.util.Strings; -import org.alfresco.repo.action.executer.CheckOutActionExecuter; -import org.alfresco.repo.action.executer.CopyActionExecuter; -import org.alfresco.repo.action.executer.ImageTransformActionExecuter; -import org.alfresco.repo.action.executer.ImporterActionExecuter; -import org.alfresco.repo.action.executer.LinkCategoryActionExecuter; -import org.alfresco.repo.action.executer.MoveActionExecuter; -import org.alfresco.repo.action.executer.SimpleWorkflowActionExecuter; -import org.alfresco.repo.action.executer.TransformActionExecuter; import org.alfresco.rest.api.Actions; import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.actions.ActionValidator; @@ -115,7 +108,7 @@ public class ActionNodeParameterValidator implements ActionValidator { return List.of(CopyActionExecuter.NAME, MoveActionExecuter.NAME, CheckOutActionExecuter.NAME, ImporterActionExecuter.NAME, LinkCategoryActionExecuter.NAME, SimpleWorkflowActionExecuter.NAME, TransformActionExecuter.NAME, - ImageTransformActionExecuter.NAME); + ImageTransformActionExecuter.NAME, AISummaryActionExecuter.NAME); } @Override diff --git a/repository/src/main/java/org/alfresco/repo/action/executer/AISummaryActionExecuter.java b/repository/src/main/java/org/alfresco/repo/action/executer/AISummaryActionExecuter.java new file mode 100644 index 0000000000..29b6cc8bdf --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/action/executer/AISummaryActionExecuter.java @@ -0,0 +1,189 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2019 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.action.executer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.rendition2.SynchronousTransformClient; +import org.alfresco.repo.rendition2.TransformationOptionsConverter; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.*; +import org.alfresco.service.cmr.rule.RuleServiceException; +import org.alfresco.service.namespace.QName; + +import static org.alfresco.service.namespace.NamespaceService.CONTENT_MODEL_1_0_URI; + +public class AISummaryActionExecuter extends ActionExecuterAbstractBase +{ + public static final String NAME = "ai-summary"; + private static final String TARGET_MIMETYPE = "text/plain"; + + private DictionaryService dictionaryService; + private ContentService contentService; + private NodeService nodeService; + private SynchronousTransformClient synchronousTransformClient; + private TransformationOptionsConverter converter; + + public void setDictionaryService(DictionaryService dictionaryService) { + this.dictionaryService = dictionaryService; + } + + public void setContentService(ContentService contentService) { + this.contentService = contentService; + } + + public void setNodeService(NodeService nodeService) { + this.nodeService = nodeService; + } + + public void setSynchronousTransformClient(SynchronousTransformClient synchronousTransformClient) { + this.synchronousTransformClient = synchronousTransformClient; + } + + public void setConverter(TransformationOptionsConverter converter) { + this.converter = converter; + } + + @Override + protected void addParameterDefinitions(List paramList) { + // No extra parameters for this action + } + + @Override + protected void executeImpl(Action ruleAction, NodeRef actionedUponNodeRef) + { + if (!this.nodeService.exists(actionedUponNodeRef)) + { + // node doesn't exist - can't do anything + return; + } + // First check that the node is a sub-type of content + QName typeQName = this.nodeService.getType(actionedUponNodeRef); + if (!this.dictionaryService.isSubClass(typeQName, ContentModel.TYPE_CONTENT)) + { + // it is not content, so can't transform + return; + } + + ContentReader contentReader = this.contentService.getReader(actionedUponNodeRef, ContentModel.PROP_CONTENT); + if (contentReader == null || !contentReader.exists()) { + throw new RuleServiceException("Content Reader not found."); + } + + String sourceMimetype = contentReader.getMimetype(); + long sourceSizeInBytes = contentReader.getSize(); + String contentUrl = contentReader.getContentUrl(); + + TransformationOptions transformationOptions = new TransformationOptions( + actionedUponNodeRef, ContentModel.PROP_NAME, null, ContentModel.PROP_NAME); + transformationOptions.setUse(Thread.currentThread().getName().contains("Async") ? "asyncRule" : "syncRule"); + + Map options = converter.getOptions(transformationOptions, sourceMimetype, TARGET_MIMETYPE); + + if (!synchronousTransformClient.isSupported(sourceMimetype, sourceSizeInBytes, + contentUrl, TARGET_MIMETYPE, options, null, actionedUponNodeRef)) { + throw new RuleServiceException("No transformer for " + sourceMimetype + " -> " + TARGET_MIMETYPE); + } + + // Write transformed content to a temp writer + ContentWriter tempWriter = contentService.getTempWriter(); + tempWriter.setMimetype(TARGET_MIMETYPE); + tempWriter.setEncoding(contentReader.getEncoding()); + + synchronousTransformClient.transform(contentReader, tempWriter, options, null, actionedUponNodeRef); + + // Read transformed content as plain text + ContentReader txtReader = tempWriter.getReader(); + try (InputStream is = txtReader.getContentInputStream()) { + String textString = new String(is.readAllBytes()); + String aiResult = sendToAIEndpoint(is); + QName AI_SUMMARY_PROP = QName.createQName(CONTENT_MODEL_1_0_URI, "AiSummary"); + nodeService.setProperty(actionedUponNodeRef, AI_SUMMARY_PROP, aiResult); + // Optionally, store or log the result + } catch (Exception e) { + throw new RuleServiceException("AI endpoint call failed: " + e.getMessage(), e); + } + } + + /** + * Placeholder for sending content to an AI endpoint. + * Implement actual HTTP call or integration as needed. + */ + private String sendToAIEndpoint(InputStream txtContent) throws Exception { + // Example: read content, send to AI, return result + // Implement actual HTTP client logic here + // Read input stream to string +// String inputText = new String(txtContent.readAllBytes(), StandardCharsets.UTF_8); +// +// // Prepare JSON payload +// String payload = "{ \"inputs\": " + escapeJson(inputText) + " }"; +// +// // Create connection +// URL url = new URL("https://api-inference.huggingface.co/models/gpt2"); +// HttpURLConnection conn = (HttpURLConnection) url.openConnection(); +// conn.setRequestMethod("POST"); +// conn.setRequestProperty("Authorization", "Bearer " + HUGGING_FACE_TOKEN); +// conn.setRequestProperty("Content-Type", "application/json"); +// conn.setDoOutput(true); +// +// // Send request +// try (OutputStream os = conn.getOutputStream()) { +// os.write(payload.getBytes(StandardCharsets.UTF_8)); +// } +// +// // Read response +// int status = conn.getResponseCode(); +// InputStream responseStream = (status >= 200 && status < 300) ? conn.getInputStream() : conn.getErrorStream(); +// String response = new String(responseStream.readAllBytes(), StandardCharsets.UTF_8); +// +// // Simple extraction of generated text (not robust, for demo) +// int start = response.indexOf("\"generated_text\":\""); +// if (start != -1) { +// start += 18; +// int end = response.indexOf("\"", start); +// if (end != -1) { +// return response.substring(start, end); +// } +// } +// return "AI summary unavailable"; + return "AI summary or insights result"; + } + + // Helper to escape JSON string + private String escapeJson(String text) + { + return "\"" + text.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\""; + } +} diff --git a/repository/src/main/resources/alfresco/action-services-context.xml b/repository/src/main/resources/alfresco/action-services-context.xml index 632f0707be..3ce48201f1 100644 --- a/repository/src/main/resources/alfresco/action-services-context.xml +++ b/repository/src/main/resources/alfresco/action-services-context.xml @@ -449,6 +449,19 @@ + + + + + + + + + {http://www.alfresco.org/model/content/1.0}content + + + + diff --git a/repository/src/test/java/org/alfresco/repo/action/ActionServiceImpl2Test.java b/repository/src/test/java/org/alfresco/repo/action/ActionServiceImpl2Test.java index bc27390f90..b74238d544 100644 --- a/repository/src/test/java/org/alfresco/repo/action/ActionServiceImpl2Test.java +++ b/repository/src/test/java/org/alfresco/repo/action/ActionServiceImpl2Test.java @@ -43,6 +43,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.alfresco.repo.action.executer.*; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -51,11 +52,6 @@ import org.junit.Rule; import org.junit.Test; import org.alfresco.model.ContentModel; -import org.alfresco.repo.action.executer.ActionExecuter; -import org.alfresco.repo.action.executer.ContentMetadataExtracter; -import org.alfresco.repo.action.executer.CounterIncrementActionExecuter; -import org.alfresco.repo.action.executer.ScriptActionExecuter; -import org.alfresco.repo.action.executer.TransformActionExecuter; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.transform.AbstractContentTransformerTest; import org.alfresco.repo.security.authentication.AuthenticationUtil; @@ -241,6 +237,34 @@ public class ActionServiceImpl2Test }); } + @Test + public void testAISummaryActionExecuterIntegration() throws Exception + { + final File file = loadAndAddQuickFileAsManager(testNode, "quick.pdf", MimetypeMap.MIMETYPE_PDF); + assertNotNull("Failed to load required test file.", file); +// // Add plain text content to the test node +// AuthenticationUtil.setFullyAuthenticatedUser(testSiteAndMemberInfo.siteManager); +// transactionHelper.doInTransaction(() -> { +// ContentWriter writer = contentService.getWriter(testNode, ContentModel.PROP_CONTENT, true); +// writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); +// writer.putContent("This is a test document for AI summary."); +// return null; +// }); + + // Execute the ai-summary action + AuthenticationUtil.setFullyAuthenticatedUser(testSiteAndMemberInfo.siteManager); + transactionHelper.doInTransaction(() -> { + Action aiSummaryAction = actionService.createAction(AISummaryActionExecuter.NAME); + actionService.executeAction(aiSummaryAction, testNode); + return null; + }); + + // Assert the summary property is set + Serializable summary = nodeService.getProperty(testNode, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "AiSummary")); + assertNotNull("AI summary property should be set", summary); + assertEquals("AI summary or insights result", summary); + } + @Test public void testParameterConstraints() throws Exception {