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