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

@ -15,13 +15,14 @@
--
CREATE TABLE ai_settings (
id UUID NOT NULL PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
version BIGINT NOT NULL DEFAULT 1,
name VARCHAR(255) NOT NULL,
provider VARCHAR(255) NOT NULL,
model VARCHAR(255) NOT NULL,
configuration JSONB NOT NULL,
id UUID NOT NULL PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
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,
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

@ -950,13 +950,14 @@ CREATE TABLE IF NOT EXISTS cf_debug_event (
) PARTITION BY RANGE (ts);
CREATE TABLE IF NOT EXISTS ai_settings (
id UUID NOT NULL PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
version BIGINT NOT NULL DEFAULT 1,
name VARCHAR(255) NOT NULL,
provider VARCHAR(255) NOT NULL,
model VARCHAR(255) NOT NULL,
configuration JSONB NOT NULL,
id UUID NOT NULL PRIMARY KEY,
created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL,
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,
model_config JSONB,
CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name)
);