AI rule node: split AI config into AI provider config and AI model config; add support for model temperature

This commit is contained in:
Dmytro Skarzhynets 2025-05-22 11:28:21 +03:00
parent d44bbe4dd8
commit 45fbbf201f
No known key found for this signature in database
GPG Key ID: 2B51652F224037DF
15 changed files with 301 additions and 59 deletions

View File

@ -21,7 +21,8 @@ CREATE TABLE ai_settings (
version BIGINT NOT NULL DEFAULT 1,
name VARCHAR(255) NOT NULL,
provider VARCHAR(255) NOT NULL,
provider_config JSONB NOT NULL,
model VARCHAR(255) NOT NULL,
configuration JSONB NOT NULL,
model_config JSONB,
CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name)
);

View File

@ -23,6 +23,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RuleEngineAiService;
import org.thingsboard.server.common.data.ai.AiSettings;
import org.thingsboard.server.common.data.ai.model.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.id.AiSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.ai.AiSettingsService;
@ -45,18 +47,39 @@ class AiServiceImpl implements RuleEngineAiService {
var aiSettings = aiSettingsOpt.get();
return switch (aiSettings.getProvider()) {
case OPENAI -> OpenAiChatModel.builder()
.apiKey(aiSettings.getConfiguration().getApiKey())
.modelName(aiSettings.getModel())
.build();
case MISTRAL_AI -> MistralAiChatModel.builder()
.apiKey(aiSettings.getConfiguration().getApiKey())
.modelName(aiSettings.getModel())
.build();
case GOOGLE_AI_GEMINI -> GoogleAiGeminiChatModel.builder()
.apiKey(aiSettings.getConfiguration().getApiKey())
.modelName(aiSettings.getModel())
.build();
case OPENAI -> {
var modelBuilder = OpenAiChatModel.builder()
.apiKey(aiSettings.getProviderConfig().getApiKey())
.modelName(aiSettings.getModel());
if (aiSettings.getModelConfig() instanceof OpenAiChatModelConfig config) {
modelBuilder.temperature(config.getTemperature());
}
yield modelBuilder.build();
}
case MISTRAL_AI -> {
var modelBuilder = MistralAiChatModel.builder()
.apiKey(aiSettings.getProviderConfig().getApiKey())
.modelName(aiSettings.getModel());
if (aiSettings.getModelConfig() instanceof MistralAiChatModelConfig config) {
modelBuilder.temperature(config.getTemperature());
}
yield modelBuilder.build();
}
case GOOGLE_AI_GEMINI -> {
var modelBuilder = GoogleAiGeminiChatModel.builder()
.apiKey(aiSettings.getProviderConfig().getApiKey())
.modelName(aiSettings.getModel());
if (aiSettings.getModelConfig() instanceof OpenAiChatModelConfig config) {
modelBuilder.temperature(config.getTemperature());
}
yield modelBuilder.build();
}
};
}

View File

@ -24,6 +24,9 @@ import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.AiProviderConfig;
import org.thingsboard.server.common.data.id.AiSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
@ -76,17 +79,27 @@ public final class AiSettings extends BaseData<AiSettingsId> implements HasTenan
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Identifier of the AI model to use",
description = "Configuration specific to the AI provider"
)
AiProviderConfig providerConfig;
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Identifier of the AI model",
example = "gpt-4o-mini"
)
String model;
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Settings specific to the selected AI provider and model"
description = """
Optional configuration specific to the AI model.
If provided, it must be one of the known AiModelConfig subtypes and any settings
you specify will override the models defaults; if omitted, the model will run with its built-in defaults."""
)
AiConfig configuration;
AiModelConfig modelConfig;
public AiSettings() {}

View File

