Initial commit for AI Summary Action Executor

This commit is contained in:
SatyamSah5
2025-09-02 12:29:56 +05:30
parent d20e8ee158
commit 4ca4f2cc00
4 changed files with 233 additions and 14 deletions

View File

@@ -39,17 +39,10 @@ import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.alfresco.repo.action.executer.*;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
import org.apache.logging.log4j.util.Strings; 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.Actions;
import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.Nodes;
import org.alfresco.rest.api.actions.ActionValidator; 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, return List.of(CopyActionExecuter.NAME, MoveActionExecuter.NAME, CheckOutActionExecuter.NAME, ImporterActionExecuter.NAME,
LinkCategoryActionExecuter.NAME, SimpleWorkflowActionExecuter.NAME, TransformActionExecuter.NAME, LinkCategoryActionExecuter.NAME, SimpleWorkflowActionExecuter.NAME, TransformActionExecuter.NAME,
ImageTransformActionExecuter.NAME); ImageTransformActionExecuter.NAME, AISummaryActionExecuter.NAME);
} }
@Override @Override

View File

@@ -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 <http://www.gnu.org/licenses/>.
* #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<ParameterDefinition> 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<String, String> 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") + "\"";
}
}

View File

@@ -449,6 +449,19 @@
</property> </property>
</bean> </bean>
<bean id="ai-summary" class="org.alfresco.repo.action.executer.AISummaryActionExecuter" parent="action-executer">
<property name="dictionaryService" ref="dictionaryService" />
<property name="contentService" ref="ContentService" />
<property name="nodeService" ref="NodeService" />
<property name="synchronousTransformClient" ref="synchronousTransformClient" />
<property name="converter" ref="transformOptionsConverter" />
<property name="applicableTypes">
<list>
<value>{http://www.alfresco.org/model/content/1.0}content</value>
</list>
</property>
</bean>
<bean id="transform-image" class="org.alfresco.repo.action.executer.ImageTransformActionExecuter" <bean id="transform-image" class="org.alfresco.repo.action.executer.ImageTransformActionExecuter"
parent="transform"> parent="transform">
</bean> </bean>

View File

@@ -43,6 +43,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.alfresco.repo.action.executer.*;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass; import org.junit.BeforeClass;
@@ -51,11 +52,6 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.alfresco.model.ContentModel; 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.MimetypeMap;
import org.alfresco.repo.content.transform.AbstractContentTransformerTest; import org.alfresco.repo.content.transform.AbstractContentTransformerTest;
import org.alfresco.repo.security.authentication.AuthenticationUtil; 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 @Test
public void testParameterConstraints() throws Exception public void testParameterConstraints() throws Exception
{ {