mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-10-08 14:51:49 +00:00
Compare commits
7 Commits
25.3.0.44
...
feature/20
Author | SHA1 | Date | |
---|---|---|---|
|
31f20033da | ||
|
92eb9ee90e | ||
|
590d455a7e | ||
|
79ffacfa42 | ||
|
9bbcd9b023 | ||
|
f1be2a07c6 | ||
|
4ca4f2cc00 |
@@ -457,4 +457,7 @@ public interface ContentModel
|
|||||||
static final QName PROP_CASCADE_CRC = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "cascadeCRC");
|
static final QName PROP_CASCADE_CRC = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "cascadeCRC");
|
||||||
static final QName PROP_CASCADE_TX = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "cascadeTx");
|
static final QName PROP_CASCADE_TX = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "cascadeTx");
|
||||||
|
|
||||||
|
static final QName ASPECT_AI = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "aillm");
|
||||||
|
static final QName PROP_AI_RESPONSE = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "airesponse");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -42,14 +42,7 @@ import java.util.stream.Collectors;
|
|||||||
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.*;
|
||||||
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
|
||||||
|
@@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* #%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.action.ParameterDefinitionImpl;
|
||||||
|
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.DataTypeDefinition;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class AISummaryActionExecuter extends ActionExecuterAbstractBase
|
||||||
|
{
|
||||||
|
public static final String NAME = "ai-action";
|
||||||
|
private static final String TARGET_MIMETYPE = "text/plain";
|
||||||
|
private String AI_ENDPOINT_URL;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAI_ENDPOINT_URL(String aiUrl)
|
||||||
|
{
|
||||||
|
this.AI_ENDPOINT_URL = aiUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addParameterDefinitions(List<ParameterDefinition> paramList)
|
||||||
|
{
|
||||||
|
// No extra parameters for this action
|
||||||
|
paramList.add(new ParameterDefinitionImpl("Prompt", DataTypeDefinition.TEXT,
|
||||||
|
true, getParamDisplayLabel("Prompt")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
|
||||||
|
// Get the AI prompt from action parameters
|
||||||
|
String aiPrompt = (String) ruleAction.getParameterValue("Prompt");
|
||||||
|
// Read transformed content as plain text
|
||||||
|
ContentReader txtReader = tempWriter.getReader();
|
||||||
|
try (InputStream is = txtReader.getContentInputStream())
|
||||||
|
{
|
||||||
|
String textString = new String(is.readAllBytes());
|
||||||
|
String aiResult = sendToAIEndpoint(textString, aiPrompt);
|
||||||
|
nodeService.setProperty(actionedUponNodeRef, ContentModel.PROP_AI_RESPONSE, aiResult);
|
||||||
|
if (!nodeService.hasAspect(actionedUponNodeRef, ContentModel.ASPECT_AI))
|
||||||
|
{
|
||||||
|
nodeService.addAspect(actionedUponNodeRef, ContentModel.ASPECT_AI, null);
|
||||||
|
}
|
||||||
|
// 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(String txtContent, String prompt) throws Exception
|
||||||
|
{
|
||||||
|
// Read input stream to string
|
||||||
|
|
||||||
|
// Build JSON payload
|
||||||
|
String payload = "{"
|
||||||
|
+ "\"context\": " + escapeJson(txtContent.trim()) + ","
|
||||||
|
+ "\"prompt\": " + escapeJson(prompt.trim())
|
||||||
|
+ "}";
|
||||||
|
|
||||||
|
// Create connection
|
||||||
|
URL url = new URL(AI_ENDPOINT_URL);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
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 jsonResponse = new String(responseStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
// Parse JSON and extract the "response" field
|
||||||
|
org.json.JSONObject obj = new org.json.JSONObject(jsonResponse);
|
||||||
|
String summary = obj.optString("response", "");
|
||||||
|
return summary.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to escape JSON string
|
||||||
|
private String escapeJson(String text)
|
||||||
|
{
|
||||||
|
return "\"" + text.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n") + "\"";
|
||||||
|
}
|
||||||
|
}
|
@@ -449,6 +449,20 @@
|
|||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean id="ai-action" 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="AI_ENDPOINT_URL" value="${ai.endpoint.url}" />
|
||||||
|
<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>
|
||||||
|
@@ -561,6 +561,23 @@
|
|||||||
|
|
||||||
|
|
||||||
<aspects>
|
<aspects>
|
||||||
|
<aspect name="cm:aillm">
|
||||||
|
<title>AI Insights</title>
|
||||||
|
<properties>
|
||||||
|
<property name="cm:airesponse">
|
||||||
|
<title>AI Response</title>
|
||||||
|
<type>d:mltext</type>
|
||||||
|
<protected>true</protected>
|
||||||
|
<mandatory>false</mandatory>
|
||||||
|
<index enabled="true">
|
||||||
|
<atomic>true</atomic>
|
||||||
|
<stored>false</stored>
|
||||||
|
<tokenised>both</tokenised>
|
||||||
|
<facetable>true</facetable>
|
||||||
|
</index>
|
||||||
|
</property>
|
||||||
|
</properties>
|
||||||
|
</aspect>
|
||||||
|
|
||||||
<aspect name="cm:titled">
|
<aspect name="cm:titled">
|
||||||
<title>Titled</title>
|
<title>Titled</title>
|
||||||
|
@@ -1045,6 +1045,9 @@ content.transformer.retryOn.different.mimetype=true
|
|||||||
transformer.debug.entries=0
|
transformer.debug.entries=0
|
||||||
transformer.log.entries=50
|
transformer.log.entries=50
|
||||||
|
|
||||||
|
# AI Endpoint
|
||||||
|
ai.endpoint.url=http://alfresco-llm-ai:5000/api/respond
|
||||||
|
|
||||||
#
|
#
|
||||||
# Lock timeout configuration
|
# Lock timeout configuration
|
||||||
#
|
#
|
||||||
|
@@ -51,11 +51,7 @@ 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.*;
|
||||||
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
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user