Add Mistral AI Function Calling support
- Make MistralAiChatClient extend the AbstractFunctionCallSupport and implement the necessary abstract classes. - Extend the MistralAiApi to include the latest (undocumented) changes providing function calling support as well. The Mistral AI is almost identical to the OpenAI API except it doesn't support parallel function colling (e.g. missing tool_call_id). - Add MistralAiApi function calling tests (implement the Mistral tutorial). - Extend the misral chat options to include the new API features and function call abstractions. - Extend Mistral's chat auto-configration to accomodate the function callback support. - Add ITs for testing function calling. - Remove redundant code from MistralAiApi and OpenAiApi. - Simplify and improve the HTTP error handling in OpenAiApi, ImageAiApi and MistralAiApi.
This commit is contained in:
@@ -16,8 +16,10 @@
|
||||
package org.springframework.ai.mistralai;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@@ -32,18 +34,29 @@ import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletion;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage.ToolCall;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.model.function.AbstractFunctionCallSupport;
|
||||
import org.springframework.ai.model.function.FunctionCallbackContext;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.retry.RetryCallback;
|
||||
import org.springframework.retry.RetryContext;
|
||||
import org.springframework.retry.RetryListener;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* @author Ricken Bazolo
|
||||
* @author Christian Tzolov
|
||||
* @since 0.8.1
|
||||
*/
|
||||
public class MistralAiChatClient implements ChatClient, StreamingChatClient {
|
||||
public class MistralAiChatClient extends
|
||||
AbstractFunctionCallSupport<MistralAiApi.ChatCompletionMessage, MistralAiApi.ChatCompletionRequest, ResponseEntity<MistralAiApi.ChatCompletion>>
|
||||
implements ChatClient, StreamingChatClient {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(getClass());
|
||||
|
||||
@@ -69,13 +82,6 @@ public class MistralAiChatClient implements ChatClient, StreamingChatClient {
|
||||
})
|
||||
.build();
|
||||
|
||||
public MistralAiChatClient(MistralAiApi mistralAiApi, MistralAiChatOptions options) {
|
||||
Assert.notNull(mistralAiApi, "MistralAiApi must not be null");
|
||||
Assert.notNull(options, "Options must not be null");
|
||||
this.mistralAiApi = mistralAiApi;
|
||||
this.defaultOptions = options;
|
||||
}
|
||||
|
||||
public MistralAiChatClient(MistralAiApi mistralAiApi) {
|
||||
this(mistralAiApi,
|
||||
MistralAiChatOptions.builder()
|
||||
@@ -86,60 +92,41 @@ public class MistralAiChatClient implements ChatClient, StreamingChatClient {
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessible for testing.
|
||||
*/
|
||||
public MistralAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
|
||||
var chatCompletionMessages = prompt.getInstructions()
|
||||
.stream()
|
||||
.map(m -> new MistralAiApi.ChatCompletionMessage(m.getContent(),
|
||||
MistralAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name())))
|
||||
.toList();
|
||||
public MistralAiChatClient(MistralAiApi mistralAiApi, MistralAiChatOptions options) {
|
||||
this(mistralAiApi, options, null);
|
||||
}
|
||||
|
||||
var request = new MistralAiApi.ChatCompletionRequest(chatCompletionMessages, stream);
|
||||
|
||||
if (this.defaultOptions != null) {
|
||||
request = ModelOptionsUtils.merge(request, this.defaultOptions, MistralAiApi.ChatCompletionRequest.class);
|
||||
}
|
||||
|
||||
if (prompt.getOptions() != null) {
|
||||
if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {
|
||||
var updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ChatOptions.class,
|
||||
MistralAiChatOptions.class);
|
||||
request = ModelOptionsUtils.merge(updatedRuntimeOptions, request,
|
||||
MistralAiApi.ChatCompletionRequest.class);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Prompt options are not of type ChatOptions: "
|
||||
+ prompt.getOptions().getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
public MistralAiChatClient(MistralAiApi mistralAiApi, MistralAiChatOptions options,
|
||||
FunctionCallbackContext functionCallbackContext) {
|
||||
super(functionCallbackContext);
|
||||
Assert.notNull(mistralAiApi, "MistralAiApi must not be null");
|
||||
Assert.notNull(options, "Options must not be null");
|
||||
this.mistralAiApi = mistralAiApi;
|
||||
this.defaultOptions = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatResponse call(Prompt prompt) {
|
||||
return retryTemplate.execute(ctx -> {
|
||||
var request = createRequest(prompt, false);
|
||||
// return retryTemplate.execute(ctx -> {
|
||||
var request = createRequest(prompt, false);
|
||||
|
||||
var completionEntity = this.mistralAiApi.chatCompletionEntity(request);
|
||||
// var completionEntity = this.mistralAiApi.chatCompletionEntity(request);
|
||||
ResponseEntity<ChatCompletion> completionEntity = this.callWithFunctionSupport(request);
|
||||
|
||||
var chatCompletion = completionEntity.getBody();
|
||||
if (chatCompletion == null) {
|
||||
log.warn("No chat completion returned for prompt: {}", prompt);
|
||||
return new ChatResponse(List.of());
|
||||
}
|
||||
var chatCompletion = completionEntity.getBody();
|
||||
if (chatCompletion == null) {
|
||||
log.warn("No chat completion returned for prompt: {}", prompt);
|
||||
return new ChatResponse(List.of());
|
||||
}
|
||||
|
||||
List<Generation> generations = chatCompletion.choices()
|
||||
.stream()
|
||||
.map(choice -> new Generation(choice.message().content(),
|
||||
Map.of("role", choice.message().role().name()))
|
||||
.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null)))
|
||||
.toList();
|
||||
List<Generation> generations = chatCompletion.choices()
|
||||
.stream()
|
||||
.map(choice -> new Generation(choice.message().content(), Map.of("role", choice.message().role().name()))
|
||||
.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null)))
|
||||
.toList();
|
||||
|
||||
return new ChatResponse(generations);
|
||||
});
|
||||
return new ChatResponse(generations);
|
||||
// });
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,4 +158,133 @@ public class MistralAiChatClient implements ChatClient, StreamingChatClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessible for testing.
|
||||
*/
|
||||
MistralAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
|
||||
|
||||
Set<String> functionsForThisRequest = new HashSet<>();
|
||||
|
||||
var chatCompletionMessages = prompt.getInstructions()
|
||||
.stream()
|
||||
.map(m -> new MistralAiApi.ChatCompletionMessage(m.getContent(),
|
||||
MistralAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name())))
|
||||
.toList();
|
||||
|
||||
var request = new MistralAiApi.ChatCompletionRequest(chatCompletionMessages, stream);
|
||||
|
||||
if (this.defaultOptions != null) {
|
||||
Set<String> defaultEnabledFunctions = this.handleFunctionCallbackConfigurations(this.defaultOptions,
|
||||
!IS_RUNTIME_CALL);
|
||||
|
||||
functionsForThisRequest.addAll(defaultEnabledFunctions);
|
||||
|
||||
request = ModelOptionsUtils.merge(request, this.defaultOptions, MistralAiApi.ChatCompletionRequest.class);
|
||||
}
|
||||
|
||||
if (prompt.getOptions() != null) {
|
||||
if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {
|
||||
var updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ChatOptions.class,
|
||||
MistralAiChatOptions.class);
|
||||
|
||||
Set<String> promptEnabledFunctions = this.handleFunctionCallbackConfigurations(updatedRuntimeOptions,
|
||||
IS_RUNTIME_CALL);
|
||||
functionsForThisRequest.addAll(promptEnabledFunctions);
|
||||
|
||||
request = ModelOptionsUtils.merge(updatedRuntimeOptions, request,
|
||||
MistralAiApi.ChatCompletionRequest.class);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException("Prompt options are not of type ChatOptions: "
|
||||
+ prompt.getOptions().getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
// Add the enabled functions definitions to the request's tools parameter.
|
||||
if (!CollectionUtils.isEmpty(functionsForThisRequest)) {
|
||||
|
||||
if (stream) {
|
||||
throw new IllegalArgumentException("Currently tool functions are not supported in streaming mode");
|
||||
}
|
||||
|
||||
request = ModelOptionsUtils.merge(
|
||||
MistralAiChatOptions.builder().withTools(this.getFunctionTools(functionsForThisRequest)).build(),
|
||||
request, ChatCompletionRequest.class);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private List<MistralAiApi.FunctionTool> getFunctionTools(Set<String> functionNames) {
|
||||
return this.resolveFunctionCallbacks(functionNames).stream().map(functionCallback -> {
|
||||
var function = new MistralAiApi.FunctionTool.Function(functionCallback.getDescription(),
|
||||
functionCallback.getName(), functionCallback.getInputTypeSchema());
|
||||
return new MistralAiApi.FunctionTool(function);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
//
|
||||
// Function Calling Support
|
||||
//
|
||||
@Override
|
||||
protected ChatCompletionRequest doCreateToolResponseRequest(ChatCompletionRequest previousRequest,
|
||||
ChatCompletionMessage responseMessage, List<ChatCompletionMessage> conversationHistory) {
|
||||
|
||||
// Every tool-call item requires a separate function call and a response (TOOL)
|
||||
// message.
|
||||
for (ToolCall toolCall : responseMessage.toolCalls()) {
|
||||
|
||||
var functionName = toolCall.function().name();
|
||||
String functionArguments = toolCall.function().arguments();
|
||||
|
||||
if (!this.functionCallbackRegister.containsKey(functionName)) {
|
||||
throw new IllegalStateException("No function callback found for function name: " + functionName);
|
||||
}
|
||||
|
||||
String functionResponse = this.functionCallbackRegister.get(functionName).call(functionArguments);
|
||||
|
||||
// Add the function response to the conversation.
|
||||
conversationHistory
|
||||
.add(new ChatCompletionMessage(functionResponse, ChatCompletionMessage.Role.TOOL, functionName, null));
|
||||
}
|
||||
|
||||
// Recursively call chatCompletionWithTools until the model doesn't call a
|
||||
// functions anymore.
|
||||
ChatCompletionRequest newRequest = new ChatCompletionRequest(conversationHistory, previousRequest.stream());
|
||||
newRequest = ModelOptionsUtils.merge(newRequest, previousRequest, ChatCompletionRequest.class);
|
||||
|
||||
return newRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ChatCompletionMessage> doGetUserMessages(ChatCompletionRequest request) {
|
||||
return request.messages();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ChatCompletionMessage doGetToolResponseMessage(ResponseEntity<ChatCompletion> chatCompletion) {
|
||||
return chatCompletion.getBody().choices().iterator().next().message();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResponseEntity<ChatCompletion> doChatCompletion(ChatCompletionRequest request) {
|
||||
return this.mistralAiApi.chatCompletionEntity(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isToolFunctionCall(ResponseEntity<ChatCompletion> chatCompletion) {
|
||||
|
||||
var body = chatCompletion.getBody();
|
||||
if (body == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var choices = body.choices();
|
||||
if (CollectionUtils.isEmpty(choices)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !CollectionUtils.isEmpty(choices.get(0).message().toolCalls());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,10 +16,22 @@
|
||||
|
||||
package org.springframework.ai.mistralai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ResponseFormat;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.FunctionTool;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallingOptions;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Ricken Bazolo
|
||||
@@ -27,7 +39,7 @@ import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
* @since 0.8.1
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class MistralAiChatOptions implements ChatOptions {
|
||||
public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions {
|
||||
|
||||
/**
|
||||
* ID of the model to use
|
||||
@@ -66,6 +78,55 @@ public class MistralAiChatOptions implements ChatOptions {
|
||||
*/
|
||||
private @JsonProperty("random_seed") Integer randomSeed;
|
||||
|
||||
/**
|
||||
* An object specifying the format that the model must output. Setting to { "type":
|
||||
* "json_object" } enables JSON mode, which guarantees the message the model generates
|
||||
* is valid JSON.
|
||||
*/
|
||||
private @JsonProperty("response_format") ResponseFormat responseFormat;
|
||||
|
||||
/**
|
||||
* A list of tools the model may call. Currently, only functions are supported as a
|
||||
* tool. Use this to provide a list of functions the model may generate JSON inputs
|
||||
* for.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private @JsonProperty("tools") List<FunctionTool> tools;
|
||||
|
||||
/**
|
||||
* Controls which (if any) function is called by the model. none means the model will
|
||||
* not call a function and instead generates a message. auto means the model can pick
|
||||
* between generating a message or calling a function.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private @JsonProperty("tool_choice") ToolChoice toolChoice;
|
||||
|
||||
/**
|
||||
* MistralAI Tool Function Callbacks to register with the ChatClient. For Prompt
|
||||
* Options the functionCallbacks are automatically enabled for the duration of the
|
||||
* prompt execution. For Default Options the functionCallbacks are registered but
|
||||
* disabled by default. Use the enableFunctions to set the functions from the registry
|
||||
* to be used by the ChatClient chat completion requests.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
@JsonIgnore
|
||||
private List<FunctionCallback> functionCallbacks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* List of functions, identified by their names, to configure for function calling in
|
||||
* the chat completion requests. Functions with those names must exist in the
|
||||
* functionCallbacks registry. The {@link #functionCallbacks} from the PromptOptions
|
||||
* are automatically enabled for the duration of the prompt execution.
|
||||
*
|
||||
* Note that function enabled with the default options are enabled for all chat
|
||||
* completion requests. This could impact the token count and the billing. If the
|
||||
* functions is set in a prompt options, then the enabled functions are only active
|
||||
* for the duration of this prompt execution.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
@JsonIgnore
|
||||
private Set<String> functions = new HashSet<>();
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
@@ -104,6 +165,38 @@ public class MistralAiChatOptions implements ChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withResponseFormat(ResponseFormat responseFormat) {
|
||||
this.options.responseFormat = responseFormat;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withTools(List<FunctionTool> tools) {
|
||||
this.options.tools = tools;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withToolChoice(ToolChoice toolChoice) {
|
||||
this.options.toolChoice = toolChoice;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
this.options.functionCallbacks = functionCallbacks;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withFunctions(Set<String> functionNames) {
|
||||
Assert.notNull(functionNames, "Function names must not be null");
|
||||
this.options.functions = functionNames;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withFunction(String functionName) {
|
||||
Assert.hasText(functionName, "Function name must not be empty");
|
||||
this.options.functions.add(functionName);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MistralAiChatOptions build() {
|
||||
return this.options;
|
||||
}
|
||||
@@ -142,6 +235,30 @@ public class MistralAiChatOptions implements ChatOptions {
|
||||
this.randomSeed = randomSeed;
|
||||
}
|
||||
|
||||
public ResponseFormat getResponseFormat() {
|
||||
return this.responseFormat;
|
||||
}
|
||||
|
||||
public void setResponseFormat(ResponseFormat responseFormat) {
|
||||
this.responseFormat = responseFormat;
|
||||
}
|
||||
|
||||
public void setTools(List<FunctionTool> tools) {
|
||||
this.tools = tools;
|
||||
}
|
||||
|
||||
public List<FunctionTool> getTools() {
|
||||
return this.tools;
|
||||
}
|
||||
|
||||
public void setToolChoice(ToolChoice toolChoice) {
|
||||
this.toolChoice = toolChoice;
|
||||
}
|
||||
|
||||
public ToolChoice getToolChoice() {
|
||||
return this.toolChoice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Float getTemperature() {
|
||||
return this.temperature;
|
||||
@@ -174,4 +291,26 @@ public class MistralAiChatOptions implements ChatOptions {
|
||||
throw new UnsupportedOperationException("Unsupported option: 'TopK'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FunctionCallback> getFunctionCallbacks() {
|
||||
return this.functionCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
|
||||
Assert.notNull(functionCallbacks, "FunctionCallbacks must not be null");
|
||||
this.functionCallbacks = functionCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getFunctions() {
|
||||
return this.functions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFunctions(Set<String> functions) {
|
||||
Assert.notNull(functions, "Function must not be null");
|
||||
this.functions = functions;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,17 +26,19 @@ import java.util.function.Predicate;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.boot.context.properties.bind.ConstructorBinding;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
@@ -45,10 +47,15 @@ import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
/**
|
||||
* Implementation of the MistralAI Embedding API:
|
||||
* <a href="https://docs.mistral.ai/api/#operation/createEmbedding">...</a> and Chat
|
||||
* Completion API:
|
||||
* <a href="https://docs.mistral.ai/api/#operation/createChatCompletion">...</a>
|
||||
* Single-class, Java Client library for Mistral AI platform. Provides implementation for
|
||||
* the <a href="https://docs.mistral.ai/api/#operation/createEmbedding">MistralAI
|
||||
* Embedding API</a> and the
|
||||
* <a href="https://docs.mistral.ai/api/#operation/createChatCompletion">Chat
|
||||
* Completion</a> APIs.
|
||||
* <p>
|
||||
* Implements <b>Synchronous</b> and <b>Streaming</b> chat completion and supports latest
|
||||
* <b>Function Calling</b> features.
|
||||
* </p>
|
||||
*
|
||||
* @author Ricken Bazolo
|
||||
* @author Christian Tzolov
|
||||
@@ -101,12 +108,12 @@ public class MistralAiApi {
|
||||
var responseErrorHandler = new ResponseErrorHandler() {
|
||||
|
||||
@Override
|
||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
||||
public boolean hasError(@NonNull ClientHttpResponse response) throws IOException {
|
||||
return response.getStatusCode().isError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
public void handleError(@NonNull ClientHttpResponse response) throws IOException {
|
||||
if (response.getStatusCode().isError()) {
|
||||
String error = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
|
||||
String message = String.format("%s - %s", response.getStatusCode().value(), error);
|
||||
@@ -151,6 +158,65 @@ public class MistralAiApi {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a tool the model may call. Currently, only functions are supported as a
|
||||
* tool.
|
||||
*
|
||||
* @param type The type of the tool. Currently, only 'function' is supported.
|
||||
* @param function The function definition.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record FunctionTool(@JsonProperty("type") Type type, @JsonProperty("function") Function function) {
|
||||
|
||||
/**
|
||||
* Create a tool of type 'function' and the given function definition.
|
||||
* @param function function definition.
|
||||
*/
|
||||
@ConstructorBinding
|
||||
public FunctionTool(Function function) {
|
||||
this(Type.FUNCTION, function);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tool of type 'function' and the given function definition.
|
||||
*/
|
||||
public enum Type {
|
||||
|
||||
/**
|
||||
* Function tool type.
|
||||
*/
|
||||
@JsonProperty("function")
|
||||
FUNCTION
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Function definition.
|
||||
*
|
||||
* @param description A description of what the function does, used by the model
|
||||
* to choose when and how to call the function.
|
||||
* @param name The name of the function to be called. Must be a-z, A-Z, 0-9, or
|
||||
* contain underscores and dashes, with a maximum length of 64.
|
||||
* @param parameters The parameters the functions accepts, described as a JSON
|
||||
* Schema object. To describe a function that accepts no parameters, provide the
|
||||
* value {"type": "object", "properties": {}}.
|
||||
*/
|
||||
public record Function(@JsonProperty("description") String description, @JsonProperty("name") String name,
|
||||
@JsonProperty("parameters") Map<String, Object> parameters) {
|
||||
|
||||
/**
|
||||
* Create tool function definition.
|
||||
* @param description tool function description.
|
||||
* @param name tool function name.
|
||||
* @param jsonSchema tool function schema as json.
|
||||
*/
|
||||
@ConstructorBinding
|
||||
public Function(String description, String name, String jsonSchema) {
|
||||
this(description, name, ModelOptionsUtils.jsonToMap(jsonSchema));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage statistics.
|
||||
*
|
||||
@@ -163,10 +229,10 @@ public class MistralAiApi {
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record Usage(
|
||||
// @formatter:off
|
||||
@JsonProperty("prompt_tokens") Integer promptTokens,
|
||||
@JsonProperty("total_tokens") Integer totalTokens,
|
||||
@JsonProperty("completion_tokens") Integer completionTokens) {
|
||||
// @formatter:on
|
||||
@JsonProperty("prompt_tokens") Integer promptTokens,
|
||||
@JsonProperty("total_tokens") Integer totalTokens,
|
||||
@JsonProperty("completion_tokens") Integer completionTokens) {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,10 +246,10 @@ public class MistralAiApi {
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record Embedding(
|
||||
// @formatter:off
|
||||
@JsonProperty("index") Integer index,
|
||||
@JsonProperty("embedding") List<Double> embedding,
|
||||
@JsonProperty("object") String object) {
|
||||
// @formatter:on
|
||||
@JsonProperty("index") Integer index,
|
||||
@JsonProperty("embedding") List<Double> embedding,
|
||||
@JsonProperty("object") String object) {
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
* Create an embedding with the given index, embedding and object type set to
|
||||
@@ -208,10 +274,10 @@ public class MistralAiApi {
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record EmbeddingRequest<T>(
|
||||
// @formatter:off
|
||||
@JsonProperty("input") T input,
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("encoding_format") String encodingFormat) {
|
||||
// @formatter:on
|
||||
@JsonProperty("input") T input,
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("encoding_format") String encodingFormat) {
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
* Create an embedding request with the given input, model and encoding format set
|
||||
@@ -245,11 +311,11 @@ public class MistralAiApi {
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record EmbeddingList<T>(
|
||||
// @formatter:off
|
||||
@JsonProperty("object") String object,
|
||||
@JsonProperty("data") List<T> data,
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("usage") Usage usage) {
|
||||
// @formatter:on
|
||||
@JsonProperty("object") String object,
|
||||
@JsonProperty("data") List<T> data,
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("usage") Usage usage) {
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,6 +364,13 @@ public class MistralAiApi {
|
||||
* @param model ID of the model to use.
|
||||
* @param messages The prompt(s) to generate completions for, encoded as a list of
|
||||
* dict with role and content. The first prompt role should be user or system.
|
||||
* @param tools A list of tools the model may call. Currently, only functions are
|
||||
* supported as a tool. Use this to provide a list of functions the model may generate
|
||||
* JSON inputs for.
|
||||
* @param toolChoice Controls which (if any) function is called by the model. none
|
||||
* means the model will not call a function and instead generates a message. auto
|
||||
* means the model can pick between generating a message or calling a function. Any
|
||||
* means the model must call a function.
|
||||
* @param temperature What sampling temperature to use, between 0.0 and 1.0. Higher
|
||||
* values like 0.8 will make the output more random, while lower values like 0.2 will
|
||||
* make it more focused and deterministic. We generally recommend altering this or
|
||||
@@ -317,19 +390,25 @@ public class MistralAiApi {
|
||||
* @param safePrompt Whether to inject a safety prompt before all conversations.
|
||||
* @param randomSeed The seed to use for random sampling. If set, different calls will
|
||||
* generate deterministic results.
|
||||
* @param responseFormat An object specifying the format that the model must output.
|
||||
* Setting to { "type": "json_object" } enables JSON mode, which guarantees the
|
||||
* message the model generates is valid JSON.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ChatCompletionRequest(
|
||||
// @formatter:off
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("messages") List<ChatCompletionMessage> messages,
|
||||
@JsonProperty("temperature") Float temperature,
|
||||
@JsonProperty("top_p") Float topP,
|
||||
@JsonProperty("max_tokens") Integer maxTokens,
|
||||
@JsonProperty("stream") Boolean stream,
|
||||
@JsonProperty("safe_prompt") Boolean safePrompt,
|
||||
@JsonProperty("random_seed") Integer randomSeed) {
|
||||
// @formatter:on
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("messages") List<ChatCompletionMessage> messages,
|
||||
@JsonProperty("tools") List<FunctionTool> tools,
|
||||
@JsonProperty("tool_choice") ToolChoice toolChoice,
|
||||
@JsonProperty("temperature") Float temperature,
|
||||
@JsonProperty("top_p") Float topP,
|
||||
@JsonProperty("max_tokens") Integer maxTokens,
|
||||
@JsonProperty("stream") Boolean stream,
|
||||
@JsonProperty("safe_prompt") Boolean safePrompt,
|
||||
@JsonProperty("random_seed") Integer randomSeed,
|
||||
@JsonProperty("response_format") ResponseFormat responseFormat) {
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
* Shortcut constructor for a chat completion request with the given messages and
|
||||
@@ -339,7 +418,7 @@ public class MistralAiApi {
|
||||
* @param model ID of the model to use.
|
||||
*/
|
||||
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model) {
|
||||
this(model, messages, 0.7f, 1f, null, false, false, null);
|
||||
this(model, messages, null, null, 0.7f, 1f, null, false, false, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,7 +433,7 @@ public class MistralAiApi {
|
||||
*/
|
||||
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, Float temperature,
|
||||
boolean stream) {
|
||||
this(model, messages, temperature, 1f, null, stream, false, null);
|
||||
this(model, messages, null, null, temperature, 1f, null, stream, false, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,7 +446,22 @@ public class MistralAiApi {
|
||||
*
|
||||
*/
|
||||
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, Float temperature) {
|
||||
this(model, messages, temperature, 1f, null, false, false, null);
|
||||
this(model, messages, null, null, temperature, 1f, null, false, false, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut constructor for a chat completion request with the given messages,
|
||||
* model, tools and tool choice. Streaming is set to false, temperature to 0.8 and
|
||||
* all other parameters are null.
|
||||
* @param messages A list of messages comprising the conversation so far.
|
||||
* @param model ID of the model to use.
|
||||
* @param tools A list of tools the model may call. Currently, only functions are
|
||||
* supported as a tool.
|
||||
* @param toolChoice Controls which (if any) function is called by the model.
|
||||
*/
|
||||
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, List<FunctionTool> tools,
|
||||
ToolChoice toolChoice) {
|
||||
this(model, messages, tools, toolChoice, null, 1f, null, false, false, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -375,7 +469,31 @@ public class MistralAiApi {
|
||||
* stream.
|
||||
*/
|
||||
public ChatCompletionRequest(List<ChatCompletionMessage> messages, Boolean stream) {
|
||||
this(null, messages, 0.7f, 1f, null, stream, false, null);
|
||||
this(null, messages, null, null, 0.7f, 1f, null, stream, false, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a tool the model should use. Use to force the model to call a
|
||||
* specific function.
|
||||
*
|
||||
*/
|
||||
public enum ToolChoice {
|
||||
|
||||
// @formatter:off
|
||||
@JsonProperty("auto") AUTO,
|
||||
@JsonProperty("any") ANY,
|
||||
@JsonProperty("none") NONE
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An object specifying the format that the model must output.
|
||||
*
|
||||
* @param type Must be one of 'text' or 'json_object'.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ResponseFormat(@JsonProperty("type") String type) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,13 +503,27 @@ public class MistralAiApi {
|
||||
* @param content The contents of the message.
|
||||
* @param role The role of the messages author. Could be one of the {@link Role}
|
||||
* types.
|
||||
* @param toolCalls The tool calls generated by the model, such as function calls.
|
||||
* Applicable only for {@link Role#ASSISTANT} role and null otherwise.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ChatCompletionMessage(
|
||||
// @formatter:off
|
||||
@JsonProperty("content") String content,
|
||||
@JsonProperty("role") Role role) {
|
||||
// @formatter:on
|
||||
@JsonProperty("content") String content,
|
||||
@JsonProperty("role") Role role,
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("tool_calls") List<ToolCall> toolCalls) {
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
* Create a chat completion message with the given content and role. All other
|
||||
* fields are null.
|
||||
* @param content The contents of the message.
|
||||
* @param role The role of the author of this message.
|
||||
*/
|
||||
public ChatCompletionMessage(String content, Role role) {
|
||||
this(content, role, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The role of the author of this message.
|
||||
@@ -402,12 +534,39 @@ public class MistralAiApi {
|
||||
public enum Role {
|
||||
|
||||
// @formatter:off
|
||||
@JsonProperty("system") SYSTEM,
|
||||
@JsonProperty("user") USER,
|
||||
@JsonProperty("assistant") ASSISTANT
|
||||
// @formatter:on
|
||||
@JsonProperty("system") SYSTEM,
|
||||
@JsonProperty("user") USER,
|
||||
@JsonProperty("assistant") ASSISTANT,
|
||||
@JsonProperty("tool") TOOL
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The relevant tool call.
|
||||
*
|
||||
* @param id The ID of the tool call. This ID must be referenced when you submit
|
||||
* the tool outputs in using the Submit tool outputs to run endpoint.
|
||||
* @param type The type of tool call the output is required for. For now, this is
|
||||
* always function.
|
||||
* @param function The function definition.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ToolCall(@JsonProperty("id") String id, @JsonProperty("type") String type,
|
||||
@JsonProperty("function") ChatCompletionFunction function) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The function definition.
|
||||
*
|
||||
* @param name The name of the function.
|
||||
* @param arguments The arguments that the model expects you to pass to the
|
||||
* function.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ChatCompletionFunction(@JsonProperty("name") String name,
|
||||
@JsonProperty("arguments") String arguments) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -416,19 +575,19 @@ public class MistralAiApi {
|
||||
public enum ChatCompletionFinishReason {
|
||||
|
||||
// @formatter:off
|
||||
/**
|
||||
* The model hit a natural stop point or a provided stop sequence.
|
||||
*/
|
||||
@JsonProperty("stop") STOP,
|
||||
/**
|
||||
* The maximum number of tokens specified in the request was reached.
|
||||
*/
|
||||
@JsonProperty("length") LENGTH,
|
||||
/**
|
||||
* The content was omitted due to a flag from our content filters.
|
||||
*/
|
||||
@JsonProperty("model_length") MODEL_LENGTH
|
||||
// @formatter:on
|
||||
/**
|
||||
* The model hit a natural stop point or a provided stop sequence.
|
||||
*/
|
||||
@JsonProperty("stop") STOP,
|
||||
/**
|
||||
* The maximum number of tokens specified in the request was reached.
|
||||
*/
|
||||
@JsonProperty("length") LENGTH,
|
||||
/**
|
||||
* The content was omitted due to a flag from our content filters.
|
||||
*/
|
||||
@JsonProperty("model_length") MODEL_LENGTH
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
@@ -447,13 +606,13 @@ public class MistralAiApi {
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ChatCompletion(
|
||||
// @formatter:off
|
||||
@JsonProperty("id") String id,
|
||||
@JsonProperty("object") String object,
|
||||
@JsonProperty("created") Long created,
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("choices") List<Choice> choices,
|
||||
@JsonProperty("usage") Usage usage) {
|
||||
// @formatter:on
|
||||
@JsonProperty("id") String id,
|
||||
@JsonProperty("object") String object,
|
||||
@JsonProperty("created") Long created,
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("choices") List<Choice> choices,
|
||||
@JsonProperty("usage") Usage usage) {
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
* Chat completion choice.
|
||||
@@ -465,10 +624,10 @@ public class MistralAiApi {
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record Choice(
|
||||
// @formatter:off
|
||||
@JsonProperty("index") Integer index,
|
||||
@JsonProperty("message") ChatCompletionMessage message,
|
||||
@JsonProperty("finish_reason") ChatCompletionFinishReason finishReason) {
|
||||
// @formatter:on
|
||||
@JsonProperty("index") Integer index,
|
||||
@JsonProperty("message") ChatCompletionMessage message,
|
||||
@JsonProperty("finish_reason") ChatCompletionFinishReason finishReason) {
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,12 +646,12 @@ public class MistralAiApi {
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ChatCompletionChunk(
|
||||
// @formatter:off
|
||||
@JsonProperty("id") String id,
|
||||
@JsonProperty("object") String object,
|
||||
@JsonProperty("created") Long created,
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("choices") List<ChunkChoice> choices) {
|
||||
// @formatter:on
|
||||
@JsonProperty("id") String id,
|
||||
@JsonProperty("object") String object,
|
||||
@JsonProperty("created") Long created,
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("choices") List<ChunkChoice> choices) {
|
||||
// @formatter:on
|
||||
|
||||
/**
|
||||
* Chat completion choice.
|
||||
@@ -504,26 +663,38 @@ public class MistralAiApi {
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ChunkChoice(
|
||||
// @formatter:off
|
||||
@JsonProperty("index") Integer index,
|
||||
@JsonProperty("delta") ChatCompletionMessage delta,
|
||||
@JsonProperty("finish_reason") ChatCompletionFinishReason finishReason) {
|
||||
// @formatter:on
|
||||
@JsonProperty("index") Integer index,
|
||||
@JsonProperty("delta") ChatCompletionMessage delta,
|
||||
@JsonProperty("finish_reason") ChatCompletionFinishReason finishReason) {
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List of well-known Mistral chat models.
|
||||
* https://docs.mistral.ai/platform/endpoints/#mistral-ai-generative-models
|
||||
*
|
||||
* <p>
|
||||
* Mistral AI provides five API endpoints featuring five leading Large Language
|
||||
* Models:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>TINY</b> - open-mistral-7b (aka mistral-tiny-2312)</li>
|
||||
* <li><b>MIXTRAL</b> - open-mixtral-8x7b (aka mistral-small-2312)</li>
|
||||
* <li><b>SMALL_LATEST</b> - mistral-small-latest (aka mistral-small-2402)</li>
|
||||
* <li><b>MEDIUM</b> - mistral-medium-latest (aka mistral-medium-2312)</li>
|
||||
* <li><b>LARGE</b> - mistral-large-latest (aka mistral-large-2402)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum ChatModel {
|
||||
|
||||
// @formatter:off
|
||||
@JsonProperty("mistral-tiny") TINY("mistral-tiny"),
|
||||
@JsonProperty("mistral-small") SMALL("mistral-small"),
|
||||
@JsonProperty("mistral-medium") MEDIUM("mistral-medium"),
|
||||
@JsonProperty("mistral-large") LARGE("mistral-large"),
|
||||
@JsonProperty("mistral-xlarge") XLARGE("mistral-xlarge");
|
||||
// @formatter:on
|
||||
TINY("open-mistral-7b"),
|
||||
MIXTRAL("open-mixtral-8x7b"),
|
||||
SMALL("mistral-small-latest"),
|
||||
MEDIUM("mistral-medium-latest"),
|
||||
LARGE("mistral-large-latest");
|
||||
// @formatter:on
|
||||
|
||||
private final String value;
|
||||
|
||||
@@ -544,8 +715,8 @@ public class MistralAiApi {
|
||||
public enum EmbeddingModel {
|
||||
|
||||
// @formatter:off
|
||||
@JsonProperty("mistral-embed") EMBED("mistral-embed");
|
||||
// @formatter:on
|
||||
@JsonProperty("mistral-embed") EMBED("mistral-embed");
|
||||
// @formatter:on
|
||||
|
||||
private final String value;
|
||||
|
||||
@@ -595,26 +766,7 @@ public class MistralAiApi {
|
||||
.bodyToFlux(String.class)
|
||||
.takeUntil(SSE_DONE_PREDICATE)
|
||||
.filter(SSE_DONE_PREDICATE.negate())
|
||||
.map(content -> parseJson(content, ChatCompletionChunk.class));
|
||||
}
|
||||
|
||||
public static Map<String, Object> parseJson(String jsonSchema) {
|
||||
try {
|
||||
return new ObjectMapper().readValue(jsonSchema, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new MistralAiApiException("Failed to parse schema: " + jsonSchema, e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T parseJson(String json, Class<T> type) {
|
||||
try {
|
||||
return this.objectMapper.readValue(json, type);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new MistralAiApiException("Failed to parse schema: " + json, e);
|
||||
}
|
||||
.map(content -> ModelOptionsUtils.jsonToObject(content, ChatCompletionChunk.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public class MistralAiTestConfiguration {
|
||||
@Bean
|
||||
public MistralAiChatClient mistralAiChatClient(MistralAiApi mistralAiApi) {
|
||||
return new MistralAiChatClient(mistralAiApi,
|
||||
MistralAiChatOptions.builder().withModel(MistralAiApi.ChatModel.SMALL.getValue()).build());
|
||||
MistralAiChatOptions.builder().withModel(MistralAiApi.ChatModel.MIXTRAL.getValue()).build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.mistralai.MistralAiChatClient;
|
||||
import org.springframework.ai.mistralai.MistralAiChatOptions;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
|
||||
@@ -15,17 +15,15 @@
|
||||
*/
|
||||
package org.springframework.ai.mistralai;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
|
||||
import org.springframework.ai.embedding.EmbeddingRequest;
|
||||
import org.springframework.ai.mistralai.MistralAiEmbeddingClient;
|
||||
import org.springframework.ai.mistralai.MistralAiEmbeddingOptions;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2024-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.mistralai.api.tool;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletion;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage.Role;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage.ToolCall;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.FunctionTool.Type;
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".+")
|
||||
@Disabled
|
||||
public class MistralAiApiToolFunctionCallIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MistralAiApiToolFunctionCallIT.class);
|
||||
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
|
||||
static final String MISTRAL_AI_CHAT_MODEL = MistralAiApi.ChatModel.LARGE.getValue();
|
||||
|
||||
MistralAiApi completionApi = new MistralAiApi(System.getenv("MISTRAL_AI_API_KEY"));
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("null")
|
||||
public void toolFunctionCall() throws JsonProcessingException {
|
||||
|
||||
// Step 1: send the conversation and available functions to the model
|
||||
var message = new ChatCompletionMessage(
|
||||
"What's the weather like in San Francisco, Tokyo, and Paris? Show the temperature in Celsius.",
|
||||
Role.USER);
|
||||
|
||||
var functionTool = new MistralAiApi.FunctionTool(Type.FUNCTION,
|
||||
new MistralAiApi.FunctionTool.Function(
|
||||
"Get the weather in location. Return temperature in 30°F or 30°C format.", "getCurrentWeather",
|
||||
ModelOptionsUtils.jsonToMap("""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state e.g. San Francisco, CA"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": ["C", "F"]
|
||||
}
|
||||
},
|
||||
"required": ["location", "unit"]
|
||||
}
|
||||
""")));
|
||||
|
||||
// Or you can use the
|
||||
// ModelOptionsUtils.getJsonSchema(FakeWeatherService.Request.class))) to
|
||||
// auto-generate the JSON schema like:
|
||||
// var functionTool = new MistralAiApi.FunctionTool(Type.FUNCTION, new
|
||||
// MistralAiApi.FunctionTool.Function(
|
||||
// "Get the weather in location. Return temperature in 30°F or 30°C format.",
|
||||
// "getCurrentWeather",
|
||||
// ModelOptionsUtils.getJsonSchema(MockWeatherService.Request.class)));
|
||||
|
||||
List<ChatCompletionMessage> messages = new ArrayList<>(List.of(message));
|
||||
|
||||
ChatCompletionRequest chatCompletionRequest = new ChatCompletionRequest(messages, MISTRAL_AI_CHAT_MODEL,
|
||||
List.of(functionTool), ToolChoice.AUTO);
|
||||
|
||||
System.out
|
||||
.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(chatCompletionRequest));
|
||||
|
||||
ResponseEntity<ChatCompletion> chatCompletion = completionApi.chatCompletionEntity(chatCompletionRequest);
|
||||
|
||||
assertThat(chatCompletion.getBody()).isNotNull();
|
||||
assertThat(chatCompletion.getBody().choices()).isNotEmpty();
|
||||
|
||||
ChatCompletionMessage responseMessage = chatCompletion.getBody().choices().get(0).message();
|
||||
|
||||
assertThat(responseMessage.role()).isEqualTo(Role.ASSISTANT);
|
||||
assertThat(responseMessage.toolCalls()).isNotNull();
|
||||
|
||||
// Check if the model wanted to call a function
|
||||
if (responseMessage.toolCalls() != null) {
|
||||
|
||||
// extend conversation with assistant's reply.
|
||||
messages.add(responseMessage);
|
||||
|
||||
// Send the info for each function call and function response to the model.
|
||||
for (ToolCall toolCall : responseMessage.toolCalls()) {
|
||||
var functionName = toolCall.function().name();
|
||||
if ("getCurrentWeather".equals(functionName)) {
|
||||
MockWeatherService.Request weatherRequest = fromJson(toolCall.function().arguments(),
|
||||
MockWeatherService.Request.class);
|
||||
|
||||
MockWeatherService.Response weatherResponse = weatherService.apply(weatherRequest);
|
||||
|
||||
// extend conversation with function response.
|
||||
messages.add(new ChatCompletionMessage("" + weatherResponse.temp() + weatherRequest.unit(),
|
||||
Role.TOOL, functionName, null));
|
||||
}
|
||||
}
|
||||
|
||||
var functionResponseRequest = new ChatCompletionRequest(messages, MISTRAL_AI_CHAT_MODEL, 0.8f);
|
||||
|
||||
ResponseEntity<ChatCompletion> chatCompletion2 = completionApi
|
||||
.chatCompletionEntity(functionResponseRequest);
|
||||
|
||||
logger.info("Final response: " + chatCompletion2.getBody());
|
||||
|
||||
assertThat(chatCompletion2.getBody().choices()).isNotEmpty();
|
||||
|
||||
assertThat(chatCompletion2.getBody().choices().get(0).message().role()).isEqualTo(Role.ASSISTANT);
|
||||
assertThat(chatCompletion2.getBody().choices().get(0).message().content()).contains("San Francisco")
|
||||
.containsAnyOf("30.0°C", "30°C");
|
||||
assertThat(chatCompletion2.getBody().choices().get(0).message().content()).contains("Tokyo")
|
||||
.containsAnyOf("10.0°C", "10°C");
|
||||
;
|
||||
assertThat(chatCompletion2.getBody().choices().get(0).message().content()).contains("Paris")
|
||||
.containsAnyOf("15.0°C", "15°C");
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static <T> T fromJson(String json, Class<T> targetClass) {
|
||||
try {
|
||||
return new ObjectMapper().readValue(json, targetClass);
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2024-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.mistralai.api.tool;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonClassDescription;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {
|
||||
|
||||
/**
|
||||
* Weather Function request.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
@JsonClassDescription("Weather API request")
|
||||
public record Request(@JsonProperty(required = true,
|
||||
value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
|
||||
@JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Temperature units.
|
||||
*/
|
||||
public enum Unit {
|
||||
|
||||
/**
|
||||
* Celsius.
|
||||
*/
|
||||
C("metric"),
|
||||
/**
|
||||
* Fahrenheit.
|
||||
*/
|
||||
F("imperial");
|
||||
|
||||
/**
|
||||
* Human readable unit name.
|
||||
*/
|
||||
public final String unitName;
|
||||
|
||||
private Unit(String text) {
|
||||
this.unitName = text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Weather Function response.
|
||||
*/
|
||||
public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
|
||||
Unit unit) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response apply(Request request) {
|
||||
|
||||
double temperature = 0;
|
||||
if (request.location().contains("Paris")) {
|
||||
temperature = 15;
|
||||
}
|
||||
else if (request.location().contains("Tokyo")) {
|
||||
temperature = 10;
|
||||
}
|
||||
else if (request.location().contains("San Francisco")) {
|
||||
temperature = 30;
|
||||
}
|
||||
|
||||
return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2024-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.mistralai.api.tool;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletion;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage.Role;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionMessage.ToolCall;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.FunctionTool;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.FunctionTool.Type;
|
||||
// import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Demonstrates how to use function calling suing Mistral AI Java API:
|
||||
* {@link MistralAiApi}.
|
||||
*
|
||||
* It is based on the <a href="https://docs.mistral.ai/guides/function-calling/">Mistral
|
||||
* AI Function Calling</a> guide.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 0.8.1
|
||||
*/
|
||||
@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".+")
|
||||
public class PaymentStatusFunctionCallingIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PaymentStatusFunctionCallingIT.class);
|
||||
|
||||
// Assuming we have the following data
|
||||
public static final Map<String, StatusDate> DATA = Map.of("T1001", new StatusDate("Paid", "2021-10-05"), "T1002",
|
||||
new StatusDate("Unpaid", "2021-10-06"), "T1003", new StatusDate("Paid", "2021-10-07"), "T1004",
|
||||
new StatusDate("Paid", "2021-10-05"), "T1005", new StatusDate("Pending", "2021-10-08"));
|
||||
|
||||
record StatusDate(String status, String date) {
|
||||
}
|
||||
|
||||
public record Transaction(@JsonProperty(required = true, value = "transaction_id") String transactionId) {
|
||||
}
|
||||
|
||||
public record Status(@JsonProperty(required = true, value = "status") String status) {
|
||||
}
|
||||
|
||||
public record Date(@JsonProperty(required = true, value = "date") String date) {
|
||||
}
|
||||
|
||||
private static class RetrievePaymentStatus implements Function<Transaction, Status> {
|
||||
|
||||
@Override
|
||||
public Status apply(Transaction paymentTransaction) {
|
||||
return new Status(DATA.get(paymentTransaction.transactionId).status);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class RetrievePaymentDate implements Function<Transaction, Date> {
|
||||
|
||||
@Override
|
||||
public Date apply(Transaction paymentTransaction) {
|
||||
return new Date(DATA.get(paymentTransaction.transactionId).date);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static Map<String, Function<Transaction, ?>> functions = Map.of("retrieve_payment_status",
|
||||
new RetrievePaymentStatus(), "retrieve_payment_date", new RetrievePaymentDate());
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("null")
|
||||
public void toolFunctionCall() throws JsonProcessingException {
|
||||
|
||||
var transactionJsonSchema = """
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"transaction_id": {
|
||||
"type": "string",
|
||||
"description": "The transaction id"
|
||||
}
|
||||
},
|
||||
"required": ["transaction_id"]
|
||||
}
|
||||
""";
|
||||
|
||||
// Alternatively, generate the JSON schema using the ModelOptionsUtils helper:
|
||||
//
|
||||
// var transactionJsonSchema = ModelOptionsUtils.getJsonSchema(Transaction.class,
|
||||
// false);
|
||||
|
||||
var paymentStatusTool = new FunctionTool(Type.FUNCTION, new FunctionTool.Function(
|
||||
"Get payment status of a transaction", "retrieve_payment_status", transactionJsonSchema));
|
||||
|
||||
var paymentDateTool = new FunctionTool(Type.FUNCTION, new FunctionTool.Function(
|
||||
"Get payment date of a transaction", "retrieve_payment_date", transactionJsonSchema));
|
||||
|
||||
List<ChatCompletionMessage> messages = new ArrayList<>(
|
||||
List.of(new ChatCompletionMessage("What's the status of my transaction with id T1001?", Role.USER)));
|
||||
|
||||
MistralAiApi mistralApi = new MistralAiApi(System.getenv("MISTRAL_AI_API_KEY"));
|
||||
|
||||
ResponseEntity<ChatCompletion> response = mistralApi.chatCompletionEntity(new ChatCompletionRequest(messages,
|
||||
MistralAiApi.ChatModel.LARGE.getValue(), List.of(paymentStatusTool, paymentDateTool), ToolChoice.AUTO));
|
||||
|
||||
ChatCompletionMessage responseMessage = response.getBody().choices().get(0).message();
|
||||
|
||||
assertThat(responseMessage.role()).isEqualTo(Role.ASSISTANT);
|
||||
assertThat(responseMessage.toolCalls()).isNotNull();
|
||||
|
||||
// extend conversation with assistant's reply.
|
||||
messages.add(responseMessage);
|
||||
|
||||
// Send the info for each function call and function response to the model.
|
||||
for (ToolCall toolCall : responseMessage.toolCalls()) {
|
||||
|
||||
var functionName = toolCall.function().name();
|
||||
// Map the function, JSON arguments into a Transaction object.
|
||||
Transaction transaction = jsonToObject(toolCall.function().arguments(), Transaction.class);
|
||||
// Call the target function with the transaction object.
|
||||
var result = functions.get(functionName).apply(transaction);
|
||||
|
||||
// Extend conversation with function response.
|
||||
// The functionName is used to identify the function response!
|
||||
messages.add(new ChatCompletionMessage(result.toString(), Role.TOOL, functionName, null));
|
||||
}
|
||||
|
||||
response = mistralApi
|
||||
.chatCompletionEntity(new ChatCompletionRequest(messages, MistralAiApi.ChatModel.LARGE.getValue()));
|
||||
|
||||
var responseContent = response.getBody().choices().get(0).message().content();
|
||||
logger.info("Final response: " + responseContent);
|
||||
|
||||
assertThat(responseContent).containsIgnoringCase("T1001");
|
||||
assertThat(responseContent).containsIgnoringCase("Paid");
|
||||
}
|
||||
|
||||
private static <T> T jsonToObject(String json, Class<T> targetClass) {
|
||||
try {
|
||||
return new ObjectMapper().readValue(json, targetClass);
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -277,7 +277,8 @@ public class OpenAiChatClient extends
|
||||
String functionResponse = this.functionCallbackRegister.get(functionName).call(functionArguments);
|
||||
|
||||
// Add the function response to the conversation.
|
||||
conversationHistory.add(new ChatCompletionMessage(functionResponse, Role.TOOL, null, toolCall.id(), null));
|
||||
conversationHistory
|
||||
.add(new ChatCompletionMessage(functionResponse, Role.TOOL, functionName, toolCall.id(), null));
|
||||
}
|
||||
|
||||
// Recursively call chatCompletionWithTools until the model doesn't call a
|
||||
@@ -291,7 +292,6 @@ public class OpenAiChatClient extends
|
||||
@Override
|
||||
protected List<ChatCompletionMessage> doGetUserMessages(ChatCompletionRequest request) {
|
||||
return request.messages();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -316,7 +316,7 @@ public class OpenAiChatClient extends
|
||||
return false;
|
||||
}
|
||||
|
||||
return choices.get(0).message().toolCalls() != null;
|
||||
return !CollectionUtils.isEmpty(choices.get(0).message().toolCalls());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallingOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ResponseFormat;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ToolChoice;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.FunctionTool;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -52,7 +51,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
|
||||
* Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing
|
||||
* frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
|
||||
*/
|
||||
private @JsonProperty("frequency_penalty") Float frequencyPenalty = 0.0f;
|
||||
private @JsonProperty("frequency_penalty") Float frequencyPenalty;
|
||||
/**
|
||||
* Modify the likelihood of specified tokens appearing in the completion. Accepts a JSON object
|
||||
* that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100.
|
||||
@@ -70,7 +69,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
|
||||
* How many chat completion choices to generate for each input message. Note that you will be charged based
|
||||
* on the number of generated tokens across all of the choices. Keep n as 1 to minimize costs.
|
||||
*/
|
||||
private @JsonProperty("n") Integer n = 1;
|
||||
private @JsonProperty("n") Integer n;
|
||||
/**
|
||||
* Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they
|
||||
* appear in the text so far, increasing the model's likelihood to talk about new topics.
|
||||
@@ -98,7 +97,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
|
||||
* more random, while lower values like 0.2 will make it more focused and deterministic. We generally recommend
|
||||
* altering this or top_p but not both.
|
||||
*/
|
||||
private @JsonProperty("temperature") Float temperature = 0.8f;
|
||||
private @JsonProperty("temperature") Float temperature;
|
||||
/**
|
||||
* An alternative to sampling with temperature, called nucleus sampling, where the model considers the
|
||||
* results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10%
|
||||
@@ -116,10 +115,9 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
|
||||
* function and instead generates a message. auto means the model can pick between generating a message or calling a
|
||||
* function. Specifying a particular function via {"type: "function", "function": {"name": "my_function"}} forces
|
||||
* the model to call that function. none is the default when no functions are present. auto is the default if
|
||||
* functions are present.
|
||||
* functions are present. Use the {@link ToolChoiceBuilder} to create a tool choice object.
|
||||
*/
|
||||
@NestedConfigurationProperty
|
||||
private @JsonProperty("tool_choice") ToolChoice toolChoice;
|
||||
private @JsonProperty("tool_choice") String toolChoice;
|
||||
/**
|
||||
* A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
|
||||
*/
|
||||
@@ -225,7 +223,7 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withToolChoice(ToolChoice toolChoice) {
|
||||
public Builder withToolChoice(String toolChoice) {
|
||||
this.options.toolChoice = toolChoice;
|
||||
return this;
|
||||
}
|
||||
@@ -358,11 +356,11 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
|
||||
this.tools = tools;
|
||||
}
|
||||
|
||||
public ToolChoice getToolChoice() {
|
||||
public String getToolChoice() {
|
||||
return this.toolChoice;
|
||||
}
|
||||
|
||||
public void setToolChoice(ToolChoice toolChoice) {
|
||||
public void setToolChoice(String toolChoice) {
|
||||
this.toolChoice = toolChoice;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.ai.openai.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
@@ -25,20 +26,20 @@ import java.util.function.Predicate;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.boot.context.properties.bind.ConstructorBinding;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
@@ -59,7 +60,6 @@ public class OpenAiApi {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final WebClient webClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* Create an new chat completion api with base URL set to https://api.openai.com
|
||||
@@ -89,8 +89,6 @@ public class OpenAiApi {
|
||||
*/
|
||||
public OpenAiApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder) {
|
||||
|
||||
this.objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
Consumer<HttpHeaders> jsonContentHeaders = headers -> {
|
||||
headers.setBearerAuth(openAiToken);
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
@@ -99,19 +97,19 @@ public class OpenAiApi {
|
||||
var responseErrorHandler = new ResponseErrorHandler() {
|
||||
|
||||
@Override
|
||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
||||
public boolean hasError(@NonNull ClientHttpResponse response) throws IOException {
|
||||
return response.getStatusCode().isError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
public void handleError(@NonNull ClientHttpResponse response) throws IOException {
|
||||
if (response.getStatusCode().isError()) {
|
||||
String error = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
|
||||
String message = String.format("%s - %s", response.getStatusCode().value(), error);
|
||||
if (response.getStatusCode().is4xxClientError()) {
|
||||
throw new OpenAiApiClientErrorException(String.format("%s - %s", response.getStatusCode().value(),
|
||||
OpenAiApi.this.objectMapper.readValue(response.getBody(), ResponseError.class)));
|
||||
throw new OpenAiApiClientErrorException(message);
|
||||
}
|
||||
throw new OpenAiApiException(String.format("%s - %s", response.getStatusCode().value(),
|
||||
OpenAiApi.this.objectMapper.readValue(response.getBody(), ResponseError.class)));
|
||||
throw new OpenAiApiException(message);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -129,6 +127,9 @@ public class OpenAiApi {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Non HTTP Error related exceptions
|
||||
*/
|
||||
public static class OpenAiApiException extends RuntimeException {
|
||||
|
||||
public OpenAiApiException(String message) {
|
||||
@@ -157,29 +158,6 @@ public class OpenAiApi {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API error response.
|
||||
* @param error Error details.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ResponseError(@JsonProperty("error") Error error) {
|
||||
|
||||
/**
|
||||
* Error details.
|
||||
* @param message Error message.
|
||||
* @param type Error type.
|
||||
* @param param Error parameter.
|
||||
* @param code Error code.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record Error(
|
||||
@JsonProperty("message") String message,
|
||||
@JsonProperty("type") String type,
|
||||
@JsonProperty("param") String param,
|
||||
@JsonProperty("code") String code) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a tool the model may call. Currently, only functions are supported as a tool.
|
||||
*
|
||||
@@ -234,7 +212,7 @@ public class OpenAiApi {
|
||||
*/
|
||||
@ConstructorBinding
|
||||
public Function(String description, String name, String jsonSchema) {
|
||||
this(description, name, parseJson(jsonSchema));
|
||||
this(description, name, ModelOptionsUtils.jsonToMap(jsonSchema));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,7 +256,7 @@ public class OpenAiApi {
|
||||
* function and instead generates a message. auto means the model can pick between generating a message or calling a
|
||||
* function. Specifying a particular function via {"type: "function", "function": {"name": "my_function"}} forces
|
||||
* the model to call that function. none is the default when no functions are present. auto is the default if
|
||||
* functions are present.
|
||||
* functions are present. Use the {@link ToolChoiceBuilder} to create the tool choice value.
|
||||
* @param user A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.
|
||||
*
|
||||
*/
|
||||
@@ -298,7 +276,7 @@ public class OpenAiApi {
|
||||
@JsonProperty("temperature") Float temperature,
|
||||
@JsonProperty("top_p") Float topP,
|
||||
@JsonProperty("tools") List<FunctionTool> tools,
|
||||
@JsonProperty("tool_choice") ToolChoice toolChoice,
|
||||
@JsonProperty("tool_choice") String toolChoice,
|
||||
@JsonProperty("user") String user) {
|
||||
|
||||
/**
|
||||
@@ -309,7 +287,7 @@ public class OpenAiApi {
|
||||
* @param temperature What sampling temperature to use, between 0 and 1.
|
||||
*/
|
||||
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, Float temperature) {
|
||||
this(messages, model, 0.0f, null, null, 1, 0.0f,
|
||||
this(messages, model, null, null, null, null, null,
|
||||
null, null, null, false, temperature, null,
|
||||
null, null, null);
|
||||
}
|
||||
@@ -324,7 +302,7 @@ public class OpenAiApi {
|
||||
* as they become available, with the stream terminated by a data: [DONE] message.
|
||||
*/
|
||||
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model, Float temperature, boolean stream) {
|
||||
this(messages, model, 0.0f, null, null, 1, 0.0f,
|
||||
this(messages, model, null, null, null, null, null,
|
||||
null, null, null, stream, temperature, null,
|
||||
null, null, null);
|
||||
}
|
||||
@@ -339,8 +317,8 @@ public class OpenAiApi {
|
||||
* @param toolChoice Controls which (if any) function is called by the model.
|
||||
*/
|
||||
public ChatCompletionRequest(List<ChatCompletionMessage> messages, String model,
|
||||
List<FunctionTool> tools, ToolChoice toolChoice) {
|
||||
this(messages, model, 0.0f, null, null, 1, 0.0f,
|
||||
List<FunctionTool> tools, String toolChoice) {
|
||||
this(messages, model, null, null, null, null, null,
|
||||
null, null, null, false, 0.8f, null,
|
||||
tools, toolChoice, null);
|
||||
}
|
||||
@@ -360,23 +338,23 @@ public class OpenAiApi {
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a tool the model should use. Use to force the model to call a specific function.
|
||||
*
|
||||
* @param type The type of the tool. Currently, only 'function' is supported.
|
||||
* @param function single field map for type 'name':'your function name'.
|
||||
* Helper factory that creates a tool_choice of type 'none', 'auto' or selected function by name.
|
||||
*/
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record ToolChoice(
|
||||
@JsonProperty("type") String type,
|
||||
@JsonProperty("function") Map<String, String> function) {
|
||||
public static class ToolChoiceBuilder {
|
||||
/**
|
||||
* Model can pick between generating a message or calling a function.
|
||||
*/
|
||||
public static final String AUTO = "none";
|
||||
/**
|
||||
* Model will not call a function and instead generates a message
|
||||
*/
|
||||
public static final String NONE = "none";
|
||||
|
||||
/**
|
||||
* Create a tool choice of type 'function' and name 'functionName'.
|
||||
* @param functionName Function name of the tool.
|
||||
* Specifying a particular function forces the model to call that function.
|
||||
*/
|
||||
@ConstructorBinding
|
||||
public ToolChoice(String functionName) {
|
||||
this("function", Map.of("name", functionName));
|
||||
public static String FUNCTION(String functionName) {
|
||||
return ModelOptionsUtils.toJsonString(Map.of("type", "function", "function", Map.of("name", functionName)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,7 +654,7 @@ public class OpenAiApi {
|
||||
.takeUntil(SSE_DONE_PREDICATE)
|
||||
// filters out the "[DONE]" message.
|
||||
.filter(SSE_DONE_PREDICATE.negate())
|
||||
.map(content -> parseJson(content, ChatCompletionChunk.class));
|
||||
.map(content -> ModelOptionsUtils.jsonToObject(content, ChatCompletionChunk.class));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -795,25 +773,5 @@ public class OpenAiApi {
|
||||
.toEntity(new ParameterizedTypeReference<>() {
|
||||
});
|
||||
}
|
||||
|
||||
public static Map<String, Object> parseJson(String jsonSchema) {
|
||||
try {
|
||||
return new ObjectMapper().readValue(jsonSchema,
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new OpenAiApiException("Failed to parse schema: " + jsonSchema, e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T parseJson(String json, Class<T> type) {
|
||||
try {
|
||||
return this.objectMapper.readValue(json, type);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new OpenAiApiException("Failed to parse schema: " + json, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.ai.openai.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -26,12 +27,12 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.ai.openai.api.OpenAiApi.OpenAiApiClientErrorException;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.OpenAiApiException;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ResponseError;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@@ -78,13 +79,12 @@ public class OpenAiImageApi {
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
if (response.getStatusCode().isError()) {
|
||||
String error = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
|
||||
String message = String.format("%s - %s", response.getStatusCode().value(), error);
|
||||
if (response.getStatusCode().is4xxClientError()) {
|
||||
throw new OpenAiApiClientErrorException(String.format("%s - %s",
|
||||
response.getStatusCode().value(),
|
||||
OpenAiImageApi.this.objectMapper.readValue(response.getBody(), ResponseError.class)));
|
||||
throw new OpenAiApiClientErrorException(message);
|
||||
}
|
||||
throw new OpenAiApiException(String.format("%s - %s", response.getStatusCode().value(),
|
||||
OpenAiImageApi.this.objectMapper.readValue(response.getBody(), ResponseError.class)));
|
||||
throw new OpenAiApiException(message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.model.ModelOptionsUtils;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletion;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionMessage;
|
||||
@@ -63,7 +64,7 @@ public class OpenAiApiToolFunctionCallIT {
|
||||
var functionTool = new OpenAiApi.FunctionTool(Type.FUNCTION,
|
||||
new OpenAiApi.FunctionTool.Function(
|
||||
"Get the weather in location. Return temperature in 30°F or 30°C format.", "getCurrentWeather",
|
||||
OpenAiApi.parseJson("""
|
||||
ModelOptionsUtils.jsonToMap("""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -129,7 +130,7 @@ public class OpenAiApiToolFunctionCallIT {
|
||||
|
||||
// extend conversation with function response.
|
||||
messages.add(new ChatCompletionMessage("" + weatherResponse.temp() + weatherRequest.unit(),
|
||||
Role.TOOL, null, toolCall.id(), null));
|
||||
Role.TOOL, functionName, toolCall.id(), null));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.util.stream.Collectors;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
@@ -59,6 +60,7 @@ import org.springframework.util.CollectionUtils;
|
||||
public final class ModelOptionsUtils {
|
||||
|
||||
private final static ObjectMapper OBJECT_MAPPER = new ObjectMapper()
|
||||
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
|
||||
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
|
||||
|
||||
private final static List<String> BEAN_MERGE_FIELD_EXCISIONS = List.of("class");
|
||||
@@ -88,6 +90,22 @@ public final class ModelOptionsUtils {
|
||||
private static TypeReference<HashMap<String, Object>> MAP_TYPE_REF = new TypeReference<HashMap<String, Object>>() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the given JSON string to an Object of the given type.
|
||||
* @param <T> the type of the object to return.
|
||||
* @param json the JSON string to convert to an object.
|
||||
* @param type the type of the object to return.
|
||||
* @return Object instance of the given type.
|
||||
*/
|
||||
public static <T> T jsonToObject(String json, Class<T> type) {
|
||||
try {
|
||||
return OBJECT_MAPPER.readValue(json, type);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException("Failed to json: " + json, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given object to a JSON string.
|
||||
* @param object the object to convert to a JSON string.
|
||||
|
||||
@@ -296,11 +296,18 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>qdrant</artifactId>
|
||||
<version>1.19.6</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>qdrant</artifactId>
|
||||
<version>1.19.6</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.skyscreamer</groupId>
|
||||
<artifactId>jsonassert</artifactId>
|
||||
<version>1.5.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -16,18 +16,23 @@
|
||||
|
||||
package org.springframework.ai.autoconfigure.mistralai;
|
||||
|
||||
import org.springframework.ai.embedding.EmbeddingClient;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.mistralai.MistralAiChatClient;
|
||||
import org.springframework.ai.mistralai.MistralAiEmbeddingClient;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.function.FunctionCallbackContext;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
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.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@@ -61,12 +66,17 @@ public class MistralAiAutoConfiguration {
|
||||
@ConditionalOnProperty(prefix = MistralAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public MistralAiChatClient mistralAiChatClient(MistralAiCommonProperties commonProperties,
|
||||
MistralAiChatProperties chatProperties, RestClient.Builder restClientBuilder) {
|
||||
MistralAiChatProperties chatProperties, RestClient.Builder restClientBuilder,
|
||||
List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext) {
|
||||
|
||||
var mistralAiApi = mistralAiApi(chatProperties.getApiKey(), commonProperties.getApiKey(),
|
||||
chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), restClientBuilder);
|
||||
|
||||
return new MistralAiChatClient(mistralAiApi, chatProperties.getOptions());
|
||||
if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) {
|
||||
chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks);
|
||||
}
|
||||
|
||||
return new MistralAiChatClient(mistralAiApi, chatProperties.getOptions(), functionCallbackContext);
|
||||
}
|
||||
|
||||
private MistralAiApi mistralAiApi(String apiKey, String commonApiKey, String baseUrl, String commonBaseUrl,
|
||||
@@ -81,4 +91,12 @@ public class MistralAiAutoConfiguration {
|
||||
return new MistralAiApi(resoledBaseUrl, resolvedApiKey, restClientBuilder);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public FunctionCallbackContext springAiFunctionManager(ApplicationContext context) {
|
||||
FunctionCallbackContext manager = new FunctionCallbackContext();
|
||||
manager.setApplicationContext(context);
|
||||
return manager;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.ai.autoconfigure.mistralai;
|
||||
|
||||
import org.springframework.ai.mistralai.MistralAiChatOptions;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
|
||||
@@ -30,7 +31,7 @@ public class MistralAiChatProperties extends MistralAiParentProperties {
|
||||
|
||||
public static final String CONFIG_PREFIX = "spring.ai.mistral.chat";
|
||||
|
||||
public static final String DEFAULT_CHAT_MODEL = "mistral-tiny";
|
||||
public static final String DEFAULT_CHAT_MODEL = MistralAiApi.ChatModel.TINY.getValue();
|
||||
|
||||
private static final Double DEFAULT_TEMPERATURE = 0.7;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.ai.autoconfigure.mistralai;
|
||||
|
||||
import org.springframework.ai.document.MetadataMode;
|
||||
import org.springframework.ai.mistralai.MistralAiEmbeddingOptions;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
|
||||
@@ -30,7 +31,7 @@ public class MistralAiEmbeddingProperties extends MistralAiParentProperties {
|
||||
|
||||
public static final String CONFIG_PREFIX = "spring.ai.mistralai.embedding";
|
||||
|
||||
public static final String DEFAULT_EMBEDDING_MODEL = "mistral-embed";
|
||||
public static final String DEFAULT_EMBEDDING_MODEL = MistralAiApi.EmbeddingModel.EMBED.getValue();
|
||||
|
||||
public static final String DEFAULT_ENCODING_FORMAT = "float";
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2024-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.mistralai.tool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.autoconfigure.mistralai.MistralAiAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.mistralai.MistralAiChatClient;
|
||||
import org.springframework.ai.mistralai.MistralAiChatOptions;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Description;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*")
|
||||
class PaymentStatusBeanIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PaymentStatusBeanIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"))
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class))
|
||||
.withUserConfiguration(Config.class);
|
||||
|
||||
@Test
|
||||
void functionCallTest() {
|
||||
|
||||
contextRunner
|
||||
.withPropertyValues("spring.ai.mistral.chat.options.model=" + MistralAiApi.ChatModel.LARGE.getValue())
|
||||
.run(context -> {
|
||||
|
||||
MistralAiChatClient chatClient = context.getBean(MistralAiChatClient.class);
|
||||
|
||||
ChatResponse response = chatClient
|
||||
.call(new Prompt(List.of(new UserMessage("What's the status of my transaction with id T1001?")),
|
||||
MistralAiChatOptions.builder()
|
||||
.withFunction("retrievePaymentStatus")
|
||||
.withFunction("retrievePaymentDate")
|
||||
.build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).containsIgnoringCase("T1001");
|
||||
assertThat(response.getResult().getOutput().getContent()).containsIgnoringCase("paid");
|
||||
});
|
||||
}
|
||||
|
||||
// Assuming we have the following data
|
||||
public static final Map<String, StatusDate> DATA = Map.of("T1001", new StatusDate("Paid", "2021-10-05"), "T1002",
|
||||
new StatusDate("Unpaid", "2021-10-06"), "T1003", new StatusDate("Paid", "2021-10-07"), "T1004",
|
||||
new StatusDate("Paid", "2021-10-05"), "T1005", new StatusDate("Pending", "2021-10-08"));
|
||||
|
||||
record StatusDate(String status, String date) {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
public record Transaction(@JsonProperty(required = true, value = "transaction_id") String transactionId) {
|
||||
}
|
||||
|
||||
public record Status(@JsonProperty(required = true, value = "status") String status) {
|
||||
}
|
||||
|
||||
public record Date(@JsonProperty(required = true, value = "date") String date) {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Description("Get payment status of a transaction")
|
||||
public Function<Transaction, Status> retrievePaymentStatus() {
|
||||
return (transaction) -> new Status(DATA.get(transaction.transactionId).status());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Description("Get payment date of a transaction")
|
||||
public Function<Transaction, Date> retrievePaymentDate() {
|
||||
return (transaction) -> new Date(DATA.get(transaction.transactionId).date());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2024-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.mistralai.tool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.ai.openai.OpenAiChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Description;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Same test as {@link PaymentStatusBeanIT.java} but using {@link OpenAiChatClient} for
|
||||
* Mistral AI Function Calling implementation.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*")
|
||||
class PaymentStatusBeanOpenAiIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PaymentStatusBeanIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"),
|
||||
"spring.ai.openai.chat.base-url=https://api.mistral.ai")
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OpenAiAutoConfiguration.class))
|
||||
.withUserConfiguration(Config.class);
|
||||
|
||||
@Test
|
||||
void functionCallTest() {
|
||||
|
||||
contextRunner
|
||||
.withPropertyValues("spring.ai.openai.chat.options.model=" + MistralAiApi.ChatModel.SMALL.getValue())
|
||||
.run(context -> {
|
||||
|
||||
OpenAiChatClient chatClient = context.getBean(OpenAiChatClient.class);
|
||||
|
||||
ChatResponse response = chatClient
|
||||
.call(new Prompt(List.of(new UserMessage("What's the status of my transaction with id T1001?")),
|
||||
OpenAiChatOptions.builder()
|
||||
.withFunction("retrievePaymentStatus")
|
||||
.withFunction("retrievePaymentDate")
|
||||
.build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).containsIgnoringCase("T1001");
|
||||
assertThat(response.getResult().getOutput().getContent()).containsIgnoringCase("paid");
|
||||
});
|
||||
}
|
||||
|
||||
// Assuming we have the following data
|
||||
public static final Map<String, StatusDate> DATA = Map.of("T1001", new StatusDate("Paid", "2021-10-05"), "T1002",
|
||||
new StatusDate("Unpaid", "2021-10-06"), "T1003", new StatusDate("Paid", "2021-10-07"), "T1004",
|
||||
new StatusDate("Paid", "2021-10-05"), "T1005", new StatusDate("Pending", "2021-10-08"));
|
||||
|
||||
record StatusDate(String status, String date) {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
public record Transaction(@JsonProperty(required = true, value = "transaction_id") String transactionId) {
|
||||
}
|
||||
|
||||
public record Status(@JsonProperty(required = true, value = "status") String status) {
|
||||
}
|
||||
|
||||
public record Date(@JsonProperty(required = true, value = "date") String date) {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Description("Get payment status of a transaction")
|
||||
public Function<Transaction, Status> retrievePaymentStatus() {
|
||||
return (transaction) -> new Status(DATA.get(transaction.transactionId).status());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Description("Get payment date of a transaction")
|
||||
public Function<Transaction, Date> retrievePaymentDate() {
|
||||
return (transaction) -> new Date(DATA.get(transaction.transactionId).date());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2024-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.mistralai.tool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.autoconfigure.mistralai.MistralAiAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.mistralai.MistralAiChatClient;
|
||||
import org.springframework.ai.mistralai.MistralAiChatOptions;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.ai.model.function.FunctionCallbackWrapper;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*")
|
||||
public class PaymentStatusPromptIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WeatherServicePromptIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"))
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class));
|
||||
|
||||
public record Transaction(@JsonProperty(required = true, value = "transaction_id") String id) {
|
||||
}
|
||||
|
||||
public record Status(@JsonProperty(required = true, value = "status") String status) {
|
||||
}
|
||||
|
||||
record StatusDate(String status, String date) {
|
||||
}
|
||||
|
||||
// Assuming we have the following payment data.
|
||||
public static final Map<Transaction, StatusDate> DATA = Map.of(new Transaction("T1001"),
|
||||
new StatusDate("Paid", "2021-10-05"), new Transaction("T1002"), new StatusDate("Unpaid", "2021-10-06"),
|
||||
new Transaction("T1003"), new StatusDate("Paid", "2021-10-07"), new Transaction("T1004"),
|
||||
new StatusDate("Paid", "2021-10-05"), new Transaction("T1005"), new StatusDate("Pending", "2021-10-08"));
|
||||
|
||||
@Test
|
||||
void functionCallTest() {
|
||||
contextRunner
|
||||
.withPropertyValues("spring.ai.mistral.chat.options.model=" + MistralAiApi.ChatModel.SMALL.getValue())
|
||||
.run(context -> {
|
||||
|
||||
MistralAiChatClient chatClient = context.getBean(MistralAiChatClient.class);
|
||||
|
||||
UserMessage userMessage = new UserMessage("What's the status of my transaction with id T1001?");
|
||||
|
||||
var promptOptions = MistralAiChatOptions.builder()
|
||||
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new Function<Transaction, Status>() {
|
||||
public Status apply(Transaction transaction) {
|
||||
return new Status(DATA.get(transaction).status());
|
||||
}
|
||||
})
|
||||
.withName("retrievePaymentStatus")
|
||||
.withDescription("Get payment status of a transaction")
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage), promptOptions));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).containsIgnoringCase("T1001");
|
||||
assertThat(response.getResult().getOutput().getContent()).containsIgnoringCase("paid");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2024-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.mistralai.tool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.autoconfigure.mistralai.MistralAiAutoConfiguration;
|
||||
import org.springframework.ai.autoconfigure.mistralai.tool.WeatherServicePromptIT.MyWeatherService.Request;
|
||||
import org.springframework.ai.autoconfigure.mistralai.tool.WeatherServicePromptIT.MyWeatherService.Response;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.mistralai.MistralAiChatClient;
|
||||
import org.springframework.ai.mistralai.MistralAiChatOptions;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi;
|
||||
import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice;
|
||||
import org.springframework.ai.model.function.FunctionCallbackWrapper;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Christian Tzolov
|
||||
* @since 0.8.1
|
||||
*/
|
||||
@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*")
|
||||
public class WeatherServicePromptIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WeatherServicePromptIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.mistralai.api-key=" + System.getenv("MISTRAL_AI_API_KEY"))
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void promptFunctionCall() {
|
||||
contextRunner
|
||||
.withPropertyValues("spring.ai.mistral.chat.options.model=" + MistralAiApi.ChatModel.LARGE.getValue())
|
||||
.run(context -> {
|
||||
|
||||
MistralAiChatClient chatClient = context.getBean(MistralAiChatClient.class);
|
||||
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in Paris?");
|
||||
// UserMessage userMessage = new UserMessage("What's the weather like in
|
||||
// San Francisco, Tokyo, and
|
||||
// Paris?");
|
||||
|
||||
var promptOptions = MistralAiChatOptions.builder()
|
||||
.withToolChoice(ToolChoice.AUTO)
|
||||
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MyWeatherService())
|
||||
.withName("CurrentWeatherService")
|
||||
.withDescription("Get the current weather in requested location")
|
||||
.build()))
|
||||
.build();
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage), promptOptions));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).containsAnyOf("15", "15.0");
|
||||
// assertThat(response.getResult().getOutput().getContent()).contains("30.0",
|
||||
// "10.0", "15.0");
|
||||
});
|
||||
}
|
||||
|
||||
public static class MyWeatherService implements Function<Request, Response> {
|
||||
|
||||
// @formatter:off
|
||||
public enum Unit { C, F }
|
||||
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public record Request(
|
||||
@JsonProperty(required = true, value = "location") String location,
|
||||
@JsonProperty(required = true, value = "unit") Unit unit) {}
|
||||
|
||||
public record Response(double temperature, Unit unit) {}
|
||||
// @formatter:on
|
||||
|
||||
@Override
|
||||
public Response apply(Request request) {
|
||||
if (request.location().contains("Paris")) {
|
||||
return new Response(15, request.unit());
|
||||
}
|
||||
else if (request.location().contains("Tokyo")) {
|
||||
return new Response(10, request.unit());
|
||||
}
|
||||
else if (request.location().contains("San Francisco")) {
|
||||
return new Response(30, request.unit());
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid request: " + request);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
* Copyright 2024-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.
|
||||
@@ -16,15 +16,15 @@
|
||||
|
||||
package org.springframework.ai.autoconfigure.openai;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
import org.skyscreamer.jsonassert.JSONCompareMode;
|
||||
|
||||
import org.springframework.ai.openai.OpenAiChatClient;
|
||||
import org.springframework.ai.openai.OpenAiEmbeddingClient;
|
||||
import org.springframework.ai.openai.OpenAiImageClient;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ResponseFormat;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ToolChoice;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder;
|
||||
import org.springframework.ai.openai.api.OpenAiApi.FunctionTool.Type;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
@@ -218,7 +218,8 @@ public class OpenAiPropertiesTests {
|
||||
"spring.ai.openai.chat.options.temperature=0.55",
|
||||
"spring.ai.openai.chat.options.topP=0.56",
|
||||
|
||||
"spring.ai.openai.chat.options.toolChoice.functionName=toolChoiceFunctionName",
|
||||
// "spring.ai.openai.chat.options.toolChoice.functionName=toolChoiceFunctionName",
|
||||
"spring.ai.openai.chat.options.toolChoice=" + ToolChoiceBuilder.FUNCTION("toolChoiceFunctionName"),
|
||||
|
||||
"spring.ai.openai.chat.options.tools[0].function.name=myFunction1",
|
||||
"spring.ai.openai.chat.options.tools[0].function.description=function description",
|
||||
@@ -272,8 +273,9 @@ public class OpenAiPropertiesTests {
|
||||
assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55f);
|
||||
assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56f);
|
||||
|
||||
assertThat(chatProperties.getOptions().getToolChoice())
|
||||
.isEqualTo(new ToolChoice("function", Map.of("name", "toolChoiceFunctionName")));
|
||||
JSONAssert.assertEquals("{\"type\":\"function\",\"function\":{\"name\":\"toolChoiceFunctionName\"}}",
|
||||
chatProperties.getOptions().getToolChoice(), JSONCompareMode.LENIENT);
|
||||
|
||||
assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ");
|
||||
|
||||
assertThat(chatProperties.getOptions().getTools()).hasSize(1);
|
||||
|
||||
Reference in New Issue
Block a user