Browse Source

Merge pull request #14014 from dskarzh/feature/ollama-support

AI models: add support for Ollama
pull/14083/head
Viacheslav Klimov 1 year ago
committed by GitHub
parent
commit
3b661bba66
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      application/pom.xml
  2. 39
      application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java
  3. 6
      common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java
  4. 8
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java
  5. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java
  6. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java
  7. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java
  8. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java
  9. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java
  10. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java
  11. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java
  12. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java
  13. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java
  14. 58
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java
  15. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java
  16. 3
      common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java
  17. 2
      common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java
  18. 48
      common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java
  19. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java
  20. 115
      ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html
  21. 69
      ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts
  22. 35
      ui-ngx/src/app/shared/models/ai-model.models.ts
  23. 24
      ui-ngx/src/assets/locale/locale.constant-en_US.json

4
application/pom.xml

@ -419,6 +419,10 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

39
application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java

@ -32,8 +32,10 @@ import dev.langchain4j.model.chat.request.ChatRequestParameters;
import dev.langchain4j.model.github.GitHubModelsChatModel; import dev.langchain4j.model.github.GitHubModelsChatModel;
import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel;
import dev.langchain4j.model.mistralai.MistralAiChatModel; import dev.langchain4j.model.mistralai.MistralAiChatModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
@ -43,10 +45,12 @@ import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelC
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer;
import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig;
import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.Region;
@ -54,7 +58,11 @@ import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.Base64;
import static java.util.Collections.singletonMap;
@Component @Component
class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer {
@ -134,7 +142,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
// set request timeout from model config // set request timeout from model config
if (chatModelConfig.timeoutSeconds() != null) { if (chatModelConfig.timeoutSeconds() != null) {
retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds())); retrySettings.setTotalTimeoutDuration(Duration.ofSeconds(chatModelConfig.timeoutSeconds()));
} }
// set updated retry settings // set updated retry settings
@ -262,6 +270,35 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
.build(); .build();
} }
@Override
public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) {
var builder = OllamaChatModel.builder()
.baseUrl(chatModelConfig.providerConfig().baseUrl())
.modelName(chatModelConfig.modelId())
.temperature(chatModelConfig.temperature())
.topP(chatModelConfig.topP())
.topK(chatModelConfig.topK())
.numCtx(chatModelConfig.contextLength())
.numPredict(chatModelConfig.maxOutputTokens())
.timeout(toDuration(chatModelConfig.timeoutSeconds()))
.maxRetries(chatModelConfig.maxRetries());
var auth = chatModelConfig.providerConfig().auth();
if (auth instanceof OllamaProviderConfig.OllamaAuth.Basic basicAuth) {
String credentials = basicAuth.username() + ":" + basicAuth.password();
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials));
} else if (auth instanceof OllamaProviderConfig.OllamaAuth.Token tokenAuth) {
builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Bearer " + tokenAuth.token()));
} else if (auth instanceof OllamaProviderConfig.OllamaAuth.None) {
// do nothing
} else {
throw new UnsupportedOperationException("Unknown authentication type: " + auth.getClass().getSimpleName());
}
return builder.build();
}
private static Duration toDuration(Integer timeoutSeconds) { private static Duration toDuration(Integer timeoutSeconds) {
return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null;
} }

6
common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java

@ -22,7 +22,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
@JsonTypeInfo( @JsonTypeInfo(
use = JsonTypeInfo.Id.NAME, use = JsonTypeInfo.Id.NAME,
property = "status", property = "status",
include = JsonTypeInfo.As.PROPERTY, include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true visible = true
) )
@JsonSubTypes({ @JsonSubTypes({
@ -51,9 +51,7 @@ public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatRes
} }
record Failure( record Failure(
@Schema( @Schema(description = "A string containing details about the failure")
description = "A string containing details about the failure"
)
String errorDetails String errorDetails
) implements TbChatResponse { ) implements TbChatResponse {

8
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelCon
import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig;
import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig;
import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; import org.thingsboard.server.common.data.ai.provider.AiProviderConfig;
@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig
import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonTypeInfo( @JsonTypeInfo(
@ -50,7 +52,8 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig;
@JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"), @JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"), @JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"), @JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS") @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS"),
@JsonSubTypes.Type(value = OllamaChatModelConfig.class, name = "OLLAMA")
}) })
public interface AiModelConfig { public interface AiModelConfig {
@ -69,7 +72,8 @@ public interface AiModelConfig {
@JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"), @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"),
@JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"), @JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"),
@JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"), @JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"),
@JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS") @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS"),
@JsonSubTypes.Type(value = OllamaProviderConfig.class, name = "OLLAMA")
}) })
AiProviderConfig providerConfig(); AiProviderConfig providerConfig();

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java

