AI rule node: change structure of response format info in node config
This commit is contained in:
		
							parent
							
								
									749b327795
								
							
						
					
					
						commit
						d4ec3f8b39
					
				@ -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<? extends Payload>[] payload() default {};
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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<ValidationMessage> errors = JsonSchemaFactory
 | 
			
		||||
                .getInstance(SpecVersion.VersionFlag.V202012)
 | 
			
		||||
                .getSchema(SchemaLocation.of(SchemaId.V202012))
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<ValidJsonSchema, ObjectNode> {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isValid(ObjectNode schema, ConstraintValidatorContext context) {
 | 
			
		||||
        return schema == null || JsonSchemaUtils.isValidJsonSchema(schema);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
@ -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<TbAiNodeConfiguration> {
 | 
			
		||||
 | 
			
		||||
@ -45,26 +43,19 @@ public class TbAiNodeConfiguration implements NodeConfiguration<TbAiNodeConfigur
 | 
			
		||||
    private String userPrompt;
 | 
			
		||||
 | 
			
		||||
    @NotNull
 | 
			
		||||
    private ResponseFormatType responseFormatType;
 | 
			
		||||
 | 
			
		||||
    private ObjectNode jsonSchema;
 | 
			
		||||
    @Valid
 | 
			
		||||
    private TbResponseFormat responseFormat;
 | 
			
		||||
 | 
			
		||||
    @Min(value = 1, message = "must be at least 1 second")
 | 
			
		||||
    @Max(value = 600, message = "cannot exceed 600 seconds (10 minutes)")
 | 
			
		||||
    private int timeoutSeconds;
 | 
			
		||||
 | 
			
		||||
    @JsonIgnore
 | 
			
		||||
    @AssertTrue(message = "provided JSON Schema must conform to the Draft 2020-12 meta-schema")
 | 
			
		||||
    public boolean isJsonSchemaValid() {
 | 
			
		||||
        return jsonSchema == null || JsonSchemaUtils.isValidJsonSchema(jsonSchema);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public TbAiNodeConfiguration defaultConfiguration() {
 | 
			
		||||
        var configuration = new TbAiNodeConfiguration();
 | 
			
		||||
        configuration.setSystemPrompt("You are helpful assistant. Your response must be in JSON format.");
 | 
			
		||||
        configuration.setUserPrompt("Tell me a joke.");
 | 
			
		||||
        configuration.setResponseFormatType(ResponseFormatType.JSON);
 | 
			
		||||
        configuration.setResponseFormat(new TbJsonResponseFormat());
 | 
			
		||||
        configuration.setTimeoutSeconds(60);
 | 
			
		||||
        return configuration;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,103 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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.rule.engine.ai;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
 | 
			
		||||
import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
			
		||||
import dev.langchain4j.model.chat.request.ResponseFormat;
 | 
			
		||||
import dev.langchain4j.model.chat.request.ResponseFormatType;
 | 
			
		||||
import jakarta.validation.constraints.NotNull;
 | 
			
		||||
import org.thingsboard.server.common.data.validation.ValidJsonSchema;
 | 
			
		||||
 | 
			
		||||
import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat;
 | 
			
		||||
import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonSchemaResponseFormat;
 | 
			
		||||
import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbTextResponseFormat;
 | 
			
		||||
 | 
			
		||||
@JsonTypeInfo(
 | 
			
		||||
        use = JsonTypeInfo.Id.NAME,
 | 
			
		||||
        include = JsonTypeInfo.As.PROPERTY,
 | 
			
		||||
        property = "type"
 | 
			
		||||
)
 | 
			
		||||
@JsonSubTypes({
 | 
			
		||||
        @JsonSubTypes.Type(value = TbTextResponseFormat.class, name = "TEXT"),
 | 
			
		||||
        @JsonSubTypes.Type(value = TbJsonResponseFormat.class, name = "JSON"),
 | 
			
		||||
        @JsonSubTypes.Type(value = TbJsonSchemaResponseFormat.class, name = "JSON_SCHEMA")
 | 
			
		||||
})
 | 
			
		||||
public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonResponseFormat, TbJsonSchemaResponseFormat {
 | 
			
		||||
 | 
			
		||||
    TbResponseFormatType type();
 | 
			
		||||
 | 
			
		||||
    ResponseFormat toLangChainResponseFormat();
 | 
			
		||||
 | 
			
		||||
    enum TbResponseFormatType {
 | 
			
		||||
 | 
			
		||||
        TEXT,
 | 
			
		||||
        JSON,
 | 
			
		||||
        JSON_SCHEMA
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    record TbTextResponseFormat() implements TbResponseFormat {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public TbResponseFormatType type() {
 | 
			
		||||
            return TbResponseFormatType.TEXT;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ResponseFormat toLangChainResponseFormat() {
 | 
			
		||||
            return ResponseFormat.builder()
 | 
			
		||||
                    .type(ResponseFormatType.TEXT)
 | 
			
		||||
                    .build();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    record TbJsonResponseFormat() implements TbResponseFormat {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public TbResponseFormatType type() {
 | 
			
		||||
            return TbResponseFormatType.JSON;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ResponseFormat toLangChainResponseFormat() {
 | 
			
		||||
            return ResponseFormat.builder()
 | 
			
		||||
                    .type(ResponseFormatType.JSON)
 | 
			
		||||
                    .build();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    record TbJsonSchemaResponseFormat(@NotNull @ValidJsonSchema ObjectNode schema) implements TbResponseFormat {
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public TbResponseFormatType type() {
 | 
			
		||||
            return TbResponseFormatType.JSON_SCHEMA;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public ResponseFormat toLangChainResponseFormat() {
 | 
			
		||||
            return ResponseFormat.builder()
 | 
			
		||||
                    .type(ResponseFormatType.JSON)
 | 
			
		||||
                    .jsonSchema(Langchain4jJsonSchemaAdapter.fromObjectNode(schema))
 | 
			
		||||
                    .build();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user