diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index f25ca02939..e148403be2 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -15,13 +15,13 @@ -- 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, - api_key VARCHAR(1000) 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, + model VARCHAR(255) NOT NULL, + configuration JSONB NOT NULL, CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) ); diff --git a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java index 9306ce5c71..7207501d7e 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/AiServiceImpl.java @@ -46,15 +46,15 @@ class AiServiceImpl implements RuleEngineAiService { return switch (aiSettings.getProvider()) { case OPENAI -> OpenAiChatModel.builder() - .apiKey(aiSettings.getApiKey()) + .apiKey(aiSettings.getConfiguration().getApiKey()) .modelName(aiSettings.getModel()) .build(); case MISTRAL_AI -> MistralAiChatModel.builder() - .apiKey(aiSettings.getApiKey()) + .apiKey(aiSettings.getConfiguration().getApiKey()) .modelName(aiSettings.getModel()) .build(); case GOOGLE_AI_GEMINI -> GoogleAiGeminiChatModel.builder() - .apiKey(aiSettings.getApiKey()) + .apiKey(aiSettings.getConfiguration().getApiKey()) .modelName(aiSettings.getModel()) .build(); }; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiConfig.java new file mode 100644 index 0000000000..5c37d21454 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiConfig.java @@ -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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java index 81a27ae914..22aff4bb62 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/AiSettings.java @@ -83,11 +83,10 @@ public final class AiSettings extends BaseData implements HasTenan @Schema( requiredMode = Schema.RequiredMode.REQUIRED, - accessMode = Schema.AccessMode.WRITE_ONLY, - description = "API key for authenticating with the selected AI provider", - example = "sk-********************************" + accessMode = Schema.AccessMode.READ_WRITE, + description = "Provider-specific settings for the chosen AI model" ) - String apiKey; + AiConfig configuration; public AiSettings() {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/GoogleAiGeminiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/GoogleAiGeminiConfig.java new file mode 100644 index 0000000000..915fb41d55 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/GoogleAiGeminiConfig.java @@ -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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/MistralAiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/MistralAiConfig.java new file mode 100644 index 0000000000..bbfd849f59 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/MistralAiConfig.java @@ -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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/OpenAiConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/OpenAiConfig.java new file mode 100644 index 0000000000..cce6e3aa90 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/OpenAiConfig.java @@ -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; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java index f9b6ab74fe..8aac27051a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AiSettingsId.java @@ -36,7 +36,7 @@ public final class AiSettingsId extends UUIDBased implements EntityId { @Override @Schema( requiredMode = Schema.RequiredMode.REQUIRED, - description = "Entity type of the AI settings, always 'AI_SETTINGS'", + description = "Entity type of the AI settings", example = "AI_SETTINGS", allowableValues = "AI_SETTINGS" ) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index ac09ce088b..20ef6ceb08 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -747,7 +747,7 @@ public class ModelConstants { 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_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}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java index f473fe6acb..458b434711 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AiSettingsEntity.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.model.sql; +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -23,7 +24,9 @@ import jakarta.persistence.Table; import lombok.Getter; 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.id.AiSettingsId; @@ -41,7 +44,7 @@ import java.util.UUID; @Table(name = ModelConstants.AI_SETTINGS_TABLE_NAME) public class AiSettingsEntity extends BaseVersionedEntity { - @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; @Column(name = ModelConstants.AI_SETTINGS_NAME_COLUMN_NAME, nullable = false) @@ -54,8 +57,9 @@ public class AiSettingsEntity extends BaseVersionedEntity { @Column(name = ModelConstants.AI_SETTINGS_MODEL_COLUMN_NAME, nullable = false) private String model; - @Column(name = ModelConstants.AI_SETTINGS_API_KEY_COLUMN_NAME, nullable = false) - private String apiKey; + @Type(JsonBinaryType.class) + @Column(name = ModelConstants.AI_SETTINGS_CONFIGURATION_COLUMN_NAME, nullable = false, columnDefinition = "JSONB") + private AiConfig configuration; public AiSettingsEntity() {} @@ -65,7 +69,7 @@ public class AiSettingsEntity extends BaseVersionedEntity { name = aiSettings.getName(); provider = aiSettings.getProvider(); model = aiSettings.getModel(); - apiKey = aiSettings.getApiKey(); + configuration = aiSettings.getConfiguration(); } @Override @@ -77,7 +81,7 @@ public class AiSettingsEntity extends BaseVersionedEntity { settings.setName(name); settings.setProvider(provider); settings.setModel(model); - settings.setApiKey(apiKey); + settings.setConfiguration(configuration); return settings; } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9d5ae69c95..b2b9ffaded 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -950,13 +950,13 @@ 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, - api_key VARCHAR(1000) 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, + model VARCHAR(255) NOT NULL, + configuration JSONB NOT NULL, CONSTRAINT ai_settings_name_unq_key UNIQUE (tenant_id, name) );