Add ZhiPu model support for AbstractToolCallSupport

Implement ZhiPu function call to work with AbstractToolCallSupport
framework. Add unit tests for function call functionality.

Resolves #1078.
This commit is contained in:
GR
2024-08-05 10:56:22 +08:00
committed by Mark Pollack
parent 197fe8105c
commit 935e1a38ed
11 changed files with 267 additions and 223 deletions

View File

@@ -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<ChatCompletionMessage, ZhiPuAiApi.ChatCompletionRequest, ResponseEntity<ChatCompletion>>
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<FunctionCallback> 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<ChatCompletion> completionEntity = this.retryTemplate
.execute(ctx -> this.zhiPuAiApi.chatCompletionEntity(request));
ResponseEntity<ChatCompletion> 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<Generation> 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<Choice> choices = chatCompletion.choices();
return new ChatResponse(generations);
});
List<Generation> generations = choices.stream().map(choice -> {
// @formatter:off
Map<String, Object> 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<String, Object> toMap(String id, ChatCompletion.Choice choice) {
Map<String, Object> 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<ChatResponse> stream(Prompt prompt) {
ChatCompletionRequest request = createRequest(prompt, true);
return this.retryTemplate.execute(ctx -> {
Flux<ChatCompletionChunk> completionChunks = this.retryTemplate
.execute(ctx -> this.zhiPuAiApi.chatCompletionStream(request));
Flux<ZhiPuAiApi.ChatCompletionChunk> 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<String, String> 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<String, String> 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> 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<Generation> 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<Generation> generations = chatCompletion2.choices().stream().map(choice -> {
if (choice.message().role() != null) {
roleMap.putIfAbsent(id, choice.message().role().name());
}
Map<String, Object> 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<String, Object> metadata) {
List<AssistantMessage.ToolCall> 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<Choice> choices = chunk.choices()
.stream()
.map(cc -> new Choice(cc.finishReason(), cc.index(), cc.delta(), cc.logprobs()))
.toList();
private ChatCompletion chunkToChatCompletion(ChatCompletionChunk chunk) {
List<ChatCompletion.Choice> 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<String> functionsForThisRequest = new HashSet<>();
List<ChatCompletionMessage> 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<MediaContent> contentList = new ArrayList<>(
List.of(new MediaContent(message.getContent())));
List<ChatCompletionMessage> chatCompletionMessages = prompt.getInstructions().stream().map(m -> {
// Add text content.
List<MediaContent> 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<ToolCall> 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<String> enabledToolsToUse = new HashSet<>();
if (prompt.getOptions() != null) {
ZhiPuAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(),
ChatOptions.class, ZhiPuAiChatOptions.class);
Set<String> 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<String> 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<ZhiPuAiApi.FunctionTool> getFunctionTools(Set<String> functionNames) {
private List<FunctionTool> getFunctionTools(Set<String> 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<ChatCompletionMessage> conversationHistory) {
// Every tool-call item requires a separate function call and a response (TOOL)
// message.
for (ToolCall toolCall : responseMessage.toolCalls()) {
var functionName = toolCall.function().name();
String functionArguments = toolCall.function().arguments();
if (!this.functionCallbackRegister.containsKey(functionName)) {
throw new IllegalStateException("No function callback found for function name: " + functionName);
}
String functionResponse = this.functionCallbackRegister.get(functionName).call(functionArguments);
// Add the function response to the conversation.
conversationHistory
.add(new ChatCompletionMessage(functionResponse, 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<ChatCompletionMessage> doGetUserMessages(ChatCompletionRequest request) {
return request.messages();
}
@Override
protected ChatCompletionMessage doGetToolResponseMessage(ResponseEntity<ChatCompletion> chatCompletion) {
return chatCompletion.getBody().choices().iterator().next().message();
}
@Override
protected ResponseEntity<ChatCompletion> doChatCompletion(ChatCompletionRequest request) {
return this.zhiPuAiApi.chatCompletionEntity(request);
}
@Override
protected Flux<ResponseEntity<ChatCompletion>> doChatCompletionStream(ChatCompletionRequest request) {
return this.zhiPuAiApi.chatCompletionStream(request)
.map(this::chunkToChatCompletion)
.map(Optional::ofNullable)
.map(ResponseEntity::of);
}
@Override
protected boolean isToolFunctionCall(ResponseEntity<ChatCompletion> chatCompletion) {
var body = chatCompletion.getBody();
if (body == null) {
return false;
}
var choices = body.choices();
if (CollectionUtils.isEmpty(choices)) {
return false;
}
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);
}
}

View File

@@ -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 + '\'' +
'}';
}
}
/**

View File

@@ -35,8 +35,8 @@ public class MockWeatherService implements Function<MockWeatherService.Request,
@JsonClassDescription("Weather API request")
public record Request(@JsonProperty(required = true,
value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
@JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat,
@JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon,
@JsonProperty(value = "lat") @JsonPropertyDescription("The city latitude") double lat,
@JsonProperty(value = "lon") @JsonPropertyDescription("The city longitude") double lon,
@JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
}

View File

@@ -17,8 +17,13 @@ package org.springframework.ai.zhipuai.api;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi.*;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletion;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionChunk;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionMessage;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionMessage.Role;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi.ChatCompletionRequest;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi.Embedding;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi.EmbeddingList;
import org.springframework.http.ResponseEntity;
import reactor.core.publisher.Flux;

View File

@@ -56,8 +56,8 @@ public class ZhiPuAiApiToolFunctionCallIT {
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, Tokyo, and Paris?",
Role.USER);
var message = new ChatCompletionMessage(
"What's the weather like in San Francisco? Return the temperature in Celsius.", Role.USER);
var functionTool = new ZhiPuAiApi.FunctionTool(Type.FUNCTION,
new ZhiPuAiApi.FunctionTool.Function(
@@ -119,7 +119,8 @@ public class ZhiPuAiApiToolFunctionCallIT {
}
}
var functionResponseRequest = new ChatCompletionRequest(messages, GLM_4.value, 0.8f);
var functionResponseRequest = new ChatCompletionRequest(messages, GLM_4.value, List.of(functionTool),
ToolChoiceBuilder.AUTO);
ResponseEntity<ChatCompletion> 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> T fromJson(String json, Class<T> targetClass) {

View File

@@ -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<Message> 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<Message> 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")

View File

@@ -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<String, FunctionCallback> 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

View File

@@ -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);
});
}

View File

@@ -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())

View File

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

View File

@@ -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<ChatResponse> response = chatModel.stream(
new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().withFunction("WeatherInfo").build()));