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;
|
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.JsonSchemaFactory;
|
||||||
import com.networknt.schema.SchemaId;
|
import com.networknt.schema.SchemaId;
|
||||||
import com.networknt.schema.SchemaLocation;
|
import com.networknt.schema.SchemaLocation;
|
||||||
@ -26,17 +26,15 @@ import java.util.Set;
|
|||||||
|
|
||||||
public final class JsonSchemaUtils {
|
public final class JsonSchemaUtils {
|
||||||
|
|
||||||
private JsonSchemaUtils() {
|
private JsonSchemaUtils() {}
|
||||||
throw new AssertionError("Can't instantiate utility class");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* @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
|
Set<ValidationMessage> errors = JsonSchemaFactory
|
||||||
.getInstance(SpecVersion.VersionFlag.V202012)
|
.getInstance(SpecVersion.VersionFlag.V202012)
|
||||||
.getSchema(SchemaLocation.of(SchemaId.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.NoNullChar;
|
||||||
import org.thingsboard.server.common.data.validation.NoXss;
|
import org.thingsboard.server.common.data.validation.NoXss;
|
||||||
import org.thingsboard.server.common.data.validation.RateLimit;
|
import org.thingsboard.server.common.data.validation.RateLimit;
|
||||||
|
import org.thingsboard.server.common.data.validation.ValidJsonSchema;
|
||||||
import org.thingsboard.server.dao.exception.DataValidationException;
|
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@ -107,6 +108,7 @@ public class ConstraintValidator {
|
|||||||
constraintMapping.constraintDefinition(Length.class).validatedBy(StringLengthValidator.class);
|
constraintMapping.constraintDefinition(Length.class).validatedBy(StringLengthValidator.class);
|
||||||
constraintMapping.constraintDefinition(RateLimit.class).validatedBy(RateLimitValidator.class);
|
constraintMapping.constraintDefinition(RateLimit.class).validatedBy(RateLimitValidator.class);
|
||||||
constraintMapping.constraintDefinition(NoNullChar.class).validatedBy(NoNullCharValidator.class);
|
constraintMapping.constraintDefinition(NoNullChar.class).validatedBy(NoNullCharValidator.class);
|
||||||
|
constraintMapping.constraintDefinition(ValidJsonSchema.class).validatedBy(JsonSchemaValidator.class);
|
||||||
return constraintMapping;
|
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.data.message.UserMessage;
|
||||||
import dev.langchain4j.model.chat.request.ChatRequest;
|
import dev.langchain4j.model.chat.request.ChatRequest;
|
||||||
import dev.langchain4j.model.chat.request.ResponseFormat;
|
import dev.langchain4j.model.chat.request.ResponseFormat;
|
||||||
import dev.langchain4j.model.chat.request.ResponseFormatType;
|
|
||||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
import org.thingsboard.common.util.JacksonUtil;
|
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
|
// LC4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT
|
||||||
if (config.getResponseFormatType() == ResponseFormatType.JSON) {
|
if (config.getResponseFormat().type() == TbResponseFormat.TbResponseFormatType.JSON) {
|
||||||
responseFormat = ResponseFormat.builder()
|
responseFormat = config.getResponseFormat().toLangChainResponseFormat();
|
||||||
.type(config.getResponseFormatType())
|
|
||||||
.jsonSchema(config.getJsonSchema() != null ? Langchain4jJsonSchemaAdapter.fromObjectNode(config.getJsonSchema()) : null)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
systemPrompt = config.getSystemPrompt();
|
systemPrompt = config.getSystemPrompt();
|
||||||
|
|||||||
@ -15,21 +15,19 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.rule.engine.ai;
|
package org.thingsboard.rule.engine.ai;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import jakarta.validation.Valid;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import dev.langchain4j.model.chat.request.ResponseFormatType;
|
|
||||||
import jakarta.validation.constraints.AssertTrue;
|
|
||||||
import jakarta.validation.constraints.Max;
|
import jakarta.validation.constraints.Max;
|
||||||
import jakarta.validation.constraints.Min;
|
import jakarta.validation.constraints.Min;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.thingsboard.common.util.JsonSchemaUtils;
|
|
||||||
import org.thingsboard.rule.engine.api.NodeConfiguration;
|
import org.thingsboard.rule.engine.api.NodeConfiguration;
|
||||||
import org.thingsboard.server.common.data.id.AiModelSettingsId;
|
import org.thingsboard.server.common.data.id.AiModelSettingsId;
|
||||||
import org.thingsboard.server.common.data.validation.Length;
|
import org.thingsboard.server.common.data.validation.Length;
|
||||||
|
|
||||||
|
import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class TbAiNodeConfiguration implements NodeConfiguration<TbAiNodeConfiguration> {
|
public class TbAiNodeConfiguration implements NodeConfiguration<TbAiNodeConfiguration> {
|
||||||
|
|
||||||
@ -45,26 +43,19 @@ public class TbAiNodeConfiguration implements NodeConfiguration<TbAiNodeConfigur
|
|||||||
private String userPrompt;
|
private String userPrompt;
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private ResponseFormatType responseFormatType;
|
@Valid
|
||||||
|
private TbResponseFormat responseFormat;
|
||||||
private ObjectNode jsonSchema;
|
|
||||||
|
|
||||||
@Min(value = 1, message = "must be at least 1 second")
|
@Min(value = 1, message = "must be at least 1 second")
|
||||||
@Max(value = 600, message = "cannot exceed 600 seconds (10 minutes)")
|
@Max(value = 600, message = "cannot exceed 600 seconds (10 minutes)")
|
||||||
private int timeoutSeconds;
|
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
|
@Override
|
||||||
public TbAiNodeConfiguration defaultConfiguration() {
|
public TbAiNodeConfiguration defaultConfiguration() {
|
||||||
var configuration = new TbAiNodeConfiguration();
|
var configuration = new TbAiNodeConfiguration();
|
||||||
configuration.setSystemPrompt("You are helpful assistant. Your response must be in JSON format.");
|
configuration.setSystemPrompt("You are helpful assistant. Your response must be in JSON format.");
|
||||||
configuration.setUserPrompt("Tell me a joke.");
|
configuration.setUserPrompt("Tell me a joke.");
|
||||||
configuration.setResponseFormatType(ResponseFormatType.JSON);
|
configuration.setResponseFormat(new TbJsonResponseFormat());
|
||||||
configuration.setTimeoutSeconds(60);
|
configuration.setTimeoutSeconds(60);
|
||||||
return configuration;
|
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