@ -24,7 +24,7 @@ public sealed interface AiChatModelConfig<C extends AiChatModelConfig<C>> extend
permits permits
OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig, OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig,
GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig, GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig,
AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig { AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig, OllamaChatModelConfig {
ChatModel configure(Langchain4jChatModelConfigurer configurer); ChatModel configure(Langchain4jChatModelConfigurer configurer);

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java

@ -33,7 +33,7 @@ public record AmazonBedrockChatModelConfig(
@NotBlank String modelId, @NotBlank String modelId,
@PositiveOrZero Double temperature, @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AmazonBedrockChatModelConfig> { ) implements AiChatModelConfig<AmazonBedrockChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java

@ -34,7 +34,7 @@ public record AnthropicChatModelConfig(
@PositiveOrZero Double temperature, @PositiveOrZero Double temperature,
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
@PositiveOrZero Integer topK, @PositiveOrZero Integer topK,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AnthropicChatModelConfig> { ) implements AiChatModelConfig<AnthropicChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java

@ -35,7 +35,7 @@ public record AzureOpenAiChatModelConfig(
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<AzureOpenAiChatModelConfig> { ) implements AiChatModelConfig<AzureOpenAiChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java

@ -35,7 +35,7 @@ public record GitHubModelsChatModelConfig(
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GitHubModelsChatModelConfig> { ) implements AiChatModelConfig<GitHubModelsChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java

@ -36,7 +36,7 @@ public record GoogleAiGeminiChatModelConfig(
@PositiveOrZero Integer topK, @PositiveOrZero Integer topK,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleAiGeminiChatModelConfig> { ) implements AiChatModelConfig<GoogleAiGeminiChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java

@ -36,7 +36,7 @@ public record GoogleVertexAiGeminiChatModelConfig(
@PositiveOrZero Integer topK, @PositiveOrZero Integer topK,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<GoogleVertexAiGeminiChatModelConfig> { ) implements AiChatModelConfig<GoogleVertexAiGeminiChatModelConfig> {

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java

@ -35,4 +35,6 @@ public interface Langchain4jChatModelConfigurer {
ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig); ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig);
ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig);
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java

@ -35,7 +35,7 @@ public record MistralAiChatModelConfig(
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<MistralAiChatModelConfig> { ) implements AiChatModelConfig<MistralAiChatModelConfig> {

58
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java

@ -0,0 +1,58 @@
/**
* 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.chat;
import dev.langchain4j.model.chat.ChatModel;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import lombok.Builder;
import lombok.With;
import org.thingsboard.server.common.data.ai.provider.AiProvider;
import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig;
@Builder
public record OllamaChatModelConfig(
@NotNull @Valid OllamaProviderConfig providerConfig,
@NotBlank String modelId,
@PositiveOrZero Double temperature,
@Positive @Max(1) Double topP,
@PositiveOrZero Integer topK,
Integer contextLength,
Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<OllamaChatModelConfig> {
@Override
public AiProvider provider() {
return AiProvider.OLLAMA;
}
@Override
public ChatModel configure(Langchain4jChatModelConfigurer configurer) {
return configurer.configureChatModel(this);
}
@Override
public boolean supportsJsonMode() {
return true;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java

@ -35,7 +35,7 @@ public record OpenAiChatModelConfig(
@Positive @Max(1) Double topP, @Positive @Max(1) Double topP,
Double frequencyPenalty, Double frequencyPenalty,
Double presencePenalty, Double presencePenalty,
@Positive Integer maxOutputTokens, Integer maxOutputTokens,
@With @Positive Integer timeoutSeconds, @With @Positive Integer timeoutSeconds,
@With @PositiveOrZero Integer maxRetries @With @PositiveOrZero Integer maxRetries
) implements AiChatModelConfig<OpenAiChatModelConfig> { ) implements AiChatModelConfig<OpenAiChatModelConfig> {

3
common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java

@ -24,6 +24,7 @@ public enum AiProvider {
MISTRAL_AI, MISTRAL_AI,
ANTHROPIC, ANTHROPIC,
AMAZON_BEDROCK, AMAZON_BEDROCK,
GITHUB_MODELS GITHUB_MODELS,
OLLAMA
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java

@ -19,4 +19,4 @@ public sealed interface AiProviderConfig
permits permits
OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig,
GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig, GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig,
AmazonBedrockProviderConfig, GitHubModelsProviderConfig {} AmazonBedrockProviderConfig, GitHubModelsProviderConfig, OllamaProviderConfig {}

48
common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java

@ -0,0 +1,48 @@
/**
* 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.provider;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
public record OllamaProviderConfig(
@NotNull String baseUrl,
@NotNull @Valid OllamaAuth auth
) implements AiProviderConfig {
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OllamaAuth.None.class, name = "NONE"),
@JsonSubTypes.Type(value = OllamaAuth.Basic.class, name = "BASIC"),
@JsonSubTypes.Type(value = OllamaAuth.Token.class, name = "TOKEN")
})
public sealed interface OllamaAuth {
record None() implements OllamaAuth {}
record Basic(@NotNull String username, @NotNull String password) implements OllamaAuth {}
record Token(@NotNull String token) implements OllamaAuth {}
}
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java

@ -60,9 +60,7 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes
@Override @Override
public ResponseFormat toLangChainResponseFormat() { public ResponseFormat toLangChainResponseFormat() {
return ResponseFormat.builder() return ResponseFormat.TEXT;
.type(ResponseFormatType.TEXT)
.build();
} }
} }
@ -76,9 +74,7 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes
@Override @Override
public ResponseFormat toLangChainResponseFormat() { public ResponseFormat toLangChainResponseFormat() {
return ResponseFormat.builder() return ResponseFormat.JSON;
.type(ResponseFormatType.JSON)
.build();
} }
} }

115
ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html

@ -55,31 +55,34 @@
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div formGroupName="providerConfig" class="tb-form-panel no-border no-padding"> <div formGroupName="providerConfig" class="tb-form-panel no-border no-padding no-gap">
@if (providerFieldsList.includes('personalAccessToken')) { @if (providerFieldsList.includes('personalAccessToken')) {
<mat-form-field class="mat-block flex-1" appearance="outline"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.personal-access-token</mat-label> <mat-label translate>ai-models.personal-access-token</mat-label>
<input type="password" required matInput formControlName="personalAccessToken" autocomplete="new-password"> <input type="password" required matInput formControlName="personalAccessToken" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('personalAccessToken').hasError('required')"> <mat-error *ngIf="aiModelForms.get('configuration.providerConfig.personalAccessToken').hasError('required') ||
aiModelForms.get('configuration.providerConfig.personalAccessToken').hasError('pattern')">
{{ 'ai-models.personal-access-token-required' | translate }} {{ 'ai-models.personal-access-token-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('projectId')) { @if (providerFieldsList.includes('projectId')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.project-id</mat-label> <mat-label translate>ai-models.project-id</mat-label>
<input matInput required formControlName="projectId"> <input matInput required formControlName="projectId">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('projectId').hasError('required')"> <mat-error *ngIf="aiModelForms.get('configuration.providerConfig.projectId').hasError('required') ||
aiModelForms.get('configuration.providerConfig.projectId').hasError('pattern')">
{{ 'ai-models.project-id-required' | translate }} {{ 'ai-models.project-id-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('location')) { @if (providerFieldsList.includes('location')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.location</mat-label> <mat-label translate>ai-models.location</mat-label>
<input matInput required formControlName="location"> <input matInput required formControlName="location">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('location').hasError('required')"> <mat-error *ngIf="aiModelForms.get('configuration.providerConfig.location').hasError('required') ||
aiModelForms.get('configuration.providerConfig.location').hasError('pattern')">
{{ 'ai-models.location-required' | translate }} {{ 'ai-models.location-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -98,16 +101,17 @@
</tb-file-input> </tb-file-input>
} }
@if (providerFieldsList.includes('endpoint')) { @if (providerFieldsList.includes('endpoint')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.endpoint</mat-label> <mat-label translate>ai-models.endpoint</mat-label>
<input required matInput formControlName="endpoint"> <input required matInput formControlName="endpoint">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('endpoint').hasError('required')"> <mat-error *ngIf="aiModelForms.get('configuration.providerConfig.endpoint').hasError('required') ||
aiModelForms.get('configuration.providerConfig.endpoint').hasError('pattern')">
{{ 'ai-models.endpoint-required' | translate }} {{ 'ai-models.endpoint-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('serviceVersion')) { @if (providerFieldsList.includes('serviceVersion')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.service-version</mat-label> <mat-label translate>ai-models.service-version</mat-label>
<input matInput formControlName="serviceVersion"> <input matInput formControlName="serviceVersion">
</mat-form-field> </mat-form-field>
@ -117,25 +121,28 @@
<mat-label translate>ai-models.api-key</mat-label> <mat-label translate>ai-models.api-key</mat-label>
<input type="password" required matInput formControlName="apiKey" autocomplete="new-password"> <input type="password" required matInput formControlName="apiKey" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('apiKey').hasError('required')"> <mat-error *ngIf="aiModelForms.get('configuration.providerConfig.apiKey').hasError('required') ||
aiModelForms.get('configuration.providerConfig.apiKey').hasError('pattern')">
{{ 'ai-models.api-key-required' | translate }} {{ 'ai-models.api-key-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('region')) { @if (providerFieldsList.includes('region')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.region</mat-label> <mat-label translate>ai-models.region</mat-label>
<input required matInput formControlName="region"> <input required matInput formControlName="region">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('region').hasError('required')"> <mat-error *ngIf="aiModelForms.get('configuration.providerConfig.region').hasError('required') ||
aiModelForms.get('configuration.providerConfig.region').hasError('pattern')">
{{ 'ai-models.region-required' | translate }} {{ 'ai-models.region-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('accessKeyId')) { @if (providerFieldsList.includes('accessKeyId')) {
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.access-key-id</mat-label> <mat-label translate>ai-models.access-key-id</mat-label>
<input required matInput formControlName="accessKeyId"> <input required matInput formControlName="accessKeyId">
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('accessKeyId').hasError('required')"> <mat-error *ngIf="aiModelForms.get('configuration.providerConfig.accessKeyId').hasError('required') ||
aiModelForms.get('configuration.providerConfig.accessKeyId').hasError('pattern')">
{{ 'ai-models.access-key-id-required' | translate }} {{ 'ai-models.access-key-id-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
@ -145,11 +152,68 @@
<mat-label translate>ai-models.secret-access-key</mat-label> <mat-label translate>ai-models.secret-access-key</mat-label>
<input type="password" required matInput formControlName="secretAccessKey" autocomplete="new-password"> <input type="password" required matInput formControlName="secretAccessKey" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration').get('providerConfig').get('secretAccessKey').hasError('required')"> <mat-error *ngIf="aiModelForms.get('configuration.providerConfig.secretAccessKey').hasError('required') ||
aiModelForms.get('configuration.providerConfig.secretAccessKey').hasError('pattern')">
{{ 'ai-models.secret-access-key-required' | translate }} {{ 'ai-models.secret-access-key-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('baseUrl')) {
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.baseurl</mat-label>
<input required matInput formControlName="baseUrl">
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.baseUrl').hasError('required') ||
aiModelForms.get('configuration.providerConfig.baseUrl').hasError('pattern')">
{{ 'ai-models.baseurl-required' | translate }}
</mat-error>
</mat-form-field>
}
@if (provider === aiProvider.OLLAMA) {
<div class="tb-form-panel stroked no-gap no-padding-bottom mb-4" formGroupName="auth">
<div class="flex flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ authenticationHint }}">
{{ 'ai-models.authentication' | translate }}
</div>
<tb-toggle-select formControlName="type">
<tb-toggle-option [value]="AuthenticationType.NONE">{{ 'ai-models.authentication-type.none' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="AuthenticationType.BASIC">{{ 'ai-models.authentication-type.basic' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="AuthenticationType.TOKEN">{{ 'ai-models.authentication-type.token' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-padding no-border no-gap padding-top">
@if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.BASIC) {
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.username</mat-label>
<input required matInput formControlName="username">
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.auth.username').hasError('required') ||
aiModelForms.get('configuration.providerConfig.auth.username').hasError('pattern')">
{{ 'ai-models.username-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.password</mat-label>
<input type="password" required matInput formControlName="password" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.auth.password').hasError('required') ||
aiModelForms.get('configuration.providerConfig.auth.password').hasError('pattern')">
{{ 'ai-models.password-required' | translate }}
</mat-error>
</mat-form-field>
}
@if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.TOKEN) {
<mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.token</mat-label>
<input type="password" required matInput formControlName="token" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.auth.token').hasError('required') ||
aiModelForms.get('configuration.providerConfig.auth.token').hasError('pattern')">
{{ 'ai-models.token-required' | translate }}
</mat-error>
</mat-form-field>
}
</div>
</div>
}
</div> </div>
</section> </section>
</section> </section>
@ -255,15 +319,18 @@
</div> </div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxOutputTokens" <input matInput formControlName="maxOutputTokens"
type="number" min="1" step="1" placeholder="{{ 'ai-models.set' | translate }}"> type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}">
<mat-icon matSuffix </mat-form-field>
matTooltipPosition="above" </div>
matTooltipClass="tb-error-tooltip" }
[matTooltip]="'ai-models.max-output-tokens-min' | translate" @if (modelFieldsList.includes('contextLength')) {
*ngIf="aiModelForms.get('configuration').get('maxOutputTokens').hasError('min')" <div class="tb-form-row space-between">
class="tb-error"> <div tb-hint-tooltip-icon="{{ 'ai-models.context-length-hint' | translate }}">
warning {{ 'ai-models.context-length' | translate }}
</mat-icon> </div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="contextLength"
type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
} }

69
ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts

@ -30,6 +30,7 @@ import {
AiModelMap, AiModelMap,
AiProvider, AiProvider,
AiProviderTranslations, AiProviderTranslations,
AuthenticationType,
ModelType, ModelType,
ProviderFieldsAllList ProviderFieldsAllList
} from '@shared/models/ai-model.models'; } from '@shared/models/ai-model.models';
@ -37,6 +38,7 @@ import { AiModelService } from '@core/http/ai-model.service';
import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { deepTrim } from '@core/utils'; import { deepTrim } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
export interface AIModelDialogData { export interface AIModelDialogData {
AIModel?: AiModel; AIModel?: AiModel;
@ -62,18 +64,23 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
providerMap: AiProvider[] = Object.keys(AiProvider) as AiProvider[]; providerMap: AiProvider[] = Object.keys(AiProvider) as AiProvider[];
providerTranslationMap = AiProviderTranslations; providerTranslationMap = AiProviderTranslations;
AuthenticationType = AuthenticationType;
provider: AiProvider = AiProvider.OPENAI; provider: AiProvider = AiProvider.OPENAI;
aiModelForms: FormGroup; aiModelForms: FormGroup;
isAdd = false; isAdd = false;
authenticationHint: string;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
protected dialogRef: MatDialogRef<AIModelDialogComponent, AiModel>, protected dialogRef: MatDialogRef<AIModelDialogComponent, AiModel>,
@Inject(MAT_DIALOG_DATA) public data: AIModelDialogData, @Inject(MAT_DIALOG_DATA) public data: AIModelDialogData,
private fb: FormBuilder, private fb: FormBuilder,
private aiModelService: AiModelService, private aiModelService: AiModelService,
private translate: TranslateService,
private dialog: MatDialog) { private dialog: MatDialog) {
super(store, router, dialogRef); super(store, router, dialogRef);
@ -89,17 +96,24 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
configuration: this.fb.group({ configuration: this.fb.group({
provider: [this.provider, []], provider: [this.provider, []],
providerConfig: this.fb.group({ providerConfig: this.fb.group({
apiKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.apiKey : '', [Validators.required]], apiKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.apiKey : '', [Validators.required, Validators.pattern(/.*\S.*/)]],
personalAccessToken: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.personalAccessToken : '', [Validators.required]], personalAccessToken: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.personalAccessToken : '', [Validators.required, Validators.pattern(/.*\S.*/)]],
endpoint: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.endpoint : '', [Validators.required]], endpoint: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.endpoint : '', [Validators.required, Validators.pattern(/.*\S.*/)]],
serviceVersion: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceVersion : ''], serviceVersion: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceVersion : ''],
projectId: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.projectId : '', [Validators.required]], projectId: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.projectId : '', [Validators.required, Validators.pattern(/.*\S.*/)]],
location: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.location : '', [Validators.required]], location: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.location : '', [Validators.required, Validators.pattern(/.*\S.*/)]],
serviceAccountKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceAccountKey : '', [Validators.required]], serviceAccountKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.serviceAccountKey : '', [Validators.required]],
fileName: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.fileName : '', [Validators.required]], fileName: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.fileName : '', [Validators.required]],
region: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.region : '', [Validators.required]], region: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.region : '', [Validators.required, Validators.pattern(/.*\S.*/)]],
accessKeyId: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.accessKeyId : '', [Validators.required]], accessKeyId: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.accessKeyId : '', [Validators.required, Validators.pattern(/.*\S.*/)]],
secretAccessKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.secretAccessKey : '', [Validators.required]], secretAccessKey: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.secretAccessKey : '', [Validators.required, Validators.pattern(/.*\S.*/)]],
baseUrl: [this.data.AIModel ? this.data.AIModel.configuration.providerConfig?.baseUrl : '', [Validators.required, Validators.pattern(/.*\S.*/)]],
auth: this.fb.group({
type: [this.data.AIModel?.configuration?.providerConfig?.auth?.type ?? AuthenticationType.NONE],
username: [this.data.AIModel?.configuration?.providerConfig?.auth?.username ?? '', [Validators.required, Validators.pattern(/.*\S.*/)]],
password: [this.data.AIModel?.configuration?.providerConfig?.auth?.password ?? '', [Validators.required, Validators.pattern(/.*\S.*/)]],
token: [this.data.AIModel?.configuration?.providerConfig?.auth?.token ?? '', [Validators.required, Validators.pattern(/.*\S.*/)]]
})
}), }),
modelId: [this.data.AIModel ? this.data.AIModel.configuration?.modelId : '', [Validators.required]], modelId: [this.data.AIModel ? this.data.AIModel.configuration?.modelId : '', [Validators.required]],
temperature: [this.data.AIModel ? this.data.AIModel.configuration?.temperature : null, [Validators.min(0)]], temperature: [this.data.AIModel ? this.data.AIModel.configuration?.temperature : null, [Validators.min(0)]],
@ -107,7 +121,8 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
topK: [this.data.AIModel ? this.data.AIModel.configuration?.topK : null, [Validators.min(0)]], topK: [this.data.AIModel ? this.data.AIModel.configuration?.topK : null, [Validators.min(0)]],
frequencyPenalty: [this.data.AIModel ? this.data.AIModel.configuration?.frequencyPenalty : null], frequencyPenalty: [this.data.AIModel ? this.data.AIModel.configuration?.frequencyPenalty : null],
presencePenalty: [this.data.AIModel ? this.data.AIModel.configuration?.presencePenalty : null], presencePenalty: [this.data.AIModel ? this.data.AIModel.configuration?.presencePenalty : null],
maxOutputTokens: [this.data.AIModel ? this.data.AIModel.configuration?.maxOutputTokens : null, [Validators.min(1)]] maxOutputTokens: [this.data.AIModel ? this.data.AIModel.configuration?.maxOutputTokens : null],
contextLength: [this.data.AIModel ? this.data.AIModel.configuration?.contextLength : null]
}) })
}); });
@ -118,7 +133,23 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
this.aiModelForms.get('configuration.modelId').reset(''); this.aiModelForms.get('configuration.modelId').reset('');
this.aiModelForms.get('configuration.providerConfig').reset({}); this.aiModelForms.get('configuration.providerConfig').reset({});
this.updateValidation(provider); this.updateValidation(provider);
}) });
this.aiModelForms.get('configuration.providerConfig.auth.type').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((type: AuthenticationType) => {
this.getAuthenticationHint(type);
this.aiModelForms.get('configuration.providerConfig.auth.username').disable();
this.aiModelForms.get('configuration.providerConfig.auth.password').disable();
this.aiModelForms.get('configuration.providerConfig.auth.token').disable();
if (type === AuthenticationType.BASIC) {
this.aiModelForms.get('configuration.providerConfig.auth.username').enable();
this.aiModelForms.get('configuration.providerConfig.auth.password').enable();
}
if (type === AuthenticationType.TOKEN) {
this.aiModelForms.get('configuration.providerConfig.auth.token').enable();
}
});
this.updateValidation(this.provider); this.updateValidation(this.provider);
} }
@ -130,6 +161,16 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
); );
} }
private getAuthenticationHint(type: AuthenticationType) {
if (type === AuthenticationType.BASIC) {
this.authenticationHint = this.translate.instant('ai-models.authentication-basic-hint');
} else if (type === AuthenticationType.TOKEN) {
this.authenticationHint = this.translate.instant('ai-models.authentication-token-hint');
} else {
this.authenticationHint = null;
}
}
private updateValidation(provider: AiProvider) { private updateValidation(provider: AiProvider) {
ProviderFieldsAllList.forEach(key => { ProviderFieldsAllList.forEach(key => {
if (AiModelMap.get(provider).providerFieldsList.includes(key)) { if (AiModelMap.get(provider).providerFieldsList.includes(key)) {
@ -137,7 +178,13 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
} else { } else {
this.aiModelForms.get('configuration.providerConfig').get(key).disable(); this.aiModelForms.get('configuration.providerConfig').get(key).disable();
} }
}) });
if (provider === AiProvider.OLLAMA) {
this.aiModelForms.get('configuration.providerConfig.auth').enable();
this.aiModelForms.get('configuration.providerConfig.auth.type').patchValue(this.data.AIModel?.configuration?.providerConfig?.auth?.type ?? AuthenticationType.NONE, {emitEvent: true});
} else {
this.aiModelForms.get('configuration.providerConfig.auth').disable();
}
} }
get providerFieldsList(): string[] { get providerFieldsList(): string[] {

35
ui-ngx/src/app/shared/models/ai-model.models.ts

@ -34,6 +34,13 @@ export interface AiModel extends Omit<BaseData<AiModelId>, 'label'>, HasTenantId
region?: string; region?: string;
accessKeyId?: string; accessKeyId?: string;
secretAccessKey?: string; secretAccessKey?: string;
baseUrl?: string;
auth?: {
type: AuthenticationType;
username?: string;
password?: string;
token?: string
}
}; };
modelId: string; modelId: string;
temperature?: number; temperature?: number;
@ -42,6 +49,7 @@ export interface AiModel extends Omit<BaseData<AiModelId>, 'label'>, HasTenantId
frequencyPenalty?: number; frequencyPenalty?: number;
presencePenalty?: number; presencePenalty?: number;
maxOutputTokens?: number; maxOutputTokens?: number;
contextLength?: number;
} }
} }
@ -57,7 +65,8 @@ export enum AiProvider {
MISTRAL_AI = 'MISTRAL_AI', MISTRAL_AI = 'MISTRAL_AI',
ANTHROPIC = 'ANTHROPIC', ANTHROPIC = 'ANTHROPIC',
AMAZON_BEDROCK = 'AMAZON_BEDROCK', AMAZON_BEDROCK = 'AMAZON_BEDROCK',
GITHUB_MODELS = 'GITHUB_MODELS' GITHUB_MODELS = 'GITHUB_MODELS',
OLLAMA = 'OLLAMA'
} }
export const AiProviderTranslations = new Map<AiProvider, string>( export const AiProviderTranslations = new Map<AiProvider, string>(
@ -69,7 +78,8 @@ export const AiProviderTranslations = new Map<AiProvider, string>(
[AiProvider.MISTRAL_AI , 'ai-models.ai-providers.mistral-ai'], [AiProvider.MISTRAL_AI , 'ai-models.ai-providers.mistral-ai'],
[AiProvider.ANTHROPIC , 'ai-models.ai-providers.anthropic'], [AiProvider.ANTHROPIC , 'ai-models.ai-providers.anthropic'],
[AiProvider.AMAZON_BEDROCK , 'ai-models.ai-providers.amazon-bedrock'], [AiProvider.AMAZON_BEDROCK , 'ai-models.ai-providers.amazon-bedrock'],
[AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'] [AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'],
[AiProvider.OLLAMA , 'ai-models.ai-providers.ollama']
] ]
); );
@ -84,10 +94,11 @@ export const ProviderFieldsAllList = [
'serviceVersion', 'serviceVersion',
'region', 'region',
'accessKeyId', 'accessKeyId',
'secretAccessKey' 'secretAccessKey',
'baseUrl'
]; ];
export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens']; export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens', 'contextLength'];
export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFieldsList: string[], modelFieldsList: string[] }>([ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFieldsList: string[], modelFieldsList: string[] }>([
[ [
@ -191,6 +202,14 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'], modelFieldsList: ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens'],
}, },
], ],
[
AiProvider.OLLAMA,
{
modelList: [],
providerFieldsList: ['baseUrl'],
modelFieldsList: ['temperature', 'topP', 'topK', 'maxOutputTokens', 'contextLength'],
},
],
]); ]);
export const AiRuleNodeResponseFormatTypeOnlyText: AiProvider[] = [AiProvider.AMAZON_BEDROCK, AiProvider.ANTHROPIC, AiProvider.GITHUB_MODELS]; export const AiRuleNodeResponseFormatTypeOnlyText: AiProvider[] = [AiProvider.AMAZON_BEDROCK, AiProvider.ANTHROPIC, AiProvider.GITHUB_MODELS];
@ -216,7 +235,8 @@ export interface AiModelWithUserMsg {
projectId?: string; projectId?: string;
location?: string; location?: string;
serviceAccountKey?: string; serviceAccountKey?: string;
fileName?: string fileName?: string;
baseUrl?: string;
}; };
modelId: string; modelId: string;
maxRetries: number; maxRetries: number;
@ -228,3 +248,8 @@ export interface CheckConnectivityResult {
status: string; status: string;
errorDetails: string; errorDetails: string;
} }
export enum AuthenticationType {
NONE = 'NONE',
BASIC = 'BASIC',
TOKEN = 'TOKEN'
}

