diff --git a/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts b/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts index 112168f12f9..badbc7a6ec5 100644 --- a/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts +++ b/client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts @@ -377,10 +377,33 @@ export const useProperty = ({ .filter((option) => option !== null); }, [options]); - const fromAiExpression = useMemo( - () => (description ? `=fromAi('${name}', '${description}')` : `=fromAi('${name}')`), - [description, name] - ); + const fromAiExpression = useMemo(() => { + const mapEntries: string[] = []; + + if (description) { + const escapedDescription = description.replace(/'/g, "''"); + + mapEntries.push(`'description': '${escapedDescription}'`); + } + + if (defaultValue !== '' && defaultValue !== null && defaultValue !== undefined) { + const escapedDefault = String(defaultValue).replace(/'/g, "''"); + + mapEntries.push(`'defaultValue': '${escapedDefault}'`); + } + + if (formattedOptions != null && formattedOptions.length > 0) { + const optionValues = formattedOptions + .map((option) => `'${String(option?.value ?? '').replace(/'/g, "''")}'`) + .join(', '); + + mapEntries.push(`'options': {${optionValues}}`); + } + + mapEntries.push(`'required': ${required}`); + + return `=fromAi('${name}', '${type}', {${mapEntries.join(', ')}})`; + }, [defaultValue, description, formattedOptions, name, required, type]); const isValidControlType = useMemo( () => controlType && INPUT_PROPERTY_CONTROL_TYPES.includes(controlType), @@ -1045,16 +1068,10 @@ export const useProperty = ({ if (fromAi) { if (editorRef.current) { - const escapedDescription = description?.replace(/'/g, "''"); - - const fromAi = escapedDescription - ? `=fromAi('${property.name}', '${escapedDescription}')` - : `=fromAi('${property.name}')`; - - editorRef.current.commands.setContent(fromAi); + editorRef.current.commands.setContent(fromAiExpression); editorRef.current.setEditable(false); - value = fromAi; + value = fromAiExpression; } } else { if (editorRef.current) { @@ -1079,9 +1096,8 @@ export const useProperty = ({ }, [ custom, - description, + fromAiExpression, path, - property.name, propertyParameterValue, setFocusedInput, type, diff --git a/server/ee/libs/platform/platform-component/platform-component-remote-client/src/main/java/com/bytechef/ee/platform/component/remote/client/facade/RemoteConnectionDefinitionFacadeClient.java b/server/ee/libs/platform/platform-component/platform-component-remote-client/src/main/java/com/bytechef/ee/platform/component/remote/client/facade/RemoteConnectionDefinitionFacadeClient.java new file mode 100644 index 00000000000..df8ff8681cb --- /dev/null +++ b/server/ee/libs/platform/platform-component/platform-component-remote-client/src/main/java/com/bytechef/ee/platform/component/remote/client/facade/RemoteConnectionDefinitionFacadeClient.java @@ -0,0 +1,26 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the ByteChef Enterprise license (the "Enterprise License"); + * you may not use this file except in compliance with the Enterprise License. + */ + +package com.bytechef.ee.platform.component.remote.client.facade; + +import com.bytechef.platform.component.ComponentConnection; +import com.bytechef.platform.component.facade.ConnectionDefinitionFacade; +import org.springframework.stereotype.Component; + +/** + * @version ee + * + * @author Ivica Cardic + */ +@Component +public class RemoteConnectionDefinitionFacadeClient implements ConnectionDefinitionFacade { + + @Override + public ComponentConnection executeConnectionRefresh(Long connectionId) { + throw new UnsupportedOperationException(); + } +} diff --git a/server/ee/libs/platform/platform-scheduler/platform-scheduler-remote-client/src/main/java/com/bytechef/ee/platform/scheduler/remote/client/RemoteConnectionRefreshSchedulerClient.java b/server/ee/libs/platform/platform-scheduler/platform-scheduler-remote-client/src/main/java/com/bytechef/ee/platform/scheduler/remote/client/RemoteConnectionRefreshSchedulerClient.java new file mode 100644 index 00000000000..07785652df7 --- /dev/null +++ b/server/ee/libs/platform/platform-scheduler/platform-scheduler-remote-client/src/main/java/com/bytechef/ee/platform/scheduler/remote/client/RemoteConnectionRefreshSchedulerClient.java @@ -0,0 +1,56 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the ByteChef Enterprise license (the "Enterprise License"); + * you may not use this file except in compliance with the Enterprise License. + */ + +package com.bytechef.ee.platform.scheduler.remote.client; + +import com.bytechef.ee.remote.client.LoadBalancedRestClient; +import com.bytechef.platform.scheduler.ConnectionRefreshScheduler; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.time.Instant; +import org.springframework.stereotype.Component; + +/** + * @version ee + * + * @author Ivica Cardic + */ +@Component +public class RemoteConnectionRefreshSchedulerClient implements ConnectionRefreshScheduler { + + private static final String CONNECTION_REFRESH_SCHEDULER = "/remote/connection-refresh-scheduler"; + private static final String SCHEDULER_APP = "scheduler-app"; + + private final LoadBalancedRestClient loadBalancedRestClient; + + @SuppressFBWarnings("EI") + public RemoteConnectionRefreshSchedulerClient(LoadBalancedRestClient loadBalancedRestClient) { + this.loadBalancedRestClient = loadBalancedRestClient; + } + + @Override + public void cancelConnectionRefresh(Long connectionId) { + loadBalancedRestClient.post( + uriBuilder -> uriBuilder + .host(SCHEDULER_APP) + .path(CONNECTION_REFRESH_SCHEDULER + "/cancel-connection-refresh") + .build(), + connectionId); + } + + @Override + public void scheduleConnectionRefresh(Long connectionId, Instant expiry) { + loadBalancedRestClient.post( + uriBuilder -> uriBuilder + .host(SCHEDULER_APP) + .path(CONNECTION_REFRESH_SCHEDULER + "/schedule-connection-refresh") + .build(), + new ScheduleConnectionRefreshRequest(connectionId, expiry)); + } + + private record ScheduleConnectionRefreshRequest(Long connectionId, Instant expiry) { + } +} diff --git a/server/ee/libs/platform/platform-workflow/platform-workflow-execution/platform-workflow-execution-remote-client/src/main/java/com/bytechef/ee/platform/workflow/execution/remote/client/facade/RemoteConnectionLifecycleFacadeClient.java b/server/ee/libs/platform/platform-workflow/platform-workflow-execution/platform-workflow-execution-remote-client/src/main/java/com/bytechef/ee/platform/workflow/execution/remote/client/facade/RemoteConnectionLifecycleFacadeClient.java new file mode 100644 index 00000000000..ef478d30fcf --- /dev/null +++ b/server/ee/libs/platform/platform-workflow/platform-workflow-execution/platform-workflow-execution-remote-client/src/main/java/com/bytechef/ee/platform/workflow/execution/remote/client/facade/RemoteConnectionLifecycleFacadeClient.java @@ -0,0 +1,64 @@ +/* + * Copyright 2025 ByteChef + * + * Licensed under the ByteChef Enterprise license (the "Enterprise License"); + * you may not use this file except in compliance with the Enterprise License. + */ + +package com.bytechef.ee.platform.workflow.execution.remote.client.facade; + +import com.bytechef.component.definition.Authorization.AuthorizationType; +import com.bytechef.ee.remote.client.LoadBalancedRestClient; +import com.bytechef.platform.workflow.execution.facade.ConnectionLifecycleFacade; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Map; +import org.springframework.stereotype.Component; + +/** + * @version ee + * + * @author Ivica Cardic + */ +@Component +public class RemoteConnectionLifecycleFacadeClient implements ConnectionLifecycleFacade { + + private static final String CONNECTION_LIFECYCLE_FACADE = "/remote/connection-lifecycle-facade"; + private static final String EXECUTION_APP = "execution-app"; + + private final LoadBalancedRestClient loadBalancedRestClient; + + @SuppressFBWarnings("EI") + public RemoteConnectionLifecycleFacadeClient(LoadBalancedRestClient loadBalancedRestClient) { + this.loadBalancedRestClient = loadBalancedRestClient; + } + + @Override + public void scheduleConnectionRefresh( + Long connectionId, Map parameters, AuthorizationType authorizationType) { + + loadBalancedRestClient.post( + uriBuilder -> uriBuilder + .host(EXECUTION_APP) + .path(CONNECTION_LIFECYCLE_FACADE + "/schedule-connection-refresh") + .build(), + new ScheduleConnectionRefreshRequest(connectionId, parameters, authorizationType)); + } + + @Override + public void deleteScheduledConnectionRefresh(Long connectionId, AuthorizationType authorizationType) { + loadBalancedRestClient.post( + uriBuilder -> uriBuilder + .host(EXECUTION_APP) + .path(CONNECTION_LIFECYCLE_FACADE + "/delete-scheduled-connection-refresh") + .build(), + new DeleteScheduledConnectionRefreshRequest(connectionId, authorizationType)); + } + + @SuppressFBWarnings("EI") + private record ScheduleConnectionRefreshRequest( + Long connectionId, Map parameters, AuthorizationType authorizationType) { + } + + private record DeleteScheduledConnectionRefreshRequest(Long connectionId, AuthorizationType authorizationType) { + } +} diff --git a/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/FromAi.java b/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/FromAi.java index 5060cbc496f..640fadda2b9 100644 --- a/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/FromAi.java +++ b/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/FromAi.java @@ -16,6 +16,8 @@ package com.bytechef.ai.tool; +import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.springframework.expression.AccessException; @@ -29,7 +31,7 @@ public class FromAi implements MethodExecutor { private static final Set VALID_TYPES = Set.of( - "STRING", "NUMBER", "INTEGER", "BOOLEAN", "ARRAY", "OBJECT"); + "STRING", "NUMBER", "INTEGER", "BOOLEAN", "ARRAY", "OBJECT", "DATE", "TIME", "DATE_TIME"); @Override public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException { @@ -37,9 +39,9 @@ public TypedValue execute(EvaluationContext context, Object target, Object... ar throw new IllegalArgumentException("fromAi requires at least a name argument."); } - if (arguments.length > 4) { + if (arguments.length > 5) { throw new IllegalArgumentException( - "fromAi accepts at most 4 arguments (name, description, type, default)."); + "fromAi accepts at most 5 arguments (name, type, description, default, options)."); } Object nameArgument = arguments[0]; @@ -50,22 +52,10 @@ public TypedValue execute(EvaluationContext context, Object target, Object... ar String name = string.trim(); - String description = null; - - if (arguments.length > 1) { - Object descriptionArgument = arguments[1]; - - if (descriptionArgument != null && !(descriptionArgument instanceof String)) { - throw new IllegalArgumentException("fromAi description argument must be a String or null."); - } - - description = (String) descriptionArgument; - } - String type = "STRING"; - if (arguments.length > 2) { - Object typeArgument = arguments[2]; + if (arguments.length > 1) { + Object typeArgument = arguments[1]; if (typeArgument != null && !(typeArgument instanceof String)) { throw new IllegalArgumentException("fromAi type argument must be a String or null."); @@ -87,8 +77,49 @@ public TypedValue execute(EvaluationContext context, Object target, Object... ar } } - Object defaultValue = arguments.length > 3 ? arguments[3] : null; + String description = null; + Object defaultValue = null; + List options = null; + boolean required = false; + + if (arguments.length > 2) { + Object thirdArgument = arguments[2]; + + if (thirdArgument instanceof Map paramMap) { + Object descArg = paramMap.get("description"); + + if (descArg != null && !(descArg instanceof String)) { + throw new IllegalArgumentException("fromAi 'description' in map must be a String or null."); + } + + description = (String) descArg; + defaultValue = paramMap.get("defaultValue"); + + Object optionsArg = paramMap.get("options"); + + if (optionsArg != null && !(optionsArg instanceof List)) { + throw new IllegalArgumentException("fromAi 'options' in map must be a List or null."); + } + + if (optionsArg != null) { + @SuppressWarnings("unchecked") + List castedOptions = (List) optionsArg; + + options = castedOptions; + } + + Object requiredArg = paramMap.get("required"); + + if (requiredArg != null && !(requiredArg instanceof Boolean)) { + throw new IllegalArgumentException("fromAi 'required' in map must be a boolean."); + } + + if (requiredArg != null) { + required = (Boolean) requiredArg; + } + } + } - return new TypedValue(new FromAiResult(name, description, type, defaultValue)); + return new TypedValue(new FromAiResult(name, type, description, defaultValue, options, required)); } } diff --git a/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/FromAiResult.java b/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/FromAiResult.java index 4161286af27..23b13e0e688 100644 --- a/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/FromAiResult.java +++ b/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/FromAiResult.java @@ -16,6 +16,7 @@ package com.bytechef.ai.tool; +import java.util.List; import java.util.Objects; /** @@ -29,24 +30,13 @@ * @param defaultValue fallback value when the AI model does not provide one (nullable) * @author Ivica Cardic */ -public record FromAiResult(String name, String description, String type, Object defaultValue) { - - private static final String DEFAULT_TYPE = "STRING"; +public record FromAiResult( + String name, String type, String description, Object defaultValue, List options, boolean required) { public FromAiResult { Objects.requireNonNull(name, "name must not be null"); Objects.requireNonNull(type, "type must not be null"); - } - - public FromAiResult(String name) { - this(name, null, DEFAULT_TYPE, null); - } - - public FromAiResult(String name, String description) { - this(name, description, DEFAULT_TYPE, null); - } - public FromAiResult(String name, String description, String type) { - this(name, description, type, null); + options = options == null ? null : List.copyOf(options); } } diff --git a/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/facade/AbstractToolFacade.java b/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/facade/AbstractToolFacade.java index dc0be807b8a..c4e6126bdde 100644 --- a/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/facade/AbstractToolFacade.java +++ b/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/facade/AbstractToolFacade.java @@ -56,6 +56,28 @@ protected List extractFromAiResults(Map parameters) { extractFromAiResults((Map) map).forEach( fromAiResult -> fromAiResultsByName.putIfAbsent(fromAiResult.name(), fromAiResult)); } + } else if (value instanceof List list) { + for (Object item : list) { + if (item instanceof String itemExpression && itemExpression.contains("fromAi(")) { + for (String fromAiCall : extractFromAiCallStrings(itemExpression)) { + FromAiResult fromAiResult = evaluateSingleFromAi(fromAiCall); + + if (fromAiResult != null) { + fromAiResultsByName.putIfAbsent(fromAiResult.name(), fromAiResult); + } + } + } else if (item instanceof Map itemMap) { + if (ConvertUtils.canConvert(itemMap, FromAiResult.class)) { + FromAiResult fromAiResult = ConvertUtils.convertValue(item, FromAiResult.class); + + fromAiResultsByName.putIfAbsent(fromAiResult.name(), fromAiResult); + } else { + extractFromAiResults((Map) itemMap).forEach( + fromAiResult -> fromAiResultsByName.putIfAbsent( + fromAiResult.name(), fromAiResult)); + } + } + } } else if (value instanceof String expression && expression.contains("fromAi(")) { for (String fromAiCall : extractFromAiCallStrings(expression)) { FromAiResult fromAiResult = evaluateSingleFromAi(fromAiCall); @@ -96,6 +118,16 @@ protected Object resolveParameterValue(Object value, Map request return resolvedMap; } + if (value instanceof List list) { + List resolvedList = new ArrayList<>(); + + for (Object item : list) { + resolvedList.add(resolveParameterValue(item, request)); + } + + return resolvedList; + } + if (!(value instanceof String expression) || !expression.contains("fromAi(")) { return value; } diff --git a/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/util/FromAiInputSchemaUtils.java b/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/util/FromAiInputSchemaUtils.java index 3d72377fd01..380da2eae5f 100644 --- a/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/util/FromAiInputSchemaUtils.java +++ b/server/libs/ai/ai-tool-api/src/main/java/com/bytechef/ai/tool/util/FromAiInputSchemaUtils.java @@ -48,6 +48,12 @@ public static String generateInputSchema(List fromAiResults) { parameterObjectNode.put("type", getJsonSchemaType(fromAiResult.type())); + String format = getJsonSchemaFormat(fromAiResult.type()); + + if (format != null) { + parameterObjectNode.put("format", format); + } + String description = fromAiResult.description(); if (description != null && !description.isEmpty()) { @@ -57,26 +63,22 @@ public static String generateInputSchema(List fromAiResults) { Object defaultValue = fromAiResult.defaultValue(); if (defaultValue != null) { - if (defaultValue instanceof Boolean booleanValue) { - parameterObjectNode.put("default", booleanValue); - } else if (defaultValue instanceof Integer integerValue) { - parameterObjectNode.put("default", integerValue); - } else if (defaultValue instanceof Long longValue) { - parameterObjectNode.put("default", longValue); - } else if (defaultValue instanceof Double doubleValue) { - parameterObjectNode.put("default", doubleValue); - } else if (defaultValue instanceof Float floatValue) { - parameterObjectNode.put("default", floatValue); - } else if (defaultValue instanceof String stringValue) { - parameterObjectNode.put("default", stringValue); - } else { - parameterObjectNode.putPOJO("default", defaultValue); + addValueToObjectNode(parameterObjectNode, "default", coerceToType(defaultValue, fromAiResult.type())); + } + + List options = fromAiResult.options(); + + if (options != null && !options.isEmpty()) { + ArrayNode enumArrayNode = parameterObjectNode.putArray("enum"); + + for (Object option : options) { + addValueToArrayNode(enumArrayNode, coerceToType(option, fromAiResult.type())); } } propertiesObjectNode.set(fromAiResult.name(), parameterObjectNode); - if (defaultValue == null) { + if (fromAiResult.required()) { requiredArray.add(fromAiResult.name()); } } @@ -84,6 +86,69 @@ public static String generateInputSchema(List fromAiResults) { return schemaObjectNode.toPrettyString(); } + private static Object coerceToType(Object value, String type) { + if (!(value instanceof String stringValue) || type == null) { + return value; + } + + String trimmedType = type.trim(); + + return switch (trimmedType.toUpperCase()) { + case "BOOLEAN" -> Boolean.parseBoolean(stringValue); + case "INTEGER" -> { + try { + yield Long.parseLong(stringValue); + } catch (NumberFormatException e) { + yield value; + } + } + case "NUMBER" -> { + try { + yield Double.parseDouble(stringValue); + } catch (NumberFormatException e) { + yield value; + } + } + default -> value; + }; + } + + private static void addValueToObjectNode(ObjectNode objectNode, String fieldName, Object value) { + if (value instanceof Boolean booleanValue) { + objectNode.put(fieldName, booleanValue); + } else if (value instanceof Integer integerValue) { + objectNode.put(fieldName, integerValue); + } else if (value instanceof Long longValue) { + objectNode.put(fieldName, longValue); + } else if (value instanceof Double doubleValue) { + objectNode.put(fieldName, doubleValue); + } else if (value instanceof Float floatValue) { + objectNode.put(fieldName, floatValue); + } else if (value instanceof String stringValue) { + objectNode.put(fieldName, stringValue); + } else { + objectNode.putPOJO(fieldName, value); + } + } + + private static void addValueToArrayNode(ArrayNode arrayNode, Object value) { + if (value instanceof Boolean booleanValue) { + arrayNode.add(booleanValue); + } else if (value instanceof Integer integerValue) { + arrayNode.add(integerValue); + } else if (value instanceof Long longValue) { + arrayNode.add(longValue); + } else if (value instanceof Double doubleValue) { + arrayNode.add(doubleValue); + } else if (value instanceof Float floatValue) { + arrayNode.add(floatValue); + } else if (value instanceof String stringValue) { + arrayNode.add(stringValue); + } else { + arrayNode.addPOJO(value); + } + } + private static String getJsonSchemaType(String type) { if (type == null || type.isBlank()) { return "string"; @@ -99,4 +164,18 @@ private static String getJsonSchemaType(String type) { default -> "string"; }; } + + private static String getJsonSchemaFormat(String type) { + if (type == null || type.isBlank()) { + return null; + } + + return switch (type.trim() + .toUpperCase()) { + case "DATE" -> "date"; + case "TIME" -> "time"; + case "DATE_TIME" -> "date-time"; + default -> null; + }; + } } diff --git a/server/libs/ai/ai-tool-api/src/test/java/com/bytechef/ai/tool/FromAiTest.java b/server/libs/ai/ai-tool-api/src/test/java/com/bytechef/ai/tool/FromAiTest.java index 4aa33edee45..6891393514b 100644 --- a/server/libs/ai/ai-tool-api/src/test/java/com/bytechef/ai/tool/FromAiTest.java +++ b/server/libs/ai/ai-tool-api/src/test/java/com/bytechef/ai/tool/FromAiTest.java @@ -60,7 +60,8 @@ public void testFromAiWithNameOnly() { @Test public void testFromAiWithNameAndDescription() { Map map = EVALUATOR.evaluate( - Map.of("param", "=fromAi('sifra_artikla', 'The product code')"), Collections.emptyMap()); + Map.of("param", "=fromAi('sifra_artikla', 'STRING', {'description': 'The product code'})"), + Collections.emptyMap()); Object result = MapUtils.get(map, "param"); @@ -77,7 +78,8 @@ public void testFromAiWithNameAndDescription() { @Test public void testFromAiWithAllArguments() { Map map = EVALUATOR.evaluate( - Map.of("param", "=fromAi('price', 'Item price', 'NUMBER', 0)"), Collections.emptyMap()); + Map.of("param", "=fromAi('price', 'NUMBER', {'description': 'Item price', 'defaultValue': 0})"), + Collections.emptyMap()); Object result = MapUtils.get(map, "param"); @@ -100,13 +102,13 @@ public void testFromAiNoArguments() { @Test public void testFromAiWithInvalidType() { assertThrowsExactly(IllegalArgumentException.class, () -> EVALUATOR.evaluate( - Map.of("param", "=fromAi('name', null, 'INVALID')"), Collections.emptyMap())); + Map.of("param", "=fromAi('name', 'INVALID')"), Collections.emptyMap())); } @Test public void testFromAiWithCaseInsensitiveType() { Map map = EVALUATOR.evaluate( - Map.of("param", "=fromAi('count', null, 'integer')"), Collections.emptyMap()); + Map.of("param", "=fromAi('count', 'integer')"), Collections.emptyMap()); Object result = MapUtils.get(map, "param"); @@ -120,7 +122,7 @@ public void testFromAiWithCaseInsensitiveType() { @Test public void testFromAiWithNullDescription() { Map map = EVALUATOR.evaluate( - Map.of("param", "=fromAi('name', null, 'BOOLEAN', true)"), Collections.emptyMap()); + Map.of("param", "=fromAi('name', 'BOOLEAN', {'defaultValue': true})"), Collections.emptyMap()); Object result = MapUtils.get(map, "param"); @@ -134,6 +136,54 @@ public void testFromAiWithNullDescription() { assertEquals(true, fromAiResult.defaultValue()); } + @Test + public void testFromAiWithRequiredTrue() { + Map map = EVALUATOR.evaluate( + Map.of("param", "=fromAi('title', 'STRING', {'description': 'The title.', 'required': true})"), + Collections.emptyMap()); + + Object result = MapUtils.get(map, "param"); + + assertInstanceOf(FromAiResult.class, result); + + FromAiResult fromAiResult = (FromAiResult) result; + + assertEquals("title", fromAiResult.name()); + assertEquals("STRING", fromAiResult.type()); + assertEquals("The title.", fromAiResult.description()); + assertEquals(true, fromAiResult.required()); + } + + @Test + public void testFromAiWithRequiredFalse() { + Map map = EVALUATOR.evaluate( + Map.of("param", "=fromAi('title', 'STRING', {'description': 'The title.', 'required': false})"), + Collections.emptyMap()); + + Object result = MapUtils.get(map, "param"); + + assertInstanceOf(FromAiResult.class, result); + + FromAiResult fromAiResult = (FromAiResult) result; + + assertEquals(false, fromAiResult.required()); + } + + @Test + public void testFromAiWithoutRequiredDefaultsFalse() { + Map map = EVALUATOR.evaluate( + Map.of("param", "=fromAi('title', 'STRING', {'description': 'The title.'})"), + Collections.emptyMap()); + + Object result = MapUtils.get(map, "param"); + + assertInstanceOf(FromAiResult.class, result); + + FromAiResult fromAiResult = (FromAiResult) result; + + assertEquals(false, fromAiResult.required()); + } + @Test public void testFromAiWithNullTypeDefaultsToString() { Map map = EVALUATOR.evaluate( diff --git a/server/libs/ai/ai-tool-api/src/test/java/com/bytechef/ai/tool/facade/AbstractToolFacadeTest.java b/server/libs/ai/ai-tool-api/src/test/java/com/bytechef/ai/tool/facade/AbstractToolFacadeTest.java index f02d8cf7227..dcd2e9f2612 100644 --- a/server/libs/ai/ai-tool-api/src/test/java/com/bytechef/ai/tool/facade/AbstractToolFacadeTest.java +++ b/server/libs/ai/ai-tool-api/src/test/java/com/bytechef/ai/tool/facade/AbstractToolFacadeTest.java @@ -50,7 +50,7 @@ private AbstractToolFacade createToolFacade() { @Test void testExtractFromAiResultsWithSingleFromAiExpression() { when(evaluator.evaluate(eq(Map.of("value", "=fromAi('subject')")), eq(Map.of()))) - .thenReturn(Map.of("value", new FromAiResult("subject", null, "STRING", null))); + .thenReturn(Map.of("value", new FromAiResult("subject", "STRING", null, null, null, false))); AbstractToolFacade toolFacade = createToolFacade(); @@ -64,9 +64,9 @@ void testExtractFromAiResultsWithSingleFromAiExpression() { @Test void testExtractFromAiResultsWithMultipleFromAiCallsInOneExpression() { when(evaluator.evaluate(eq(Map.of("value", "=fromAi('subject')")), eq(Map.of()))) - .thenReturn(Map.of("value", new FromAiResult("subject", null, "STRING", null))); + .thenReturn(Map.of("value", new FromAiResult("subject", "STRING", null, null, null, false))); when(evaluator.evaluate(eq(Map.of("value", "=fromAi('name')")), eq(Map.of()))) - .thenReturn(Map.of("value", new FromAiResult("name", null, "STRING", null))); + .thenReturn(Map.of("value", new FromAiResult("name", "STRING", null, null, null, false))); AbstractToolFacade toolFacade = createToolFacade(); @@ -83,7 +83,7 @@ void testExtractFromAiResultsWithMultipleFromAiCallsInOneExpression() { @Test void testExtractFromAiResultsDeduplicatesByName() { when(evaluator.evaluate(eq(Map.of("value", "=fromAi('subject')")), eq(Map.of()))) - .thenReturn(Map.of("value", new FromAiResult("subject", null, "STRING", null))); + .thenReturn(Map.of("value", new FromAiResult("subject", "STRING", null, null, null, false))); AbstractToolFacade toolFacade = createToolFacade(); @@ -103,7 +103,7 @@ void testExtractFromAiResultsDeduplicatesByName() { void testExtractFromAiResultsWithFromAiResultObjectValue() { AbstractToolFacade toolFacade = createToolFacade(); - FromAiResult directResult = new FromAiResult("directParam", "A direct param", "STRING", null); + FromAiResult directResult = new FromAiResult("directParam", "STRING", "A direct param", null, null, false); List results = toolFacade.extractFromAiResults(Map.of("param", directResult)); @@ -124,6 +124,7 @@ void testExtractFromAiResultsWithMapConvertibleToFromAiResult() { fromAiResultMap.put("description", "A map-based param"); fromAiResultMap.put("type", "STRING"); fromAiResultMap.put("defaultValue", null); + fromAiResultMap.put("required", false); List results = toolFacade.extractFromAiResults(Map.of("param", fromAiResultMap)); @@ -138,7 +139,7 @@ void testExtractFromAiResultsWithMapConvertibleToFromAiResult() { void testExtractFromAiResultsWithMapConvertibleToFromAiResultDeduplicatesWithDirectResult() { AbstractToolFacade toolFacade = createToolFacade(); - FromAiResult directResult = new FromAiResult("sharedName", "Direct", "STRING", null); + FromAiResult directResult = new FromAiResult("sharedName", "STRING", "Direct", null, null, false); Map fromAiResultMap = new LinkedHashMap<>(); @@ -159,6 +160,50 @@ void testExtractFromAiResultsWithMapConvertibleToFromAiResultDeduplicatesWithDir .description()); } + @Test + void testExtractFromAiResultsWithListOfFromAiExpressions() { + when(evaluator.evaluate( + eq(Map.of("value", "=fromAi('Email__0', 'STRING', {'description': 'The attendee email address.'})")), + eq(Map.of()))) + .thenReturn(Map.of("value", + new FromAiResult("Email__0", "STRING", "The attendee email address.", null, null, false))); + + AbstractToolFacade toolFacade = createToolFacade(); + + List results = toolFacade.extractFromAiResults( + Map.of("attendees", + List.of("=fromAi('Email__0', 'STRING', {'description': 'The attendee email address.'})"))); + + assertEquals(1, results.size()); + assertEquals("Email__0", results.getFirst() + .name()); + assertEquals("The attendee email address.", results.getFirst() + .description()); + } + + @Test + void testExtractFromAiResultsWithListOfPreEvaluatedFromAiResultMaps() { + AbstractToolFacade toolFacade = createToolFacade(); + + Map fromAiResultMap = new LinkedHashMap<>(); + + fromAiResultMap.put("name", "Email__0"); + fromAiResultMap.put("description", "The attendee's email address."); + fromAiResultMap.put("type", "STRING"); + fromAiResultMap.put("defaultValue", null); + fromAiResultMap.put("options", null); + fromAiResultMap.put("required", false); + + List results = toolFacade.extractFromAiResults( + Map.of("attendees", List.of(fromAiResultMap))); + + assertEquals(1, results.size()); + assertEquals("Email__0", results.getFirst() + .name()); + assertEquals("The attendee's email address.", results.getFirst() + .description()); + } + @Test void testExtractFromAiResultsWithNonFromAiParameters() { AbstractToolFacade toolFacade = createToolFacade(); @@ -182,7 +227,7 @@ void testExtractFromAiResultsWithNullParameters() { void testResolveParameterValueWithFromAiResultObject() { AbstractToolFacade toolFacade = createToolFacade(); - FromAiResult fromAiResult = new FromAiResult("subject", null, "STRING", null); + FromAiResult fromAiResult = new FromAiResult("subject", "STRING", null, null, null, false); Object result = toolFacade.resolveParameterValue(fromAiResult, Map.of("subject", "Hello World")); @@ -193,7 +238,7 @@ void testResolveParameterValueWithFromAiResultObject() { void testResolveParameterValueWithFromAiResultObjectFallsBackToDefault() { AbstractToolFacade toolFacade = createToolFacade(); - FromAiResult fromAiResult = new FromAiResult("subject", null, "STRING", "default value"); + FromAiResult fromAiResult = new FromAiResult("subject", "STRING", null, "default value", null, false); Object result = toolFacade.resolveParameterValue(fromAiResult, Map.of()); @@ -204,7 +249,7 @@ void testResolveParameterValueWithFromAiResultObjectFallsBackToDefault() { void testResolveParameterValueWithFromAiResultObjectReturnsNullWhenNoDefaultAndNotInRequest() { AbstractToolFacade toolFacade = createToolFacade(); - FromAiResult fromAiResult = new FromAiResult("subject", null, "STRING", null); + FromAiResult fromAiResult = new FromAiResult("subject", "STRING", null, null, null, false); Object result = toolFacade.resolveParameterValue(fromAiResult, Map.of()); @@ -221,6 +266,7 @@ void testResolveParameterValueWithMapConvertibleToFromAiResult() { fromAiResultMap.put("description", null); fromAiResultMap.put("type", "STRING"); fromAiResultMap.put("defaultValue", null); + fromAiResultMap.put("required", false); Object result = toolFacade.resolveParameterValue(fromAiResultMap, Map.of("subject", "Resolved Value")); @@ -237,6 +283,7 @@ void testResolveParameterValueWithMapConvertibleToFromAiResultFallsBackToDefault fromAiResultMap.put("description", null); fromAiResultMap.put("type", "STRING"); fromAiResultMap.put("defaultValue", "fallback"); + fromAiResultMap.put("required", "false"); Object result = toolFacade.resolveParameterValue(fromAiResultMap, Map.of()); @@ -253,6 +300,7 @@ void testResolveParameterValueWithMapConvertibleToFromAiResultReturnsNullWhenNoD fromAiResultMap.put("description", null); fromAiResultMap.put("type", "STRING"); fromAiResultMap.put("defaultValue", null); + fromAiResultMap.put("required", false); Object result = toolFacade.resolveParameterValue(fromAiResultMap, Map.of()); @@ -262,7 +310,7 @@ void testResolveParameterValueWithMapConvertibleToFromAiResultReturnsNullWhenNoD @Test void testResolveParameterValueWithPureFromAiExpression() { when(evaluator.evaluate(eq(Map.of("value", "=fromAi('subject')")), eq(Map.of()))) - .thenReturn(Map.of("value", new FromAiResult("subject", null, "STRING", null))); + .thenReturn(Map.of("value", new FromAiResult("subject", "STRING", null, null, null, false))); AbstractToolFacade toolFacade = createToolFacade(); @@ -274,7 +322,7 @@ void testResolveParameterValueWithPureFromAiExpression() { @Test void testResolveParameterValueWithPureFromAiExpressionPreservesIntegerType() { when(evaluator.evaluate(eq(Map.of("value", "=fromAi('count', null, 'INTEGER')")), eq(Map.of()))) - .thenReturn(Map.of("value", new FromAiResult("count", null, "INTEGER", null))); + .thenReturn(Map.of("value", new FromAiResult("count", "INTEGER", null, null, null, false))); AbstractToolFacade toolFacade = createToolFacade(); @@ -287,9 +335,9 @@ void testResolveParameterValueWithPureFromAiExpressionPreservesIntegerType() { @Test void testResolveParameterValueWithCompositeExpression() { when(evaluator.evaluate(eq(Map.of("value", "=fromAi('subject')")), eq(Map.of()))) - .thenReturn(Map.of("value", new FromAiResult("subject", null, "STRING", null))); + .thenReturn(Map.of("value", new FromAiResult("subject", "STRING", null, null, null, false))); when(evaluator.evaluate(eq(Map.of("value", "=fromAi('name')")), eq(Map.of()))) - .thenReturn(Map.of("value", new FromAiResult("name", null, "STRING", null))); + .thenReturn(Map.of("value", new FromAiResult("name", "STRING", null, null, null, false))); when(evaluator.evaluate( eq(Map.of("value", "='Generated by AI: ' + 'Hello World' + ' by ' + 'John'")), eq(Map.of()))) .thenReturn(Map.of("value", "Generated by AI: Hello World by John")); @@ -306,7 +354,7 @@ void testResolveParameterValueWithCompositeExpression() { @Test void testResolveParameterValueWithCompositeExpressionUsesDefaults() { when(evaluator.evaluate(eq(Map.of("value", "=fromAi('subject')")), eq(Map.of()))) - .thenReturn(Map.of("value", new FromAiResult("subject", null, "STRING", "Default Subject"))); + .thenReturn(Map.of("value", new FromAiResult("subject", "STRING", null, "Default Subject", null, false))); when(evaluator.evaluate(eq(Map.of("value", "='Title: ' + 'Default Subject'")), eq(Map.of()))) .thenReturn(Map.of("value", "Title: Default Subject")); @@ -317,6 +365,19 @@ void testResolveParameterValueWithCompositeExpressionUsesDefaults() { assertEquals("Title: Default Subject", result); } + @Test + void testResolveParameterValueWithListOfFromAiExpressions() { + when(evaluator.evaluate(eq(Map.of("value", "=fromAi('Email__0')")), eq(Map.of()))) + .thenReturn(Map.of("value", new FromAiResult("Email__0", "STRING", null, null, null, false))); + + AbstractToolFacade toolFacade = createToolFacade(); + + Object result = toolFacade.resolveParameterValue( + List.of("=fromAi('Email__0')"), Map.of("Email__0", "user@example.com")); + + assertEquals(List.of("user@example.com"), result); + } + @Test void testResolveParameterValueWithNonFromAiStringValue() { AbstractToolFacade toolFacade = createToolFacade(); diff --git a/server/libs/automation/automation-configuration/automation-configuration-service/src/test/java/com/bytechef/automation/configuration/config/ProjectIntTestConfigurationSharedMocks.java b/server/libs/automation/automation-configuration/automation-configuration-service/src/test/java/com/bytechef/automation/configuration/config/ProjectIntTestConfigurationSharedMocks.java index 77cf95a78e4..1b7ae52a4a9 100644 --- a/server/libs/automation/automation-configuration/automation-configuration-service/src/test/java/com/bytechef/automation/configuration/config/ProjectIntTestConfigurationSharedMocks.java +++ b/server/libs/automation/automation-configuration/automation-configuration-service/src/test/java/com/bytechef/automation/configuration/config/ProjectIntTestConfigurationSharedMocks.java @@ -33,6 +33,7 @@ import com.bytechef.platform.security.service.ApiKeyService; import com.bytechef.platform.user.service.AuthorityService; import com.bytechef.platform.user.service.UserService; +import com.bytechef.platform.workflow.execution.facade.ConnectionLifecycleFacade; import com.bytechef.platform.workflow.execution.facade.PrincipalJobFacade; import com.bytechef.platform.workflow.execution.facade.TriggerLifecycleFacade; import com.bytechef.platform.workflow.execution.service.PrincipalJobService; @@ -50,11 +51,11 @@ @Retention(RetentionPolicy.RUNTIME) @MockitoBean(types = { ApiKeyFacade.class, ApiKeyService.class, AuthorityService.class, ComponentConnectionFacade.class, - ComponentDefinitionService.class, ConnectionFacade.class, EnvironmentService.class, GitHubProxyClient.class, - JobFacade.class, JobService.class, ConnectionService.class, PrincipalJobFacade.class, PrincipalJobService.class, - TaskExecutionService.class, TriggerDefinitionService.class, TriggerExecutionService.class, - TriggerLifecycleFacade.class, UserService.class, WorkflowCacheManager.class, WorkflowNodeParameterFacade.class, - WorkflowNodeTestOutputService.class + ComponentDefinitionService.class, ConnectionFacade.class, ConnectionLifecycleFacade.class, + EnvironmentService.class, GitHubProxyClient.class, JobFacade.class, JobService.class, ConnectionService.class, + PrincipalJobFacade.class, PrincipalJobService.class, TaskExecutionService.class, TriggerDefinitionService.class, + TriggerExecutionService.class, TriggerLifecycleFacade.class, UserService.class, WorkflowCacheManager.class, + WorkflowNodeParameterFacade.class, WorkflowNodeTestOutputService.class }) public @interface ProjectIntTestConfigurationSharedMocks { } diff --git a/server/libs/automation/automation-mcp/automation-mcp-service/src/test/java/com/bytechef/automation/mcp/config/McpProjectIntTestConfigurationSharedMocks.java b/server/libs/automation/automation-mcp/automation-mcp-service/src/test/java/com/bytechef/automation/mcp/config/McpProjectIntTestConfigurationSharedMocks.java index b43b9ed8204..817a82bb3a4 100644 --- a/server/libs/automation/automation-mcp/automation-mcp-service/src/test/java/com/bytechef/automation/mcp/config/McpProjectIntTestConfigurationSharedMocks.java +++ b/server/libs/automation/automation-mcp/automation-mcp-service/src/test/java/com/bytechef/automation/mcp/config/McpProjectIntTestConfigurationSharedMocks.java @@ -35,6 +35,7 @@ import com.bytechef.platform.security.service.ApiKeyService; import com.bytechef.platform.user.service.AuthorityService; import com.bytechef.platform.user.service.UserService; +import com.bytechef.platform.workflow.execution.facade.ConnectionLifecycleFacade; import com.bytechef.platform.workflow.execution.facade.PrincipalJobFacade; import com.bytechef.platform.workflow.execution.facade.TriggerLifecycleFacade; import com.bytechef.platform.workflow.execution.service.PrincipalJobService; @@ -52,7 +53,7 @@ @Retention(RetentionPolicy.RUNTIME) @MockitoBean(types = { ApiKeyFacade.class, ApiKeyService.class, AuthorityService.class, ComponentConnectionFacade.class, - ComponentDefinitionService.class, ConnectionFacade.class, + ComponentDefinitionService.class, ConnectionFacade.class, ConnectionLifecycleFacade.class, ConnectionService.class, EnvironmentService.class, GitHubProxyClient.class, JobFacade.class, JobService.class, PrincipalJobFacade.class, PrincipalJobService.class, SharedTemplateFileStorage.class, TaskExecutionService.class, TriggerDefinitionService.class, TriggerExecutionService.class, TriggerLifecycleFacade.class, UserService.class, diff --git a/server/libs/modules/components/ai/agent/utils/src/test/resources/definition/agent-utils_v1.json b/server/libs/modules/components/ai/agent/utils/src/test/resources/definition/agent-utils_v1.json deleted file mode 100644 index e13c57aa915..00000000000 --- a/server/libs/modules/components/ai/agent/utils/src/test/resources/definition/agent-utils_v1.json +++ /dev/null @@ -1,219 +0,0 @@ -{ - "actions": null, - "clusterElements": [ { - "name": "fileSystemTools", - "description": "Read, write, and edit files with precise control.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": null, - "title": "File System Tools", - "type": { - "name": "CLAUDE_CODE_TOOLS", - "key": "claudeCodeTools", - "label": "Claude Code Tools", - "multipleElements": true, - "required": false - }, - "workflowNodeDescription": null - }, { - "name": "shellTools", - "description": "Execute shell commands with timeout control, background process management, and regex output filtering.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": null, - "title": "Shell Tools", - "type": { - "name": "CLAUDE_CODE_TOOLS", - "key": "claudeCodeTools", - "label": "Claude Code Tools", - "multipleElements": true, - "required": false - }, - "workflowNodeDescription": null - }, { - "name": "grepTool", - "description": "Pure Java grep implementation for code search with regex, glob filtering, and multiple output modes.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": null, - "title": "Grep Tool", - "type": { - "name": "CLAUDE_CODE_TOOLS", - "key": "claudeCodeTools", - "label": "Claude Code Tools", - "multipleElements": true, - "required": false - }, - "workflowNodeDescription": null - }, { - "name": "globTool", - "description": "Fast file pattern matching tool for finding files by name patterns with glob syntax.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": null, - "title": "Glob Tool", - "type": { - "name": "CLAUDE_CODE_TOOLS", - "key": "claudeCodeTools", - "label": "Claude Code Tools", - "multipleElements": true, - "required": false - }, - "workflowNodeDescription": null - }, { - "name": "smartWebFetchTool", - "description": "AI-powered web content summarization with caching.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": null, - "title": "Smart Web Fetch Tool", - "type": { - "name": "CLAUDE_CODE_TOOLS", - "key": "claudeCodeTools", - "label": "Claude Code Tools", - "multipleElements": true, - "required": false - }, - "workflowNodeDescription": null - }, { - "name": "braveWebSearchTool", - "description": "Web search with domain filtering using the Brave Search API.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": null, - "title": "Brave Web Search Tool", - "type": { - "name": "CLAUDE_CODE_TOOLS", - "key": "claudeCodeTools", - "label": "Claude Code Tools", - "multipleElements": true, - "required": false - }, - "workflowNodeDescription": null - }, { - "name": "skillsTool", - "description": "Extend AI agent capabilities with reusable, composable knowledge modules defined in Markdown with YAML front-matter.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": [ { - "advancedOption": null, - "controlType": "ARRAY_BUILDER", - "defaultValue": null, - "description": "Select skills to make available to the agent.", - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "items": [ { - "advancedOption": null, - "controlType": "SELECT", - "defaultValue": null, - "description": null, - "displayCondition": null, - "exampleValue": null, - "expressionEnabled": null, - "hidden": null, - "label": "Skill", - "languageId": null, - "maxLength": null, - "metadata": { }, - "minLength": null, - "name": "skillId", - "options": null, - "optionsDataSource": { - "options": { }, - "optionsLookupDependsOn": null - }, - "placeholder": null, - "regex": null, - "required": true, - "type": "STRING" - } ], - "label": "Skills", - "maxItems": null, - "metadata": { }, - "minItems": null, - "multipleValues": null, - "name": "skills", - "options": null, - "optionsDataSource": null, - "placeholder": "Choose a skill...", - "required": null, - "type": "ARRAY" - } ], - "title": "Skills Tool", - "type": { - "name": "TOOLS", - "key": "tools", - "label": "Tools", - "multipleElements": true, - "required": false - }, - "workflowNodeDescription": null - }, { - "name": "todoWriteTool", - "description": "Structured task management with state tracking.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": null, - "title": "Todo Write Tool", - "type": { - "name": "CLAUDE_CODE_TOOLS", - "key": "claudeCodeTools", - "label": "Claude Code Tools", - "multipleElements": true, - "required": false - }, - "workflowNodeDescription": null - }, { - "name": "taskTool", - "description": "Delegate complex tasks to specialized sub-agents for parallel execution.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": null, - "title": "Task Tool", - "type": { - "name": "CLAUDE_CODE_TOOLS", - "key": "claudeCodeTools", - "label": "Claude Code Tools", - "multipleElements": true, - "required": false - }, - "workflowNodeDescription": null - } ], - "componentCategories": [ { - "name": "artificial-intelligence", - "label": "Artificial Intelligence" - } ], - "connection": null, - "customAction": null, - "customActionHelp": null, - "description": "AI Agent Utils brings Claude Code-inspired tools and agent skills.", - "icon": "path:assets/agent-utils.svg", - "metadata": null, - "name": "aiAgentUtils", - "resources": null, - "tags": null, - "title": "AI Agent Utils", - "triggers": null, - "unifiedApi": null, - "version": 1 -} \ No newline at end of file diff --git a/server/libs/modules/components/ai/agent/utils/src/test/resources/definition/ai_agent-utils_v1.json b/server/libs/modules/components/ai/agent/utils/src/test/resources/definition/ai_agent-utils_v1.json index 69c979ad00b..e13c57aa915 100644 --- a/server/libs/modules/components/ai/agent/utils/src/test/resources/definition/ai_agent-utils_v1.json +++ b/server/libs/modules/components/ai/agent/utils/src/test/resources/definition/ai_agent-utils_v1.json @@ -1,23 +1,6 @@ { "actions": null, "clusterElements": [ { - "name": "askUserQuestionTool", - "description": "Ask the user clarifying questions to gather preferences, clarify instructions, or get decisions.", - "element": { }, - "help": null, - "outputDefinition": null, - "processErrorResponse": null, - "properties": null, - "title": "Ask User Question Tool", - "type": { - "name": "TOOLS", - "key": "tools", - "label": "Tools", - "multipleElements": true, - "required": false - }, - "workflowNodeDescription": null - }, { "name": "fileSystemTools", "description": "Read, write, and edit files with precise control.", "element": { }, diff --git a/server/libs/platform/platform-component/platform-component-service/src/main/java/com/bytechef/platform/component/facade/ConnectionDefinitionFacadeImpl.java b/server/libs/platform/platform-component/platform-component-service/src/main/java/com/bytechef/platform/component/facade/ConnectionDefinitionFacadeImpl.java index 2bd93c7f3c8..03ebdfac415 100644 --- a/server/libs/platform/platform-component/platform-component-service/src/main/java/com/bytechef/platform/component/facade/ConnectionDefinitionFacadeImpl.java +++ b/server/libs/platform/platform-component/platform-component-service/src/main/java/com/bytechef/platform/component/facade/ConnectionDefinitionFacadeImpl.java @@ -22,6 +22,7 @@ import com.bytechef.platform.component.service.ConnectionDefinitionService; import com.bytechef.platform.connection.domain.Connection; import com.bytechef.platform.connection.service.ConnectionService; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.jspecify.annotations.Nullable; import org.springframework.stereotype.Service; @@ -35,7 +36,7 @@ public class ConnectionDefinitionFacadeImpl implements ConnectionDefinitionFacad private final ConnectionDefinitionService connectionDefinitionService; private final TokenRefreshHandler tokenRefreshHandler; - @SuppressWarnings("E1") + @SuppressFBWarnings("EI2") public ConnectionDefinitionFacadeImpl( ConnectionService connectionService, ConnectionDefinitionService connectionDefinitionService, TokenRefreshHandler tokenRefreshHandler) {