From d4ec3f8b396294b6495e8e68fafcd5ef401f0087 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 7 Jul 2025 20:11:52 +0300 Subject: [PATCH] AI rule node: change structure of response format info in node config --- .../data/validation/ValidJsonSchema.java | 39 +++++++ .../common/util/JsonSchemaUtils.java | 12 +- .../dao/service/ConstraintValidator.java | 2 + .../dao/service/JsonSchemaValidator.java | 31 ++++++ .../thingsboard/rule/engine/ai/TbAiNode.java | 8 +- .../rule/engine/ai/TbAiNodeConfiguration.java | 21 +--- .../rule/engine/ai/TbResponseFormat.java | 103 ++++++++++++++++++ 7 files changed, 188 insertions(+), 28 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/JsonSchemaValidator.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.java b/common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.java new file mode 100644 index 0000000000..d37d7eb9e7 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/validation/ValidJsonSchema.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * 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 + * + * http://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 org.thingsboard.server.common.data.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Constraint(validatedBy = {}) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidJsonSchema { + + String message() default "must conform to the Draft 2020-12 meta-schema"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java b/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java index db45994826..57ddd8a5ac 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JsonSchemaUtils.java @@ -15,7 +15,7 @@ */ package org.thingsboard.common.util; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.JsonSchemaFactory; import com.networknt.schema.SchemaId; import com.networknt.schema.SchemaLocation; @@ -26,17 +26,15 @@ import java.util.Set; public final class JsonSchemaUtils { - private JsonSchemaUtils() { - throw new AssertionError("Can't instantiate utility class"); - } + private JsonSchemaUtils() {} /** - * Validates that the provided JsonNode is a valid JSON Schema (Draft 2020-12). + * Validates that the provided ObjectNode is a valid JSON Schema (Draft 2020-12). * - * @param schemaNode the JSON Schema document as a JsonNode + * @param schemaNode the JSON Schema document as an ObjectNode * @return true if the schema is well-formed, false otherwise */ - public static boolean isValidJsonSchema(JsonNode schemaNode) { + public static boolean isValidJsonSchema(ObjectNode schemaNode) { Set errors = JsonSchemaFactory .getInstance(SpecVersion.VersionFlag.V202012) .getSchema(SchemaLocation.of(SchemaId.V202012)) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java index d220762f1b..0fd55bb494 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoNullChar; import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.RateLimit; +import org.thingsboard.server.common.data.validation.ValidJsonSchema; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.Collection; @@ -107,6 +108,7 @@ public class ConstraintValidator { constraintMapping.constraintDefinition(Length.class).validatedBy(StringLengthValidator.class); constraintMapping.constraintDefinition(RateLimit.class).validatedBy(RateLimitValidator.class); constraintMapping.constraintDefinition(NoNullChar.class).validatedBy(NoNullCharValidator.class); + constraintMapping.constraintDefinition(ValidJsonSchema.class).validatedBy(JsonSchemaValidator.class); return constraintMapping; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/JsonSchemaValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/JsonSchemaValidator.java new file mode 100644 index 0000000000..eefefbb3d7 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/JsonSchemaValidator.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * 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 + * + * http://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 org.thingsboard.server.dao.service; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.thingsboard.common.util.JsonSchemaUtils; +import org.thingsboard.server.common.data.validation.ValidJsonSchema; + +public final class JsonSchemaValidator implements ConstraintValidator { + + @Override + public boolean isValid(ObjectNode schema, ConstraintValidatorContext context) { + return schema == null || JsonSchemaUtils.isValidJsonSchema(schema); + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index fe05712bea..62d92ef2ce 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -23,7 +23,6 @@ import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ResponseFormat; -import dev.langchain4j.model.chat.request.ResponseFormatType; import dev.langchain4j.model.chat.response.ChatResponse; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; @@ -81,11 +80,8 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { } // LC4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT - if (config.getResponseFormatType() == ResponseFormatType.JSON) { - responseFormat = ResponseFormat.builder() - .type(config.getResponseFormatType()) - .jsonSchema(config.getJsonSchema() != null ? Langchain4jJsonSchemaAdapter.fromObjectNode(config.getJsonSchema()) : null) - .build(); + if (config.getResponseFormat().type() == TbResponseFormat.TbResponseFormatType.JSON) { + responseFormat = config.getResponseFormat().toLangChainResponseFormat(); } systemPrompt = config.getSystemPrompt(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index d9c815192d..ebfcf943f7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -15,21 +15,19 @@ */ package org.thingsboard.rule.engine.ai; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.node.ObjectNode; -import dev.langchain4j.model.chat.request.ResponseFormatType; -import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.Data; -import org.thingsboard.common.util.JsonSchemaUtils; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.server.common.data.id.AiModelSettingsId; import org.thingsboard.server.common.data.validation.Length; +import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat; + @Data public class TbAiNodeConfiguration implements NodeConfiguration { @@ -45,26 +43,19 @@ public class TbAiNodeConfiguration implements NodeConfiguration