24
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1112,7 +1112,8 @@
"mistral-ai": "Mistral AI", "mistral-ai": "Mistral AI",
"anthropic": "Anthropic", "anthropic": "Anthropic",
"amazon-bedrock": "Amazon Bedrock", "amazon-bedrock": "Amazon Bedrock",
"github-models": "GitHub Models" "github-models": "GitHub Models",
"ollama": "Ollama"
}, },
"name-required": "Name is required.", "name-required": "Name is required.",
"name-max-length": "Name must be 255 characters or less.", "name-max-length": "Name must be 255 characters or less.",
@ -1155,17 +1156,34 @@
"frequency-penalty": "Frequency penalty", "frequency-penalty": "Frequency penalty",
"frequency-penalty-hint": "Applies a penalty to a token's likelihood that increases based on its frequency in the text.", "frequency-penalty-hint": "Applies a penalty to a token's likelihood that increases based on its frequency in the text.",
"max-output-tokens": "Maximum output tokens", "max-output-tokens": "Maximum output tokens",
"max-output-tokens-min": "Must be greater than 0.",
"max-output-tokens-hint": "Sets the maximum number of tokens that the \nmodel can generate in a single response.", "max-output-tokens-hint": "Sets the maximum number of tokens that the \nmodel can generate in a single response.",
"context-length": "Context length",
"context-length-hint": "Defines the size of the context window in tokens. This value sets the total memory limit for the model, including both the user's input and the generated response.",
"endpoint": "Endpoint", "endpoint": "Endpoint",
"endpoint-required": "Endpoint is required.", "endpoint-required": "Endpoint is required.",
"baseurl": "Base URL",
"baseurl-required": "Base URL is required.",
"service-version": "Service version", "service-version": "Service version",
"check-connectivity": "Check connectivity", "check-connectivity": "Check connectivity",
"check-connectivity-success": "Test request was successful", "check-connectivity-success": "Test request was successful",
"check-connectivity-failed": "Test request failed", "check-connectivity-failed": "Test request failed",
"no-model-matching": "No models matching '{{entity}}' were found.", "no-model-matching": "No models matching '{{entity}}' were found.",
"model-required": "Model is required.", "model-required": "Model is required.",
"no-model-text": "No models found." "no-model-text": "No models found.",
"authentication": "Authentication",
"authentication-basic-hint": "Uses standard HTTP Basic authentication. The username and password will be combined, Base64-encoded, and sent in an \"Authorization\" header with each request to the Ollama server.",
"authentication-token-hint": "Uses Bearer token authentication. The provided token will be sent directly in an \"Authorization\" eader with each request to the Ollama server.",
"authentication-type": {
"none": "None",
"basic": "Basic",
"token": "Token"
},
"username": "Username",
"username-required": "Username is required.",
"password": "Password",
"password-required": "Password is required.",
"token": "Token",
"token-required": "Token is required."
}, },
"confirm-on-exit": { "confirm-on-exit": {
"message": "You have unsaved changes. Are you sure you want to leave this page?", "message": "You have unsaved changes. Are you sure you want to leave this page?",

Loading…
Cancel
Save