@ -0,0 +1,47 @@
/**
* 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.ai.model;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "model",
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "gpt-4o"),
@JsonSubTypes.Type(value = OpenAiChatModelConfig.class, name = "gpt-4o-mini"),
@JsonSubTypes.Type(value = GoogleAiGeminiChatModelConfig.class, name = "gemini-2.0-flash"),
@JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "mistral-medium-latest")
})
public abstract class AiModelConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Identifier of the AI model"
)
private String model;
}

View File

@ -0,0 +1,50 @@
/**
* 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.ai.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@Schema(
name = "GoogleAiGeminiChatModelConfig",
description = "Configuration for Google AI Gemini chat models"
)
public final class GoogleAiGeminiChatModelConfig extends AiModelConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Identifier of the AI model",
allowableValues = "gemini-2.0-flash",
example = "gemini-2.0-flash"
)
public String getModel() {
return super.getModel();
}
@Schema(
accessMode = Schema.AccessMode.READ_WRITE,
description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)",
example = "0.7"
)
private Double temperature;
}

View File

@ -0,0 +1,50 @@
/**
* 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.ai.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@Schema(
name = "MistralAiChatModelConfig",
description = "Configuration for Mistral AI chat models"
)
public final class MistralAiChatModelConfig extends AiModelConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Identifier of the AI model",
allowableValues = "mistral-medium-latest",
example = "mistral-medium-latest"
)
public String getModel() {
return super.getModel();
}
@Schema(
accessMode = Schema.AccessMode.READ_WRITE,
description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)",
example = "0.7"
)
private Double temperature;
}

View File

@ -0,0 +1,50 @@
/**
* 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.ai.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@Schema(
name = "OpenAiChatModelConfig",
description = "Configuration for OpenAI chat models"
)
public final class OpenAiChatModelConfig extends AiModelConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Identifier of the AI model",
allowableValues = {"gpt-4o", "gpt-4o-mini"},
example = "gpt-4o"
)
public String getModel() {
return super.getModel();
}
@Schema(
accessMode = Schema.AccessMode.READ_WRITE,
description = "Sampling temperature to control randomness: 0.0 (most deterministic) to 1.0 (most creative)",
example = "0.7"
)
private Double temperature;
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.ai;
package org.thingsboard.server.common.data.ai.provider;
public enum AiProvider {

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.ai;
package org.thingsboard.server.common.data.ai.provider;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@ -30,17 +30,16 @@ import lombok.NoArgsConstructor;
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OpenAiConfig.class, name = "OPENAI"),
@JsonSubTypes.Type(value = GoogleAiGeminiConfig.class, name = "GOOGLE_AI_GEMINI"),
@JsonSubTypes.Type(value = MistralAiConfig.class, name = "MISTRAL_AI")
@JsonSubTypes.Type(value = OpenAiProviderConfig.class, name = "OPENAI"),
@JsonSubTypes.Type(value = GoogleAiGeminiProviderConfig.class, name = "GOOGLE_AI_GEMINI"),
@JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI")
})
public abstract class AiConfig {
public abstract class AiProviderConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "API key for authenticating with the AI provider",
example = "sk-********************************"
description = "API key for authenticating with the AI provider"
)
private String apiKey;

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.ai;
package org.thingsboard.server.common.data.ai.provider;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -22,10 +22,10 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(
name = "GoogleAiGemini",
description = "Configuration properties for the Google AI Gemini"
name = "GoogleAiGeminiProviderConfig",
description = "Configuration for the Google AI Gemini provider"
)
public class GoogleAiGeminiConfig extends AiConfig {
public final class GoogleAiGeminiProviderConfig extends AiProviderConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.ai;
package org.thingsboard.server.common.data.ai.provider;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -22,10 +22,10 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(
name = "MistralAi",
description = "Configuration properties for the Mistral AI"
name = "MistralAiProviderConfig",
description = "Configuration for the Mistral AI provider"
)
public class MistralAiConfig extends AiConfig {
public final class MistralAiProviderConfig extends AiProviderConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.ai;
package org.thingsboard.server.common.data.ai.provider;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -22,10 +22,10 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(
name = "OpenAiConfig",
description = "Configuration properties for the OpenAI"
name = "OpenAiProviderConfig",
description = "Configuration for the OpenAI provider"
)
public class OpenAiConfig extends AiConfig {
public final class OpenAiProviderConfig extends AiProviderConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,

View File

@ -746,8 +746,9 @@ public class ModelConstants {
public static final String AI_SETTINGS_TENANT_ID_COLUMN_NAME = TENANT_ID_COLUMN;
public static final String AI_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY;
public static final String AI_SETTINGS_PROVIDER_COLUMN_NAME = "provider";
public static final String AI_SETTINGS_PROVIDER_CONFIG_COLUMN_NAME = "provider_config";
public static final String AI_SETTINGS_MODEL_COLUMN_NAME = "model";
public static final String AI_SETTINGS_CONFIGURATION_COLUMN_NAME = "configuration";
public static final String AI_SETTINGS_MODEL_CONFIG_COLUMN_NAME = "model_config";
protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN};

View File

@ -26,9 +26,10 @@ import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.Type;
import org.hibernate.proxy.HibernateProxy;
import org.thingsboard.server.common.data.ai.AiConfig;
import org.thingsboard.server.common.data.ai.AiProvider;
import org.thingsboard.server.common.data.ai.AiSettings;
import org.thingsboard.server.common.data.ai.model.AiModelConfig;
import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.AiProviderConfig;
import org.thingsboard.server.common.data.id.AiSettingsId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.model.BaseVersionedEntity;
@ -54,12 +55,16 @@ public class AiSettingsEntity extends BaseVersionedEntity<AiSettings> {
@Column(name = ModelConstants.AI_SETTINGS_PROVIDER_COLUMN_NAME, nullable = false)
private AiProvider provider;
@Type(JsonBinaryType.class)
@Column(name = ModelConstants.AI_SETTINGS_PROVIDER_CONFIG_COLUMN_NAME, nullable = false, columnDefinition = "JSONB")
private AiProviderConfig providerConfig;
@Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false)
private String model;
@Type(JsonBinaryType.class)
@Column(name = ModelConstants.AI_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB")
private AiConfig configuration;
@Column(name = ModelConstants.AI_SETTINGS_MODEL_CONFIG_COLUMN_NAME, columnDefinition = "JSONB")
private AiModelConfig modelConfig;
public AiSettingsEntity() {}
@ -68,8 +73,9 @@ public class AiSettingsEntity extends BaseVersionedEntity<AiSettings> {
tenantId = getTenantUuid(aiSettings.getTenantId());
name = aiSettings.getName();
provider = aiSettings.getProvider();
providerConfig = aiSettings.getProviderConfig();
model = aiSettings.getModel();
configuration = aiSettings.getConfiguration();
modelConfig = aiSettings.getModelConfig();
}
@Override
@ -80,8 +86,9 @@ public class AiSettingsEntity extends BaseVersionedEntity<AiSettings> {
settings.setTenantId(TenantId.fromUUID(tenantId));
settings.setName(name);
settings.setProvider(provider);
settings.setProviderConfig(providerConfig);
settings.setModel(model);
settings.setConfiguration(configuration);
settings.setModelConfig(modelConfig);
return settings;
}

View File

@ -956,7 +956,8 @@ CREATE TABLE IF NOT EXISTS ai_settings (
version BIGINT NOT NULL DEFAULT 1,
name VARCHAR(255) NOT NULL,
provider VARCHAR(255) NOT NULL,
provider_config JSONB NOT NULL,
model VARCHAR(255) NOT NULL,
configuration JSONB NOT NULL,
model_config JSONB,
CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name)
);