From 0927bd197db7ed4f8ac7fa7d99c9cbfdc24b3c27 Mon Sep 17 00:00:00 2001 From: GR Date: Thu, 25 Jul 2024 21:43:19 +0800 Subject: [PATCH] Fix MiniMax model function call implementation Implement function call capability for MiniMax model and add unit tests based on new tool classes. Address most scenarios, but note limitations in complex English contexts with multiple function calls. Weather query example: may stop prematurely when querying multiple locations due to single-location parameter limit. This behavior stems from model performance constraints. Streaming function calling is not passing tests, will be address seperately. Resolves #1077 Implement function call capability for the Moonshot model. Include unit tests to verify the new functionality. This feature addresses the requirements outlined in issue #1058. fix: MiniMax function call review --- .../ai/minimax/MiniMaxChatModel.java | 355 ++++++++++-------- .../ai/minimax/MiniMaxChatOptions.java | 10 +- .../ai/minimax/api/MiniMaxApi.java | 4 +- .../MiniMaxStreamFunctionCallingHelper.java | 108 ++---- .../api/MiniMaxApiToolFunctionCallIT.java | 6 +- .../minimax/MiniMaxAutoConfiguration.java | 12 +- .../minimax/FunctionCallbackInPromptIT.java | 14 +- ...nctionCallbackWithPlainFunctionBeanIT.java | 21 +- .../minimax/FunctionCallbackWrapperIT.java | 14 +- .../minimax/MiniMaxAutoConfigurationIT.java | 2 +- .../minimax/MockWeatherService.java | 2 +- 11 files changed, 276 insertions(+), 272 deletions(-) diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java index b884584f4..257ea0264 100644 --- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java +++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java @@ -17,24 +17,33 @@ package org.springframework.ai.minimax; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.MessageType; +import org.springframework.ai.chat.messages.ToolResponseMessage; +import org.springframework.ai.chat.metadata.ChatGenerationMetadata; +import org.springframework.ai.chat.metadata.ChatResponseMetadata; +import org.springframework.ai.chat.metadata.RateLimit; +import org.springframework.ai.chat.model.AbstractToolCallSupport; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.model.StreamingChatModel; -import org.springframework.ai.chat.metadata.ChatGenerationMetadata; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.minimax.api.MiniMaxApi; import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletion; +import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletion.Choice; import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionChunk; import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionFinishReason; import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage; +import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage.ChatCompletionFunction; import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage.Role; import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage.ToolCall; import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionRequest; import org.springframework.ai.minimax.api.MiniMaxApi.FunctionTool; +import org.springframework.ai.minimax.metadata.MiniMaxUsage; import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.ai.model.function.AbstractFunctionCallSupport; +import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackContext; import org.springframework.ai.retry.RetryUtils; import org.springframework.http.ResponseEntity; @@ -42,12 +51,11 @@ import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -61,9 +69,7 @@ import java.util.concurrent.ConcurrentHashMap; * @see MiniMaxApi * @since 1.0.0 M1 */ -public class MiniMaxChatModel extends - AbstractFunctionCallSupport> - implements ChatModel, StreamingChatModel { +public class MiniMaxChatModel extends AbstractToolCallSupport implements ChatModel, StreamingChatModel { private static final Logger logger = LoggerFactory.getLogger(MiniMaxChatModel.class); @@ -113,10 +119,27 @@ public class MiniMaxChatModel extends */ public MiniMaxChatModel(MiniMaxApi miniMaxApi, MiniMaxChatOptions options, FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) { - super(functionCallbackContext); + this(miniMaxApi, options, functionCallbackContext, List.of(), retryTemplate); + } + + /** + * Initializes a new instance of the MiniMaxChatModel. + * @param miniMaxApi The MiniMaxApi instance to be used for interacting with the + * MiniMax Chat API. + * @param options The MiniMaxChatOptions to configure the chat model. + * @param functionCallbackContext The function callback context. + * @param toolFunctionCallbacks The tool function callbacks. + * @param retryTemplate The retry template. + */ + public MiniMaxChatModel(MiniMaxApi miniMaxApi, MiniMaxChatOptions options, + FunctionCallbackContext functionCallbackContext, List toolFunctionCallbacks, + RetryTemplate retryTemplate) { + super(functionCallbackContext, options, toolFunctionCallbacks); Assert.notNull(miniMaxApi, "MiniMaxApi must not be null"); Assert.notNull(options, "Options must not be null"); Assert.notNull(retryTemplate, "RetryTemplate must not be null"); + Assert.isTrue(CollectionUtils.isEmpty(options.getFunctionCallbacks()), + "The default function callbacks must be set via the toolFunctionCallbacks constructor parameter"); this.miniMaxApi = miniMaxApi; this.defaultOptions = options; this.retryTemplate = retryTemplate; @@ -124,94 +147,150 @@ public class MiniMaxChatModel extends @Override public ChatResponse call(Prompt prompt) { - ChatCompletionRequest request = createRequest(prompt, false); - return this.retryTemplate.execute(ctx -> { + ResponseEntity completionEntity = this.retryTemplate + .execute(ctx -> this.miniMaxApi.chatCompletionEntity(request)); - ResponseEntity completionEntity = this.callWithFunctionSupport(request); + var chatCompletion = completionEntity.getBody(); - var chatCompletion = completionEntity.getBody(); - if (chatCompletion == null) { - logger.warn("No chat completion returned for prompt: {}", prompt); - return new ChatResponse(List.of()); - } + if (chatCompletion == null) { + logger.warn("No chat completion returned for prompt: {}", prompt); + return new ChatResponse(List.of()); + } - if (chatCompletion.baseResponse() != null && chatCompletion.baseResponse().statusCode() != 0) { - throw new RuntimeException(chatCompletion.baseResponse().message()); - } + List choices = chatCompletion.choices(); + if (choices == null) { + logger.warn("No choices returned for prompt: {}, because: {}}", prompt, + chatCompletion.baseResponse().message()); + return new ChatResponse(List.of()); + } - List generations = chatCompletion.choices().stream().map(choice -> { - return new Generation(choice.message().content(), toMap(chatCompletion.id(), choice)) - .withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null)); - }).toList(); + List generations = choices.stream().map(choice -> { + // @formatter:off + Map metadata = Map.of( + "id", chatCompletion.id(), + "role", choice.message().role() != null ? choice.message().role().name() : "", + "finishReason", choice.finishReason() != null ? choice.finishReason().name() : ""); + // @formatter:on + return buildGeneration(choice, metadata); + }).toList(); - return new ChatResponse(generations); - }); + ChatResponse chatResponse = new ChatResponse(generations, from(completionEntity.getBody())); + + if (isToolCall(chatResponse, + Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) { + var toolCallConversation = handleToolCalls(prompt, chatResponse); + // Recursively call the call method with the tool call message + // conversation that contains the call responses. + return this.call(new Prompt(toolCallConversation, prompt.getOptions())); + } + + return chatResponse; } - private Map toMap(String id, ChatCompletion.Choice choice) { - Map map = new HashMap<>(); - - var message = choice.message(); - if (message.role() != null) { - map.put("role", message.role().name()); - } - if (choice.finishReason() != null) { - map.put("finishReason", choice.finishReason().name()); - } - map.put("id", id); - return map; + @Override + public ChatOptions getDefaultOptions() { + return MiniMaxChatOptions.fromOptions(this.defaultOptions); } @Override public Flux stream(Prompt prompt) { - ChatCompletionRequest request = createRequest(prompt, true); - return this.retryTemplate.execute(ctx -> { + Flux completionChunks = this.retryTemplate + .execute(ctx -> this.miniMaxApi.chatCompletionStream(request)); - Flux completionChunks = this.miniMaxApi.chatCompletionStream(request); + // For chunked responses, only the first chunk contains the choice role. + // The rest of the chunks with same ID share the same role. + ConcurrentHashMap roleMap = new ConcurrentHashMap<>(); - // For chunked responses, only the first chunk contains the choice role. - // The rest of the chunks with same ID share the same role. - ConcurrentHashMap roleMap = new ConcurrentHashMap<>(); - - // Convert the ChatCompletionChunk into a ChatCompletion to be able to reuse - // the function call handling logic. - return completionChunks.map(this::chunkToChatCompletion).map(chatCompletion -> { + // Convert the ChatCompletionChunk into a ChatCompletion to be able to reuse + // the function call handling logic. + Flux chatResponse = completionChunks.map(this::chunkToChatCompletion) + .switchMap(chatCompletion -> Mono.just(chatCompletion).map(chatCompletion2 -> { try { - chatCompletion = handleFunctionCallOrReturn(request, ResponseEntity.of(Optional.of(chatCompletion))) - .getBody(); - @SuppressWarnings("null") - String id = chatCompletion.id(); + String id = chatCompletion2.id(); - List generations = chatCompletion.choices().stream().map(choice -> { - if (choice.message().role() != null) { - roleMap.putIfAbsent(id, choice.message().role().name()); - } - String finish = (choice.finishReason() != null ? choice.finishReason().name() : ""); - var generation = new Generation(choice.message().content(), - Map.of("id", id, "role", roleMap.get(id), "finishReason", finish)); - if (choice.finishReason() != null) { - generation = generation.withGenerationMetadata( - ChatGenerationMetadata.from(choice.finishReason().name(), null)); - } - return generation; - }).toList(); + // @formatter:off + List generations = chatCompletion2.choices().stream().map(choice -> { + if (choice.message().role() != null) { + roleMap.putIfAbsent(id, choice.message().role().name()); + } + Map metadata = Map.of( + "id", chatCompletion2.id(), + "role", roleMap.getOrDefault(id, ""), + "finishReason", choice.finishReason() != null ? choice.finishReason().name() : ""); + return buildGeneration(choice, metadata); + }).toList(); + // @formatter:on - return new ChatResponse(generations); + if (chatCompletion2.usage() != null) { + return new ChatResponse(generations, from(chatCompletion2)); + } + else { + return new ChatResponse(generations); + } } catch (Exception e) { logger.error("Error processing chat completion", e); return new ChatResponse(List.of()); } - }); + })); + + return chatResponse.flatMap(response -> { + + if (isToolCall(response, + Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) { + var toolCallConversation = handleToolCalls(prompt, response); + // Recursively call the stream method with the tool call message + // conversation that contains the call responses. + return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); + } + else { + return Flux.just(response); + } }); } + private ChatResponseMetadata from(ChatCompletion result, RateLimit rateLimit) { + Assert.notNull(result, "MiniMax ChatCompletionResult must not be null"); + return ChatResponseMetadata.builder() + .withId(result.id()) + .withUsage(MiniMaxUsage.from(result.usage())) + .withModel(result.model()) + .withRateLimit(rateLimit) + .withKeyValue("created", result.created()) + .build(); + } + + private ChatResponseMetadata from(ChatCompletion result) { + Assert.notNull(result, "MiniMax ChatCompletionResult must not be null"); + return ChatResponseMetadata.builder() + .withId(result.id()) + .withUsage(MiniMaxUsage.from(result.usage())) + .withModel(result.model()) + .withKeyValue("created", result.created()) + .build(); + } + + private static Generation buildGeneration(Choice choice, Map metadata) { + List toolCalls = choice.message().toolCalls() == null ? List.of() + : choice.message() + .toolCalls() + .stream() + .map(toolCall -> new AssistantMessage.ToolCall(toolCall.id(), "function", + toolCall.function().name(), toolCall.function().arguments())) + .toList(); + + var assistantMessage = new AssistantMessage(choice.message().content(), metadata, toolCalls); + String finishReason = (choice.finishReason() != null ? choice.finishReason().name() : ""); + var generationMetadata = ChatGenerationMetadata.from(finishReason, null); + return new Generation(assistantMessage, generationMetadata); + } + /** * Convert the ChatCompletionChunk into a ChatCompletion. The Usage is set to null. * @param chunk the ChatCompletionChunk to convert @@ -235,49 +314,73 @@ public class MiniMaxChatModel extends */ ChatCompletionRequest createRequest(Prompt prompt, boolean stream) { - Set functionsForThisRequest = new HashSet<>(); + List chatCompletionMessages = prompt.getInstructions().stream().map(message -> { + if (message.getMessageType() == MessageType.USER || message.getMessageType() == MessageType.SYSTEM) { + Object content = message.getContent(); + return List.of(new ChatCompletionMessage(content, + ChatCompletionMessage.Role.valueOf(message.getMessageType().name()))); + } + else if (message.getMessageType() == MessageType.ASSISTANT) { + var assistantMessage = (AssistantMessage) message; + List toolCalls = null; + if (!CollectionUtils.isEmpty(assistantMessage.getToolCalls())) { + toolCalls = assistantMessage.getToolCalls().stream().map(toolCall -> { + var function = new ChatCompletionFunction(toolCall.name(), toolCall.arguments()); + return new ToolCall(toolCall.id(), toolCall.type(), function); + }).toList(); + } + return List.of(new ChatCompletionMessage(assistantMessage.getContent(), + ChatCompletionMessage.Role.ASSISTANT, null, null, toolCalls)); + } + else if (message.getMessageType() == MessageType.TOOL) { + ToolResponseMessage toolMessage = (ToolResponseMessage) message; - List chatCompletionMessages = prompt.getInstructions() - .stream() - .map(m -> new MiniMaxApi.ChatCompletionMessage(m.getContent(), - MiniMaxApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))) - .toList(); + toolMessage.getResponses().forEach(response -> { + Assert.isTrue(response.id() != null, "ToolResponseMessage must have an id"); + Assert.isTrue(response.name() != null, "ToolResponseMessage must have a name"); + }); + + return toolMessage.getResponses() + .stream() + .map(tr -> new ChatCompletionMessage(tr.responseData(), ChatCompletionMessage.Role.TOOL, tr.name(), + tr.id(), null)) + .toList(); + } + else { + throw new IllegalArgumentException("Unsupported message type: " + message.getMessageType()); + } + }).flatMap(List::stream).toList(); ChatCompletionRequest request = new ChatCompletionRequest(chatCompletionMessages, stream); + Set enabledToolsToUse = new HashSet<>(); + if (prompt.getOptions() != null) { MiniMaxChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, MiniMaxChatOptions.class); - Set promptEnabledFunctions = this.handleFunctionCallbackConfigurations(updatedRuntimeOptions, - IS_RUNTIME_CALL); - functionsForThisRequest.addAll(promptEnabledFunctions); + enabledToolsToUse.addAll(this.runtimeFunctionCallbackConfigurations(updatedRuntimeOptions)); request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, ChatCompletionRequest.class); } - if (this.defaultOptions != null) { - - Set defaultEnabledFunctions = this.handleFunctionCallbackConfigurations(this.defaultOptions, - !IS_RUNTIME_CALL); - - functionsForThisRequest.addAll(defaultEnabledFunctions); - - request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class); + if (!CollectionUtils.isEmpty(this.defaultOptions.getFunctions())) { + enabledToolsToUse.addAll(this.defaultOptions.getFunctions()); } - // Add the enabled functions definitions to the request's tools parameter. - if (!CollectionUtils.isEmpty(functionsForThisRequest)) { + request = ModelOptionsUtils.merge(request, this.defaultOptions, ChatCompletionRequest.class); + + if (!CollectionUtils.isEmpty(enabledToolsToUse)) { request = ModelOptionsUtils.merge( - MiniMaxChatOptions.builder().withTools(this.getFunctionTools(functionsForThisRequest)).build(), - request, ChatCompletionRequest.class); + MiniMaxChatOptions.builder().withTools(this.getFunctionTools(enabledToolsToUse)).build(), request, + ChatCompletionRequest.class); } return request; } - private List getFunctionTools(Set functionNames) { + private List getFunctionTools(Set functionNames) { return this.resolveFunctionCallbacks(functionNames).stream().map(functionCallback -> { var function = new FunctionTool.Function(functionCallback.getDescription(), functionCallback.getName(), functionCallback.getInputTypeSchema()); @@ -285,80 +388,4 @@ public class MiniMaxChatModel extends }).toList(); } - @Override - protected ChatCompletionRequest doCreateToolResponseRequest(ChatCompletionRequest previousRequest, - ChatCompletionMessage responseMessage, List 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, Role.TOOL, functionName, toolCall.id(), null)); - } - - // Recursively call chatCompletionWithTools until the model doesn't call a - // functions anymore. - ChatCompletionRequest newRequest = new ChatCompletionRequest(conversationHistory, false); - newRequest = ModelOptionsUtils.merge(newRequest, previousRequest, ChatCompletionRequest.class); - - return newRequest; - } - - @Override - protected List doGetUserMessages(ChatCompletionRequest request) { - return request.messages(); - } - - @Override - protected ChatCompletionMessage doGetToolResponseMessage(ResponseEntity chatCompletion) { - return chatCompletion.getBody().choices().iterator().next().message(); - } - - @Override - protected ResponseEntity doChatCompletion(ChatCompletionRequest request) { - return this.miniMaxApi.chatCompletionEntity(request); - } - - @Override - protected Flux> doChatCompletionStream(ChatCompletionRequest request) { - return this.miniMaxApi.chatCompletionStream(request) - .map(this::chunkToChatCompletion) - .map(Optional::ofNullable) - .map(ResponseEntity::of); - } - - @Override - protected boolean isToolFunctionCall(ResponseEntity chatCompletion) { - var body = chatCompletion.getBody(); - if (body == null) { - return false; - } - - var choices = body.choices(); - if (CollectionUtils.isEmpty(choices)) { - return false; - } - - var choice = choices.get(0); - var message = choice.message(); - return message != null && !CollectionUtils.isEmpty(choice.message().toolCalls()) - && choice.finishReason() == ChatCompletionFinishReason.TOOL_CALLS; - } - - @Override - public ChatOptions getDefaultOptions() { - return MiniMaxChatOptions.fromOptions(this.defaultOptions); - } - } diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatOptions.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatOptions.java index f1bf80773..9626004e3 100644 --- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatOptions.java +++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatOptions.java @@ -15,11 +15,6 @@ */ package org.springframework.ai.minimax; -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.JsonInclude.Include; @@ -31,6 +26,11 @@ import org.springframework.ai.model.function.FunctionCallingOptions; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.util.Assert; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + /** * MiniMaxChatOptions represents the options for performing chat completion using the * MiniMax API. It provides methods to set and retrieve various options like model, diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApi.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApi.java index eb4923464..981e3335b 100644 --- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApi.java +++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApi.java @@ -714,7 +714,7 @@ public class MiniMaxApi { .takeUntil(SSE_DONE_PREDICATE) .filter(SSE_DONE_PREDICATE.negate()) .map(content -> ModelOptionsUtils.jsonToObject(content, ChatCompletionChunk.class)) - .map(chunk -> { + .map(chunk -> { if (this.chunkMerger.isStreamingToolFunctionCall(chunk)) { isInsideTool.set(true); } @@ -730,7 +730,7 @@ public class MiniMaxApi { .concatMapIterable(window -> { Mono monoChunk = window.reduce( new ChatCompletionChunk(null, null, null, null, null, null), - this.chunkMerger::merge); + (previous, current) -> this.chunkMerger.merge(previous, current)); return List.of(monoChunk); }) .flatMap(mono -> mono); diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxStreamFunctionCallingHelper.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxStreamFunctionCallingHelper.java index ceb92d03d..06454a348 100644 --- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxStreamFunctionCallingHelper.java +++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxStreamFunctionCallingHelper.java @@ -15,7 +15,16 @@ */ package org.springframework.ai.minimax.api; +import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionChunk; +import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionChunk.ChunkChoice; +import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionFinishReason; +import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage; +import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage.ChatCompletionFunction; +import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage.Role; +import org.springframework.ai.minimax.api.MiniMaxApi.ChatCompletionMessage.ToolCall; +import org.springframework.ai.minimax.api.MiniMaxApi.LogProbs; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.List; @@ -29,14 +38,7 @@ import java.util.List; */ public class MiniMaxStreamFunctionCallingHelper { - /** - * Merge the previous and current ChatCompletionChunk into a single one. - * @param previous the previous ChatCompletionChunk - * @param current the current ChatCompletionChunk - * @return the merged ChatCompletionChunk - */ - public MiniMaxApi.ChatCompletionChunk merge(MiniMaxApi.ChatCompletionChunk previous, - MiniMaxApi.ChatCompletionChunk current) { + public ChatCompletionChunk merge(ChatCompletionChunk previous, ChatCompletionChunk current) { if (previous == null) { return current; @@ -49,47 +51,38 @@ public class MiniMaxStreamFunctionCallingHelper { : previous.systemFingerprint()); String object = (current.object() != null ? current.object() : previous.object()); - MiniMaxApi.ChatCompletionChunk.ChunkChoice previousChoice0 = (CollectionUtils.isEmpty(previous.choices()) ? null - : previous.choices().get(0)); - MiniMaxApi.ChatCompletionChunk.ChunkChoice currentChoice0 = (CollectionUtils.isEmpty(current.choices()) ? null - : current.choices().get(0)); + ChunkChoice previousChoice0 = (CollectionUtils.isEmpty(previous.choices()) ? null : previous.choices().get(0)); + ChunkChoice currentChoice0 = (CollectionUtils.isEmpty(current.choices()) ? null : current.choices().get(0)); - MiniMaxApi.ChatCompletionChunk.ChunkChoice choice = merge(previousChoice0, currentChoice0); - List chunkChoices = choice == null ? List.of() : List.of(choice); - return new MiniMaxApi.ChatCompletionChunk(id, chunkChoices, created, model, systemFingerprint, object); + ChunkChoice choice = merge(previousChoice0, currentChoice0); + List chunkChoices = choice == null ? List.of() : List.of(choice); + return new ChatCompletionChunk(id, chunkChoices, created, model, systemFingerprint, object); } - private MiniMaxApi.ChatCompletionChunk.ChunkChoice merge(MiniMaxApi.ChatCompletionChunk.ChunkChoice previous, - MiniMaxApi.ChatCompletionChunk.ChunkChoice current) { + private ChunkChoice merge(ChunkChoice previous, ChunkChoice current) { if (previous == null) { return current; } - MiniMaxApi.ChatCompletionFinishReason finishReason = (current.finishReason() != null ? current.finishReason() + ChatCompletionFinishReason finishReason = (current.finishReason() != null ? current.finishReason() : previous.finishReason()); Integer index = (current.index() != null ? current.index() : previous.index()); + LogProbs logprobs = (current.logprobs() != null ? current.logprobs() : previous.logprobs()); - MiniMaxApi.ChatCompletionMessage message = merge(previous.delta(), current.delta()); - - MiniMaxApi.LogProbs logprobs = (current.logprobs() != null ? current.logprobs() : previous.logprobs()); - return new MiniMaxApi.ChatCompletionChunk.ChunkChoice(finishReason, index, message, logprobs); + ChatCompletionMessage message = merge(previous.delta(), current.delta()); + return new ChunkChoice(finishReason, index, message, logprobs); } - private MiniMaxApi.ChatCompletionMessage merge(MiniMaxApi.ChatCompletionMessage previous, - MiniMaxApi.ChatCompletionMessage current) { + private ChatCompletionMessage merge(ChatCompletionMessage previous, ChatCompletionMessage current) { String content = (current.content() != null ? current.content() - : (previous.content() != null) ? previous.content() : ""); - MiniMaxApi.ChatCompletionMessage.Role role = (current.role() != null ? current.role() : previous.role()); - role = (role != null ? role : MiniMaxApi.ChatCompletionMessage.Role.ASSISTANT); // default - // to - // ASSISTANT - // (if - // null + : "" + ((previous.content() != null) ? previous.content() : "")); + Role role = (current.role() != null ? current.role() : previous.role()); + role = (role != null ? role : Role.ASSISTANT); // default to ASSISTANT (if null String name = (current.name() != null ? current.name() : previous.name()); String toolCallId = (current.toolCallId() != null ? current.toolCallId() : previous.toolCallId()); - List toolCalls = new ArrayList<>(); - MiniMaxApi.ChatCompletionMessage.ToolCall lastPreviousTooCall = null; + List toolCalls = new ArrayList<>(); + ToolCall lastPreviousTooCall = null; if (previous.toolCalls() != null) { lastPreviousTooCall = previous.toolCalls().get(previous.toolCalls().size() - 1); if (previous.toolCalls().size() > 1) { @@ -101,43 +94,40 @@ public class MiniMaxStreamFunctionCallingHelper { throw new IllegalStateException("Currently only one tool call is supported per message!"); } var currentToolCall = current.toolCalls().iterator().next(); - if (currentToolCall.id() != null) { + if (currentToolCall.id() == null + || (lastPreviousTooCall != null && currentToolCall.id().equals(lastPreviousTooCall.id()))) { + toolCalls.add(merge(lastPreviousTooCall, currentToolCall)); + } + else { if (lastPreviousTooCall != null) { toolCalls.add(lastPreviousTooCall); } toolCalls.add(currentToolCall); } - else { - toolCalls.add(merge(lastPreviousTooCall, currentToolCall)); - } } else { if (lastPreviousTooCall != null) { toolCalls.add(lastPreviousTooCall); } } - return new MiniMaxApi.ChatCompletionMessage(content, role, name, toolCallId, toolCalls); + return new ChatCompletionMessage(content, role, name, toolCallId, toolCalls); } - private MiniMaxApi.ChatCompletionMessage.ToolCall merge(MiniMaxApi.ChatCompletionMessage.ToolCall previous, - MiniMaxApi.ChatCompletionMessage.ToolCall current) { + private ToolCall merge(ToolCall previous, ToolCall current) { if (previous == null) { return current; } String id = (current.id() != null ? current.id() : previous.id()); String type = (current.type() != null ? current.type() : previous.type()); - MiniMaxApi.ChatCompletionMessage.ChatCompletionFunction function = merge(previous.function(), - current.function()); - return new MiniMaxApi.ChatCompletionMessage.ToolCall(id, type, function); + ChatCompletionFunction function = merge(previous.function(), current.function()); + return new ToolCall(id, type, function); } - private MiniMaxApi.ChatCompletionMessage.ChatCompletionFunction merge( - MiniMaxApi.ChatCompletionMessage.ChatCompletionFunction previous, - MiniMaxApi.ChatCompletionMessage.ChatCompletionFunction current) { + private ChatCompletionFunction merge(ChatCompletionFunction previous, ChatCompletionFunction current) { if (previous == null) { return current; } - String name = (current.name() != null ? current.name() : previous.name()); + String name = (StringUtils.hasLength(current.name()) ? current.name() : previous.name()); StringBuilder arguments = new StringBuilder(); if (previous.arguments() != null) { arguments.append(previous.arguments()); @@ -145,14 +135,14 @@ public class MiniMaxStreamFunctionCallingHelper { if (current.arguments() != null) { arguments.append(current.arguments()); } - return new MiniMaxApi.ChatCompletionMessage.ChatCompletionFunction(name, arguments.toString()); + return new ChatCompletionFunction(name, arguments.toString()); } /** * @param chatCompletion the ChatCompletionChunk to check * @return true if the ChatCompletionChunk is a streaming tool function call. */ - public boolean isStreamingToolFunctionCall(MiniMaxApi.ChatCompletionChunk chatCompletion) { + public boolean isStreamingToolFunctionCall(ChatCompletionChunk chatCompletion) { if (chatCompletion == null || CollectionUtils.isEmpty(chatCompletion.choices())) { return false; @@ -170,7 +160,7 @@ public class MiniMaxStreamFunctionCallingHelper { * @return true if the ChatCompletionChunk is a streaming tool function call and it is * the last one. */ - public boolean isStreamingToolFunctionCallFinish(MiniMaxApi.ChatCompletionChunk chatCompletion) { + public boolean isStreamingToolFunctionCallFinish(ChatCompletionChunk chatCompletion) { if (chatCompletion == null || CollectionUtils.isEmpty(chatCompletion.choices())) { return false; @@ -180,23 +170,7 @@ public class MiniMaxStreamFunctionCallingHelper { if (choice == null || choice.delta() == null) { return false; } - return choice.finishReason() == MiniMaxApi.ChatCompletionFinishReason.TOOL_CALLS; - } - - /** - * Convert the ChatCompletionChunk into a ChatCompletion. The Usage is set to null. - * @param chunk the ChatCompletionChunk to convert - * @return the ChatCompletion - */ - public MiniMaxApi.ChatCompletion chunkToChatCompletion(MiniMaxApi.ChatCompletionChunk chunk) { - List choices = chunk.choices() - .stream() - .map(chunkChoice -> new MiniMaxApi.ChatCompletion.Choice(chunkChoice.finishReason(), chunkChoice.index(), - chunkChoice.delta(), chunkChoice.logprobs())) - .toList(); - - return new MiniMaxApi.ChatCompletion(chunk.id(), choices, chunk.created(), chunk.model(), - chunk.systemFingerprint(), "chat.completion", null, null); + return choice.finishReason() == ChatCompletionFinishReason.TOOL_CALLS; } } diff --git a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxApiToolFunctionCallIT.java b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxApiToolFunctionCallIT.java index e4aecfcf5..77abfd0ce 100644 --- a/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxApiToolFunctionCallIT.java +++ b/models/spring-ai-minimax/src/test/java/org/springframework/ai/minimax/api/MiniMaxApiToolFunctionCallIT.java @@ -54,7 +54,8 @@ public class MiniMaxApiToolFunctionCallIT { public void toolFunctionCall() { // Step 1: send the conversation and available functions to the model - var message = new ChatCompletionMessage("What's the weather like in San Francisco?", Role.USER); + var message = new ChatCompletionMessage( + "What's the weather like in San Francisco? Return the temperature in Celsius.", Role.USER); var functionTool = new MiniMaxApi.FunctionTool(Type.FUNCTION, new MiniMaxApi.FunctionTool.Function( "Get the weather in location. Return temperature in 30°F or 30°C format.", "getCurrentWeather", """ @@ -126,7 +127,8 @@ public class MiniMaxApiToolFunctionCallIT { 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", "30.0°F", "30°F"); + .containsAnyOf("30.0°C", "30°C", "30.0") + .containsAnyOf("°C", "Celsius"); } private static T fromJson(String json, Class targetClass) { diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java index d8d1f0a69..a4601d082 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java @@ -62,16 +62,8 @@ public class MiniMaxAutoConfiguration { var miniMaxApi = miniMaxApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, responseErrorHandler); - MiniMaxChatModel chatModel = new MiniMaxChatModel(miniMaxApi, chatProperties.getOptions(), - functionCallbackContext, retryTemplate); - - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - Map toolFunctionCallbackMap = toolFunctionCallbacks.stream() - .collect(Collectors.toMap(FunctionCallback::getName, Function.identity(), (a, b) -> b)); - chatModel.getFunctionCallbackRegister().putAll(toolFunctionCallbackMap); - } - - return chatModel; + return new MiniMaxChatModel(miniMaxApi, chatProperties.getOptions(), functionCallbackContext, + toolFunctionCallbacks, retryTemplate); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java index 335dc6aaa..6d97b82de 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java @@ -20,10 +20,10 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxChatOptions; @@ -53,11 +53,12 @@ public class FunctionCallbackInPromptIT { @Test void functionCallTest() { - contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6-chat").run(context -> { + contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"); + UserMessage userMessage = new UserMessage( + "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); var promptOptions = MiniMaxChatOptions.builder() .withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService()) @@ -78,11 +79,12 @@ public class FunctionCallbackInPromptIT { @Test void streamingFunctionCallTest() { - contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6-chat").run(context -> { + contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"); + UserMessage userMessage = new UserMessage( + "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); var promptOptions = MiniMaxChatOptions.builder() .withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService()) diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWithPlainFunctionBeanIT.java index 8af0a7960..ceb00d476 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWithPlainFunctionBeanIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWithPlainFunctionBeanIT.java @@ -20,10 +20,10 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxChatOptions; @@ -57,14 +57,16 @@ class FunctionCallbackWithPlainFunctionBeanIT { RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) .withUserConfiguration(Config.class); + // FIXME: multiple function calls may stop prematurely due to model performance @Test void functionCallTest() { - contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6-chat").run(context -> { + contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); // Test weatherFunction - UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"); + UserMessage userMessage = new UserMessage( + "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), MiniMaxChatOptions.builder().withFunction("weatherFunction").build())); @@ -86,12 +88,13 @@ class FunctionCallbackWithPlainFunctionBeanIT { @Test void functionCallWithPortableFunctionCallingOptions() { - contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6-chat").run(context -> { + contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); // Test weatherFunction - UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"); + UserMessage userMessage = new UserMessage( + "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); PortableFunctionCallingOptions functionOptions = FunctionCallingOptions.builder() .withFunction("weatherFunction") @@ -103,14 +106,16 @@ class FunctionCallbackWithPlainFunctionBeanIT { }); } + // FIXME: multiple function calls may stop prematurely due to model performance @Test void streamFunctionCallTest() { - contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6-chat").run(context -> { + contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); // Test weatherFunction - UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"); + UserMessage userMessage = new UserMessage( + "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); Flux response = chatModel.stream(new Prompt(List.of(userMessage), MiniMaxChatOptions.builder().withFunction("weatherFunction").build())); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWrapperIT.java index 98a077d78..612622d69 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWrapperIT.java @@ -20,10 +20,10 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxChatOptions; @@ -57,11 +57,12 @@ public class FunctionCallbackWrapperIT { @Test void functionCallTest() { - contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6-chat").run(context -> { + contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"); + UserMessage userMessage = new UserMessage( + "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); ChatResponse response = chatModel.call( new Prompt(List.of(userMessage), MiniMaxChatOptions.builder().withFunction("WeatherInfo").build())); @@ -75,11 +76,12 @@ public class FunctionCallbackWrapperIT { @Test void streamFunctionCallTest() { - contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6-chat").run(context -> { + contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"); + UserMessage userMessage = new UserMessage( + "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); Flux response = chatModel.stream( new Prompt(List.of(userMessage), MiniMaxChatOptions.builder().withFunction("WeatherInfo").build())); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfigurationIT.java index eff89cb0d..507ed41ff 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfigurationIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfigurationIT.java @@ -20,8 +20,8 @@ import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.embedding.EmbeddingResponse; import org.springframework.ai.minimax.MiniMaxChatModel; diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MockWeatherService.java index 29967264e..b7f792d3a 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MockWeatherService.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MockWeatherService.java @@ -34,7 +34,7 @@ public class MockWeatherService implements Function