Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: MiniMax model function call #1116

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +26,11 @@
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ public Flux<ChatCompletionChunk> chatCompletionStream(ChatCompletionRequest chat
.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);
}
Expand All @@ -726,7 +726,7 @@ public Flux<ChatCompletionChunk> chatCompletionStream(ChatCompletionRequest chat
.concatMapIterable(window -> {
Mono<ChatCompletionChunk> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,14 +38,7 @@
*/
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;
Expand All @@ -49,47 +51,38 @@ public MiniMaxApi.ChatCompletionChunk merge(MiniMaxApi.ChatCompletionChunk previ
: 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<MiniMaxApi.ChatCompletionChunk.ChunkChoice> chunkChoices = choice == null ? List.of() : List.of(choice);
return new MiniMaxApi.ChatCompletionChunk(id, chunkChoices, created, model, systemFingerprint, object);
ChunkChoice choice = merge(previousChoice0, currentChoice0);
List<ChunkChoice> 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<MiniMaxApi.ChatCompletionMessage.ToolCall> toolCalls = new ArrayList<>();
MiniMaxApi.ChatCompletionMessage.ToolCall lastPreviousTooCall = null;
List<ToolCall> toolCalls = new ArrayList<>();
ToolCall lastPreviousTooCall = null;
if (previous.toolCalls() != null) {
lastPreviousTooCall = previous.toolCalls().get(previous.toolCalls().size() - 1);
if (previous.toolCalls().size() > 1) {
Expand All @@ -101,58 +94,55 @@ private MiniMaxApi.ChatCompletionMessage merge(MiniMaxApi.ChatCompletionMessage
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());
}
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;
Expand All @@ -170,7 +160,7 @@ public boolean isStreamingToolFunctionCall(MiniMaxApi.ChatCompletionChunk chatCo
* @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;
Expand All @@ -180,23 +170,7 @@ public boolean isStreamingToolFunctionCallFinish(MiniMaxApi.ChatCompletionChunk
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<MiniMaxApi.ChatCompletion.Choice> 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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", """
Expand Down Expand Up @@ -126,7 +127,7 @@ public void toolFunctionCall() {

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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to update to use

				.containsAnyOf("30.0°C", "30°C", "30.0")
				.containsAnyOf("°C", "Celsius");

as the answer said "30 Celcius"

also, it seems sometimes i get an answer back in chinese. Is there a way for it to be force to reply in english?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as far as i know, there's can't to set the talk language through options, but we can add an explicit request in the message, like please answer in english

}

private static <T> T fromJson(String json, Class<T> targetClass) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,8 @@ public MiniMaxChatModel miniMaxChatModel(MiniMaxConnectionProperties commonPrope
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<String, FunctionCallback> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
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;
Expand Down Expand Up @@ -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())
Expand All @@ -78,11 +79,12 @@ void functionCallTest() {
@Test
void streamingFunctionCallTest() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fails with

java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1]

	at org.springframework.ai.model.function.AbstractFunctionCallback.fromJson(AbstractFunctionCallback.java:115)
	at org.springframework.ai.model.function.AbstractFunctionCallback.call(AbstractFunctionCallback.java:104)
	at org.springframework.ai.model.function.FunctionCallbackWrapper.call(FunctionCallbackWrapper.java:35)
	at org.springframework.ai.chat.model.AbstractToolCallSupport.executeFunctions(AbstractToolCallSupport.java:200)
	at org.springframework.ai.chat.model.AbstractToolCallSupport.handleToolCalls(AbstractToolCallSupport.java:139)
	at org.springframework.ai.minimax.MiniMaxChatModel.lambda$stream$6(MiniMaxChatModel.java:247)
	at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:388)
	at reactor.core.publisher.FluxSwitchMapNoPrefetch$SwitchMapInner.onNext(FluxSwitchMapNoPrefetch.java:408)
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129)
	at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2571)
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:171)
	at reactor.core.publisher.FluxSwitchMapNoPrefetch$SwitchMapInner.onSubscribe(FluxSwitchMapNoPrefetch.java:378)
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:96)
	at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:55)
	at reactor.core.publisher.Mono.subscribe(Mono.java:4568)
	at reactor.core.publisher.FluxSwitchMapNoPrefetch$SwitchMapMain.subscribeInner(FluxSwitchMapNoPrefetch.java:219)
	at reactor.core.publisher.FluxSwitchMapNoPrefetch$SwitchMapMain.onNext(FluxSwitchMapNoPrefetch.java:193)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmit(FluxFlatMap.java:547)
	at reactor.core.publisher.FluxFlatMap$FlatMapInner.onNext(FluxFlatMap.java:988)
	at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2097)
	at reactor.core.publisher.MonoReduceSeed$ReduceSeedSubscriber.onComplete(MonoReduceSeed.java:163)
	at reactor.core.publisher.FluxWindowPredicate$WindowFlux.checkTerminated(FluxWindowPredicate.java:768)
	at reactor.core.publisher.FluxWindowPredicate$WindowFlux.drainRegular(FluxWindowPredicate.java:662)
	at reactor.core.publisher.FluxWindowPredicate$WindowFlux.drain(FluxWindowPredicate.java:748)
	at reactor.core.publisher.FluxWindowPredicate$WindowFlux.onComplete(FluxWindowPredicate.java:814)
	at reactor.core.publisher.FluxWindowPredicate$WindowPredicateMain.onNext(FluxWindowPredicate.java:243)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	at reactor.core.publisher.FluxFilter$FilterSubscriber.onNext(FluxFilter.java:113)
	at reactor.core.publisher.FluxTakeUntil$TakeUntilPredicateSubscriber.onNext(FluxTakeUntil.java:95)
	at reactor.core.publisher.MonoFlatMapMany$FlatMapManyInner.onNext(MonoFlatMapMany.java:251)
	at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)
	at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:539)
	at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.innerNext(FluxConcatMapNoPrefetch.java:259)
	at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:865)
	at reactor.core.publisher.FluxConcatMap$WeakScalarSubscription.request(FluxConcatMap.java:480)
	at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2367)
	at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.onNext(FluxConcatMapNoPrefetch.java:202)
	at reactor.core.publisher.FluxBufferPredicate$BufferPredicateSubscriber.onNextNewBuffer(FluxBufferPredicate.java:317)
	at reactor.core.publisher.FluxBufferPredicate$BufferPredicateSubscriber.tryOnNext(FluxBufferPredicate.java:227)
	at reactor.core.publisher.FluxBufferPredicate$BufferPredicateSubscriber.onNext(FluxBufferPredicate.java:200)
	at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onNext(FluxPeekFuseable.java:503)
	at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299)
	at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)
	at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onNext(FluxDoFinally.java:113)
	at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:180)
	at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:453)
	at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724)
	at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onNext(FluxFlattenIterable.java:256)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:200)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	at reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:294)
	at reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:403)
	at reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:426)


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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
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;
Expand Down Expand Up @@ -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()));
Expand All @@ -86,12 +88,13 @@ void functionCallTest() {

@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")
Expand All @@ -103,14 +106,16 @@ void functionCallWithPortableFunctionCallingOptions() {
});
}

