diff --git a/server/ee/libs/platform/platform-component/platform-component-remote-client/src/main/java/com/bytechef/ee/platform/component/remote/client/service/RemoteClusterElementDefinitionServiceClient.java b/server/ee/libs/platform/platform-component/platform-component-remote-client/src/main/java/com/bytechef/ee/platform/component/remote/client/service/RemoteClusterElementDefinitionServiceClient.java index 6d2033b4703..ac8ad2bfc3d 100644 --- a/server/ee/libs/platform/platform-component/platform-component-remote-client/src/main/java/com/bytechef/ee/platform/component/remote/client/service/RemoteClusterElementDefinitionServiceClient.java +++ b/server/ee/libs/platform/platform-component/platform-component-remote-client/src/main/java/com/bytechef/ee/platform/component/remote/client/service/RemoteClusterElementDefinitionServiceClient.java @@ -81,6 +81,14 @@ public Object executeTool( throw new UnsupportedOperationException(); } + @Override + public Object executeTool( + String componentName, int componentVersion, String clusterElementName, Map inputParameters, + Map extensions, Map componentConnections, boolean editorEnvironment) { + + throw new UnsupportedOperationException(); + } + @Override public String executeWorkflowNodeDescription( String componentName, int componentVersion, String clusterElementName, Map inputParameters) { diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java index 40d8d700013..7bcaf9e447d 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/action/AbstractAiAgentChatAction.java @@ -39,6 +39,7 @@ import com.bytechef.platform.component.definition.ai.agent.ChatMemoryFunction; import com.bytechef.platform.component.definition.ai.agent.GuardrailsFunction; import com.bytechef.platform.component.definition.ai.agent.ModelFunction; +import com.bytechef.platform.component.definition.ai.agent.MultipleConnectionsToolFunction; import com.bytechef.platform.component.definition.ai.agent.RagFunction; import com.bytechef.platform.component.definition.ai.agent.ToolCallbackProviderFunction; import com.bytechef.platform.component.service.ClusterElementDefinitionService; @@ -249,6 +250,9 @@ private List getToolCallbacks( } catch (Exception exception) { throw new RuntimeException(exception); } + } else if (clusterElementFunction instanceof MultipleConnectionsToolFunction) { + toolCallbacks.add( + aiAgentToolFacade.getFunctionToolCallback(clusterElement, connectionParameters, editorEnvironment)); } else { ComponentConnection componentConnection = connectionParameters.get( clusterElement.getWorkflowNodeName()); diff --git a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/facade/AiAgentToolFacade.java b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/facade/AiAgentToolFacade.java index 4275f84abcd..2e1e94fbb34 100644 --- a/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/facade/AiAgentToolFacade.java +++ b/server/libs/modules/components/ai/agent/src/main/java/com/bytechef/component/ai/agent/facade/AiAgentToolFacade.java @@ -86,6 +86,42 @@ public ToolCallback getFunctionToolCallback( return builder.build(); } + public ToolCallback getFunctionToolCallback( + ClusterElement clusterElement, Map componentConnections, + boolean editorEnvironment) { + + ClusterElementDefinition clusterElementDefinition = + clusterElementDefinitionService.getClusterElementDefinition( + clusterElement.getComponentName(), clusterElement.getComponentVersion(), + clusterElement.getClusterElementName()); + + Map toolParameters = clusterElement.getParameters(); + + List fromAiResults = extractFromAiResults(toolParameters); + + FunctionToolCallback.Builder, Object> builder = FunctionToolCallback.builder( + getToolName(clusterElementDefinition.getComponentName(), clusterElementDefinition.getName(), + toolParameters), + getMultipleConnectionsToolCallbackFunction( + clusterElement.getComponentName(), clusterElement.getComponentVersion(), + clusterElementDefinition.getName(), toolParameters, clusterElement.getExtensions(), + componentConnections, editorEnvironment)) + .inputType(Map.class) + .inputSchema(FromAiInputSchemaUtils.generateInputSchema(fromAiResults)); + + String toolDescription = getToolDescription(toolParameters, clusterElement.getExtensions()); + + if (toolDescription == null) { + toolDescription = clusterElementDefinition.getDescription(); + } + + if (toolDescription != null) { + builder.description(toolDescription); + } + + return builder.build(); + } + private Function, Object> getFromAiToolCallbackFunction( String componentName, int componentVersion, String clusterElementName, Map parameters, @Nullable ComponentConnection componentConnection, boolean editorEnvironment) { @@ -103,4 +139,21 @@ private Function, Object> getFromAiToolCallbackFunction( }; } + private Function, Object> getMultipleConnectionsToolCallbackFunction( + String componentName, int componentVersion, String clusterElementName, Map parameters, + Map extensions, Map componentConnections, boolean editorEnvironment) { + + return request -> { + Map resolvedParameters = new HashMap<>(); + + for (Map.Entry entry : parameters.entrySet()) { + resolvedParameters.put(entry.getKey(), resolveParameterValue(entry.getValue(), request)); + } + + return clusterElementDefinitionService.executeTool( + componentName, componentVersion, clusterElementName, MapUtils.concat(request, resolvedParameters), + extensions, componentConnections, editorEnvironment); + }; + } + } diff --git a/server/libs/platform/platform-component/platform-component-api/src/main/java/com/bytechef/platform/component/service/ClusterElementDefinitionService.java b/server/libs/platform/platform-component/platform-component-api/src/main/java/com/bytechef/platform/component/service/ClusterElementDefinitionService.java index aeed33a195b..b083c604572 100644 --- a/server/libs/platform/platform-component/platform-component-api/src/main/java/com/bytechef/platform/component/service/ClusterElementDefinitionService.java +++ b/server/libs/platform/platform-component/platform-component-api/src/main/java/com/bytechef/platform/component/service/ClusterElementDefinitionService.java @@ -61,6 +61,10 @@ Object executeTool( String componentName, int componentVersion, String clusterElementName, Map inputParameters, @Nullable ComponentConnection componentConnection, boolean editorEnvironment); + Object executeTool( + String componentName, int componentVersion, String clusterElementName, Map inputParameters, + Map extensions, Map componentConnections, boolean editorEnvironment); + String executeWorkflowNodeDescription( String componentName, int componentVersion, String clusterElementName, Map inputParameters); diff --git a/server/libs/platform/platform-component/platform-component-service/src/main/java/com/bytechef/platform/component/service/ClusterElementDefinitionServiceImpl.java b/server/libs/platform/platform-component/platform-component-service/src/main/java/com/bytechef/platform/component/service/ClusterElementDefinitionServiceImpl.java index be4ab2f93fd..0b6184c80c1 100644 --- a/server/libs/platform/platform-component/platform-component-service/src/main/java/com/bytechef/platform/component/service/ClusterElementDefinitionServiceImpl.java +++ b/server/libs/platform/platform-component/platform-component-service/src/main/java/com/bytechef/platform/component/service/ClusterElementDefinitionServiceImpl.java @@ -46,6 +46,7 @@ import com.bytechef.platform.component.definition.ClusterRootComponentDefinition; import com.bytechef.platform.component.definition.ParametersFactory; import com.bytechef.platform.component.definition.PropertyFactory; +import com.bytechef.platform.component.definition.ai.agent.MultipleConnectionsToolFunction; import com.bytechef.platform.component.definition.ai.agent.ToolCallbackProviderFunction; import com.bytechef.platform.component.definition.datastream.ClusterElementResolverFunction; import com.bytechef.platform.component.domain.ClusterElementDefinition; @@ -206,6 +207,24 @@ public Object executeTool( clusterElementContext); } + @Override + public Object executeTool( + String componentName, int componentVersion, String clusterElementName, Map inputParameters, + Map extensions, Map componentConnections, boolean editorEnvironment) { + + ComponentConnection firstConnection = componentConnections.isEmpty() + ? null : componentConnections.values() + .iterator() + .next(); + + ClusterElementContext clusterElementContext = contextFactory.createClusterElementContext( + componentName, componentVersion, clusterElementName, firstConnection, editorEnvironment); + + return doExecuteTool( + componentName, componentVersion, clusterElementName, inputParameters, extensions, componentConnections, + clusterElementContext); + } + @Override public String executeWorkflowNodeDescription( String componentName, int componentVersion, String clusterElementName, Map inputParameters) { @@ -464,6 +483,45 @@ private Object doExecuteTool( } } + private Object doExecuteTool( + String componentName, Integer componentVersion, String clusterElementName, Map inputParameterMap, + Map extensionMap, Map componentConnections, + ClusterElementContext context) { + + Object clusterElement = getClusterElement(componentName, componentVersion, clusterElementName); + + Parameters inputParameters = ParametersFactory.create(inputParameterMap); + Parameters connectionParameters = ParametersFactory.create(Map.of()); + Parameters extensions = ParametersFactory.create(extensionMap); + + try { + if (clusterElement instanceof MultipleConnectionsToolFunction multipleConnectionsToolFunction) { + return multipleConnectionsToolFunction.apply( + inputParameters, connectionParameters, extensions, componentConnections, context); + } + + if (clusterElement instanceof ToolCallbackProviderFunction toolCallbackProviderFunction) { + return toolCallbackProviderFunction.apply(inputParameters, connectionParameters, context); + } + + if (clusterElement instanceof ToolFunction toolFunction) { + return toolFunction.apply(inputParameters, connectionParameters, context); + } + + throw new ExecutionException( + "Unsupported cluster element type: " + clusterElement.getClass() + .getName(), + inputParameters, ClusterElementDefinitionErrorType.EXECUTE_PERFORM); + } catch (Exception exception) { + if (exception instanceof ProviderException) { + throw (ProviderException) exception; + } + + throw new ExecutionException( + exception, inputParameterMap, ClusterElementDefinitionErrorType.EXECUTE_PERFORM); + } + } + private String executeWorkflowNodeDescription( String componentName, int componentVersion, String clusterElementName, Map inputParameters, ClusterElementContext context) { diff --git a/server/libs/platform/platform-configuration/platform-configuration-api/build.gradle.kts b/server/libs/platform/platform-configuration/platform-configuration-api/build.gradle.kts index dd18c0458c7..52550c0aeb9 100644 --- a/server/libs/platform/platform-configuration/platform-configuration-api/build.gradle.kts +++ b/server/libs/platform/platform-configuration/platform-configuration-api/build.gradle.kts @@ -12,4 +12,6 @@ dependencies { implementation(project(":server:libs:core:commons:commons-util")) implementation(project(":server:libs:platform:platform-connection:platform-connection-api")) implementation(project(":server:libs:platform:platform-user:platform-user-api")) + + testImplementation(project(":server:libs:test:test-support")) } diff --git a/server/libs/platform/platform-configuration/platform-configuration-api/src/main/java/com/bytechef/platform/configuration/domain/ClusterElementMap.java b/server/libs/platform/platform-configuration/platform-configuration-api/src/main/java/com/bytechef/platform/configuration/domain/ClusterElementMap.java index 8750cbe41a6..5c689aa18fa 100644 --- a/server/libs/platform/platform-configuration/platform-configuration-api/src/main/java/com/bytechef/platform/configuration/domain/ClusterElementMap.java +++ b/server/libs/platform/platform-configuration/platform-configuration-api/src/main/java/com/bytechef/platform/configuration/domain/ClusterElementMap.java @@ -124,19 +124,88 @@ public ClusterElement getClusterElement( .filter(curClusterElement -> Objects.equals( curClusterElement.getWorkflowNodeName(), clusterElementWorkflowNodeName)) .findFirst() + .or(() -> findNestedClusterElement(clusterElementType, clusterElementWorkflowNodeName)) .orElseThrow(() -> new IllegalArgumentException( "Cluster element %s not found".formatted(clusterElementWorkflowNodeName))); } else { - ClusterElement clusterElement = getClusterElement(clusterElementType); + ClusterElement clusterElement = super.get(clusterElementType.key()) instanceof ClusterElement ce + ? ce : null; - if (!Objects.equals(clusterElement.getWorkflowNodeName(), clusterElementWorkflowNodeName)) { - throw new IllegalArgumentException("Cluster element type %s not found".formatted(clusterElementType)); + if (clusterElement == null || + !Objects.equals(clusterElement.getWorkflowNodeName(), clusterElementWorkflowNodeName)) { + + return findNestedClusterElement(clusterElementType, clusterElementWorkflowNodeName) + .orElseThrow(() -> new IllegalArgumentException( + "Cluster element type %s not found".formatted(clusterElementType))); } return clusterElement; } } + private Optional findNestedClusterElement( + ClusterElementType clusterElementType, String clusterElementWorkflowNodeName) { + + for (Entry entry : entrySet) { + Object value = entry.getValue(); + + if (value instanceof ClusterElement clusterElement) { + Optional clusterElementOptional = searchClusterElementInside( + clusterElement, clusterElementType, clusterElementWorkflowNodeName); + + if (clusterElementOptional.isPresent()) { + return clusterElementOptional; + } + } else if (value instanceof List list) { + for (Object item : list) { + if (item instanceof ClusterElement clusterElement) { + Optional clusterElementOptional = searchClusterElementInside( + clusterElement, clusterElementType, clusterElementWorkflowNodeName); + + if (clusterElementOptional.isPresent()) { + return clusterElementOptional; + } + } + } + } + } + + return Optional.empty(); + } + + private static Optional searchClusterElementInside( + ClusterElement clusterElement, ClusterElementType clusterElementType, String clusterElementWorkflowNodeName) { + + Map extensions = clusterElement.getExtensions(); + + if (extensions == null || !extensions.containsKey(WorkflowExtConstants.CLUSTER_ELEMENTS)) { + return Optional.empty(); + } + + ClusterElementMap clusterElementMap = of(extensions); + + if (clusterElementType.multipleElements()) { + Optional direct = clusterElementMap.getClusterElements(clusterElementType) + .stream() + .filter(curClusterElement -> Objects.equals( + curClusterElement.getWorkflowNodeName(), clusterElementWorkflowNodeName)) + .findFirst(); + + if (direct.isPresent()) { + return direct; + } + } else { + Optional clusterElementOptional = clusterElementMap.fetchClusterElement(clusterElementType) + .filter(ce -> Objects.equals(ce.getWorkflowNodeName(), clusterElementWorkflowNodeName)); + + if (clusterElementOptional.isPresent()) { + return clusterElementOptional; + } + } + + return clusterElementMap.findNestedClusterElement(clusterElementType, clusterElementWorkflowNodeName); + } + @SuppressWarnings("unchecked") public List getClusterElements(ClusterElementType clusterElementType) { List clusterElements = (List) super.get(clusterElementType.key()); diff --git a/server/libs/platform/platform-configuration/platform-configuration-api/src/test/java/com/bytechef/platform/configuration/domain/ClusterElementMapTest.java b/server/libs/platform/platform-configuration/platform-configuration-api/src/test/java/com/bytechef/platform/configuration/domain/ClusterElementMapTest.java new file mode 100644 index 00000000000..40f0e47d438 --- /dev/null +++ b/server/libs/platform/platform-configuration/platform-configuration-api/src/test/java/com/bytechef/platform/configuration/domain/ClusterElementMapTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bytechef.platform.configuration.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.bytechef.atlas.configuration.constant.WorkflowConstants; +import com.bytechef.component.definition.ClusterElementDefinition.ClusterElementType; +import com.bytechef.platform.configuration.constant.WorkflowExtConstants; +import com.bytechef.test.extension.ObjectMapperSetupExtension; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * @author Ivica Cardic + */ +@ExtendWith(ObjectMapperSetupExtension.class) +class ClusterElementMapTest { + + private static final ClusterElementType TOOLS = new ClusterElementType("TOOLS", "tools", "Tools", true, false); + + @Test + void testGetClusterElementResolvesDirectChild() { + Map extensions = Map.of( + WorkflowExtConstants.CLUSTER_ELEMENTS, + Map.of("tools", List.of( + Map.of( + WorkflowConstants.NAME, "hubspot_1", + WorkflowConstants.TYPE, "hubspot/v1/createContact", + WorkflowConstants.PARAMETERS, Map.of())))); + + ClusterElementMap clusterElementMap = ClusterElementMap.of(extensions); + + ClusterElement clusterElement = clusterElementMap.getClusterElement(TOOLS, "hubspot_1"); + + assertThat(clusterElement.getWorkflowNodeName()).isEqualTo("hubspot_1"); + assertThat(clusterElement.getComponentName()).isEqualTo("hubspot"); + } + + @Test + void testGetClusterElementResolvesNestedChild() { + Map hubspotTool = Map.of( + WorkflowConstants.NAME, "hubspot_1", + WorkflowConstants.TYPE, "hubspot/v1/createContact", + WorkflowConstants.PARAMETERS, Map.of()); + + Map aiAgentTool = Map.of( + WorkflowConstants.NAME, "aiAgent_tool_1", + WorkflowConstants.TYPE, "aiAgent/v1/aiAgentTool", + WorkflowConstants.PARAMETERS, Map.of(), + WorkflowExtConstants.CLUSTER_ELEMENTS, Map.of("tools", List.of(hubspotTool))); + + Map extensions = Map.of( + WorkflowExtConstants.CLUSTER_ELEMENTS, Map.of("tools", List.of(aiAgentTool))); + + ClusterElementMap clusterElementMap = ClusterElementMap.of(extensions); + + ClusterElement clusterElement = clusterElementMap.getClusterElement(TOOLS, "hubspot_1"); + + assertThat(clusterElement.getWorkflowNodeName()).isEqualTo("hubspot_1"); + assertThat(clusterElement.getComponentName()).isEqualTo("hubspot"); + } + + @Test + void testGetClusterElementThrowsWhenMissing() { + Map extensions = Map.of( + WorkflowExtConstants.CLUSTER_ELEMENTS, + Map.of("tools", List.of( + Map.of( + WorkflowConstants.NAME, "hubspot_1", + WorkflowConstants.TYPE, "hubspot/v1/createContact", + WorkflowConstants.PARAMETERS, Map.of())))); + + ClusterElementMap clusterElementMap = ClusterElementMap.of(extensions); + + assertThatThrownBy(() -> clusterElementMap.getClusterElement(TOOLS, "missing_1")) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/server/libs/platform/platform-configuration/platform-configuration-service/src/main/java/com/bytechef/platform/configuration/facade/WorkflowNodeOptionFacadeImpl.java b/server/libs/platform/platform-configuration/platform-configuration-service/src/main/java/com/bytechef/platform/configuration/facade/WorkflowNodeOptionFacadeImpl.java index 36263fbb902..729dd0b3f33 100644 --- a/server/libs/platform/platform-configuration/platform-configuration-service/src/main/java/com/bytechef/platform/configuration/facade/WorkflowNodeOptionFacadeImpl.java +++ b/server/libs/platform/platform-configuration/platform-configuration-service/src/main/java/com/bytechef/platform/configuration/facade/WorkflowNodeOptionFacadeImpl.java @@ -28,6 +28,7 @@ import com.bytechef.platform.component.facade.ClusterElementDefinitionFacade; import com.bytechef.platform.component.facade.TriggerDefinitionFacade; import com.bytechef.platform.component.service.ClusterElementDefinitionService; +import com.bytechef.platform.configuration.constant.WorkflowExtConstants; import com.bytechef.platform.configuration.domain.ClusterElement; import com.bytechef.platform.configuration.domain.ClusterElementMap; import com.bytechef.platform.configuration.domain.WorkflowTestConfigurationConnection; @@ -143,26 +144,44 @@ public List