diff --git a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java index 79eec8eab..773e8106f 100644 --- a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java +++ b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java @@ -17,8 +17,13 @@ package org.springframework.ai.zhipuai; 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.messages.UserMessage; import org.springframework.ai.chat.metadata.ChatGenerationMetadata; +import org.springframework.ai.chat.metadata.ChatResponseMetadata; +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; @@ -26,32 +31,35 @@ import org.springframework.ai.chat.model.StreamingChatModel; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.ai.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.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletion; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletion.Choice; +import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionChunk; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionFinishReason; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionMessage; +import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionMessage.ChatCompletionFunction; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionMessage.MediaContent; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionMessage.Role; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionMessage.ToolCall; import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionRequest; +import org.springframework.ai.zhipuai.api.ZhiPuAiApi.FunctionTool; +import org.springframework.ai.zhipuai.metadata.ZhiPuAiUsage; import org.springframework.http.ResponseEntity; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.Base64; -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; @@ -65,9 +73,7 @@ import java.util.concurrent.ConcurrentHashMap; * @see ZhiPuAiApi * @since 1.0.0 M1 */ -public class ZhiPuAiChatModel extends - AbstractFunctionCallSupport> - implements ChatModel, StreamingChatModel { +public class ZhiPuAiChatModel extends AbstractToolCallSupport implements ChatModel, StreamingChatModel { private static final Logger logger = LoggerFactory.getLogger(ZhiPuAiChatModel.class); @@ -108,7 +114,7 @@ public class ZhiPuAiChatModel extends } /** - * Initializes a new instance of the ZhiPuAiChatModel. + * Initializes an instance of the ZhiPuAiChatModel. * @param zhiPuAiApi The ZhiPuAiApi instance to be used for interacting with the * ZhiPuAI Chat API. * @param options The ZhiPuAiChatOptions to configure the chat model. @@ -117,10 +123,27 @@ public class ZhiPuAiChatModel extends */ public ZhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options, FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) { - super(functionCallbackContext); + this(zhiPuAiApi, options, functionCallbackContext, List.of(), retryTemplate); + } + + /** + * Initializes a new instance of the ZhiPuAiChatModel. + * @param zhiPuAiApi The ZhiPuAiApi instance to be used for interacting with the + * ZhiPuAI Chat API. + * @param options The ZhiPuAiChatOptions to configure the chat model. + * @param functionCallbackContext The function callback context. + * @param toolFunctionCallbacks The tool function callbacks. + * @param retryTemplate The retry template. + */ + public ZhiPuAiChatModel(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options, + FunctionCallbackContext functionCallbackContext, List toolFunctionCallbacks, + RetryTemplate retryTemplate) { + super(functionCallbackContext, options, toolFunctionCallbacks); Assert.notNull(zhiPuAiApi, "ZhiPuAiApi 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.zhiPuAiApi = zhiPuAiApi; this.defaultOptions = options; this.retryTemplate = retryTemplate; @@ -128,103 +151,150 @@ public class ZhiPuAiChatModel extends @Override public ChatResponse call(Prompt prompt) { - ChatCompletionRequest request = createRequest(prompt, false); - return this.retryTemplate.execute(ctx -> { + ResponseEntity completionEntity = this.retryTemplate + .execute(ctx -> this.zhiPuAiApi.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()); + } - 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 choices = chatCompletion.choices(); - return new ChatResponse(generations); - }); + 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(); + + 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 ZhiPuAiChatOptions.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.zhiPuAiApi.chatCompletionStream(request)); - Flux completionChunks = this.zhiPuAiApi.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(chunk -> chunkToChatCompletion(chunk)).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) { + Assert.notNull(result, "ZhiPuAI ChatCompletionResult must not be null"); + return ChatResponseMetadata.builder() + .withId(result.id()) + .withUsage(ZhiPuAiUsage.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 * @return the ChatCompletion */ - private ZhiPuAiApi.ChatCompletion chunkToChatCompletion(ZhiPuAiApi.ChatCompletionChunk chunk) { - List choices = chunk.choices() - .stream() - .map(cc -> new Choice(cc.finishReason(), cc.index(), cc.delta(), cc.logprobs())) - .toList(); + private ChatCompletion chunkToChatCompletion(ChatCompletionChunk chunk) { + List choices = chunk.choices().stream().map(cc -> { + ChatCompletionMessage delta = cc.delta(); + if (delta == null) { + delta = new ChatCompletionMessage("", Role.ASSISTANT); + } + return new ChatCompletion.Choice(cc.finishReason(), cc.index(), delta, cc.logprobs()); + }).toList(); - return new ZhiPuAiApi.ChatCompletion(chunk.id(), choices, chunk.created(), chunk.model(), - chunk.systemFingerprint(), "chat.completion", null); + return new ChatCompletion(chunk.id(), choices, chunk.created(), chunk.model(), chunk.systemFingerprint(), + "chat.completion", null); } /** @@ -232,54 +302,82 @@ public class ZhiPuAiChatModel 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(); + if (message instanceof UserMessage userMessage) { + if (!CollectionUtils.isEmpty(userMessage.getMedia())) { + List contentList = new ArrayList<>( + List.of(new MediaContent(message.getContent()))); - List chatCompletionMessages = prompt.getInstructions().stream().map(m -> { - // Add text content. - List contents = new ArrayList<>(List.of(new MediaContent(m.getContent()))); - if (m instanceof UserMessage userMessage) { - if (!CollectionUtils.isEmpty(userMessage.getMedia())) { - // Add media content. - contents.addAll(userMessage.getMedia() - .stream() - .map(media -> new MediaContent( - new MediaContent.ImageUrl(this.fromMediaData(media.getMimeType(), media.getData())))) - .toList()); + contentList.addAll(userMessage.getMedia() + .stream() + .map(media -> new MediaContent(new MediaContent.ImageUrl( + this.fromMediaData(media.getMimeType(), media.getData())))) + .toList()); + + content = contentList; + } } - } - return new ChatCompletionMessage(contents, ChatCompletionMessage.Role.valueOf(m.getMessageType().name())); - }).toList(); + 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; + + 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) { ZhiPuAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class, ZhiPuAiChatOptions.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( - ZhiPuAiChatOptions.builder().withTools(this.getFunctionTools(functionsForThisRequest)).build(), - request, ChatCompletionRequest.class); + ZhiPuAiChatOptions.builder().withTools(this.getFunctionTools(enabledToolsToUse)).build(), request, + ChatCompletionRequest.class); } return request; @@ -289,7 +387,7 @@ public class ZhiPuAiChatModel extends if (mediaContentData instanceof byte[] bytes) { // Assume the bytes are an image. So, convert the bytes to a base64 encoded // following the prefix pattern. - return Base64.getEncoder().encodeToString(bytes); + return String.format("data:%s;base64,%s", mimeType.toString(), Base64.getEncoder().encodeToString(bytes)); } else if (mediaContentData instanceof String text) { // Assume the text is a URLs or a base64 encoded image prefixed by the user. @@ -301,87 +399,12 @@ public class ZhiPuAiChatModel extends } } - private List getFunctionTools(Set functionNames) { + private List getFunctionTools(Set functionNames) { return this.resolveFunctionCallbacks(functionNames).stream().map(functionCallback -> { - var function = new ZhiPuAiApi.FunctionTool.Function(functionCallback.getDescription(), - functionCallback.getName(), functionCallback.getInputTypeSchema()); - return new ZhiPuAiApi.FunctionTool(function); + var function = new FunctionTool.Function(functionCallback.getDescription(), functionCallback.getName(), + functionCallback.getInputTypeSchema()); + return new FunctionTool(function); }).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.zhiPuAiApi.chatCompletionEntity(request); - } - - @Override - protected Flux> doChatCompletionStream(ChatCompletionRequest request) { - return this.zhiPuAiApi.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); - return !CollectionUtils.isEmpty(choice.message().toolCalls()) - && choice.finishReason() == ChatCompletionFinishReason.TOOL_CALLS; - } - - @Override - public ChatOptions getDefaultOptions() { - return ZhiPuAiChatOptions.fromOptions(this.defaultOptions); - } - } diff --git a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/api/ZhiPuAiApi.java b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/api/ZhiPuAiApi.java index f634c8a8e..42df85da8 100644 --- a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/api/ZhiPuAiApi.java +++ b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/api/ZhiPuAiApi.java @@ -33,8 +33,10 @@ import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; @@ -684,7 +686,7 @@ public class ZhiPuAiApi { .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); } @@ -750,6 +752,25 @@ public class ZhiPuAiApi { public Embedding(Integer index, float[] embedding) { this(index, embedding, "embedding"); } + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Embedding embedding1)) return false; + return Objects.equals(index, embedding1.index) && Arrays.equals(embedding, embedding1.embedding) && Objects.equals(object, embedding1.object); + } + @Override + public int hashCode() { + int result = Objects.hash(index, object); + result = 31 * result + Arrays.hashCode(embedding); + return result; + } + + @Override public String toString() { + return "Embedding{" + + "index=" + index + + ", embedding=" + Arrays.toString(embedding) + + ", object='" + object + '\'' + + '}'; + } } /** diff --git a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/MockWeatherService.java b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/MockWeatherService.java index 937665d3d..0d68d135f 100644 --- a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/MockWeatherService.java +++ b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/api/MockWeatherService.java @@ -35,8 +35,8 @@ public class MockWeatherService implements Function chatCompletion2 = zhiPuAiApi.chatCompletionEntity(functionResponseRequest); @@ -130,10 +131,6 @@ public class ZhiPuAiApiToolFunctionCallIT { 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"); - assertThat(chatCompletion2.getBody().choices().get(0).message().content()).contains("Tokyo") - .containsAnyOf("10.0°C", "10°C", "10.0°F", "10°F"); - assertThat(chatCompletion2.getBody().choices().get(0).message().content()).contains("Paris") - .containsAnyOf("15.0°C", "15°C", "15.0°F", "15°F"); } private static T fromJson(String json, Class targetClass) { diff --git a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelIT.java b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelIT.java index cab8298e7..6e967afab 100644 --- a/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelIT.java +++ b/models/spring-ai-zhipuai/src/test/java/org/springframework/ai/zhipuai/chat/ZhiPuAiChatModelIT.java @@ -22,7 +22,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.model.Media; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatModel; @@ -35,6 +34,7 @@ import org.springframework.ai.chat.prompt.SystemPromptTemplate; import org.springframework.ai.converter.BeanOutputConverter; import org.springframework.ai.converter.ListOutputConverter; import org.springframework.ai.converter.MapOutputConverter; +import org.springframework.ai.model.Media; import org.springframework.ai.model.function.FunctionCallbackWrapper; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiTestConfiguration; @@ -224,7 +224,8 @@ class ZhiPuAiChatModelIT { @Test void functionCallTest() { - 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."); List messages = new ArrayList<>(List.of(userMessage)); @@ -249,11 +250,13 @@ class ZhiPuAiChatModelIT { @Test void streamFunctionCallTest() { - 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."); List messages = new ArrayList<>(List.of(userMessage)); var promptOptions = ZhiPuAiChatOptions.builder() + .withModel(ZhiPuAiApi.ChatModel.GLM_4.getValue()) .withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService()) .withName("getCurrentWeather") .withDescription("Get the weather in location") diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfiguration.java index b49b70a14..036a1b806 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfiguration.java @@ -33,15 +33,11 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; /** * @author Geng Rong @@ -64,16 +60,8 @@ public class ZhiPuAiAutoConfiguration { var zhiPuAiApi = zhiPuAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, responseErrorHandler); - ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(zhiPuAiApi, 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 ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), functionCallbackContext, + toolFunctionCallbacks, retryTemplate); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfigurationIT.java index e6227a306..b9ec4e45f 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfigurationIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfigurationIT.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.image.ImagePrompt; @@ -89,7 +89,7 @@ public class ZhiPuAiAutoConfigurationIT { assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - assertThat(embeddingModel.dimensions()).isEqualTo(1536); + assertThat(embeddingModel.dimensions()).isEqualTo(1024); }); } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java index be37c6862..8dc63f205 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java @@ -21,10 +21,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; -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.model.function.FunctionCallbackWrapper; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; @@ -58,7 +58,8 @@ public class FunctionCallbackInPromptIT { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.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 = ZhiPuAiChatOptions.builder() .withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService()) @@ -83,7 +84,8 @@ public class FunctionCallbackInPromptIT { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.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 = ZhiPuAiChatOptions.builder() .withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService()) diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWithPlainFunctionBeanIT.java index d454c9086..7b440c381 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWithPlainFunctionBeanIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWithPlainFunctionBeanIT.java @@ -21,10 +21,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; -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.model.function.FunctionCallingOptions; import org.springframework.ai.model.function.FunctionCallingOptionsBuilder.PortableFunctionCallingOptions; @@ -65,7 +65,8 @@ class FunctionCallbackWithPlainFunctionBeanIT { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.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), ZhiPuAiChatOptions.builder().withFunction("weatherFunction").build())); @@ -92,7 +93,8 @@ class FunctionCallbackWithPlainFunctionBeanIT { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.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") @@ -111,7 +113,8 @@ class FunctionCallbackWithPlainFunctionBeanIT { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.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), ZhiPuAiChatOptions.builder().withFunction("weatherFunction").build())); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWrapperIT.java index 1ffe871a2..2596f3b84 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWrapperIT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWrapperIT.java @@ -21,10 +21,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; -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.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackWrapper; @@ -62,7 +62,8 @@ public class FunctionCallbackWrapperIT { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.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), ZhiPuAiChatOptions.builder().withFunction("WeatherInfo").build())); @@ -80,7 +81,8 @@ public class FunctionCallbackWrapperIT { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.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), ZhiPuAiChatOptions.builder().withFunction("WeatherInfo").build()));