From 57518b76f018b36b900c7696baf21d52924026d2 Mon Sep 17 00:00:00 2001 From: wmz7year Date: Sun, 2 Jun 2024 11:21:42 +0800 Subject: [PATCH] Add Amazon Bedrock Mistral model support. --- README.md | 2 +- models/spring-ai-bedrock/README.md | 1 + models/spring-ai-bedrock/pom.xml | 6 + .../mistral/BedrockMistralChatModel.java | 146 ++++++++++ .../mistral/BedrockMistralChatOptions.java | 155 ++++++++++ .../mistral/api/MistralChatBedrockApi.java | 272 ++++++++++++++++++ .../BedrockMistralChatCreateRequestTests.java | 67 +++++ .../mistral/BedrockMistralChatModelIT.java | 219 ++++++++++++++ .../mistral/api/MistralChatBedrockApiIT.java | 82 ++++++ .../bedrock-mistral-chat-low-level-api.png | Bin 0 -> 92100 bytes .../src/main/antora/modules/ROOT/nav.adoc | 1 + .../modules/ROOT/pages/api/bedrock.adoc | 1 + .../api/chat/bedrock/bedrock-mistral.adoc | 251 ++++++++++++++++ .../modules/ROOT/pages/api/chatmodel.adoc | 1 + .../api/structured-output-converter.adoc | 3 +- .../modules/ROOT/pages/getting-started.adoc | 1 + .../BedrockMistralChatAutoConfiguration.java | 68 +++++ .../mistral/BedrockMistralChatProperties.java | 72 +++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...BedrockMistralChatAutoConfigurationIT.java | 159 ++++++++++ 20 files changed, 1506 insertions(+), 2 deletions(-) create mode 100644 models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModel.java create mode 100644 models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatOptions.java create mode 100644 models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApi.java create mode 100644 models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatCreateRequestTests.java create mode 100644 models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModelIT.java create mode 100644 models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApiIT.java create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/images/bedrock/bedrock-mistral-chat-low-level-api.png create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/bedrock/bedrock-mistral.adoc create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatAutoConfiguration.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatProperties.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatAutoConfigurationIT.java diff --git a/README.md b/README.md index fe4aa92acdb..33fff49f2a2 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ You can find more details in the [Reference Documentation](https://docs.spring.i Spring AI supports many AI models. For an overview see here. Specific models currently supported are * OpenAI * Azure OpenAI -* Amazon Bedrock (Anthropic, Llama, Cohere, Titan, Jurassic2) +* Amazon Bedrock (Anthropic, Llama, Cohere, Titan, Jurassic2, Mistral) * HuggingFace * Google VertexAI (PaLM2, Gemini) * Mistral AI diff --git a/models/spring-ai-bedrock/README.md b/models/spring-ai-bedrock/README.md index 19e48518a60..782af7a8535 100644 --- a/models/spring-ai-bedrock/README.md +++ b/models/spring-ai-bedrock/README.md @@ -8,4 +8,5 @@ - [Titan Chat Documentation](https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/api/chat/bedrock/bedrock-titan.html) - [Titan Embedding Documentation](https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/api/embeddings/bedrock-titan-embedding.html) - [Jurassic2 Chat Documentation](https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/api/chat/bedrock/bedrock-jurassic2.html) +- [Mistral Chat Documentation](https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/api/chat/bedrock/bedrock-mistral.html) diff --git a/models/spring-ai-bedrock/pom.xml b/models/spring-ai-bedrock/pom.xml index e3b79d30bda..ae7913822a0 100644 --- a/models/spring-ai-bedrock/pom.xml +++ b/models/spring-ai-bedrock/pom.xml @@ -29,6 +29,12 @@ ${project.parent.version} + + org.springframework.ai + spring-ai-retry + ${project.parent.version} + + org.springframework spring-web diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModel.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModel.java new file mode 100644 index 00000000000..660837960dd --- /dev/null +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModel.java @@ -0,0 +1,146 @@ +/* + * Copyright 2023 - 2024 the original author or 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 + * + * https://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.springframework.ai.bedrock.mistral; + +import java.util.List; + +import org.springframework.ai.bedrock.BedrockUsage; +import org.springframework.ai.bedrock.MessageToPromptConverter; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatRequest; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatResponse; +import org.springframework.ai.chat.metadata.ChatGenerationMetadata; +import org.springframework.ai.chat.metadata.Usage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; +import org.springframework.ai.chat.model.StreamingChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; + +/** + * @author Wei Jiang + * @since 1.0.0 + */ +public class BedrockMistralChatModel implements ChatModel, StreamingChatModel { + + private final MistralChatBedrockApi chatApi; + + private final BedrockMistralChatOptions defaultOptions; + + /** + * The retry template used to retry the Bedrock API calls. + */ + private final RetryTemplate retryTemplate; + + public BedrockMistralChatModel(MistralChatBedrockApi chatApi) { + this(chatApi, BedrockMistralChatOptions.builder().build()); + } + + public BedrockMistralChatModel(MistralChatBedrockApi chatApi, BedrockMistralChatOptions options) { + this(chatApi, options, RetryUtils.DEFAULT_RETRY_TEMPLATE); + } + + public BedrockMistralChatModel(MistralChatBedrockApi chatApi, BedrockMistralChatOptions options, + RetryTemplate retryTemplate) { + Assert.notNull(chatApi, "MistralChatBedrockApi must not be null"); + Assert.notNull(options, "BedrockMistralChatOptions must not be null"); + Assert.notNull(retryTemplate, "RetryTemplate must not be null"); + + this.chatApi = chatApi; + this.defaultOptions = options; + this.retryTemplate = retryTemplate; + } + + @Override + public ChatResponse call(Prompt prompt) { + + MistralChatRequest request = createRequest(prompt); + + return this.retryTemplate.execute(ctx -> { + MistralChatResponse response = this.chatApi.chatCompletion(request); + + List generations = response.outputs().stream().map(g -> { + return new Generation(g.text()); + }).toList(); + + return new ChatResponse(generations); + }); + } + + public Flux stream(Prompt prompt) { + + MistralChatRequest request = createRequest(prompt); + + return this.retryTemplate.execute(ctx -> { + return this.chatApi.chatCompletionStream(request).map(g -> { + List generations = g.outputs().stream().map(output -> { + Generation generation = new Generation(output.text()); + + if (g.amazonBedrockInvocationMetrics() != null) { + Usage usage = BedrockUsage.from(g.amazonBedrockInvocationMetrics()); + generation.withGenerationMetadata(ChatGenerationMetadata.from(output.stopReason(), usage)); + } + + return generation; + }).toList(); + + return new ChatResponse(generations); + }); + }); + } + + /** + * Test access. + */ + MistralChatRequest createRequest(Prompt prompt) { + final String promptValue = MessageToPromptConverter.create().toPrompt(prompt.getInstructions()); + + var request = MistralChatRequest.builder(promptValue) + .withTemperature(this.defaultOptions.getTemperature()) + .withTopP(this.defaultOptions.getTopP()) + .withTopK(this.defaultOptions.getTopK()) + .withMaxTokens(this.defaultOptions.getMaxTokens()) + .withStopSequences(this.defaultOptions.getStopSequences()) + .build(); + + if (prompt.getOptions() != null) { + if (prompt.getOptions() instanceof ChatOptions runtimeOptions) { + BedrockMistralChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, + ChatOptions.class, BedrockMistralChatOptions.class); + request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, MistralChatRequest.class); + } + else { + throw new IllegalArgumentException("Prompt options are not of type ChatOptions: " + + prompt.getOptions().getClass().getSimpleName()); + } + } + + return request; + } + + @Override + public ChatOptions getDefaultOptions() { + return defaultOptions; + } + +} diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatOptions.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatOptions.java new file mode 100644 index 00000000000..e1447d5a1ad --- /dev/null +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatOptions.java @@ -0,0 +1,155 @@ +/* + * Copyright 2023 - 2024 the original author or 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 + * + * https://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.springframework.ai.bedrock.mistral; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import org.springframework.ai.chat.prompt.ChatOptions; + +/** + * @author Wei Jiang + * @since 1.0.0 + */ +@JsonInclude(Include.NON_NULL) +public class BedrockMistralChatOptions implements ChatOptions { + + /** + * The temperature value controls the randomness of the generated text. Use a lower + * value to decrease randomness in the response. + */ + private @JsonProperty("temperature") Float temperature; + + /** + * (optional) The maximum cumulative probability of tokens to consider when sampling. + * The generative uses combined Top-k and nucleus sampling. Nucleus sampling considers + * the smallest set of tokens whose probability sum is at least topP. + */ + private @JsonProperty("top_p") Float topP; + + /** + * (optional) Specify the number of token choices the generative uses to generate the + * next token. + */ + private @JsonProperty("top_p") Integer topK; + + /** + * (optional) Specify the maximum number of tokens to use in the generated response. + */ + private @JsonProperty("max_tokens") Integer maxTokens; + + /** + * (optional) Configure up to four sequences that the generative recognizes. After a + * stop sequence, the generative stops generating further tokens. The returned text + * doesn't contain the stop sequence. + */ + private @JsonProperty("stop") List stopSequences; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private final BedrockMistralChatOptions options = new BedrockMistralChatOptions(); + + public Builder withTemperature(Float temperature) { + this.options.setTemperature(temperature); + return this; + } + + public Builder withTopP(Float topP) { + this.options.setTopP(topP); + return this; + } + + public Builder withTopK(Integer topK) { + this.options.setTopK(topK); + return this; + } + + public Builder withMaxTokens(Integer maxTokens) { + this.options.setMaxTokens(maxTokens); + return this; + } + + public Builder withStopSequences(List stopSequences) { + this.options.setStopSequences(stopSequences); + return this; + } + + public BedrockMistralChatOptions build() { + return this.options; + } + + } + + public void setTemperature(Float temperature) { + this.temperature = temperature; + } + + @Override + public Float getTemperature() { + return this.temperature; + } + + public void setTopP(Float topP) { + this.topP = topP; + } + + @Override + public Float getTopP() { + return this.topP; + } + + public void setTopK(Integer topK) { + this.topK = topK; + } + + @Override + public Integer getTopK() { + return this.topK; + } + + public Integer getMaxTokens() { + return maxTokens; + } + + public void setMaxTokens(Integer maxTokens) { + this.maxTokens = maxTokens; + } + + public List getStopSequences() { + return stopSequences; + } + + public void setStopSequences(List stopSequences) { + this.stopSequences = stopSequences; + } + + public static BedrockMistralChatOptions fromOptions(BedrockMistralChatOptions fromOptions) { + return builder().withTemperature(fromOptions.getTemperature()) + .withTopP(fromOptions.getTopP()) + .withTopK(fromOptions.getTopK()) + .withMaxTokens(fromOptions.getMaxTokens()) + .withStopSequences(fromOptions.getStopSequences()) + .build(); + } + +} diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApi.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApi.java new file mode 100644 index 00000000000..da6c861fcba --- /dev/null +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApi.java @@ -0,0 +1,272 @@ +/* + * Copyright 2023 - 2024 the original author or 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 + * + * https://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.springframework.ai.bedrock.mistral.api; + +import java.time.Duration; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.ObjectMapper; + +import reactor.core.publisher.Flux; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; + +import org.springframework.ai.bedrock.api.AbstractBedrockApi; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatRequest; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatResponse; +import org.springframework.ai.model.ModelDescription; +import org.springframework.util.Assert; + +/** + * Java client for the Bedrock Mistral chat model. + * https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-mistral-text-completion.html + * + * @author Wei Jiang + * @since 1.0.0 + */ +// @formatter:off +public class MistralChatBedrockApi extends AbstractBedrockApi { + + /** + * Create a new MistralChatBedrockApi instance using the default credentials provider chain, the default object + * mapper, default temperature and topP values. + * + * @param modelId The model id to use. See the {@link MistralChatModel} for the supported models. + * @param region The AWS region to use. + */ + public MistralChatBedrockApi(String modelId, String region) { + super(modelId, region); + } + + /** + * Create a new MistralChatBedrockApi instance using the provided credentials provider, region and object mapper. + * + * @param modelId The model id to use. See the {@link MistralChatModel} for the supported models. + * @param credentialsProvider The credentials provider to connect to AWS. + * @param region The AWS region to use. + * @param objectMapper The object mapper to use for JSON serialization and deserialization. + */ + public MistralChatBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, String region, + ObjectMapper objectMapper) { + super(modelId, credentialsProvider, region, objectMapper); + } + + /** + * Create a new MistralChatBedrockApi instance using the default credentials provider chain, the default object + * mapper, default temperature and topP values. + * + * @param modelId The model id to use. See the {@link MistralChatModel} for the supported models. + * @param region The AWS region to use. + * @param timeout The timeout to use. + */ + public MistralChatBedrockApi(String modelId, String region, Duration timeout) { + super(modelId, region, timeout); + } + + /** + * Create a new MistralChatBedrockApi instance using the provided credentials provider, region and object mapper. + * + * @param modelId The model id to use. See the {@link MistralChatModel} for the supported models. + * @param credentialsProvider The credentials provider to connect to AWS. + * @param region The AWS region to use. + * @param objectMapper The object mapper to use for JSON serialization and deserialization. + * @param timeout The timeout to use. + */ + public MistralChatBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, String region, + ObjectMapper objectMapper, Duration timeout) { + super(modelId, credentialsProvider, region, objectMapper, timeout); + } + + /** + * Create a new MistralChatBedrockApi instance using the provided credentials provider, region and object mapper. + * + * @param modelId The model id to use. See the {@link MistralChatModel} for the supported models. + * @param credentialsProvider The credentials provider to connect to AWS. + * @param region The AWS region to use. + * @param objectMapper The object mapper to use for JSON serialization and deserialization. + * @param timeout The timeout to use. + */ + public MistralChatBedrockApi(String modelId, AwsCredentialsProvider credentialsProvider, Region region, + ObjectMapper objectMapper, Duration timeout) { + super(modelId, credentialsProvider, region, objectMapper, timeout); + } + + /** + * MistralChatRequest encapsulates the request parameters for the Mistral model. + * + * @param prompt The input prompt to generate the response from. + * @param temperature (optional) Use a lower value to decrease randomness in the response. + * @param topP (optional) Use a lower value to ignore less probable options. Set to 0 or 1.0 to disable. + * @param topK (optional) Specify the number of token choices the model uses to generate the next token. + * @param maxTokens (optional) Specify the maximum number of tokens to use in the generated response. + * @param stopSequences (optional) Configure up to four sequences that the model recognizes. After a stop sequence, + * the model stops generating further tokens. The returned text doesn't contain the stop sequence. + */ + @JsonInclude(Include.NON_NULL) + public record MistralChatRequest( + @JsonProperty("prompt") String prompt, + @JsonProperty("temperature") Float temperature, + @JsonProperty("top_p") Float topP, + @JsonProperty("top_k") Integer topK, + @JsonProperty("max_tokens") Integer maxTokens, + @JsonProperty("stop") List stopSequences) { + + /** + * Get MistralChatRequest builder. + * @param prompt compulsory request prompt parameter. + * @return MistralChatRequest builder. + */ + public static Builder builder(String prompt) { + return new Builder(prompt); + } + + /** + * Builder for the MistralChatRequest. + */ + public static class Builder { + private final String prompt; + private Float temperature; + private Float topP; + private Integer topK; + private Integer maxTokens; + private List stopSequences; + + public Builder(String prompt) { + this.prompt = prompt; + } + + public Builder withTemperature(Float temperature) { + this.temperature = temperature; + return this; + } + + public Builder withTopP(Float topP) { + this.topP = topP; + return this; + } + + public Builder withTopK(Integer topK) { + this.topK = topK; + return this; + } + + public Builder withMaxTokens(Integer maxTokens) { + this.maxTokens = maxTokens; + return this; + } + + public Builder withStopSequences(List stopSequences) { + this.stopSequences = stopSequences; + return this; + } + + public MistralChatRequest build() { + return new MistralChatRequest( + prompt, + temperature, + topP, + topK, + maxTokens, + stopSequences + ); + } + } + } + + /** + * MistralChatResponse encapsulates the response parameters for the Mistral model. + * + * @param A list of outputs from the model. Each output has the following fields. + */ + @JsonInclude(Include.NON_NULL) + public record MistralChatResponse( + @JsonProperty("outputs") List outputs, + @JsonProperty("amazon-bedrock-invocationMetrics") AmazonBedrockInvocationMetrics amazonBedrockInvocationMetrics) { + + /** + * Generated result along with the likelihoods for tokens requested. + * + * @param text The text that the model generated. + * @param stopReason The reason why the response stopped generating text. + */ + public record Generation( + @JsonProperty("text") String text, + @JsonProperty("stop_reason") String stopReason) { + } + + } + + /** + * Mistral models version. + */ + public enum MistralChatModel implements ModelDescription { + + /** + * mistral.mistral-7b-instruct-v0:2 + */ + MISTRAL_7B_INSTRUCT("mistral.mistral-7b-instruct-v0:2"), + + /** + * mistral.mixtral-8x7b-instruct-v0:1 + */ + MISTRAL_8X7B_INSTRUCT("mistral.mixtral-8x7b-instruct-v0:1"), + + /** + * mistral.mistral-large-2402-v1:0 + */ + MISTRAL_LARGE("mistral.mistral-large-2402-v1:0"), + + /** + * mistral.mistral-small-2402-v1:0 + */ + MISTRAL_SMALL("mistral.mistral-small-2402-v1:0"); + + private final String id; + + /** + * @return The model id. + */ + public String id() { + return id; + } + + MistralChatModel(String value) { + this.id = value; + } + + @Override + public String getModelName() { + return this.id; + } + } + + @Override + public MistralChatResponse chatCompletion(MistralChatRequest mistralRequest) { + Assert.notNull(mistralRequest, "'MistralChatRequest' must not be null"); + return this.internalInvocation(mistralRequest, MistralChatResponse.class); + } + + @Override + public Flux chatCompletionStream(MistralChatRequest mistralRequest) { + Assert.notNull(mistralRequest, "'MistralChatRequest' must not be null"); + return this.internalInvocationStream(mistralRequest, MistralChatResponse.class); + } + +} +//@formatter:on \ No newline at end of file diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatCreateRequestTests.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatCreateRequestTests.java new file mode 100644 index 00000000000..356223add3c --- /dev/null +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatCreateRequestTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2023 - 2024 the original author or 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 + * + * https://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.springframework.ai.bedrock.mistral; + +import java.time.Duration; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.regions.Region; + +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatModel; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatRequest; +import org.springframework.ai.chat.prompt.Prompt; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Wei Jiang + * @since 1.0.0 + */ +public class BedrockMistralChatCreateRequestTests { + + private MistralChatBedrockApi chatApi = new MistralChatBedrockApi(MistralChatModel.MISTRAL_8X7B_INSTRUCT.id(), + EnvironmentVariableCredentialsProvider.create(), Region.US_EAST_1.id(), new ObjectMapper(), + Duration.ofMinutes(2)); + + @Test + public void createRequestWithChatOptions() { + + var client = new BedrockMistralChatModel(chatApi, + BedrockMistralChatOptions.builder() + .withTemperature(66.6f) + .withTopK(66) + .withTopP(0.66f) + .withMaxTokens(678) + .withStopSequences(List.of("stop1", "stop2")) + .build()); + + MistralChatRequest request = client.createRequest(new Prompt("Test message content")); + + assertThat(request.prompt()).isNotEmpty(); + + assertThat(request.temperature()).isEqualTo(66.6f); + assertThat(request.topK()).isEqualTo(66); + assertThat(request.topP()).isEqualTo(0.66f); + assertThat(request.maxTokens()).isEqualTo(678); + assertThat(request.stopSequences()).containsExactly("stop1", "stop2"); + } + +} diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModelIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModelIT.java new file mode 100644 index 00000000000..1c172aa2c8f --- /dev/null +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModelIT.java @@ -0,0 +1,219 @@ +/* + * Copyright 2023 - 2024 the original author or 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 + * + * https://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.springframework.ai.bedrock.mistral; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatModel; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.chat.prompt.PromptTemplate; +import org.springframework.ai.chat.prompt.SystemPromptTemplate; +import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.ai.converter.ListOutputConverter; +import org.springframework.ai.converter.MapOutputConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.io.Resource; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import reactor.core.publisher.Flux; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.regions.Region; + +/** + * @author Wei Jiang + * @since 1.0.0 + */ +@SpringBootTest +@EnabledIfEnvironmentVariable(named = "AWS_ACCESS_KEY_ID", matches = ".*") +@EnabledIfEnvironmentVariable(named = "AWS_SECRET_ACCESS_KEY", matches = ".*") +public class BedrockMistralChatModelIT { + + @Autowired + private BedrockMistralChatModel chatModel; + + @Value("classpath:/prompts/system-message.st") + private Resource systemResource; + + @Test + void multipleStreamAttempts() { + + Flux joke1Stream = chatModel.stream(new Prompt(new UserMessage("Tell me a joke?"))); + Flux joke2Stream = chatModel.stream(new Prompt(new UserMessage("Tell me a toy joke?"))); + + String joke1 = joke1Stream.collectList() + .block() + .stream() + .map(ChatResponse::getResults) + .flatMap(List::stream) + .map(Generation::getOutput) + .map(AssistantMessage::getContent) + .collect(Collectors.joining()); + String joke2 = joke2Stream.collectList() + .block() + .stream() + .map(ChatResponse::getResults) + .flatMap(List::stream) + .map(Generation::getOutput) + .map(AssistantMessage::getContent) + .collect(Collectors.joining()); + + assertThat(joke1).isNotBlank(); + assertThat(joke2).isNotBlank(); + } + + @Test + void roleTest() { + String request = "Tell me about 3 famous pirates from the Golden Age of Piracy and why they did."; + String name = "Bob"; + String voice = "pirate"; + UserMessage userMessage = new UserMessage(request); + SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource); + Message systemMessage = systemPromptTemplate.createMessage(Map.of("name", name, "voice", voice)); + Prompt prompt = new Prompt(List.of(userMessage, systemMessage)); + ChatResponse response = chatModel.call(prompt); + assertThat(response.getResult().getOutput().getContent()).contains("Blackbeard"); + } + + @Test + void listOutputConverter() { + DefaultConversionService conversionService = new DefaultConversionService(); + ListOutputConverter outputConverter = new ListOutputConverter(conversionService); + + String format = outputConverter.getFormat(); + String template = """ + List five {subject} + {format} + """; + PromptTemplate promptTemplate = new PromptTemplate(template, + Map.of("subject", "ice cream flavors.", "format", format)); + Prompt prompt = new Prompt(promptTemplate.createMessage()); + Generation generation = this.chatModel.call(prompt).getResult(); + + List list = outputConverter.convert(generation.getOutput().getContent()); + assertThat(list).hasSize(5); + } + + @Test + void mapOutputConverter() { + MapOutputConverter outputConverter = new MapOutputConverter(); + + String format = outputConverter.getFormat(); + String template = """ + Remove Markdown code blocks from the output. + Provide me a List of {subject} + {format} + """; + PromptTemplate promptTemplate = new PromptTemplate(template, + Map.of("subject", "an array of numbers from 1 to 9 under they key name 'numbers'", "format", format)); + Prompt prompt = new Prompt(promptTemplate.createMessage()); + Generation generation = chatModel.call(prompt).getResult(); + + Map result = outputConverter.convert(generation.getOutput().getContent()); + assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)); + + } + + record ActorsFilmsRecord(String actor, List movies) { + } + + @Test + void beanOutputConverterRecords() { + + BeanOutputConverter outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class); + + String format = outputConverter.getFormat(); + String template = """ + Generate the filmography of 5 movies for Tom Hanks. + {format} + Remove Markdown code blocks from the output. + """; + PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format)); + Prompt prompt = new Prompt(promptTemplate.createMessage()); + Generation generation = chatModel.call(prompt).getResult(); + + ActorsFilmsRecord actorsFilms = outputConverter.convert(generation.getOutput().getContent()); + assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks"); + assertThat(actorsFilms.movies()).hasSize(5); + } + + @Test + void beanStreamOutputConverterRecords() { + + BeanOutputConverter outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class); + + String format = outputConverter.getFormat(); + String template = """ + Generate the filmography of 5 movies for Tom Hanks. + {format} + Remove Markdown code blocks from the output. + """; + PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format)); + Prompt prompt = new Prompt(promptTemplate.createMessage()); + + String generationTextFromStream = chatModel.stream(prompt) + .collectList() + .block() + .stream() + .map(ChatResponse::getResults) + .flatMap(List::stream) + .map(Generation::getOutput) + .map(AssistantMessage::getContent) + .collect(Collectors.joining()); + + ActorsFilmsRecord actorsFilms = outputConverter.convert(generationTextFromStream); + System.out.println(actorsFilms); + assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks"); + assertThat(actorsFilms.movies()).hasSize(5); + } + + @SpringBootConfiguration + public static class TestConfiguration { + + @Bean + public MistralChatBedrockApi mistralApi() { + return new MistralChatBedrockApi(MistralChatModel.MISTRAL_8X7B_INSTRUCT.id(), + EnvironmentVariableCredentialsProvider.create(), Region.US_EAST_1.id(), new ObjectMapper(), + Duration.ofMinutes(2)); + } + + @Bean + public BedrockMistralChatModel mistralChatModel(MistralChatBedrockApi cohereApi) { + return new BedrockMistralChatModel(cohereApi); + } + + } + +} diff --git a/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApiIT.java b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApiIT.java new file mode 100644 index 00000000000..3389a9e520b --- /dev/null +++ b/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApiIT.java @@ -0,0 +1,82 @@ +/* + * Copyright 2023 - 2024 the original author or 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 + * + * https://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.springframework.ai.bedrock.mistral.api; + +import java.time.Duration; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatRequest; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatResponse; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatModel; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import reactor.core.publisher.Flux; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.regions.Region; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Wei Jiang + * @since 1.0.0 + */ +@EnabledIfEnvironmentVariable(named = "AWS_ACCESS_KEY_ID", matches = ".*") +@EnabledIfEnvironmentVariable(named = "AWS_SECRET_ACCESS_KEY", matches = ".*") +public class MistralChatBedrockApiIT { + + private MistralChatBedrockApi mistralChatApi = new MistralChatBedrockApi( + MistralChatModel.MISTRAL_8X7B_INSTRUCT.id(), EnvironmentVariableCredentialsProvider.create(), + Region.US_EAST_1.id(), new ObjectMapper(), Duration.ofMinutes(2)); + + @Test + public void chatCompletion() { + + MistralChatRequest request = MistralChatRequest.builder("Hello, Who are you?") + .withTemperature(0.9f) + .withTopP(0.9f) + .build(); + + MistralChatResponse response = mistralChatApi.chatCompletion(request); + + assertThat(response).isNotNull(); + assertThat(response.outputs()).isNotEmpty(); + assertThat(response.outputs().get(0)).isNotNull(); + assertThat(response.outputs().get(0).text()).isNotNull(); + assertThat(response.outputs().get(0).stopReason()).isNotNull(); + } + + @Test + public void chatCompletionStream() { + + MistralChatRequest request = MistralChatRequest.builder("Hello, Who are you?") + .withTemperature(0.9f) + .withTopP(0.9f) + .build(); + Flux responseStream = mistralChatApi.chatCompletionStream(request); + List responses = responseStream.collectList().block(); + + assertThat(responses).isNotNull(); + assertThat(responses).hasSizeGreaterThan(10); + assertThat(responses.get(0).outputs()).isNotEmpty(); + + MistralChatResponse lastResponse = responses.get(responses.size() - 1); + assertThat(lastResponse.amazonBedrockInvocationMetrics()).isNotNull(); + } + +} diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/bedrock/bedrock-mistral-chat-low-level-api.png b/spring-ai-docs/src/main/antora/modules/ROOT/images/bedrock/bedrock-mistral-chat-low-level-api.png new file mode 100644 index 0000000000000000000000000000000000000000..e1276c7b22f3cb6aa692c6dfbf2820efd1b8c929 GIT binary patch literal 92100 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 literal 0 HcmV?d00001 diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc index 120a8e5c8b2..380b39c3852 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc @@ -16,6 +16,7 @@ **** xref:api/chat/bedrock/bedrock-cohere.adoc[Cohere] **** xref:api/chat/bedrock/bedrock-titan.adoc[Titan] **** xref:api/chat/bedrock/bedrock-jurassic2.adoc[Jurassic2] +**** xref:api/chat/bedrock/bedrock-mistral.adoc[Mistral] *** xref:api/chat/huggingface.adoc[HuggingFace] *** xref:api/chat/google-vertexai.adoc[Google VertexAI] **** xref:api/chat/vertexai-palm2-chat.adoc[VertexAI PaLM2 ] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock.adoc index f8b2b2062a1..dcad2f6e5e9 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/bedrock.adoc @@ -108,3 +108,4 @@ For more information, refer to the documentation below for each supported model. * xref:api/chat/bedrock/bedrock-titan.adoc[Spring AI Bedrock Titan Chat]: `spring.ai.bedrock.titan.chat.enabled=true` * xref:api/embeddings/bedrock-titan-embedding.adoc[Spring AI Bedrock Titan Embeddings]: `spring.ai.bedrock.titan.embedding.enabled=true` * xref:api/chat/bedrock/bedrock-jurassic2.adoc[Spring AI Bedrock Ai21 Jurassic2 Chat]: `spring.ai.bedrock.jurassic2.chat.enabled=true` +* xref:api/chat/bedrock/bedrock-mistral.adoc[Spring AI Bedrock Mistral Chat]: `spring.ai.bedrock.mistral.chat.enabled=true` diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/bedrock/bedrock-mistral.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/bedrock/bedrock-mistral.adoc new file mode 100644 index 00000000000..0d329e8fa7a --- /dev/null +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/bedrock/bedrock-mistral.adoc @@ -0,0 +1,251 @@ += Mistral Chat + +Provides Bedrock Mistral chat model. +Integrate generative AI capabilities into essential apps and workflows that improve business outcomes. + +The https://aws.amazon.com/bedrock/mistral/[AWS Bedrock Mistral Model Page] and https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html[Amazon Bedrock User Guide] contains detailed information on how to use the AWS hosted model. + +== Prerequisites + +Refer to the xref:api/bedrock.adoc[Spring AI documentation on Amazon Bedrock] for setting up API access. + +=== Add Repositories and BOM + +Spring AI artifacts are published in Spring Milestone and Snapshot repositories. Refer to the xref:getting-started.adoc#repositories[Repositories] section to add these repositories to your build system. + +To help with dependency management, Spring AI provides a BOM (bill of materials) to ensure that a consistent version of Spring AI is used throughout the entire project. Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build system. + + +== Auto-configuration + +Add the `spring-ai-bedrock-ai-spring-boot-starter` dependency to your project's Maven `pom.xml` file: + +[source,xml] +---- + + org.springframework.ai + spring-ai-bedrock-ai-spring-boot-starter + +---- + +or to your Gradle `build.gradle` build file. + +[source,gradle] +---- +dependencies { + implementation 'org.springframework.ai:spring-ai-bedrock-ai-spring-boot-starter' +} +---- + +TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file. + +=== Enable Mistral Chat Support + +By default the Mistral model is disabled. +To enable it set the `spring.ai.bedrock.mistral.chat.enabled` property to `true`. +Exporting environment variable is one way to set this configuration property: + +[source,shell] +---- +export SPRING_AI_BEDROCK_MISTRAL_CHAT_ENABLED=true +---- + +=== Chat Properties + +The prefix `spring.ai.bedrock.aws` is the property prefix to configure the connection to AWS Bedrock. + +[cols="3,3,3"] +|==== +| Property | Description | Default + +| spring.ai.bedrock.aws.region | AWS region to use. | us-east-1 +| spring.ai.bedrock.aws.timeout | AWS timeout to use. | 5m +| spring.ai.bedrock.aws.access-key | AWS access key. | - +| spring.ai.bedrock.aws.secret-key | AWS secret key. | - +|==== + +The prefix `spring.ai.bedrock.mistral.chat` is the property prefix that configures the chat model implementation for Mistral. + +[cols="2,5,1"] +|==== +| Property | Description | Default + +| spring.ai.bedrock.mistral.chat.enabled | Enable or disable support for Mistral | false +| spring.ai.bedrock.mistral.chat.model | The model id to use. See the https://github.com/spring-projects/spring-ai/blob/4ba9a3cd689b9fd3a3805f540debe398a079c6ef/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApi.java#L326C14-L326C29[MistralChatModel] for the supported models. | mistral.mistral-large-2402-v1:0 +| spring.ai.bedrock.mistral.chat.options.temperature | Controls the randomness of the output. Values can range over [0.0,1.0] | 0.7 +| spring.ai.bedrock.mistral.chat.options.topP | The maximum cumulative probability of tokens to consider when sampling. | AWS Bedrock default +| spring.ai.bedrock.mistral.chat.options.topK | Specify the number of token choices the model uses to generate the next token | AWS Bedrock default +| spring.ai.bedrock.mistral.chat.options.maxTokens | Specify the maximum number of tokens to use in the generated response. | AWS Bedrock default +| spring.ai.bedrock.mistral.chat.options.stopSequences | Configure up to four sequences that the model recognizes. | AWS Bedrock default +|==== + +Look at the https://github.com/spring-projects/spring-ai/blob/4ba9a3cd689b9fd3a3805f540debe398a079c6ef/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApi.java#L326C14-L326C29[MistralChatModel] for other model IDs. +Supported values are: `mistral.mistral-7b-instruct-v0:2`, `mistral.mixtral-8x7b-instruct-v0:1`, `mistral.mistral-large-2402-v1:0` and `mistral.mistral-small-2402-v1:0`. +Model ID values can also be found in the https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids-arns.html[AWS Bedrock documentation for base model IDs]. + +TIP: All properties prefixed with `spring.ai.bedrock.mistral.chat.options` can be overridden at runtime by adding a request specific <> to the `Prompt` call. + +== Runtime Options [[chat-options]] + +The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatOptions.java[BedrockMistralChatOptions.java] provides model configurations, such as temperature, topK, topP, etc. + +On start-up, the default options can be configured with the `BedrockMistralChatModel(api, options)` constructor or the `spring.ai.bedrock.mistral.chat.options.*` properties. + +At run-time you can override the default options by adding new, request specific, options to the `Prompt` call. +For example to override the default temperature for a specific request: + +[source,java] +---- +ChatResponse response = chatModel.call( + new Prompt( + "Generate the names of 5 famous pirates.", + BedrockMistralChatOptions.builder() + .withTemperature(0.4) + .build() + )); +---- + +TIP: In addition to the model specific https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatOptions.java[BedrockMistralChatOptions] you can use a portable https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptions.java[ChatOptions] instance, created with the https://github.com/spring-projects/spring-ai/blob/main/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/ChatOptionsBuilder.java[ChatOptionsBuilder#builder()]. + +== Sample Controller + +https://start.spring.io/[Create] a new Spring Boot project and add the `spring-ai-bedrock-ai-spring-boot-starter` to your pom (or gradle) dependencies. + +Add a `application.properties` file, under the `src/main/resources` directory, to enable and configure the Mistral chat model: + +[source] +---- +spring.ai.bedrock.aws.region=eu-central-1 +spring.ai.bedrock.aws.timeout=1000ms +spring.ai.bedrock.aws.access-key=${AWS_ACCESS_KEY_ID} +spring.ai.bedrock.aws.secret-key=${AWS_SECRET_ACCESS_KEY} + +spring.ai.bedrock.mistral.chat.enabled=true +spring.ai.bedrock.mistral.chat.options.temperature=0.8 +---- + +TIP: replace the `regions`, `access-key` and `secret-key` with your AWS credentials. + +This will create a `BedrockMistralChatModel` implementation that you can inject into your class. +Here is an example of a simple `@Controller` class that uses the chat model for text generations. + +[source,java] +---- +@RestController +public class ChatController { + + private final BedrockMistralChatModel chatModel; + + @Autowired + public ChatController(BedrockMistralChatModel chatModel) { + this.chatModel = chatModel; + } + + @GetMapping("/ai/generate") + public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) { + return Map.of("generation", chatModel.call(message)); + } + + @GetMapping("/ai/generateStream") + public Flux generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) { + Prompt prompt = new Prompt(new UserMessage(message)); + return chatModel.stream(prompt); + } +} +---- + +== Manual Configuration + +The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModel.java[BedrockMistralChatModel] implements the `ChatModel` and `StreamingChatModel` and uses the <> to connect to the Bedrock Mistral service. + +Add the `spring-ai-bedrock` dependency to your project's Maven `pom.xml` file: + +[source,xml] +---- + + org.springframework.ai + spring-ai-bedrock + +---- + +or to your Gradle `build.gradle` build file. + +[source,gradle] +---- +dependencies { + implementation 'org.springframework.ai:spring-ai-bedrock' +} +---- + +TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file. + +Next, create an https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModel.java[BedrockMistralChatModel] and use it for text generations: + +[source,java] +---- +MistralChatBedrockApi api = new MistralChatBedrockApi(MistralChatModel.MISTRAL_8X7B_INSTRUCT.id(), + EnvironmentVariableCredentialsProvider.create(), + Region.US_EAST_1.id(), + new ObjectMapper(), + Duration.ofMillis(1000L)); + +BedrockMistralChatModel chatModel = new BedrockMistralChatModel(api, + BedrockMistralChatOptions.builder() + .withTemperature(0.6f) + .withTopK(10) + .withTopP(0.5f) + .withMaxTokens(678) + .build() + +ChatResponse response = chatModel.call( + new Prompt("Generate the names of 5 famous pirates.")); + +// Or with streaming responses +Flux response = chatModel.stream( + new Prompt("Generate the names of 5 famous pirates.")); +---- + +== Low-level MistralChatBedrockApi Client [[low-level-api]] + +The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/mistral/api/MistralChatBedrockApi.java[MistralChatBedrockApi] provides is lightweight Java client on top of AWS Bedrock https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-mistral-text-completion.html[Mistral Command models]. + +Following class diagram illustrates the MistralChatBedrockApi interface and building blocks: + +image::bedrock/bedrock-mistral-chat-low-level-api.png[MistralChatBedrockApi Class Diagram] + +The MistralChatBedrockApi supports the `mistral.mistral-7b-instruct-v0:2`, `mistral.mixtral-8x7b-instruct-v0:1`, `mistral.mistral-large-2402-v1:0` and `mistral.mistral-small-2402-v1:0` models for both synchronous (e.g. `chatCompletion()`) and streaming (e.g. `chatCompletionStream()`) requests. + +Here is a simple snippet how to use the api programmatically: + +[source,java] +---- +MistralChatBedrockApi mistralChatApi = new MistralChatBedrockApi( + MistralChatModel.MISTRAL_8X7B_INSTRUCT.id(), + Region.US_EAST_1.id(), + Duration.ofMillis(1000L)); + +var request = MistralChatRequest + .builder("What is the capital of Bulgaria and what is the size? What it the national anthem?") + .withTemperature(0.5f) + .withTopP(0.8f) + .withTopK(15) + .withMaxTokens(100) + .withStopSequences(List.of("END")) + .build(); + +MistralChatResponse response = mistralChatApi.chatCompletion(request); + +var request = MistralChatRequest + .builder("What is the capital of Bulgaria and what is the size? What it the national anthem?") + .withTemperature(0.5f) + .withTopP(0.8f) + .withTopK(15) + .withMaxTokens(100) + .withStopSequences(List.of("END")) + .build(); + +Flux responseStream = mistralChatApi.chatCompletionStream(request); +List responses = responseStream.collectList().block(); +---- + + diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatmodel.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatmodel.adoc index 9fdce3b424f..8918e34e0eb 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatmodel.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatmodel.adoc @@ -202,6 +202,7 @@ image::spring-ai-chat-completions-clients.jpg[align="center", width="800px"] ** xref:api/chat/bedrock/bedrock-titan.adoc[Titan Chat Completion] ** xref:api/chat/bedrock/bedrock-anthropic.adoc[Anthropic Chat Completion] ** xref:api/chat/bedrock/bedrock-jurassic2.adoc[Jurassic2 Chat Completion] +** xref:api/chat/bedrock/bedrock-mistral.adoc[Mistral Chat Completion] * xref:api/chat/mistralai-chat.adoc[Mistral AI Chat Completion] (streaming & function-calling support) * xref:api/chat/anthropic-chat.adoc[Anthropic Chat Completion] (streaming) diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc index 6346d4e5c16..8d9d9f3b487 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc @@ -210,7 +210,8 @@ The following AI Models have been tested to support List, Map and Bean structure | xref:api/chat/bedrock/bedrock-anthropic.adoc[Bedrock Anthropic 2] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic/BedrockAnthropicChatModelIT.java[BedrockAnthropicChatModelIT.java] | xref:api/chat/bedrock/bedrock-anthropic3.adoc[Bedrock Anthropic 3] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/anthropic3/BedrockAnthropic3ChatModelIT.java[BedrockAnthropic3ChatModelIT.java] | xref:api/chat/bedrock/bedrock-cohere.adoc[Bedrock Cohere] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/cohere/BedrockCohereChatModelIT.java[BedrockCohereChatModelIT.java] -| xref:api/chat/bedrock/bedrock-llama.adoc[Bedrock Llama] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatModelIT.java[BedrockLlamaChatModelIT.java.java] +| xref:api/chat/bedrock/bedrock-llama.adoc[Bedrock Llama] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/llama/BedrockLlamaChatModelIT.java[BedrockLlamaChatModelIT.java] +| xref:api/chat/bedrock/bedrock-mistral.adoc[Bedrock Mistral] | link:https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-bedrock/src/test/java/org/springframework/ai/bedrock/mistral/BedrockMistralChatModelIT.java[BedrockMistralChatModelIT.java] |==== == Build-in JSON mode diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/getting-started.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/getting-started.adoc index 224df22be4f..b4378566d8c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/getting-started.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/getting-started.adoc @@ -153,6 +153,7 @@ Each of the following sections in the documentation shows which dependencies you *** xref:api/chat/bedrock/bedrock-titan.adoc[Titan Chat Completion] *** xref:api/chat/bedrock/bedrock-anthropic.adoc[Anthropic Chat Completion] *** xref:api/chat/bedrock/bedrock-jurassic2.adoc[Jurassic2 Chat Completion] +*** xref:api/chat/bedrock/bedrock-mistral.adoc[Mistral Chat Completion] ** xref:api/chat/mistralai-chat.adoc[MistralAI Chat Completion] (streaming and function-calling support) === Image Generation Models diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatAutoConfiguration.java new file mode 100644 index 00000000000..2c897a5a5c5 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatAutoConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright 2023 - 2024 the original author or 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 + * + * https://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.springframework.ai.autoconfigure.bedrock.mistral; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionConfiguration; +import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionProperties; +import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; +import org.springframework.ai.bedrock.mistral.BedrockMistralChatModel; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.retry.support.RetryTemplate; + +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.providers.AwsRegionProvider; + +/** + * {@link AutoConfiguration Auto-configuration} for Bedrock Mistral Chat Client. + * + * @author Wei Jiang + * @since 1.0.0 + */ +@AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class }) +@ConditionalOnClass(MistralChatBedrockApi.class) +@EnableConfigurationProperties({ BedrockMistralChatProperties.class, BedrockAwsConnectionProperties.class }) +@ConditionalOnProperty(prefix = BedrockMistralChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true") +@Import(BedrockAwsConnectionConfiguration.class) +public class BedrockMistralChatAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) + public MistralChatBedrockApi mistralChatApi(AwsCredentialsProvider credentialsProvider, + AwsRegionProvider regionProvider, BedrockMistralChatProperties properties, + BedrockAwsConnectionProperties awsProperties) { + return new MistralChatBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), + new ObjectMapper(), awsProperties.getTimeout()); + } + + @Bean + @ConditionalOnBean(MistralChatBedrockApi.class) + public BedrockMistralChatModel mistralChatModel(MistralChatBedrockApi mistralChatApi, + BedrockMistralChatProperties properties, RetryTemplate retryTemplate) { + + return new BedrockMistralChatModel(mistralChatApi, properties.getOptions(), retryTemplate); + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatProperties.java new file mode 100644 index 00000000000..e3952efcecd --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatProperties.java @@ -0,0 +1,72 @@ +/* + * Copyright 2023 - 2024 the original author or 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 + * + * https://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.springframework.ai.autoconfigure.bedrock.mistral; + +import org.springframework.ai.bedrock.mistral.BedrockMistralChatOptions; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * Bedrock Mistral Chat autoconfiguration properties. + * + * @author Wei Jiang + * @since 1.0.0 + */ +@ConfigurationProperties(BedrockMistralChatProperties.CONFIG_PREFIX) +public class BedrockMistralChatProperties { + + public static final String CONFIG_PREFIX = "spring.ai.bedrock.mistral.chat"; + + /** + * Enable Bedrock Mistral Chat Client. False by default. + */ + private boolean enabled = false; + + /** + * Bedrock Mistral Chat generative name. Defaults to + * 'mistral.mistral-large-2402-v1:0'. + */ + private String model = MistralChatBedrockApi.MistralChatModel.MISTRAL_LARGE.id(); + + @NestedConfigurationProperty + private BedrockMistralChatOptions options = BedrockMistralChatOptions.builder().build(); + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getModel() { + return this.model; + } + + public void setModel(String model) { + this.model = model; + } + + public BedrockMistralChatOptions getOptions() { + return this.options; + } + + public void setOptions(BedrockMistralChatOptions options) { + this.options = options; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index c744be669c2..dfde9b22cef 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,6 +13,7 @@ org.springframework.ai.autoconfigure.bedrock.anthropic.BedrockAnthropicChatAutoC org.springframework.ai.autoconfigure.bedrock.anthropic3.BedrockAnthropic3ChatAutoConfiguration org.springframework.ai.autoconfigure.bedrock.titan.BedrockTitanChatAutoConfiguration org.springframework.ai.autoconfigure.bedrock.titan.BedrockTitanEmbeddingAutoConfiguration +org.springframework.ai.autoconfigure.bedrock.mistral.BedrockMistralChatAutoConfiguration org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration org.springframework.ai.autoconfigure.mistralai.MistralAiAutoConfiguration org.springframework.ai.autoconfigure.vectorstore.pgvector.PgVectorStoreAutoConfiguration diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatAutoConfigurationIT.java new file mode 100644 index 00000000000..dbef230a05c --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/mistral/BedrockMistralChatAutoConfigurationIT.java @@ -0,0 +1,159 @@ +/* + * Copyright 2023 - 2024 the original author or 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 + * + * https://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.springframework.ai.autoconfigure.bedrock.mistral; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.messages.AssistantMessage; +import reactor.core.publisher.Flux; +import software.amazon.awssdk.regions.Region; + +import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionProperties; +import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; +import org.springframework.ai.bedrock.mistral.BedrockMistralChatModel; +import org.springframework.ai.bedrock.mistral.api.MistralChatBedrockApi.MistralChatModel; +import org.springframework.ai.chat.model.Generation; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.chat.prompt.SystemPromptTemplate; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Wei Jiang + * @since 1.0.0 + */ +@EnabledIfEnvironmentVariable(named = "AWS_ACCESS_KEY_ID", matches = ".*") +@EnabledIfEnvironmentVariable(named = "AWS_SECRET_ACCESS_KEY", matches = ".*") +public class BedrockMistralChatAutoConfigurationIT { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.ai.bedrock.mistral.chat.enabled=true", + "spring.ai.bedrock.aws.access-key=" + System.getenv("AWS_ACCESS_KEY_ID"), + "spring.ai.bedrock.aws.secret-key=" + System.getenv("AWS_SECRET_ACCESS_KEY"), + "spring.ai.bedrock.aws.region=" + Region.US_EAST_1.id(), + "spring.ai.bedrock.mistral.chat.model=" + MistralChatModel.MISTRAL_SMALL.id(), + "spring.ai.bedrock.mistral.chat.options.temperature=0.5") + .withConfiguration( + AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, BedrockMistralChatAutoConfiguration.class)); + + private final Message systemMessage = new SystemPromptTemplate(""" + You are a helpful AI assistant. Your name is {name}. + You are an AI assistant that helps people find information. + Your name is {name} + You should reply to the user's request with your name and also in the style of a {voice}. + """).createMessage(Map.of("name", "Bob", "voice", "pirate")); + + private final UserMessage userMessage = new UserMessage( + "Tell me about 3 famous pirates from the Golden Age of Piracy and why they did."); + + @Test + public void chatCompletion() { + contextRunner.run(context -> { + BedrockMistralChatModel mistralChatModel = context.getBean(BedrockMistralChatModel.class); + ChatResponse response = mistralChatModel.call(new Prompt(List.of(userMessage, systemMessage))); + assertThat(response.getResult().getOutput().getContent()).contains("Blackbeard"); + }); + } + + @Test + public void chatCompletionStreaming() { + contextRunner.run(context -> { + + BedrockMistralChatModel mistralChatModel = context.getBean(BedrockMistralChatModel.class); + + Flux response = mistralChatModel.stream(new Prompt(List.of(userMessage, systemMessage))); + + List responses = response.collectList().block(); + assertThat(responses.size()).isGreaterThan(2); + + String stitchedResponseContent = responses.stream() + .map(ChatResponse::getResults) + .flatMap(List::stream) + .map(Generation::getOutput) + .map(AssistantMessage::getContent) + .collect(Collectors.joining()); + + assertThat(stitchedResponseContent).contains("Blackbeard"); + }); + } + + @Test + public void propertiesTest() { + + new ApplicationContextRunner() + .withPropertyValues("spring.ai.bedrock.mistral.chat.enabled=true", + "spring.ai.bedrock.aws.access-key=ACCESS_KEY", "spring.ai.bedrock.aws.secret-key=SECRET_KEY", + "spring.ai.bedrock.mistral.chat.model=MODEL_XYZ", + "spring.ai.bedrock.aws.region=" + Region.US_EAST_1.id(), + "spring.ai.bedrock.mistral.chat.options.temperature=0.55") + .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, + BedrockMistralChatAutoConfiguration.class)) + .run(context -> { + var mistralChatProperties = context.getBean(BedrockMistralChatProperties.class); + var awsProperties = context.getBean(BedrockAwsConnectionProperties.class); + + assertThat(mistralChatProperties.isEnabled()).isTrue(); + assertThat(awsProperties.getRegion()).isEqualTo(Region.US_EAST_1.id()); + + assertThat(mistralChatProperties.getOptions().getTemperature()).isEqualTo(0.55f); + assertThat(mistralChatProperties.getModel()).isEqualTo("MODEL_XYZ"); + + assertThat(awsProperties.getAccessKey()).isEqualTo("ACCESS_KEY"); + assertThat(awsProperties.getSecretKey()).isEqualTo("SECRET_KEY"); + }); + } + + @Test + public void chatCompletionDisabled() { + + // It is disabled by default + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, + BedrockMistralChatAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(BedrockMistralChatProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(BedrockMistralChatModel.class)).isEmpty(); + }); + + // Explicitly enable the chat auto-configuration. + new ApplicationContextRunner().withPropertyValues("spring.ai.bedrock.mistral.chat.enabled=true") + .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, + BedrockMistralChatAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(BedrockMistralChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(BedrockMistralChatModel.class)).isNotEmpty(); + }); + + // Explicitly disable the chat auto-configuration. + new ApplicationContextRunner().withPropertyValues("spring.ai.bedrock.mistral.chat.enabled=false") + .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, + BedrockMistralChatAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(BedrockMistralChatProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(BedrockMistralChatModel.class)).isEmpty(); + }); + } + +}