// FIXME: multiple function calls may stop prematurely due to model performance
@Test
void streamFunctionCallTest() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fails with

java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1]

	at org.springframework.ai.model.function.AbstractFunctionCallback.fromJson(AbstractFunctionCallback.java:115)
	at org.springframework.ai.model.function.AbstractFunctionCallback.call(AbstractFunctionCallback.java:104)
	at org.springframework.ai.model.function.FunctionCallbackWrapper.call(FunctionCallbackWrapper.java:35)
	at org.springframework.ai.chat.model.AbstractToolCallSupport.executeFunctions(AbstractToolCallSupport.java:200)
	at org.springframework.ai.chat.model.AbstractToolCallSupport.handleToolCalls(AbstractToolCallSupport.java:139)
	at org.springframework.ai.minimax.MiniMaxChatModel.lambda$stream$6(MiniMaxChatModel.java:247)
	at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:388)
	at reactor.core.publisher.FluxSwitchMapNoPrefetch$SwitchMapInner.onNext(FluxSwitchMapNoPrefetch.java:408)
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129)
	at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2571)
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:171)
	at reactor.core.publisher.FluxSwitchMapNoPrefetch$SwitchMapInner.onSubscribe(FluxSwitchMapNoPrefetch.java:378)
	at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:96)
	at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:55)
	at reactor.core.publisher.Mono.subscribe(Mono.java:4568)
	at reactor.core.publisher.FluxSwitchMapNoPrefetch$SwitchMapMain.subscribeInner(FluxSwitchMapNoPrefetch.java:219)
	at reactor.core.publisher.FluxSwitchMapNoPrefetch$SwitchMapMain.onNext(FluxSwitchMapNoPrefetch.java:193)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmit(FluxFlatMap.java:547)
	at reactor.core.publisher.FluxFlatMap$FlatMapInner.onNext(FluxFlatMap.java:988)
	at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2097)
	at reactor.core.publisher.MonoReduceSeed$ReduceSeedSubscriber.onComplete(MonoReduceSeed.java:163)
	at reactor.core.publisher.FluxWindowPredicate$WindowFlux.checkTerminated(FluxWindowPredicate.java:768)
	at reactor.core.publisher.FluxWindowPredicate$WindowFlux.drainRegular(FluxWindowPredicate.java:662)
	at reactor.core.publisher.FluxWindowPredicate$WindowFlux.drain(FluxWindowPredicate.java:748)
	at reactor.core.publisher.FluxWindowPredicate$WindowFlux.onComplete(FluxWindowPredicate.java:814)
	at reactor.core.publisher.FluxWindowPredicate$WindowPredicateMain.onNext(FluxWindowPredicate.java:243)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	at reactor.core.publisher.FluxFilter$FilterSubscriber.onNext(FluxFilter.java:113)
	at reactor.core.publisher.FluxTakeUntil$TakeUntilPredicateSubscriber.onNext(FluxTakeUntil.java:95)
	at reactor.core.publisher.MonoFlatMapMany$FlatMapManyInner.onNext(MonoFlatMapMany.java:251)
	at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)
	at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onNext(FluxOnAssembly.java:539)
	at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.innerNext(FluxConcatMapNoPrefetch.java:259)
	at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:865)
	at reactor.core.publisher.FluxConcatMap$WeakScalarSubscription.request(FluxConcatMap.java:480)
	at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2367)
	at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.onNext(FluxConcatMapNoPrefetch.java:202)
	at reactor.core.publisher.FluxBufferPredicate$BufferPredicateSubscriber.onNextNewBuffer(FluxBufferPredicate.java:317)
	at reactor.core.publisher.FluxBufferPredicate$BufferPredicateSubscriber.tryOnNext(FluxBufferPredicate.java:227)
	at reactor.core.publisher.FluxBufferPredicate$BufferPredicateSubscriber.onNext(FluxBufferPredicate.java:200)
	at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onNext(FluxPeekFuseable.java:503)
	at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299)
	at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)
	at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onNext(FluxDoFinally.java:113)
	at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onNext(FluxConcatArray.java:180)
	at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:453)
	at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724)
	at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onNext(FluxFlattenIterable.java:256)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:200)
	at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
	at reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:294)
	at reactor.netty.channel.FluxReceive.onInboundNext(FluxReceive.java:403)
	at reactor.netty.channel.ChannelOperations.onInboundNext(ChannelOperations.java:426)

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<ChatResponse> response = chatModel.stream(new Prompt(List.of(userMessage),
MiniMaxChatOptions.builder().withFunction("weatherFunction").build()));
Expand Down
Loading