AI rule node: add polymorphic JSON config to AI settings

This commit is contained in:
Dmytro Skarzhynets 2025-05-16 20:06:42 +03:00
parent ad0161e3df
commit f2075c6c39
No known key found for this signature in database
GPG Key ID: 2B51652F224037DF
11 changed files with 200 additions and 30 deletions

View File

@ -15,13 +15,13 @@
-- --
CREATE TABLE ai_settings ( CREATE TABLE ai_settings (
id UUID NOT NULL PRIMARY KEY, id UUID NOT NULL PRIMARY KEY,
created_time BIGINT NOT NULL, created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL, tenant_id UUID NOT NULL,
version BIGINT NOT NULL DEFAULT 1, version BIGINT NOT NULL DEFAULT 1,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
provider VARCHAR(255) NOT NULL, provider VARCHAR(255) NOT NULL,
model VARCHAR(255) NOT NULL, model VARCHAR(255) NOT NULL,
api_key VARCHAR(1000) NOT NULL, configuration JSONB NOT NULL,
CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name)
); );

View File

@ -46,15 +46,15 @@ class AiServiceImpl implements RuleEngineAiService {
return switch (aiSettings.getProvider()) { return switch (aiSettings.getProvider()) {
case OPENAI -> OpenAiChatModel.builder() case OPENAI -> OpenAiChatModel.builder()
.apiKey(aiSettings.getApiKey()) .apiKey(aiSettings.getConfiguration().getApiKey())
.modelName(aiSettings.getModel()) .modelName(aiSettings.getModel())
.build(); .build();
case MISTRAL_AI -> MistralAiChatModel.builder() case MISTRAL_AI -> MistralAiChatModel.builder()
.apiKey(aiSettings.getApiKey()) .apiKey(aiSettings.getConfiguration().getApiKey())
.modelName(aiSettings.getModel()) .modelName(aiSettings.getModel())
.build(); .build();
case GOOGLE_AI_GEMINI -> GoogleAiGeminiChatModel.builder() case GOOGLE_AI_GEMINI -> GoogleAiGeminiChatModel.builder()
.apiKey(aiSettings.getApiKey()) .apiKey(aiSettings.getConfiguration().getApiKey())
.modelName(aiSettings.getModel()) .modelName(aiSettings.getModel())
.build(); .build();
}; };

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;
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 = "provider",
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")
})
public abstract class AiConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "API key for authenticating with the AI provider",
example = "sk-********************************"
)
private String apiKey;
}

View File

@ -83,11 +83,10 @@ public final class AiSettings extends BaseData<AiSettingsId> implements HasTenan
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.WRITE_ONLY, accessMode = Schema.AccessMode.READ_WRITE,
description = "API key for authenticating with the selected AI provider", description = "Provider-specific settings for the chosen AI model"
example = "sk-********************************"
) )
String apiKey; AiConfig configuration;
public AiSettings() {} public AiSettings() {}

View File

@ -0,0 +1,40 @@
/**
* 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;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(
name = "GoogleAiGemini",
description = "Configuration properties for the Google AI Gemini"
)
public class GoogleAiGeminiConfig extends AiConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Name of the AI provider",
example = "GOOGLE_AI_GEMINI",
allowableValues = "GOOGLE_AI_GEMINI",
type = "string"
)
private AiProvider provider = AiProvider.GOOGLE_AI_GEMINI;
}

View File

@ -0,0 +1,40 @@
/**
* 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;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(
name = "MistralAi",
description = "Configuration properties for the Mistral AI"
)
public class MistralAiConfig extends AiConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Name of the AI provider",
example = "MISTRAL_AI",
allowableValues = "MISTRAL_AI",
type = "string"
)
private AiProvider provider = AiProvider.MISTRAL_AI;
}

View File

@ -0,0 +1,40 @@
/**
* 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;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(
name = "OpenAiConfig",
description = "Configuration properties for the OpenAI"
)
public class OpenAiConfig extends AiConfig {
@Schema(
requiredMode = Schema.RequiredMode.REQUIRED,
accessMode = Schema.AccessMode.READ_WRITE,
description = "Name of the AI provider",
example = "OPENAI",
allowableValues = "OPENAI",
type = "string"
)
private AiProvider provider = AiProvider.OPENAI;
}

View File

@ -36,7 +36,7 @@ public final class AiSettingsId extends UUIDBased implements EntityId {
@Override @Override
@Schema( @Schema(
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
description = "Entity type of the AI settings, always 'AI_SETTINGS'", description = "Entity type of the AI settings",
example = "AI_SETTINGS", example = "AI_SETTINGS",
allowableValues = "AI_SETTINGS" allowableValues = "AI_SETTINGS"
) )

View File

@ -747,7 +747,7 @@ public class ModelConstants {
public static final String AI_SETTINGS_NAME_COLUMN_NAME = NAME_PROPERTY; 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_COLUMN_NAME = "provider";
public static final String AI_SETTINGS_MODEL_COLUMN_NAME = "model"; public static final String AI_SETTINGS_MODEL_COLUMN_NAME = "model";
public static final String AI_SETTINGS_API_KEY_COLUMN_NAME = "api_key"; public static final String AI_SETTINGS_CONFIGURATION_COLUMN_NAME = "configuration";
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}; 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

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.model.sql; package org.thingsboard.server.dao.model.sql;
import io.hypersistence.utils.hibernate.type.json.JsonBinaryType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
@ -23,7 +24,9 @@ import jakarta.persistence.Table;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import org.hibernate.annotations.Type;
import org.hibernate.proxy.HibernateProxy; 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.AiProvider;
import org.thingsboard.server.common.data.ai.AiSettings; import org.thingsboard.server.common.data.ai.AiSettings;
import org.thingsboard.server.common.data.id.AiSettingsId; import org.thingsboard.server.common.data.id.AiSettingsId;
@ -41,7 +44,7 @@ import java.util.UUID;
@Table(name = ModelConstants.AI_SETTINGS_TABLE_NAME) @Table(name = ModelConstants.AI_SETTINGS_TABLE_NAME)
public class AiSettingsEntity extends BaseVersionedEntity<AiSettings> { public class AiSettingsEntity extends BaseVersionedEntity<AiSettings> {
@Column(name = ModelConstants.AI_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "uuid") @Column(name = ModelConstants.AI_SETTINGS_TENANT_ID_COLUMN_NAME, nullable = false, columnDefinition = "UUID")
private UUID tenantId; private UUID tenantId;
@Column(name = ModelConstants.AI_SETTINGS_NAME_COLUMN_NAME, nullable = false) @Column(name = ModelConstants.AI_SETTINGS_NAME_COLUMN_NAME, nullable = false)
@ -54,8 +57,9 @@ public class AiSettingsEntity extends BaseVersionedEntity<AiSettings> {
@Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false) @Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false)
private String model; private String model;
@Column(name = ModelConstants.AI_SETTINGS_API_KEY_COLUMN_NAME, nullable = false) @Type(JsonBinaryType.class)
private String apiKey; @Column(name = ModelConstants.AI_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB")
private AiConfig configuration;
public AiSettingsEntity() {} public AiSettingsEntity() {}
@ -65,7 +69,7 @@ public class AiSettingsEntity extends BaseVersionedEntity<AiSettings> {
name = aiSettings.getName(); name = aiSettings.getName();
provider = aiSettings.getProvider(); provider = aiSettings.getProvider();
model = aiSettings.getModel(); model = aiSettings.getModel();
apiKey = aiSettings.getApiKey(); configuration = aiSettings.getConfiguration();
} }
@Override @Override
@ -77,7 +81,7 @@ public class AiSettingsEntity extends BaseVersionedEntity<AiSettings> {
settings.setName(name); settings.setName(name);
settings.setProvider(provider); settings.setProvider(provider);
settings.setModel(model); settings.setModel(model);
settings.setApiKey(apiKey); settings.setConfiguration(configuration);
return settings; return settings;
} }

View File

@ -950,13 +950,13 @@ CREATE TABLE IF NOT EXISTS cf_debug_event (
) PARTITION BY RANGE (ts); ) PARTITION BY RANGE (ts);
CREATE TABLE IF NOT EXISTS ai_settings ( CREATE TABLE IF NOT EXISTS ai_settings (
id UUID NOT NULL PRIMARY KEY, id UUID NOT NULL PRIMARY KEY,
created_time BIGINT NOT NULL, created_time BIGINT NOT NULL,
tenant_id UUID NOT NULL, tenant_id UUID NOT NULL,
version BIGINT NOT NULL DEFAULT 1, version BIGINT NOT NULL DEFAULT 1,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
provider VARCHAR(255) NOT NULL, provider VARCHAR(255) NOT NULL,
model VARCHAR(255) NOT NULL, model VARCHAR(255) NOT NULL,
api_key VARCHAR(1000) NOT NULL, configuration JSONB NOT NULL,
CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name)
); );